Nightly 0.1.9-nightly.20261110.4491 still shows this on a long Antigravity 1.4.5 (gemini-4-carbon) thread. The merged 15-minute inactivity timeout (#30377, for #30271) does not settle it.
What happens: the session log's newest step for the prompt is a finished planner reply (step_type 15, status 3), with no non-terminal step after it. session/prompt stays started. The assistant row stays streaming=1. The prompt later becomes failed / AcpProcessExitedError only when a follow-up user message interrupts the hung process (~15s later). The provider turn is interrupted, not the inactivity failure (sent nothing for 15 minutes is absent). The same orchestration run stays running because that follow-up is attempt N+1 of the same run.
Observed on thread 632a763d-d37a-4b7a-926c-a6f25f6195a6, session fe72880c-976e-4cd2-8fc1-6881ab137d5b (times Taipei, 2026-11-11):
| user step | last step | planner finished | T3 closed the turn |
|---|---|---|---|
| idx 427 at 11:00:19 | idx 449 type 15 status 3 | 11:03:45 | 11:17:52 interrupted |
| idx 450 at 11:18:12 | idx 462 type 15 status 3 | 11:21:18 | 11:33:06 interrupted |
| idx 463 at 11:33:38 | idx 501 type 15 status 3 | 11:41:16 | 11:49:01 interrupted |
A short control thread on the same nightly, same official 1.4.5 binary, and the same stdio wrapper does settle: bd15b956-ca58-48ce-9bbe-332e8a3c5879 turn 1 ends type 15 status 3 at 11:04:38, and session/prompt is succeeded in that same second, while its delegated child is still running. So a live child is not why prompt() stays open. v2 prompt() discards the RPC body and waits for state_update / idle (effect-acp completePrompt). finalizeTurn / closeTextStreams run only after that. watchAntigravityPromptOutcome runs only after prompt() returns, and only looks for a type 17 error step. Antigravity does not set deferFinalizeForBackgroundWork or followAgentWorkState.
#30377 fails a silent prompt after 15 minutes. These answers were already stored, and a person sent the next message before that deadline, so the turn was interrupted instead of completed.
Suggested settle, raced with the real idle so a prompt that does return is unchanged: if every step since the pre-prompt index is terminal (status 3 or 7) and the newest is type 15 status 3 for 5 unchanged polls, finalizeTurn(context, "completed"), then session/cancel, and if promptWireSettled is still pending after 5s, close the runtime. catchCause already returns when context.finalized is set, so the later process-exit error does not rewrite the completed turn.
I tested that predicate, not a patched nightly (patching the running app would restart live threads). Synthetic cases: a finished planner tail settles; an in-progress planner step, a pending command, a tail that is still a tool, and an error tail do not. Five identical polls settle; a new idx or an open tail resets the count. On a copy of the session log above, the three stuck tails match, and cutting off the final planner step makes each one not match. Both control-thread tails match too, which is what we want: idle already won in the same second, so the watcher would see context.finalized and stop.
One limit: if a background run_command is stored as a terminal step while STATE_WAITING_FOR_TASKS still holds session/prompt (#28898), this would complete that turn after 5s. This session had no non-terminal step for many minutes after the planner reply, and the control delegate still receives idle immediately.