feat(agent): Reaction Protocol + dismiss_event_in_reaction operator slice (Slice A)#22
Merged
Merged
Conversation
…h per-subscriber batch_size The projection worker hardcoded batch_size=100 and treated all Subscribers identically, even though RunDebriefer and CautionDrafter take 5-15 s per apply (LLM round-trip). The run_debriefer docstring claimed batch_size=1 but the worker ignored it; one pool connection could be held for 100 * 15s = 25 minutes. Add Reaction Protocol as the public sibling to Projection. Both satisfy Subscriber structurally; the operational split is fast batch (Projection, default 100) vs slow batch (Reaction, declares its own, typically 1). Worker reads per-subscriber batch_size via getattr so the 14 existing Projections need no migration. Classify RunDebrieferSubscriber and CautionDrafterSubscriber as Reactions with batch_size = 1. The docstring's design intent is now enforced by the type system. Future widening (separate ReactionWorker with its own pool budget, operator escape-hatch for wedged bookmarks) stays deferred behind named triggers: 3rd Reaction OR first wedged-bookmark incident. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a Reaction's bookmark wedges on a poison event, the only
recovery before this slice was SSH into prod, open psql, hand-edit
projection_bookmarks.last_position. No audit, no review, no chance
to add a reason. The 3am playbook was hostile.
New slice: POST /agent/reactions/{name}/dismiss-event +
DismissEventInReaction MCP tool. Operator supplies subscriber_name,
event_id, and a free-form reason. Handler:
1. Authorizes via deps.authz (denied -> 403)
2. Refuses in-memory mode (no projection_bookmarks table) -> 503
3. Acquires pool conn, begins TX, locks the bookmark row FOR UPDATE
4. Loads the target event by event_id from the events table
5. Pure decider validates: reason non-empty (400), event strictly
after current bookmark in lexicographic (transaction_id,
position) order (409 on rewind)
6. Atomic same-TX writes: UPDATE projection_bookmarks past the
dismissed event + EventStore.append_streams a DecisionRegistered
(context=ReactionDismissal, choice=EventDismissed) carrying the
reason and the cursor moved past in inputs
Mirrors the forget_actor cross-store pattern (non-event SQL write
inside the same conn.transaction() as the event append). Decision
audit reuses existing aggregate, no new event_type. ReactionDismissal
context + EventDismissed choice Literal added additively to Decision
state.py per the open-choice convention.
29 tests across 6 files: 11 decider unit + 5 PBT (Hypothesis) pinning
the validation cascade, 2 handler unit for authz-deny and
in-memory-mode branches, 6 endpoint contract + 2 MCP tool contract,
5 PG integration covering happy path + 3 error mappings + atomicity.
Co-Authored-By: Claude Opus 4.7 <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
Two-commit slice closing the highest-severity finding from a recent cross-BC architecture critique: the Subscriber Protocol is operationally a saga framework wearing a projection's face, but the type system did not say so.
Commit 1 (foundation, +570/-73): Adds
ReactionProtocol as the public sibling toProjection. Both satisfySubscriberstructurally; the operational split is fast batch (Projection, default 100) vs slow batch (Reaction, declares its own, typically 1). Worker reads per-subscriberbatch_sizeviagetattrso the 14 existing Projections need no migration.RunDebrieferSubscriberandCautionDrafterSubscriberclassified as Reactions withbatch_size = 1; the docstring's design intent (claimed-but-not-enforced) is now type-system enforced.Commit 2 (slice, +2095/-2): Adds
dismiss_event_in_reactionoperator slice. When a Reaction's bookmark wedges on a poison event, the slice atomically advances theprojection_bookmarksrow past it AND records the dismissal as aDecisionRegistered(context = ReactionDismissal,choice = EventDismissed) carrying the operator's reason and the cursor moved past. Mirrors theforget_actorcross-store pattern (non-event SQL write inside the sameconn.transaction()as the event append). Replaces the pre-slice on-call playbook of "SSH into prod, open psql, hand-editprojection_bookmarks.last_position."29 tests across 6 files: 11 decider unit + 5 PBT (Hypothesis) for the validation cascade, 2 handler unit for authz-deny and in-memory-mode branches, 6 endpoint contract + 2 MCP tool contract, 5 PG integration for happy path + 3 error mappings + atomicity.
Test plan
make lintcleanmake typecheck0 errorsOut of scope (deferred per documented triggers)
ReactionWorkerwith dedicated pool budget: deferred to 3rd Reaction OR first wedged-bookmark incidentProjectionRegistry→SubscriberRegistry: pure cosmetics, rename ripples across 16 BCs🤖 Generated with Claude Code