Skip to content

fix(acp): assistant message fragmentation on background tool updates & leaked harness telemetry #13133

Description

@adeebahmad01

Description

When using ACP-backed providers (such as Antigravity) with asynchronous background tools, two interrelated issues corrupt assistant message formatting and clutter thread history:

  1. Abrupt Assistant Segment Severing on Background Tool Updates:
    In AcpSessionRuntime.ts, closeActiveAssistantSegment is invoked unconditionally upon receiving any ToolCallUpdated event:

    if (event._tag === "ToolCallUpdated") {
      yield* closeActiveAssistantSegment({
        queue,
        assistantSegmentRef,
      });

    If a background command finishes while the assistant is actively streaming a response (such as a markdown table, code block, or link), the active segment is severed mid-stream. The remainder of the response is placed in a new segment/bubble without the preceding context, breaking Markdown formatting (e.g. unclosed links [text](http://... cut off before closing parenthesis, and tables split mid-cell where succeeding rows become raw pipe-separated text | row | data |).

  2. Harness Telemetry Parroting (<SYSTEM_MESSAGE>) & Ghost Segments:
    When background tasks complete, reactive system notifications are delivered into the provider context:

    The following is a <SYSTEM_MESSAGE> not actually sent by the user. It is provided by the system as important information to pay attention to.
    <SYSTEM_MESSAGE>
    [Message] timestamp=... sender=.../task-170 priority=... content=Task id finished with result: exit code 0
    </SYSTEM_MESSAGE>
    

    Certain models (e.g. gemini-3.8-flash-high) occasionally parrot this telemetry into their response deltas. Because each tool invocation or update resets the segment lifecycle, this results in an explosion of separate chat bubbles (e.g. 40+ bubbles in a single turn) containing only raw <SYSTEM_MESSAGE> XML boilerplate.

  3. Double Message ID Prefix Bug:
    In ProviderRuntimeIngestion.ts, assistantSegmentMessageId prepends assistant: to baseKey. Since ACP's itemId already begins with assistant:, this results in duplicated prefixes in the database (assistant:assistant:c19d...).


Reproduction Example

Send the following prompt in a thread with an ACP provider supporting background execution:

First, start a background command with run_command and WaitMsBeforeAsync=500:
`sleep 3 && echo "Background task finished successfully"`

Immediately after starting that command (without waiting for it), stream a 50-row Markdown table with columns | Index | Name | Status | Timestamp |. 
Keep writing the rows continuously so that the background task finishes while you are actively streaming the middle of the table.

Actual Behavior:

  1. The assistant begins streaming the table in Bubble 1.
  2. When sleep 3 finishes, ToolCallUpdated is fired.
  3. closeActiveAssistantSegment cuts Bubble 1 off mid-table.
  4. Bubble 2 begins with the remaining rows, but without table headers. The markdown parser fails to render a table, displaying raw | characters and broken elements.
  5. In turns with heavy background tasking, echoed <SYSTEM_MESSAGE> notifications spam the chat UI as individual bubbles.

Expected Behavior:

  • Background tool completions that do not require tool call insertion should not sever an active assistant streaming segment mid-token or mid-block.
  • Provider streaming sanitizers should suppress leaked harness telemetry <SYSTEM_MESSAGE>...</SYSTEM_MESSAGE>.
  • Empty or whitespace-only assistant segments should not be projected as empty bubbles.
  • Message IDs should be cleanly normalized without double assistant:assistant: prefixes.

Activity

  1. juliusmarminge commented on Sep 22, 2026

    @juliusmarminge
    Member

    Triage

    Partly confirmed on current main. One of the three claims is a real bug in the shared ACP session runtime. The system-message leak is already tracked. The double assistant: id is real and harmless.

    There is no version, no provider event log, and no screenshot on this report. The repro is a prompt the model has to follow, not a captured session.

    Segment close

    handleSessionUpdate in apps/server/src/provider/acp/AcpSessionRuntime.ts closes the active assistant segment on every parsed ToolCallUpdated, and it does that before decideToolCallUpdateEmission.

    Both ACP tool_call and tool_call_update are parsed as ToolCallUpdated (parseSessionUpdateEvent in AcpRuntimeModel.ts). A progress notification that the coalesce logic then drops (emit: false) has already ended the bubble. The next agent_message_chunk opens a new item. Nothing is inserted between the two bubbles, because the tool event was not emitted.

    Closing on the initial tool_call is intentional. AcpJsonRpcConnection.test.ts ("segments assistant text around ACP tool calls") expects text, then the tool, then a new assistant item. That is how a tool card lands between two answers.

    A later tool_call_update is different. A background command that reaches completed (or any coalesced stdout tick) while the same answer is still streaming cuts that answer in two. A markdown table or link split at that boundary will not render. This is shared ACP runtime code, so Cursor, Grok, and Antigravity all do it. It has been the close-on-ToolCallUpdated behavior since the ACP runtime landed; the coalesce check was added in front of the queue write and the close was left ahead of it.

    Whitespace-only deltas do not open a segment when none is active. Empty bubbles are not what this path creates. The bubbles are the two halves of the text.

    System-message text

    This repo does not generate <SYSTEM_MESSAGE> or <system_message>. Whatever text the ACP agent puts in agent_message_chunk is stored as assistant text. There is no sanitizer.

    That leak is #11432: Antigravity, Gemini 3.8 Flash, with screenshots. The payload there is lowercase <system_message> Background task … has completed. Task exit code: …, delivered as assistant text, on v0.0.41-nightly.20260912.1599. #13074 is an open, unmerged attempt to strip that form. It does not change segment closing.

    The wrapper in this report ("The following is a <SYSTEM_MESSAGE> not actually sent by the user…") is a different string. Nothing in-tree emits it, and this report does not show a trace of the model echoing it. If that text does arrive as content deltas, the close above can put each piece in its own bubble. The 40-bubble count is not evidenced here.

    Message ids

    ACP item ids are assistant:<sessionId>:runtime:<runtimeId>:segment:<n> (assistantItemId). assistantSegmentMessageId then prefixes assistant: again, so the row id is assistant:assistant:<sessionId>:runtime:<runtimeId>:segment:<n>. The completion fallback uses the same assistant:${itemId} formula, so the two paths agree.

    The assistant: on the item id is from #3932 / #3791: segment counters restart at 0 when the runtime is recreated, and message ids are primary keys. The runtime uuid is what keeps a resumed session from appending to an old row. A second assistant: does not change that and does not affect markdown. Renaming it would mint new primary keys for no user-visible fix.

    Next step

    Leave this open as the segment-close bug: do not close an in-progress assistant item on a tool_call_update that is not emitted, and do not treat a background completion as a new prose boundary while that item is still streaming. Keep the close on the initial tool_call.

    Follow the harness-text leak on #11432. Do not fold the id prefix into the fix.

    @adeebahmad01 — a provider trace around the split (session update types and timestamps, not just the prompt) would show whether Antigravity is sending tool_call_update mid-chunk. The code will split if it does.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Sep 22, 2026
  3. adeebahmad01 commented on Sep 22, 2026

    @adeebahmad01
    ContributorAuthor

    A pull request addressing this issue has been opened: #13137

  4. juliusmarminge commented on Oct 2, 2026

    @juliusmarminge
    Member

    Thanks for taking the time to report this and provide the details. We revisited it during the orchestrator V2 cleanup.

    The accepted segment-close bug is fixed in current ACP runtime: coalesced updates are skipped before segmentation, and only a newly shown tool call closes prose. Updates/completion for already-shown calls preserve the active segment.

    I’m closing this based on the current source and the evidence in this thread.

    Source reviewed.

    Closure applies to the accepted prose segmentation scope. Harness telemetry remains separately tracked; double assistant prefix was harmless and is not claimed repaired.

    If you still hit this on a current build, please reply with the app/server versions and the steps that reproduce it. We can reopen this if the original problem is still there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions