Skip to content

agents proxy: opencode M3 — history on re-attach - #1949

Merged
SSharma-10 merged 3 commits into
feat/agents-subcommandsfrom
juliaye/opencode-proxy-m3
Sep 17, 2026
Merged

SSharma-10 merged 3 commits into
feat/agents-subcommandsfrom
juliaye/opencode-proxy-m3

Conversation

@julia-ye

@julia-ye julia-ye commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1948 (M2 — review that first; this diff is M3 only). Third slice of the opencode facade: re-attaching shows prior turns.

What's here

  • GET /session/{id}/message served from a one-shot replay_only StreamSession pass over the session's durable event history, reconstructed into the [{info, parts}] shape captured from a real server. opencode replays history client-side — attach fetches this endpoint (?limit=100 observed live, honored) — so nothing is pushed on connect and codex's --replay flag machinery has no opencode equivalent.
  • Session list gating: the bridged session appears in GET /session when it has replayable history (or was used through this proxy), which is what opencode attach --continue and the session picker resume through a freshly started proxy. A truly fresh session still lists empty (the M0-captured fresh-attach state).
  • GET /session/{id}/diff and /todo stubs (empty arrays) — the TUI's post-turn and resume refresh lookups, previously 404s.

Found while verifying live

  • run.started's prompt text arrives on the SSE wire as data.agent, not the proto's user_input — both keys are accepted (with the proto name preferred), so history reconstructs the user side of each turn.
  • time.completed must come from the run.completed event, not the run's max event time: late stragglers (run.log after completion) inflated a 2-second turn into a "20m 1s" duration badge.
  • History ids are minted with the id time-encoding from agents proxy: opencode M2 — prompts bridge to the hosted session, answers stream back #1948's ordering fix, with monotonic T allocation across turns — two turns whose events share a millisecond would otherwise interleave in the TUI (caught by a test before it shipped).

Testing

  • Unit: two-turn reconstruction (roles, parts, parentID linkage, finish/time.completed, reasoning-as-own-part, chronological id ordering across the conversation), limit handling, fresh-session emptiness, failed-turn shape, history-gated session listing.
  • Live: a fresh proxy against a session with six prior turns + opencode attach --continue rendered all six with correct prompts, answers, order, and durations.

Not in this PR

Tool-call parts and real token/cost numbers in history and live turns (M4), permission round-trips (M5), stream reconnect (M6).

🤖 Generated with Claude Code

Comment thread internal/agentproxy/opencode/history.go Outdated
Comment on lines +18 to +19
// captured from a real server (see the capture doc). No cache: history is
// fetched per request, and the TUI asks once per attach.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this header is stale after the cache commit, it still says "No cache: history is fetched per request", but history() below is cached + single-flight and invalidated on turn end. Worth updating so the file doc matches the behavior

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in the same commit — the header now says replays are cached for the proxy's lifetime (fetched once, single-flighted, warmed at first request, invalidated on turn completion), and the history() doc comment describes the outside-the-lock design.

Comment thread internal/agentproxy/opencode/history.go Outdated
Comment on lines +64 to +69
f.histMu.Lock()
defer f.histMu.Unlock()
if f.histValid {
return f.hist, nil
}
msgs, err := f.fetchHistory(ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

history() holds histMu for the whole replay (~8s). invalidateHistory() needs that same lock, so if a turn completes while history is still loading, the event loop can wait on that lock until the replay finishes

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 83ede4b — the fetch now runs outside histMu. Single-flight is a histFetching flag + done channel (waiters honor their ctx), and a histGen counter catches an invalidation that overlapped a fetch: the overlapped result is still returned to its caller (valid point-in-time read) but not cached, so the next read replays fresh. invalidateHistory() is now only ever a fast lock. Regression test holds a replay mid-stream (via the harness's WaitForHITL gate — HangStreamAfterEvents exempts replay_only streams) and asserts invalidation returns immediately and the superseded fetch doesn't populate the cache.

julia-ye and others added 2 commits September 15, 2026 14:03
GET /session/{id}/message is served from a one-shot replay_only
StreamSession pass, reconstructing each hosted run as its user message
(run.started's prompt text — the SSE wire delivers it as data.agent, not
the proto's user_input; both are accepted) and assistant message (token
deltas accumulated; reasoning as its own part). opencode's attach
replays history client-side by fetching this endpoint (?limit=100
observed live), so re-attaching a fresh proxy shows prior turns; the
session list now includes the bridged session whenever it has
replayable history, which is what --continue resumes through.

Ids are minted from turn event times with the real time-encoding, with
monotonic T allocation across turns — two turns sharing a millisecond
would otherwise interleave (caught by test). time.completed comes from
the run.completed event, not the run's max event time — late stragglers
(run.log after completion) otherwise inflate a 2s turn into a 20m badge
(found live). Also stubs GET /session/{id}/diff and /todo (empty
arrays, the TUI's post-turn refresh lookups).

Live-verified: a fresh proxy + `opencode attach --continue` renders
the session's six prior turns with correct prompts, answers, order, and
durations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
harness-api's replay_only stream lingers ~8s after catching up (measured:
68 events, close at exactly 8.0s both runs), and an attach fetches history
twice (session-list gating + the message list), so resume rendered ~16s
late. History is now reconstructed once and cached — the mutex doubles as
single-flight — warmed in the background off the facade's first request
(the TUI's health preflight precedes the attach burst), and invalidated
when a live turn completes. Warm-path timings: 8s -> 11ms.

The 8s replay-close linger itself is a harness-api behavior worth fixing
server-side (it also slows doctl agents logs); tracked outside this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@julia-ye
julia-ye force-pushed the juliaye/opencode-proxy-m3 branch from a3605c4 to 5899192 Compare September 15, 2026 21:16
history() held histMu across the whole ~8s replay, and invalidateHistory()
needs that same lock — so a turn completing while history was still loading
stalled the event loop (and every frame behind it) until the replay
finished. The fetch now runs outside the lock: single-flight rides a
histFetching flag + done channel (waiters also honor their ctx), and a
histGen counter detects an invalidation that overlapped a fetch — the
overlapped result is returned to its caller but not cached. Regression test
holds a replay mid-stream via the harness's WaitForHITL gate and asserts
invalidateHistory returns immediately.

Also refreshes the stale file header that still said "No cache: history is
fetched per request" from before the cache commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from juliaye/opencode-proxy-m2 to feat/agents-subcommands September 17, 2026 05:54
@SSharma-10
SSharma-10 merged commit 9de8b67 into feat/agents-subcommands Sep 17, 2026
2 checks passed
@SSharma-10
SSharma-10 deleted the juliaye/opencode-proxy-m3 branch September 17, 2026 05:56
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.

2 participants