Skip to content

feat(agent): CampaignWatcher flags Held campaigns stuck past a window#310

Merged
xmap merged 1 commit into
mainfrom
worktree-campaign-watcher
Jun 22, 2026
Merged

feat(agent): CampaignWatcher flags Held campaigns stuck past a window#310
xmap merged 1 commit into
mainfrom
worktree-campaign-watcher

Conversation

@xmap

@xmap xmap commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

CORA's 9th seeded agent and the first new consumer of the shared cora.api._flag_watcher scaffold (#308). A deterministic, flag-only, composition-root periodic watcher: each tick it lists Held (operator-paused) campaigns 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.

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 DecisionRegistered envelope, and the loop/lifespan come from _flag_watcher. The simplest consumer yet: no activity fold, because Held makes no run-execution progress (last_status_changed_at, advanced only by resume_campaign / close_campaign, 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 proposed "reuse Stall", which 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. Gate review verified global uniqueness empirically (10 sets / 32 tokens / 32 unique).
  • agent kind CampaignWatcher; agent id in a new cab1 block (distinct from calibration's ca11)

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.

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

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>
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/agent
  __init__.py
  seed_campaign_watcher.py
  apps/api/src/cora/api
  _calibration_watcher.py
  _campaign_watcher.py
  _clearance_watcher.py
  _procedure_watcher.py
  main.py
  apps/api/src/cora/decision/aggregates/decision
  __init__.py
  state.py
  apps/api/src/cora/infrastructure
  config.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap xmap merged commit 8f522d0 into main Jun 22, 2026
16 checks passed
@xmap xmap deleted the worktree-campaign-watcher branch June 22, 2026 11:56
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.

1 participant