feat(agent): CampaignWatcher flags Held campaigns stuck past a window#310
Merged
Conversation
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>
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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
CORA's 9th seeded agent and the first new consumer of the shared
cora.api._flag_watcherscaffold (#308). A deterministic, flag-only, composition-root periodic watcher: each tick it listsHeld(operator-paused) campaigns and records oneDecision(context=CampaignProgress, choice=Stuck)per stuck episode for any whoselast_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 onActor.active.Top pick of the post-8-fleet ideation (real gap, buildable now, migration-free, un-spoofable, ladder-respecting).
Thin scaffold consumer
On the scaffold this module owns only the Held drain, the campaign vocabulary, and the namespace — the staleness rule, the per-episode Decision id, the
DecisionRegisteredenvelope, and the loop/lifespan come from_flag_watcher. The simplest consumer yet: no activity fold, becauseHeldmakes no run-execution progress (last_status_changed_at, advanced only byresume_campaign/close_campaign, is the true clock; membership curation touches onlyrun_count). A defensivestatus == "Held"re-check guards a future filter widening.naming-r3
CampaignProgress(family-clean withClearanceProgress/ProcedureProgress)Stuck— the ideation proposed "reuseStall", which would have collided (Stallis owned byProcedureProgress, and choice tokens must be globally unique in theDecisionChoiceprojection), so this context owns its own token. Gate review verified global uniqueness empirically (10 sets / 32 tokens / 32 unique).CampaignWatcher; agent id in a newcab1block (distinct from calibration'sca11)No migration
proj_campaign_summaryalready carrieslast_status_changed_at+ admitsHeld, andlist_campaignsalready filters by status. v1 watchesHeldonly;Planned(legitimately not-started-yet) is deferred.Tests / verification
New unit tests: vocab disjointness (unions every sibling closed set), the watcher runtime (is_stalled boundary, flag/skip-fresh, cannot-tell defer, defensive non-Held guard, paginated drain, idempotency, actor-absent + deactivated kill-switches, disabled/enabled lifespan, failing-tick survival), and the seed shape. Local: ruff, pyright, tach, architecture (26,902), full unit tier (10,492) all green.
Gate-reviewed (correctness/foolability, naming-r3, wiring + coverage): ship, no P0/P1.
🤖 Generated with Claude Code