Skip to content

fix(trigger-schedule,service-automation): persisted sys_flow_dispatch ledger makes time-relative sweeps idempotent per matched window - #10316

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10220-flow-dispatch-ledger
Aug 20, 2026
Merged

fix(trigger-schedule,service-automation): persisted sys_flow_dispatch ledger makes time-relative sweeps idempotent per matched window#10316
os-zhuang merged 2 commits into
mainfrom
claude/issue-10220-flow-dispatch-ledger

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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 object sys_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 alongside sys_automation_run; exposed to triggers as AutomationEngine.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 returns false instead 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 for withinDays ("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 the automation service it already resolves; it never learns the table name.
  • @objectstack/spec — one registry line: sys_flow_dispatch under service-automation in PLATFORM_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

  • two sweeps over the same window → dispatched once (offset and range modes);
  • simulated kernel rebuild (fresh trigger/engine instances over the same surviving ledger) → still once;
  • range mode: twice in one day → once; next day → fires again (spec prose preserved);
  • offset mode: a dateField edit that moves the window → fires again for the new window;
  • claim-store failure → dispatch proceeds (and is logged); no claim surface → in-process dedup + one-time degradation warning;
  • store semantics: check-and-record, insert-race loser reads as false, genuine store failure propagates to the engine's fallback;
  • records without an id are dispatched unconditionally (never claimable — unchanged pre-existing behaviour).

Verification (at the reported head): dependency-closure build green; @objectstack/trigger-schedule 57/57 and typecheck green; @objectstack/service-automation 998/998 green (the package has no typecheck script; a direct tsc --noEmit shows 3 pre-existing private-access errors in the untouched nested-region-parity.test.ts, unrelated to this diff); spec platform-object-names conformance 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

os-zhuangand others added 2 commits August 20, 2026 23:29
…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/service-automation, @objectstack/spec, @objectstack/trigger-schedule, touching 36 documentable anchor(s).

37 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json be5918dad2ac1b69e56af02fe2963823562950e9.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/services/service-automation/src/index.ts, packages/triggers/trigger-schedule/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: isSystem (5 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 34 pages)
  • 14 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 115 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json be5918dad2ac1b69e56af02fe2963823562950e9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5c4bae7003f3f8d9437ee41ea50317bcefb871e1 — the merge of head cc49bead06251f0858108230082930892fc68dc3 into base be5918dad2ac1b69e56af02fe2963823562950e9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs be5918dad2ac1b69e56af02fe2963823562950e9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling protocol:system labels Aug 20, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 20, 2026 16:07
@os-zhuang
os-zhuang added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 73d9795Aug 20, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10220-flow-dispatch-ledger branch August 20, 2026 17:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

time-relative sweep 非幂等:每次扫描对同一记录重复铸行(5s interval 下 70 秒 15 条重复提醒),需要落库幂等键

1 participant

@os-zhuang