Skip to content

Record who schedules wakeups and starts cron runs, and run them for that person - #520

Draft
alex-clickhouse wants to merge 9 commits into
alex/attr-05-workflowsfrom
alex/attr-06-schedules
Draft

alex-clickhouse wants to merge 9 commits into
alex/attr-05-workflowsfrom
alex/attr-06-schedules

Conversation

@alex-clickhouse

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

Copy link
Copy Markdown
Collaborator

What

Nerve records who scheduled a wakeup or a deferred message and who started a cron run, and a cron run that a person starts runs for that person. Scheduled work follows the session cause model (#515): the agent's work in a session, including a wakeup's turn, is for the session's cause.

How

  • Migration v057 adds session_wakeups.created_by_actor_id, session_wakeups.caused_by_actor_id, cron_logs.actor_id and cron_logs.caused_by_actor_id. They are nullable foreign keys to actor_refs, with no backfill. The attribution column registry lists them.
  • "Run later" records the person on the wakeup row.
  • A wakeup that the agent sets (the Claude CLI's ScheduleWakeup, or the schedule_wakeup tool) records the system actor and the session's cause.
  • A wakeup fires in its session, so its turn (tool changes and, in external mode, model requests) is for the session's cause.
  • Cron:
    • Scheduled, catch-up and missed runs log the system actor, and their sessions have the system actor as cause.
    • A run that a person starts (the web UI, or POST /api/cron/jobs/{id}/trigger) logs the person. A session that the run creates (an isolated run, or a new persistent generation) has that person as cause.
    • A workflow-run job that a person starts this way starts its run for that person.
  • API and UI:
    • GET /api/cron/logs rows carry actor_id and caused_by_actor_id.
    • Sidebar session rows and the session_running event carry pending_wakeup_created_by_actor_id and pending_wakeup_caused_by_actor_id.
    • For a session that waits on a wakeup, the sidebar tooltip names who scheduled it: "by Alice", "by Nerve for Bob" or "by Nerve".

Why

The principal on a model request decides who pays for it, and that principal is the session's cause. A manual cron run must create its session for the person who started it; otherwise it counts as the agent's own work.

Approximations

  • A deferred message or a wakeup in an existing session is attributed to that session's cause, also when another person deferred it.
  • A persistent cron session that a person's "Run now" created stays attributed to that person for later scheduled runs, until it rotates.

Behaviour changes

  • The trigger route resolves the caller's identity and passes it to the run.
  • A cron run ID is now the start second plus eight random hex digits (for example cron:nightly:20260102-030405-ac21b72a), so two runs started in the same second get separate sessions. The usage report reads both the old and the new shape.

Testing

  • New tests for every path. The external-mode tests check that a fired wakeup in Alice's session names Alice, that a "Run now" session names the person who clicked, and that two people who trigger one job in the same second get separate sessions and headers. Every commit passes its tests.
  • Full suite on this branch (the top of the stack): 5931 passed, 39 skipped. Web: build ok, 740 tests passed.

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 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 (this PR): Schedules: run-now, run-later and wakeups

🤖 Generated with Claude Code

alex-clickhouse and others added 9 commits October 17, 2026 10:51
A scheduled turn must run for the principal that caused it, so that its
model requests and tool changes name that person. Until now a run-later
message, an agent wakeup and a "Run now" cron run all ran for the system
actor.

Migration v057 adds session_wakeups.created_by_actor_id and
.caused_by_actor_id, and cron_logs.actor_id and .caused_by_actor_id
(nullable foreign keys to actor_refs, no backfill). The registry lists
the four columns.

- Run later through REST records the person as scheduler and cause.
- An agent wakeup (the ScheduleWakeup hook of the Claude CLI, or the
  schedule_wakeup tool) records the system actor and the session's
  cause.
- A fired wakeup runs in its session, so its turn has the session's
  cause.
- Scheduled, catch-up and missed cron runs log the system actor as
  actor and cause. POST /api/cron/jobs/{id}/trigger logs the person,
  and a session that the run creates (an isolated run, or a new
  persistent generation) has that person as its cause.
- Session list rows and the session_running event carry
  pending_wakeup_created_by_actor_id and
  pending_wakeup_caused_by_actor_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parked dot of a session that waits on a wake-up names who scheduled
it: "by Alice" for a deferred message, "by Nerve for Bob" for a wakeup
that the agent set in Bob's turn, "by Nerve" for autonomous work. The
names come from the actor directory. A wake-up without a recorded
scheduler shows only its time.

The attribution hooks move from ActorLabel.tsx to
hooks/useActorAttribution.ts, so that a component that needs the text
for a tooltip can use the same "<actor> for <person>" rule as
ActorAttribution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/accounts.md lists the four new attribution columns and the actor
and cause of "Run now", "Run later", agent wakeups and the turns they
fire. docs/api.md describes the actor fields of cron logs, the trigger
route and the pending wakeup fields of session rows. docs/cron.md lists
the two cron_logs columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The writing rules forbid em-dashes in docs and comments. Replace the
four in the text that this branch adds with a colon, parentheses or a
comma.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inference principal header is in the base of this branch, so the
tests of the header on fired wakeups and on a run-now session run
unconditionally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cron job that declares a workflow run started the run for the system
actor also when a person triggered the job through the REST API, so the
run's turns and model requests named the system principal. The cron
service now passes the cause of the cron run to the run: the system
actor starts it, for the person who used "Run now", or for itself on a
scheduled run.

docs/accounts.md, docs/api.md and docs/workflow-runs.md say so, and the
actor directory intro in docs/api.md names wakeups and cron run logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the session cause model a session has one cause, and every agent
turn in it runs for that cause. A fired wakeup runs in its session, so
the cron service no longer passes the wakeup's stored cause to the
turn; the wakeup row keeps who scheduled it and for whom. A deferred
message that Bob leaves in Alice's session therefore fires for Alice,
the documented approximation.

The comments of the wakeup store, the migration, the ScheduleWakeup
hook and the schedule_wakeup tool say that an agent wakeup records the
session's cause.

Tests: the wakeup sweep passes no cause to the turn. The cron rotation
test seeds a person as the session cause and expects the retitle to be
the system actor's for that person. A persistent session that a person's
"Run now" creates keeps that person as cause for a later scheduled run,
its rotation retitle names the person, and the next session is the
system actor's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Isolated cron runs and persistent generations got session IDs with the
start time to the second. Two people who used "Run now" on one job in
the same second shared one session, so both runs had the cause (and the
inference principal) of whoever created it first. An immediate rotation
could also reuse the retired generation.

A run ID is now the UTC start second and eight random hex digits
(new_cron_run_id in nerve/agent/sessions.py), used for isolated runs,
persistent generations and run_cron's default. The cron:<job>: prefix
stays. The usage report matches the new shape and the older one.

Tests: two concurrent "Run now" requests of Alice and Bob in one second
get two sessions with their own causes and principal headers; a manual
rotation and a rotated generation in the same second get new sessions;
the usage report groups both run ID shapes. docs/cron.md and
docs/architecture.md describe the run ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/accounts.md lists three approximations of the session cause model
(shared sessions, deferred messages and wakeups, a persistent cron
session that a person started); the lead sentence said two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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