ACP v2 idle errors are discarded and failed turns are marked completed#21219

opened12d agobyAanesthetised

Description

When an ACP v2 agent accepts session/prompt and later reports an error through an idle state_update, T3 marks the turn as completed without showing the error.

This happened with acplause when OpenRouter rejected a request with HTTP 401. Fixing the API key restored normal responses, but T3 had hidden the authentication failure.

Environment

  • T3 Code Beta 0.1.0, macOS
  • ACP registry provider: acplause
  • Negotiated protocol version: 2
  • T3 source inspected: 9c16146

The relevant behavior was also confirmed in the installed app bundle.

Draft-version compatibility clarification

The two implementations use different ACP v2 draft revisions:

  • acplause pins 2.0.0-alpha.9 at upstream commit 3be9cfc8ea2dfe5a28973b461fd956b4045f24d3: pinned version.
  • At the inspected T3 commit, the schema generator pins schema-v2.0.0-alpha.3: generator source.

Both negotiate protocolVersion: 2, which does not distinguish these draft revisions. The newer contract reports failures after prompt insertion through an idle state update with stopReason: "error" and error details; T3's inspected idle schema has no error field.

This report therefore documents a compatibility gap when T3 interacts with an agent implementing the newer draft. The expected behavior and suggested tests below describe support for that newer contract. Maintainer triage should account for the draft-version mismatch when deciding whether to update the schema and handling or explicitly constrain supported revisions.

Steps to reproduce

  1. Configure acplause with an invalid OpenRouter API key.
  2. Open a session and select an available model.
  3. Send a prompt.
  4. Observe that T3 records the turn as completed without displaying the authentication error.

Protocol evidence

A separate diagnostic run of acplause using the same provider configuration accepted the prompt, then emitted this update:

json
1{
2 "sessionUpdate": "state_update",
3 "state": "idle",
4 "stopReason": "error",
5 "error": {
6 "code": -32000,
7 "message": "Missing Authentication header",
8 "data": {
9 "kind": "auth",
10 "status": 401
11 }
12 }
13}

This is the params.update payload of a session/update notification. It is structured protocol data, not an error inferred from assistant text.

Expected behavior

The turn is marked failed and the agent's error message is displayed.

Actual behavior

The turn is marked completed and the error message is lost.

Relevant code

All links below refer to the inspected T3 commit:

Suggested regression coverage

A mock ACP v2 peer can accept the prompt and then emit the update above, without real credentials or API calls.

Assert that:

  • The turn fails and retains the original error message.
  • stopReason: "error" without error details still fails with a fallback message.
  • Normal completion and cancellation retain their existing behavior.
  • The turn is finalized exactly once.

This mock regression test has not yet been implemented.

Related reports

  • #8293 and merged PR #14487 addressed a similar issue in Grok's provider-specific completion extension.
  • #20097 was closed because no structured ACP failure was demonstrated. This report concerns an explicit ACP v2 idle error.

Note

Grok responding on behalf of Julius.

Confirmed on main. This is a bug in our ACP v2 client, not an acplause extension.

The ACP v2 schema now defines error as a stop reason. An idle state_update with stopReason: "error" can carry an optional error field holding a JSON-RPC error object (ErrorStopReason), and the spec says that after insertion the session/prompt response stays successful, so the idle update is the only place the failure shows up. Upstream added this in agentclientprotocol/agent-client-protocol#2281 and it shipped in schema 2.0.0-alpha.8. The payload in this report matches that shape.

T3 still generates its v2 types from schema 2.0.0-alpha.3, which predates it:

  • packages/effect-acp/scripts/generate.ts pins schema-v2.0.0-alpha.3, and the idle variant in packages/effect-acp/src/_generated/schema.gen.ts has only stopReason, usage and _meta.
  • packages/effect-acp/src/client.ts resolves the prompt completion with only those three fields, so error is dropped.
  • packages/provider-acp/src/server/adapter.ts maps every stop reason other than cancelled to completed, so "error" finishes the turn as a success.

#14487 fixed the same symptom only for Grok's own completion extension, so it doesn't cover generic ACP agents. #20097 was a different case with no structured failure.

Fixing it means understanding the newer stop reason and mapping error to a failed turn that keeps error.message, with a fallback message when no details are sent. The mock peer test outlined in the report covers the right cases.

