Repository navigation
fix: multi-instance shared-state hardening — activePack round-trip + affinity clobber guard (#1724) - #1737
Conversation
📦 Built Package ArtifactBranch: Option A — Install this exact build (recommended)npm install -g billion-context@0.0.1688-pr.1737.c1b2602This exact version is immutable and reproducible. The rolling Option B — Download artifact
tar xzf billion-context-pr1737.tgz
npm install -g packageInstall instructions are refreshed on each push. |
[bot] 🏷 Review complete for PR #1737 (head ① Direction vs #1724 — correct. Both fixes target real root causes of the multi-instance shared-state race, not symptoms: Fix 1 removes the mechanism that produces the ② Regression inventory (existing behaviors audited):
③ Code review — found two gaps, fixed directly on the branch (commit
④ Test validity — real teeth. Verified pre/post on separate worktrees: on ⑤ Diff cleanliness: 4 files, small, every line on-topic. My commit stays within the same 2 source files + their test file. Clean. Stale base: branch is 4 commits behind master (subagent-sidecar work, #1702/#1722) but those commits touch only Open question: in #1724 floor 8 the agreed batch 1 had three items — ① activePack read-back, ② affinity clobber guard, ③ fork-clobber visibility extending #405's freshness guard to session files (making silent message-loss visible). This PR delivers ①② only, and ③ isn't mentioned as deferred anywhere in the body. Where did ③ go — dropped, folded into Track 3, or a follow-up? If it's still owed, say so explicitly so it doesn't evaporate. CI at Verdict: mergeable once the 4 matrix jobs go green and the ③ question gets an answer (it doesn't block the code — both shipped fixes stand independently). 中文摘要:审查了多实例共享状态加固 PR——两个修复都打到了根因(activePack 读取端丢失、affinity 文件最后写者覆盖),我直接在分支上补了两个加固(磁盘条目完整校验、写入时按 store 自身上限做 TTL/LRU 收敛,修复了 union 导致的文件无限增长),测试前后对照证明有效,矩阵任务跑完且 ③(session 文件 fork 可见性)着落说明后即可合并。 |
The 7-day TTL contradicted the month/year-level single-session promise: a chain idle over a week lost its recognition, forking the resumed conversation into a fresh session on the next request. Chains are now permanent — they leave the table only under LRU capacity pressure (raised 256 -> 1024, ~8MB worst case): - drop the TTL gate in peekChain() and the sweep in resolve()/ findResumeParent(); lastSeen stays as the LRU/freshness tie-break - importSnapshot() no longer drops aged entries (400-day-old chains reattach); malformed-entry defenses unchanged - note for #1737: its normalizeForWrite TTL/256 re-application should be dropped on rebase; the union + deeper-depth-wins merge survives
…ianglei#1724) Record in TECHNICAL-NOTES (en/zh-CN) that all instances share one state dir whose control plane is lane-aware but whose data plane is not partitioned — no per-session owner, no per-lane isolation. State the two consequences (cross-instance session visibility in every instance's web UI; the restart drain race and its ranxianglei#1737/ranxianglei#1742/ranxianglei#405 mitigations) and the current security posture: only the admin loopback gate + filesystem permissions, i.e. no per-session authorization. Sufficient single-user, insufficient multi-user (must partition). Issue direction ranxianglei#5. Docs-only.
Summary
Part of #1724 (multi-instance shared
~/.local/state/billion-context/restart race). This PR lands batch 1 of the agreed plan — the two safe, independently-valuable persistence fixes. The drain barrier (Track 1) and web-UI scoping (Track 2) follow as separate PRs; state partitioning (Track 3) stays deferred to owner design.Fix 1 —
meta.activePackround-trip (src/persist.ts)The observed symptom — a dsh-web session's
/acppanel flippingpack: lean→pack: defaultafter an auto-update restart — has a root cause independent of the race:buildRecord()persists the whole meta via spread (meta: { ...session.meta }, soactivePackreaches disk), butbuildSession()reconstructed meta as only{protocol, upstreamOrigin, label, title}and droppedactivePack. Every reload therefore reset it toundefined, and the panel's?? "default"fell through. This fires on every restart, including clean single-instance ones.activePack?: stringto the persisted-meta type (it was already written to disk; the type just did not declare it).buildSession()now readsactivePackback from the groupedmeta.Fix 2 — prefix-affinity cross-instance clobber guard (
src/affinity-persist.ts)prefix-affinity.jsonwas written whole-file last-writer-wins with no staleness guard (unlike the session files, which carry the #405 counter guard). Two coexisting instances — or a new process hydrating before an old process's final flush — would each overwrite the other's chains, silently dropping them (the issue's risk #2, chain fork). A pure monotonic generation watermark cannot order two processes (both hydrate the same base), so this generalizes #405's "keep fresher" policy to a store that has no global counter:sessionId; per id the deeper/fresher chain wins (depth, tie →lastSeen).Behavior changes (flagged)
activePacknow survives a proxy restart (old: reset todefault/absent on every reload → new: preserved). This is the intended fix for the reported symptom.prefix-affinity.jsonwrite is now a read-merge-write union, not a blind snapshot. The on-disk file may retain chains created by other instances; a same-depth fork emits one info log line. Minor benign side effect: a chain TTL-evicted in one process can linger on disk until the next hydrate re-filters it (affinity hints self-heal via TTL).No wire/message-shape change. No config surface added (no new field/env/flag). No acp-kernel pin change.
Tests
tests/persist-active-pack.test.ts(new): assertsmeta.activePacksurvives a persist→reload round-trip (fails before Fix 1).tests/affinity-persist.test.ts: two new cases — a sibling instance's disk-only chain survives our flush (clobber prevention), and a fresher on-disk copy is not rolled back by a stale in-memory write. Both fail without Fix 2.Pre-flight
npm run typecheck— cleannpm test— 3335 tests, 0 fail (17 skipped = e2e-gated)npm run build— successPart of #1724 (not closing — drain barrier + web scoping remain).