Skip to content

fix: restart restores savedAt as lastSeen instead of fabricating boot-time activity (#404) - #487

Merged
ranxianglei merged 1 commit into
masterfrom
2026-09-03_issue-404-restore-timestamps
Sep 3, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-09-03_issue-404-restore-timestamps

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #404.

What

Every boot used to stamp every disk-restored session with lastSeen: Date.now(), so all restored sessions shared one millisecond — the panel's "最后活动" column showed the boot time for every row, and handlePluginStatus's fallback=latest broke the N-way tie by readdir order, attaching a fresh client (dsh / opencode status bar) to an unrelated old session. Boot-load truncation keyed on createdAt only, so an active-but-old session could lose its slot to a newer-created idle one.

  • buildSession (src/persist.ts): lastSeen = parsed.savedAt ?? Date.now(); restored sessions flagged in-memory restored: true (not persisted, no schema change)
  • getSession (src/session.ts): both touch paths (existing-session hit and memory-miss reload — a reload IS a real request) clear the flag and stamp fresh activity
  • initSessions: truncation key max(createdAt, savedAt)
  • handlePluginStatus (src/plugin.ts): fallback=latest filters restored !== true; when only restored sessions exist it 404s with an explicit "no session with activity since boot" instead of guessing
  • sendStats exposes restored; web sessions table marks restored rows "(恢复,开机后未活动)"

Side benefits: evictOldest and listSessions now order by true activity (restored sessions sink first), since they consume the same lastSeen.

Pre-flight

  • typecheck ✓
  • tests/issue404-restore-timestamps.test.ts 3/3 (boot truncation by freshness + savedAt restore + fallback skip + exhausted fallback + memory-miss touch)
  • full suite 977/979 — the 2 failures are the known permanent plugin-agent.test.ts env failures, reproduce on clean master
  • build ✓; E2E 4-phase (warmup / load / ACP compress / purity) ✓ 243s against local sglang

…-time activity (#404)

- buildSession: lastSeen = parsed.savedAt ?? Date.now(); restored sessions
  flagged in-memory `restored: true` until their first real request
- getSession touch (existing + memory-miss reload) clears the flag
- initSessions boot truncation keys on max(createdAt, savedAt) so an
  active-but-old session keeps its slot over a newer-created idle one
- handlePluginStatus fallback=latest only considers sessions with
  post-boot activity; exhausted fallback 404s explicitly instead of
  resolving the readdir-order tie to an unrelated old session
- panel stats expose `restored`; web sessions table marks restored rows
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-03_issue-404-restore-timestamps (7036711)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-487

Each push to this PR publishes a new version under the pr-487 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr487.tgz
npm install -g package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 PR #487 reviewed — verified independently on 2026-09-03_issue-404-restore-timestamps (7036711).

Pre-flight re-run (my environment)

  • typecheck ✓ clean
  • tests/issue404-restore-timestamps.test.ts 3/3 ✓
  • full suite 978/979 — the single failure here is launcher.test.ts "resolveClientCommand: codex/claude resolve to themselves" (/usr/bin/codex vs codex). This sandbox has codex on PATH so resolveOnPath returns the absolute path; this PR touches neither launcher.ts nor that test, so it's environmental/pre-existing. The 2 plugin-agent.test.ts env failures you reported did not reproduce here (they pass in my env) — consistent with "known permanent env failures", just a different set per machine. Nothing in either set relates to 重启伪造时间戳:buildSession 用 Date.now() 覆盖 lastSeen,面板"最后活动"全员开机时刻、fallback=latest 在 ~245 路平局里随机挂会话 #404.
  • build ✓
  • E2E 4-phase: not independently re-run (needs your local sglang upstream); taking the 243s run on trust.

Branch hygiene: merge-base is 37ecfce (post-#486 master), so the PR is exactly one commit ahead of master — no stacked/unmerged commits riding along.

Triage verdict: bug is real, fix hits the root cause

Reproduced the failure mode by reading the code paths, not just the report:

  1. Root cause confirmed: buildSession stamped lastSeen: Date.now() for every disk-restored session → N-way tie at boot time. All three symptoms trace to that one fabricated value: panel "最后活动" = boot time; fallback=latest tie broken by insertion (readdir) order attaching fresh clients to unrelated old sessions; boot truncation keyed on createdAt alone ignoring activity freshness.
  2. Right layer: the fix corrects the data (lastSeen carries the on-disk savedAt, which buildRecord writes as Date.now() at persist time ≈ last real activity) and adds a first-class restored flag for the question "used since boot?" that a corrected lastSeen can no longer answer. It does NOT paper over the fallback tie-break with a heuristic — good.
  3. Consumer audit (all Session.lastSeen readers): sendStats (display), plugin.ts status response + fallback (now filtered on restored !== true), listSessions ordering, evictOldest ordering. prefix-affinity.ts keeps its own independent lastSeen map — unaffected. No TTL/idle-drop logic keys off lastSeen, so restored sessions carrying genuinely old timestamps can't trigger spurious drops.
  4. Eviction safety: evictOldest flushes before delete and refuses to drop never-persisted sessions whose flush failed (src/session.ts:384-407); evicted restored sessions reload cleanly via the memory-miss path in getSession, which is exactly where the flag clears. Restored sessions sinking first in eviction order is correct, not data loss.
  5. No schema change: buildRecord (src/persist.ts:330) builds the payload explicitly and omits restored; legacy files without savedAt fall back to Date.now() via parsed.savedAt ?? Date.now(). Direct-hit status lookups use peekSession (no touch) — correct, a status query isn't activity. The 404 body shape {ok:false,error} matches existing handlePluginStatus errors.

Minor notes (non-blocking)

  1. Test gap: the "only restored sessions exist → 404" case is actually exercised against a fully empty session map (the _resetSessionsForTest() before res2 wipes everything), not a map containing only restored sessions. The filter branch producing an empty candidate list while restored sessions are present isn't directly asserted. Cheap to close: in test 1, call handlePluginStatus("never-seen", res, true) right after initSessions() (A/C restored, before creating ACTIVE) and assert 404 — that hits the exact branch.
  2. Behavioral trade-off, confirming intent: after a bili restart, a client's first status-bar query with fallback=latest will 404 until it issues its first model request (previously it attached to something, possibly the wrong session). Explicit 404 > wrong attachment, and clients already handle 404 for unknown conversation ids — just flagging that dsh/opencode status bars should read that 404 as "no active session yet" rather than an error state worth surfacing loudly.

LGTM — ship it. The one-line test addition above is optional polish you can fold in or skip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant