feat: surface OpenCode provider limit errors (#2167)
This commit is contained in:
@@ -30,6 +30,10 @@ Stall detection is advisory and recovery remains operator controlled:
|
||||
process. `RUNNING` sessions retain the conservative prompt-admission behavior
|
||||
defined by
|
||||
[ADR-2026-07-28-coarse-running-busy-signal](2026-07-28-coarse-running-busy-signal.md).
|
||||
- This advisory rule applies to inactivity without terminal evidence. A
|
||||
sanitized, session-correlated provider diagnostic follows
|
||||
[ADR-2026-08-02-agent-terminal-diagnostics-over-stderr](2026-08-02-agent-terminal-diagnostics-over-stderr.md)
|
||||
and may fail the prompt immediately.
|
||||
- The watchdog checks once per 60 seconds and logs only the first detected stall
|
||||
for a prompt generation; it does not repeat a notice every check.
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# ADR-2026-08-02-agent-terminal-diagnostics-over-stderr: Capture Agent Terminal Diagnostics From Managed Stderr
|
||||
|
||||
**Status:** accepted
|
||||
**Date:** 2026-08-02
|
||||
**Area:** backend, frontend, protocol, security
|
||||
|
||||
## Context
|
||||
|
||||
OpenCode can log a terminal provider stream failure while leaving its ACP
|
||||
`session/prompt` request open. Kandev then sees an indefinitely quiet running
|
||||
turn even though the agent knows it cannot continue. The same diagnostic is
|
||||
available in OpenCode's private data-directory log and, when explicitly
|
||||
enabled, on the stderr of the `opencode acp` process that agentctl owns.
|
||||
|
||||
## Decision
|
||||
|
||||
Agent-private files are not a Kandev runtime contract. When an agent supports
|
||||
explicit diagnostic emission, Kandev enables error-only output on the managed
|
||||
agent subprocess and consumes it inside agentctl, in the executor where the
|
||||
process runs.
|
||||
|
||||
An out-of-band diagnostic may settle a prompt only when an agent-specific
|
||||
parser validates the terminal signature and correlates it to the current agent
|
||||
type, active provider session, and active foreground prompt. Parsers retain
|
||||
only allowlisted fields, sanitize before crossing the agentctl boundary, and
|
||||
never forward raw log lines. Unmatched, malformed, background, stale, or
|
||||
unsupported diagnostics are ignored.
|
||||
|
||||
For OpenCode, a correlated `stream error` is terminal evidence, not an
|
||||
inactivity inference. It cancels the held prompt operation and enters Kandev's
|
||||
existing generation-scoped error lifecycle. Silence without such evidence
|
||||
continues to follow
|
||||
[ADR-2026-07-29-agent-stall-user-controlled-recovery](2026-07-29-agent-stall-user-controlled-recovery.md).
|
||||
|
||||
Raw stderr is inspected only in memory by the protocol-specific parser. For
|
||||
OpenCode, the process manager applies the parser's safe message projection
|
||||
before writing generic logs, the recent-stderr ring, or process-exit events;
|
||||
malformed and unrelated records are excluded. Other agents retain their
|
||||
existing stderr behavior. Raw OpenCode lines are never persisted, included in
|
||||
browser payloads, or exposed through a general API for reading agent logs. The
|
||||
observable behavior is specified in
|
||||
[`docs/specs/agent-stall-recovery/spec.md`](../specs/agent-stall-recovery/spec.md).
|
||||
|
||||
## Consequences
|
||||
|
||||
Provider failures that an ACP bridge swallows can become immediate, truthful
|
||||
session failures across local, container, and remote executors without granting
|
||||
the central backend filesystem access. Existing lifecycle reconciliation,
|
||||
recovery messages, and provider-error classification remain the single path
|
||||
after agentctl validates the diagnostic.
|
||||
|
||||
Kandev takes on a small agent-compatibility parser for each supported stderr
|
||||
format. A vendor format change degrades safely to ACP plus advisory stall
|
||||
recovery rather than guessing. Enabling unsupported CLI diagnostic flags can
|
||||
affect startup, so the managed runtime contract and compatibility tests must
|
||||
cover those arguments.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Tail `~/.local/share/opencode/log/opencode.log`.** Rejected because it is a
|
||||
private, rotating, multi-session file containing unrelated paths, URLs, and
|
||||
identifiers, and the central backend cannot access every executor's home.
|
||||
- **Expose the recent stderr buffer to the browser.** Rejected because raw
|
||||
stderr is broader than the user-facing failure and may contain sensitive or
|
||||
unrelated diagnostics.
|
||||
- **Treat any OpenCode stderr error as terminal.** Rejected because title,
|
||||
background, stale, and concurrent-session work can emit unrelated failures.
|
||||
- **Rely only on the inactivity watchdog.** Rejected because it delays a known
|
||||
terminal failure and continues to describe a failed turn as running.
|
||||
- **Add a universal hard prompt timeout.** Rejected because quiet long-running
|
||||
work is legitimate and silence is not terminal evidence.
|
||||
- **Wait only for an upstream OpenCode ACP fix.** Rejected as the sole remedy;
|
||||
upstream conformance remains desirable, but Kandev can safely use diagnostics
|
||||
from the subprocess it already supervises.
|
||||
@@ -104,3 +104,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
|
||||
| 2026-08-01-share-artifact-locale | [Shared-Task Artifacts Render in the Creator's Locale](2026-08-01-share-artifact-locale.md) | accepted | backend | 2026-08-01 |
|
||||
| 2026-08-02-new-workspace-github-access-defaults | [Bootstrap New Workspaces From Host GitHub Access](2026-08-02-new-workspace-github-access-defaults.md) | accepted | backend, frontend, security | 2026-08-02 |
|
||||
| 2026-08-02-single-owner-agent-task-titles | [Assign Agent Task Titles to One Session](2026-08-02-single-owner-agent-task-titles.md) | accepted | backend, frontend, protocol, workflow | 2026-08-02 |
|
||||
| 2026-08-02-agent-terminal-diagnostics-over-stderr | [Capture Agent Terminal Diagnostics From Managed Stderr](2026-08-02-agent-terminal-diagnostics-over-stderr.md) | accepted | backend, frontend, protocol, security | 2026-08-02 |
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
spec: docs/specs/agent-stall-recovery/spec.md
|
||||
decision: docs/decisions/2026-08-02-agent-terminal-diagnostics-over-stderr.md
|
||||
created: 2026-08-02
|
||||
status: 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.ManagedNPMRuntime` in
|
||||
`apps/backend/internal/agent/agents/opencode_acp.go` so every managed OpenCode
|
||||
ACP entry point uses `acp --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`, and
|
||||
`apps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md`.
|
||||
|
||||
### Non-blocking stderr observation
|
||||
|
||||
- Add an optional `StderrLineConsumer` interface beside `StderrProvider` in
|
||||
`apps/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.readStderr` in
|
||||
`apps/backend/internal/agentctl/server/process/manager.go` to 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.go`
|
||||
with a narrow key/value parser for OpenCode's emitted error records. Accept a
|
||||
record only when `level=ERROR`, `message="stream error"`, `small` is not
|
||||
`true`, `session.id` is present, and `error.error` is 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.go` and attach it
|
||||
optionally to `streams.AgentEvent`. Fields are limited to `source`,
|
||||
`provider_id`, `model_id`, `message`, `occurred_at`, and `reset_at`.
|
||||
- Let the ACP adapter implement `StderrLineConsumer` only for
|
||||
`agentID == "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.go`
|
||||
so 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
|
||||
`handleWSPrompt` and `Manager.SendErrorEvent` in
|
||||
`apps/backend/internal/agentctl/server/api/agent.go` and
|
||||
`apps/backend/internal/agentctl/server/process/manager.go` so 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`, and `watcher.AgentEventData` in
|
||||
`apps/backend/internal/agent/runtime/lifecycle/types.go`,
|
||||
`apps/backend/internal/agent/runtime/lifecycle/event_types.go`,
|
||||
`apps/backend/internal/agent/runtime/lifecycle/events.go`, and
|
||||
`apps/backend/internal/orchestrator/watcher/watcher.go`. Also add `AgentID` to
|
||||
the lifecycle payload so downstream classification uses `opencode-acp`
|
||||
rather than inferring an agent from prose.
|
||||
- In `manager_events.go`, copy structured details from the winning generation's
|
||||
error event before `MarkCompleted` publishes `agent.failed`. Do not retain
|
||||
details from a stale generation or a later duplicate terminal event.
|
||||
- Extend `apps/backend/internal/agent/runtime/routingerr/rules.go` so OpenCode's
|
||||
exact `usage limit reached` signature classifies as high-confidence
|
||||
`quota_limited`. Preserve the existing sanitizer and ensure account/workspace
|
||||
URLs and long identifiers never enter `RawExcerpt`.
|
||||
- In `createRecoveryStatusMessage` in
|
||||
`apps/backend/internal/orchestrator/event_handlers_agent.go`, classify the
|
||||
failure using the event's `AgentID`, validate the structured provider record,
|
||||
and persist the existing recovery actions with metadata:
|
||||
`failure_kind: provider_quota_limited`, safe `provider_name`, optional
|
||||
`model_id`, optional RFC3339 `reset_at`, and bounded sanitized `error_output`.
|
||||
Fall back to the generic recoverable-error card when classification or
|
||||
structured correlation is absent.
|
||||
- Continue using the current `MarkCompleted` -> `agent.failed` ->
|
||||
`handleRecoverableFailure` path. Non-Office sessions become
|
||||
`WAITING_FOR_INPUT`; Office sessions keep their existing `FAILED` behavior.
|
||||
This feature does not invent a second lifecycle state or retry loop.
|
||||
|
||||
---
|
||||
|
||||
## Frontend
|
||||
|
||||
### Localized provider-limit recovery
|
||||
|
||||
- Extend `ActionMeta` / `MessageAction` metadata typing in
|
||||
`apps/web/components/task/chat/types.ts` and
|
||||
`apps/web/components/task/chat/messages/action-message.tsx` for the safe
|
||||
provider-limit fields.
|
||||
- Add a `ProviderQuotaRecovery` branch beside `MissingBranchRecovery`. It uses
|
||||
`failure_kind`, not string matching, to render a localized heading naming the
|
||||
model when available, localized reset guidance when `reset_at` is present,
|
||||
the existing recovery actions, and the existing collapsed **Technical
|
||||
details** disclosure for sanitized `error_output`.
|
||||
- Add all new copy to `apps/web/src/locales/en/chat.json` and
|
||||
`apps/web/src/locales/pseudo/chat.json`; resolve chat keys through
|
||||
`useTranslation` at 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:** `MissingBranchRecovery` supplies the localized
|
||||
alert-card/details composition, while
|
||||
`mobile-transient-retry.spec.ts` and
|
||||
`mobile-pause-resume-recovery.spec.ts` supply 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.go` or 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 holds `session/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 reached` becomes high-confidence `quota_limited`, raw
|
||||
excerpts are sanitized, and recovery metadata contains only safe model/reset
|
||||
fields plus existing actions.
|
||||
**Files:** `routingerr/classify_test.go` and
|
||||
`orchestrator/event_handlers_test.go` or 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`, and `pnpm run i18n:ratchet` passed.
|
||||
- Task 05: managed desktop and mobile Playwright runs passed after the E2E seed was corrected to include `completed_at`: one Chromium test and one `mobile-chrome` test 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:
|
||||
|
||||
- [x] [Task 01: Capture OpenCode error diagnostics](task-01-capture-opencode-error-diagnostics.md)
|
||||
|
||||
Wave 2:
|
||||
|
||||
- [x] [Task 02: Settle prompts from correlated diagnostics](task-02-settle-correlated-opencode-prompt.md)
|
||||
|
||||
Wave 3:
|
||||
|
||||
- [x] [Task 03: Classify and persist provider failures](task-03-classify-provider-failure.md)
|
||||
|
||||
Wave 4:
|
||||
|
||||
- [x] [Task 04: Render localized quota recovery](task-04-render-provider-quota-recovery.md)
|
||||
|
||||
Wave 5:
|
||||
|
||||
- [x] [Task 05: Prove desktop and mobile recovery](task-05-e2e-provider-quota-recovery.md)
|
||||
|
||||
## 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.
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
id: "01-capture-opencode-error-diagnostics"
|
||||
title: "Capture OpenCode error diagnostics"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/agent-stall-recovery/spec.md"
|
||||
---
|
||||
|
||||
# Task 01: Capture OpenCode error diagnostics
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Every managed OpenCode ACP command enables `--print-logs --log-level ERROR`,
|
||||
while other managed agent commands remain unchanged.
|
||||
- Agentctl delivers cleaned live stderr lines to an optional consumer without
|
||||
blocking child-process drainage or changing the bounded recent-stderr API.
|
||||
- For OpenCode, generic logs, recent-stderr, and nonzero-exit events contain
|
||||
only the parser's safe provider-message projection; unrecognized records are
|
||||
excluded so workspace URLs and identifiers cannot leak through the fallback
|
||||
error path.
|
||||
- The OpenCode parser accepts only foreground `stream error` records and
|
||||
returns allowlisted, URL-free provider/model/message/timestamp/reset fields;
|
||||
background, malformed, and unrelated records are rejected.
|
||||
|
||||
## Verification
|
||||
|
||||
- `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)'`
|
||||
|
||||
Use TDD: command-contract, stderr-delivery, and parser tests must fail before
|
||||
their production changes are added.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/agent/agents/opencode_acp.go`
|
||||
- `apps/backend/internal/agent/agents/opencode_acp_test.go`
|
||||
- `apps/backend/internal/agent/agents/managed_npm_runtime_test.go`
|
||||
- `apps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md`
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/manager_launch_test.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/adapter.go`
|
||||
- `apps/backend/internal/agentctl/server/process/manager.go`
|
||||
- `apps/backend/internal/agentctl/server/process/manager_test.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr_test.go`
|
||||
- `apps/backend/internal/agentctl/types/streams/provider_error.go`
|
||||
- `apps/backend/internal/agentctl/types/streams/agent.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Parallelism
|
||||
|
||||
Sequential. Task 02 consumes the parser, consumer, and safe provider-error
|
||||
contract introduced here.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Spec sections `Correlated terminal diagnostics`, `Diagnostic contract`, and
|
||||
diagnostic failure-mode scenarios
|
||||
- ADR-2026-08-02 stderr ownership, correlation, and sanitization rules
|
||||
- Existing `Manager.readStderr`, `StderrProvider`, `ManagedNPMRuntimeSpec`, and
|
||||
ACP adapter construction patterns
|
||||
|
||||
## Risks
|
||||
|
||||
- `readStderr` is a process-liveness boundary; consumer delivery must remain
|
||||
non-blocking, including when the bounded provider-diagnostic channel is full.
|
||||
- The parser must not preserve the OpenCode workspace URL or identifiers found
|
||||
in the observed `error.error` suffix.
|
||||
- The generic process-exit path must consume only the safe OpenCode projection;
|
||||
raw stderr must never appear in logs, the recent-stderr ring, or exit events.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report RED assertions, exact argv, normalized field allowlist, rejected record
|
||||
cases, test results, files changed, blockers, and risks. Mark this task `done`
|
||||
and update its plan checkbox in the same conversation.
|
||||
|
||||
## Results
|
||||
|
||||
The managed command contract, optional stderr consumer, parser privacy,
|
||||
bounded-channel, and nonzero-exit projection cases all have focused
|
||||
regressions. OpenCode now uses `acp --print-logs --log-level ERROR`; the parser
|
||||
keeps only the validated foreground
|
||||
session/provider/model/message/timestamp/reset fields and rejects title,
|
||||
`small=true`, malformed, unrelated, or incomplete records. The focused
|
||||
verification passed with 26 tests across three packages; the full backend
|
||||
suite also passed.
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
---
|
||||
id: "02-settle-correlated-opencode-prompt"
|
||||
title: "Settle correlated OpenCode prompts"
|
||||
status: done
|
||||
wave: 2
|
||||
depends_on: ["01-capture-opencode-error-diagnostics"]
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/agent-stall-recovery/spec.md"
|
||||
---
|
||||
|
||||
# Task 02: Settle Prompts From Correlated Diagnostics
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A validated OpenCode diagnostic settles only the pending foreground prompt
|
||||
whose active ACP session ID matches the diagnostic.
|
||||
- Diagnostic, ACP response, and user-cancel races emit exactly one terminal
|
||||
result for the owning prompt generation and release the losing operation.
|
||||
- The resulting agent error event carries the safe provider-error record;
|
||||
wrong-session, background, stale, post-settlement, and non-OpenCode lines do
|
||||
not affect the prompt.
|
||||
|
||||
## Verification
|
||||
|
||||
- `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)'`
|
||||
|
||||
Use a fake ACP agent that intentionally holds `session/prompt`; do not call a
|
||||
real provider or depend on wall-clock stall detection.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/adapter.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt_cancel.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr_test.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt_test.go`
|
||||
- `apps/backend/internal/agentctl/server/api/agent.go`
|
||||
- `apps/backend/internal/agentctl/server/api/agent_test.go`
|
||||
- `apps/backend/internal/agentctl/server/process/manager.go`
|
||||
- `apps/backend/internal/agentctl/server/process/manager_test.go`
|
||||
- `apps/backend/internal/agentctl/types/streams/provider_error.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 01.
|
||||
|
||||
## Parallelism
|
||||
|
||||
Sequential. Task 03 consumes the structured terminal event created here.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Spec scenarios for active, wrong-session, background, stale, and racing
|
||||
diagnostics
|
||||
- Plan section `OpenCode diagnostic normalization and prompt correlation`
|
||||
- Existing prompt-turn gate, generation claim, cancel acknowledgement, async
|
||||
`handleWSPrompt`, and `SendErrorEvent` paths
|
||||
|
||||
## Risks
|
||||
|
||||
- A losing `conn.Prompt` goroutine must receive cancellation and cannot retain a
|
||||
prompt gate, trace span, or later error delivery.
|
||||
- Stderr delivery cannot block on the prompt consumer channel.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report RED race assertions, correlation rules, single-terminal proof, exact
|
||||
tests, files changed, blockers, and risks. Mark this task `done` and update its
|
||||
plan checkbox in the same conversation.
|
||||
|
||||
## Results
|
||||
|
||||
The ACP adapter correlates diagnostics to the active ACP session and prompt
|
||||
turn, cancels the held RPC through its context cause, and forwards one typed
|
||||
provider error through the existing async prompt event. Wrong-session and
|
||||
post-settlement diagnostics are ignored; duplicate lifecycle terminal events
|
||||
are guarded by prompt generation. The exact verification command passed with
|
||||
3 tests across three packages.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
id: "03-classify-provider-failure"
|
||||
title: "Classify and persist provider failures"
|
||||
status: done
|
||||
wave: 3
|
||||
depends_on: ["02-settle-correlated-opencode-prompt"]
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/agent-stall-recovery/spec.md"
|
||||
---
|
||||
|
||||
# Task 03: Classify and Persist Provider Failures
|
||||
|
||||
## Acceptance
|
||||
|
||||
- The winning generation's safe provider-error record and concrete agent ID
|
||||
reach `agent.failed`; stale or duplicate terminal events cannot replace them.
|
||||
- OpenCode `usage limit reached` classifies as high-confidence
|
||||
`quota_limited`, with the existing sanitizer removing URLs and long
|
||||
identifiers from every persisted excerpt.
|
||||
- Non-Office recovery messages persist `provider_quota_limited` metadata with
|
||||
only safe provider/model/reset/details fields and existing recovery actions;
|
||||
absent or unrecognized details use the generic error card.
|
||||
|
||||
## Verification
|
||||
|
||||
- `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)'`
|
||||
|
||||
Use TDD at the lifecycle event, classifier, watcher, and persisted-message
|
||||
boundaries. Assert the observed OpenCode workspace URL and identifier are
|
||||
absent from error messages, raw excerpts, and message metadata.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/types.go`
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/event_types.go`
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/events.go`
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/manager_events.go`
|
||||
- `apps/backend/internal/agent/runtime/lifecycle/manager_events_test.go`
|
||||
- `apps/backend/internal/agent/runtime/routingerr/rules.go`
|
||||
- `apps/backend/internal/agent/runtime/routingerr/classify_test.go`
|
||||
- `apps/backend/internal/orchestrator/watcher/watcher.go`
|
||||
- `apps/backend/internal/orchestrator/watcher/watcher_test.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_agent.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_test.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 02.
|
||||
|
||||
## Parallelism
|
||||
|
||||
Sequential. Task 04 consumes the persisted metadata contract defined here.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Spec `Diagnostic contract`, persistence guarantees, and quota scenarios
|
||||
- Plan sections `Lifecycle propagation and quota classification` and Tests
|
||||
- Existing `MarkCompleted`, `newAgentEventPayload`, `routingerr.Classify`,
|
||||
`handleRecoverableFailure`, and `createRecoveryStatusMessage` paths
|
||||
|
||||
## Risks
|
||||
|
||||
- Provider classification must use the concrete `opencode-acp` agent ID and
|
||||
must not infer trust from arbitrary error prose.
|
||||
- Raw recent stderr remains outside the persisted safe record.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report RED assertions, event and metadata shapes, classification rule and
|
||||
confidence, sanitization proof, exact tests, files changed, blockers, and
|
||||
risks. Mark this task `done` and update its plan checkbox in the same
|
||||
conversation.
|
||||
|
||||
## Results
|
||||
|
||||
The safe provider record and concrete `opencode-acp` ID now reach
|
||||
`agent.failed`. The exact five-hour usage-limit signature is high-confidence
|
||||
`quota_limited`; URL/session/workspace identifiers are redacted before
|
||||
persistence. Non-Office recovery metadata includes only the specialized
|
||||
failure kind, provider/model/reset values, sanitized details, and existing
|
||||
actions. The exact verification command passed with 13 tests across four
|
||||
packages.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
id: "04-render-provider-quota-recovery"
|
||||
title: "Render localized quota recovery"
|
||||
status: done
|
||||
wave: 4
|
||||
depends_on: ["03-classify-provider-failure"]
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/agent-stall-recovery/spec.md"
|
||||
---
|
||||
|
||||
# Task 04: Render Localized Quota Recovery
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `provider_quota_limited` metadata renders localized model/reset guidance,
|
||||
existing recovery actions, and sanitized technical details collapsed by
|
||||
default; no raw OpenCode URL or identifier is displayed.
|
||||
- Unknown provider failures retain the generic settled-error card, and running
|
||||
inactivity notices retain their neutral compact behavior.
|
||||
- Desktop and phone use the same logic; phone actions remain at least 44px,
|
||||
expanded details stay within the chat width, and no workflow depends on
|
||||
hover.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd apps && pnpm install --frozen-lockfile`
|
||||
- `cd apps && pnpm --filter @kandev/web test -- --run components/task/chat/messages/action-message.test.tsx`
|
||||
- `cd apps/web && pnpm run typecheck`
|
||||
|
||||
Use TDD for provider metadata, missing model/reset fallbacks, details
|
||||
disclosure, generic-error preservation, and pseudo-locale rendering.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/task/chat/types.ts`
|
||||
- `apps/web/components/task/chat/messages/action-message.tsx`
|
||||
- `apps/web/components/task/chat/messages/action-message.test.tsx`
|
||||
- `apps/web/src/locales/en/chat.json`
|
||||
- `apps/web/src/locales/pseudo/chat.json`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 03.
|
||||
|
||||
## Parallelism
|
||||
|
||||
Sequential. Task 05 validates this presentation and its backend metadata.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Spec provider-limit desktop/mobile scenarios
|
||||
- Plan `Localized provider-limit recovery` and mobile design contract
|
||||
- Existing `MissingBranchRecovery`, `TechnicalDetails`, `ActionButtons`, and
|
||||
`useTranslation("chat")` patterns
|
||||
|
||||
## Risks
|
||||
|
||||
- Reset timestamps must be formatted at render time and must not call `t()` at
|
||||
module scope.
|
||||
- The safe technical string is display-only; control flow must use
|
||||
`failure_kind` rather than translated copy or substring matching.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report RED assertions, translation keys, desktop/mobile hierarchy, technical
|
||||
details behavior, exact test/typecheck results, files changed, blockers, and
|
||||
risks. Mark this task `done` and update its plan checkbox in the same
|
||||
conversation.
|
||||
|
||||
## Results
|
||||
|
||||
Added the localized `ProviderQuotaRecovery` inline card with model/reset
|
||||
fallbacks, collapsed technical details, existing recovery actions, and shared
|
||||
desktop/mobile layout. Unknown failures and running stall notices retain their
|
||||
existing renderers. The focused component suite passed all 17 tests; web
|
||||
typecheck, i18n key/pseudo checks, and the new-code ratchet passed.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
id: "05-e2e-provider-quota-recovery"
|
||||
title: "Prove desktop and mobile quota recovery"
|
||||
status: done
|
||||
wave: 5
|
||||
depends_on:
|
||||
- "03-classify-provider-failure"
|
||||
- "04-render-provider-quota-recovery"
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/agent-stall-recovery/spec.md"
|
||||
---
|
||||
|
||||
# Task 05: Prove Desktop and Mobile Recovery
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Desktop Playwright proves a settled OpenCode quota failure replaces the
|
||||
running status with model/reset guidance, keeps technical details collapsed,
|
||||
and reveals no workspace URL or identifier when expanded.
|
||||
- Mobile Playwright proves the same information and recovery actions are
|
||||
reachable by touch, actions are at least 44px, expanded details remain
|
||||
viewport-contained, and the document has no horizontal overflow.
|
||||
- Tests use E2E-only session/message seeding and never exhaust real provider
|
||||
quota, read a developer's OpenCode log, or add a production test hook.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd apps && pnpm install --frozen-lockfile`
|
||||
- `cd apps/web && pnpm e2e:run --no-build tests/session/provider-quota-recovery.spec.ts`
|
||||
- `cd apps/web && pnpm e2e:run --no-build --project mobile-chrome tests/session/mobile-provider-quota-recovery.spec.ts`
|
||||
|
||||
Run against freshly built backend and Vite artifacts through the managed E2E
|
||||
runner. Inspect the rendered phone viewport in addition to assertions.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/e2e/pages/session-page.ts`
|
||||
- `apps/web/e2e/tests/session/provider-quota-recovery.spec.ts`
|
||||
- `apps/web/e2e/tests/session/mobile-provider-quota-recovery.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Tasks 03 and 04.
|
||||
|
||||
## Parallelism
|
||||
|
||||
Sequential. This is the integrated user-visible proof for the completed
|
||||
backend and frontend contracts.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Spec desktop/mobile quota scenarios and privacy guarantees
|
||||
- Plan E2E section and mobile design contract
|
||||
- Existing `seedTaskSession`, `seedSessionMessage`, missing-branch technical
|
||||
details, transient-retry, and mobile pause/recovery patterns
|
||||
|
||||
## Risks
|
||||
|
||||
- Seed only the safe normalized metadata contract; do not copy the real
|
||||
OpenCode workspace URL or account identifier into fixtures.
|
||||
- A passing seeded UI test does not replace Task 02's real prompt-boundary
|
||||
integration regression.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report RED failures, exact desktop/mobile results, rendered mobile inspection,
|
||||
privacy assertions, files changed, blockers, risks, and teardown evidence.
|
||||
Mark this task `done`, update its plan checkbox, and synchronize the plan's
|
||||
verification results in the same conversation.
|
||||
|
||||
## Results
|
||||
|
||||
Added E2E-only seeded desktop and mobile specs. Both managed production-build
|
||||
runs passed: Chromium verifies the localized card, collapsed/expanded privacy
|
||||
details, and recovery actions; `mobile-chrome` verifies touch disclosure,
|
||||
44px actions, viewport containment, and no horizontal overflow. The first run
|
||||
found and fixed a fixture requirement for `completed_at`; no provider quota or
|
||||
developer log was used.
|
||||
+1
-1
@@ -179,7 +179,7 @@ System pages (Radarr/Sonarr-style) for status, disk usage, database maintenance,
|
||||
| Spec | Status |
|
||||
|---|---|
|
||||
| [agent-resume-runtime-recovery](agent-resume-runtime-recovery/spec.md) | shipped |
|
||||
| [agent-stall-recovery](agent-stall-recovery/spec.md) | draft |
|
||||
| [agent-stall-recovery](agent-stall-recovery/spec.md) | approved |
|
||||
| [mcp-session-observability](mcp-session-observability/spec.md) | approved |
|
||||
| [auth](auth/spec.md) | building |
|
||||
| [create-local-repository](create-local-repository/spec.md) | shipped |
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
---
|
||||
status: draft
|
||||
status: approved
|
||||
created: 2026-07-29
|
||||
updated: 2026-08-02
|
||||
owner: Kandev
|
||||
---
|
||||
|
||||
# Agent Stall Recovery
|
||||
|
||||
Decision:
|
||||
[ADR-2026-07-29-agent-stall-user-controlled-recovery](../../decisions/2026-07-29-agent-stall-user-controlled-recovery.md)
|
||||
Decisions:
|
||||
|
||||
- [ADR-2026-07-29-agent-stall-user-controlled-recovery](../../decisions/2026-07-29-agent-stall-user-controlled-recovery.md)
|
||||
- [ADR-2026-08-02-agent-terminal-diagnostics-over-stderr](../../decisions/2026-08-02-agent-terminal-diagnostics-over-stderr.md)
|
||||
|
||||
Implementation plans:
|
||||
|
||||
- [Agent stall recovery](../../plans/agent-stall-recovery/plan.md)
|
||||
- [OpenCode terminal error surfacing](../../plans/opencode-terminal-error-surfacing/plan.md)
|
||||
|
||||
## Why
|
||||
|
||||
An agent turn can remain `RUNNING` forever after its active tool stops emitting
|
||||
events. Users currently see only “Agent is running,” while Resume is correctly
|
||||
blocked by the active-session guard and the backend repeats a diagnostic that
|
||||
the UI never receives.
|
||||
|
||||
## Broken behavior
|
||||
|
||||
`waitForPromptDone` detects five minutes without events but only logs the
|
||||
condition every 30 seconds. A top-level shell tool that remains `in_progress`
|
||||
can therefore hold the prompt waiter and session state indefinitely even
|
||||
though Kandev already has a bounded cancel-escalation path capable of releasing
|
||||
the turn.
|
||||
An agent turn can remain `RUNNING` after it stops emitting events. Silence alone
|
||||
is ambiguous because a legitimate long-running tool may also be quiet, but some
|
||||
agents know that their provider stream has failed and fail to close the ACP
|
||||
prompt. Users need Kandev to distinguish trustworthy terminal evidence from
|
||||
mere inactivity so a failed turn is not presented as healthy work.
|
||||
|
||||
## What
|
||||
|
||||
### Advisory inactivity recovery
|
||||
|
||||
- Kandev checks running prompts for inactivity once per 60 seconds.
|
||||
- After five minutes without agent events, Kandev creates at most one
|
||||
user-visible notice for that prompt generation.
|
||||
@@ -46,16 +49,80 @@ the turn.
|
||||
- The backend logs the first stall detected for a prompt generation and does
|
||||
not emit another notice or log entry on every watchdog check.
|
||||
|
||||
### Correlated terminal diagnostics
|
||||
|
||||
- When an agent emits a terminal provider diagnostic outside ACP, Kandev acts
|
||||
on it only when the diagnostic identifies the current agent type, active
|
||||
provider session, and active foreground prompt.
|
||||
- OpenCode provider diagnostics are collected from the managed `opencode acp`
|
||||
process's error-only stderr stream. Kandev does not read OpenCode's private
|
||||
files under `~/.local/share/opencode/log`.
|
||||
- A correlated OpenCode `stream error` ends the affected prompt through the
|
||||
existing agent-error lifecycle instead of waiting for the inactivity
|
||||
threshold. The session stops presenting itself as `RUNNING` and retains the
|
||||
existing recovery actions.
|
||||
- Kandev preserves only allowlisted diagnostic fields needed for recovery:
|
||||
source agent, provider, model, sanitized message, occurrence time, and a
|
||||
reset time when one can be derived. Raw log lines, workspace URLs, account or
|
||||
workspace identifiers, paths, credentials, and unrelated stderr are not
|
||||
persisted or sent to the browser.
|
||||
- A recognized OpenCode usage-limit message is classified as
|
||||
`quota_limited`. Its recovery surface names the affected model when known,
|
||||
explains when capacity resets, and keeps sanitized technical details
|
||||
collapsed by default.
|
||||
- The user-facing provider-limit copy is localized. Desktop and phone layouts
|
||||
expose the same failure explanation, technical details, and recovery actions.
|
||||
- A diagnostic for a background/title request, another OpenCode session, an
|
||||
earlier prompt generation, or a prompt that has already settled cannot fail
|
||||
the active turn.
|
||||
- An unrecognized, malformed, or unavailable diagnostic stream does not become
|
||||
proof of failure. The prompt continues and the advisory inactivity recovery
|
||||
remains available.
|
||||
|
||||
## Diagnostic contract
|
||||
|
||||
The internal normalized provider diagnostic contains only:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `source` | Stable diagnostic source identifier, initially `opencode_stderr` |
|
||||
| `provider_id` | Provider identifier reported by the agent, when present |
|
||||
| `model_id` | Model identifier reported by the agent, when present |
|
||||
| `message` | Sanitized provider message with URLs and identifiers removed |
|
||||
| `occurred_at` | Parsed diagnostic timestamp |
|
||||
| `reset_at` | Derived by adding a relative reset duration to `occurred_at`; absolute reset timestamps are not accepted in this version |
|
||||
|
||||
The persisted recovery-message metadata uses
|
||||
`failure_kind: provider_quota_limited`, plus the safe model and reset fields,
|
||||
to select localized presentation. `error_output` may contain the same bounded,
|
||||
sanitized diagnostic message for the collapsed technical-details surface.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- If active tool identity is unavailable, the generic notice and cancel action
|
||||
still appear.
|
||||
- If active tool identity is unavailable, the generic inactivity notice and
|
||||
cancel action still appear.
|
||||
- If the agent acknowledges cancellation, normal cancellation reconciliation
|
||||
makes the session input-ready.
|
||||
- If the agent does not acknowledge cancellation, the existing bounded
|
||||
cancel-escalation path releases the prompt and reconciles the session.
|
||||
- If notice-message persistence fails, the failure is logged without changing
|
||||
or terminating the running session.
|
||||
- If OpenCode does not support error-only stderr emission, changes its log
|
||||
format, or emits a line that fails validation, Kandev ignores the line and
|
||||
retains normal ACP plus inactivity behavior.
|
||||
- If a validated terminal diagnostic races with an ACP prompt response, only
|
||||
one terminal event settles the prompt generation.
|
||||
- If sanitization removes all usable provider text, Kandev surfaces a generic
|
||||
OpenCode provider failure without exposing the raw diagnostic.
|
||||
|
||||
## Persistence guarantees
|
||||
|
||||
- Advisory stall notices retain their existing persisted-message behavior.
|
||||
- Sanitized provider failures use the existing session error and recovery
|
||||
message persistence. They survive reloads like other recoverable failures.
|
||||
- Raw agent stderr remains a bounded, executor-local in-memory diagnostic. It
|
||||
is not added to session metadata, messages, debug exports, or durable logs by
|
||||
this feature.
|
||||
|
||||
## Scenarios
|
||||
|
||||
@@ -81,13 +148,37 @@ the turn.
|
||||
- **GIVEN** a quiet but legitimate long-running turn, **WHEN** the inactivity
|
||||
threshold passes and the user does not cancel, **THEN** Kandev leaves the
|
||||
turn and process running.
|
||||
- **GIVEN** the active OpenCode prompt emits a `stream error` for its own ACP
|
||||
session, **WHEN** the diagnostic says the model's five-hour usage limit was
|
||||
reached, **THEN** Kandev ends the running state and shows a localized
|
||||
quota-limit recovery message with the model and reset time.
|
||||
- **GIVEN** that quota-limit recovery is shown, **WHEN** the user expands
|
||||
technical details, **THEN** the sanitized provider message is visible and no
|
||||
OpenCode workspace URL or identifier is present.
|
||||
- **GIVEN** OpenCode emits a title-generation error or an error for another
|
||||
session, **WHEN** a foreground prompt is active, **THEN** Kandev ignores that
|
||||
diagnostic and does not settle the foreground prompt.
|
||||
- **GIVEN** a correlated diagnostic and the ACP response arrive concurrently,
|
||||
**WHEN** the turn settles, **THEN** exactly one terminal outcome is emitted
|
||||
for that prompt generation.
|
||||
- **GIVEN** an OpenCode build without usable diagnostic stderr, **WHEN** its ACP
|
||||
prompt hangs without further events, **THEN** the five-minute advisory notice
|
||||
remains the recovery path.
|
||||
- **GIVEN** the provider-limit recovery renders on a phone viewport, **WHEN**
|
||||
the user reads details or selects a recovery action, **THEN** the same outcome
|
||||
is available through touch targets of at least 44px with no horizontal page
|
||||
overflow.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Automatically timing out, failing, cancelling, or killing quiet turns.
|
||||
- Automatically timing out, failing, cancelling, or killing a turn based only
|
||||
on inactivity.
|
||||
- Making the inactivity threshold user-configurable.
|
||||
- Provider-specific repair of shell commands that hold child-process streams
|
||||
open.
|
||||
- Treating event silence as proof that an agent process crashed.
|
||||
- Allowing Resume or direct prompt admission while the session remains
|
||||
`RUNNING`.
|
||||
- Reading, tailing, or exposing an agent vendor's private log files.
|
||||
- Treating arbitrary stderr text as trusted terminal evidence.
|
||||
- Persisting raw stderr or raw OpenCode log records.
|
||||
- Following or exposing OpenCode account/balance URLs from provider errors.
|
||||
- Automatically purchasing capacity, changing models, or scheduling a retry at
|
||||
the reset time.
|
||||
- Repairing OpenCode's ACP implementation upstream; OpenCode should still be
|
||||
encouraged to return the provider failure directly from `session/prompt`.
|
||||
|
||||
Reference in New Issue
Block a user