12 KiB
spec, decision, created, status
| spec | decision | created | status |
|---|---|---|---|
| docs/specs/agent-stall-recovery/spec.md | docs/decisions/2026-08-07-allowlisted-provider-action-links.md | 2026-08-07 | implemented |
Implementation Plan: OpenCode Actionable Error Links
Outcome
Carry an optional, validated OpenCode remediation URL from an ACP/managed diagnostic into Kandev's existing recoverable-error lifecycle, then render it as a localized external link in every recovery surface. The short error remains the primary copy; raw stderr and raw ACP error payloads remain excluded.
This plan is an incremental repair to the completed OpenCode terminal-error surfacing work. It does not change prompt correlation, retry behavior, session states, or recovery actions.
Mobile design contract
The recovery link is an inline addition to the existing recovery cards and notices; there is no new navigation or surface.
- Entry point: same chat/office surface on every viewport.
- Nearest shipped exemplar: the provider-quota recovery card
(
mobile-provider-quota-recovery.spec.ts); the link reuses its inline composition and 44px phone touch sizing (min-h-11belowsm,min-h-8above). - Presentation: inline text link next to the existing recovery actions — no drawer, dialog, or new route (content depth does not justify one).
- Scroll owner: unchanged chat panel; the link is
break-words-safe and adds no horizontal scroll. - Touch/focus: minimum 44px phone target, native anchor keyboard reachability,
target="_blank"+rel="noopener noreferrer". - Mobile Playwright:
mobile-provider-remediation-link.spec.tsproves the link, focus, 44px target, URL-free details, and zero document overflow.
Root-cause constraint
The reported OpenCode 1.18.5 ACP failure contains only the short message in Kandev's ACP error, logs, and managed stderr. OpenCode's TUI has additional provider-side context, including the workspace URL, but that context is not transported by the ACP service-failure response. No Kandev-only parser can display bytes that never crossed the managed process boundary.
The Kandev half therefore supports the optional field now and keeps a truthful
fallback. A separate upstream OpenCode change is required for the exact
1.18.5-style failure to produce a link: OpenCode must emit an optional
action_url in its ACP service-failure data or in its already-supported
structured error-only stderr record. The plan must not add a private-log tail or
raw-error side channel to work around that dependency.
Contract and security rules
- Add optional
remediation_urlto the normalized safe provider diagnostic and persist the same field inLastAgentErrorand recovery metadata. - Accept only
https://opencode.ai/workspace/<safe-workspace-id>/gowith an exact host, HTTPS scheme, no userinfo/query/fragment, bounded identifier, and bounded total length. - Extract the URL before sanitizing the provider message; the message must remain URL-free and identifier-redacted.
- For a future ACP
RequestError, read only the structuredaction_urland safe message fields; never derive a browser link by copyingerr.Error(). - Keep the URL out of generic logs, recent-stderr projections, raw ACP payloads,
and technical text. Render it as a separate link with
noopener noreferrer. - If validation or transport fails, preserve the current generic recovery card and current short message.
Backend work
1. Normalize transport-provided action URLs
Extend the existing OpenCode stderr parser and ACP prompt-error projection with one shared allowlist validator. Structured stderr extracts a valid URL from the provider error before sanitization; future ACP service-failure data accepts only the explicit structured field. The provider diagnostic remains bounded and optional, and all existing session/generation correlation rules stay intact.
2. Persist and publish the safe field
Extend LastAgentError with the validated URL and add the URL to generic
recovery-message metadata independently of quota classification. Reuse the
existing session metadata and session.state_changed snapshot so reloads,
Kanban banners, and Office failed-session rows see the same value. Do not change
the session's plain error_message contract.
3. Preserve existing classification behavior
Keep quota classification and error_output URL-free. A provider diagnostic may
carry a remediation URL even when it is not quota-classified, so the generic
recoverable card can still show the link. Tests must prove that URL-bearing
metadata never appears for invalid or untrusted inputs.
Frontend work
4. Render one shared, mobile-safe link affordance
Add defense-in-depth URL validation at the browser boundary and a small shared localized link component. Use it from:
- Kanban
ActionMessageDetailsand the persistent last-agent-error notice; - Office
RunErrorEntry, including the livesession.state_changedmetadata projection.
The link is separate from the collapsed technical text, opens in a new tab with
rel="noopener noreferrer", and has a minimum 44px touch target on phones.
The existing recovery buttons, error copy, and chat scroll ownership remain
unchanged. New labels go through the existing task/chat translation files
and their pseudo-locale counterparts.
Tests and verification
Backend focused tests
- Parser accepts the observed OpenCode workspace route, stores it only in
remediation_url, and keeps the URL/workspace ID out ofmessageand sanitized stderr. - Parser rejects wrong hosts, schemes, paths, query/fragment components, malformed IDs, overlong values, and unrelated URLs.
- ACP structured error data with
action_urlfollows the same allowlist; absent or malformed data keeps the current generic error path. - Lifecycle and orchestrator tests prove the field survives only through the
winning error event, is persisted in
LastAgentError/recovery metadata, and is absent for invalid input.
Frontend unit tests
ActionMessagerenders a validated link for generic and quota recovery while preserving collapsed technical details and existing actions.readLastAgentErrorand the persistent notice retain/render the safe field.- Office session mapping and
buildRunErrorsFromSessionspreserve the field;RunErrorEntryrenders the same link and remains safe for invalid metadata. - English and pseudo-locale checks pass; no hardcoded user-facing copy is added.
E2E tests
Add seeded desktop and mobile-chrome scenarios that inject only the safe
remediation_url metadata. Verify the link destination, no URL in the
sanitized message, keyboard/touch reachability, 44px controls, and no
horizontal overflow. Add a no-link case for the current short ACP failure.
These tests must not invoke a real provider, exhaust quota, or read an
OpenCode log.
Targeted commands:
cd apps/backend && go test ./internal/agentctl/server/adapter/transport/acp ./internal/agentctl/server/api ./internal/agent/runtime/lifecycle ./internal/agent/runtime/routingerr ./internal/orchestrator -run 'Test(OpenCode|ProviderError|RecoverableFailure|RecoveryStatus)' -count=1
cd apps && pnpm --filter @kandev/web test -- --run components/task/chat/messages/action-message.test.tsx lib/session-last-agent-error.test.ts components/task/simple/chat-entries.test.ts lib/remediation-url.test.ts
cd apps/web && pnpm run typecheck && pnpm run i18n:check && pnpm run i18n:ratchet
cd apps/web && pnpm e2e:run --no-build tests/session/provider-remediation-link.spec.ts
cd apps/web && pnpm e2e:run --no-build --project mobile-chrome tests/session/mobile-provider-remediation-link.spec.ts
cd apps/web && pnpm e2e:run --no-build tests/office/provider-remediation-link.spec.ts
Likely files
Backend:
apps/backend/internal/agentctl/types/streams/provider_error.goapps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr.goapps/backend/internal/agentctl/server/adapter/transport/acp/opencode_stderr_test.goapps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.goapps/backend/internal/agentctl/server/api/agent.goapps/backend/internal/agentctl/server/api/agent_test.goapps/backend/internal/task/models/models.goapps/backend/internal/orchestrator/event_handlers_agent.goapps/backend/internal/orchestrator/event_handlers_test.goapps/backend/internal/orchestrator/recovery_actions_test.go
Frontend:
apps/web/lib/session-last-agent-error.tsapps/web/components/task/chat/messages/action-message.tsxapps/web/components/task/chat/messages/action-message.test.tsxapps/web/components/task/chat/message-list-shared.tsxapps/web/app/office/tasks/[id]/page.tsxapps/web/app/office/tasks/[id]/types.tsapps/web/components/task/simple/chat-entries.tsapps/web/components/task/simple/components/run-error-entry.tsxapps/web/src/locales/en/task.jsonapps/web/src/locales/en/chat.json- matching pseudo-locale files and focused tests
Sequencing and handoff
The work is sequential because the frontend consumes the persisted metadata contract. There is no safe parallel implementation wave:
- Normalize and test the transport contract.
- Persist/publish it through the existing lifecycle.
- Render both recovery surfaces and live Office updates.
- Run focused unit, backend, i18n, desktop, and mobile verification.
The external OpenCode emission change is a prerequisite for live URL coverage of the reported 1.18.5 error, but not for Kandev's parser, persistence, or seeded UI tests. If upstream does not emit the field, Kandev must retain the short-error fallback and report that limitation rather than exposing private logs.
Verification Results
Implemented 2026-08-07.
- Backend allowlist validator
NormalizeOpenCodeActionURLinapps/backend/internal/agentctl/server/adapter/transport/acp/opencode_remediation.go, shared by the stderr parser (inline URL oraction_urlfield) and the structured ACPRequestErrorprojection.ProviderError.RemediationURL,LastAgentError.RemediationURL, recovery-messageremediation_url, and thetask_session.error_changedevent all carry the field;session.state_changedcarries it via the persistedsession_metadata. - Frontend:
lib/remediation-url.tsbrowser-edge validation, sharedcomponents/task/remediation-link.tsx(target="_blank",rel="noopener noreferrer", min-h-11 touch target), wired intoActionMessageDetails(generic + quota cards), the persistentLastAgentErrorNotice, and OfficeRunErrorEntry(initial API data + livesession.state_changedmetadata merge). - Generic logs: the
session.state_changedmetadata debug echo redactsremediation_url(redactRemediationURLintask_notifications.go). - Verification runs (all green):
cd apps/backend && go test ./internal/agentctl/server/adapter/transport/acp ./internal/agentctl/server/api ./internal/agent/runtime/lifecycle ./internal/agent/runtime/routingerr ./internal/orchestrator -run 'Test(OpenCode|ProviderError|RecoverableFailure|RecoveryStatus)' -count=1— passed.- Full touched-package backend suites (acp, streams, task/models, orchestrator, lifecycle, agentctl api/process, gateway/websocket) — passed.
golangci-lint run ./... --new-from-rev=<base> --timeout=5m— no issues.pnpm --filter @kandev/web test -- --run lib/remediation-url.test.ts lib/session-last-agent-error.test.ts components/task/chat/messages/action-message.test.tsx components/task/chat/message-list-shared.test.tsx components/task/simple/chat-entries.test.ts— 106 passed.- Full web unit suite — 1236 files / 9669 tests passed.
pnpm run typecheck,pnpm run i18n:check,pnpm run i18n:ratchet— all clean (pseudo locale regenerated).- Desktop E2E
tests/session/provider-remediation-link.spec.ts— 3 passed (link render + keyboard focus, short-error fallback, last-agent-error notice). - Mobile E2E
tests/session/mobile-provider-remediation-link.spec.ts(mobile-chrome) — 1 passed (44px target, focus, overflow). - Office E2E
tests/office/provider-remediation-link.spec.ts— 2 passed (link on RunErrorEntry, no-link for invalid/absent metadata).
External dependency stands: OpenCode must emit action_url (ACP structured
error data or structured stderr) for the live 1.18.5-style failure to show the
TUI workspace URL; Kandev keeps the short-error fallback until then.