Reproduced on current main b5a226e49c: Linux web dev client with an isolated server and acplause v0.202610.0, OpenAI GPT-4.1 Mini, deliberately invalid OpenRouter key. A fresh thread and a provider-handoff thread both persisted the ACP turn as completed without displaying the error. A separate acplause stdio run accepted the prompt and emitted the reported idle error with HTTP 401. Sanitized runtime transcript. This is a protocol/error-reporting bug; no exploitable vulnerability is reported.

Fixed in #23218, mirrored as #23217. An accepted ACP v2 idle error now fails the turn and retains its message or a fallback. Existing command lifecycles remain unchanged. Focused registry and replay-fixture tests pass 31/31; two fresh reviews returned MERGE. Final proof audit. The PR is unmerged.

Synced from GitHub

This symptom is also occurring with Kimi Code 2.3.1 on Windows x64, using the local ACP command kimi acp, model kimi-code/k3.1-pro, in T3 Code 0.1.3-nightly.20261026.3726 (version read from the running server).

Although this issue is closed, #23218 is currently still open and unmerged. Could this issue remain open until the fix lands, and could the Kimi path be checked against it?

Observed behavior

An existing, previously working Kimi conversation encountered a TLS connection failure while refreshing OAuth credentials at https://auth.kimi.com/api/oauth/token. The current turn stopped. Two subsequent user messages each finished after approximately 18 seconds with no assistant response and no visible error. T3 persisted both runs as completed.

Correlated evidence

All timestamps below are UTC on 2026-10-26:

User promptT3 requestedAtT3 completedAtT3 statusKimi outcome
Short acknowledgement22:04:49.56922:05:08.171completedfailed
Follow-up asking why there was no response22:05:35.22322:05:53.372completedfailed

Kimi's own session agents/main/wire.jsonl contains:

json
1{"turnId":2,"outcome":"failed","errorMessage":"OAuthConnectionError: OAuth request to https://auth.kimi.com/api/oauth/token failed: fetch failed: Client network socket disconnected before secure TLS connection was established","type":"agent.turn.ended","time":1793052308148,"kind":"event"}

It also records turn.ended with reason: "failed", an error named OAuthConnectionError, and prompt.completed with reason: "failed". The next prompt has the same failure at 1793052353352.

By contrast, T3's t3_thread_read reported status: "completed" for both runs. Their timeline contained the user messages and empty checkpoints, with no assistant message or failure item. The thread had no active run or pending request.

These are Kimi's internal session events and T3's persisted run/timeline data; I did not capture the ACP stdio frames, so I cannot yet confirm that Kimi emits the exact idle state_update payload shown in the original report. This is additional evidence of the same user-visible symptom, rather than proof of an identical protocol cause.

Recovery and expected behavior

After connectivity recovered, a single constrained test in the same T3 thread and existing Kimi process successfully returned a short response at 22:10:23 UTC. No restart, re-login, or configuration change was needed.

T3 should expose the provider failure and mark the run failed, so the user can diagnose/retry it. A transient authentication-network failure should not appear as a successful empty response. Restoring connectivity resolved this occurrence, but did not address the missing error reporting.

No full conversation, credentials, or unrelated workspace contents are included here.

Synced from GitHub

Same symptom, different trigger: Kimi Code 2.3.3 via kimi acp, T3 Code server 0.1.2 (linux x64), model kimi-code/kimi-for-coding.

In our case the provider hit its 5-hour usage limit. Outside ACP, kimi -p prints the error clearly:

1error: failed to run prompt: provider.api_error: 429 You've reached your 5-hour usage limit.
2Your quota will reset when the current 5-hour window ends.

Through kimi acp, T3's session/prompt returns successfully in ~2-4 s, the turn is persisted as completed, and no turn items are produced (no reasoning, no assistant message) — the chat shows silence with no error. Reproduced on 3 consecutive prompts (01:45, 01:59, 02:38 UTC, 2026-10-29); the only earlier turn in the same session that did produce output was before the quota was exhausted.

Server-side statev2.sqlite confirms the affected runs persisted only user_message + checkpoint items with no provider items, and the ACP registry log shows each session/prompt as succeeded.

So this is a second trigger for the same failure class: plain API errors after prompt acceptance (429 usage limit), not only OAuth/TLS failures during credential refresh. Would be good if #23218 covers this path as well.