refactor(provider): adapter-owned state composition — ReadChat replaces ReadTranscript; PendingDialog/BlockedState become adapter-private (ADR-0038) #94

Merged
dominik.polakovics merged 1 commit from afk/92 into main 2026-07-10 13:21:18 +02:00

Closes #92

What

The seam method ReadTranscript(path) becomes ReadChat(runID, runtimeDir, transcriptPath), and conversational-state composition moves wholly into the adapters. Core owns run lifecycle only — the StateEnded override and transcript identity. Behavior-neutral: identical state semantics, identical HTTP payload (pending_dialog, transcript_id, state vocabulary byte-for-byte).

How

  • Seam (internal/provider/provider.go): ReadChat with pinned "" semantics — an empty transcriptPath (active run, transcript not yet located) must still consult live signals and yields an idle empty chat, never an error; an empty runtimeDir is a transcript-only read, which is exactly how core reads ended runs, so terminal spool-residue leaks are impossible by construction. provider.Chat gains the PendingDialog side-channel field — never a synthetic message, so the #89/#90 reparse-stable-seq invariant transfers verbatim.
  • LiveSignals narrows to the lifecycle plumbing lab genuinely owns: Setup, SpoolSig, SweepSpools. PendingDialog/BlockedState are now claudecode-private functions, applied inside its ReadChat with core's exact old precedence (live spool dialog → question + side channel; transcript-flushed dialog stands — the dormant fallback; blocked marker → needs_input; else transcript state).
  • Core collapse (internal/chat): applyLiveSignals and the liveSignals() resolver deleted; Read and the tailer call ReadChat; View forwards Chat.PendingDialog via embedding and keeps TranscriptID + the StateEnded override. The tailer stages stat results and commits them only after a successful read, so a failed read retries next tick.
  • codex: pure rollout fold, runID/runtimeDir ignored; both adapters' exported ParseTranscript compat entry points untouched.
  • Conformance: new hermetic Tier-1 read-chat obligation (empty-path idle contract, ErrTranscriptGone sentinel) — both adapters pass through the existing registration table; broken-adapter coverage for all three failure shapes.
  • providertest fake composes scripted signals adapter-style and records ReadChat args, so core tests pin the forwarding (seam args per run outcome), while composition itself is pinned in the adapters' own tests.
  • ADR-0038 records the decision (amends ADR-0016/0026's read method and ADR-0020's interpretation floor; the spool protocol/compat §9 coupling stand unchanged).

Regression guards (issue checklist)

  • #89/#90 pending dialog survives mid-window writes/refetch churn: httpapi tests unchanged and green; side-channel + seq invariant pinned on Chat.PendingDialog and in claudecode's composition tests.
  • Dormant fallback: transcript-flushed dialog → StateQuestion with nil side channel (claudecode + core forwarding tests).
  • Pre-transcript spool consult: active run, no transcript, pending dialog surfaces (claudecode + core tests, seam args asserted).
  • Ended run is StateEnded regardless of spool residue — asserted by construction (ReadCalls shows runtimeDir "" for the ended read).

Verification

gofmt clean, CGO_ENABLED=0 go build ./..., go vet ./..., golangci-lint run (0 issues), go test ./... — all green except internal/tmuxx integration tests, which fail identically on the base commit in this sandbox (tmux sessions cannot spawn here; pre-existing, unrelated). Live compat recipe tests compile (vet) and stay env-gated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Erjjo4AEr16vSAxM9AFqou

