* feat(wakeups): accept blocked in --until-issue-state Lets a coordinator wait for a specific child to reach blocked, alongside the existing in_review handoff, without waiting for the child to close. Exact status key match, same as in_review. Refs #9344 Co-authored-by: multica-agent <[email protected]> * fix(i18n): keep blocked state key in lowercase English in zh wakeup copy Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
42 KiB
Issue wakeups
An agent or a member can save an event subscription, a condition or a timer on an issue; the agent finishes its run and receives another ordinary run when the input arrives. The platform only compares facts it stores (a field value, sub-issue status, a linked pull request's state); business completion is still decided by the agent after reading current state. There is no sleeping process or second run lifecycle.
Product contract
The shared web/Desktop issue sidebar lists event and time wakeups together,
as compact trigger/target summaries with a separate execution status when available.
Instructions, full filters, errors and
latest-run transcript are in the details popover. A directly visible toggle
controls enabled configurations and restores manually disabled subscriptions or
recurring schedules. Consumed one-shots offer Cancel this execution while unclaimed,
then Enable again (events) or Set a new time (time).
Wakeup history is collapsed by default. A consumed one-shot remains
in the current group while its run is queued, deferred, dispatched or running.
Running or already
claimed tasks use the normal Stop controls. Terminal lifecycle categories
(done and closed, including custom statuses) disable all configurations;
reopening does not reactivate them.
Board and list activity cues prioritize current runs, mark wakeup-origin runs, and show enabled wakeup rules as a separate count, not a forecast of executions. Without an active run they show the next scheduled time (including a date when needed) or waiting for an event. A shared workspace summary request contains exact enabled counts and at most three previews per issue, never prompts or history. Access follows shared issue visibility and workspace membership; private source-agent and source-run filters are redacted consistently. The issue surface polls once every ten seconds; cards select their own rows from that shared cache.
Restoring an interval schedules from now; cron uses its next future occurrence, without replaying missed times. An unconsumed future one-shot can be toggled back on; expired or consumed time wakeups require choosing a new future time. One-shot rearming is refused while a previous run is active. Closed issues cannot enable wakeups; reopening an issue still requires manual enable.
The additive POST /api/issues/{id}/wakeups/{wakeupID}/enable accepts the observed
revision, plus optional rearm and future at. It loads stored configuration
under the existing issue/configuration locks and reuses Save's validation,
authorization, receipt cleanup and revision fencing. A stale revision returns
409, and a repeated already-enabled request with the current revision is a no-op.
Clients do not resend instructions, filters or thread references. The new UI
requires this endpoint for restore; deploy the server first. Old clients and the
existing full-config CLI update continue to work without a migration.
Agents manage configurations with multica issue wakeup:
multica issue wakeup events
multica issue wakeup create ISSUE --kind at --after 10m --instruction-file ./instruction.md
multica issue wakeup create ISSUE --kind every --every 1h --instruction-file ./instruction.md
multica issue wakeup create ISSUE --kind cron --cron '0 * * * *' --timezone Asia/Shanghai --instruction-file ./instruction.md
multica issue wakeup create ISSUE --kind event --event task.completed,task.failed,task.cancelled --task-id RUN --instruction-file ./instruction.md
multica issue wakeup create ISSUE --until-status in_review --instruction-file ./instruction.md
multica issue wakeup create ISSUE --until-pr checks --expires-in 2h --on-timeout wake --instruction-file ./instruction.md
multica issue wakeup create ISSUE --until-children-done --stage 1 --instruction-file ./instruction.md
multica issue wakeup create ISSUE --until-issue MUL-123 --until-issue-state done --instruction-file ./instruction.md
multica issue wakeup list ISSUE
multica issue wakeup get ISSUE WAKEUP
multica issue wakeup runs ISSUE WAKEUP
multica issue wakeup trigger ISSUE WAKEUP
multica issue wakeup disable ISSUE WAKEUP
multica issue wakeup delete ISSUE WAKEUP
multica issue wakeup checkin ISSUE WAKEUP --note "CI still running"
Specify --agent-id for human callers; authenticated agents default to themselves.
Event subscriptions default to once; --mode continuous keeps listening.
A concrete source run must belong to this issue. Registration checks its current
terminal state under a lock, so a run that just finished is not missed. A busy
source run returns 409 and asks the caller to retry; registration never waits
on a source lock while holding the issue lock. The server returns the stable
wakeup_source_busy code for this rolled-back conflict; CLI create retries it
once after 250ms. Other conflicts and ambiguous network failures are not retried.
Broad
agent filters observe future facts, without replaying history or following retry
chains. --parent COMMENT preserves the original result-delivery thread.
update ISSUE WAKEUP accepts the complete create configuration, replaces it,
increments its revision, withdraws old unclaimed inputs, and explicitly enables
it. The creator or a workspace owner/admin may update or disable; the replacing
caller must themselves be allowed to invoke the selected agent and becomes the
new configuration's recorded human principal.
The scheduler checks approximately every 30 seconds. One-shot timers remain queued while their runtime is offline. Repeating timers coalesce missed periods into one pending check and continue from the next future time; they do not replay every historical tick. Runs keep normal comment delivery, except that a run an every/cron rule started may end with a check-in instead (see below). Linked pull request conditions read the stored PR snapshot; there is no separate CI event.
Event catalog
multica issue wakeup events lists the 25 supported issue-scoped subscriptions:
| Area | Events |
|---|---|
| Run | task.queued, task.dispatched, task.started, task.deferred, task.waiting_local_directory, task.completed, task.failed, task.cancelled |
| Issue | issue.updated, issue.status_changed, issue.assignee_changed, issue.parent_changed, issue.project_changed, issue.labels_changed, issue.properties_changed, issue.metadata_changed |
| Comment | comment.created, comment.updated, comment.deleted, comment.resolved, comment.unresolved |
| Reaction | reaction.added, reaction.removed (issue or comment) |
| Attachment | attachment.attached, attachment.detached (issue or comment) |
task.started means the persisted run entered running. Retries emit a new
task.queued with retry_of_task_id; manual reruns carry rerun_of_task_id.
Queue/defer transitions may happen repeatedly. Unchanged writes, bookkeeping
revisions, duplicate reactions, and pruning an already-deleted comment do not
produce another fact. Attachment events describe binding to an issue/comment,
not uploading an unbound file; moving a file produces detach and attach facts.
issue.updated includes changed_fields for meaningful issue fields, excluding
position, revision and timestamps. Specialized issue events can accompany it;
subscribing to both produces two inputs that the normal dispatcher coalesces.
Metadata/properties events include changed keys, never their values. Comment
events contain comment/thread/parent references, not bodies. Attachment events
contain references, not private URLs or filenames. Agents pull current state to
decide what to do. There is no business-condition evaluator.
Each newly captured fact includes event_id, event_type, version,
occurred_at, workspace/issue IDs, actor identity and optional source run/agent.
The already-terminal registration snapshot additionally has observed_at and
registration_snapshot; its occurred_at can be null for historical runs with
no completion timestamp. Older queued payloads remain readable.
--filter-agent-id selects the run's agent for task events. New mutation-only
requests using this legacy flag normalize to filter_actor_type=agent and
filter_actor_id; use actor flags for comment/issue/reaction/attachment changes.
Existing stored rules and legacy mixed task/mutation requests remain supported. Editing another agent's comment does not make that
agent the editor; payloads distinguish actor from author. --task-id accepts
only run events. Filters never expand the subscription beyond its current issue.
Use --filter-actor-type member --filter-actor-id USER_ID to wait for a
specific workspace member, or type agent for a source agent. For example:
multica issue wakeup create ISSUE --kind event --event comment.created \
--filter-actor-type member --filter-actor-id USER_ID \
--instruction-file ./instruction.md
These optional paired filters apply to issue, comment, reaction and attachment changes. They match the actor who performed the change; for a new comment this is its author, but for an edit it is the editor, not the original author. Unknown system attribution does not match. Task subscriptions keep their existing agent/run filters. Actor filters cannot be combined with those source filters. The ID is the user's UUID, not the membership record's UUID; it must belong to the current workspace. Other people's events neither enqueue nor consume a one-shot rule. Broad subscriptions without actor filters behave as before. Enable/rearm retains the stored actor filter; an explicit full update can replace or clear it. The sidebar, board summary and inventory show the actor's name; private agent references are redacted, retaining a generic selected-actor label.
Migrations 531–532 add nullable actor columns and update transactional capture. Apply migrations, then deploy the API/CLI before creating actor-filtered rules; update web/Desktop to display the actor restriction. Old clients can read the additive response, but cannot show the new restriction. On application rollback, keep these migrations: the database still enforces stored filters even for older consumers. Schema rollback deliberately refuses while any actor-filtered rule exists, including disabled rules, so a later re-enable cannot silently broaden it. Disable/drain and remove those configurations before rolling 532/531 back.
The platform lifecycle names issue.created and issue.deleted are listed
separately and rejected for self-wakeups: subscription requires an existing
issue, and deleting it withdraws its work. Workspace/cross-issue subscriptions,
external CI push events, and expanding the plugin subscription contract are
outside this version. Terminal issue transitions disable wakeups rather than
starting a final run on the closed issue.
Implementation
pkg/eventcontractowns stable business-event names independently of plugins. Plugin constants alias the existing names, preserving their payload contract.issue_wakeupstores configuration andissue_wakeup_receiptstores matching inputs. Run transitions and the collaboration changes in the catalog above capture matching receipts in the source transaction. SQL capture hooks cover service, scheduler and HTTP writers without a best-effort in-memory hop. They do not build a general event archive or evaluate business predicates.- Registration is prospective once committed; agents should subscribe before querying current state. The explicit run filter also checks current state during registration. There is no global event order or historical replay API.
- The existing scheduler leases
issue_wakeup_dispatch. The adapter locks one issue/configuration, rechecks scope and the creator's current invoke rights, and consumes receipts together with ordinary task enqueue. Failure leaves the receipt available for retry. Claim rechecks permissions after an offline wait. - Event notifications merge by rule, revision and event type, keeping the first occurrence time, count and latest source reference. Agents read current state and comment history when intermediate references have been condensed. Time inputs replace the pending time note with the newest signal. A claimed prompt is immutable; later input becomes at most one subsequent queued task.
- Mutations and run events from the same wakeup's run are ignored. HTTP mutation transactions stamp server-resolved actor and source task identity in local PostgreSQL settings; those settings do not survive connection reuse. A client cannot choose source identity through a request body or an untrusted task header.
context.wakeup_idandcontext.wakeup_revisionidentify the new trigger. Bounded structured facts live in additivecontext.wakeup_evidence(version 1). Its instruction/facts travel in the ordinary per-turn handoff note. Daemon wakeup prompts preserve this instruction even when a delivery thread exists. Automatic retries inherit both context and note.- The existing pending-task index is retained. For wakeup tasks only, its derived
comment_thread_idscheduling scope is the configuration ID; the real delivery thread stays intrigger_comment_id. This preserves old retry SQL during a rolling server upgrade. Comment/assign coalescing excludes wakeup inputs, and the existing issue/agent execution fence still serializes actual runs. - Issue/workspace deletion explicitly removes configurations and receipts in the application deletion graph. No foreign keys or cascading relationships are added.
Deployment and verification
Apply additive migrations before starting the new server. Existing pending-task indexes are not rebuilt or dropped, and historical queue rows are not rewritten. Deploy the updated CLI and daemon with the server to recognize the wakeup command and per-turn prompt. Before rollback, disable/drain wakeups; do not remove their configuration tables while tasks still reference them.
Migration 520 adds capture hooks without indexes, table rewrites, or foreign keys. Deploy it before admitting subscriptions to the expanded catalog. Older servers still dispatch the added receipts and older sidebars fall back to raw event names; only updated servers accept create/update with new event types. During a rolling upgrade, mutation attribution from older HTTP servers is best effort (some older write paths provide no source identity). Keep new event subscriptions disabled until all mutation-serving instances are upgraded, so their own writes cannot feed back without attribution. Before rolling application code back, disable subscriptions using the expanded catalog; before rolling the capture migration back, drain their inputs as well. The down migration restores the original five-event capture behavior and retains configuration/receipt data.
Migration 521 adds a concurrent partial index for enabled workspace summaries. It can be rolled back independently of configuration data. Deploy the server before the UI: an older server lacks the summary endpoint, so cards cannot show future wakeups until it is upgraded. Existing task activity still works. New detail fields are optional for rolling compatibility.
Migration 523 excludes the registering run's own events, in addition to events from runs produced by the same rule. Human/external events with no source run still match. Apply this migration before enabling broad agent-created subscriptions. Rolling it back restores the previous capture function without rewriting data; disable affected subscriptions first to avoid registration feedback.
This unmerged branch's wakeup migrations use prefixes 509–532, following main's migrations through 508. The previously used 500–523 wakeup names were renumbered by +9 without changing their SQL or relative execution order.
Before updating a local database that has already applied the old branch,
stop its API and daemon and rename only the exact wakeup
schema_migrations.version entries from the old filenames to the new filenames
(+9). Do not rename main's similarly numbered migrations or rerun the base
wakeup table creation. Databases still on the older 495–508 wakeup names first
need the previous +5 rename to the 500–513 names, then this +9 rename.
Keep this ledger change transactional; leave all wakeup rules, receipts and
queued tasks intact. Fresh databases use the normal migration runner.
Dispatch keeps the instruction and recent evidence within a 40,000-byte prompt
budget. Large or older details are explicitly condensed; original receipts remain
linked to the task and are consumed atomically with its queue update. A claimed
prompt is immutable, so further inputs remain pending until it starts or recovers.
After the normal 90-second claim recovery window with an expired/absent prepare
lease, last_error exposes that wait. Existing runtime claim recovery owns retries;
wakeups do not add another execution timeout. Timer progress advances while waiting.
Integration coverage includes transaction rollback, already-terminal registration, source scope, one-shot deduplication, independent comment/assign input, merging, self-loop suppression, custom terminal statuses, reopen behavior, offline timers, configuration replacement, revoked permission at enqueue/claim, and retry prompt inheritance. UI tests cover disabling and consumed one-shot state; API response schemas reject malformed wakeup state rather than presenting an empty list. Expanded-event integration tests execute writes for every advertised event and cover attachment rebinding, repeated queue transitions, tombstone cleanup, meaningful-change suppression, rollback, actor/author distinction, forged source headers, metadata redaction and self-loop suppression on HTTP mutations.
Workspace management
Web and Desktop expose Issue wakeups inside the Autopilot page (?tab=wakeups).
The tab remains available without any autopilots. GET /api/issue-wakeups returns
an access-filtered inventory with counts, agent filter choices, and offset
pagination (50 by default, maximum 100). Scope, trigger kind, target agent, and
literal case-insensitive search run on the server; page rows and counts share
one database snapshot. Prompts are omitted from this collection response.
Active means an enabled rule on an open issue or an unfinished run, including consumed one-shot rules and manually disabled rules with running work. The query looks up runs by their wakeup context, so a retry remains visible even if the rule's last-task pointer refers to an older attempt. Configuration state and run state are separate columns. Rules on terminal issues appear under Ended once all their runs finish. Counts include every rule on shared issues, including rules targeting private agents. Reading a rule does not grant permission to invoke or configure its agent. Private source-agent and source-run references are redacted in inventory, sidebar and board summary responses alike. Instructions remain shared issue content and are omitted from inventory and summary responses.
Trigger labels describe conditions (for example, "When Emacs's run succeeds"), not an outcome that has already happened. The monitored agent and specific run are distinct from the agent to wake. Multiple event types mean any of those events. Rule labels distinguish waiting, scheduled, turned off, triggered, expired, and stopped because the issue ended. A consumed one-shot is Triggered even when its execution is still queued or has failed. A manually disabled rule remains Turned off even if its previous execution succeeded. Execution results have their own labels and transcript entrypoint. Familiar schedules use natural language, while details retain the original cron and timezone. Disabled one-time rules retain their original scheduled time.
The sidebar and inventory share enable, resubscribe, reschedule, and withdrawal controls. Batch disable is limited to explicitly selected enabled rules on the current page. It confirms that already-started runs continue, sends bounded sequential calls to the existing authorized disable endpoint, and retains only failed selections for retry. The inventory polls every ten seconds; mutations invalidate inventory, sidebar, board summaries, and task caches. No scheduler or Autopilot execution semantics change.
Database load and notification retention
The scheduler discovers candidates from due timers and the partial pending-receipt index, instead of scanning disabled rule history. Each dispatch has a two-second budget and a 50 ms PostgreSQL lock timeout; diagnostic/fairness writes have their own 100 ms budgets. Contention leaves inputs pending for the next tick and does not spend the full batch deadline on one issue. The global inventory counts only unfinished runs before pagination; indexed latest-run lookups happen for the selected page, rather than sorting the entire completed-run history.
At most 32 rules per issue and 1,000 per workspace can be enabled. These are
activation limits: disabled history does not count, existing rules are not
silently removed, and editing an already-enabled rule does not consume another
slot. A database trigger serializes activation within a workspace, including
concurrent creation on different issues. Capacity errors return HTTP 400 with
wakeup_capacity_exceeded. These initial limits bound synchronous source-event
fanout and can be revisited with measured workload data.
New event captures retain at most one pending notification per rule revision and event type (25 currently supported types). Repeated inputs retain a count, first occurrence time and the latest source reference, with an explicit prompt instruction to read source state. This is a wakeup notification, not an immutable event archive or a promise to execute once per source event. Legacy pending receipts remain readable and drain in batches of 100. Processed receipts become eligible for deletion after seven days, with up to 1,000 deleted per scheduler tick; backlog can extend that retention. Pending inputs are never age-expired. Run history and source comments are unaffected. Receipt keys suppress retained first/latest fact duplicates; they are not a permanent deduplication ledger for all intermediate coalesced facts.
Migrations 524–527 add concurrent lookup/expiry indexes, 528 adds a nullable coalescing key, 529 adds its partial unique index, and 530 enables bounded capture and activation limits. Apply in that order before the new server. No existing receipt rewrite is needed. New consumers lock receipt rows before constructing queue evidence. Each merge also rotates its receipt ID: an old consumer can only mark the version it read as processed, so a concurrent replacement remains pending during rolling upgrades (a redundant check is possible, lost new input is avoided). Existing event/task keys and API fields remain compatible.
Rolling application code back leaves the database limits and coalescing active; old consumers can still drain the notifications. To roll the schema back, reverse 530 before dropping 529/528 and the additive indexes. This restores the prior capture function and removes limits without deleting rules or pending inputs. The base wakeup tables must still be retained while any wakeup runs reference them.
Summary compatibility and capture locks
Pending-run evidence is merged as structured facts and rendered once, without
parsing a previous prompt's headings or omission messages. The total rendered
note still fits 40KB. Older tasks, or notes rewritten by an older dispatcher,
are carried as one opaque bounded legacy item; overflowing history is marked
as condensed and the latest input remains readable. Existing readers and retries
continue using handoff_note; no migration or new execution lifecycle is needed.
Dispatch takes the pending-task lock before locking event receipts, so waiting for an existing task does not hold up capture on those receipt rows. Receipt consumption and task enqueue/update still commit in one transaction. Source writes can still wait during that short critical section; this is not a promise of nonblocking comment writes. Splitting enqueue from consumption would require another delivery/recovery protocol and is intentionally avoided.
Direct self-trigger protection is scoped to one rule and trusted source-run identity. Cross-rule cycles remain possible; avoid mutually triggering continuous comment subscriptions. Prefer a member actor filter for waiting on a human. Target agent names follow shared issue visibility, while source references and invocation/management authority keep their existing checks.
Editing wakeup instructions
The issue sidebar details and Autopilot wakeup list share a prompt editor. The
workspace list loads the issue's prompts only when the editor opens. Saving uses
PATCH /api/issues/:id/wakeups/:wakeupID/instruction with instruction,
expected_instruction, and revision; success returns 204. The existing full
replacement PUT still has its original reconfiguration/rearm semantics.
Prompt-only edits preserve schedule, enabled/consumed state, creator, provenance, subscription revision, pending receipts and existing runs. Later dispatch uses the new instructions (including when merging new evidence into a still-pending run); saving itself does not rewrite queued or running tasks. Structured evidence keeps the instruction used for its last rendering, so editing does not turn structured facts into a nested old prompt. Pre-snapshot notes remain bounded historical context, explicitly subordinate to the current instruction. Closed/disabled rules may be edited without resuming them. The caller must be the creator or a workspace admin/owner and must currently be allowed to invoke the target agent.
Under the issue/config locks, both the prior prompt and subscription revision must match; otherwise saving returns 409 and the UI retains the draft. Keeping the subscription revision avoids invalidating receipts and queued work on a text edit. This additive endpoint needs no migration and does not change existing clients. Deploy the API before using the editor; an older API rejects the new endpoint and the editor keeps the unsaved text.
Deadlines, member-created rules and the child-done system rule
Rules can end on their own. expires_in_seconds is a relative wait stored in
expiry_seconds; re-enabling restarts it from now. expires_at is an absolute
end, typically a recurring check's end date; a passed absolute end cannot be
re-enabled without replacing the rule. Single-time (at) rules end when they
fire and take no deadline. Rules without either field keep the previous
open-ended behavior, so existing agent-created rules are unaffected.
The scheduler treats enabled AND expires_at <= now() as a candidate (partial
index 554). Under the usual rule lock, a reached deadline disables the rule and
sets timed_out_at; disabled_at stays NULL because it means "turned off by a
person" and claim-time checks refuse wakeup runs whose rule has disabled_at.
Inputs captured before the deadline still dispatch. on_timeout=wake (event
rules only) records one wakeup.timeout receipt, so the target runs once to
escalate, extend or stop; end (the default) ends without a run.
Members create rules from the issue sidebar with the existing create endpoint;
the form maps choices (a time, a recurring check with an end date, someone's
reply, an agent's run ending, or raw events) onto the same configuration an
agent sends. List responses add created_by_agent, created_by_name and the
redacted source_agent_* of the run that created a rule.
The parent-assignee wake on a closed sub-issue stage is the platform's
child_done system rule: an ordinary issue_wakeup row with system_rule
set (migration 555), one per parent (unique index 556). It shares the
condition, receipts, runaway protection, timeline entries, list and run model
of people's rules; what differs is owned by the platform:
- Target.
agent_idandcreated_byare NULL. When it fires the rule resolves the parent's assignee: an agent gets a wakeup run, a squad's leader gets a leader-role run, a member gets achildren_doneinbox notification, and no assignee (or an agent that cannot run) only records the timeline entry. The run is accountable to the parent's own provenance, as a run its assignment would start, and it is not re-checked against a creator at claim time; claim requires the rule on and the agent runnable. - Condition.
{"type":"children_done","each_stage":true}(clients cannot seteach_stage). It holds with fingerprintstage:Nwhen stage N and every earlier stage are closed while a later stage waits, and withallwhen every sub-issue, staged or not, is closed; the last stage alone does not hold, so the wrap-up waits for unstaged sub-issues. A reopened sub-issue clears the fingerprint, so closing it again fires again. People'schildren_doneconditions read the same sub-issue set (childrenDone). - Evaluation. Not polled (
next_fire_atstays NULL). Migration 558 addsissue_child_eventand triggers onissuethat record, in the writing transaction and for every writer, a sub-issue entering or leaving a closed status (built-in or custom done/closed category), joining or leaving a parent, or changing stage. The request that wrote it processes the rows right after commit (ProcessChildEvents: claim per parent, create the parent's rule if missing, give every sub-issue rule on the parent achildren.changedhint, dispatch them, people's rules first); theissue_child_event_sweepjob retries rows left unclaimed for 30 seconds or claimed for more than five minutes and deletes processed rows after a week. The rule also subscribes toissue.status_changedon the parent, so leaving backlog re-evaluates it. - Holds. A parent in backlog is not woken and nothing is marked as seen: leaving backlog wakes the assignee once for what closed meanwhile. A closed parent keeps the rule (the platform never disables a system rule for a closed issue); it rests until the parent reopens.
- Joining a waiting run. If the target agent already has an unstarted run
on the parent that runs as the person the rule's own run would (see "Repeat
runs" below), the rule keeps its facts for that run instead of queuing a
second one. Runaway protection is the hourly limit
(12 runs, then
paused_reason=rate); loop detection does not apply because stage hand-offs cross issues by design, and there is no fire cap. - Instruction. The run's
[WAKEUP]block carries the instruction and the observation (each stage's closed and cancelled counts, the closed stage, the next stage). The instruction is the issue's own (instructionon the row), else the workspace's (system_wakeup_child_done_instruction), else the built-inChildDoneDefaultInstruction. The daemon tells the agent the rule is the platform's and not to change it. No system comment is posted; thewakeup_triggeredentry (rule=child_done,stage,total,target_*,outcome:woke,notified,mergedornone) is the record.
People manage it, agents cannot. GET /api/issues/{id}/system-wakeups
describes what the parent is waiting for (lowest open stage or every
sub-issue, remaining, target, why no run would start) with the row's id,
revision, enabled, instruction, default_instruction, customized and
paused_reason. PUT /api/issues/{id}/system-wakeups/child_done accepts a
partial body from a member (403 for an agent), creates the row if needed, sets
customized_at, clears a pause when turning it on, and records what already
holds so facts that closed while it was off do not fire. The ordinary
per-rule endpoints (update, enable, disable, instruction, trigger, delete)
reject system rows. The workspace default lives in the settings keys
system_wakeup_child_done (only an explicit false turns it off) and
system_wakeup_child_done_instruction, edited under Settings → Wakeups through
GET /api/system-wakeups and PUT /api/system-wakeups/child_done (owners and
admins). Changing the default applies to every rule nobody customized and the
platform did not pause, re-baselining the ones it turns on.
Open parents that predate the rule get it from the sweep job's one-time
backfill, with what already holds recorded so nothing fires. Whichever writer
creates a rule (the backfill or processing a change), sub-issues whose closing
is recorded in issue_child_event but not yet processed count as still open
in its baseline, so that closing still wakes the assignee. System rows do not count
toward the per-issue and per-workspace capacity (guard_issue_wakeup_capacity).
Migrations 553–560 are additive for existing rows. Deploy them before the
server and the server before clients; older clients ignore the new fields.
Issue and workspace deletion remove the rows with the issue's other wakeups
and issue_child_event rows.
Repeat runs: acknowledged and merged firings
Runs are serialized per issue and agent (ClaimAgentTask), so a wakeup never
runs beside another run of its agent, but it can queue behind one and repeat it.
Pending runs are unique per issue, agent and scope (comment_thread_id, which
is the rule id for wakeup runs), so an assignment run, a comment run and each
rule's run used to queue separately. Every rule, the system rule included, now
checks two things before it creates a run:
- Acknowledged. Every input came from the target agent itself. An issue,
comment, reaction or attachment event whose single actor is the agent never
wakes it (task events, times, "wake now" and deadlines never count as the
agent's own). A
condition.metinput counts when the rule is the platform's or its creating run belonged to the agent, and every run that caused it is the agent's unfinished run on this issue. Causes travel with the hints: captured events carrysource_task_id, andissue_child_event.source_task_idrecordsmultica.source_task_idfor closing, leaving and restaging (adding or reopening a sub-issue satisfies nothing and is ignored). A person's condition rule still runs after the agent's own change, because the running agent does not have its instruction. The inputs are consumed with awakeup_triggeredentry (outcome=acknowledged). This is the #8849 fix: a coordinator closing its own stage is not woken again while that run is going. - Merged. The agent has a queued run on the issue that runs as the same
person the rule's own run would: the rule's creator, or for
child_donethe personchildDoneRunAsresolves from the parent. The rule then starts nothing and keeps its inputs (FindWaitingIssueRun); timers advance and aoncerule stays on, as while its own run is claimed. When a daemon that advertisesjoined-wakeups-v1claims that run,JoinWaitingWakeupsruns after every claim gate. For each waiting rule, locked withSKIP LOCKED, it checks that the rule is on, has no run of its own, targets this agent, runs as this run's originator, the creator may still use the agent, the inputs are not the agent's own and do not close a loop. It then reserves the inputs for the run (issue_wakeup_receipt.task_id, still unprocessed), adds the rule's chain to the run'swakeup_chainand appends{wakeup_id, wakeup_revision, note}to the run'scontext.wakeup_joined. The claim response carries the notes aswakeup_joined; the daemon renders them as a[WAKEUP — joined this run]block for every prompt kind. - Settling a merge. Reserved inputs stay the rule's until the run starts.
On the rule's next dispatch (
takenReceipts): inputs of a run that has started are consumed into it, count as a firing (aoncerule ends, a merge counts towardmax_firesand can pause the rule there) and write awakeup_triggeredentry (outcome=merged,task_id); inputs of a run that ended without starting go back to the rule, which then handles them like any other input; inputs of a run that has not started yet keep waiting. Turning a rule off, deleting it or changing it discards its pending inputs, reserved ones included. A later claim of the same run (after a claim that did not go through) checks every entry again and drops the ones whose rule no longer has inputs reserved for it or may no longer reach the run.
Nothing is consumed before the run starts, so a rule never loses inputs to a run that does not run them: if the waiting run is cancelled, fails before starting, or an older daemon claims it, the rule still has its inputs and starts its own run on a later tick. Joining skips the hourly run limit, which counts the rule's own runs; every join needs a run some other trigger queued, so joins alone cannot run away. The create form tells a member when a reply or comment rule would wake the issue's agent assignee, whose comment-triggered run it will join.
Conditions, runaway protection and check-ins
Conditions. condition on the create/update body is a structured predicate
the scheduler evaluates:
| type | fields | holds when |
|---|---|---|
issue_field |
field=status, value |
this issue has that status key |
issue_field |
field=assignee, assignee_type, assignee_id |
it is assigned to that member, agent or squad |
issue_field |
field=label, label_id |
it has that label |
issue_field |
field=property, property_id, value |
the property's stored value equals value |
children_done |
optional stage |
every sub-issue (or every one up to that stage) is closed |
pull_request |
event=checks_finished, merged or conflicts |
a linked PR's checks finished on its current head, it merged, or an open GitHub PR is CONFLICTING on its current head |
other_issue |
issue_id, state=done, ended, in_review or blocked |
another issue in the workspace reaches that state |
A condition rule stays kind=event. Validation derives the same-issue events
that can change the fact (for example issue.status_changed) as hints: the
existing capture triggers record them, and dispatch consumes them only to
evaluate early. The scheduler also re-evaluates every 30 seconds. A satisfied
predicate becomes one condition.met receipt carrying the observed facts; the
fingerprint of those facts is kept in condition_state, so a repeating rule
fires again only after the predicate turned false or its facts changed (a new PR
head's result, another sub-issue set). States already true at registration fire
on the first check; finished checks from before registration are ignored. The
watched issue's identifier is stored in the condition for display.
conflicts reads the GitHub API snapshot (api_mergeable) and counts only a
snapshot fetched for the PR's current head, so a verdict from before a push
never fires. Its key is pr_id@head:conflicting (PR numbers and heads repeat
across forks). GitHub reports
UNKNOWN while it recomputes mergeability; on the same head that keeps the
previous verdict, so a continuous rule is not woken twice for one conflict.
Self-hosted VCS rows store no mergeability; a rule linked only to them records
a diagnostic. For every linked-PR condition, a run starts only when some PR
key occurs more often than in condition_state (checks and merged keys carry
only the PR number, so same-numbered PRs from two repositories share one); a
PR leaving the set (resolved, closed, unlinked) only updates
condition_state, so it does not wake the agent again for the PRs that stayed. A base-branch move sends no event for the PR, so the snapshot
pipeline keeps watched PRs (linked to an issue with an enabled conflicts
rule) fresh: the 10-minute sweep re-fetches them once their snapshot is older
than 5 minutes even when decided, rotating an address cursor like the
undecided sweep so repeatedly failing fetches cannot starve the rest, and a merged pull_request webhook
re-fetches the repository's other watched open PRs after 30 seconds and 3
minutes. No migration is involved; older clients show the rule without a
condition label.
Runaway protection. Repeating event rules default to max_fires=20
(1–1000, any repeating rule may set it); the run that reaches the cap is
created and the rule is paused with paused_reason=max_fires. Each wakeup run
stores wakeup_chain, the rules that led to it; a rule whose chain already
passes through it twice (a third pass without a person in between) is paused
with loop, and an event rule that already started 12 runs in the past hour is
paused with rate. Both also set disabled_at, so queued runs of the rule are
refused at claim. A member's action carries no source run and so starts a fresh
chain; "wake now" is exempt. Turning a rule back on clears the reason and
restarts the count. GET /api/issue-wakeup-paused lists paused rules on open
issues for board cues.
Check-ins. POST /api/issues/{id}/wakeups/{wakeupID}/checkin ({ "note" },
at most 500 characters) is accepted only from the running task the rule
started, and only for every/cron rules. It stores wakeup_checkin in the
task's context and a wakeup_checkin activity; completion then skips the
synthesized fallback comment. Comment- and assignment-triggered runs are
unchanged. The [WAKEUP] block of a scheduled check gives the exact command,
and the runtime brief states the exception.
Management. POST .../trigger queues one run as if the rule fired
(creator or admin, refused while the rule is turned off or paused by a loop or
burst), DELETE .../{wakeupID} removes the rule and its pending inputs and
withdraws its unstarted runs, and GET .../runs returns the latest ten runs
with their triggers, check-in note and whether they commented.
Timeline and lists. The service writes wakeup_created,
wakeup_triggered (not for every/cron runs, which show as runs),
wakeup_timed_out, wakeup_paused and wakeup_checkin activities with a
snapshot of the rule, published after commit. The workspace list adds a
source filter and column (member, agent, system), a paused scope,
runs_7d, and the child-done system rule of each open parent still waiting on
sub-issues (a row once the rule exists; its target is the parent's assignee). The issue header shows what
the issue is waiting for and opens the Wakeups section; board cards say it in a
few words, or that a rule was paused.
Migrations 557–560 are additive (new columns, a table with its trigger, and two concurrent indexes). Deploy them before the server and the server before the clients; older clients ignore the new fields, and older servers read as "no condition, not paused" in new clients.