Skip to content

Record who changes a session, and who deletes it - #517

Merged
pufit merged 7 commits into
alex/attr-02-inference-principalfrom
alex/attr-03-session-changes
Oct 17, 2026
Merged

pufit merged 7 commits into
alex/attr-02-inference-principalfrom
alex/attr-03-session-changes

Conversation

@alex-clickhouse

@alex-clickhouse alex-clickhouse commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

What

Nerve records who changed a session and on whose behalf. Each change is a session event with actor_id and caused_by_actor_id. A delete removes the session's events, so Nerve records each delete in a new table, session_deletions.

How

  • Migration v054 adds session_events.actor_id, session_events.caused_by_actor_id, and the table session_deletions (session ID, last title, time, actor, cause). The columns are nullable foreign keys to actor_refs. Old rows stay NULL.
  • A PATCH to a session records one event for each field that changes: title_changed, starred / unstarred, model_changed or parent_changed, with the old and the new value. The event is written in the same transaction as the change.
  • These actions record the signed-in person: archive (each session of a cascade too), unarchive, resume and delete through REST; stop and resume through the browser WebSocket.
  • The delete and its session_deletions row are one transaction.
  • A change that the agent or Nerve makes in a session is the system actor's change, for the session's cause. This covers the placeholder and the generated title, the retitle of a rotated cron session, the stop of a workflow run, and external tool call events.
  • The created event names the creator and the session's cause.
  • Automatic archives and the Codex sync archive are the system actor's own housekeeping, with the system actor as both actor and cause.
  • Channel commands (/new, /stop, /star) record no actor and no cause.
  • Runtime state events (started, idle, error) stay unattributed.

Why

Session changes and deletes had no record of the person. This PR is part of the series that records the actor and the cause of every change.

Behaviour changes

  • An event is recorded only when the change had an effect:
    • An archive of an archived session does nothing (no event, no memorize).
    • An unarchive changes only an archived session.
    • A resume of a resumed session records nothing.
    • When two people archive, restore, resume or create the same session at the same time, only the change that has an effect is recorded.
  • Resume adds reason: "resume" to its created event.
  • A stop that reaches a running turn adds a stop_requested event.
  • The events API returns two more fields.

Testing

  • tests/test_session_attribution.py (new), including concurrency tests held at a barrier. Every commit passes its tests.
  • Full suite on this branch: 5848 passed, 38 skipped.

Stack

  1. Record each session's cause and who changes a task #515 Actor and cause plumbing; task attribution (base main)
  2. Name the session's cause on each model request in external mode #516 Model requests name the session's cause
  3. Record who changes a session, and who deletes it #517 (this PR): Session change attribution
  4. Record who answers notifications and reviews plans #518 Notification answers, plan reviews and approval decisions
  5. Record who starts, kills and decides workflow runs and review loops #519 Workflow runs and review loops
  6. Record who schedules wakeups and starts cron runs, and run them for that person #520 Schedules: run-now, run-later and wakeups

🤖 Generated with Claude Code

alex-clickhouse and others added 7 commits October 17, 2026 09:00
Session changes had no attribution, and a delete removed the session's
events, so nothing told who deleted a session. Each change that a
principal makes is now a session event with an actor and a cause.

- v054 adds session_events.actor_id and caused_by_actor_id, and the
  table session_deletions (last title, time, actor, cause). The four
  columns reference actor_refs and are in the actor column registry.
- REST PATCH records title_changed, starred, unstarred, model_changed
  and parent_changed for each field that changes, in the transaction of
  the write. Archive (with its cascade), unarchive, resume and delete
  through REST, and stop and resume through the WebSocket, record the
  person as actor and cause.
- A delete writes its session_deletions row in the same transaction.
- The placeholder and the generated title are the system actor's change
  for the cause of the turn. The title task gets the cause as an
  argument, so a later turn of another person cannot change it.
- Automatic archives, the Codex sync archive, the cron rotation retitle
  and the workflow stop are the system actor's own work. Channel
  commands record no actor and no cause. External tool call events take
  the attribution of their tool context.
- Events that record the runtime state (started, idle, error, a stopped
  status after a cancel) stay unattributed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the session event types, their details, their actor and cause,
and the session_deletions record. The API docs show the two new fields
on session events, and the WebSocket stop and resume events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An archive of an archived session wrote another archived event, so a
cascade could record that Alice archived a child that Bob had archived
before, and two archives at the same time recorded two people for one
change. The same applied to a restore.

change_session_status reads the status, writes it and appends the event
in one transaction, and writes nothing when the change has no effect.
Archive, unarchive and the Codex sync archive use it. A cascade counts an
archived descendant as archived and records no event for it. A restore
changes only an archived session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also use plain punctuation instead of dashes in the new text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two resumes at the same time each wrote a created event for one status
change, and two callers that created one session ID each wrote a
created event, although the row names one creator. A stop that only
interrupted an idle client recorded a stop request with nothing to stop.

- transition uses change_session_status, so a resume writes its event
  only when its own write changes the status, in one transaction.
- create_session takes created_event and appends the created event in
  the transaction of the insert, only when this insert made the row.
  _create_session uses it.
- stop_session records stop_requested only when a turn or a registered
  task of the session runs (SessionManager.has_running_work).

Tests hold two REST or WebSocket resumes, and two creates of one ID,
until both callers have read the session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A resume of a resumed session, a create of an existing ID and a stop
when no turn runs record nothing, like an archive of an archived
session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each session now has one fixed cause, and every change that the agent or
Nerve makes in a session is the system actor's change for that cause.

- The placeholder and the generated title record the system actor and
  the session's cause, whoever sent the message. The title task no
  longer takes a cause argument; the stored cause does not change.
- The created event records the creator and the session's cause from
  the creation cause, in the transaction of the insert.
- The retitle of a rotated cron session and the stop of a workflow run
  record the system actor and the session's cause.
- Automatic archives and the Codex sync archive stay the system actor's
  own housekeeping. Changes that a person makes are unchanged.
- Tests: a title in Alice's session after Bob's message names Alice; the
  created event of a session that Nerve creates for Alice names Alice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alex-clickhouse
alex-clickhouse force-pushed the alex/attr-03-session-changes branch from 46699d1 to 253e2ec Compare October 17, 2026 11:42
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review October 17, 2026 12:06
@pufit
pufit merged commit 8d7c5d9 into main Oct 17, 2026
6 checks passed
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.

2 participants