15 KiB
15 KiB
spec, decision, created, status
| spec | decision | created | status |
|---|---|---|---|
| docs/specs/agent-stall-recovery/spec.md | docs/decisions/2026-08-02-agent-terminal-diagnostics-over-stderr.md | 2026-08-02 | done |
Implementation Plan: OpenCode Terminal Error Surfacing
Overview
Enable OpenCode's error-only stderr output, normalize only session-correlated
foreground stream error records inside agentctl, and use that terminal signal
to release a prompt whose ACP request never returns. Carry a bounded safe
provider-error record through the existing lifecycle, classify usage-limit
messages, and render a localized recovery card with collapsed technical details
on desktop and mobile. Ambiguous silence continues to use the shipped advisory
stall notice.
Backend
Managed OpenCode diagnostic output
- Change
OpenCodeACP.ManagedNPMRuntimeinapps/backend/internal/agent/agents/opencode_acp.goso every managed OpenCode ACP entry point usesacp --print-logs --log-level ERROR. Because normal launch, capability probes, and one-shot inference all consume this shared runtime spec, none may construct a divergent OpenCode command. - Update the command contracts in
apps/backend/internal/agent/agents/opencode_acp_test.go,apps/backend/internal/agent/agents/managed_npm_runtime_test.go,apps/backend/internal/agent/runtime/lifecycle/manager_launch_test.go, andapps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md.
Non-blocking stderr observation
- Add an optional
StderrLineConsumerinterface besideStderrProviderinapps/backend/internal/agentctl/server/adapter/adapter.go. The interface receives an ANSI-stripped line as it is appended to the existing bounded stderr ring; it is not a browser API and does not expose historical files. - Update
Manager.readStderrinapps/backend/internal/agentctl/server/process/manager.goto deliver each cleaned line to an installed consumer without allowing parsing or channel backpressure to block draining the child process. For OpenCode, apply a protocol-specific safe projection before generic logging, the recent-stderr ring, or process-exit events; malformed or unrelated records are excluded. Other managed agents retain their existing stderr behavior. - Add focused process-manager regressions proving delivery, ANSI stripping, non-consumer behavior, and that a nonzero OpenCode exit cannot expose raw workspace URLs or identifiers through logs or exit events.
OpenCode diagnostic normalization and prompt correlation
- Add
apps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr.gowith a narrow key/value parser for OpenCode's emitted error records. Accept a record only whenlevel=ERROR,message="stream error",smallis nottrue,session.idis present, anderror.erroris non-empty. Parse the timestamp, provider ID, and model ID; strip URLs and identifier-bearing suffixes from the error text before constructing the normalized record. - Add a protocol-neutral safe record in
apps/backend/internal/agentctl/types/streams/provider_error.goand attach it optionally tostreams.AgentEvent. Fields are limited tosource,provider_id,model_id,message,occurred_at, andreset_at. - Let the ACP adapter implement
StderrLineConsumeronly foragentID == "opencode-acp". Route a validated diagnostic to the current prompt only when its provider session ID equals the adapter's active ACP session and a foreground prompt turn is still pending. The delivery path must be non-blocking and must discard stale lines after the prompt settles. - Extend the prompt-turn wait in
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.goso a correlated terminal diagnostic competes with the ACP RPC and user cancellation. Whichever terminal path claims the prompt first cancels the losing operation, preserves generation ownership, and emits one outcome. - Return a typed prompt error carrying the safe provider-error record. Update
handleWSPromptandManager.SendErrorEventinapps/backend/internal/agentctl/server/api/agent.goandapps/backend/internal/agentctl/server/process/manager.goso the existing error event includes those structured details. Do not send a second error if the ACP RPC loses the race after the diagnostic has settled the prompt.
Lifecycle propagation and quota classification
- Add the optional safe provider-error record to
AgentExecution,AgentEventPayload, andwatcher.AgentEventDatainapps/backend/internal/agent/runtime/lifecycle/types.go,apps/backend/internal/agent/runtime/lifecycle/event_types.go,apps/backend/internal/agent/runtime/lifecycle/events.go, andapps/backend/internal/orchestrator/watcher/watcher.go. Also addAgentIDto the lifecycle payload so downstream classification usesopencode-acprather than inferring an agent from prose. - In
manager_events.go, copy structured details from the winning generation's error event beforeMarkCompletedpublishesagent.failed. Do not retain details from a stale generation or a later duplicate terminal event. - Extend
apps/backend/internal/agent/runtime/routingerr/rules.goso OpenCode's exactusage limit reachedsignature classifies as high-confidencequota_limited. Preserve the existing sanitizer and ensure account/workspace URLs and long identifiers never enterRawExcerpt. - In
createRecoveryStatusMessageinapps/backend/internal/orchestrator/event_handlers_agent.go, classify the failure using the event'sAgentID, validate the structured provider record, and persist the existing recovery actions with metadata:failure_kind: provider_quota_limited, safeprovider_name, optionalmodel_id, optional RFC3339reset_at, and bounded sanitizederror_output. Fall back to the generic recoverable-error card when classification or structured correlation is absent. - Continue using the current
MarkCompleted->agent.failed->handleRecoverableFailurepath. Non-Office sessions becomeWAITING_FOR_INPUT; Office sessions keep their existingFAILEDbehavior. This feature does not invent a second lifecycle state or retry loop.
Frontend
Localized provider-limit recovery
- Extend
ActionMeta/MessageActionmetadata typing inapps/web/components/task/chat/types.tsandapps/web/components/task/chat/messages/action-message.tsxfor the safe provider-limit fields. - Add a
ProviderQuotaRecoverybranch besideMissingBranchRecovery. It usesfailure_kind, not string matching, to render a localized heading naming the model when available, localized reset guidance whenreset_atis present, the existing recovery actions, and the existing collapsed Technical details disclosure for sanitizederror_output. - Add all new copy to
apps/web/src/locales/en/chat.jsonandapps/web/src/locales/pseudo/chat.json; resolve chat keys throughuseTranslationat render time. Do not translate tokens that are compared in control flow. - Keep the generic settled-error behavior unchanged for unknown provider failures and keep the existing neutral running-stall notice unchanged.
Mobile design contract
- Desktop outcome: the active spinner disappears when the correlated error settles the turn. A compact error card identifies the affected model and reset timing, keeps sanitized technical details collapsed, and exposes the existing recovery actions.
- Mobile entry point and outcome: the same card appears inline in the task chat. No drawer or route is added; quota recovery is short, contextual content already owned by the chat transcript.
- Nearest shipped exemplars:
MissingBranchRecoverysupplies the localized alert-card/details composition, whilemobile-transient-retry.spec.tsandmobile-pause-resume-recovery.spec.tssupply the phone recovery-action and geometry patterns. - Hierarchy and geometry: heading, reset guidance, collapsed details, then recovery actions. Chat remains the single scroll owner; technical output has its own bounded vertical overflow only when expanded. Mobile actions retain the existing full-width 44px touch geometry, and the page gains no horizontal overflow.
- Shared logic: metadata interpretation, reset-time formatting, details, and action handlers are shared across viewports; only responsive classes control layout.
Tests
- What: every managed OpenCode ACP command enables error-only stderr while
other managed agents remain unchanged.
Files:
apps/backend/internal/agent/agents/opencode_acp_test.go,managed_npm_runtime_test.go, and lifecycle launch tests. How: exact argv assertions across runtime, probe, and inference builders. - What: process stderr reaches an optional consumer as cleaned live lines
without changing the bounded recent-stderr contract.
File:
apps/backend/internal/agentctl/server/process/manager_test.goor a focused sibling test. How: fixture process writes ANSI stderr; assertions cover ordered delivery, no consumer, bounded ring behavior, a backlogged diagnostic channel that does not block the drain loop, and a nonzero OpenCode exit whose log, ring, and exit event contain only the safe projection. - What: only a main-session OpenCode stream error is normalized.
File:
apps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr_test.go. How: table tests cover the observed quota line, quoted fields, URL sanitization,small=true, wrong level/message, missing fields, malformed input, and unrelated session IDs. - What: a correlated diagnostic releases a held prompt exactly once while
stale/background/wrong-session diagnostics do not.
File:
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt_test.go. How: fake ACP agent holdssession/prompt; inject stderr through the consumer and race it against RPC completion and cancellation. - What: structured provider details survive the lifecycle event boundary
only for the winning prompt generation.
Files: lifecycle
manager_events_test.go, event publisher tests, and orchestrator watcher tests. How: direct error events with matching and stale generations. - What:
usage limit reachedbecomes high-confidencequota_limited, raw excerpts are sanitized, and recovery metadata contains only safe model/reset fields plus existing actions. Files:routingerr/classify_test.goandorchestrator/event_handlers_test.goor a focused provider-failure test. How: table classifier tests plus direct recoverable-failure handler tests. - What: provider quota metadata renders localized model/reset copy,
collapsed sanitized details, and 44px recovery actions without changing
generic errors or running stall notices.
File:
apps/web/components/task/chat/messages/action-message.test.tsx. How: store-backed rendered component tests using English and pseudo locale fixtures.
E2E Tests
- Scenario: given a settled OpenCode quota failure, opening the task shows
model/reset guidance rather than a running spinner; expanding technical
details reveals the sanitized message but no OpenCode workspace URL or ID.
File:
apps/web/e2e/tests/session/provider-quota-recovery.spec.ts. - Scenario: the same provider-limit explanation, details disclosure, and
recovery actions are usable by touch with no horizontal overflow.
File:
apps/web/e2e/tests/session/mobile-provider-quota-recovery.spec.ts. - Seed the settled session and persisted status-message metadata through the existing E2E-only task/session/message helpers. Do not require a live quota exhaustion or write provider credentials; the real stderr-to-error boundary is covered by the agentctl and lifecycle integration tests.
Verification Results
- Task 01:
cd apps/backend && go test ./internal/agent/agents ./internal/agentctl/server/process ./internal/agentctl/server/adapter/transport/acp -run 'Test(OpenCode|ManagedNPMRuntime|Manager.*Stderr|ParseOpenCode)' -count=1— 24 tests passed. - Task 02:
cd apps/backend && go test ./internal/agentctl/server/adapter/transport/acp ./internal/agentctl/server/api ./internal/agentctl/server/process -run 'Test(OpenCodeDiagnostic|Prompt.*Diagnostic|HandleWSPrompt.*ProviderError|SendErrorEvent)' -count=1— 3 tests passed. - Task 03:
cd apps/backend && go test ./internal/agent/runtime/lifecycle ./internal/agent/runtime/routingerr ./internal/orchestrator ./internal/orchestrator/watcher -run 'Test(.*ProviderError|.*OpenCode.*UsageLimit|HandleRecoverableFailure.*Quota|CreateRecoveryStatusMessage.*Quota)' -count=1— 13 tests passed. - Task 04:
cd apps && pnpm --filter @kandev/web test -- --run components/task/chat/messages/action-message.test.tsx— 17 tests passed;cd apps/web && pnpm run typecheck,pnpm run i18n:check, andpnpm run i18n:ratchetpassed. - Task 05: managed desktop and mobile Playwright runs passed after the E2E seed was corrected to include
completed_at: one Chromium test and onemobile-chrometest passed. - Fixup:
cd apps/backend && go test ./internal/agent/agents ./internal/agentctl/server/process ./internal/agentctl/server/adapter/transport/acp -run 'Test(OpenCode|ManagedNPMRuntime|Manager.*Stderr|ManagerProcessExit|ParseOpenCode)' -count=1— 26 tests passed;go test ./...passed with 10,237 tests.
Implementation Waves And Parallel Candidates
Execution is sequential in the primary conversation. Each task consumes a contract introduced by the previous task, so none is marked parallel-safe.
Wave 1:
Wave 2:
Wave 3:
Wave 4:
Wave 5:
Risks and non-goals
- OpenCode's stderr record is an agent compatibility dialect. Parsing fails closed: a format change loses the immediate error but cannot fail an unrelated turn.
- The stderr reader must never block, because blocking it can deadlock the supervised process; the OpenCode diagnostic channel is bounded and its consumer drops records when backlogged.
- Prompt RPC completion, user cancellation, and the diagnostic may race; prompt generation ownership and a single terminal claim are correctness boundaries.
- Only the sanitized normalized provider record crosses agentctl. OpenCode raw stderr is inspected in memory by its parser but is never written to generic logs, the recent-stderr ring, process-exit events, persisted recovery metadata, or browser payloads.
- The plan does not read OpenCode log files, add an automatic model switch, schedule reset-time retries, change Office routing, or replace the upstream ACP fix.