Skip to content

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

Open
julia-ye wants to merge 2 commits into
juliaye/opencode-proxy-m2from
juliaye/opencode-proxy-m3
Open

agents proxy: opencode M3 — history on re-attach#1949
julia-ye wants to merge 2 commits into
juliaye/opencode-proxy-m2from
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-sideattach 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

julia-ye and others added 2 commits September 4, 2026 00:15
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>
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

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

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