Skip to content

refactor(server): separate provider history reads from interactive resume - #4473

Open
dwyanewang wants to merge 9 commits into
getpaseo:mainfrom
dwyanewang:refactor/provider-history-read
Open

refactor(server): separate provider history reads from interactive resume#4473
dwyanewang wants to merge 9 commits into
getpaseo:mainfrom
dwyanewang:refactor/provider-history-read

Conversation

@dwyanewang

Copy link
Copy Markdown
Contributor

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:

  • adds optional AgentClient.readSessionHistory() with a narrow history context;
  • returns { events, coverage: { kind: "complete" } }, leaving an internal result seam for future coverage variants without implementing pagination;
  • removes AgentResumeSessionOptions and all purpose: "history" plumbing;
  • hydrates archived records into retained closed snapshots without registering an interactive provider session;
  • shares concurrent archived reads, upgrades deferred broadcast requests, and preserves concurrent unarchive promotion;
  • migrates Codex, ACP-based providers, Claude, OpenCode, OMP, and Pi to provider-owned history readers;
  • forwards the contract through derived/custom providers while remapping event provider IDs.

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/main and on this branch; the main failures are the behaviour this PR fixes.

Codex — codex-archived-history-read.local.e2e.test.ts

Real codex 0.153.4 against a local stub model backend, following the existing codex-app-server-agent.local.e2e.test.ts pattern, so it needs no credentials to reproduce.

Case upstream/main This PR
Native thread genuinely archived a history read must not retain its app-server: expected [ 647875 ] to deeply equal []
Native thread left active by the Paseo archive CodexAppServerRpcError: thread 01a07fed-… already has an active writer

The 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 main the read demonstrably resumed the thread and claimed its writer. This PR's read only issues thread/read, takes no writer, and the archive succeeds.

The first case answers "release temporary resources": the test lists codex app-server processes this test process spawned (they are detached, so the parent link is the handle). On main one stays alive after the read because the archived record is registered as a live interactive session and nothing ever closes it — ensureAgentLoaded has no close on the archived path and there is no idle reclamation.

OpenCode — opencode-archived-history-read.real.e2e.test.ts

Real opencode serve 1.14.46 behind a recording proxy, so every request Paseo issues is observable.

Assertion upstream/main This PR
Issues no session.abort expected 1 to be +0
Opens no global event stream

A history read followed by the close a tab teardown triggers sends exactly one session-scoped abort on main. abortOpenCodeSession is unconditional in close(), 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.e2e files. Neither runs in the default test:e2e sweep.

A neighbouring bug this surfaced

archiveAgentUnlocked syncs the native archive before it closes the runtime, so thread/archive loses to the thread's own writer lock and syncNativeArchiveState's best-effort catch swallows it. A Paseo-archived Codex agent therefore usually leaves its native thread active — which is exactly the state the second Codex case covers, and why allowArchivedHistory on main rarely engages: thread/resume succeeds 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 the resumeOptions this PR removes. Rather than drop the field's only producer and leave "history" unreachable, interactive resume now pins purpose: "interactive" and the history path reports purpose: "history" through a narrow resolveHistoryReadEnv. It deliberately does not reuse buildLaunchContext: isPaseoToolPolicyEnabled(undefined) returns true, 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 returns undefined, leaving provider behaviour unchanged.

Provider behavior

Provider Read-only history path
Codex Initialize a temporary app-server and call thread/read directly; no loaded-list, resume, unarchive, skills, collaboration modes, or interactive tool setup
ACP Initialize a temporary process and call session/load with mcpServers: []; resume-only providers return an unsupported error
Claude Read the persisted transcript without starting an SDK query; resolve CLAUDE_CONFIG_DIR from explicit config, provider profile env, daemon env, then ~/.claude
OpenCode Call session metadata/messages directly; no global event attachment or native abort; release every acquisition on success and failure
OMP Start only the minimal history RPC runtime; no model, mode, prompt, host-tool, or subagent subscription setup
Pi Parse the persisted JSONL directly without starting a runtime or installing temporary MCP/extensions

Codex 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

  • A history read leaves Paseo archivedAt and provider-native archive state unchanged.
  • The snapshot is published only after provider resources are released and all events are applied.
  • A failed read leaves no partial timeline, provider-subagent state, or closed snapshot.
  • A concurrent unarchive releases the closed snapshot and runs normal interactive resume.
  • Successful history is reused during that promotion; failed history is hydrated normally by the interactive session.
  • Closed responses use the stored agent record as authority for title, status, archive timestamp, labels, owner, attention, persistence metadata, and internal visibility.
  • Registering a live session, deleting the agent, or discarding retained state removes the closed snapshot.

Scope guard

This PR does not include:

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/server plus 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:check and npm run typecheck --workspace=@getpaseo/server all pass. Repo-wide npm run typecheck reports two pre-existing packages/app/src/plugins/* errors identical on upstream/main; they come from a stale generated .expo/types/router.d.ts and are untouched by this branch.

The full test suite was not run locally per repository guidance.

Closes #2364.

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR separates read-only provider-history hydration from interactive session resume.

  • Adds a narrow provider history-read contract and implementations for persistent providers.
  • Hydrates archived records into memory-only closed snapshots without registering live provider sessions.
  • Preserves archive state, internal visibility, concurrent read sharing, unarchive promotion, and temporary-resource cleanup.
  • The changes since the previous review fix native archive ordering by closing the live runtime before synchronizing provider archive state.

Confidence Score: 5/5

The pull request appears safe to merge; no actionable regressions or outstanding previous findings remain.

Both previous findings were resolved, and the subsequent archive-ordering change releases the live provider writer before native archive or restore synchronization without disturbing retained history snapshots.

Important Files Changed

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
Loading

Reviews (3): Last reviewed commit: "test(server): keep archived history read..." | Re-trigger Greptile

Comment thread packages/server/src/server/agent/providers/claude/agent.ts
Comment thread packages/server/src/server/agent/agent-loading.test.ts Outdated
@dwyanewang

Copy link
Copy Markdown
Contributor Author

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.

dwyanewang and others added 9 commits September 9, 2026 15:21
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>
@dwyanewang
dwyanewang force-pushed the refactor/provider-history-read branch from 3182be0 to 310d340 Compare September 9, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(server): separate provider history reads from interactive resume

1 participant