Repository navigation
fix: bound sessions storage growth (#407) - #478
ranxianglei wants to merge 1 commit into
Conversation
- store identical one/full block views once: leaf blocks serialize only full (one restored on load); in-memory cache shares one object - startup GC (stat-only, before any read): retention BILI_SESSION_RETENTION_DAYS (default 90d) + byte budget BILI_SESSION_MAX_BYTES (default 1GiB), 0 disables; canonical+spill (.fb.json) handled as one unit, .tmp-* untouched - single startup walk: #286 legacy-id migration folded into loadAllMigrated (was two full loadAll walks per boot); migration log fires only when a rekey actually happens instead of every boot
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-478Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr478.tgz
npm install -g packageThis comment is automatically updated on each push. |
[bot] 🏷 Reviewed #478 ( Pre-flight — reproduced; claims accurate
Merge state
Correctness pass
Non-blocking observations
Compliance: no Ready to merge from my side (merge itself stays human-only). |
[bot] 🏷 Outcome summary (follow-up to the full review above): LGTM to merge.
Note for the tracker: this eework task is numbered #479, but no GitHub issue/PR #479 exists (the code under review is this PR, #478). Attempts to post on #479 return 404, so this review + summary are posted here on the PR itself. |
[bot] 🏷 三个根因全部实测确认,已按你的四条建议修复 → PR: #479 审核结论(动手前的验证)
修复内容(对应你的建议 1–4)
验收标准对照
预检typecheck 干净; |
… one-pass startup (#478) Session registry storage grew unbounded (~387MB / 452 files measured locally, ~60MB/day) and every boot performed two synchronous full walk+parse passes over it. Three stacked causes, all addressed: 1. Leaf blocks persisted byte-identical `one` and `full` views — 50% of all blockContents bytes were duplicates corpus-wide. Verified empirically: the kernel produces identical text for both views when a block has no active direct children, which is always true for T1 leaf output. applyRanges now stores `one: null` when the two views are byte-equal; load normalizes legacy duplicate pairs into the same form so existing files collapse on their next write. 2. No retention policy: startup GC prunes records older than BILI_SESSIONS_RETENTION_DAYS (default 90d) and oldest-first past BILI_SESSIONS_MAX_BYTES (default 1 GiB). Session state is derived — the conversation lives client-side — so pruning is safe; pruned sessions start fresh. 3. Startup ran migrateLegacyIds() (with its own loadAll) plus initSessions' loadAll(): two recursive directory walks and double JSON.parse, and the "one-time" migration log line repeated on every boot because no completion marker was ever written. New SessionStore.boot() does ONE walk: migrate -> gc -> hydrate. Migration is now marker-gated (.bili-migration-286.done) so it runs once, ever. MAX_SESSIONS keeps its memory-cap semantics; a true parse budget needs a kernel-side loadAll preselection API (follow-up issue filed). Tests: tests/fix-478-storage.test.ts — one:null round-trip, legacy duplicate normalization, 300 expired files pruned in a single readFileSync pass (probe-counted), migration log fires exactly once across boots, size-budget pruning, disabled-pass escapes. Pre-flight: typecheck clean; node --test 949/950 (single failure is environmental: launcher.test.ts "codex/claude resolve to themselves" assumes /usr/bin/codex is absent; this box has it installed and unmodified master fails identically); build ok.
|
被 #479 取代(其 body 已声明 Closes #478,且经过独立复审 + rebase 到 master,见 https://github.com/ranxianglei/billion-context/pull/479#issuecomment-5537975850)。本初稿分支不再推进。 |
…xianglei#1725) - acp-kernel 0.0.99 → 0.0.100. Brings four kernel changes: * ak#484: matchToolPattern is case-insensitive — fixes ranxianglei#1725 (opencode "read" vs Claude-style "Read" protectedTools entries silently never matched, so their history could be folded). * ak#496: ref capacity widened to 9,999,999, hybrid widths (ranxianglei#483) — long side-request sessions no longer burn out at m99999. * ak#495: language preservation prompt rule, kernel-side opt-in only (exposing a bc config knob is a follow-up decision). * ak#479: nudge cadence re-anchors when the live count drops below the shown reference (ranxianglei#478) — the kernel-side fix for the class bc#1595 patched around. - tests/protected-tools.test.ts: new regression — "Read" pattern protects opencode's lowercase "read" pair end-to-end (BLOCKED refs, fold carve-out, survival). Verified it FAILS on 0.0.99 and PASSES on 0.0.100. - tests/issue1595-nudge-reanchor.test.ts: with ak#479 the kernel retires the phantom reference inside the nudge node before bc's usage-settle hook runs, so the bc log line legitimately disappears; the invariant under test is now the end state (phantom never survives a real usage report), log asserted only when bc is the retiring side. Local: typecheck ✓ build ✓ full suite 3591/3571/1/19 pre-adaptation (the 1 failure was this test's outdated log expectation), 16/16 after.
…eady path (#1960) The steady request path (client echoes the round back) silently dropped redacted_thinking blocks because anthropicToCore had no case for them, so every later request rebuilt the latest assistant message without the block and Anthropic answered 400 "cannot be modified" forever — the half of #1960 that kept the session stuck after the compress turn. - acp-kernel pin 0.0.99 → 0.0.101 (exact), which carries kernel PR #500 (redacted_thinking sidecar round-trip). Also rides along from v0.0.100+: #483 ref capacity widened to 9,999,999, #484 case-insensitive protected tool matching, #478 nudge cadence re-anchor, #493 language-preservation behind an opt-in flag (default off), and #499 prune/recommend shared per-turn index (hundreds of ms saved on ordinary requests in large sessions — release-note material). - tests/issue1960-steady-redacted.test.ts: client-echoed [thinking(sig), redacted(data), tool_use] must survive prepare→outbound byte-identical in order. Verified failing on 0.0.99, passing on 0.0.101. Preflight: typecheck clean; targeted loop/thinking/reasoning suites 186/186 green incl. both new #1960 files; build OK.
) A long-running session compressed at the 75% band, and the first post-compression reading landed at a transitional 92106 while the fold was still settling — the real level 20s later was 44038. The weld pinned the baseline at 92106, and the only downward channel (kernel: single sustained drop ≥ one full growth interval; host mirror ranxianglei#1595: same margin over the whole interval) needed a 50000 drop while this one was 48068 — 1932 short. Growth stayed negative for 13 hours and every mid-band nudge sat at 'nudge idle' until the next 75% over-limit event. Fix: all three reference families — lastPerMessageNudgeTokens (baseline), lastNudgeShownTokens (shown) and lastShownByTier (per-tier cadence stamps) — become low-water marks: every sample, ref = min(ref, reading). The reference rides down with any settle, transitional welds and estimate-grade overshoots (ranxianglei#478 phantom levels) retire on the very next sample, and no full-interval drop event is needed. Refs never ride UP, so growth is always measured as the climb from the nearest valley — exactly what a cadence wants. The post-compression reset stays untouched: ranxianglei#194's first-sight bypass still keys on baseline === 0. Host side: the ranxianglei#1595 mirror (reanchorNudgeOnUsageDrop in session.ts, called from cache-ledger.ts) is retired — the kernel low-water marks cover its scenario at every grade, including the estimate lane it missed. Its regression suite moved to the kernel replay test. Bump acp-kernel 0.0.133 -> 0.0.134 (kernel-guard rule 2).
… one-pass startup (ranxianglei#478) Session registry storage grew unbounded (~387MB / 452 files measured locally, ~60MB/day) and every boot performed two synchronous full walk+parse passes over it. Three stacked causes, all addressed: 1. Leaf blocks persisted byte-identical `one` and `full` views — 50% of all blockContents bytes were duplicates corpus-wide. Verified empirically: the kernel produces identical text for both views when a block has no active direct children, which is always true for T1 leaf output. applyRanges now stores `one: null` when the two views are byte-equal; load normalizes legacy duplicate pairs into the same form so existing files collapse on their next write. 2. No retention policy: startup GC prunes records older than BILI_SESSIONS_RETENTION_DAYS (default 90d) and oldest-first past BILI_SESSIONS_MAX_BYTES (default 1 GiB). Session state is derived — the conversation lives client-side — so pruning is safe; pruned sessions start fresh. 3. Startup ran migrateLegacyIds() (with its own loadAll) plus initSessions' loadAll(): two recursive directory walks and double JSON.parse, and the "one-time" migration log line repeated on every boot because no completion marker was ever written. New SessionStore.boot() does ONE walk: migrate -> gc -> hydrate. Migration is now marker-gated (.bili-migration-286.done) so it runs once, ever. MAX_SESSIONS keeps its memory-cap semantics; a true parse budget needs a kernel-side loadAll preselection API (follow-up issue filed). Tests: tests/fix-478-storage.test.ts — one:null round-trip, legacy duplicate normalization, 300 expired files pruned in a single readFileSync pass (probe-counted), migration log fires exactly once across boots, size-budget pruning, disabled-pass escapes. Pre-flight: typecheck clean; node --test 949/950 (single failure is environmental: launcher.test.ts "codex/claude resolve to themselves" assumes /usr/bin/codex is absent; this box has it installed and unmodified master fails identically); build ok.
…glei#478 boot GC prunes 1970-era stamps
What changed
Fixes the three stacked root causes of unbounded sessions-dir growth:
1. Leaf-block
blockContentsstored byte-identicalone+fulltwice (~50% of file bytes)cacheBlockContent(src/session.ts): identical views now share one object in memory (consumers are read-only).buildRecord/serializeBlockContents(src/persist.ts): whenone.text === full.text && one.count === full.count(all leaf blocks — kernelcollectBlockContentwith no nested children yields the same message set), onlyfullis written;oneis omitted from the record.buildSession: restoresonefromfullon load (independent copy). Old files with both keys load unchanged — backward compatible.2. Zero GC → new startup GC pre-pass (src/session-gc.ts)
initSessions, so expired files cost zero parse time.BILI_SESSION_RETENTION_DAYS(default 90,0= off). Age ≈ mtime because writes are atomic temp+rename.BILI_SESSION_MAX_BYTES(default 1 GiB,0= off), drop oldest first.<base>.jsonand its spill sibling<base>.fb.jsonform one unit (age = newest mtime, size = sum) so a record and its spill are never separated;.tmp-*in-flight files are untouched.3. Two full synchronous walks per boot + migration log repeating every boot
migrateLegacyIds()(ownloadAll) +initSessions()(loadAll) → singleloadAllMigrated(): one walk + parse, 会话身份不稳定:credential/upstream 维度导致长会话状态孤儿化(sessions 缺失) #286 rekey applied in-memory on the same data. Collision/unlabeled semantics unchanged.rekeyed || unlabeled, and unlabeled>0 forever).MAX_SESSIONS (#407 suggestion 4): kept as the post-load newest-N cap; true stop-parsing-early would need an early-stop hook in the kernel's recursive walk (cross-repo change, acp-kernel must ship first). With GC bounding total bytes at the default 1 GiB, parse cost no longer grows without bound — corpus is bounded by construction.
Tests
tests/proxy-persist.test.ts: leaf-block dedup round-trip (identical view stored once, distinct views keep both, ~half serialized size,onerestored on load), migration rekey test migrated toloadAllMigrated, migration-log-fires-once test (log capture).tests/session-gc.test.ts(new): retention/budget/disabled/spill-unit/.tmp-*/missing-dir unit tests + env defaults; acceptance integration test — 300 expired + 5 fresh files: one startup removes the expired BEFORE any read (pool size is the read probe: MAX_SESSIONS raised above 305, only the 5 survivors land in memory), disk total stable across boots.Pre-flight
npm run typecheck✅npm test: 877/878 pass; the 1 failure (launcher.test.ts: resolveClientCommand codex/claude) fails identically on pristine master — environment-dependent (/usr/bin/codexpresent on this host), unrelated to this change.npm run build✅