Skip to content

fix(autonudge): create-only monitor arms no longer deadlock on a stopped automation - #8515

Merged
pepmach merged 1 commit into
mainfrom
fix/monitor-create-only-stalled
Sep 5, 2026
Merged

fix(autonudge): create-only monitor arms no longer deadlock on a stopped automation#8515
pepmach merged 1 commit into
mainfrom
fix/monitor-create-only-stalled

Conversation

@pepmach

@pepmach pepmach commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

monitor_start and monitor_watch are create-only so a racing arm cannot clobber a live automation — but the occupancy test read ANY retained row on the slot as an occupant, including inactive ones. That deadlocks the product's own documented remedy:

  1. A babysit loop's tool approval goes unanswered → the loop is deactivated and retained with stopped_reason="approval_stalled" (deliberate: inspectable/restartable).
  2. monitor_update refuses to revive an approval-stalled loop and says "re-enable auto-approve, then re-arm it with monitor_start".
  3. monitor_start then refuses too — MonitorUpdateConflict: session already has an automation — because the retained inactive row still occupies the slot.

Observed live: a babysit re-arm at 2026-09-04 05:33:48 UTC bounced off its own predecessor's approval-stall tombstone (session-directive DENIED at apply for session_key='dashboard:chat-105-…' kind='monitor_start'), with the same denial on two other dashboard slots in the same gateway log. The structured twin exists too: monitor_stop retains a terminal record for inspection, after which monitor_watch in the same session is refused forever.

Fix

A deliberate split, per GPT's round-1 security finding (its first cut widened the shared predicate and would have let dashboard REST creates silently delete retained inspection records):

  • The service adds gain an explicit replace_stopped opt-in (default False), threaded through authorize_and_add_nudge.
  • Only the two session-directive appliers (monitor_start, monitor_watch) set it: their create-only refusal narrows to ACTIVE records, so a retained inactive row — approval-stalled, capped, budget-spent, or a terminal monitor kept for inspection — is replaced atomically inside the same locked write. This is the deadlock fix, scoped to the exact path the remedy message points at.
  • Dashboard REST creates keep any-record occupancy: replace_existing=False alone still refuses over any retained row, active or stopped, so inspection history is never discarded by a create — pinned byte-identically by two new tests.
  • The wake-in-flight guard runs on every replacement path, so a terminal record whose accepted wake still awaits completion evidence keeps its own refusal instead of having its correlation orphaned.

The monitor_start tool description and docs/system-specs/modules/learn-cron-dashboard.md are synced in the same commit.

Tested

Red-first, proven by stashing the production fix:

  • test_create_only_add_replaces_an_inactive_approval_stalled_loop — red on unfixed code with the exact live error, green after.
  • test_create_only_add_monitor_replaces_a_terminal_monitor — the monitor_stopmonitor_watch deadlock, red before / green after.
  • test_create_only_add_still_refuses_a_terminal_row_with_an_inflight_wake — pins the transition from the blanket refusal to the wake-in-flight refusal.
  • test_create_only_add_still_refuses_an_active_legacy_loop — a LIVE automation refuses even on the replace_stopped path.
  • test_dashboard_create_only_add_preserves_a_stopped_row / test_dashboard_create_only_add_monitor_preserves_a_terminal_record — the GPT-finding pins: dashboard-style creates (no opt-in) refuse over inactive rows and leave the store byte-identical.

Focused suites: test_autonudge.py + test_autonudge_stop_auth.py + test_monitor_mcp.py + test_driver_session_directives.py — 282 passed. One deselected failure, TestSentinelPathRepair::test_unnormalized_path_escaping_legacy_is_preserved, reproduces identically on pristine origin/main on this host (a /local/home realpath artifact) and is unrelated.

Gates: black ratchet, subprocess-encoding ratchet, isort, flake8, mypy (CI-parity venv, 1,287 files), docs-lint — all clean on the committed head.

Pattern harvest

