fix(schedule): close concurrent durability gaps
This commit is contained in:
@@ -29,7 +29,7 @@ The version-1 `schedule/change` stream is the only durable Schedule authority. A
|
||||
|
||||
The current rule accepts a non-empty prompt and exactly one positive safe-integer `after_seconds`. Its record is `{ id, kind: 'after', prompt, afterSeconds, scheduledAt }`; dispatch stores only the id because the record already fixes its occurrence. `at`, `every_seconds`, `cron`, and `time_zone` are rejected rather than hidden in unused fields. Tool values derive `scheduled` or `overdue` and always include `deliveryMode: 'session-local'`.
|
||||
|
||||
Every tool operation that reads or decides from the fold first awaits `ctx.sessions.flush(session)`. Create may reject input-shape failures before this preflight; after a successful preflight it allocates an id, appends create, and waits for a second barrier. Delete preflights before deciding whether an id is active and waits for a second barrier only when it appends. List and unknown or finished delete never answer from an unconfirmed live suffix. A failed barrier returns `persistence_uncertain` rather than guessing whether an eager write committed.
|
||||
An Agent-scoped FIFO serializes each accepted management transaction and the live owner's due transaction from preflight through any post-append barrier. Every tool operation that reads or decides from the fold first awaits `ctx.sessions.flush(session)`. Create may reject input-shape failures before entering the FIFO; after a successful preflight it allocates an id, appends create, and waits for a second barrier. Delete validates its id before the FIFO, then preflights before deciding whether the id is active and waits for a second barrier only when it appends. List and unknown or finished delete never answer from an unconfirmed live suffix or observe a dispatch before its own barrier. A failed barrier returns `persistence_uncertain` rather than guessing whether an eager write committed.
|
||||
|
||||
Every successful management preflight also asks the live owner to recompute. This closes the recovery path where create appended successfully but its post-append barrier rejected: a later list can confirm the coordinator's retained batch, return the active record, and arm its timer without a Schedule-specific retry loop.
|
||||
|
||||
@@ -41,7 +41,7 @@ The persistence coordinator supplies that acknowledgement only after its write p
|
||||
|
||||
### Live delivery lifecycle
|
||||
|
||||
The Agent-scoped owner derives its earliest target from the durable fold. Long targets use bounded timer segments, and every wake reads the wall clock again, so a rollback cannot fire early and a forward jump becomes overdue. If a turn or another maintenance task already owns the Agent, `runMaintenance()` rejects the claim; the record stays active and one `whenIdle()` wait triggers a later retry. A rejected persistence preflight also leaves the record active, but no private retry timer runs; later Agent activity reaching idle or a successful Schedule management preflight asks the owner to try again.
|
||||
The Agent-scoped owner derives its earliest target from the durable fold. Long targets use bounded timer segments, and every wake reads the wall clock again, so a rollback cannot fire early and a forward jump becomes overdue. If a turn or another maintenance task already owns the Agent, `runMaintenance()` rejects the claim; the record stays active and one `whenIdle()` wait triggers a later retry. A rejected persistence preflight or contained framing/synchronous-enqueue failure also leaves the record active, but no private retry timer runs; later Agent activity reaching idle or a successful Schedule management preflight asks the owner to try again.
|
||||
|
||||
The accepted path first clears pending persistence and claims the true idle phase through `runMaintenance()`. Inside that task it refolds the exact Session suffix so a direct management mutation that won the claim race cannot be followed by a stale dispatch, samples the decision clock once, constructs the complete fixed reminder frame with JSON-escaped id and prompt, synchronously queues one `followup()`, and appends the id-only dispatch. Waking input remains parked until maintenance settles, so the driver cannot claim the message before dispatch enters the log; only after the task releases the phase does the owner wait for the dispatch barrier. A framing or synchronous enqueue failure is contained and appends no dispatch. An append failure faults that owner because the message may already be queued. A later prompt-admission, request-checkpoint, or model failure cannot retract a dispatch.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user