Repository navigation
Record who answers notifications and reviews plans - #518
Merged
pufit merged 20 commits intoOct 17, 2026
Merged
Conversation
This was referenced Oct 17, 2026
alex-clickhouse
added this pull request to stack #521
October 17, 2026 07:20
Answers, dismissals, silence rules and plan reviews did not record a person: the web answer route dropped the signed-in actor, an injected answer turn ran as the system actor, and the REST plan approval moved the task as "system". Migration v055 adds six nullable actor_refs columns, appended to the registry: notifications.answered_by_actor_id and dismissed_by_actor_id, notification_silences.created_by_actor_id and caused_by_actor_id, plans.reviewed_by_actor_id and reviewed_caused_by_actor_id. Old rows stay NULL. - The web answer, dismiss and dismiss-all routes record the person. Telegram, Slack and hosted answers keep the provider user ID in metadata and record no actor. - An injected answer turn runs as the person who answered, on that person's behalf; a channel answer gives actor and cause NULL. The expiry note stays the system actor's turn. - Approval dispatchers take the answering actor as the keyword argument "actor", so a dispatcher can record the decision on its target. The mechanical-action audit record names the actor ID. - WS notification_answered and answer_injected carry actor_id. - Plan approve, decline, revise and PATCH through REST record the person as reviewer; the approval's task transition and the decline's task_done record the person too. Plan tools record the system actor and the turn's cause. request_plan_revision takes the requesting actor in addition to the cause. - Silences created through the web record the person; the tool records the system actor and the turn's cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The notifications page names the person who answered ("by Alice via
web") and who dismissed a notification, and the silences panel names
who created each rule ("Nerve for Bob" for a rule that the agent made
in Bob's turn). The plan view names who reviewed the plan. A
chat-channel answer and older rows keep the transport text only.
The answer broadcast updates the answerer in place, and the injected
answer bubble carries the answerer's actor ID, as the stored message
does. Names resolve through the actor store and ActorAttribution.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List the new columns and the actor and cause of answers, dismissals, silences, plan reviews and relayed answer turns in the accounts guide. The plans guide lists the reviewer columns and what each review path records. The architecture overview says what an approval dispatcher receives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plan decision read the status, checked it, and then wrote with no status condition, so two concurrent decisions could both pass the check: both approvals started an implementation, and a decline could overwrite an approval and close the task while the implementation ran. - compare_and_update_plan writes only while the plan has the status the decision read, and reports whether it changed the row. REST approve, PATCH and revise return 409 when they lose; the plan tools answer that the plan is no longer pending. Only the winner starts an implementation, a revision turn or a task change. update_plan no longer records a reviewer. - v055 adds plans.created_by_actor_id and plans.caused_by_actor_id: plan_propose and plan_update record the system actor and the turn's cause as the writer of the version they create. plan_update still records no reviewer on the version it replaces, because the planner replacing its own plan is not a review. - A web snooze of an approval stores the person in answered_by_actor_id, so the row names who deferred it after the audit log is gone or the card expires. A later decision replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan view names who wrote the version ("Proposed by Nerve for
Alice"). A snoozed approval names who snoozed it, also when the snooze
arrives through the WebSocket. The live-update tests change store state
inside act().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plans guide lists the writer columns and says that of two concurrent decisions only one applies. The accounts guide lists the writer columns and the snoozer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A revision request stored its feedback but left the plan pending, so an approval, a decline or a second revision made from the same pending read still matched the compare-and-update and also applied: a revision turn and an implementation could start for one plan. A revision request now moves the plan from pending to the new status "revising" in its compare-and-update. While the plan is revising, approve, decline (REST and tools) and another revision refuse with 409 or "no longer pending". plan_update accepts a pending or revising plan, because the planner's new version is the answer to the revision. When the revision turn ends, or its dispatch fails, a plan that it did not replace is pending again; at startup Nerve reopens plans that a stop left revising. A task with a revising plan takes no new proposal, and plan_list shows revising plans by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan view says that a revision runs and offers no approve, decline or revision while the plan is revising. The plan list has a "Revising" filter and badge tone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plans guide lists "revising", says which decisions refuse while a revision runs, and when the plan is pending again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH compared against the status it read, so a request that read "revising" could reopen or decline the plan while its revision turn ran, and an approval could then start an implementation beside it. At startup every revising plan was reopened, also when resume-after-restart continued its revision session. - PATCH refuses to change a revising plan (409) and to set "revising" (400). Only plan_update or the end of the revision turn leaves the status. - At startup, recover_revising_plans reopens only plans whose revision session is not enrolled for resume. finish_revisions_after_resume awaits the resume and then reopens the held plans that are still revising. The lifespan still calls resume_enrolled_sessions synchronously, so a failure there still stops startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plans guide says that PATCH cannot change or set "revising" and how startup treats a revising plan whose revision session is resumed. The answer routing entry of the architecture guide uses a colon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A REST approval creates the implementation session for the approver, an approval by the agent creates it for the cause of the approving session, and a revision that has no planner session yet creates it for the person who asked. These tests check the "created" events of those sessions and, in external mode with the CLI replaced by a recorder, that each session's model requests name its cause: a relayed answer names the cause of the session that asked, not the person who answered, and a relayed Telegram answer in a session with no cause names no principal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The project decided against a separate status for a plan under revision (rework decision R3). A revision request again leaves the plan pending, as before this change set; a revision and an approval of the same plan can both proceed, which is a known limitation outside this change. Removed: the "revising" status and its compare-and-update, the PATCH refusals (409, 400), the reopen at the end of a revision turn, the startup recovery (recover_revising_plans, finish_revisions_after_resume), the block on new proposals and the default listing in plan_list. compare_and_update_plan takes one expected status again. Kept: approve, decline, PATCH, revise and plan_update apply only while the plan has the status they read, so a lost decision changes nothing and the reviewer is the person whose decision applied. The created event of an implementation or planner session keeps its cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan view no longer shows a revision note or hides the decision buttons for a revising plan, and the plan list has no "Revising" filter or badge tone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plans guide no longer lists "revising". Its concurrency paragraph names approve, decline, PATCH and plan_update as the decisions of which only one applies, and says that a revision and an approval can both proceed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only decisions that change the plan's status exclude each other: a feedback-only PATCH or a revision request keeps the status, so two of them can both apply. The plans guide and the compare_and_update_plan docstring say so. Web tests check that a pending plan with revision feedback keeps its Approve, Decline and Request Revision buttons, and that the plan list has no "Revising" filter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under rework R1 (plan Part 0R) a session has one fixed cause, and there is no cause per turn or per message. - WS answer_injected carries only actor_id: the relayed message is the answerer's, and the agent's work in that turn has the asking session's cause. The web type drops caused_by_actor_id. - Docstrings, comments and docs name the session's cause where they named the cause of the calling turn: plan tools, the silence tool, plan authorship, and the sessions that an approval or a revision creates. - Tests use session-cause wording in their names and comments, and check that the injected event has no cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
request_plan_revision wrote the task note through a context with no actor and no cause, so the task row write was unattributed for a REST revision and for the plan_revise tool. The note context now carries the actor and cause that the caller supplies: the person for REST, the system actor and the session's cause for the tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The answerer was written only after the dispatcher finished. When the expiry sweep took the claimed row during a slow dispatch, the action ran and its audit named the person, but the expired row had no answered_by_actor_id. claim_pending_approval now writes answered_by_actor_id in the same UPDATE that claims the row, so the person stays on the row whatever happens after the claim. The service also checks the result of the snooze write, as it did for the answer write, and logs when the row stopped being pending. A test expires the row while an approve or a snooze dispatch waits, and checks the row and the audit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a decline, an approval or a revision request, the plan store changed only the fields it sent, so the reviewer stayed hidden until a reload, and a status that another decision had set stayed stale. The store now fetches the plan after each successful decision and installs the stored row. Interaction tests decline and request a revision in the plan view and check that "Reviewed by" shows at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alex-clickhouse
force-pushed
the
alex/attr-04-notifications-plans
branch
from
October 17, 2026 11:42
68f7466 to
2570271
Compare
alex-clickhouse
marked this pull request as ready for review
October 17, 2026 12:06
pufit
approved these changes
Oct 17, 2026
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.
What
Nerve records who answered, snoozed or dismissed a notification, who created a silence rule, and who wrote and who reviewed a plan. Approval dispatchers get the person who decided. Plan decisions that change a plan's status become atomic.
How
actor_refs. Old rows stay empty, and the attribution column registry lists the new columns:notifications.answered_by_actor_id,notifications.dismissed_by_actor_id;notification_silences.created_by_actor_id,notification_silences.caused_by_actor_id;plans.reviewed_by_actor_id,plans.reviewed_caused_by_actor_id,plans.created_by_actor_id,plans.caused_by_actor_id.pending, as before. A revision and an approval of the same plan can still both proceed. This is existing behaviour and stays a follow-up.plan_proposeandplan_updaterecord the system actor and the session's cause as the writer of the new version.actor(Nonefor a channel answer). The mechanical-action audit log names the person's actor ID.notification_answeredandanswer_injectedcarryactor_id.Why
These decisions start or stop work: an implementation session, a mechanical action, a review-loop leg. Recording the person completes the audit trail and gives that work the right cause, which is also the principal on its model requests (#516).
Behaviour changes
webinstead ofsystem.request_plan_revisiontakes a requiredactorargument. Every approval dispatcher must accept theactorkeyword argument.Testing
Stack
main)🤖 Generated with Claude Code