refactor(server): separate provider history reads from interactive resume - #4473
refactor(server): separate provider history reads from interactive resume#4473dwyanewang wants to merge 9 commits into
Conversation
|
| Filename | Overview |
|---|---|
| packages/server/src/server/agent/agent-loading.ts | Routes archived records through shared read-only history hydration and promotes concurrent unarchives to normal interactive resume. |
| packages/server/src/server/agent/agent-manager.ts | Owns retained closed snapshots, memory-only timeline hydration, visibility enforcement, and corrected native archive ordering. |
| packages/server/src/server/agent/agent-sdk-types.ts | Introduces the narrow optional provider history-read contract and explicit coverage result. |
| packages/server/src/server/session.ts | Serves archived snapshots through public visibility-aware timeline and agent payload paths. |
| packages/server/src/server/agent/providers/codex-app-server-agent.ts | Reads Codex history through a temporary app-server without resuming or retaining a native writer. |
| packages/server/src/server/agent/providers/opencode-agent.ts | Reads OpenCode session metadata and messages without attaching global events or issuing session aborts. |
| packages/server/src/server/agent/providers/claude/agent.ts | Reads persisted Claude transcripts while honoring the resolved history-purpose environment. |
Sequence Diagram
sequenceDiagram
participant C as Client
participant L as Agent loader
participant M as Agent manager
participant P as Provider history reader
C->>L: Open archived agent
L->>M: Read persisted history
M->>P: readSessionHistory(handle, context)
P-->>M: Events with complete coverage
M->>M: Build memory-only closed snapshot
M-->>L: Closed snapshot
L-->>C: Archived timeline
Note over M,P: No interactive session is registered
C->>M: Unarchive
M->>M: Close any live writer
M->>P: Restore native archive state
M->>M: Resume interactive session
Reviews (3): Last reviewed commit: "test(server): keep archived history read..." | Re-trigger Greptile
|
Opened #4535 to describe the workflow behind this, per the contributing guide — it frames the problem from the usage side rather than as a refactor, which is what the closed #2364 got wrong. No rush on this PR: if the framing in that discussion turns out to be the wrong problem, or the right problem in the wrong shape, I would rather rework it than have anyone spend review time here first. |
The dedicated read path was only covered by fake clients, which cannot show whether a real Codex thread stays archived or whether a real OpenCode session gets aborted. These two suites drive the real binaries. Codex (`local.e2e`, real `codex` against a stub model backend): - an archived native thread is still archived after a history read; - a thread the Paseo archive left active is not resumed, proven by archiving it afterwards — Codex rejects that while a writer is held; - the temporary app-server is gone once the read returns. `archiveAgentUnlocked` syncs the native archive before closing the runtime, so `thread/archive` loses to the thread's own writer lock and the best-effort sync swallows it. The first case archives again afterwards to reach a genuinely archived thread; the second covers the state the ordinary path actually leaves. OpenCode (`real.e2e`, real `opencode serve` behind a recording proxy): - a history read plus the close a tab teardown triggers issues no `session.abort`, which is session-scoped and would cancel whatever that session is running; - it opens no global event stream. Both suites skip when the provider binary is absent. The OpenCode one takes any OpenAI-compatible endpoint, defaulting to OpenRouter's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`resolveHistoryReadEnv` lets a plugin rewrite the environment for a history read, but Claude's transcript lookup resolved its config directory from the explicit config, profile settings, and daemon environment only. A plugin that redirected `CLAUDE_CONFIG_DIR` was stored as `launchEnv` and then ignored, so the read resolved a different directory than the session ran in and returned the wrong transcript or none at all. `createProviderEnv` already overlays `launchEnv` on top of the profile settings for the interactive path, so config-dir resolution now prefers it the same way, below only an explicit config directory. Also replaces a conditional in an agent-loading test body with a harness helper, per the repository rule that tests carry no conditionals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3182be0 to
310d340
Compare
Replaces #2388, which cannot be reopened: the branch was rebased onto current
main, and GitHub refuses to reopen a closed PR whose head was force-pushed. Same branch, same work, plus the real-provider evidence @boudra asked for.Rebased onto
1f5b6143d. #4353, #4283, #4300 and #4314 all landed in the meantime and touch the same files.Summary
Separates read-only provider history access from interactive session resume:
AgentClient.readSessionHistory()with a narrow history context;{ events, coverage: { kind: "complete" } }, leaving an internal result seam for future coverage variants without implementing pagination;AgentResumeSessionOptionsand allpurpose: "history"plumbing;Closed history hydration is memory-only. It does not restore file timeline persistence, seed or commit the injectable durable timeline store, or reintroduce history reconciliation.
Real-provider evidence
Two suites drive the real binaries. Both were run on
upstream/mainand on this branch; themainfailures are the behaviour this PR fixes.Codex —
codex-archived-history-read.local.e2e.test.tsReal
codex0.153.4 against a local stub model backend, following the existingcodex-app-server-agent.local.e2e.test.tspattern, so it needs no credentials to reproduce.upstream/maina history read must not retain its app-server: expected [ 647875 ] to deeply equal []CodexAppServerRpcError: thread 01a07fed-… already has an active writerThe second case is the direct answer to "leaves the native thread untouched". After the history read it archives the thread again; Codex rejects that while a writer is held, so on
mainthe read demonstrably resumed the thread and claimed its writer. This PR's read only issuesthread/read, takes no writer, and the archive succeeds.The first case answers "release temporary resources": the test lists
codex app-serverprocesses this test process spawned (they are detached, so the parent link is the handle). Onmainone stays alive after the read because the archived record is registered as a live interactive session and nothing ever closes it —ensureAgentLoadedhas no close on the archived path and there is no idle reclamation.OpenCode —
opencode-archived-history-read.real.e2e.test.tsReal
opencode serve1.14.46 behind a recording proxy, so every request Paseo issues is observable.upstream/mainsession.abortexpected 1 to be +0A history read followed by the close a tab teardown triggers sends exactly one session-scoped abort on
main.abortOpenCodeSessionis unconditional inclose(), and OpenCode's abort is session-scoped rather than turn-scoped, so it cancels whatever that session is running. OpenCode's native archive is only a numeric field and does not stop a session from running, so an archived session can legitimately be busy.The suite takes any OpenAI-compatible endpoint and defaults to OpenRouter's, so it needs no new credential type.
Both suites skip when the provider binary is missing, matching the other
*.local.e2e/*.real.e2efiles. Neither runs in the defaulttest:e2esweep.A neighbouring bug this surfaced
archiveAgentUnlockedsyncs the native archive before it closes the runtime, sothread/archiveloses to the thread's own writer lock andsyncNativeArchiveState's best-effortcatchswallows it. A Paseo-archived Codex agent therefore usually leaves its native thread active — which is exactly the state the second Codex case covers, and whyallowArchivedHistoryonmainrarely engages:thread/resumesucceeds instead of raising the archived error the guard keys off.Not fixed here; it belongs in its own change. The tests document the current behaviour rather than working around it.
Rebase note
#4435 added
PluginSessionOpenRequest.purpose: "interactive" | "history"to the public plugin SDK, populated from theresumeOptionsthis PR removes. Rather than drop the field's only producer and leave"history"unreachable, interactive resume now pinspurpose: "interactive"and the history path reportspurpose: "history"through a narrowresolveHistoryReadEnv. It deliberately does not reusebuildLaunchContext:isPaseoToolPolicyEnabled(undefined)returnstrue, so that path would build a Paseo tool catalog for a read — the opposite of what this PR is for. With no plugin installed it returnsundefined, leaving provider behaviour unchanged.Provider behavior
thread/readdirectly; no loaded-list, resume, unarchive, skills, collaboration modes, or interactive tool setupsession/loadwithmcpServers: []; resume-only providers return an unsupported errorCLAUDE_CONFIG_DIRfrom explicit config, provider profile env, daemon env, then~/.claudeCodex persisted child histories are loaded with a maximum concurrency of 8 and the existing 100-thread cap, deduplication, nested traversal, and per-child failure isolation. Results are applied in batch route order so completion timing cannot reorder the restored timeline.
Archive behavior
archivedAtand provider-native archive state unchanged.Scope guard
This PR does not include:
thread/turns/list, timeline cursors, or CLI tail changes;37 files, +3393/-272 — of which tests are +2305 and the two evidence suites are 569. Implementation is +1065/-196 across 16 files, limited to
packages/serverplus the two owning docs.Verification
Focused Vitest files passed on the rebased branch: archived loader, AgentManager (182), session (151), Codex/OpenCode/ACP (387), Pi/OMP/Claude/MCP/wire-compat (268), provider registry.
npm run build:server,npm run lint,npm run format:checkandnpm run typecheck --workspace=@getpaseo/serverall pass. Repo-widenpm run typecheckreports two pre-existingpackages/app/src/plugins/*errors identical onupstream/main; they come from a stale generated.expo/types/router.d.tsand are untouched by this branch.The full test suite was not run locally per repository guidance.
Closes #2364.