fix(autopilot): offload completion result reads - #3783
Conversation
When the stage loop finishes a plan it builds one final summary message by re-reading every captured stage-result file off disk. Those reads run synchronously inside the async loop, so the gateway's single event loop is blocked for their filesystem duration -- once per completed stage, all of it arriving at the same moment the plan ends. Snapshot the recorded result paths on the loop thread (the tracker's result map is live orchestration state) and hand only that immutable sequence to a worker, which performs the reads and returns the derived excerpts. Summary assembly, redaction, the slot append, the broadcast, and the SEL record all stay on the loop. A plan that captured nothing skips the worker hop entirely. Excerpt selection, the 120-char cap, ordering, separator-line skipping, and the "done" fallback for an unreadable result are unchanged.
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Design Review (Fable 5, fork) — ✅ PASSAdvisory design-level review of Design-Verdict: PASS Sound, minimal offload following the established #3772 pattern — snapshot on loop, immutable data to worker, semantics pinned by tests. Watch
Suggestions
[DESIGN-REVIEWED] 64c34d3 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsThe diff is a faithful, behavior-preserving refactor. I verified the new No findings. [OPUS-REVIEWED] 64c34d3 |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSAdvisory premise-level review of I've read the contract, intent, patch, and the base First-Principles-Verdict: CONCERNS The offload is the recorded #3772 decision applied at its declared sibling site — but the same function still does heavier sync disk I/O per stage, unmentioned. What this change shipsIntent: keep the gateway event loop responsive while the plan-completion summary re-reads stage results off disk — a FIX (scheduling defect).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 64c34d3 |
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: perf (2 files). Criteria: no conflict, no requested changes, no security surface, AI reviewers green. Category: offloads completion-result file reads off the autopilot event loop — pure performance fix with matching test, no behavior change.
When the stage loop finishes a plan it builds one final summary message by re-reading every captured stage-result file off disk. Those reads run synchronously inside the async loop, so the gateway's single event loop is blocked for their filesystem duration -- once per completed stage, all of it arriving at the same moment the plan ends. Snapshot the recorded result paths on the loop thread (the tracker's result map is live orchestration state) and hand only that immutable sequence to a worker, which performs the reads and returns the derived excerpts. Summary assembly, redaction, the slot append, the broadcast, and the SEL record all stay on the loop. A plan that captured nothing skips the worker hop entirely. Excerpt selection, the 120-char cap, ordering, separator-line skipping, and the "done" fallback for an unreadable result are unchanged.
Problem / Motivation
_stage_loopinsrc/kiro_crew/dashboard/chat_orchestrator.pyis anasync defthat drives a plan stage by stage. When the
forloop completes without a break— every stage ran — it falls into the
elsebranch and builds the terminal"✅ All N stages complete." summary.
That summary is assembled by re-reading the captured stage results off disk:
safe_read_fileis synchronous. It callsos.path.realpath, checksis_sensitive_path, thenos.open(..., O_NOFOLLOW)and reads the file tocompletion. Nothing about that call yields to the event loop, and the branch runs
one such call per completed stage — so a plan's entire final read set is issued
back to back, on the loop, at the moment the plan ends.
This is a different lifecycle boundary from the per-stage previous-result reads
in #3772. Those happen before each next stage and feed that stage's prompt;
these happen after the whole loop and feed the final user-visible message. The
caller branch, the timing, the consumer, and the failure semantics are all
separate, so this is verified on its own.
Why it matters
Scheduling only. The gateway runs a single asyncio event loop, and for the
duration of these filesystem operations that loop cannot advance anything else —
no WebSocket broadcast, no other slot's turn, no HTTP handler. The number of
reads scales with the plan's stage count, and they all land in the same
uninterrupted window.
No latency figure is claimed here; none was measured.
What changed
The read set is moved off the loop without moving any state with it.
(stage number, path)pairs out oftracker._stage_resultsinto an immutable tuple. The tracker's result map islive orchestration state the loop mutates via
record_stage_result, so it isread on the loop thread only.
_completion_excerpts(...)takes that tuple,performs the
safe_read_filecalls, derives each stage's excerpt, and returnsa plain
dict[int, str]. It touches no slot, no tracker, and no live flag.summary_lines, then runsredaction,
slot.append,broadcast_ws, and the SELauto_run_completedrecord exactly as before.
A plan that captured no result paths skips the
asyncio.to_threadhop entirelyrather than paying for an empty round trip.
Semantics are preserved deliberately: excerpt selection (first non-empty line
that does not start with
───), the 120-character cap, stage ordering, theper-stage containment of a read error, and the
— donefallback for a missing orunreadable result are all unchanged. A stage whose read fails is simply absent
from the returned mapping, which reproduces the old
excerpt = ""fallback.No new executor, abstraction, caching, retry, or error-handling policy is
introduced, and no truncation limit or result content changes.
Testing
New
test/test_completion_result_read_off_loop.pydrives the real_stage_loopto plan completion and wraps the production
safe_read_filebinding inchat_orchestratorwith a recorder that delegates to the real implementation —so the recorded thread is the thread that genuinely opened and read the file, not
merely one that reached a call site. The observation is scoped to the stage-result
paths, and a non-empty guard makes a vacuous pass impossible.
Deterministic fail-before / pass-after on this branch:
test_completion_result_read_runs_off_the_loop_threadassert 62540 not in [62540, 62540, 62540]test_completion_reads_every_captured_stage_resultThe remaining eight cases are preservation coverage and pass on both trees, which
is what pins the behaviour this change must not alter: ordering and titles,
separator-line skipping, the 120-char cap, blank-result fallback, a deleted
result file degrading to
— donewhile its sibling still resolves, credentialredaction of the excerpt, the no-worker-hop path when nothing was captured, and
terminal
_auto_runstate.Also green:
flake8,isort --check-only,mypy --platform linuxon thechanged module ("Success: no issues found"),
scripts/docs_lint.py,BRAND_BASE_REF=origin/main scripts/check_brand_name.py,git diff --check.Screenshots / video
Backend scheduling only — the completion message's text is byte-identical.
Related Issues
None. Self-discovered while validating the sibling event-loop I/O findings in
#3772.