Uh oh!
There was an error while loading. Please reload this page.
fix(trigger-schedule,service-automation): persisted sys_flow_dispatch ledger makes time-relative sweeps idempotent per matched window - #10316
Conversation
…dger makes time-relative sweeps idempotent per matched window Fixes#10220 per the 2026-08-20 maintainer ruling (Q1=A, Q2=A): - new platform object sys_flow_dispatch owned by @objectstack/service-automation (ADR-0057 telemetry retention, 30d), registered alongside sys_automation_run; one registry line in PLATFORM_OBJECTS_BY_PACKAGE (maintainer-authorized). - AutomationEngine.claim(key): check-and-record against the persisted ledger; in-process fallback with a one-time degradation warning when no ledger is available; a ledger ERROR falls back per-key so a store outage never blocks a dispatch (availability over strict-once). - TimeRelativeTrigger claims a key derived from the MATCHED WINDOW's identity before launching: offset mode (flowName, recordId, windowDay, offset); range mode (flowName, recordId, sweepDay, rangeSpec) — the documented withinDays semantic (fires every day in range) stays true, but never twice in one day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ice it uses (find/insert) FlowDispatchStoreEngine narrows the store's dependency to the keyed read + insert claim() actually makes — the ledger never updates or deletes (the platform Reaper owns deletion via declared retention) — and the test double now REFUSES unsupported where shapes instead of silently matching everything. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 37 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 115 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5c4bae7003f3f8d9437ee41ea50317bcefb871e1 && git checkout 5c4bae7003f3f8d9437ee41ea50317bcefb871e1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin be5918dad2ac1b69e56af02fe2963823562950e9 cc49bead06251f0858108230082930892fc68dc3 && git checkout -B drift-repro be5918dad2ac1b69e56af02fe2963823562950e9 && git merge --no-ff cc49bead06251f0858108230082930892fc68dc3
node scripts/docs-audit/affected-docs.mjs --json be5918dad2ac1b69e56af02fe2963823562950e9
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10220
What
Time-relative sweeps re-dispatched the same record on every tick (measured: 15 duplicate reminders in ~70s on a 5s interval; a kernel rebuild re-dispatched the day's window even under a daily cron). This PR gives the sweep a persisted memory, per the maintainer ruling recorded 2026-08-20 on the issue (Q1 = A, Q2 = A):
@objectstack/service-automation— new platform objectsys_flow_dispatch: a dispatch-claim ledger with ADR-0057 telemetry retention (maxAge: '30d', per the ruling; comfortably >= any near-term catch-up horizon for objectstack-ai/cloud#1288 — this ledger is what unblocks its catch-up sweeps). Registered with the manifest alongsidesys_automation_run; exposed to triggers asAutomationEngine.claim(key: string): Promise<boolean>on the automation service surface.claim()is check-and-record: the key is the row's primary id, so a concurrent duplicate insert fails on the id and the loser re-reads and returnsfalseinstead of erroring.@objectstack/trigger-schedule— the sweep computes a dispatch key from the matched window's identity and claims it before launching. Offset mode keys on(flowName, recordId, windowDay, offset)— editing the date field moves the window day, so the flow legitimately re-fires for the new window. Range mode keys on(flowName, recordId, sweepDay, rangeSpec)— the spec prose forwithinDays("fires every day the record stays in range") stays true: a new day is a new key, but never twice in one day. One derivation (computeWindowClaimScopes) produces both the query window and the key, so the matching rule and the dedup key cannot drift. The trigger resolves the claim surface structurally from theautomationservice it already resolves; it never learns the table name.@objectstack/spec— one registry line:sys_flow_dispatchunderservice-automationinPLATFORM_OBJECTS_BY_PACKAGE(the maintainer-authorized exception; the conformance test enforces it).Trade-off: availability over strict-once (stated per issue requirement 4)
A claim-store error never blocks the dispatch. On a ledger failure the engine logs the cause and falls back to the in-process check for that key — the dispatch proceeds unless this process already made it, so a store outage degrades to at-least-once instead of silently swallowing reminders. Symmetrically, when no persisted ledger can exist at all (no ObjectQL engine / object not registered / automation service without
claim()), dedup degrades to in-process only and says so once at warn — a silent fallback would hide a permanently weakened guarantee (kernel rebuilds can re-dispatch until the ledger is available).Tests
false, genuine store failure propagates to the engine's fallback;Verification (at the reported head): dependency-closure build green;
@objectstack/trigger-schedule57/57 and typecheck green;@objectstack/service-automation998/998 green (the package has notypecheckscript; a directtsc --noEmitshows 3 pre-existing private-access errors in the untouchednested-region-parity.test.ts, unrelated to this diff); specplatform-object-namesconformance 7/7 green; dispatch-gates battery green except two pre-existing macOS-only self-test harness failures (check-adr-0087-registration --self-test,objectui-changeset-digest --self-test) reproduced identically on the pristine checkout at a different HEAD — already filed as #10303, and #10086 names the underlying symlink-entry-guard class.Generated by Claude Code