Rule candidate: an occupancy/conflict guard keyed on record EXISTENCE where the protected invariant is record LIVENESS (or vice versa) — whenever a refuse-if-present check is added over a store that deliberately RETAINS terminal rows, require a test that arms-after-stop, or the retention feature and the guard deadlock each other. Reviewable mechanically: any new raise on _find_by_* truthiness deserves an active/terminal-state audit. The second half of this defect (a shared chokepoint fix silently widening sibling callers' semantics) is the existing enumerate-the-callers discipline and needs no new rule.

@pepmach
pepmach requested a review from a team as a code owner September 4, 2026 18:21
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of f6ce68128c6a187f3f241d664d14a8b3036f730d — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

Sound deadlock fix with a fail-closed evidence split, but the description claims a monitor_stopmonitor_watch fix the shipped ruling deliberately does not deliver.

Watch

  • Phantom description claim. The problem statement names the "structured twin" — "monitor_stop retains a terminal record … after which monitor_watch in the same session is refused forever" — and the Tested section cites test_create_only_add_monitor_replaces_a_terminal_monitor ("red before / green after"). No such test exists in the diff; stop_monitor records USER_STOP, and test_replace_stopped_preserves_a_user_stopped_monitor pins that this re-arm is still refused. That residual deadlock (agent-facing remedy is only the dashboard restart route) may be the right ruling, but the description should say so instead of claiming it fixed.
  • Cap re-arm is a new agent capability. Including cycle_cap/runtime_budget/BUDGET in the replaceable set lets the automation's own final wake turn issue monitor_start and chain past its spent bound — the caps become per-incarnation, not per-session backstops. Each arm still passes authorize_and_add_nudge, and it is disclosed in the tool description, but confirm admission/authz is the intended runaway bound, since the observed deadlock only required the approval-stall and terminal-subject cases.

Suggestions

  • The version/evidence/wake-in-flight guard triplet is now duplicated across the legacy and monitor add paths with a shared exception text; extracting one shared refusal helper would keep the two arms from drifting when the next stop reason lands.

[DESIGN-REVIEWED] f6ce681

@github-actions github-actions Bot added the readiness: action required A blocking check or review needs attention label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of f6ce68128c6a187f3f241d664d14a8b3036f730d and found no blocking issues.

This comment is updated in place on each push.

Review details

FINDING -- src/kiro_crew/autonudge.py:174 -- MonitorOutcome.USER_STOP is excluded, so monitor_stopmonitor_watch still returns 409 despite the stated deadlock fix -> Fix: include MonitorOutcome.USER_STOP in the replaceable outcomes. (origin: validation)
[GPT-REVIEWED] f6ce681

False positive or not applicable? A repository writer can comment:
/ai-review override gpt f6ce68128c6a187f3f241d664d14a8b3036f730d: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of f6ce68128c6a187f3f241d664d14a8b3036f730d — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims verified: the remedy message that creates the deadlock is real (session_directive_apply.py:488 — "re-enable auto-approve, then re-arm it with monitor_start"), both occupancy guards over the store are fixed with no unfixed siblings, replace_stopped has exactly 2 real setters, and the quarantine constant (MONITOR_STOP_INVALID_RECORD, synthesized at monitoring/models.py:579) exists as claimed. One description–diff gap found. Final review:

First-Principles-Verdict: CONCERNS

The description claims the monitor_stop → monitor_watch deadlock is fixed red/green; the diff pins the opposite — USER_STOP rows still refuse the re-arm.

What this change ships

Intent: let a session re-arm its own automation after the system stopped it, which the retained stop record previously deadlocked — a FIX.

  1. Directive re-arm now replaces a system-stopped automation instead of erroring — justified (live defect; remedy message at session_directive_apply.py:488).
  2. User stops, manual pauses, tombstones, quarantined records still refuse, new "retained as evidence" error — justified, partially contradicts description.
  3. Dashboard creates keep any-record 409, byte-identical store — justified (inspection evidence).
  4. Stopped future-version records get their own "newer gateway" refusal — undeclared rider, justified (data-loss guard).
  5. replace_stopped parameter threaded through service and authorizer — justified; 2 consumers (grepped: session_directive_apply.py:262,332).
  6. Inactive terminal rows with an in-flight wake fall through to the wake refusal — declared, justified.
  7. monitor_start tool description and module spec synced — mandated by AGENTS.md same-commit rule.

Watch

  • Description names test_create_only_add_monitor_replaces_a_terminal_monitor ("the monitor_stop → monitor_watch deadlock, red before / green after"); no such test is in the diff, and test_replace_stopped_preserves_a_user_stopped_monitor asserts that exact sequence still refuses. The "structured twin" the description reports as observed remains unfixed unless "its owner must clear it first" names a path reachable from the stuck session — verify one exists.
  • Fix bullet "or a terminal monitor kept for inspection — is replaced" is contradicted by the same test; the shipped rule is narrower (system-imposed outcomes only). Description is stale against the owner ruling the tests cite.

[FIRST-PRINCIPLES-REVIEWED] f6ce681

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed f6ce68128c6a187f3f241d664d14a8b3036f730d — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] f6ce681

