Agent shell waiter ignores completed-job footer (`exit_code:`) and injects late “task finished” turns for background servers

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

Summary

Agent Composer waits 1–3 minutes after a shell command has already finished (or after a server is already serving). This is not a slow cargo/wasmtime process. Two harness behaviors force the wait:

  1. AwaitShell does not treat process completion as a match when the model waits on exit_code:. That string only appears in the terminal footer. The waiter is specified not to match header/footer metadata. The command exits; the agent still blocks until block_until_ms (often 180000).

  2. A background server (block_until_ms: 0) stays a tracked task. After the agent has already used it (curl succeeded), the harness later aborts it and injects a system “task has finished” user turn, minutes after the useful work. That looks like the agent is still waiting.

These are product issues. Agent mistakes (chaining build && serve, | tail, oversized timeouts) make them worse, but the two items above are reproducible even with a correct agent.


Bug 1 — Completed command, waiter still blocks for the full timeout

What happened

Shell 885331: cargo fmt && cargo test -p rocci-cli

  • Tests finished in 26.61s; whole command 51.3s
  • Footer already present:
---
exit_code: 101
elapsed_ms: 51309
ended_at: 2026-08-30T10:58:01.025Z
---

The agent then called the shell waiter with:

AwaitShell(shell_id=885331, pattern="exit_code:", block_until_ms=180000)

Actual: waiter did not return on process exit. It sat until the 180s budget because exit_code: is footer metadata and is excluded from pattern matching.

Expected: either

  • return immediately when the process has exited (footer written), or
  • reject / no-op pattern: "exit_code:" as invalid, or
  • document and train models that completion is signaled by the tool result / ended_at, never by matching exit_code: in the file.

Same class of miss: 971161 (cargo test … | tail -40) aborted at 30s with an empty body. The agent then waited 180s for test result:|error:, which never appeared.


Bug 2 — Late “task finished” turn for an intentional background server

What happened

wasmtime serve started with block_until_ms: 0 (correct). Ready line appeared immediately:

Serving HTTP on http://127.0.0.1:18080/

The agent curled GET / and got the expected body. The serve process then stayed tracked.

Terminal 971162:

  • started 2026-08-30T10:23:31Z
  • ready immediately
  • later status: failed, exit_code: unknown, elapsed_ms: 13415

Terminal 971163 (same pattern, env-log serve):

  • started 10:28:20Z
  • printed Serving HTTP plus the expected stderr line
  • later aborted (elapsed_ms: 19354)

Actual: at 12:24 and 12:29 local, Cursor injected:

The following task has finished. … status: aborted
title: Start wasmtime serve …
title: Serve env-log component …

Those turns interrupted the implementation chat long after the proof was done and committed. From the UI this is “the agent waited several minutes.”

Expected:

  • A block_until_ms: 0 process that is a server should not generate a blocking user-visible “task finished” turn after abort, unless the current turn is waiting on that shell_id.
  • Aborting a background serve after the agent already moved on should be silent (or a small status chip), not a new user message the agent must answer.

Minimal reproduction (no project required)

A. Footer wait (Bug 1)

  1. Agent Shell: echo ready && sleep 2 (finishes in ~2s).
  2. Agent AwaitShell on that id with pattern: "exit_code:", block_until_ms: 180000.
  3. Observe: command file already has exit_code: 0; waiter still blocks up to 3 minutes.

B. Late abort notification (Bug 2)

  1. Agent Shell with block_until_ms: 0: python3 -m http.server 8765.
  2. Wait only until the serving line, then curl -sS http://127.0.0.1:8765/.
  3. Continue the chat with an unrelated message; do not kill the server in that turn.
  4. Observe: minutes later, a system “task has finished / aborted” user turn for that server.

What this is not

  • Not a hung wasmtime or cargo (those printed “Serving HTTP” / test result: on time).
  • Not “always wait block_until_ms for finite jobs” — finite Shell calls that return on process exit are fine. The hang is AwaitShell after exit and background-task notifications.
  • Not asking you to change how long cargo may run. Asking for completion and background-task signaling that matches the file the UI already wrote.

