diff --git a/Makefile b/Makefile index fec629b..1434373 100644 --- a/Makefile +++ b/Makefile @@ -20,14 +20,12 @@ web: cd web && bun install && bun run build worker-image: - docker build -f docker/worker.Dockerfile \ - --build-arg LVMH_PLUGIN=build-plugin/lvmh-agent.ts \ - -t lvmh-worker:latest . - @# build-plugin/ is prepared by deploy/prep-worker.sh + bash deploy/rsync-pi-agent.sh + docker build -f docker/worker.Dockerfile -t lvmh-worker:latest . verify: daemon cd daemon && go vet ./... - cd daemon && staticcheck ./... || true + cd daemon && staticcheck ./... cd daemon && go test ./... -count=1 cd web && bun run build cd plugin && bunx tsc@5.9 --noEmit -p tsconfig.json @@ -35,7 +33,7 @@ verify: daemon test: cd daemon && go test ./... -count=1 -deploy: verify worker-image +deploy: verify bash deploy/deploy.sh $(REMOTE) $(REMOTE_DIR) e2e: diff --git a/docker/bridge/index.mjs b/docker/bridge/index.mjs index 26bedca..a80e399 100644 --- a/docker/bridge/index.mjs +++ b/docker/bridge/index.mjs @@ -17,15 +17,23 @@ async function runRepoSetup() { if (!existsSync(SETUP_PATH)) return; console.error(`[lvmh-bridge] running repo setup: ${SETUP_PATH}`); const code = await new Promise((resolve) => { + // detached + negative-pid kill: take down the whole process group so + // setup grandchildren cannot outlive the timeout (node-as-PID1 never + // reaps adopted orphans). const child = spawn("bash", [SETUP_PATH], { cwd: "/workspace", + detached: true, stdio: ["ignore", "inherit", "inherit"], }); const timer = setTimeout(() => { console.error( - `[lvmh-bridge] setup timed out after ${SETUP_TIMEOUT_MS}ms, killing`, + `[lvmh-bridge] setup timed out after ${SETUP_TIMEOUT_MS}ms, killing process group`, ); - child.kill("SIGKILL"); + try { + process.kill(-child.pid, "SIGKILL"); + } catch { + child.kill("SIGKILL"); + } resolve(124); }, SETUP_TIMEOUT_MS); timer.unref?.(); diff --git a/plugin/lvmh-agent.ts b/plugin/lvmh-agent.ts index fe6d4b5..d4a374d 100644 --- a/plugin/lvmh-agent.ts +++ b/plugin/lvmh-agent.ts @@ -487,6 +487,20 @@ export default function (pi: ExtensionAPI): void { const counter: number = seqCounters.get(currentSessionId) ?? 0; if (counter <= lastSeq) seqCounters.set(currentSessionId, lastSeq); } + // Events already covered by the daemon's lastSeq are silently absent + // from both replay and queue below — count them so the buffer_overflow + // notice reflects every gap, not just cap drops. + const covered: number = + replayBuf.filter((f) => f.seq <= lastSeq).length + + sendQueue.filter( + (f) => !TRANSIENT_TYPES.has(f.type) && f.seq <= lastSeq, + ).length; + if (covered > 0) { + droppedEvents += covered; + log( + `welcome lastSeq=${lastSeq} covers ${covered} in-flight events; flagging`, + ); + } // Replay persisted events the daemon is missing, before queued live // frames. Persisted-kind frames still sitting in the queue are dropped // here: they are already in replayBuf, so sending both would duplicate @@ -620,6 +634,10 @@ export default function (pi: ExtensionAPI): void { } if (sessionId === null) { log("no session id; lvmh mirroring disabled for this session"); + // Enforce the logged contract: stop mirroring rather than + // attributing this session's events to the previous session id. + currentSessionId = null; + handleDisconnect(); return; } const assignedId = process.env[ENV_SESSION_ID];