Verdict parsed from the review's SHA-scoped output markers for commit f6ce68128c6a187f3f241d664d14a8b3036f730d.

False positive or not applicable? A repository writer can comment:
/ai-review override fable f6ce68128c6a187f3f241d664d14a8b3036f730d: <one-sentence reason>

@pepmach
pepmach force-pushed the fix/monitor-create-only-stalled branch from 7e0e3ed to 14bfb5d Compare September 4, 2026 18:54
@pepmach

pepmach commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT blocking finding (round 1)

  • FIXED span=1c5c73f18adb — "Shared create-only checks delete retained dashboard records." Correct: the first cut changed the shared predicate at the service chokepoint, so the two dashboard REST creates (replace_existing=False) inherited replace-inactive semantics and would have silently deleted retained inspection records their contract documents as a 409. Fixed as prescribed at 14bfb5d6d: a replace_stopped opt-in threaded through authorize_and_add_nudge, set only by the two session-directive appliers; dashboard creates keep any-record occupancy, pinned byte-identically by test_dashboard_create_only_add_preserves_a_stopped_row and test_dashboard_create_only_add_monitor_preserves_a_terminal_record.

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@pepmach
pepmach force-pushed the fix/monitor-create-only-stalled branch from 14bfb5d to aef7698 Compare September 4, 2026 19:28
@pepmach

pepmach commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT blocking finding (round 2)

  • FIXED span=1c5c73f18adb — "Stopped replacement deletes opaque future-version monitors." Correct: _load() deliberately retains a future-version monitor record inactive across a downgrade so an upgrade can resume it, and the replace_stopped path would have let a directive re-arm on the downgraded gateway delete that opaque state. Fixed at aef7698ed: both add paths refuse a create-only replacement when the existing monitor's version != MONITOR_STATE_VERSION. Red-first: test_replace_stopped_never_deletes_a_future_version_monitor reproduced the deletion (DID NOT RAISE) on the prior head and pins both add paths refusing while the record survives object-identically.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@pepmach
pepmach force-pushed the fix/monitor-create-only-stalled branch from aef7698 to 9968592 Compare September 4, 2026 20:35
@pepmach

pepmach commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT blocking finding (round 3, resolved under owner ruling)

  • FIXED span=1c5c73f18adb — "Inactive-only check deletes source-owned stop tombstones." Third consecutive blocking round on this span, so it was escalated to the owner per the repository's same-span rule; the owner ruled for the stop-reason allowlist. Verified before accepting: auto_research/handlers.py consumes retained AUTONUDGE_STOP_REASON rows at three sites to distinguish deliberate completion from crash cleanup. Implemented at 9968592f3: replace_stopped displaces only system-imposed stops (approval_stalled, cycle_cap, runtime_budget, terminal-subject records); consumer-recorded stops — research tombstones, manual pauses, USER_STOP, SESSION_CLOSE — are preserved with a distinct evidence-retention refusal, and unknown stop reasons fail closed to preserved. Red-first: test_replace_stopped_preserves_a_research_tombstone and test_replace_stopped_preserves_a_manual_pause reproduced the deletion on the prior head.

@pepmach pepmach changed the title fix: create-only monitor arms no longer deadlock on a stopped automation fix(autonudge): create-only monitor arms no longer deadlock on a stopped automation Sep 4, 2026
@pepmach
pepmach force-pushed the fix/monitor-create-only-stalled branch from 9968592 to 2e9c6e4 Compare September 4, 2026 21:24
@pepmach

