Skip to content

Scope the debrief lease to agent kind so RunDebriefer and CautionDrafter coexist#556

Merged
xmap merged 1 commit into
mainfrom
worktree-lease-kind-scope
Jul 12, 2026
Merged

Scope the debrief lease to agent kind so RunDebriefer and CautionDrafter coexist#556
xmap merged 1 commit into
mainfrom
worktree-lease-kind-scope

Conversation

@xmap

@xmap xmap commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Supersedes #555, which GitHub auto-closed when its base branch (worktree-budget-metering, PR #554) was merged and deleted; the branch is now rebased onto main with the single lease commit.

Summary

Fixes a pre-existing defect surfaced by #554's gate review: the debrief-lease winner scan matched ANY DecisionDebriefRequested marker by terminal_event_id, so RunDebriefer and CautionDrafter, which both subscribe to all four terminal Run events, contended for one lease. Whichever applied second saw the other as the winner and wrote a DebriefConflicted/CautionDraftConflicted audit Decision instead of doing its job, leaving the second-registered agent systematically dead in a full deployment. Every subscriber test drove one agent at a time, so the defect was invisible; the caution_drafter call-site comment claimed an independence the code never implemented.

Fix

Contention is now scoped by agent kind (the design memo's target is multi-agent pollution among agents doing the same job: rainbow-deploy variants of one debriefer, different LLM backends of one drafter):

  • DecisionDebriefRequested gains an additive debriefer_kind: str | None payload field; the winner scan filters by it. Kindless legacy markers contend with every kind (wildcard), so pre-scoping streams replay unchanged.
  • Same-kind semantics unchanged: variants race on stream version; same-agent retries stay idempotent via the uuid5 event_id.
  • The one-shot append became a bounded load-scan-append loop (3 attempts): with kinds independent, another kind's lease landing between load and append is now the common version bump, and it retries instead of degrading to a no-winner loss. Exhaustion keeps the degenerate (False, None) exit. decided_by log values: scan / append / retry_exhaustion.
  • Glossary + Run module doc updated; the lease design memo carries an amendment note.

Test plan

  • Reproducer: test_apply_coexists_with_run_debriefer_on_same_terminal_event fails on pre-fix code (CautionDrafter wrote CautionDraftConflicted), passes now with both agents writing substantive Decisions.
  • Primitive pins: cross-kind independence, legacy-wildcard contention, retry-past-non-competitor, retry exhaustion, plus the retrofitted same-kind suite (18 lease tests).
  • Full suites green: unit 12204, integration 1044, contract 3336, architecture 28385; pyright clean; naming review (R1-R6) all pass.

🤖 Generated with Claude Code

The lease-winner scan matched ANY DecisionDebriefRequested marker by
terminal_event_id, so RunDebriefer and CautionDrafter, which both
subscribe to all four terminal Run events, contended for one lease:
whichever applied second saw the other as the winner and wrote a
Conflicted audit Decision instead of doing its job. In the full app
that left the second-registered agent systematically dead; the defect
stayed invisible because every subscriber test drives one agent at a
time. The caution_drafter call-site comment claimed independence the
scan never implemented.

The design memo's contention target is multi-agent pollution among
agents doing the SAME job (rainbow-deploy variants of one debriefer,
different LLM backends of one drafter), and the Agent aggregate's kind
already names that job, so the marker now carries debriefer_kind
(additive payload field) and the winner scan filters by it. Kindless
legacy markers contend with every kind, preserving replay behavior for
pre-scoping streams. Same-kind semantics are unchanged: variants race
on stream version, same-agent retries stay idempotent via the uuid5
event_id.

The one-shot append also became a bounded load-scan-append loop: with
kinds independent, another kind's lease landing between load and
append is now the COMMON version-bump, and treating it as a degenerate
loss would drop real work. A non-competing bump retries (3 attempts);
exhaustion keeps the old (False, None) exit. decided_by log values are
now scan, append, and retry_exhaustion.

Verified by test_apply_coexists_with_run_debriefer_on_same_terminal_event,
which reproduces the contention on the pre-fix code (CautionDrafter
wrote CautionDraftConflicted; both agents now write substantive
Decisions), plus primitive-level pins for cross-kind independence,
legacy wildcard, retry-past-non-competitor, and retry exhaustion.

Co-Authored-By: Claude Fable 5 <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
  _subscriber_lease.py
  apps/api/src/cora/agent/subscribers
  caution_drafter.py
  run_debriefer.py
  apps/api/src/cora/run/aggregates/run
  events.py
Project Total  

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

@xmap xmap merged commit 8463ecd into main Jul 12, 2026
16 checks passed
@xmap xmap deleted the worktree-lease-kind-scope branch July 12, 2026 02:38
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