Closes #92 ## What The seam method `ReadTranscript(path)` becomes `ReadChat(runID, runtimeDir, transcriptPath)`, and conversational-state composition moves wholly into the adapters. Core owns run lifecycle only — the `StateEnded` override and transcript identity. **Behavior-neutral**: identical state semantics, identical HTTP payload (`pending_dialog`, `transcript_id`, state vocabulary byte-for-byte). ## How - **Seam** (`internal/provider/provider.go`): `ReadChat` with pinned `""` semantics — an empty `transcriptPath` (active run, transcript not yet located) must still consult live signals and yields an idle empty chat, never an error; an empty `runtimeDir` is a transcript-only read, which is exactly how core reads **ended** runs, so terminal spool-residue leaks are impossible by construction. `provider.Chat` gains the `PendingDialog` side-channel field — never a synthetic message, so the #89/#90 reparse-stable-seq invariant transfers verbatim. - **LiveSignals narrows** to the lifecycle plumbing lab genuinely owns: `Setup`, `SpoolSig`, `SweepSpools`. `PendingDialog`/`BlockedState` are now claudecode-private functions, applied inside its `ReadChat` with core's exact old precedence (live spool dialog → question + side channel; transcript-flushed dialog stands — the dormant fallback; blocked marker → needs_input; else transcript state). - **Core collapse** (`internal/chat`): `applyLiveSignals` and the `liveSignals()` resolver deleted; `Read` and the tailer call `ReadChat`; `View` forwards `Chat.PendingDialog` via embedding and keeps `TranscriptID` + the `StateEnded` override. The tailer stages stat results and commits them only after a successful read, so a failed read retries next tick. - **codex**: pure rollout fold, `runID`/`runtimeDir` ignored; both adapters' exported `ParseTranscript` compat entry points untouched. - **Conformance**: new hermetic Tier-1 `read-chat` obligation (empty-path idle contract, `ErrTranscriptGone` sentinel) — both adapters pass through the existing registration table; broken-adapter coverage for all three failure shapes. - **providertest fake** composes scripted signals adapter-style and records `ReadChat` args, so core tests pin the *forwarding* (seam args per run outcome), while composition itself is pinned in the adapters' own tests. - **ADR-0038** records the decision (amends ADR-0016/0026's read method and ADR-0020's interpretation floor; the spool protocol/compat §9 coupling stand unchanged). ## Regression guards (issue checklist) - #89/#90 pending dialog survives mid-window writes/refetch churn: httpapi tests unchanged and green; side-channel + seq invariant pinned on `Chat.PendingDialog` and in claudecode's composition tests. - Dormant fallback: transcript-flushed dialog → `StateQuestion` with nil side channel (claudecode + core forwarding tests). - Pre-transcript spool consult: active run, no transcript, pending dialog surfaces (claudecode + core tests, seam args asserted). - Ended run is `StateEnded` regardless of spool residue — asserted by construction (`ReadCalls` shows `runtimeDir ""` for the ended read). ## Verification `gofmt` clean, `CGO_ENABLED=0 go build ./...`, `go vet ./...`, `golangci-lint run` (0 issues), `go test ./...` — all green except `internal/tmuxx` integration tests, which fail identically on the base commit in this sandbox (tmux sessions cannot spawn here; pre-existing, unrelated). Live compat recipe tests compile (vet) and stay env-gated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Erjjo4AEr16vSAxM9AFqou
State was composed on two floors: adapters derived a transcript state, then
core's chat.Service.applyLiveSignals overlaid the provider's own spool
signals with core-owned precedence. The adapter now owns "what state is my
agent in" inside ReadChat; core owns run lifecycle only (StateEnded
override, transcript identity). Behavior-neutral: no state semantics
change, no API payload change.

- seam: ReadChat(runID, runtimeDir, transcriptPath) with pinned "" semantics
  (pre-transcript reads consult live signals; runtimeDir "" is transcript-
  only — how core reads ended runs, so spool residue can't leak by
  construction); provider.Chat gains the PendingDialog side channel, the
  #89/#90 seq-stability invariant preserved verbatim
- LiveSignals narrows to lifecycle plumbing (Setup, SpoolSig, SweepSpools);
  PendingDialog/BlockedState are claudecode-private composition inputs,
  applied with core's exact old precedence
- core: applyLiveSignals and the liveSignals resolver deleted; Read and the
  tailer call ReadChat; View forwards Chat.PendingDialog and keeps
  TranscriptID + the StateEnded override
- codex: pure rollout fold, runID/runtimeDir ignored; exported
  ParseTranscript compat entry points untouched on both adapters
- conformance: hermetic Tier-1 read-chat obligation (empty-path idle
  contract, ErrTranscriptGone sentinel); both adapters pass via the
  existing registration table
- providertest fake composes scripted signals adapter-style and records
  ReadChat args so core tests pin the forwarding, not the composition

Closes #92

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Erjjo4AEr16vSAxM9AFqou
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!94
No description provided.