pepmach commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT round 4 (blocking)

  • FIXED span=1c5c73f18adb — "Quarantined malformed monitor records are replaceable." Correct, and an instance of the owner's ruled fail-closed principle rather than a new question: _load() synthesizes a BLOCKED outcome with stopped_reason=invalid_monitor_record precisely to retain the raw corrupt payload for inspection, so the BLOCKED bucket over-included it. _stopped_row_is_replaceable now excludes MONITOR_STOP_INVALID_RECORD explicitly (shipped at 2e9c6e486). Red-first: test_replace_stopped_preserves_a_quarantined_monitor_record reproduced the deletion (DID NOT RAISE) on the prior head. Fixed under the existing ruling rather than re-escalated.

@pepmach
pepmach force-pushed the fix/monitor-create-only-stalled branch from 2e9c6e4 to 1ef558d Compare September 4, 2026 23:45
@pepmach

pepmach commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT round 4 (advisory, tool description)

  • FIXED span=d255bda2d10b — the monitor_start tool description's "a stopped or expired automation is replaced" overstated the ruled semantics. It now reads: "a system-stopped or expired automation — an approval stall, a spent cap or budget, a finished subject — is replaced by the new arm; manual pauses, user stops and retained tombstones are preserved." Shipped at 2e9c6e486.

@pepmach

pepmach commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT advisory (stale rationale comment)

  • FIXED span=3c5c68f64dc6 — "monitor_stop → monitor_watch remains refused because USER_STOP is preserved" flagged the structured add site's comment still citing the pre-ruling deadlock rationale. Correct: the owner's ruling deliberately preserves USER_STOP records, with the dashboard restart route as the sanctioned successor path. The comment now states exactly that (shipped at 1ef558da5); no behavior change.

monitor_start and monitor_watch became create-only so a racing arm
cannot clobber a live automation, but the occupancy test read ANY
retained row as an occupant. A loop stopped for approval_stalled is
retained inactive on purpose, and monitor_update refuses to revive it
while naming monitor_start as the remedy -- which then refused too,
deadlocking the session's only re-arm path (observed live on a babysit
re-arm). Under the owner's ruling, the directive re-arm path gains a
replace_stopped opt-in that displaces only system-imposed stops (an
approval stall, a spent cap or budget, a finished subject); consumer-
recorded stops -- manual pauses, user stops, session-close retention,
research tombstones, quarantined records -- are preserved as evidence,
with unknown reasons failing closed to preserved. Dashboard REST
creates keep any-record occupancy, and a future-version record is
never replaceable.
@pepmach
pepmach force-pushed the fix/monitor-create-only-stalled branch from 1ef558d to f6ce681 Compare September 5, 2026 00:11
@pepmach

pepmach commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT advisory (authorizer docstring)

  • FIXED span=9297b665b175 — "'narrows the refusal to ACTIVE records' contradicts the inactive evidence-row refusals." Correct: the replace_stopped parameter comment in authorize_and_add_nudge still described the pre-ruling semantics. It now describes the system-stop allowlist and the preserved evidence classes (shipped at f6ce68128); no behavior change.

@pepmach

pepmach commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — Opus advisory (TARGET_UNAVAILABLE re-armability)

  • FIXED span=fd0d21da5fd4 — "TARGET_UNAVAILABLE is omitted from the re-armable set, re-creating the directive re-arm deadlock for a vanished/undeliverable subject." Correct under the owner's ruling: that outcome is system-imposed (dispatch failure, shadow NOT_FOUND) — no consumer authored it — so refusing it contradicted both the ruling and the predicate's own docstring. MonitorOutcome.TARGET_UNAVAILABLE is now in the re-armable tuple, pinned by test_create_only_add_replaces_a_target_unavailable_monitor (shipped at f6ce68128).

@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Sep 5, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 5, 2026
@pepmach