Asks

  1. Treat process exit as a terminal condition for AwaitShell even when pattern is set (pattern = “unblock early”, exit = always unblock).
  2. Do not allow / do not honor pattern: "exit_code:" if footers are excluded from matching — return immediately with “already completed” if the footer exists.
  3. Do not inject a Composer user turn when a block_until_ms: 0 server is later aborted, unless that turn is explicitly waiting on it.
  4. Optional: surface ended_at / exit_code in the waiter tool result so models stop scraping the footer.

Happy to attach the two terminal files (971162.txt, 885331.txt) and the exported chat if useful.

Steps to Reproduce

Two separate reproductions. Do them in a new Agent chat. You never call AwaitShell yourself — the prompts below make the agent do it.


A. Finished command, agent still waits ~3 minutes

  1. New Agent chat, empty folder is fine.
  2. Paste:
Run exactly: echo ready && sleep 2

When the Shell tool returns or backgrounds the job, immediately call AwaitShell
on that shell_id with:
  pattern: "exit_code:"
  block_until_ms: 180000

Do not start any other command. Tell me when AwaitShell returns and how long it waited.
  1. Watch the tool calls (not just the chat).

Pass (bug): echo finishes in ~2s. The terminal file already has a footer like exit_code: 0. The next tool is AwaitShell and it stays pending for up to 180s.

Fail (fixed / not this bug): AwaitShell returns as soon as the process exits, or the agent refuses pattern: "exit_code:" and says the job already completed.


B. Background server → late “task finished” turn

  1. New Agent chat.
  2. Paste:
Start this with block_until_ms: 0 (background, do not wait for exit):

  python3 -m http.server 8765 --bind 127.0.0.1

AwaitShell only until the output contains "Serving HTTP".
Then run: curl -sS -D - http://127.0.0.1:8765/ | head -20
Leave the server running. Do not kill it. Reply with the HTTP status only.
  1. After the agent replies with 200, send a second message:
Thanks. Do not touch the server. Just say ok.
  1. Wait, or stop the background task from the UI / end the turn and sit idle for a few minutes.

Pass (bug): A later system user message appears, e.g. “The following task has finished” / status: aborted, for “Start http.server” (or similar). The agent then writes a status reply even though you already had the curl result.

Fail (fixed): No injected “task finished” turn. The server stays a quiet background task, or it dies without a new user message.


Optional check (same class as A)

If A is “too synthetic,” this is the real-session variant:

Run: cargo test -p rocci-wasi-http
When it finishes, AwaitShell that same shell_id with pattern "exit_code:" and block_until_ms 180000.

Same signal: tests already printed test result: and the file has exit_code:; waiter still burns the 180s.


Do A and B as two posts or two numbered sections. Do not combine them in one chat — a leftover http.server confuses timing.

If you only have time for one, A is the clearer product bug (wrong waiter contract). B is what you felt in the WASI session.

Operating System

MacOS

Version Information

Version: 3.18.9
VS Code Extension API: 1.128.0
Commit: 2ba48ff3f7514cc4643c52ca9f7b3173d9b66130
Date: 2026-08-27T01:42:22.092Z
Layout: IDE
Build Type: Stable
Release Track: Default
Electron: 40.10.3
Chromium: 144.0.7559.236
Node.js: 24.15.0
V8: 14.4.258.32-electron.0
xterm.js: 6.1.0-beta.291
OS: Darwin arm64 24.6.0

For AI issues: add Request ID with privacy disabled

839750e0-85d2-4e9f-8a1a-14c5469d8dbf

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey @koliyo,

First item (the waiter): not intended behavior, and it’s an issue we’re tracking. Two things that help right now:

  • Match a real output line (test result:, Serving HTTP), not exit_code:. Patterns match the written body, but exit_code: only lives in the footer metadata, so it never matches.
  • Use shorter block_until_ms slices with a quick read in between, so a missed completion costs seconds. The wait’s tool result already gives you the exit code, so there’s no need to scrape the footer.

