feat: expose session monitors to agents - #5184
Conversation
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Reviewed: RFC (in base, extended here), Design-Verdict: CONCERNS Sound RFC-driven controller design, but the new exactly-once machinery is poured into two already-huge legacy modules instead of the monitoring package it belongs to. Watch
Suggestions
[DESIGN-REVIEWED] 7b90924 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of I've completed the review pass: intent file, full source diff, the RFC milestone the stack implements, and consumer-count greps for the new surface. Final review follows. First-Principles-Verdict: CONCERNS Six browser What this change shipsIntent: let an agent arm a server-probed PR monitor that wakes its session only on actionable change — an ADDITION, PR 4 of the recorded RFC (
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 7b90924 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
3820dfd to
b4492c4
Compare
|
Fixed the two newly actionable failures in the PR that owns the behavior: Slack legacy monitor completion now rejects synthetic timeout evidence before accounting, and the session-close restoration regression uses the public accepted/dispatched transitions. Focused verification: 195 monitor tests passed; black, subprocess-encoding, isort, flake8, Linux mypy, SDK-boundary, docs, harness-parity, and brand gates passed. |
|
Fixed in |
|
Fixed the current backend shard failure at this owning layer. The cancellation regression now uses authoritative |
|
Addressed the current completion-accounting blocker in b793000. Genuine provider end_turn events now settle successful monitor turns; the ACP compatibility path marks fabricated end_turn terminals with explicit synthetic_completion provenance, which dashboard and channel accounting reject. Focused dashboard/driver/ACP propagation tests pass (23 tests across the relevant selections). |
bolichen97
left a comment
There was a problem hiding this comment.
Review: session monitors + controller
Reviewed origin/token-monitors-github-probe...origin/token-monitors-tools (59 files, +8285/−613) at 52293ce01. The PR description asserts 30+ concurrency/restart/exactly-once invariants, so the review targeted those specifically and verified findings by executing the real AutoNudgeService, MonitorController, apply_session_directive and the transport seams rather than by reading. The existing suite is green (293 passed across the 9 monitor/autonudge/slack modules; 256 across the controller set), so every item below is an untested path.
The two most serious are the same failure mode reached two ways: MonitorController.tick has no branch for wake_in_flight=True, wake_delivery=None, and that state is both persisted by restore_monitor_after_failed_session_close and left behind by any exception escaping _dispatch_claimed. In both cases the monitor reports active forever while doing nothing — and the first survives restart, which defeats the restart-durability invariant the description leads with.
Blocking
1. monitoring/controller.py:142 — an in-flight claim whose delivery marker is still None never expires and never re-dispatches; the monitor is wedged forever, across restarts. Expiry is gated on wake_delivery is DISPATCHED, but Slack/Discord call mark_accepted() at turn start while record_monitor_dispatched only runs after the whole turn returns, so wake_delivery is None for the entire action turn (up to _NUDGE_TURN_TIMEOUT=1800s) while _accepted_monitor_turns is set. Close the slot in that window → _retain_accepted_terminal_completion retains wake_in_flight=True + completion_evidence_deadline=stopped_at+7260 with wake_delivery=None; if history persistence then fails, restore_monitor_after_failed_session_close (autonudge.py:1785) clears outcome, sets active=True, and schedules the BUSY retry cadence but never sets wake_delivery = BUSY. tick() then matches no branch → return NO_CHANGE. Verified end-to-end: after restore, state is active=True wake_in_flight=True delivery=None evid_deadline=7380 next_due=140, and tick returns no_change at now=140, 200, 7381 and 1e9 — never calling record_monitor_completion_evidence_unavailable, never probing, never re-dispatching. _timer's finally re-arms from the past next_due_ts every _OVERDUE_REARM_SECS=10s forever (measured 44 tick invocations per wall second with the interval compressed 500×); on restart _load sets next_due_ts=completion_evidence_deadline and the same no-op loop resumes. Flipping the same state to DISPATCHED expires correctly on the first tick, which isolates the gate as the cause. Also reproduced straight from a persisted record with wake_in_flight:true and no wake_delivery key. Every escape is closed: update_monitor(target=…) → refused ("target or objective cannot change while a wake is in flight"), re-arm → refused, api_monitor_restart → refused ("only terminal monitors can restart"). One line closes it: staged_state.wake_delivery = MonitorDispatchResult.BUSY in the autonudge.py:1785 branch.
2. monitoring/controller.py:206 — an exception escaping _dispatch_claimed after the claim is persisted leaves the monitor wedged with no timer at all. except Exception does not catch CancelledError, and the TypeError at line 229 is outside the try. apply_monitor_probe persists the claim as wake_in_flight=True, wake_delivery=None, next_due_ts=0.0; if _cancel_timer fires from a concurrent stop/close mid-handoff, or a dispatcher returns a non-MonitorDispatchResult, none of record_monitor_dispatch_failure/_busy/_dispatched runs, and _timer's finally declines to re-arm because it requires next_due_ts > 0 (autonudge.py:2544-2549). Verified: a dispatcher returning True → TypeError: monitor dispatcher returned an untyped result, state active=True wake_in_flight=True delivery=None outcome=None next_due_ts=0.0, _arm_timer never called, tick returns no_change at now=300 and 1e9. Same for the CancelledError variant. The monitor shows active in the dashboard and monitor_inspect but is dead — no timer, no probe, no expiry, no stop reason; only a process restart retires it, and then only as BLOCKED/completion_evidence_unavailable. A wake_delivery is None fallback in tick plus mapping the contract violation to UNAVAILABLE closes both this and finding 1.
3. dashboard/session_directive_apply.py:283 — _monitor_watch omits replace_existing=False, so the agent's monitor_watch silently destroys a user-created legacy babysit loop on the same slot. The browser's POST /api/monitors refuses the identical situation with 409 (api_monitor_create passes replace_existing=False, handlers/autonudge.py:369), but the directive path inherits authorize_and_add_nudge's default True, and _add_monitor_locked's only replacement guard is existing_monitor is not None and existing_monitor.wake_in_flight — a legacy loop has monitor is None, so it is removed unconditionally. Verified: with a user loop "keep an eye on the deploy and page me" armed on chat-1, one monitor_watch directive returns "Structured monitor 20236018 started on this session." and the legacy loop is gone, with no error, no warning and no audit of the destruction. _monitor_start has the same shape in reverse — the legacy monitor_start tool (still promoted by agent.py:4867/5193 and the prepare-pr/pipeline-conductor skills) removes a structured monitor's durable record from memory and disk with emit=False, so no removed WS event fires and the retained terminal outcome is unrecoverable. These are the only two authorize_and_add_nudge call sites left on the default; every other route in this PR was hardened.
4. monitoring/decision.py:78 — the cumulative provider-error budget is tracked, surfaced, and never enforced. monitor_budget_reason checks runtime, agent turns and tokens only; the sole provider bound is consecutive_provider_errors + 1 >= max_provider_errors in _provider_error_decision. Verified: provider_error_count=10_000, consecutive_provider_errors=0, max_provider_errors=3 → monitor_budget_reason() == '' and decide_monitor(...) == retry_provider. A flapping target (error, success, error, success…) polls for the full max_runtime_secs — 48 probes at the default cadence, 960 at MIN_MONITOR_CADENCE_SECS=15 — while monitor_inspect reports provider_error_count far above the max_provider_errors the operator set.
5. slack/gateway.py:5596 — the Slack exception path reports DISPATCHED whenever _turn_started is set, even though the accepted-check immediately above proves the turn never reached the provider. _turn_started = True is assigned at line 5434, before TurnDriver is even constructed. Verified by monkeypatching gw.build_tool_gate to raise: _fire_slack_nudge(loop, "[Monitor wake] x") returns DISPATCHED with accepted == False. record_monitor_dispatched then bumps wake_count 0→1 and arms completion_evidence_deadline = now + 7260; no completion can ever arrive, so the monitor sits idle ~2h and is retired BLOCKED/completion_evidence_unavailable instead of retried as BUSY in 15s. The sibling except asyncio.TimeoutError branch (5581-5585) keys off .accepted and does the right thing — the two paths disagree. Note the 7260s constant is justified as "the normal turn ceiling is two hours" while Slack/Discord monitor turns are capped at _NUDGE_TURN_TIMEOUT = 1800.0, so the dead window is 4× the surface's own ceiling.
6. discord/transport_dispatch.py:308 — Discord fails in the opposite direction: monitor_result is seeded UNAVAILABLE, so any transient pre-acceptance failure permanently retires the monitor. A transient _render_config read error, an attachment/typing failure, a governance deny, or the generic except Exception at 888 before mark_accepted all leave the seed in place; record_monitor_dispatch_failure then sets active=False, outcome=TARGET_UNAVAILABLE, stopped_reason=session_unavailable and cancels the timer. Verified: record_monitor_dispatch_failure → outcome=TARGET_UNAVAILABLE active=False, versus record_monitor_dispatch_busy → outcome=None active=True next_due=+15s. Slack maps the same class to BUSY, so at least one of the two surfaces is wrong — a one-shot embedding hiccup permanently kills a 4-hour monitor. Also: asyncio.TimeoutError is TimeoutError and is an Exception subclass, so _fire_discord_nudge:5751's bare except Exception maps a 1800s turn timeout to UNAVAILABLE too, where Slack returns BUSY.
7. slack/gateway.py:6183 — the MonitorDispatchResult return contract is enforced only by bare assert isinstance(...), which python -O strips. Lines 6183/6187/6192 are the only runtime enforcement that each _fire_*_nudge returned the enum on the monitor path, and _delivery_result returns a plain bool whenever wake_message is None (_fire_discord_nudge:5750, _fire_slack_nudge:5582,5600 do return bools). Under PYTHONOPTIMIZE=1 a bool reaches _dispatch_claimed, matches none of the three is comparisons, and hits the TypeError at controller.py:229 — i.e. finding 2's permanent wedge. Because every str, Enum member is truthy, a mode confusion in the other direction would read BUSY/UNAVAILABLE as "delivered" and inflate cycle_count. This needs an explicit if not isinstance(...) mapping to UNAVAILABLE, not an assert — the file itself says so 300 lines earlier ("Guard (not assert): stripped under -O", gateway.py:5868).
Should fix
8. autonudge.py:2228 — completion_evidence_deadline is anchored on the tick-start now, not on when dispatch actually returned, so the documented +60s margin is consumed by up to 1800s of in-dispatch time. Verified: tick at 1000, turn actually starts at 2800 (1800s queued), deadline 8260 → only 5460s of evidence window for a turn whose own ceiling is 7200s, expiring 1740s before the turn can be cancelled by its own timeout. The timeout then clears the claim, and the authoritative completion that arrives afterwards hits the not state.wake_in_flight guard at :1938 and is silently dropped — agent_turns/input_tokens/output_tokens all stay 0 for a turn that really ran and spent 180k/40k tokens. (Double-charging is correctly prevented; the failure mode is zero-charge.)
9. autonudge.py:1952 — record_monitor_turn_completion sets wake_in_flight = False but never resets wake_delivery, leaving (False, DISPATCHED) — the exact combination the other six writers deliberately reset to None (1683, 1866, 1917, 2038, 2274, 2455). monitor_state_public_dict publishes it verbatim, so any inspect between completion and the next wake shows an idle monitor whose delivery reads DISPATCHED. Verified against the real service. wake_in_flight and wake_delivery are one state machine split across two fields with ten writer sites; adding a CLAIMED member and re-exposing wake_in_flight as @property: self.wake_delivery is not None makes the desynced state unrepresentable and keeps all 28 read sites working.
10. monitoring/models.py:471 — a quarantined record is never actually persisted, so it re-quarantines forever and pays a full fsync'd store rewrite on every boot. _load builds quarantine_monitor_state(...) (outcome=BLOCKED, stopped_reason='invalid_monitor_record') and sets _store_dirty, but monitor_state_to_dict short-circuits on _raw_payload is not None and returns the original malformed payload verbatim. Verified with created_ts='not-a-number': in-memory outcome=BLOCKED, on disk outcome=None, stopped_reason=None, payload byte-identical to the original; second boot re-quarantines with a WARNING+traceback and rewrites the same bad bytes. monitor_state_public_dict reports BLOCKED to the browser while disk records no terminal state at all. Same short-circuit means MONITOR_STOP_UNSUPPORTED_VERSION set at :629 is never durable.
11. autonudge.py:624 — the unsupported-version branch is an elif head, so such a record keeps active=True forever and no supported API can clear it. It skips both the terminal-record repair (active=False, wake_in_flight=False, next_due_ts=0) and the unwired-monitor guard. Verified with version:99: active=True next_due_ts=123.0 outcome=BLOCKED. _arm_from_deadline bails on the version mismatch so it never fires, but stop_monitor returns unchanged (outcome already set), api_monitor_restart 409s, update_monitor returns None — the dashboard shows a permanently "active" monitor that never runs and cannot be removed. Related: _load:642 reads loop.next_due_ts before _repair_number sanitizes it at :691, so a BUSY claim with an out-of-range persisted deadline is wrongly retired as completion_evidence_unavailable even though BUSY proves no action turn started — contradicting the branch's own comment.
12. autonudge.py:1698 — every monitor tick fsyncs the entire store, including NO_CHANGE ticks that move only probe_count/last_probe_at/next_probe_at. The persist call is outside every decision branch, and _monitor_snapshot_with_replacement serializes all loops, so N monitors ticking each rewrite all N records — O(N²) bytes per cadence period, under the global service lock. Measured on APFS SSD: 0.69 ms and 5.3 KB with 1 monitor; 3.54 ms and 274,866 bytes with 25 monitors — at MIN_MONITOR_CADENCE_SECS=15 that is 25 fsyncs + 6.7 MB per 15s, ~144,000 fsyncs and ~40 GB/day for cadence bookkeeping, and on a spinning disk or network volume an fsync is 10–50 ms taken under the lock, so legacy nudge loops block behind it. self._store_dirty already exists but is consulted only for the load-repair flush. Related: BUSY retry is a flat 15s with no backoff and re-persists byte-identical state — one long user turn produces ~480 retries (~126 MiB, 480 fsyncs) carrying zero new information, while the provider-retry path in the same file already has exponential backoff.
13. mcp_tools/control.py:1157 — monitor_update resolves its session with the wider _autonudge_binding_key while monitor_watch/monitor_inspect/monitor_stop all use _structured_monitor_binding_key. Verified: binding_key_for('webex:room-1') == 'webex:room-1' but structured_monitor_binding_key_for(...) is None. So a Webex session is refused by three tools in one step with a clear message and a denied SEL record, but monitor_update admits it, encodes a directive carrying structured-only keys, tells the agent "Monitor-loop update requested for this session", and only then raises _DirectiveDenied in the consumer. Separately, monitor_watch/monitor_stop skip their refusal and their denied SEL event when strict resolution yields an empty key (... is None and sk), contradicting the guarantee this PR added to docs/architecture/mcp.md; monitor_inspect (which drops the and sk) is the only one that behaves as documented.
14. dashboard/session_directive_apply.py:166 — monitor_stop's reason is schema-validated, stripped, redacted and encoded into the directive, then discarded. The consumer calls _monitor_stop(session_key) with no args (the function takes no args parameter), and stop_monitor writes the constant MONITOR_STOP_USER unconditionally. The SEL metadata is {"caller": …} only. So an agent calling monitor_stop(reason="PR merged, review complete") leaves an operator-visible terminal record reading only user_stop, and the reason is unrecoverable from both monitor_inspect and the audit trail. Every sibling directive (_monitor_start/_monitor_watch/_monitor_update) receives args.
15. monitoring/models.py:46 — MONITOR_PUBLIC_FIELDS omits created_ts and last_wake_reason_code, so neither the dashboard nor monitor_inspect can show elapsed runtime or why the last wake fired. Verified by diffing the dataclass against the tuple: declared - public == ['created_ts', 'last_wake_reason_code']. monitor_budget_reason stops a monitor on now - state.created_ts >= budgets.max_runtime_secs — the bound most likely to end a monitor — yet an agent sees max_runtime_secs: 14400 with no way to compute time remaining, then gets stopped mid-objective with stopped_reason: runtime_budget and no warning. _compact_monitor_inspection drops 11 more fields including wake_delivery, so the agent cannot tell whether its wake was DISPATCHED or is spinning in the 15s BUSY loop — the exact distinction the new typed result was introduced to make durable. Both lists are hand-maintained and can be derived (tuple(f.name for f in fields(MonitorState) if f.name not in {'extra_fields','_raw_payload'})).
Notes, lower severity
autonudge.py:2365/2398 — notify_user_input cancels the timer of a terminal-but-awaiting-evidence monitor and notify_turn_complete's not loop.active guard then refuses to re-arm it, so the bounded evidence window is dropped and the claim leaks for the process lifetime (verified: claim leaked: True); notify_turn_complete also has no monitor branch at all, so _arm_from_deadline's min(remaining, loop.idle_secs) clamp silently reinterprets the 7260s evidence deadline as a cadence-length poll, and can re-arm a monitor the controller deliberately disarmed at next_due_ts == 0. · handlers/autonudge.py:262/273 filter structured monitors out of the legacy GET while _observer still broadcasts them on the same autonudge_state channel, so the AutoNudge popover on such a slot shows an empty form, 409s on Save (autonudge_not_armed), then 409s again on PATCH (structured_monitor_requires_monitor_api) — both dead ends, and no /api/monitors consumer exists in website/ at this layer. · handlers/autonudge.py:633 answers {"ok": true} for a DELETE that removes nothing once terminal, and with create/PATCH/POST /api/autonudge all refusing, the slot can never be retargeted from the browser. · handlers/autonudge.py:567's new replace_existing=False also 409s over an inactive legacy tombstone, contradicting its own docstring and the documented "start or replace" contract. · gateway.py:6106 — the acceptance callback re-reads state.last_wake_fingerprint lazily while the hook captured it eagerly, making mark_monitor_turn_accepted's fingerprint guard a tautology and allowing the accepted marker and the completing hook to name different fingerprints. · models.py:208 validates the agent-turn ceiling against DEFAULT_MONITOR_AGENT_TURNS instead of MAX_MONITOR_AGENT_TURNS (both 8 today, so invisible). · token_usage_known is write-only with zero readers, and it records the one condition that voids max_tokens — on the ACP seam max_tokens can never fire. · mark_monitor_action_in_flight has no production caller: 47 lines duplicating the most safety-critical transition in the PR, with a subtly different precondition. · controller.py:265 re-implements security.redact_and_truncate, and the 4096-char cap is a blind suffix chop applied after redaction, so the only content it can delete is the trailing operator Next action: line (verified: instruction entirely gone at len 4096) while provider-controlled values are preserved in full. · controller.py:11 imports MONITOR_WAKE_PREFIX from dashboard.state, dragging 18 dashboard modules (172 total, 0.47s) into any import of the "transport-independent" monitoring package. · _write_state fsyncs the temp file but never the containing directory after replace_with_retry, so the rename itself is not durable — the "fsync before publish" invariant holds for file contents only. · One bound has three owners with two semantics (validation.py rejects, _bounded_int rejects, add_monitor/update_monitor silently clamp using a different constant pair), and MONITOR_UPDATE_SCHEMA admits max_runtime_secs=0 that MonitorBudgets then rejects two layers down. · Four new copies of the owner-gate / internal-secret-gate / redact-then-truncate helpers where canonical ones exist; the _require_monitor_owner copy writes a client-chosen X-Session-Key header into the audit caller field on a denied request and emits dashboard_owner_required instead of the standard owner_only. · Conventions: the new [Monitor wake] prefix was added to dashboard/state.py but not to session_summary._INJECTED_PREFIXES, so a monitor wake is counted as user intent by the chat-summary/intent path; code-style.md's owning-module index gained no monitoring/ row for 11 new limits; AGENTS.md's own injected-envelope list still names three envelopes; and two black-baseline prunes ride inside feature commits (both files verified genuinely black-clean, so the prune is correct — just not its own commit).
Execution-verified AI-assisted review (Claude Code) across 10 parallel angles plus a gap sweep, run against a local checkout at 52293ce01. Nothing was modified; nothing was posted anywhere else. Confirmed as not problems, so please don't re-spend on them: generation-safe compare-and-replace is sound (update_monitor bumps config_generation exactly when target/objective change, refuses that change while in-flight, and apply_monitor_probe rejects a stale generation, so the wake envelope's target always matches the probed target); double-charging is correctly prevented by the wake_in_flight guard under the lock; synthetic_completion is plumbed through every terminal-event producer, so removing TURN_STOP_REASON_END_TURN from _SYNTHETIC_STOP_REASONS leaves no unflagged synthetic path; _accepted_monitor_turns is correctly bounded; the wake path correctly skips compose_nudge_body/render_snapshot; MCP statelessness holds for all three new tools; and no dispatcher can leak a bool into _dispatch_claimed while asserts are enabled.
|
Addressed the current review feedback and restacked this branch in 41ef44c. Monitor dispatch recovery is bounded and completion provenance remains intact. Focused backend verification for this slice passes, and the composed upper-stack monitor suite passes 234/234. |
|
Addressed the current GPT blocker in 11966da. Supplemental provider failures now advance both total and consecutive provider-error accounting while preserving the readable canonical observation, matching the shadow path. Added a bounded-streak regression; the focused monitor controller suite passes 46/46. This PR is also consolidated to the two-commit hygiene limit. |
Audit note — this PR is one slice of a declared branch stack, not a duplicateA duplicate-detection sweep flagged #5184 / #5185 / #5186 / #5305 as overlapping at FULL coverage. That is a stacking artifact, and it is worth stating plainly so nobody acts on it: each branch physically contains the previous one, so the shared code is inherited and each PR's review diff overstates what it actually authored. Proved mechanically, not by reading code:
The two PRs are also cleanly split by surface, which is why they are complementary rather than redundant. #5184 is backend-only: 26 files under src/kiro_crew/ (monitoring/controller.py, mcp_tools/control.py, validation.py, session_directive.py, dashboard/handlers/autonudge.py, slack/gateway.py, discord/gateway.py, ...) and ZERO files under website/. #5185's own delta ( The 11 non-website files in #5185's own delta are strictly ADDITIVE refinements in service of that frontend, not a competing design: monitoring/models.py gains Neither has landed: 550ea7b, fc82d95, 4c7b130 and 331c07d are all absent from origin/main; main carries monitoring/{init,completion,decision,github_pull_request,models,shadow}.py but NOT controller.py, and website/src/monitoring/ and SessionAutomationPopover.tsx do not exist on main. Ironically the first-pass record itself, the cached PR data, states in prose "Stacked descendant (PR 6). Its branch is #5184's head plus 4c7b130 + 331c07d; the shared code is inherited, which per the brief is a stacked branch, not duplicate" — the FULL/complete_coverage=true coding of that same relation is what the scan surfaced as a duplicate signal.
Proof of stacking: #5186's own delta ( Per-file judgment of the 14 shared paths: every one is a file #5184 creates or extends and #5186 then builds further on. src/kiro_crew/monitoring/controller.py does not exist on origin/main at all (git ls-tree of the monitoring package shows only init.py, completion.py, decision.py, github_pull_request.py, models.py, shadow.py) -- #5184 creates it, #5186 adds 9 lines to it. mcp_tools/control.py: #5184 introduces the monitor_watch schema and #5186 edits the schema #5184 authored. That is a build-on-top dependency, the opposite of two implementations of one behaviour. Deciding question: if #5184 merged, would #5186 still carry real, wanted work? Yes -- the whole babysit-skill migration, the shipped monitoring doc, the unbounded-loop ban, the pre-probe budget stop, evidence_scope, and the Slack terminal notifications. The reverse direction is not a redundancy signal: #5186 "contains" #5184 only because it is branched off it.
The task premise that both are "open against main" is FACTUALLY WRONG for 5305. Its GitHub base ref is On its OWN range (7ce67a7..9a6f511) 5305 is 77 files / +5,642 / -432 — matching GitHub's own 77 / +5605 / -432 for the PR. That range EXTENDS the four files 5184 created (src/kiro_crew/monitoring/controller.py, test/test_monitor_controller.py, test/test_monitor_directive_apply.py, test/test_monitor_mcp.py); it does not reimplement any of them. It also ADDS six modules 5184 has no counterpart for at all: monitoring/pull_request.py (the provider-neutral review-readiness contract), monitoring/targets.py (strict target parsers), monitoring/gitlab_merge_request.py, monitoring/azure_devops_pull_request.py, monitoring/bitbucket_pull_request.py, monitoring/provider_cli.py — plus dashboard/handlers/source_providers.py and a whole frontend surface (SessionAutomationPopover.tsx +626, monitoring/automation.ts +455, MonitorRadar.tsx) across 12 locale catalogs. That is a capability 5184 STRUCTURALLY CANNOT DELIVER: 5184's MonitorController drives only the single GitHub probe already on main (monitoring/github_pull_request.py, landed by the stack's merged #5183 = dc35c19, which is 5184's merge base). GitLab / Azure DevOps / Bitbucket are different addressing schemes and different providers. Conversely 5184 is the substrate 5305 consumes — MonitorController, the monitor_watch/inspect/update/stop MCP tools, the owner-gated REST routes, format_monitor_wake, MonitorDispatchResult. Neither can be closed without destroying real, wanted work: closing 5184 deletes the controller 5305 imports; closing 5305 deletes four provider probes, the neutral readiness contract, and the entire multi-provider dashboard surface. The maintainer agrees in practice. bolichen97 reviewed each slice on its own per-slice range — 5184 as Consequence for reviewLand the stack in order. Reviewing a later slice before its base lands means reviewing the base's code again, and the FULL-overlap signal a keyword or file-overlap sweep produces here means nothing. Separately, #5184 vs #7634 (the zero-token-probe monitor gate) was adjudicated as independent — same monitor area, different behaviour — so neither blocks the other. From a repository-wide duplicate/overlap audit of every pull request open against |
Review — head
|
bolichen97
left a comment
There was a problem hiding this comment.
Approving the current head after the re-review above: all prior blocking findings verified fixed, CI green; leftovers are tracked as follow-ups.
|
@kyleseaman Ready to land #5184 — all prior blocking findings are fixed on Could you re-push the branch so you are the last pusher? Same content is fine, e.g. CI reruns (~30–60 min); I will re-approve the new head and merge, then continue with #5185 → #5186 → #5305 (those three you pushed last, so my approvals there are valid). No need to rebase onto |
|
@bolichen97 Re-pushed by kyleseaman at |
Problem / Motivation
Agents and sessions cannot create or manage structured monitors, and the runtime has no authoritative controller that applies bounded decisions consistently across transports.
Why it matters
A model-only polling loop cannot provide zero-turn unchanged probes, restart-safe claims, exact completion accounting, strict caller ownership, or durable terminal evidence.
What changed (motivation → approach → change)
monitor_watch,monitor_inspect,monitor_update, andmonitor_stopMCP tools.!new; authorization refusal before provider entry returns UNAVAILABLE without recording or persisting a successful session; after the hook-bearing turn is accepted, later Discord errors or an outer timeout remain DISPATCHED for bounded evidence recovery.[monitor wake]turns without feeding those operational wakes into memory consolidation.Tests
Manual verification
N/A — controller, API, session, and transport contracts are covered with deterministic integration tests; no live channel credentials are required.
Related Issues
N/A — implements the live tool/controller layer specified in #5180 and depends on #5183.
Checklist
Contribution License Agreement
N/A — the repository template does not yet supply final CLA wording.