fix: retry session init after transient failure instead of pinning sessionId (#411) - #412
ranxianglei wants to merge 3 commits into
Conversation
…ssionId (#411) ensureSessionInitialized assigned state.sessionId synchronously before its first await, so any later init failure left the fast-path condition set permanently: every subsequent getOrCreate for that session returned immediately on a fresh empty SessionState (persisted blocks, model context limit, nudge baselines never loaded) for the process lifetime. Two-part fix: - state.ts: on init failure clear state.sessionId = null and re-throw so the next request retries initialization (resetSessionState at retry start wipes partial mutations). The sync assignment is kept as a concurrency guard against racing resetters. - persistence.ts loadSessionState: stop conflating I/O errors with absent/corrupt files. ENOENT still resolves null silently; EISDIR/ENOTDIR still warn+null; any other read error (EACCES/EIO/EMFILE...) now re-throws so transient failures surface to the caller instead of silently taking the fresh/fork branch. Parse/validation behavior unchanged. Tests: registry regression test (seed persisted state, chmod file 0o000, assert first getOrCreate fails + sessionId null, chmod back, assert second getOrCreate loads modelContextLimit), plus two persistence unit tests (unreadable file rejects EACCES; unsearchable storage dir rejects EACCES). All root-self-skipping. Full suite 1277 pass / 0 fail.
📦 Built Plugin ArtifactBranch: Option A — Install from npm PR tag (recommended)opencode plugin opencode-acp@pr-412 --globalEach push to this PR publishes a new version under the Option B — Install from GitHubopencode plugin "github:ranxianglei/opencode-acp#2026-09-16_init-failure-retry" --globalOption C — Download artifact
tar xzf opencode-acp-pr412.tgz
cp -r package/dist ~/.cache/opencode/packages/opencode-acp@latest/node_modules/opencode-acp/dist
This comment is automatically updated on each push. |
…411) Review nit from independent PR review: if an assertion in the failed-init phase threw, the chmod 0o644 restore never ran and the file stayed unreadable until afterEach cleanup. Wrap the phase in try/finally.
…og acceptance state Review follow-ups applied directly on the PR branch: - tests/persistence.test.ts: new test covering the preserved warn+null branch for corrupted layouts (state path is a directory; EISDIR on POSIX / ENOTDIR on Windows) — previously untested. Suite now 1278 pass / 0 fail. - devlog REQ.md: acceptance checkboxes updated to reflect implemented state. - devlog WORKLOG.md: review follow-ups section (incl. write-only-FS retry tradeoff as a tracked follow-up candidate) + files-changed accuracy.
PR Review — verified independently, two direct fixes pushed to the branchDiff cleanliness: clean. Single fix commit What I verified (re-derived, not taken on trust)Bug is real and correctly layered. On master, Fix logic verified line-by-line:
Tests — red-first reproduced myself. In a master worktree with the branch's test files dropped in: all 3 new tests FAIL against unfixed code ( Duplicate screening: no other open issue/PR covers this — #412 is the sole PR referencing #411. Independent second review (dual-review requirement)A separate agent adversarially reviewed all six risk areas (partial-mutation window, concurrency, error classification, type safety, state integrity, test quality): all PASS, no blockers/majors. Findings: one minor follow-up candidate + nits. Fixed directly on the branch (commit
|
CI confirmed green — ready for human merge ✅Final tip Nothing outstanding from the review side. When you're ready, please merge it yourself (human-only operation): #412 最终提交 |
|
继续 |
Fixes #411
Problem (verified)
ensureSessionInitializedassignsstate.sessionId = sessionIdsynchronously before its first await. If initialization then fails, every subsequentgetOrCreatefor that session hits the idempotency fast path (state.sessionId === sessionId) forever — the session silently runs on a fresh emptySessionStatefor the process lifetime (persisted compression blocks, model context limit, and nudge baselines never loaded).Trigger analysis correction (from triage): on master,
getSessionParentId(lib/state/utils.ts:72–82) swallows all host-API errors → returns undefined, andloadSessionStateswallowed all errors including I/O failures → returned null. So a host-API hiccup alone cannot throw; the dominant real-world path is a transient FS read error (EACCES/EIO/EMFILE) being conflated with "file absent": init silently takes the fresh/fork branch, pinssessionId, and the session never retries. Secondary throw paths:saveSessionStatequeue rejection and syncrebuildCompressionStatethrows.Fix (two parts — one part alone is insufficient)
ensureSessionInitializedwraps the extractedrunSessionInitialization(...)in try/catch: on failure it clearsstate.sessionId = nulland re-throws, so the next request retries initialization. The synchronous assignment is kept deliberately (it is the concurrency guard against racing resetters);resetSessionStateat the top of each attempt wipes partial mutations before re-running.loadSessionStateno longer conflates I/O errors with absent/corrupt files:nullsilently (unchanged)null(unchanged, preserves behavior for corrupted layouts where the state path is a directory)null(unchanged)existsSyncpre-check was removed: it returns false for unsearchable directories too, which masked permission loss as "file absent".All
sessionIdconsumers are null-safe (verified: hooks.ts guard, truncate-tools fallback, saveSessionState no-op). Compress-side callers (prepareSession, decompress) propagate the error to the tool result — an explicit tool error beats compressing on stale/missing persisted state.Tests (red-first verified)
tests/registry.test.ts: regression test seeds real persisted state (viacreateSessionState+saveSessionState, bypassing the registry under test), chmods the state file 0o000, asserts the firstgetOrCreatefails AND leavessessionId === null, restores perms, asserts the secondgetOrCreateloadsmodelContextLimit. Fails against unfixed code (first run confirmed; an initial seeding-via-registry design flaw that masked the bug was caught during the red phase).tests/persistence.test.ts: unreadable file rejects with EACCES; unsearchable storage directory rejects with EACCES (the removed pre-check would have hidden this case). Both root-self-skipping, like the registry test.Follow-up (tracked in devlog WORKLOG.md)
PR #408's
runSessionInitializationwrapper has the same missing failure handling and needs the same treatment when it lands (its "failed init coalesces" test may pin the old semantics).Devlog:
devlog/2026-09-16_init-failure-retry/