Second item (the late “task finished” turns): I’d like to pin this down before saying more. Could you share:

  • The serve terminal files (971162.txt, 971163.txt) and the exported chat, if you still have them.
  • In the cases where the late turn showed up, did you leave the server idle (so it ended via cleanup), or stop it yourself from the UI?
  • Did that turn make the agent run a full reply, or was it just a status line?

I’ll follow up here once I’ve looked.

Hey @koliyo,

Quick update on the first item: the long waits after a command had already finished came from a recent regression on our side in how the waiter re-read the terminal file, and a fix is now rolled out. You shouldn’t see the waiter sit out the full timeout on a completed command anymore, and no update is needed on your end.

If you still catch a wait running long after a command finished, let us know!

On the second item (the late “task finished” turns), the questions above still stand, the terminal files and exported chat would help us pin that down.

Thanks for update, here is agent response:

The original 971162.txt / 971163.txt were recycled from
~/.cursor/projects/Users-nils-Projects-okmate/terminals/ (gone as of 2 Sep).
Reconstructed from a full read on 30 Aug; transcript still at
…/agent-transcripts/9992234f-3775-4ce4-a0f3-5912044d8c71/9992234f-3775-4ce4-a0f3-5912044d8c71.jsonl

Idle vs UI stop: neither. I did not stop them in the UI. The agent did not
kill those PIDs. Both were block_until_ms: 0, curl succeeded, then the
harness set status=failed / exit_code=unknown after 13s and 19s
(end-of-turn cleanup, not a long idle serve).

The late turn was a full agent reply, not a status chip. Cursor injected
the usual “Briefly inform the user about the task result…” user message;
the model wrote two sentences. That happened immediately after the Phase 2
and Phase 3 handoffs (12:24 and 12:29 CEST), not after hours of idle.

971162: started 10:23:31Z, ended 10:23:44Z, notify 12:24 CEST
971163: started 10:28:20Z, ended 10:28:39Z, notify 12:29 CEST

Reconstructed 971162.txt


pid: 31470

cwd: “/Users/nils/Projects/rocci”

command: "wasm-tools metadata show /Users/nils/Projects/rocci/target/wasm32-wasip2/debug/rocci_wasi_http_component.wasm 2>&1 | head -20

# serve in background

cd /Users/nils/Projects/rocci

wasmtime serve -Sp3 -Scli --addr 127.0.0.1:18080 \

target/wasm32-wasip2/debug/rocci_wasi_http_component.wasm"

title: “Start wasmtime serve for Roc-linked component”

status: failed

started_at: 2026-08-30T10:23:31.544Z

running_for_ms: 13415

-–

… wasm-tools metadata table …

Serving HTTP on http://127.0.0.1:18080/

-–

exit_code: unknown

elapsed_ms: 13415

ended_at: 2026-08-30T10:23:44.959Z

-–

Reconstructed 971163.txt


pid: 41333

cwd: “/Users/nils/Projects/rocci”

command: “cd /Users/nils/Projects/rocci && wasmtime serve -Sp3 -Scli --env GREETING=phase3-greeting --addr 127.0.0.1:18083 target/wasm32-wasip2/debug/rocci_wasi_http_component.wasm”

title: “Serve env-log component with GREETING set”

status: failed

started_at: 2026-08-30T10:28:20.622Z

running_for_ms: 19355

-–

Serving HTTP on http://127.0.0.1:18083/

stderr [0] :: env-log

stderr [1] :: env-log

stderr [2] :: env-log

-–

exit_code: unknown

elapsed_ms: 19354

ended_at: 2026-08-30T10:28:39.976Z

-–

Attaching full chat transcript as well

9992234f-3775-4ce4-a0f3-5912044d8c71.jsonl.zip (102.3 KB)