Skip to content

[Bug]: Claude Monitor streaming events + t3_background_wait loop across turns and lose roster state after a failed/interrupted turn #31817

Open

Description

@JZongf

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Start a Claude Code thread (e.g. using antigravity/gemini-4-argon via a local Anthropic-compatible gateway) and have the agent launch a background Bash command (task_a) plus a Monitor task (task_b) that emits progress lines every 30–60 seconds.
  2. Following CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS ("To wait for a background command or Monitor you started (tests, a build), call t3_background_wait with its task ID before you end your turn"), the agent calls mcp__t3-code__t3_background_wait({"taskIds": ["task_a", "task_b"], "waiting": true}).
  3. Observe what happens during and after the t3_background_wait call:
    • While t3_background_wait or the subsequent LLM round-trip is in flight, Monitor emits a <task-notification><task-id>task_b</task-id><summary>Monitor event: ...</summary><event>...</event></task-notification>, which Claude Code enqueues as a queued_command and drains at the t3_background_wait tool-result boundary.
    • Seeing the newly injected <task-notification>, the agent calls t3_background_wait again in the same turn (repeating 2–3 times in a single turn as new Monitor lines arrive).
    • Once t3_background_wait returns {"matched":["task_a","task_b"],"unmatched":[]} with no queued notification at that exact millisecond, the model treats the turn as already waiting and emits 0 output tokens on the post-tool /v1/messages turn, causing gateways that reject 0-token Gemini streams to fail with 502 after exhausting retries.
  4. When that turn ends with status: "failed" (or "interrupted"), observe the next turn (woken 1 second later by the still-running Monitor task in the same live Claude Code process):
    • The agent calls t3_background_wait({"taskIds": ["task_a", "task_b"], "waiting": true}) again.
    • Even though the Claude Code subprocess is still alive and both task_a and task_b are still running (and task_a later emits its completion <task-notification>), t3_background_wait now returns {"matched":[],"unmatched":["task_a","task_b"]}.

Expected behavior

  1. Clear post-t3_background_wait turn completion & Monitor guidance:
    • t3_background_wait's tool description / result (or CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS) should instruct the model to end its turn immediately with a brief one-line status text after calling t3_background_wait (preventing 0-token post-tool responses on Gemini models), and clarify that t3_background_wait only needs to be called once when entering a wait state rather than re-called on every intermediate Monitor <task-notification> event.
  2. Preserve live background task roster across failed/interrupted turns on a still-alive Claude subprocess:
    • In ClaudeAdapterV2, when a turn ends with input.status !== "completed" ("failed" or "interrupted") while liveQuery remains alive, pendingBackgroundTasksByNativeThread and wakeEligibleBackgroundTasksByNativeThread should not be unconditionally wiped for background tasks that are still running in that process.

Actual behavior

  1. Repeated t3_background_wait calls and silent post-tool turn:
    • Every streaming <task-notification> from Monitor causes the agent to re-invoke t3_background_wait (both multiple times within the same turn and on every wake turn), and the post-t3_background_wait /v1/messages request frequently produces 0 output tokens because the model has nothing left to say after {"matched":[...],"unmatched":[]}.
  2. Roster wipe on non-completed turn termination:
    • In ClaudeAdapterV2 turn finalization:
      if (nativeThreadId !== null) {
        if (input.status !== "completed") {
          yield* clearPendingBackgroundTasksForNativeThread(nativeThreadId);
          yield* clearNativeThreadTaskIdSet(wakeEligibleBackgroundTasksByNativeThread, nativeThreadId);
          yield* clearNativeThreadTaskIdSet(opaqueBackgroundTaskReplayTombstonesByNativeThread, nativeThreadId);
        }
      }
      Because clearPendingBackgroundTasksForNativeThread(nativeThreadId) is called on any "failed" or "interrupted" turn even when the underlying Claude Code subprocess stays alive, still-running background Bash and Monitor tasks are erased from pendingBackgroundTasksByNativeThread. Subsequent t3_background_wait calls in the next turn return {"matched":[],"unmatched":[...]} while those tasks are still actively running.

Impact

Major degradation or frequent failure

Version or commit

0.1.8-nightly.20261106.4247

Environment

Linux x64 (Ubuntu), T3 Code v0.1.8-nightly.20261106.4247, Claude Code 2.1.318 (CLAUDE_PROVIDER), model antigravity/gemini-4-argon

Logs or stack traces

# Run 12: Monitor starts and emits streaming events while t3_background_wait is called twice in the same turn
01:49:46.811Z tool_result (Monitor): "Monitor started (task bd2o2yhwp, expires in 30m...)"
01:49:47.021Z queue-operation enqueue: <task-notification><task-id>bd2o2yhwp</task-id><summary>Monitor event: ...</summary><event>[LOCAL] ...</event></task-notification>
01:49:54.039Z tool_use: mcp__t3-code__t3_background_wait {"taskIds": ["bfyvtbzo0", "bd2o2yhwp"], "waiting": true}
01:49:54.062Z tool_result: {"matched":["bfyvtbzo0","bd2o2yhwp"],"unmatched":[]}
01:49:54.171Z queue-operation remove (queued_command drained into same turn)
01:50:01.260Z tool_use: mcp__t3-code__t3_background_wait {"taskIds": ["bfyvtbzo0", "bd2o2yhwp"], "waiting": true}
01:50:01.277Z tool_result: {"matched":["bfyvtbzo0","bd2o2yhwp"],"unmatched":[]}
01:50:01.286Z api-request -> model produces 0 output tokens after tool_result -> 502 retry loop until 02:14:12Z (Run 12 ends with status: "failed")

# Run 13 (1 second later): both bfyvtbzo0 and bd2o2yhwp are still running in the same Claude process,
# but clearPendingBackgroundTasksForNativeThread wiped the roster when Run 12 failed:
02:14:21.358Z tool_use: mcp__t3-code__t3_background_wait {"taskIds": ["bfyvtbzo0", "bd2o2yhwp"], "waiting": true}
02:14:21.493Z tool_result: {"matched":[],"unmatched":["bfyvtbzo0","bd2o2yhwp"]}
02:14:32.152Z tool_use: mcp__t3-code__t3_background_wait {"taskIds": ["bfyvtbzo0", "bd2o2yhwp"], "waiting": true}
02:14:32.241Z tool_result: {"matched":[],"unmatched":["bfyvtbzo0","bd2o2yhwp"]}
02:19:46.811Z queue-operation enqueue: <task-notification><task-id>bd2o2yhwp</task-id>...[Monitor expired after 30m with 7 events delivered.]</task-notification>
02:40:20.663Z queue-operation enqueue: <task-notification><task-id>bfyvtbzo0</task-id><status>completed</status>...</task-notification>

Workaround

Avoid combining Monitor with t3_background_wait on Gemini models, or instruct the agent to always emit a short status line when ending its turn after t3_background_wait.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions