Provider seam: adapter-owned state composition — ReadChat(runID, runtimeDir, transcriptPath) replaces ReadTranscript; PendingDialog/BlockedState become adapter-private #92
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#92
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Decision from the 2026-07-10 grill on #62's design (full decision record on #62). Today, conversational state is composed on two floors: the adapter derives a state from its transcript (
claudecode.deriveState— tail guess;codex.deriveState— rollout lifecycle events), then core'schat.Service.applyLiveSignals(internal/chat/chat.go) overlays the provider's spool signals (PendingDialog,BlockedState) with core-owned precedence rules. Core is second-guessing the adapter using the adapter's own signals.The codex adapter (#87 / ADR-0037) shows the right altitude: it composes state from its agent's native signals (rollout
task_started/task_complete/turn_aborted) with no spool at all, and core has nothing to add. The seam should make that the rule: the adapter owns "what state is my agent in", composed from whatever its agent's best signals are; core owns run lifecycle only (StateEnded override, transcript identity). "Liveness" never becomes a seam concept — #62's claude-code hook edges will then be a purely adapter-private feature, and core works identically with any adapter.This refactor is behavior-neutral: no state semantics change, no API payload change.
Design
Seam signature.
ReadTranscript(path string) (Chat, error)becomes:transcriptPathmay be""(active run, transcript not yet located): the adapter must still consult its live signals — today core does this (chat.Service.Readconsults the spool before the firstLocateTranscripthit so a pre-transcript pending dialog surfaces).runtimeDirmay be""(no runtime dir configured): signals off, transcript-only — same degradation core'sliveSignals()gate applies today.provider.Chatgains the side-channel fieldPendingDialog *Dialog, moved fromchat.View. It stays OUT of the message stream — the #89/#90 invariant (seq numbers reparse-stable; the dialog is never a synthetic message) is preserved verbatim, just owned by the adapter now.What leaves the seam.
PendingDialogandBlockedStateare removed from theLiveSignalsinterface and become claudecode-private functions, called inside itsReadChatwith exactly the precedence core applies today:StateQuestion+Chat.PendingDialog;StateNeedsInput;What stays on the seam.
LiveSignalsnarrows to lifecycle plumbing lab genuinely owns:Setup(arming at spawn — lab writes the settings file and appends spawn args),SpoolSig(cheap change digest so the tailer knows when to re-read while the transcript is byte-frozen),SweepSpools(runtime-dir GC against the active keep-set). Unchanged.Core collapse.
chat.Service.Readand the tailer callReadChat;applyLiveSignalsand theliveSignals()resolver are deleted.ViewkeepsTranscriptIDand theStateEndedoverride for terminal runs (lifecycle, not signals) and forwardsChat.PendingDialogso the HTTP payload shape is unchanged.Adapters.
deriveStateand the spool readers become one private composition path.ReadChatignoresrunID/runtimeDir(pure rollout fold; the exportedParseTranscriptcompat-test entry point is untouched).LiveSignalslifecycle methods.Conformance. Tier-1 read-path checks (
internal/provider/providertest/conformance.go) driveReadChat; both registered adapters pass via the existing registration table.Regression guards (must not change behavior)
StateQuestionwith no spool present.StateEndedregardless of any spool residue (core-owned, unchanged).Acceptance
ReadChat(runID, runtimeDir, transcriptPath)on the seam;PendingDialog/BlockedStategone from theLiveSignalsinterface; no core code interprets spool signals.applyLiveSignalsdeleted; state composition lives wholly in adapters; core keeps only theStateEndedoverride +TranscriptID.PendingDialog,TranscriptID, state vocabulary identical).Out of scope
Sequencing
Independently landable now. #62's implementation phase is blocked on this (its adapter needs
runID+runtimeDirat read time to see its liveness markers).Validation of PR #94 (
afk/92) — verdict: PASSRelied-on verification signal: Forgejo CI
ci / native— success (exit 0, 6m47s). Not re-run.refactor(provider):); validCloses #92.ReadroutesruntimeDironly for active runs; ended runs read transcript-only by construction (runtimeDir "") and are force-StateEndedwith nil dialog. Active-no-transcript delegates toReadChat(id, dir, "");transcriptID("")correctly yields""(no identity leak).ReadChat, retrying on read error; a robustness improvement, not a regression.ErrTranscriptGoneis unchanged. codex is a pure rollout fold ignoring runID/runtimeDir.read-chatconformance obligation (empty-path idle contract +ErrTranscriptGonesentinel) driving both adapters, with negative broken-adapter coverage for all three failure shapes.No blockers, no concerns. Clean to merge on human free-text go-ahead.