refactor(agent): extract the shared flag-only-watcher scaffold#308
Merged
Conversation
The three deterministic flag-only watchers (ClearanceWatcher, CalibrationWatcher,
ProcedureWatcher) had fired the rule-of-three: each was a near-clone carrying the
same agent-invariant mechanics. This hoists those into a new
cora.api._flag_watcher module:
- is_stalled: the pure staleness comparison (inclusive >= boundary).
- derive_watcher_decision_id: the per-episode deterministic id
(uuid5(namespace, "decision:{entity_id}:{episode_at}")) that makes a re-flag of
the same stall episode a ConcurrencyError no-op.
- record_watcher_decision: the DecisionRegistered envelope + idempotent append.
- flag_watcher_lifespan: the off-by-default gate, the periodic loop (a failed
tick is logged and retried, cancellation propagates), and task teardown.
Each watcher keeps what genuinely differs per agent: its drain (which list query
+ status filter), its recency fold (clearance UnderReview review-step;
procedure Running activity recency; calibration none), its clock source, its
Decision vocabulary, and its namespace UUID. Each also keeps a thin per-agent
_record_decision / _derive_decision_id / is_stalled surface delegating to the
scaffold, so behavior (and every existing test) is unchanged: all 56 watcher
unit tests pass verbatim, the behavior-preservation proof.
naming: the shared envelope/id helpers are record_watcher_decision /
derive_watcher_decision_id, not *_flag_* -- "flag" is the agent-family adjective
(flag-only watcher), but a Decision's choice can literally be "Flag" (owned by
ClearanceProgress), so it must not modify "decision". A new test_flag_watcher.py
pins the loop's cancel-propagation contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||
This was referenced Jun 22, 2026
xmap
added a commit
that referenced
this pull request
Jun 22, 2026
…#310) CORA's 9th seeded agent and the first new consumer of the shared cora.api._flag_watcher scaffold (PR #308). A deterministic, flag-only, composition-root periodic watcher: each tick it lists Held campaigns (operator-paused) and records one Decision(context=CampaignProgress, choice=Stuck) per stuck episode for any whose last_status_changed_at (the time it was held) has sat past an operator window without being resumed or closed. It issues no command (it surfaces the forgotten pause so a human resumes or closes the campaign). Off by default; gates on Actor.active. On the scaffold it is a thin module: the staleness rule, the per-episode Decision id, the DecisionRegistered envelope, and the loop/lifespan come from _flag_watcher; this module owns only the Held drain, the campaign vocabulary, and the namespace. The simplest consumer yet: no activity fold needed, because Held makes no run-execution progress (last_status_changed_at, advanced only by resume/close, is the true clock; membership curation touches only run_count). A defensive status==Held re-check guards a future filter widening. naming-r3: context CampaignProgress (family-clean with ClearanceProgress / ProcedureProgress); choice Stuck -- the ideation's proposed "reuse Stall" would have collided (Stall is owned by ProcedureProgress, and choice tokens must be globally unique in the DecisionChoice projection), so this context owns its own token. Agent kind CampaignWatcher; agent id in a new cab1 block. No migration: proj_campaign_summary already carries last_status_changed_at + admits Held, and list_campaigns already filters by status. v1 watches Held only; Planned (legitimately not-started-yet) is deferred to a later variant. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The three deterministic flag-only watchers (
ClearanceWatcher,CalibrationWatcher,ProcedureWatcher) had fired the rule-of-three: each was a structural near-clone carrying the same agent-invariant mechanics. This hoists those into a newcora.api._flag_watchermodule so the next flag-only watcher is a thin consumer, not a fourth copy.The scaffold owns:
is_stalled— the pure staleness comparison (inclusive>=boundary).derive_watcher_decision_id— the per-episode deterministic id (uuid5(namespace, "decision:{entity_id}:{episode_at}")) that makes a re-flag of the same stall episode aConcurrencyErrorno-op.record_watcher_decision— theDecisionRegisteredenvelope + idempotent append.flag_watcher_lifespan— the off-by-default gate, the periodic loop (a failed tick is logged and retried, cancellation propagates), and task teardown.Each watcher keeps what genuinely differs per agent: its drain (list query + status filter), its recency fold (clearance UnderReview review-step; procedure Running activity recency; calibration none), its clock source, its Decision vocabulary, and its namespace UUID — plus a thin per-agent
_record_decision/_derive_decision_id/is_stalledsurface delegating to the scaffold.Behavior preservation
This is a behavior-preserving refactor. All 56 existing watcher unit tests pass verbatim (none were touched) — the deterministic Decision ids (per-agent namespaces
ffff0002/ca110002/0c0c0002), the full envelope (decided_by, context/choice/rule, verbatim reasoning, identical inputs dicts,confidence_source,event_id, append atexpected_version=0with the ConcurrencyError swallow), the gates, the folds, and the off-by-default lifespan are all unchanged. The only cosmetic difference is theflaggedlog line keyingentity_id=instead of<entity>_id=(unasserted).Gate-reviewed (behavior-preservation diff vs
origin/mainfor all three watchers, naming-r3, seam + coverage): ship-with-nits, no P0/P1. The naming nit is applied — the shared helpers arerecord_watcher_decision/derive_watcher_decision_id, not*_flag_*, because a Decision'schoicecan literally beFlag(owned byClearanceProgress), so "flag" must not modify "decision" (it stays the agent-family adjective in_flag_watcher/flag_watcher_lifespan).Tests
test_flag_watcher.pypins the loop's cancel-propagation contract (an in-flight tick is cancelled cleanly on lifespan exit) — the one scaffold line the per-watcher suites do not reach. Local: ruff, pyright, tach, architecture (26,895), full unit tier (10,465) all green.This is the first of two: a follow-up PR adds CampaignWatcher as the scaffold's first new consumer.
🤖 Generated with Claude Code