plugin: benign-reconnect overflow-notice fix (maxAssignedSeq gate), null-id reconnect timer clear; rsync: mkdir before early exit
This commit is contained in:
@@ -6,12 +6,14 @@ set -euo pipefail
|
|||||||
SRC="${LVMH_PI_AGENT_DIR:-$HOME/.dotfiles/pi/agent}"
|
SRC="${LVMH_PI_AGENT_DIR:-$HOME/.dotfiles/pi/agent}"
|
||||||
DEST="$(dirname "$0")/../docker/pi-agent"
|
DEST="$(dirname "$0")/../docker/pi-agent"
|
||||||
|
|
||||||
|
# DEST must exist even without dotfiles: the worker Dockerfile COPYs it
|
||||||
|
# unconditionally (a missing dir would fail the build).
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
|
||||||
if [ ! -f "$SRC/settings.json" ]; then
|
if [ ! -f "$SRC/settings.json" ]; then
|
||||||
echo "rsync-pi-agent: $SRC/settings.json not found — skipping dotfiles sync" >&2
|
echo "rsync-pi-agent: $SRC/settings.json not found — skipping dotfiles sync" >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$DEST"
|
|
||||||
rsync -a --delete \
|
rsync -a --delete \
|
||||||
--exclude 'auth.json' \
|
--exclude 'auth.json' \
|
||||||
--exclude 'models.json' \
|
--exclude 'models.json' \
|
||||||
|
|||||||
+28
-8
@@ -93,6 +93,7 @@ interface SessionSnapshot {
|
|||||||
|
|
||||||
/** Per-session seq counters survive reconnects and instance rebinds in-process. */
|
/** Per-session seq counters survive reconnects and instance rebinds in-process. */
|
||||||
const seqCounters: Map<string, number> = new Map();
|
const seqCounters: Map<string, number> = new Map();
|
||||||
|
const maxAssignedSeq: Map<string, number> = new Map();
|
||||||
const sessionStartTs: Map<string, number> = new Map();
|
const sessionStartTs: Map<string, number> = new Map();
|
||||||
|
|
||||||
const logPath: string = path.join(os.homedir(), ".pi", "lvmh-agent.log");
|
const logPath: string = path.join(os.homedir(), ".pi", "lvmh-agent.log");
|
||||||
@@ -110,6 +111,11 @@ function log(message: unknown): void {
|
|||||||
function nextSeq(sessionId: string): number {
|
function nextSeq(sessionId: string): number {
|
||||||
const n: number = (seqCounters.get(sessionId) ?? 0) + 1;
|
const n: number = (seqCounters.get(sessionId) ?? 0) + 1;
|
||||||
seqCounters.set(sessionId, n);
|
seqCounters.set(sessionId, n);
|
||||||
|
// High-water mark of seqs THIS process produced — distinguishes a benign
|
||||||
|
// reconnect (daemon lastSeq <= maxAssigned: frames were delivered by us)
|
||||||
|
// from a foreign high-water after a plugin restart (real gaps).
|
||||||
|
const seen = maxAssignedSeq.get(sessionId) ?? 0;
|
||||||
|
if (n > seen) maxAssignedSeq.set(sessionId, n);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,19 +493,27 @@ export default function (pi: ExtensionAPI): void {
|
|||||||
const counter: number = seqCounters.get(currentSessionId) ?? 0;
|
const counter: number = seqCounters.get(currentSessionId) ?? 0;
|
||||||
if (counter <= lastSeq) seqCounters.set(currentSessionId, lastSeq);
|
if (counter <= lastSeq) seqCounters.set(currentSessionId, lastSeq);
|
||||||
}
|
}
|
||||||
// Events already covered by the daemon's lastSeq are silently absent
|
// Gap accounting: only a daemon lastSeq ABOVE anything this process ever
|
||||||
// from both replay and queue below — count them so the buffer_overflow
|
// assigned indicates real gaps (plugin restart against a daemon that has
|
||||||
// notice reflects every gap, not just cap drops.
|
// history). lastSeq <= maxAssigned means the daemon acked frames we sent
|
||||||
const covered: number =
|
// ourselves — benign reconnect, no notice. replayBuf alone holds every
|
||||||
replayBuf.filter((f) => f.seq <= lastSeq).length +
|
// persisted-kind frame (sendQueue copies are a subset), so counting it
|
||||||
sendQueue.filter((f) => !TRANSIENT_TYPES.has(f.type) && f.seq <= lastSeq)
|
// once cannot double-count.
|
||||||
.length;
|
const maxAssigned: number =
|
||||||
|
currentSessionId !== null
|
||||||
|
? (maxAssignedSeq.get(currentSessionId) ?? 0)
|
||||||
|
: 0;
|
||||||
|
if (lastSeq > maxAssigned) {
|
||||||
|
const covered: number = replayBuf.filter(
|
||||||
|
(f) => f.seq <= lastSeq,
|
||||||
|
).length;
|
||||||
if (covered > 0) {
|
if (covered > 0) {
|
||||||
droppedEvents += covered;
|
droppedEvents += covered;
|
||||||
log(
|
log(
|
||||||
`welcome lastSeq=${lastSeq} covers ${covered} in-flight events; flagging`,
|
`welcome lastSeq=${lastSeq} > maxAssigned=${maxAssigned}: ${covered} replayed frames never acknowledged; flagging`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Replay persisted events the daemon is missing, before queued live
|
// Replay persisted events the daemon is missing, before queued live
|
||||||
// frames. Persisted-kind frames still sitting in the queue are dropped
|
// frames. Persisted-kind frames still sitting in the queue are dropped
|
||||||
// here: they are already in replayBuf, so sending both would duplicate
|
// here: they are already in replayBuf, so sending both would duplicate
|
||||||
@@ -637,6 +651,12 @@ export default function (pi: ExtensionAPI): void {
|
|||||||
// attributing this session's events to the previous session id.
|
// attributing this session's events to the previous session id.
|
||||||
currentSessionId = null;
|
currentSessionId = null;
|
||||||
handleDisconnect();
|
handleDisconnect();
|
||||||
|
try {
|
||||||
|
if (reconnectTimer !== null) clearTimeout(reconnectTimer);
|
||||||
|
} catch {
|
||||||
|
// clearable timers never throw in practice
|
||||||
|
}
|
||||||
|
reconnectTimer = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const assignedId = process.env[ENV_SESSION_ID];
|
const assignedId = process.env[ENV_SESSION_ID];
|
||||||
|
|||||||
Reference in New Issue
Block a user