Repository navigation
Built-in wait tool capped at ~50s causes MASSIVE token burn on long waits (multi_agent_v2 re-samples every 50s) #32640
Description
What version of Codex CLI is running?
codex-cli 0.144.1
What subscription do you have?
Plus Plan
Which model were you using?
gpt-5.6-luna
What platform is your computer?
Darwin 25.5.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
Terminal.app
Codex doctor report
{
"schemaVersion": 1,
"generatedAt": "1783892002s since unix epoch",
"overallStatus": "ok",
"codexVersion": "0.144.1",
"checks": {
"app_server.status": {
"id": "app_server.status",
"category": "app-server",
"status": "ok",
"summary": "background server is not running",
"details": {
"control socket": "/Users/danieldobberpuhl/.codex/app-server-control/app-server-control.sock",
"daemon state dir": "/Users/danieldobberpuhl/.codex/app-server-daemon",
"mode": "ephemeral",
"pid file": "/Users/danieldobberpuhl/.codex/app-server-daemon/app-server.pid (missing)",
"settings": "/Users/danieldobberpuhl/.codex/app-server-daemon/settings.json (missing)",
"status": "not running",
"update-loop pid file": "/Users/danieldobberpuhl/.codex/app-server-daemon/app-server-updater.pid (missing)"
},
"remediation": null,
"durationMs": 0
},
"auth.credentials": {
"id": "auth.credentials",
"category": "auth",
"status": "ok",
"summary": "auth is configured",
"details": {
"auth file": "/Users/danieldobberpuhl/.codex/auth.json",
"auth storage mode": "File",
"stored API key": "false",
"stored ChatGPT tokens": "true",
"stored agent identity": "false",
"stored auth mode": "chatgpt"
},
"remediation": null,
"durationMs": 0
},
"config.load": {
"id": "config.load",
"category": "config",
"status": "ok",
"summary": "config loaded",
"details": {
"CODEX_HOME": "/Users/danieldobberpuhl/.codex",
"config.toml": "/Users/danieldobberpuhl/.codex/config.toml",
"config.toml parse": "ok",
"cwd": "/Users/danieldobberpuhl/.openclaw/workspace/haiku",
"enabled feature flags": "shell_tool, unified_exec, shell_snapshot, code_mode_host, terminal_resize_reflow, sqlite, hooks, enable_request_compression, multi_agent, tool_search_always_defer_mcp_tools, tool_suggest, plugins, in_app_browser, remote_plugin, plugin_sharing, resize_all_images, skill_mcp_dependency_install, mentions_v2, steer, guardian_approval, goals, collaboration_modes, tool_call_mcp_elicitation, auth_elicitation, personality, fast_mode, tui_app_server, remote_compaction_v2, workspace_dependencies",
"feature flag overrides": "apps=false, browser_use=false, browser_use_full_cdp_access=false, browser_use_external=false, computer_use=false, image_generation=false",
"feature flags enabled": "29",
"log dir": "/Users/danieldobberpuhl/.codex/log",
"mcp servers": "1",
"model": "gpt-5.6-luna",
"model provider": "openai",
"sqlite home": "/Users/danieldobberpuhl/.codex"
},
"remediation": null,
"durationMs": 0
},
"git.environment": {
"id": "git.environment",
"category": "git",
"status": "ok",
"summary": "git version 2.50.1 (Apple Git-155)",
"details": {
".git entry": "directory",
"PATH git #1": "/usr/bin/git",
"PATH git entries": "1",
"git branch": "wip-atomic64-fix-20260704",
"git build options": "git version 2.50.1 (Apple Git-155); cpu: arm64; no commit associated with this build; sizeof-long: 8; sizeof-size_t: 8; shell-path: /bin/sh; feature: fsmonitor--daemon; libcurl: 8.7.1; zlib: 1.2.12; SHA-1: SHA1_DC; SHA-256: SHA256_BLK",
"git exec path": "/Library/Developer/CommandLineTools/usr/libexec/git-core",
"git version": "git version 2.50.1 (Apple Git-155)",
"repo detected": "true",
"repo root": "/Users/danieldobberpuhl/.openclaw/workspace/haiku",
"selected git": "/usr/bin/git"
},
"remediation": null,
"durationMs": 96
},
"installation": {
"id": "installation",
"category": "install",
"status": "ok",
"summary": "installation looks consistent",
"details": {
"PATH codex #1": "/Users/danieldobberpuhl/.local/bin/codex",
"PATH codex #2": "/Users/danieldobberpuhl/.local/bin/codex",
"PATH codex entries": "2",
"current executable": "/Users/danieldobberpuhl/.local/bin/codex",
"install context": "standalone (unix, package /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin, bin /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin/bin, resources /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin/codex-resources, path /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin/codex-path)",
"managed by bun": "false",
"managed by npm": "false",
"managed by pnpm": "false",
"managed package root": "not set"
},
"remediation": null,
"durationMs": 6
},
"mcp.config": {
"id": "mcp.config",
"category": "mcp",
"status": "ok",
"summary": "MCP configuration is locally consistent",
"details": {
"configured servers": "1",
"disabled servers": "0",
"stdio servers": "1"
},
"remediation": null,
"durationMs": 0
},
"network.env": {
"id": "network.env",
"category": "network",
"status": "ok",
"summary": "network-related environment looks readable",
"details": {
"proxy env vars": "none"
},
"remediation": null,
"durationMs": 0
},
"network.provider_reachability": {
"id": "network.provider_reachability",
"category": "reachability",
"status": "ok",
"summary": "active provider endpoints are reachable over HTTP",
"details": {
"ChatGPT base URL": "https://chatgpt.com/backend-api/ reachable (HTTP 404)",
"reachability mode": "ChatGPT auth"
},
"remediation": null,
"durationMs": 238
},
"network.websocket_reachability": {
"id": "network.websocket_reachability",
"category": "websocket",
"status": "ok",
"summary": "Responses WebSocket handshake succeeded",
"details": {
"DNS": "2 IPv4, 0 IPv6, first IPv4",
"auth mode": "chatgpt",
"connect timeout": "15000 ms",
"endpoint": "wss://chatgpt.com/backend-api/<redacted>",
"handshake result": "HTTP 101 Switching Protocols",
"model provider": "openai",
"models etag present": "true",
"provider name": "OpenAI",
"proxy env vars": "none",
"reasoning header": "false",
"server model present": "false",
"supports websockets": "true",
"wire API": "responses"
},
"remediation": null,
"durationMs": 772
},
"runtime.provenance": {
"id": "runtime.provenance",
"category": "runtime",
"status": "ok",
"summary": "running standalone on macos-aarch64",
"details": {
"commit": "unknown",
"current executable": "/Users/danieldobberpuhl/.local/bin/codex",
"install method": "standalone (unix, package /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin, bin /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin/bin, resources /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin/codex-resources, path /Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin/codex-path)",
"platform": "macos-aarch64",
"version": "0.144.1"
},
"remediation": null,
"durationMs": 0
},
"runtime.search": {
"id": "runtime.search",
"category": "search",
"status": "ok",
"summary": "search is OK (bundled)",
"details": {
"search command": "/Users/danieldobberpuhl/.codex/packages/standalone/releases/0.144.1-aarch64-apple-darwin/codex-path/rg",
"search command readiness": "file exists",
"search provider": "bundled"
},
"remediation": null,
"durationMs": 0
},
"sandbox.helpers": {
"id": "sandbox.helpers",
"category": "sandbox",
"status": "ok",
"summary": "sandbox configuration is readable",
"details": {
"approval policy": "OnRequest",
"codex-linux-sandbox helper": "none",
"execve wrapper helper": "/Users/danieldobberpuhl/.codex/tmp/arg0/codex-arg0aZu0v8/codex-execve-wrapper",
"filesystem sandbox": "restricted",
"network sandbox": "restricted"
},
"remediation": null,
"durationMs": 0
},
"state.paths": {
"id": "state.paths",
"category": "state",
"status": "ok",
"summary": "state paths and databases are inspectable",
"details": {
"CODEX_HOME": "/Users/danieldobberpuhl/.codex (dir)",
"active rollout files": "21 files, 23691341 total bytes, 1128159 average bytes",
"archived rollout files": "0 files, 0 total bytes, 0 average bytes",
"goals DB": "/Users/danieldobberpuhl/.codex/goals_1.sqlite (file)",
"goals DB integrity": "ok",
"log DB": "/Users/danieldobberpuhl/.codex/logs_2.sqlite (file)",
"log DB integrity": "ok",
"log dir": "/Users/danieldobberpuhl/.codex/log (dir)",
"memories DB": "/Users/danieldobberpuhl/.codex/memories_1.sqlite (file)",
"memories DB integrity": "ok",
"sqlite home": "/Users/danieldobberpuhl/.codex (dir)",
"standalone release cache": "4 entries in /Users/danieldobberpuhl/.codex/packages/standalone/releases",
"state DB": "/Users/danieldobberpuhl/.codex/state_5.sqlite (file)",
"state DB integrity": "ok"
},
"remediation": null,
"durationMs": 153
},
"state.rollout_db_parity": {
"id": "state.rollout_db_parity",
"category": "threads",
"status": "ok",
"summary": "rollout files and state DB thread inventory agree",
"details": {
"default model provider": "openai",
"rollout DB active files": "21",
"rollout DB active rows": "21",
"rollout DB archive mismatches": "0",
"rollout DB archived files": "0",
"rollout DB archived rows": "0",
"rollout DB duplicate DB paths": "0",
"rollout DB duplicate rollout thread ids": "0",
"rollout DB malformed file names": "0",
"rollout DB missing active rows": "0",
"rollout DB missing archived rows": "0",
"rollout DB model providers": "openai=21",
"rollout DB rows": "21",
"rollout DB scan cap reached": "false",
"rollout DB scan errors": "0",
"rollout DB sources": "cli=21",
"rollout DB stale rows": "0"
},
"remediation": null,
"durationMs": 210
},
"system.environment": {
"id": "system.environment",
"category": "system",
"status": "ok",
"summary": "OS language de-DE",
"details": {
"EDITOR": "not set",
"LANG": "de_DE.UTF-8",
"VISUAL": "not set",
"os": "Mac OS 26.5.2 [64-bit]",
"os language": "de-DE",
"os type": "Mac OS",
"os version": "26.5.2"
},
"remediation": null,
"durationMs": 2
},
"terminal.env": {
"id": "terminal.env",
"category": "terminal",
"status": "ok",
"summary": "terminal metadata was detected",
"details": {
"COLORTERM": "truecolor",
"TERM_PROGRAM": "Apple_Terminal",
"color output": "enabled",
"effective locale": "de_DE.UTF-8",
"stderr is terminal": "true",
"stdin is terminal": "true",
"stdout is terminal": "true",
"terminal": "Apple Terminal",
"terminal size": "120x30",
"terminal version": "470.2"
},
"remediation": null,
"durationMs": 0
},
"terminal.title": {
"id": "terminal.title",
"category": "title",
"status": "ok",
"summary": "terminal title default",
"details": {
"terminal title activity": "true",
"terminal title items": "activity, project-name",
"terminal title project source": "git repo root",
"terminal title project value": "haiku",
"terminal title source": "default"
},
"remediation": null,
"durationMs": 0
},
"updates.status": {
"id": "updates.status",
"category": "updates",
"status": "ok",
"summary": "update configuration is locally consistent",
"details": {
"cached latest version": "0.144.1",
"check for update on startup": "true",
"last checked at": "2026-07-12T15:48:33.167341Z",
"latest version": "0.144.1",
"latest version status": "current version is not older",
"update action": "standalone installer",
"version cache": "/Users/danieldobberpuhl/.codex/version.json"
},
"remediation": null,
"durationMs": 130
}
}
}What issue are you seeing?
Massive, avoidable token/quota burn during long waits.
When the agent waits for a long-running external job (e.g. an ~11–36 min
remote build+boot driven through a custom MCP server), it uses the
built-in wait tool. That tool is hard-capped per call at
max_wait_timeout_ms (~50000 ms observed). Because of the 50s cap the
model cannot wait once — it is forced to call wait(50s) back-to-back,
and every 50s call returns to the model and triggers a fresh sampling
request over the ENTIRE context.
For a high-reasoning model with a large context this turns a single
"wait for the job" into ~12+ full re-samples in 10 minutes.
Concrete impact: one ~36 min job dropped my weekly quota from 84% to
65% remaining — for what should have been 1–3 blocking calls. The cost
scales roughly linearly with wait duration. A purpose-built,
server-side blocking MCP wait existed (one call, 840s, zero
intermediate re-samples), but the built-in wait tool was used instead,
and its 50s cap makes the re-sample storm unavoidable even when the tool
is "used correctly".
Environment:
- codex-cli: 0.144.1
- model: gpt-5.6-luna,
model_reasoning_effort = high - OS: macOS (darwin 25.5.0)
- Feature:
features.multi_agent_v2(provides the built-inwaittool)
Evidence (from ~/.codex/logs_2.sqlite, single session, single thread_id):
12 consecutive model-invoked `wait` calls in one ~10 min window:
18:59:22 tool_name="wait" handler_duration_ms=50005
19:00:15 tool_name="wait" handler_duration_ms=50006
19:01:08 tool_name="wait" handler_duration_ms=50003
19:02:00 tool_name="wait" handler_duration_ms=50005
19:02:53 tool_name="wait" handler_duration_ms=50004
19:03:45 tool_name="wait" handler_duration_ms=50005
19:04:39 tool_name="wait" handler_duration_ms=50004
19:05:37 tool_name="wait" handler_duration_ms=50004
19:06:30 tool_name="wait" handler_duration_ms=50004
19:07:22 tool_name="wait" handler_duration_ms=50004
19:08:15 tool_name="wait" handler_duration_ms=50005
19:09:07 tool_name="wait" handler_duration_ms=50004
- Each
waitis model-invoked (function_call,from="output_item_done",
tool_name="wait") — 24 such events in the window. - Model is NOT suspended between waits:
codex_api::sse::responsesruns
continuously at ~21 events/min for the whole 10 min (active sampling
between every wait). current_time_reminder: 0 injections in the window — NOT the trigger.- Subagent spawns in the entire session: 0 —
multi_agent_v2was never
used for actual multi-agent work; only itswaittool was pulled in.
What steps can reproduce the bug?
- Enable
features.multi_agent_v2(default in this build). - Start a long-running external job (>= 5 min) via a tool (e.g. an MCP
server that runs a build/boot). - Ask the agent to wait for completion.
- Observe repeated
tool_name="wait"calls at ~50s each, with a full
model sampling request between each, for the whole duration. - Check token/quota usage: it grows roughly linearly with the wait time
instead of staying near the cost of a single blocking call.
Log query used to confirm (SQLite over ~/.codex/logs_2.sqlite):
Look for rows where feedback_log_body contains
tool_name="wait" and handler_duration_ms=50004, spaced ~50s apart,
while codex_api::sse::responses rows continue throughout.
What is the expected behavior?
Waiting for a 10-minute job should NOT cost ~12 full re-samples. Any of:
- The built-in
waittool should allow a single long wait (respect a
caller-supplied duration up to a high ceiling), so waiting for a
10-minute job is ONE tool call and ONE re-sample. - When a
waitcall hits the internal cap, the runtime should
transparently re-arm the timer and keep waiting WITHOUT round-tripping
through a full model sampling request, unless something actually
changed. - At minimum,
max_wait_timeout_ms/default_wait_timeout_msshould be
documented and user-configurable in~/.codex/config.toml, and the
built-inwaittool should be independently disable-able, so a
workflow that relies on a server-side blocking MCP wait can opt out of
the re-sample storm.
Actual behavior: wait silently caps each call at ~50s and re-enters
model sampling on every expiry, multiplying token/quota usage roughly
linearly with wait duration, even when a zero-cost blocking wait is
available.
Additional information
Same MCP config on a different client shows NO problem (isolates the bug to codex-cli):
With the EXACT same MCP server and the same server-side blocking wait
tool, another agent CLI (Anthropic's Claude Code) makes ONE blocking MCP
call and suspends cleanly for the whole job — no periodic re-sampling, no
measurable token burn. Only codex-cli reaches for its built-in sleep/wait
tool and loops it. This strongly indicates the burn is codex-cli's
client-side behavior (the model reaching for the built-in sleep tool plus
its short per-call cap), NOT the MCP server or the workload.
Underlying mechanism — code_mode (verified): MCP tools are invoked
through a JS runtime ("cells"): tool dispatch is logged as
dispatch_tool_call_with_code_mode_result. A long-running MCP call (here
a server-side blocking wait) becomes a background cell, and the model
polls it with the built-in wait tool, whose args are
{"cell_id": ..., "yield_time_ms": ..., "max_tokens": ...}. Each
yield_time_ms chunk (observed 10s / 25s / 30s / 50s) ends the tool call,
returns to the model, and triggers a fresh full sampling request. This is
exactly why a client WITHOUT code-mode (calling MCP directly/blocking)
shows no burn on the same MCP server.
Feature toggles tried:
multi_agent_v2 = false(verified by restart + probe): tool still
fires; it only removed a cap override, dropping the per-call yield from
~50s to the ~30s default — i.e. MORE frequent re-samples
(counterproductive).current_time_reminder = false(verified by restart + probe): tool
still fires afterwards (observed
wait {"cell_id":"1","yield_time_ms":25000,...}in a post-restart
session).
Thewaittool args (cell_id/yield_time_ms/max_tokens) and the
dispatch_tool_call_with_code_mode_resultdispatch tie the tool to
code_mode, not to those features.
Relevant config schema (extracted from the codex binary):
MultiAgentV2ConfigToml:
min_wait_timeout_ms
max_wait_timeout_ms <-- observed effective cap ~50000
default_wait_timeout_ms
...
FeatureToml variants include: multi_agent_v2, current_time_reminder, ...
Suggested fixes:
- Expose
max_wait_timeout_ms/default_wait_timeout_msas documented
config.tomlkeys. - Allow disabling the built-in
waittool independently (confirm the
untaggedFeatureTomlbool form disables it). - Prefer re-arming the wait timer internally over a full re-sample when
nothing has changed.
WORKING WORKAROUND (live-verified): route the MCP namespace directly
(not through a code-mode cell) via direct_only_tool_namespaces:
[features.code_mode]
direct_only_tool_namespaces = ["mcp__chimera_bridge"]
(Namespace string is exactly what Codex logs internally:
namespace: Some("mcp__chimera_bridge").) After this, a long blocking MCP
call dispatches via codex_mcp::connection_manager (direct) and the model
blocks silently on it — 0 SSE sampling, 0 wait/cell polls — until it
returns, i.e. ONE call, ONE re-sample. This confirms the burn is caused by
code-mode wrapping MCP calls into polled cells.
NOTE what does NOT work: code_mode = false (still wrapped — the MCP call
still dispatched via code_mode_result and was polled), multi_agent_v2 = false, current_time_reminder = false (all verified ineffective, see
above). The per-namespace direct_only_tool_namespaces is the only lever
that worked. Request: please document this as the supported way to make a
long-running MCP tool call block instead of being polled, and/or make
blocking (wait=true-style) MCP calls not incur a re-sample per cell
yield by default.
Possibly related (different bug): #23807 (codex-cli stalls for
exactly 300s between tool result and next model request). Not the same
root cause, but also in the wait/timeout area.
Activity
github-actions commentedon Jul 12, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
MPC7500 commentedon Jul 12, 2026
Related to #31935: the same short-increment polling burn also hits server-side-blocking MCP tool calls, not just shell/subagent waits. With code_mode on, a bridge tool called with wait=true (blocks ~840s server-side) is still wrapped into a polled cell and re-sampled every ~30–50s. Setting [features.code_mode] direct_only_tool_namespaces = ["mcp__"] routes that namespace directly (dispatch via codex_mcp::connection_manager) so the model blocks silently on one call (0 SSE, 0 wait polls) — a usable workaround today until the 60s limit from #31935 is lifted. For contrast, another CLI (Claude Code) on the exact same MCP server makes one blocking call with no polling, so this is client-side behavior, not the MCP server.
taylorsatula commentedon Jul 12, 2026
This could be what happened to me. The model sat and waited for a long running subagent for like 20 minutes and burnt 17% usage.
MPC7500 commentedon Jul 13, 2026
Yes, that's the same root cause. When the parent "waits" for a long-running subagent it doesn't actually block — it yields on the subagent via the built-in wait tool in ~30–50s slices, and every yield is a full re-sample of the whole context. So the cost scales with wait-duration ÷ yield-interval: ~20 min ÷ ~30s ≈ 30–40 re-samples, which lines up with your 17%.
One caveat: my MCP-specific workaround (direct_only_tool_namespaces) won't help the subagent case, since a subagent isn't an MCP namespace. The subagent wait is bounded by multi_agent_v2's max_wait_timeout_ms — I checked the binary and the yield tool literally declares "must be between 1 and <max_wait_timeout_ms>", and in my logs the yield never exceeds 50s, so that's the current ceiling. Raising it allows a longer yield (fewer re-samples), but it only helps if the model actually chooses a bigger yield — and the ~60s blocking-wait guidance from #31935 likely keeps it small regardless. So I'd treat max_wait_timeout_ms as "necessary but maybe not sufficient," and the #31935 fix (let the parent block once for the whole wait) as the real lever. Same underlying re-sample-per-yield behavior you hit, either way.
gary23w commentedon Sep 4, 2026
Concrete runtime design: a predicate watch, not a timer yield
The square-tire behavior here comes from treating elapsed time as an event worth returning to the model. In current main, both the initial code-mode execution and subsequent wait use
ObserveMode::YieldAfter(...), and the timeout is capped:That makes "50 seconds passed; nothing changed" a tool result, then a fresh inference. A larger timeout lowers the multiplier but does not fix the control model.
A useful reference is nl-veil's typed
polloperation. One runtime call samples file growth/patterns, URL readiness, ports, processes, commands, or a swarm; it checks Stop/steer at each sample boundary and returns a typed verdict:That implementation still bounds one call to 180 seconds, so it is a reference for where predicate evaluation belongs, not the final Codex design. Codex can go further by making the watch a durable app-server primitive:
Acceptance tests should make the economic invariant explicit:
This would fix the quota burn at its ownership boundary and also give Desktop a truthful "waiting on X" state instead of making the model narrate repeated sleeps.
MPC7500 commentedon Sep 4, 2026
One additional data point from current
main: code-mode already has a non-timer observation primitive that looks relevant to this design.InProcessCodeModeSessioncurrently exposes both timer-based and pending-aware paths:[execute()](https://github.com/openai/codex/blob/main/codex-rs/code-mode-runtime/src/service.rs)usesObserveMode::YieldAfter(...).[wait()](https://github.com/openai/codex/blob/main/codex-rs/code-mode-runtime/src/service.rs)also usesObserveMode::YieldAfter(...).[execute_to_pending()](https://github.com/openai/codex/blob/main/codex-rs/code-mode-runtime/src/service.rs)usesObserveMode::PendingFrontier.[wait_to_pending()](https://github.com/openai/codex/blob/main/codex-rs/code-mode-runtime/src/service.rs)likewise observesPendingFrontier.That seems important because
PendingFrontieris already a semantic/runtime frontier rather than “N milliseconds elapsed”. In other words, the runtime appears to already contain part of the machinery needed to decouple runtime progress from timer-driven model wakeups.My understanding is that this was introduced earlier for pending-aware code-mode execution, not specifically to solve the quota-burn issue here, and it is not currently a drop-in replacement for
wait(): a pending frontier is not yet a durable predicate such as “process exited”, “tool completed”, “log matched”, etc.But it suggests the missing piece may be above the low-level cell runtime rather than requiring a completely new execution model:
The important part would be that
Pendingitself must not simply become another model-visible polling result, otherwise this only replaces:with:
and preserves the same economic problem.
So I think the proposed durable
watchprimitive fits the direction of the current runtime quite well:PendingFrontiercan provide a semantic suspension boundary, while the app-server owns the durable predicate, reconnect/cancel/deadline semantics, and the policy for when the model is actually woken.The invariant would still be the same:
For a healthy ten-minute operation with no meaningful state change, internal sampling may happen many times, but there should still be only the model call that starts the operation and the one that receives its terminal result.