Repository navigation
Conversation
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of First-Principles-Verdict: CONCERNS The separate marker file manufactures its own complexity: identity-triple matching, recycled-PID logic and symlink/FIFO hardening all exist only because the ready-record lives outside the dump. Not justified as shipped
What this change shipsInventory (8 items) — 6 justifiedIntent: stop the post-stall recovery boot from being the slowest boot when the stalled instance had already finished starting up (#6590) — a FIX.
[FIRST-PRINCIPLES-REVIEWED] f630e71 |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Verify the repro host's cycle also breaks when posture stays tight/critical, where an exonerated dump still keeps the 2s mild stagger rather than zero. Suggestions
[DESIGN-REVIEWED] f630e71 |
Opus 5 Review (fork) — ✅ no blocking findingsReviewed Review detailsOne non-blocking regression: the new healthy-boot marker lands inside the crash-dumps directory and silently evicts a real stall dump from every diagnostics bundle. FINDING — src/kiro_crew/dashboard/crash_dump_store.py:913 — [OPUS-REVIEWED] f630e71 |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
c81a3a9 to
bb14bd1
Compare
bb14bd1 to
4f14e97
Compare
4f14e97 to
7ba66de
Compare
|
🤖 Kiro Crew [operator: dwu96#8c08bee0]: This PR has been inactive for 7+ days with failing CI. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author. Assessment: nothing in this PR's own code is failing. All six review lanes are ✅ on the current head
Fix plan: rebase onto current main, force-push with a SHA-pinned lease, re-run the suite, and report. If the macOS reds survive the rebase they are the repo-wide failures above and belong to #13030 / #13095, not here — I'll say so rather than touching unrelated tests. If you'd prefer I don't touch this PR, add the |
|
@GoZippy Maintainer sweep (2026-09-24, on behalf of @bolichen97): this PR carries State: GitHub still reports a clean merge, but the fork head is 1060 commits behind |
Cautious boot staggers the startup battery when a recent loop-stall dump says the last instance wedged. It reads the dump's age and the host's current memory posture, but not whether the instance that wrote the dump had ever finished starting up -- so a gateway that served for nine hours and then stalled got the same treatment as one that wedged mid-boot. The start immediately after a stall is therefore the slowest one, on exactly the host that most needs to come back, and on a host already near the stall threshold that stagger can trip the watchdog again and write another dump. Reported upstream as kirodotdev#6590, with the cycle observed directly: two-minute lifetimes while it held, and 7.4h, 9h and 15.5h once the dumps were moved aside by hand. The stagger exists to protect the startup BATTERY. An instance that reached a serving state got the whole battery away before it stalled, so the battery is not what wedged it. DashboardState.ready is the codebase's own definition of that point, so both sites that publish it now record the instance's identity triple -- the same (pid, domain, start_id) a dump header carries -- and the next boot asks whether the dump it found was written by a process that had reached it. A downgrade, not an off-switch: a host still reading tight or critical keeps a mild stagger, because current pressure is its own signal and is not answered by what the previous instance managed to do. The match is deliberately strict, because the two errors do not cost the same. A false "reached healthy" removes the stagger and is how a host that wedges during startup re-wedges; a false negative only costs the slower boot that happens today. So a missing marker, a start identity absent on either side, a recycled pid, a marker from another host and the legacy header that carries no identity at all all answer no. That also makes the change inert on any platform without a start identity, and leaves every existing decision path untouched. The marker is written off-loop: it is a small write, but the data home can be on network storage, and this runs on the gateway loop. Recording it is best-effort and never raises -- a data home that cannot take the write must not fail a gateway that is otherwise healthy. A missing marker reads as "did not reach healthy", which is the conservative answer.
Three ways the marker could cost the boot it exists to speed up. The write was awaited. `await asyncio.to_thread(...)` keeps the event loop responsive but not the coroutine, and this coroutine's RETURN is what publishes KIROCREW_READY -- so a data home on a stalled network share held readiness open indefinitely, and a supervisor waiting on that line respawns into the same hang. Nothing about the marker belongs to this boot, so it is dispatched as a tracked background task instead. The read followed and blocked. The marker sits in the dumps directory, which the agent can write to, and `read_text` follows a symlink planted at that name and blocks opening a FIFO. The enclosing except is fail-OPEN and cannot catch a hang. The read now uses O_NOFOLLOW and O_NONBLOCK where the platform has them, checks S_ISREG everywhere, and is bounded to one short line, so anything that is not a plain file reads as absent -- the same conservative answer a missing marker already gives. The write followed too, which is worse than reading wrong. Its temp name was derived from the PID, and PID 1 in a container is deterministic across restarts, so a symlink can be waiting at that name before the process that writes through it exists -- and a plain write follows it and truncates the target. It now publishes through atomic_write, whose temp file is unique and O_EXCL. Also drops an issue number from a test docstring, and prunes three entries that this change made black-clean from the formatting baseline.
7ba66de to
f630e71
Compare
|
Thank you, @GoZippy, for this fix — it's a genuinely good one. Diagnosing that cautious boot was slowing exactly the recovery it exists to protect, and tying "did the previous instance actually reach a serving state?" to To get it merged, your two commits (authorship unchanged) have been rebased onto current Closing with thanks. Please follow #16954 for the rest of the way to merge. |
Problem / Motivation
Cautious boot staggers the dashboard's startup battery when a recent loop-stall
dump says the previous instance wedged. It decides from the dump's age and the
host's current memory posture, but not from anything about the instance that
wrote the dump. So a gateway that served for nine hours and then stalled is
treated identically to one that wedged part-way through starting up.
The result is that the slowest start this code can produce is the one
immediately after a stall, on exactly the host that most needs to come back.
On a host already near the stall threshold the stagger is itself enough to trip
the watchdog again, which writes another dump, which justifies another stagger.
Observed directly while reproducing #6590: two-minute
instance lifetimes for as long as the cycle held, and 7.4h, 9h and 15.5h
lifetimes once the dumps were moved aside by hand.
Why it matters
The stagger exists to protect the startup battery. An instance that reached a
serving state already got the whole battery away before it stalled, so the
battery is demonstrably not what wedged it, and slowing the battery cannot
help. Worse, the failure is self-sustaining: the mitigation manufactures the
evidence that triggers the mitigation. An operator sees a gateway that will not
stay up and no indication that the recovery path is what is holding it down.
What changed
DashboardState.readyis this codebase's own definition of "this instancefinished starting up", so both sites that publish it now record the instance's
identity triple, the same
(pid, domain, start_id)a dump header alreadycarries. On the next boot, cautious boot asks whether the dump it found was
written by a process that had reached that point, and staggers only when the
answer is no.
Every existing decision path is untouched: a dump from an instance that never
became ready staggers exactly as it did before.
Recording the marker is best-effort and never raises. The data home can be on
network storage and this runs on the gateway loop, so the write is off-loop,
and a data home that cannot take it must not fail a gateway that is otherwise
healthy. A missing marker reads as "did not reach healthy", which is the
conservative answer and preserves today's behaviour.
Tests
test/test_cautious_boot.py— 31 passed, run on Windows 10 Pro 19045 withPython 3.12. The new cases cover a dump from a ready instance (no stagger), a
dump from an instance that never became ready (stagger, as today), a missing
marker (stagger), and an unwritable data home (no raise, stagger).
Manual verification
Reproduced on the reporting host: with the cycle in place, instance lifetimes
were ~2 minutes; with the dumps moved aside by hand, 7.4h, 9h and 15.5h. The
marker makes the second outcome the automatic one.
Pattern harvest
Not generalizable: the defect is that a mitigation read one property of a
failure (a dump exists, and how old it is) and not the property that decides
whether the mitigation applies (what the failing instance had already
accomplished). The general shape — a self-sustaining mitigation whose own cost
manufactures its trigger — is worth a reviewer's attention, but there is no
mechanical rule here. No lint can tell which property a mitigation should have
read, and a rule against "recovery paths that can cause the condition they
recover from" would have no decidable predicate.
Related Issues
Part of #6590. Does not close it: #6590 asks for a
supervisor as well as this, and the supervisor half is a separate pull request.
Checklist