-
Notifications
You must be signed in to change notification settings - Fork 2
plat 105
PLAT-105 — retained delivery and per-turn completion must share one transport-neutral session contract
| Field | Value |
|---|---|
| Status |
blocking live regression; implementation incomplete — delivery through the durable Session works, but a 2026-08-15 Social Media run proved that a provider can capture its final response and exit while AgentWorks never receives or settles the canonical per-turn completion. The UI remains busy indefinitely. Do not close this ticket from unit tests or delivery-only evidence. |
| Priority | P0 |
| Owner | mcpagent session lifetime and delivery acknowledgement |
| Reported | 2026-08-14 |
| Related | PLAT-020, PLAT-035, PLAT-099, PLAT-102, PLAT-103 |
The 2026-08-19 Sales Outreach chat proves the per-turn contract also fails in the opposite direction from the Social Media reproduction below. Instead of losing a real completion, mcpagent emitted completion too early.
For AgentWorks session c2b1bb4c-5a4d-4fd1-9709-ad7329c34a7b, Claude Code
emitted “Let me check what sequence the workflow actually defines.” as text on
a message whose native stop_reason was tool_use. The retained Session's
generic transcript reader returned that in-progress text, and mcpagent emitted
source=mcpagent_session unified_completion at 11:15:47 IST. AgentWorks
truthfully settled what it was given. Claude then ran three more tool rounds
and committed its real stop_reason=end_turn answer at 11:16:10; tmux showed
that answer, but the already-settled Formatted turn could not receive it.
This narrows the producer defect: Claude's retained completion path uses
readClaudeTranscriptMessages (an in-progress tool-trail reader) instead of
the existing completedAssistantResponseFromTranscript end-of-turn oracle.
It is not a frontend spinner, polling, or deduplication defect.
The mandatory P0 must therefore cover both terminal-signal directions:
- no completion may be emitted for assistant text attached to
tool_use; - exactly one matching completion must be emitted after
end_turn; - the final answer must be visible, busy must clear only then, and the same Session must remain reusable.
The Claude producer half is now repaired in multi-llm-provider-go: retained
completion reads only a committed stop_reason=end_turn, with a regression
test based on this live transcript. PLAT-105 remains P0 blocking until the
restarted product path proves the canonical completion reaches AgentWorks once,
settles busy at the correct boundary, and leaves the Session reusable; the
older missing-completion direction is also still independently open.
Video Studio session 5e79aae3-d8f8-45d4-a29d-12258dba0d1e reproduced the
same terminal-signal violation outside the retained-turn reader. Claude Code
successfully completed three api-bridge tool calls and its tmux pane displayed
final-looking character-generation narration. The native JSONL transcript still
contained only stop_reason=tool_use; no current-turn end_turn had committed.
The ordinary interactive adapter treated pane readiness as completion, waited a
fixed two-second transcript grace, raised current turn has no completed end_turn response, and discarded the persistent tmux session.
This was not an MCP, shell, credential, or frontend failure. The retained-turn
repair described above already used the correct completion oracle, but
ClaudeCodeInteractiveAdapter.GenerateContent still had a second completion
clock: waitForMarkedResponse followed by claudeTranscriptCommitGrace.
The shared provider repair removes that fixed grace. Once a transcript exists,
the adapter now waits for its authoritative non-empty stop_reason=end_turn
until the owning turn context is cancelled. Pane readiness remains a transport
hint and cannot terminate an active tool loop. Context cancellation is returned
as cancellation rather than being rewritten as a transcript-protocol error.
Regression coverage delays the final end_turn beyond the former two-second
boundary and separately proves that turn cancellation still bounds the wait.
Focused provider tests pass. This ticket remains open until the restarted Video Studio path proves the final answer is visible, exactly one canonical completion settles busy, and the same Claude session remains reusable.
The 2026-08-15 Social Media backlog-audit chat provides a concrete production
reproduction. Session
86500e07-f362-4d47-8b47-220282b0e981 ran in tmux
mlp-codex-cli-int-1786777150030462000-719b6a79. The provider completed the
work, and server_debug.log recorded at 12:35:00:
codex interactive response captured owner=86500e07-f362-4d47-8b47-220282b0e981
tmux=mlp-codex-cli-int-1786777150030462000-719b6a79 elapsed=5m47.854s
codex interactive trailing capture skipped ... reason=persistent_session
The tmux process subsequently no longer existed and runtime health showed zero
running processes. Nevertheless, AgentWorks still exposed the Chat tab as
busy, kept the cancel action and spinner visible, and showed only 59 tool calls; the captured final assistant response was not projected into Formatted
mode.
This proves that successful provider response capture is not reliably followed by the canonical terminal event that settles the host turn. The defect is at the mcpagent-to-AgentWorks lifecycle boundary, not in the spinner component. Frontend timeout, polling, deduplication, or tmux-pane heuristics are not valid primary fixes.
The system still has overlapping notions of completion:
- the coding CLI returned/captured a final response;
- the provider process or tmux became idle or exited;
- the mcpagent Session considers a turn active or complete;
- AgentWorks holds
sessionBusyand tracked-execution state; - the terminal event store projects the state and final answer to the UI.
Earlier fixes reconciled individual consumers or made the durable Session own retained delivery. They did not prove that every accepted turn emits one terminal completion and that AgentWorks consumes that event before the provider session is retained or discarded. In this reproduction, the adapter captured the answer but the host-side turn-completion defer/settlement never ran. Whether the producer omitted the event or the bridge dropped it must be pinned with tracing before changing code; the observable contract failure is already established.
The durable conversation and an individual turn have different lifetimes. A tmux/provider session may remain alive for later messages without keeping its current turn busy.
- Assign one stable
turn_idwhen AgentWorks accepts a user or scheduled message. Preserve it through AgentWorks, mcpagent, the provider adapter, normalized events, persistence, and UI projection. - For every accepted turn, mcpagent must emit exactly one terminal
unified_completioncarrying thatturn_id. A successful final assistant response must be included in, or durably correlated with, that completion. - AgentWorks must clear
sessionBusyand complete the tracked execution from that canonical event. It must not infer normal completion from tmux pane contents, provider process lifetime, polling inactivity, or the continued existence of the reusable Session. - Provider/tmux lifetime represents conversation transport availability only. It must never be used as the busy state of the most recent turn.
- Completion is idempotent: duplicate terminal signals for the same
turn_idare ignored, while a second accepted message receives a newturn_id. - Add diagnostic reconciliation as a safety net, not the main lifecycle: if a durable final response exists, no provider turn is live, and canonical completion is missing after a bounded grace period, record the invariant violation and settle that same turn exactly once. Do not silently leave the UI busy and do not manufacture a second assistant message.
- Add trace logging for
turn_idat response capture, Session terminal-event emission, bridge receipt, AgentWorks settlement, and UI-event persistence. Use the live reproduction to identify whether the producer or bridge loses the event. - Repair that single ownership path so provider final-response capture closes the mcpagent turn and emits its canonical completion before retained-session cleanup or reuse.
- Make AgentWorks settlement consume the event idempotently and clear busy in a defer/failure-safe path.
- Remove any frontend or server workaround that independently guesses that a turn ended, once the contract test proves the canonical path.
Extend IC-11 with a real host-visible retained-conversation test. It must use the normal product path rather than constructing registry or completion state:
- start a real first turn and wait for its final response;
- assert the final assistant response is persisted and visible in Formatted mode;
- assert Chat busy is false, the spinner/cancel state is gone, and tracked execution is terminal;
- assert exactly one
unified_completionexists for the firstturn_id; - assert the provider Session/tmux remains reusable;
- send a follow-up through that same Session and repeat assertions 2–4 for a
distinct
turn_id; - assert exactly one user message, assistant response, and terminal completion per turn, with no Agent reconstruction and no duplicate tool receipts.
The P0 must fail if it observes only provider-level response capture while the
host remains busy. Tests that directly invoke Session.Close, inject a
completion event, or manually manufacture the retained window do not satisfy
this requirement.
AgentWorks has two delivery paths for a follow-up message:
- when a short-lived
mcpagent.Agentis still registered, it callsmcpagent.DeliverAgentInput; - when the Go agent object has ended but its coding-CLI tmux remains live,
deliverRetainedMainTerminalInputdetects the provider, reconciles it against the stored continuation request, readsGetCodingAgentProviderContract(...).SupportsLiveInput, and callsllmproviders.SendCodingAgentLiveInputdirectly.
The second path is fast and functionally necessary today, but it makes the host application own provider transport details that mcpagent already owns.
The same split also drops structured tool observability on retained turns. A
real 2026-08-14 Codex follow-up executed execute_shell_command and
agent_browser repeatedly through the session-scoped MCP bridge, and the
backend logs carried every call under the correct session ID, but Formatted
mode displayed no tools. The initial wrapped turn receives transcript-derived
tool_call_start/end events; the direct retained-tmux branch bypasses that
stream and did not publish the bridge's authoritative execution receipts.
This is a lifetime and reachability defect, not a missing abstraction.
mcpagent already implements transport-neutral submission in
agent/message_delivery.go (deliverUserMessage). That function normalizes and
validates the message into a typed CodingAgentDeliveryError, resolves the
coding-agent contract, routes tmux live-input against structured/API steer
queueing, and returns a typed UserMessageDeliveryResult{Provider, Transport, DeliveryStatus}. It is exported as mcpagent.DeliverAgentInput.
Its only defect is its signature: it requires a *Agent.
AgentWorks registers runningAgents[sessionID] immediately after
FinalizeDefinition and deletes it when the streaming loop ends — the in-code
comment reads "Clean up running agent reference (steer injection no longer
possible)". The provider tmux is unaffected and stays live. So the map entry is
scoped to the streaming turn, while the provider conversation is scoped to
the tmux process. The only transport-neutral delivery path in the system
becomes unreachable at exactly the moment it is needed, and the host
re-implements it against provider internals.
The older fallback compounded this by treating a missing Go agent as a missing provider conversation and rebuilding a complete agent — tools, skills, MCP configuration, permissions, and provider startup — even when a reusable coding session still existed. PLAT-102 added the correct fast retained path, but at the AgentWorks layer rather than by fixing reachability in mcpagent.
An implementer must start from these, not from a blank sheet. Three of the four pieces the previous revision of this ticket proposed are already shipped:
| Proposed responsibility | Already implemented as |
|---|---|
Session.Submit(message) |
Session.Send(ctx, input) (DeliveryResult, error) — agent/turn_session.go
|
Session.Events() |
Session.Events() <-chan *events.AgentEvent, plus mcpagent.SubscribeAgentEvents
|
Session.Close(reason) |
Session.Close() error — contract pinned by TestAgentAndSessionCloseContractsMatch
|
| Opaque continuation state |
AgentSessionHandle (agent/session_handle.go), persisted per conversation by CodingSessionStore (agent/coding_session.go); provider-native state nested in llmtypes.CodingProviderSessionHandle
|
Session is pinned by TestSessionPublicMethodSurface to exactly
{Close, Events, Run, Send, Snapshot}.
The single missing property is that Session is minted by Agent.Start()
and holds a *Agent field, so its lifetime is bound to the agent object rather
than to the durable provider conversation. That binding — not the API shape — is
what this ticket must remove.
Introducing a second, parallel session type would leave the codebase with four overlapping continuation concepts. It must not happen.
Make the existing delivery path reachable by session identity rather than
object identity, and let the retained conversation own a Session whose
lifetime matches the provider process.
- Decouple
Sessionfrom the*Agentturn object, so a session created for a new conversation survives the streaming loop that created it. - Give mcpagent a lookup from
sessionIDto that live session, so a warm follow-up resolves a session without an*Agentand without reconstructing configuration. - Move retained completion, reconnection, and sidecar ownership out of AgentWorks and into the provider adapters behind the session.
- Replace both AgentWorks delivery branches with one session submission call.
AgentWorks must stop identifying providers, reading
SupportsLiveInput, parsing pane readiness, and reading provider sidecars. - Delete the superseded provider-specific retained-turn code in AgentWorks only after real E2E parity is proven.
The safe first migration slice is implemented and covered by tests:
-
mcpagent.Sessionis registered by session ID.CloseSessionremoves it, and closing an older replaced session cannot accidentally remove the newer session.LLMAgentWrappernow owns that session across main-chat turns. - both warm AgentWorks submission endpoints resolve that durable session first
and call
Session.Send; they no longer reconstruct an agent for the normal between-turn case; - the delivery acknowledgement reports the transport actually used, including a structured transport selected in spite of a provider contract whose default is tmux;
- the MCP bridge publishes canonical direct-execution tool start/end/error receipts for retained turns, including arguments, result/error, and duration; those receipts are suppressed while a wrapped run is active because that run already has transcript-derived tool events. This prevents both forms of the observed bug: missing retained tool rows and duplicate ordinary tool rows.
- the one-turn
Agent.Runconvenience path closes its temporarySessionbefore returning, so it cannot pin an Agent and its tool/MCP state in the process registry indefinitely; -
Agent.Closeinvalidates a registered session only when that session still belongs to the exact Agent being closed. This prevents both a half-closed Agent remaining addressable and an old replaced Agent deleting the newer session under the same conversation ID; - if a warm
Session.Sendfails, AgentWorks now tries the still-live retained terminal before rebuilding a complete turn. An accepted structured-transport queue result does not trigger that bypass.
A real Pi continuation exposed a lifecycle hole after the durable delivery work.
Session 5c0ca18f-4b1f-4933-b7f2-be89bc90049c accepted its follow-up through
mcpagent.Session at 13:09:30. Its tools continued until 13:11:59 and the Pi
tmux then exited, but AgentWorks still showed the conversation busy more than
30 minutes later. Delivery was successful; completion was never observed.
The cause was precise: Session.Send called the transport-neutral delivery
function and returned its acknowledgement, but only Session.Run owned a turn
lifecycle. AgentWorks' older retained-tmux path used its own pane observer, but
the new durable-session path deliberately bypassed that host-owned observer.
The migration had therefore removed the only completion detector without
putting one behind the Session contract.
The repair keeps one owner:
- an idle
Session.Sendthat is accepted by a retained tmux starts a Session- owned completion watch using the provider adapter's canonical retained final response; - the Session appends the user and assistant messages to its durable history
and emits exactly one
unified_completionmarkedsource=mcpagent_session; - direct sends are serialized, and
Session.Runrejects while a retained turn is being delivered or remains active. A rapid follow-up therefore steers the same retained turn instead of racing a second foreground run; - AgentWorks marks the host conversation busy after delivery but starts no second tmux observer. It settles from that canonical completion instead;
-
BaseEventBridgedoes not carry an AgentWorks terminal ID. The settlement code therefore resolves the main terminal from the owner session only when the nested completion has the exactmcpagent_sessionsource marker. A child completion cannot accidentally finish the main conversation; -
Session.Runnow marks the Agent turn in flight. This makes the existing direct-receipt suppression effective and prevents bridge receipts from being duplicated alongside transcript-derived tool events during ordinary turns.
Focused regressions prove Session-owned completion/history, direct-receipt
suppression during Session.Run, and settlement through the real bridge event
shape with no terminal ID. go test ./agent/... and go build ./... pass in
mcpagent; focused AgentWorks retained-turn tests pass. A backend restart and a
live Pi continuation remain the final runtime proof; this document does not
claim that proof before it is run.
Review on 2026-08-14 found that two bullets above were mutually exclusive on the
path that matters. Agent.Run closing its temporary session and the durable
session surviving the wrapped turn cannot both hold, because the main chat
turn runs through Agent.Run:
server.go llmAgent.StreamWithEvents(agentCtx, chatQuery)
llm_agent.go runtimeAgent.Run(ctx, mcpagent.Turn{...}) <- Agent.Run
turn_session.go Start() registers -> defer session.Close() unregisters
Agent.Run calls Start (which registers) and now defers Close (which
unregisters). The session is therefore destroyed at exactly the moment the
retained window opens. LookupSession(sessionID) misses, and both AgentWorks
endpoints fall through to deliverRetainedMainTerminalInput — the provider-
specific branch this ticket exists to retire. The new path is inert on the
only flow that reaches it.
Verified directly rather than by inspection: a probe that called Agent.Run on
an agent with a session ID and then queried the registry reported the session
gone. internal/agentsession is unaffected because it holds a Session from
Start and calls Session.Run; that subsystem is not the chat turn.
Why no test caught it. turn_session_registry_test.go calls Start and
Close directly and never exercises Agent.Run, so it proves registry
mechanics against a path the product does not take. Build, unit tests, and the
focused server tests all pass while the feature does nothing. This is precisely
the failure mode IC-11 proof 2's anti-requirement was written to prevent — the
retained window must be reached by letting a real turn end, never by
constructing the state directly.
Resolution. The dilemma is false: Agent.Run should not have to choose
between pinning an Agent forever and destroying a durable conversation. Session
ownership belongs to the caller that owns the conversation. LLMAgentWrapper
now holds one Session obtained from Start for the lifetime of the chat
session and drives turns through Session.Run, exactly as
internal/agentsession already does, leaving Agent.Run a genuine one-shot
convenience whose deferred Close is correct and harmless. Replacing or
removing a stored wrapper closes its prior wrapper-owned Session, so the map no
longer silently drops an Agent/Session graph.
A unit regression test drives the wrapper's turn method and proves the session remains resolvable after that method returns, then proves wrapper close unregisters it. This closes the inert-path regression but is not substituted for IC-11 proof 2: that proof must still complete a real CLI streaming turn before exercising the retained-window follow-up.
The durable-session migration initially gave LLMAgentWrapper and
mcpagent.Session separate histories. StreamWithEvents appended the current
prompt only to the wrapper history and called Session.Run with Turn.History
but an empty Turn.Input. That works once: after the first run the Session owns
non-empty history and deliberately ignores later Turn.History. Every later
background completion therefore resumed the provider with the last human input
from the first turn instead of the supplied auto-notification.
This was reproduced in the live salesoutreach session: prompt receipts at
12:01, 12:08, 12:13, and later synthetic turns all recorded hi, even though
the server log showed distinct background-agent completion dispatches at those
times. It was a backend replay, not a frontend rendering duplicate.
LLMAgentWrapper now snapshots only prior wrapper history and always passes the
current prompt as Turn.Input. The Session appends that input to its own durable
history on every run. A focused regression test pins that continuation input so
future session-lifetime changes cannot silently revive stale-message replay.
The implementation is intentionally not marked complete. A provider tmux can survive a backend restart while the new in-memory session registry cannot. The old AgentWorks retained-terminal branch therefore remains only as cold-restart compatibility. Retained completion/reconnection and sidecar ownership also remain in AgentWorks. They can be deleted only after IC-11's real retained-window E2E proves delivery, tool events, final response, provider exit, and restart behavior through the shared session contract.
The cold-restart decision is now explicit: the fallback is not the desired
permanent architecture. A server restart must rehydrate the mcpagent session
from the persisted AgentSessionHandle / CodingSessionStore before the static
neutrality gate can turn green. Session identity alone is insufficient while
the registry is process-local. Until rehydration and IC-11 exist, removing the
fallback would regress live tmux conversations that survive a backend restart.
IC-11 proof 2 is now implemented in coding_agent_chat_e2e.go and is required
for every selected provider by scripts/run-coding-cli-p0.sh. It lets the real
first turn finish, verifies the foreground steer target is gone while tmux is
live, submits a tool-backed follow-up, and requires the durable mcpagent source,
actual transport, sub-second acknowledgement, exactly one paired tool receipt,
exactly one final response, no agent_start, and a reusable tmux. The harness
has now passed authenticated live runs for Codex and Claude Code. The Claude run
used claude-sonnet-5. Cursor accepted model auto and completed turn one, but
the live logs showed Executing Cursor CLI structured: cursor-agent --print
and no retained tmux existed, despite the provider contract declaring tmux.
That is a real contract/adapter mismatch; proof 2 correctly remains red for
Cursor rather than pretending a structured one-shot process was retained.
The mismatch was traced to multi-provider commit 4c9e1825: it changed
CursorCLIAdapter.GenerateContent to unconditionally execute structured
--print, but left the shared provider contract and AgentWorks interactive-chat
configuration on tmux. The repair does not force every Cursor use case onto one
transport. It restores the existing explicit selector:
- ordinary interactive chat follows the provider contract and uses retained tmux, so live input and the IC-11 retained window are real;
- workflow/background execution continues to pass
WithCursorStructuredTransport(true)and receives typed stream-json events.
Regression coverage pins unset/false to tmux and true to structured. Focused
provider, mcpagent, wrapper, and server tests pass, as do go build ./... in
both multi-llm-provider-go and AgentWorks. The authenticated real Cursor tmux
contract also passes with model auto (TestCursorCLIRealInteractiveTmuxFullContract,
44.64s), including reuse of the same tmux session for its second turn. After the
backend restart, the cross-repository Cursor IC-11 run passed too: the real first
turn completed, the retained session accepted the follow-up, exactly one tool
receipt and one final response were observed, no Agent reconstruction occurred,
and the tmux remained reusable.
Verification completed for this slice:
-
go test ./agent/... -count=1inmcpagent; - focused AgentWorks live-input/retained/coding-agent server tests;
- the
LLMAgentWrappersession-lifecycle regression test; -
go build ./...inagent_go.
a. The typed acknowledgement can report the wrong transport.
deliverUserMessage sets result.Transport = contract.Transport — the
declared contract transport — but routes on a.usesStructuredTransport(),
which is the union of wantsStructuredTransport(), the per-provider
CodexStructuredTransport / CursorStructuredTransport / PiStructuredTransport
flags, and a non-tmux contract. A Codex run on the structured transport
therefore returns Transport: "tmux" alongside DeliveryStatus: "queued_for_injection". The acknowledgement contradicts itself.
This is not hypothetical: legacyCodingProviderSessionHandle
(agent/session_handle.go) already carries a comment documenting the identical
bug class found live on the continuation handle, where recording
Transport=tmux for a structured turn silently lost all turn-1 context on
Codex/Cursor. The delivery result needs the same fix — report the transport the
call actually used.
b. There is no rejected status. UserMessageDeliveryStatus has exactly
two values, sent_to_cli and queued_for_injection. Rejection is an error
return carrying CodingAgentDeliveryError{Kind, Provider, Reason}. Either add
an explicit rejected status or state that rejection stays a typed error — but
the contract must say which, because acceptance depends on it.
Decision implemented: DeliveryResult represents accepted delivery only;
rejection remains a typed CodingAgentDeliveryError. Session.Send no longer
replaces an empty-message rejection with a generic formatting error, so hosts
can branch on the stable error kind without parsing text.
Adapters implement the mechanics against the transport recorded on the handle
(llmtypes.CodingProviderTransportTmux / CodingProviderTransportStructured),
plus API-backed providers:
- interactive tmux: inject input, observe provider readiness, read the authoritative transcript/sidecar;
- structured JSON/event: submit through the provider channel and normalize its structured stream;
- API: continue through the provider conversation identifier and normalize streamed or bounded responses.
- Fix the transport-reporting defect in
deliverUserMessageand decide the rejection representation. Both are prerequisites for a stable contract. - Decouple
Sessionlifetime from*Agentand add session lookup bysessionID. - Have agent creation register the durable session handle for the conversation.
- Move retained completion, reconnection, and sidecar ownership from AgentWorks into the provider adapters.
- Replace both AgentWorks delivery branches with the single session call.
- Update the pinned API golden files in the same commit (see Gates).
- Land IC-11 proof 1 (static neutrality assertion) with step 5, and IC-11 proof 2 (retained-window E2E) before step 8.
- Remove the superseded AgentWorks retained-turn code only after E2E parity. IC-11 proof 1 turning green is the objective signal that this is complete.
The existing P0 certification suite cannot catch this defect, and it is worth being precise about why before adding coverage.
requiredP0CertificationIDs includes CertLiveInput and CertBusyLiveInput,
both enforced as real E2Es. Neither helps here, for two independent reasons:
-
Wrong layer. Those certifications live in
multi-llm-provider-goand prove Boundary 3 (adapter ↔ CLI). They pass identically whether AgentWorks routes through mcpagent or bypasses it and callsllmproviders.SendCodingAgentLiveInputitself — which is what it does today. The broken boundary is the one they do not observe. -
Wrong moment. Every live-input certification and the IC-10 chat E2E
exercise the in-flight case:
coding_agent_chat_e2e.gostarts a query, waits for the session to become steerable, and steers mid-turn.CertBusyLiveInputis explicitly a follow-up during a slow tool call. This ticket's defect lives in the window between turns, afterrunningAgents[sessionID]is deleted and while the tmux is still alive. No suite enters that state.
Also note RequiredP0CodingAgentCertificationIDs returns nil for any
non-tmux contract, so structured-transport runs currently have no required P0
floor at all — directly relevant to acceptance 3 and 6.
This coverage therefore belongs at Boundary 1 as a new integration-contract
area in agent_go/docs/cross_repo_integration_contract.md, not as a new ID
in the multi-llm-provider-go certification registry: that registry is keyed
per provider adapter and its runner asserts the proof lives under
pkg/adapters/<provider>/, which is the wrong repo and the wrong layer.
A follow-up to a live provider conversation is submitted through one mcpagent API, with no provider-specific knowledge in the host.
Proof 1 — static neutrality assertion (always on, no CLI required).
An AST test over agent_go/cmd/server asserting that the package contains zero
references to llmproviders.SendCodingAgentLiveInput,
GetCodingAgentProviderContract, and the retained sidecar readers. This mirrors
the approach already used in mcpagent/agent/public_api_golden_test.go, runs in
ordinary CI, and is the assertion that actually prevents regression — a runtime
spy can only prove one path was not taken on one run, whereas this proves the
capability is absent. It also gives migration step 7 an objective completion
signal.
Proof 2 — retained-window live E2E (opt-in, gated).
The state no current suite reaches. Following the cmd/testing/ opt-in live
command pattern used by IC-10:
- Start a turn and let it complete.
- Wait until
runningAgents[sessionID]has been deleted and the provider tmux is still live — the assertion setup, not an incidental precondition. - Send a follow-up.
- Assert: it is accepted; the acknowledgement is the typed session result with
the transport that was actually used; exactly one non-empty
unified_completion.final_resultis persisted; the session settles while the tmux stays reusable; and delivery latency stays within PLAT-102's envelope. - Assert no agent reconstruction occurred — no tool-registry or MCP-bridge rebuild for the warm turn. PLAT-102 established that reconstruction costs seconds and is visible in the startup-timing stages, so this is observable rather than inferred.
Run it per transport, enumerated: Claude Code, Codex, Cursor, and Pi have retained sidecar recovery today. Structured and API transports are explicitly out of scope for proof 2 until acceptance 6's gap is closed; that exclusion must be recorded rather than left implicit.
Anti-requirement. The E2E must reach the retained window by letting a real
turn end, never by deleting the agent registration to manufacture the state.
PLAT-035 records the precedent — a regression test that injected streaming_end
itself proved what happened after an event arrived but not that a real
retained input produced one — and CertReplyFormattingFidelity carries the same
warning that "a green test that proved nothing is how this defect shipped in
the first place."
-
agent/public_api_golden_test.gopins the public surface and its own comment requires that "any deliberate change updates this list in the same commit." A new exported function must be added toTestPackageFunctionSurfaceDoesNotRegrow, and — if its signature mentions*Agent— also toTestAgentFacadeFunctionSurface. NewSessionmethods changeTestSessionPublicMethodSurface.*Agentitself is pinned to exactly four methods and zero exported fields; this work must not regrow it. - PLAT-099 hazard:
deliverRetainedMainTerminalInputcurrently clearsmodelIDwhen the live tmux provider disagrees with the stored continuation provider, which is what stops a message reaching a stale provider after Update Automation switches providers. Moving that logic behind the session must preserve it, or PLAT-099 reopens. Acceptance 5 owns this.
- AgentWorks uses one mcpagent API for follow-up submission regardless of
transport, and contains no provider identification,
SupportsLiveInputcheck, pane-readiness parsing, or sidecar read. Proven by IC-11 proof 1, which fails if any of that capability remains inagent_go/cmd/server. - A warm follow-up does not rebuild an
Agent, tool registry, skills, MCP configuration, or provider process. - Every transport returns the same acknowledgement type, with transport
differences expressed as field values, never as different call shapes. The
reported
Transportmatches the transport actually used, including when a per-provider structured flag overrides a tmux contract. Codex legitimately accepting a follow-up as the next turn while Claude Code, Cursor, and Pi accept it in-turn is a value difference, not a contract violation. - Final responses, tool calls, errors, queueing, cancellation, rate limits, reconnection, and process exit have transport-independent host semantics. In particular, a warm retained turn emits exactly one visible tool row per real bridge execution, carrying the authoritative arguments, result/error, and duration; it must neither omit the call nor duplicate it with a transcript-intent event.
- Rapid messages are serialized per session and never merged or delivered to a stale provider after a configuration change. The PLAT-099 provider-switch case has explicit regression coverage.
- Real E2E covers first turn, warm continuation, concurrent submission,
provider exit, reconnection, and final-response recovery for each supported
transport, enumerated explicitly. Note the current gap:
ReadCodingAgentRetainedTurnMessagesimplements retained-turn recovery for Claude Code, Codex, Cursor, and Pi only, and returnsnilby default — so structured and API transports have no retained-response path yet. Either close that gap here or scope this acceptance to the tmux transports and open a follow-up. - PLAT-102's warm retained latency does not regress; the measured sub-second delivery path is preserved.
- Do not introduce a new session type alongside
Session,AgentSessionHandle, andCodingProviderSessionHandle. - Do not force every provider to use tmux or pretend API calls are persistent processes.
- Do not standardize provider-internal mechanics; standardize host-visible submission and lifecycle semantics.
- Do not remove the current fast path before the shared contract has equivalent runtime coverage.
Auto-synced from docs/ on main. Edit there, not here.