Repository navigation
feat(trigger-schedule): a scheduled run whose due time passed while no kernel was ticking is recorded as missed, never replayed - #26485
Conversation
…while no kernel was ticking as missed (wip) Claude-Session: https://claude.ai/code/session_01Q4SPCWwEtTZJXVsgRzJgcz Co-authored-by: Claude <noreply@anthropic.com>
…e job adapter (wip) Claude-Session: https://claude.ai/code/session_01Q4SPCWwEtTZJXVsgRzJgcz Co-authored-by: Claude <noreply@anthropic.com>
…; a refusal disarms the job (wip) Per ruling 6353729816 on #25235: - per flow per detection, at most the store's maxTerminalRunsPerFlow newest due times are recorded; one warn names the flow, the count not recorded and the earliest due time dropped (AutomationEngine.missedRunLimit); - with no persisted dispatch ledger the check is skipped with one warn naming the flow; - a flow refused on purpose (scheduled work off, missing organization, unreadable schedule) is released: its job is cancelled by name whether or not this process bound it (optional FlowTrigger.release, called by the engine's policy gate), so a refusal never reads as a silence; - the status description drops runtime vocabulary. Claude-Session: https://claude.ai/code/session_01Q4SPCWwEtTZJXVsgRzJgcz Co-authored-by: Claude <noreply@anthropic.com>
…cess suite, not a nightly-tier file The `.e2e.test.ts` suffix put it in the nightly tier, which trigger-schedule's vitest config does not partition (check:tier-file-adoption). It runs in milliseconds, so it stays in the package's own suite under a plain name. Claude-Session: https://claude.ai/code/session_01Q4SPCWwEtTZJXVsgRzJgcz Co-authored-by: Claude <noreply@anthropic.com>
…lock sits on recordMissedRun; say which way a late arming read can fail Claude-Session: https://claude.ai/code/session_01FEkQ5ofGbFRago9bvAetNb Co-authored-by: Claude <noreply@anthropic.com>
…ce and ledger surfaces now name Claude-Session: https://claude.ai/code/session_01FEkQ5ofGbFRago9bvAetNb Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 7 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 2054eb0a6e884a0824a559f9e2f190c92ddaa023 && git checkout 2054eb0a6e884a0824a559f9e2f190c92ddaa023
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d0a7e748a933a7fb549c38c3105c1c49f5935dec 1bff9c7e9fc201d2593133e1c14ac19cf185b704 && git checkout -B drift-repro d0a7e748a933a7fb549c38c3105c1c49f5935dec && git merge --no-ff 1bff9c7e9fc201d2593133e1c14ac19cf185b704
node scripts/docs-audit/affected-docs.mjs --json d0a7e748a933a7fb549c38c3105c1c49f5935dec
|
Contract reviewServed-tier: Inputs read: card #25235 (body and all 11 comments: claim ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged against the ruling (
② Semver levelChangeset
③ Boundary flags
Dev flags (
Reviewer's own flags, none blocking:
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #25235, three patch changesets (trigger-schedule, service-automation, service-job). 15 files, +1381/−13. Not governed (0 of 15 paths), under the human-merge size threshold. |
| Fix | Ruling 6353729816 letter A, as ordered: each scheduled due time that passed while no kernel was ticking gets one missed run-history entry (due time, detection time). Nothing is replayed. The id comes from (flow, due time). The bound is the store's own maxTerminalRunsPerFlow, read through missedRunLimit() with no second literal. Detection is skipped with a warn when there is no persisted dispatch ledger. Item 6's fix is in: a refusal on purpose cancels the job by its name through release(), so a policy-OFF kernel leaves no false gap. |
| Resume | Commits 432f4dc8be, ddd0a34e33, 819e61a1f0 and 3830dd8c71 were cherry-picked from rtuv45 onto fresh main; the spec commit and its revert were skipped. 0 paths under packages/spec, packages/platform-objects or packages/runtime. The spec member is on main (execution.zod.ts:53, #25363). |
| Vocabulary | The three stored-vocabulary sites widen together with the producer, the refused precedent (#15788): TERMINAL_RUN_STATUSES, the sys_automation_run.status options, and the retention onlyWhen set. plugin-approvals RUN_STATUS_LIVENESS already carries missed on main. |
| Declarations | The dev measured Clause-②: yes (widening) on the diff, against the order's guess of no: new public methods, optional interface members and barrel types on three published packages. This matches the ruling's own item 4, and the contract review agrees. Metadata-protocol: none, so patch. |
| Evidence | CI on head 1bff9c7e9f is green: 36 checks succeeded, 11 were expected skips, and none failed or are pending; mergeable clean. Red first: 20 new pins fail with main's sources. 9 ablations each turned their pins red. Suites on the final head: trigger-schedule 191, service-job 121, service-automation 2719, plugin-approvals 1290. Derived gates: 63 of 63 exit 0, with 0 NOT-MEASURED. |
| Item 8 (the reader) | objectui is now ui/ in this repo, and .objectui-sha no longer exists, so the seat read ui/ on origin/main. FlowRunsPanel.tsx statusMeta() falls back to the raw status string for any member its map lacks, as timed_out and refused already do, so missed shows as its label. FlowRunStatus in the studio and Problems panel is the flow's arming state, not a run status. The map is not closed, so no domain:ui card is owed: this is the acceptance-notes line. |
| Carried | Ruling item 5(g), whether missed rows take a tighter bound than the store's cap, stays flagged for the maintainer and does not block. If one is set, missedRunLimit() is the one place it is read. Review notes, non-blocking: the durable id reads run_run_missed_… (cosmetic), and two replicas binding inside the sub-second claim window can record a due time that the other replica runs. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #25235
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
missed(开始=到点时间,结束=发现时间),不补跑。missed已由 feat(spec): ExecutionStatus gains missed, a scheduled due time that passed while no kernel was ticking #25363 落地,这是生产者一半。packages/triggers/trigger-schedule/src/schedule-trigger.ts(recordSilence与release)、packages/services/service-automation/src/engine.ts(recordMissedRun、策略闸门调release)、packages/services/service-job/src/db-job-adapter.ts(readArming)。pnpm --filter @objectstack/trigger-schedule exec vitest run src/missed-run-two-kernels.test.ts,预期 8 个用例全绿(停一次记一条、停三次记三条、中间夹一个关闭开关的内核记零条、跑通的内核记零条)。What changes
A scheduled flow whose due time passed while no kernel was ticking now gets one run-history entry with status
missed(due time, detected time), written by the next kernel that binds the flow. Nothing is run for it.trigger-schedule). On a flow's first bind in a kernel,ScheduleTriggerreads how the previous kernel left the flow's job (DbJobAdapter.readArming, adapter-local likesetReplayGuard). Only asys_jobrow stillactivecounts as a silence: a kernel that stops ticking cancels nothing, while a deliberate unbind cancels. The due times of the schedule that kernel armed, strictly after its last-alive instant and up to now, are computed in a new sibling module,missed-due-times.ts.service-automation).AutomationEngine.recordMissedRunchecks each due time against the dispatch ledger (sys_flow_dispatch). A claimed window was dispatched and is skipped. An unclaimed one is written as amissedterminal row with an id derived from (flow, due time), so two replicas write one row. With no persisted ledger, or a ledger read that throws, nothing is recorded and the trigger warns, naming the flow.maxTerminalRunsPerFlow, read throughAutomationEngine.missedRunLimit(), no second literal) newest due times per flow per detection. Onewarnnames the flow, the count not recorded and the earliest due time dropped.ScheduleTrigger.release, an optionalFlowTrigger.releasethe engine's scheduled-work policy gate calls, since that gate returns before the trigger is reached). Without this, a later kernel would read the refusing kernel's whole life as missed due times.refusedprecedent did (ADR-0078):TERMINAL_RUN_STATUSES(which the store's row gate reads),sys_automation_run.statusoptions and the retentiononlyWhenset.plugin-approvalsRUN_STATUS_LIVENESSalready carriesmissed: 'terminal'onmain.Why
Ruling
6353729816(letter A) on cloud#3149 item 5: record only, no replay. The spec membermissedlanded in #25363; this is the producer.Basis for
Clause-②: yes (widening), measured on the diff (the claim line saidnoand asked for the measurement): the diff adds public surface to published packages:AutomationEngine.recordMissedRunandmissedRunLimit, the optionalFlowTrigger.release,SuspendedRunStore.maxTerminalRunsPerFlow,DbJobAdapter.readArming,ScheduleTrigger.release, and the barrel typesMissedScheduledRun,MissedRunOutcome,JobArming,JobArmingReading,MissedScheduledRunInput; and the storedsys_automation_run.statuscolumn accepts one more option. Nothing accepted before is refused.Metadata-protocol: none: nothing a metadata author writes changes.Tests
packages/triggers/trigger-schedule/src/missed-run-two-kernels.test.ts(in-process, the test-budget shape): a kernel stopped across one due time records onemissedentry and runs nothing; an evicted kernel across three due times records three; a kernel that ticked through records none; a due time whose fire was claimed before the kernel died records none; a flow deliberately unbound before the silence records none; an OFF kernel between two ON kernels records none; a long silence records only the cap's newest and warns; no persisted ledger skips with one warning.missed-due-times.test.ts(cron, interval, once; the cap and the dropped count),service-automation/src/missed-run-record.test.ts(ledger verdicts, deterministic id, the three vocabulary sites,missedRunLimit, the policy gate'srelease),suspended-run-store.test.ts(amissedrow is stored and listed as history),service-job/src/db-job-adapter.test.ts(readArming: absent, every schedule type, fire moves last-alive, cancel disarms, eviction leaves it armed, a failed read rejects).Acceptance notes
claude/project-thread-rtuv45@3830dd8c71by cherry-picking its four services commits ontomain(the local spec commit and its revert skipped); they applied cleanly. This branch adds a docblock fix (therecordMissedRundocblock sat abovemissedRunLimit), a note on which way a late arming read can fail, and the barrel export of the two structural types.schedule(). If a pooled driver still answered it after the rewrite, the row would read as touched just now and no silence would be found, so the failure direction loses amissedentry and never invents one. Makingschedule()wait for the read was tried and dropped: astop()in between would be overtaken by a late re-arm.missedwhile the other replica runs it. Single-kernel hosted environments, the case cloud#3149 describes, cannot hit it.ui/packages/app-shell/src/views/metadata-admin/previews/FlowRunsPanel.tsxmaps five statuses and falls back to the raw status string for the rest, as it already does fortimed_outandrefused;missedrenders as the labelmissedwith no duration (none is recorded). Noui/edit in this PR.missedrows should be bounded tighter than the store's per-flow cap so real history survives a long outage. This PR uses the store's cap, as ruled.Verification
Head
1bff9c7e9f(after mergingorigin/mainat68901b0383; base of the card work08ed7e295f).Red first: the new pins against main's sources
Production sources restored to
08ed7e295f(tree only), this branch's tests,service-automationrebuilt at that base:missed-run-record.test.ts+suspended-run-store.test.ts11 failed / 57 passed;db-job-adapter.test.ts4 failed / 20 passed;missed-run-two-kernels.test.ts5 failed (the 12 passing aremissed-due-times.test.ts, a new pure module that exists on both sides). Restored bygit checkout HEAD,git diff HEADempty.Suites on the final head
@objectstack/trigger-schedule191/191 ·@objectstack/service-job121/121 ·@objectstack/service-automation2719/2719 ·@objectstack/plugin-approvals1290/1290.typecheckfor the three touched packages: exit 0.eslint --no-inline-configover theirsrc/: exit 0, no output.test:repoprojects for core (83), objectql (5), types (11), spec (1293): all pass.Ablations, one per pinned behaviour, each landed on disk (anchor 1 to 0) and restored to the HEAD blob
a claimed window was dispatched).misseddropped fromTERMINAL_RUN_STATUSES: 4 red (engine, two-replica, vocabulary, store row).readArmingreads every row as active: 1 red (cancel() disarms; eviction leaves the row armed).ScheduleTrigger.releasenever cancels an unbound job: 1 red (the OFF kernel between two ON kernels).activebit: 2 red (deliberately unbound flow, OFF kernel).missedRunLimitanswers a number with no ledger (dist rebuilt, preflight marker present, then absent after restore): 1 red in each of the engine unit and the two-kernel suites.release(dist rebuilt, preflight as above): 2 red in the engine unit suite, 1 red in the two-kernel suite.Three first attempts did not count and were rerun: ablations 4 to 6 first ran against a
service-automationdist left at the red-first base (rebuilt, then rerun); ablation 8's first marker was elided by the build (preflight failed), rerun with a marker that survives; ablation 6 first used a bound so large the cases timed out, rerun with cap + 5.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat1bff9c7e9f: 63 commands. All 63 ran on this head and every one exited 0;dispatch-gates --ranreconciles 63 run, 0 NOT MEASURED. A first round at92f11484f3hadcheck:dual-build-cjs-loadsanswer PREREQUISITE NOT MET (exit 3, no full build in the worktree); on this head it ran and passed (112 entry points, 69 packages). Outside the derived list and left to CI: the workspace type-check lanes, Build Core, and the dogfood and temporal batteries that run only onmain.Generated by Claude Code