pepmach commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Disposition — GPT advisory (USER_STOP re-armability)

  • REBUTTED span=3c5c68f64dc6 — "include MonitorOutcome.USER_STOP in the replaceable outcomes." This asks to reverse an explicit owner ruling made on this PR after a three-round same-span escalation: USER_STOP is a consumer-recorded stop — monitor_stop retains it deliberately ("stopped and retained for inspection"), and this same reviewer's round-1 finding is what established that dashboard-visible retained records must not be silently deleted. The ruled taxonomy replaces only system-imposed stops; the sanctioned successor path for a user-stopped monitor is the dashboard restart route (conditional replace on the exact monitor id and config generation), which preserves the ABA guarantee a bare re-arm lacks. The one capability this forgoes — same-session directive re-arm after a deliberate user stop — was weighed by the owner and accepted. No change.

@buluoray buluoray left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against source at f6ce681, with the guards mutation-tested. 0 blocking findings.

One correction to the framing: this is not an async lock deadlock — there is no second acquisition and no await-under-lock cycle. It is a product-flow deadlock, and it reproduces from pre-fix source. session_directive_apply.py:488 tells the user to "re-enable auto-approve, then re-arm it with monitor_start", but monitor_start applied with replace_existing=False, and the pre-fix guard refused over any retained row (autonudge.py:1301,1397) — including the inactive approval-stall row that triggered the advice. The documented remedy had no working path. Circular, and real.

The fix is correctly scoped. replace_stopped narrows the create-only refusal to active rows, and it is set True at exactly two sites, both session-directive appliers (session_directive_apply.py:262,332), defaulting False everywhere else — so dashboard REST creates still 409 at any record. Both occupancy guards were fixed (autonudge.py:1376,1518), with no unfixed sibling.

The narrowed-guard race does not apply here, which was my main concern going in. In both _add_unserialized and _add_monitor_locked, _find_by_slot → decision → remove_sync → insert → persist all stay inside the same single async with self._lock hold; the lock scope is byte-for-byte unchanged and only the branch predicate moved, so the read-then-write stays atomic. Every new raise is inside the context manager, and the legacy path's except BaseException still restores the prior loop on a persist failure. No asyncio primitive is created, so the Loop-Bound Locks Gate is satisfied on substance, not just on the green check.

Mutation results:

Mutation Result
Revert to if not replace_existing: (reintroduce the deadlock) 3 RED, with the live session already has an automation error
_stopped_row_is_replaceable → always True (defeat the evidence allowlist) 4 RED (user-stop, research tombstone, manual pause, quarantine)
Disable the future-version guard 1 RED

Unknown stop reasons fail closed to preserved, the MONITOR_STOP_INVALID_RECORD quarantine is excluded, future-version retention is refused first, and learn-cron-dashboard.md plus the monitor_start tool description are synced in the same commit.

Non-blocking, and the first one is worth fixing before merge:

  1. The PR description does not describe this diff. ## Summary and ## Tested claim a monitor_stop → monitor_watch fix and cite test_create_only_add_monitor_replaces_a_terminal_monitor, which is not in the diff. The shipped code does the opposite on purpose: test_replace_stopped_preserves_a_user_stopped_monitor pins that a USER_STOP re-arm is still refused. The behavior is right — USER_STOP is consumer-recorded evidence, and the dashboard restart route (replace_existing=True, conditional on monitor id + config generation) is the successor path, so no residual deadlock — but the prose should match.
  2. Cap re-arm is a real capability expansion: cycle_cap / runtime_budget / BUDGET are re-armable, so an automation's own final wake can reset its own bound, making caps per-incarnation rather than per-session. Each arm still passes authorize_and_add_nudge admission and it is disclosed in the tool description — flagging for awareness, not as an objection.
  3. The version / evidence-allowlist / wake-in-flight refusal triplet is copy-pasted across both add paths with shared exception text; a shared helper would prevent drift.

Not verified: the full 282-test suite across the four cited files, the mypy/black/docs-lint gates, and the auto_research watchdog's consumption of retained stop-reason rows (I relied on the diff's three read sites, which are consistent with the fail-closed preservation this adds).

@pepmach
pepmach merged commit c2d9162 into main Sep 5, 2026
64 checks passed
@pepmach
pepmach deleted the fix/monitor-create-only-stalled branch September 5, 2026 06:09
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants