diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs
index a7a0c36c51..0ad8870352 100644
--- a/crates/tui/src/core/engine/turn_loop.rs
+++ b/crates/tui/src/core/engine/turn_loop.rs
@@ -1203,6 +1203,10 @@ impl Engine {
"Turn ending with {running} sub-agent(s) still running in the background; they'll report when done."
)))
.await;
+ // Inject a waiting hint so the model does not poll
+ // with peek/status/sleep on the next turn (issue #4097).
+ self.add_session_message(waiting_for_subagents_runtime_message(running))
+ .await;
}
}
if subagent_completions > 0 {
@@ -2507,6 +2511,28 @@ XML unless the user explicitly asks to debug sub-agent internals.\n\n\
)
}
+fn waiting_for_subagents_runtime_message(running: usize) -> Message {
+ Message {
+ role: "user".to_string(),
+ content: vec![
+ ContentBlock::Text {
+ text: format!(
+ "\n\
+This is an internal runtime event, not user input. Your {running} sub-agent(s) \
+are still running. Do NOT poll them with agent(action=\"peek\") or \
+agent(action=\"status\"). Do NOT use sleep or any shell blocking primitive as a \
+waiting strategy. The runtime will deliver sentinels \
+automatically when each child finishes — polling will never make that happen \
+sooner. Stop immediately: emit zero tool calls and end the turn.\n\
+"
+ ),
+ cache_control: None,
+ },
+ runtime_event_turn_metadata_block(UserInputProvenance::SubAgentHandoff),
+ ],
+ }
+}
+
fn subagent_completion_runtime_message(payload: &str) -> Message {
// Role is "user", not "system": some OpenAI-compatible backends apply a
// strict chat template (e.g. vLLM serving Qwen3) that requires any system
diff --git a/crates/tui/src/prompts/constitution.md b/crates/tui/src/prompts/constitution.md
index 94de6c95c6..f7d80d39e6 100644
--- a/crates/tui/src/prompts/constitution.md
+++ b/crates/tui/src/prompts/constitution.md
@@ -373,6 +373,15 @@ Reach for them when the work is genuinely independent:
launch more; do not invent a smaller per-turn limit. To fan out more gently
you can lower `[subagents].launch_concurrency` (how many start at once);
the default is the full running cap.
+- **Waiting, not polling**: When you end a turn because every remaining task
+ depends on background results that haven't arrived yet, do **not** poll those
+ children with `agent(action="peek")` or `agent(action="status")` on your next
+ turn. Do not use `sleep` or any shell blocking primitive as a waiting strategy.
+ The runtime delivers `` sentinels automatically as
+ user messages the moment a child finishes — polling will never make that happen
+ sooner, it only burns turns and budget. If you receive a new turn with no new
+ user message and your children are still running, stop immediately: emit zero
+ tool calls and end the turn.
## Thinking Delegation