Durable timers and event-based waits
Timers, waits and subscriptions implement B04’s timer, wait and continuation rules in the production runtime. Timers, scheduler polling and waits/subscriptions use public storage contracts. A host supplies a trusted clock and drives bounded operations; these packages do not start a daemon or a sleeping background loop. In the daemon, the host loop in internal/bootstrap calls Scheduler.Poll and ReconcileSubscriptions on every pass.
Timers
Section titled “Timers”NewTimers(store, now, options) supports trusted Arm/ArmRecurring, lease-fenced ArmDerived/ArmDerivedRecurring, revisioned cancellation, Fire and bounded Due. Only explicitly configured independent schedules or operator actions create roots. Executor-created timers derive their parent from current attempt authority, including resumed continuation ancestry. Fixed-duration recurrence preserves its arming parent and uses a distinct stable delivery identity for each occurrence.
FireTimerInTx joins scoped acceptance, causal charging, firing history and timer deletion/replacement in one transaction. Repeating a committed firing returns its original result; a conflicting replacement cannot rewrite it. Revision tombstones protect a cancelled/recreated timer from stale firing requests. Causal refusal commits a redacted diagnostic and disables the firing/recurrence without publishing an Event. A persisted round-robin cursor prevents one overdue recurrence or invalid timer from monopolizing Due(limit) at a fixed clock.
Calendar, cron and catch-up policy belong to the schedule extension; core supplies calendar recurrence as a hook. TimerOptions.Recurrences registers pure RecurrenceFunc policies by kind, and Timers.ArmRecurrence stores a configured root timer with an opaque spec. Inside the firing transaction core asks the policy for the occurrence (or none: a skip) and the replacement. It validates both: the occurrence keeps the timer’s source and parent, and the replacement keeps the ID, fires after now and has a distinct identity. Acceptance, the firing ledger (Skipped for a skip) and the rearm commit atomically. A substituted or skipped firing replaces or deletes the timer without accepting its template. A replay returns the original result, and an unregistered kind is refused with an invalid_timer diagnostic that does not starve other timers. The schedule extension’s policy implements a six-hour catch-up and clock-change rules (scheduled jobs).
Polling and execution deadlines
Section titled “Polling and execution deadlines”Scheduler.Poll(ctx, timers, limit) drives bounded passes over due timers, accepted pending retry/deadline Events, active Waits and expired execution leases. Each pass advances a durable fair cursor. Recoveries return handles for the host to observe; polling does not block in RunHandle.Wait or launch a replacement for an expired lease. Retry Events survive a crash between firing and policy processing. Independent item failures produce safe diagnostics while storage/context failures remain explicit.
Active execution deadlines have durable core.execution.timeout templates. Deadline processing validates the current active segment, recovers expired authority on the same attempt when necessary, then applies failure under its current fence. Stale segment deadlines cannot adopt a parked attempt or affect a resumed segment. Timeout control checkpoints retain Work/Execution in the immutable envelope while using an empty routed Work association, so earlier runnable input cannot delay authority fencing. If causal acceptance refuses the deadline Event, core still fences the execution, records the refusal and holds automatic retry with BlockedReason=causal_rejection; no new root or runnable branch is invented. Physical resource exclusion still requires the trusted stop evidence described in execution scheduling.
Scheduler.Wait(ctx, lease, wait, resume) atomically registers an owned-key/type predicate, stores its exclusive observation watermark, releases authority and makes Work waiting. It scans accepted history in the same transaction, including already routed Events. A watermark cannot exceed accepted history or move behind the attempt’s acknowledged watermark. Timeout identity is retained separately from the live Timer, so an already accepted timeout remains recognizable after timer deletion. An Event the Work’s own executions published (it carries the Work’s ID and an execution ID: the host’s turn-ended Event, an Event the attempt emitted) never matches the Work’s own Wait, whatever its type; the same Event wakes any other Work waiting on its key. Wake selects the lowest accepted sequence among matching input and that timeout. At the deadline an already accepted normal match wins if the timeout has not yet been accepted.
Wake commits one historical winner, ordered context and queue association, timeout cancellation, subscription deactivation and ready state together. Earlier ordinary unrouted input becomes durable waiting context before the winner’s checkpoint; it cannot cause a missed wake. Input that doesn’t wake the Work reaches it as context only when a route would pass it to the Work: the router asks the scope’s routes as it would for Work that isn’t waiting, and input none of them matches is recorded as unmatched, unless it matches the wait (a wake the causal limits blocked), so a route’s skip holds while its Work waits. A route that errs keeps the input as context. Already checkpointed input retains its original route history. Nonmatching input never bypasses a wait. Replaying a match, loser, timeout or wake scan cannot create another continuation. Cancellation preserves the winning history if wake happened first, or prevents all runnable wake activity if cancellation happened first.
These are trusted host coordination APIs. Final wait registration asserts that the executor has cooperatively suspended or finished the prior active segment. It releases scheduling capacity. A toolbelt wait request alone cannot prove that a process stopped or suspended: the toolbelt records the request as a declaration, and the scheduler registers the wait only when the host observes the executor finishing or suspending the segment through Complete (toolbelt). WaitWithResult joins the structured waiting outcome, usage and artifact references to registration; references remain in public execution history for backup/retention. A later host acknowledgement may enrich a still-parked record without reviving authority, but cannot rewrite a terminal waiting outcome or add new terminal artifact references. Old-epoch acknowledgements fail after Resume. Active execution timeout budget pauses while waiting and resumes with the remaining budget, so a multi-day wait consumes no active runtime and repeated parking cannot reset the full budget.
Resume and continuation
Section titled “Resume and continuation”Resume-capable execution retains its immutable original Execution.TriggerID, resumes the same attempt at a higher epoch, and receives the accepted winner as RunRequest.Trigger. Other execution uses attempt N+1 with that winner. RunRequest.Inputs supplies the ordered accumulated batch, while durable attempt input IDs reproduce it on recovery. Consumption history retains each attempt association even when historical observed input later becomes a wake winner. Request trigger and causal continuation authority remain distinct.
A stable derived core.wait.woken bridge carries resumed authority. It records the actual winner and Wait in its payload, so the audit link from the wake to the attempt survives whatever the parent is. A wake is charged to the Event that woke it: the winner supplies the bridge’s parent. A winner from an independent external root (a check finishing, a person’s comment) therefore starts a shallow chain, and an attempt can park and wake any number of times on outside input without its depth growing; the per-minute budget of each root still bounds bursts. A winner that the attempt’s own output caused descends from its trigger, so a loop with no outside input (execution → Event → wake → execution) still deepens and stops at causal_max_depth. The wait’s own timeout, which the attempt scheduled, wakes from the attempt’s trigger, one step deeper per timed-out round. Emit, child creation, retry/handoff and executor-created timers use the bridge as their trusted causal trigger. The bridge has an internal nonrunnable checkpoint with Work/Execution preserved in the envelope; it never becomes another queued executor input.
A refused bridge records a diagnostic, disables the wait’s subscriptions and timeout and blocks the Wait, and it holds the Work (blockWaitInTx). The operator hold carries the reason (Its wait is blocked: the causal limits refused its wake: its chain would reach depth 33, over the limit of 32 (depth_limit); release the hold to wake it on what has arrived) and a root core.wait.blocked Event that names the wait, the winner and the reason. So work get and the web view show the Work as held, with that reason, instead of waiting on a wait that can never fire. Releasing the hold (ReleaseWork) unblocks the wait: it rearms the subscriptions and an unfired timeout, and arbitrates the wait again with the release, the operator’s outside input, as the parent of the next wake (unblockWaitsInTx). Cancellation remains available. There is no separate cap on how many rounds one attempt may wait.
Subscriptions
Section titled “Subscriptions”Subscribe and ReconcileSubscriptions provide durable dependency aliases without transferring correlation ownership. Subscription namespace plus ID identifies a registration; source key/types, dependent-owned key/Wait and the accepted-history checkpoint persist together. Registration backscans existing observations. Alias delivery identity includes subscription namespace/ID and original Event ID; its minimal payload contains lineage rather than the provider body. Alias acceptance, checkpoint advancement and any wake effects are atomic. Refused alias branches are diagnosed and checkpointed to prevent a tight retry loop. Woken/cancelled subscriptions emit no new activity.
Cross-scope aliases require an explicit AuthorizeAlias callback from trusted composition. It executes outside storage; source/target ancestor snapshots are revalidated before publication. Authorization is checked again on reconciliation, and a durable cursor prevents an idle subscription from starving others. The daemon’s callback answers from the current configuration’s B05 read ceiling; a callback cannot grant correlation ownership or write authority in the source scope. Provider registration/read/reconcile gap closure and domain translation remain adapter responsibilities.
Translated waits (engineering tools, waits and playbooks) carry their dependency subscriptions in the toolbelt declaration and persist them as pending in the wait-registration transaction. ReconcileSubscriptions installs them through Subscribe (best effort right after Complete), so a crash before installation only delays it. SubscriptionRequest adds After (the source scan start; zero scans all history), an opaque Filter evaluated by SchedulerOptions.AliasFilter, and a Label. The alias payload carries subscription, event, source_type and label.
Evidence
Section titled “Evidence”The wait runtime tests adapt all nineteen shared runtime scenarios to production components over persistent/reopened SQLite, with deterministic clocks and synchronization. Timer runtime tests exercise recurrence, causal refusal, revision replay, restart and atomic fire/rearm process boundaries. The recurrence contract test covers the core side of calendar recurrence. These are local correctness/durability proofs and make no production throughput claim.
History
Section titled “History”- Ticket 02-05 connected the production runtime to B04’s timer, wait and continuation rules. Calendar, cron and catch-up policy were left to the schedule extension in stage 05. The executor handshake that turns a toolbelt request into exclusion evidence was assigned to 02-06/02-07. Real shell suspension, multi-day service observation, production throughput and the daemon restart gate were left downstream; the daemon core gate covers the restart drills.
- Ticket 03-04 added translated waits: dependency subscriptions carried in the toolbelt declaration,
After,Filter,Labeland the extended alias payload. - Ticket 05-01 added calendar recurrence (
TimerOptions.Recurrences,Timers.ArmRecurrence) and the recurrence contract test.