Skip to content

Commit 2282aa8

Browse files
feat(trigger-schedule): a scheduled run whose due time passed while no kernel was ticking is recorded as missed, never replayed (#26485)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XSuvxsiC4BEpZzbv7B7H3Yk)_ Fixes #25235 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:托管内核停摆(驱逐、重启、崩溃)期间到点的定时流程,下一个内核绑定该流程时为每个错过的到点时间在运行历史里记一条 `missed`(开始=到点时间,结束=发现时间),不补跑。 - 为什么改:cloud#3149 第 5 项裁决(本卡裁决 6353729816 字母 A):应用今天无法得知每日清扫没跑;spec 成员 `missed` 已由 #25363 落地,这是生产者一半。 - 风险与回滚:只新增一种终态行与一次绑定时的只读检测;故意拒绝的流程(定时开关关、缺组织、调度不可读)现在会注销上一个内核留下的 job,这是唯一改变既有行为的点。回滚 = revert 本 PR。 - 先看:`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. - **Detection (`trigger-schedule`).** On a flow's first bind in a kernel, `ScheduleTrigger` reads how the previous kernel left the flow's job (`DbJobAdapter.readArming`, adapter-local like `setReplayGuard`). Only a `sys_job` row still `active` counts 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`. - **Record (`service-automation`).** `AutomationEngine.recordMissedRun` checks each due time against the dispatch ledger (`sys_flow_dispatch`). A claimed window was dispatched and is skipped. An unclaimed one is written as a `missed` terminal 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. - **Bound.** At most the run history's own per-flow cap (`maxTerminalRunsPerFlow`, read through `AutomationEngine.missedRunLimit()`, no second literal) newest due times per flow per detection. One `warn` names the flow, the count not recorded and the earliest due time dropped. - **A refusal is not a silence.** A flow refused on purpose (scheduled work off, missing acting organization, unreadable schedule) now has its job cancelled by its deterministic name even when this kernel never bound it (`ScheduleTrigger.release`, an optional `FlowTrigger.release` the 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. - **The three run-row vocabulary sites widen together**, as the `refused` precedent did (ADR-0078): `TERMINAL_RUN_STATUSES` (which the store's row gate reads), `sys_automation_run.status` options and the retention `onlyWhen` set. `plugin-approvals` `RUN_STATUS_LIVENESS` already carries `missed: 'terminal'` on `main`. ## Why Ruling `6353729816` (letter A) on cloud#3149 item 5: record only, no replay. The spec member `missed` landed in #25363; this is the producer. Basis for `Clause-②: yes (widening)`, measured on the diff (the claim line said `no` and asked for the measurement): the diff adds public surface to published packages: `AutomationEngine.recordMissedRun` and `missedRunLimit`, the optional `FlowTrigger.release`, `SuspendedRunStore.maxTerminalRunsPerFlow`, `DbJobAdapter.readArming`, `ScheduleTrigger.release`, and the barrel types `MissedScheduledRun`, `MissedRunOutcome`, `JobArming`, `JobArmingReading`, `MissedScheduledRunInput`; and the stored `sys_automation_run.status` column 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 one `missed` entry 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's `release`), `suspended-run-store.test.ts` (a `missed` row 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 - The work was resumed from `claude/project-thread-rtuv45@3830dd8` by cherry-picking its four services commits onto `main` (the local spec commit and its revert skipped); they applied cleanly. This branch adds a docblock fix (the `recordMissedRun` docblock sat above `missedRunLimit`), a note on which way a late arming read can fail, and the barrel export of the two structural types. - Ordering of the arming read and the row rewrite: the read is issued before `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 a `missed` entry and never invents one. Making `schedule()` wait for the read was tried and dropped: a `stop()` in between would be overtaken by a late re-arm. - Observation, not filed: with two replicas ticking at once, a replica that binds in the few milliseconds between a due time and the ticking replica's ledger claim would record that due time as `missed` while the other replica runs it. Single-kernel hosted environments, the case cloud#3149 describes, cannot hit it. - The run-history reader on the console side: `ui/packages/app-shell/src/views/metadata-admin/previews/FlowRunsPanel.tsx` maps five statuses and falls back to the raw status string for the rest, as it already does for `timed_out` and `refused`; `missed` renders as the label `missed` with no duration (none is recorded). No `ui/` edit in this PR. - Not ruled and flagged by the ruling for the maintainer (item 5 g): whether `missed` rows 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 merging `origin/main` at `68901b0383`; base of the card work `08ed7e295f`). <details><summary>Red first: the new pins against main's sources</summary> Production sources restored to `08ed7e295f` (tree only), this branch's tests, `service-automation` rebuilt at that base: `missed-run-record.test.ts` + `suspended-run-store.test.ts` 11 failed / 57 passed; `db-job-adapter.test.ts` 4 failed / 20 passed; `missed-run-two-kernels.test.ts` 5 failed (the 12 passing are `missed-due-times.test.ts`, a new pure module that exists on both sides). Restored by `git checkout HEAD`, `git diff HEAD` empty. </details> <details><summary>Suites on the final head</summary> - `@objectstack/trigger-schedule` 191/191 · `@objectstack/service-job` 121/121 · `@objectstack/service-automation` 2719/2719 · `@objectstack/plugin-approvals` 1290/1290. - `typecheck` for the three touched packages: exit 0. `eslint --no-inline-config` over their `src/`: exit 0, no output. - `test:repo` projects for core (83), objectql (5), types (11), spec (1293): all pass. </details> <details><summary>Ablations, one per pinned behaviour, each landed on disk (anchor 1 to 0) and restored to the HEAD blob</summary> 1. Engine ignores a claimed window: 1 red (`a claimed window was dispatched`). 2. `missed` dropped from `TERMINAL_RUN_STATUSES`: 4 red (engine, two-replica, vocabulary, store row). 3. `readArming` reads every row as active: 1 red (`cancel() disarms; eviction leaves the row armed`). 4. `ScheduleTrigger.release` never cancels an unbound job: 1 red (the OFF kernel between two ON kernels). 5. Trigger ignores the `active` bit: 2 red (deliberately unbound flow, OFF kernel). 6. Detection writes 5 past the cap: 1 red (the long-silence cap case). 7. `missedRunLimit` answers 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. 8. The policy gate no longer calls `release` (dist rebuilt, preflight as above): 2 red in the engine unit suite, 1 red in the two-kernel suite. 9. The missed row's id is no longer per (flow, due time): 2 red. Three first attempts did not count and were rerun: ablations 4 to 6 first ran against a `service-automation` dist 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. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `1bff9c7e9f`: 63 commands. All 63 ran on this head and every one exited 0; `dispatch-gates --ran` reconciles 63 run, 0 NOT MEASURED. A first round at `92f11484f3` had `check:dual-build-cjs-loads` answer 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 on `main`. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5ofGbFRago9bvAetNb)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c199064 commit 2282aa8

15 files changed

Lines changed: 1381 additions & 13 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/trigger-schedule': patch
3+
'@objectstack/service-automation': patch
4+
'@objectstack/service-job': patch
5+
---
6+
7+
feat(trigger-schedule): a scheduled run whose due time passed while no kernel was ticking is recorded as `missed` in the flow's run history, and is not run
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
A hosted kernel that is stopped, evicted or restarting does not tick, and a scheduled flow due in that gap used to leave no trace: nothing ran and nothing said so, so an app could not tell a sweep had not happened. Now the next kernel to bind the flow records each such due time once, as a run-history entry with status `missed`:
13+
14+
- **What the entry says.** `startedAt` is the due time that passed and `completedAt` the instant the next kernel detected it. No step ran, so `steps` is empty; `trigger.type` is `schedule`. The entry is listed by `listRuns` (and `GET /api/v1/automation/:name/runs`), `?status=missed` selects it, and it is durable history in `sys_automation_run`, aged out like every other terminal run.
15+
- **Record only.** Nothing is run for a missed due time. It is never replayed or caught up; the flow's next due time runs as usual.
16+
- **What counts as a silence.** The flow's job was still armed when its kernel stopped (its `sys_job` row still `active`: a kernel that stops ticking cancels nothing, while disabling, republishing or withdrawing a flow does), and a due time of the schedule that kernel armed falls after its last fire and before the next kernel bound the flow. A kernel that ticked through records nothing, and a flow that was deliberately unbound records nothing for the time it was unbound.
17+
- **A refusal is not a silence.** A kernel that refuses the flow on purpose (scheduled work switched off for it, a missing acting organization, an unreadable schedule) now disarms the job by its name even when it never bound it, so the next kernel to run the flow does not read that kernel's life as missed due times. `FlowTrigger` gains an optional `release(flowName)`, which the engine calls when its scheduled-work policy refuses a flow before the trigger is reached.
18+
- **Never a run that happened.** A due time whose tick window is claimed in the dispatch ledger (`sys_flow_dispatch`) was dispatched, landed or died in flight, and is not recorded. With no persisted ledger the check is skipped, and a ledger read that fails records nothing for that due time; the trigger says either at `warn`, naming the flow.
19+
- **Bounded by the run history's own cap.** At most the run history's per-flow cap (`maxTerminalRunsPerFlow`, 100 by default) of the newest due times are recorded per flow per detection, since the store keeps no more terminal rows than that per flow. One `warn` line names the flow, how many earlier due times were not recorded and the earliest of them.
20+
- **Two replicas, one row.** The entry's id is derived from the flow and the due time, so replicas that detect the same silence write one history row.
21+
22+
`DbJobAdapter` gains `readArming(name)` (adapter-local, like `setReplayGuard`; not part of `IJobService`), which reads how a job's `sys_job` row was left. `AutomationEngine` gains `recordMissedRun(...)` and `missedRunLimit()`, the run stores expose their configured `maxTerminalRunsPerFlow`, and `TERMINAL_RUN_STATUSES` and the `sys_automation_run.status` options gain `missed`.

‎packages/services/service-automation/src/engine.ts‎

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,19 @@ export interface FlowTrigger {
602602
readonly type: string;
603603
start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;
604604
stop(flowName: string): void;
605+
/**
606+
* [#25235] Optional. Called when the engine refuses to arm `flowName` ON
607+
* PURPOSE before ever reaching {@link start} — the scheduled-work policy is
608+
* off — so a trigger that keeps arming state outside this process can
609+
* disarm what an earlier kernel left armed, whether or not this one bound
610+
* it. {@link stop} cannot: it tears down only this process's own binding.
611+
*
612+
* The schedule trigger is the one that needs it: its `sys_job` row still
613+
* `active` is how the next kernel tells "the kernel stopped ticking" from
614+
* "the flow was refused", and a refusal that left the row armed would read
615+
* as a silence and record every due time of it as `missed`.
616+
*/
617+
release?(flowName: string): void;
605618
}
606619

607620
// ─── Connector Registry (Plugin Extension Point) ────────────────────
@@ -1244,6 +1257,16 @@ export function buildRunTrigger(
12441257
};
12451258
}
12461259

1260+
/**
1261+
* The run id of a `missed` entry ({@link AutomationEngine.recordMissedRun}):
1262+
* one per (flow, due time), the same on every replica, so a silence two
1263+
* replicas both detect is one history row.
1264+
*/
1265+
export function missedRunId(flowName: string, dueAt: string): string {
1266+
const ms = Date.parse(dueAt);
1267+
return `run_missed_${flowName}_${Number.isFinite(ms) ? ms : dueAt}`;
1268+
}
1269+
12471270
/**
12481271
* Internal sentinel thrown by {@link AutomationEngine.executeNode} when a node
12491272
* signals `suspend`. It unwinds the synchronous DAG recursion up to
@@ -1638,8 +1661,14 @@ export interface SuspendedRun {
16381661
* sites widen together, in this change. ⛔ A refusal is NOT a failure: nothing
16391662
* threw, the flow evaluated successfully and said no, and the authored reason
16401663
* rides beside the status as `refusalMessage` rather than in `error`.
1664+
*
1665+
* [#25235] `missed` joins them on the same terms, in the same change as its one
1666+
* producer ({@link AutomationEngine.recordMissedRun}): a scheduled due time
1667+
* that passed while the kernel was not ticking, recorded by the next kernel.
1668+
* It is a terminal that never started — nothing ran and nothing will — so it
1669+
* is history, aged out like the rest, and ⛔ never a failure either.
16411670
*/
1642-
export const TERMINAL_RUN_STATUSES = ['completed', 'failed', 'cancelled', 'timed_out', 'refused'] as const;
1671+
export const TERMINAL_RUN_STATUSES = ['completed', 'failed', 'cancelled', 'timed_out', 'refused', 'missed'] as const;
16431672

16441673
/** One member of {@link TERMINAL_RUN_STATUSES}. */
16451674
export type TerminalRunStatus = (typeof TERMINAL_RUN_STATUSES)[number];
@@ -2166,6 +2195,13 @@ export interface SuspendedRunStore {
21662195
* so "open a past failed run" works after a restart.
21672196
*/
21682197
loadTerminal?(runId: string): Promise<RunRecord | null>;
2198+
/**
2199+
* [#25235] The per-flow cap on terminal history rows this store enforces
2200+
* at write time, as configured; `0` when the cap is disabled. Optional — a
2201+
* store that does not say is read as declaring none. Read by
2202+
* {@link AutomationEngine.missedRunLimit}.
2203+
*/
2204+
readonly maxTerminalRunsPerFlow?: number;
21692205
}
21702206

21712207
/** What a claimed dispatch turned into (#14501). */
@@ -2192,6 +2228,33 @@ export interface FlowDispatchClaim {
21922228
settledAt: string | null;
21932229
}
21942230

2231+
/**
2232+
* One scheduled due time that passed while the kernel was not ticking, as the
2233+
* schedule trigger hands it to {@link AutomationEngine.recordMissedRun}.
2234+
*/
2235+
export interface MissedScheduledRun {
2236+
readonly flowName: string;
2237+
/** The due time that passed (ISO-8601). */
2238+
readonly dueAt: string;
2239+
/** When the next kernel found it undelivered (ISO-8601). */
2240+
readonly detectedAt: string;
2241+
/**
2242+
* The `(flow, tick-window)` dispatch key a fire at {@link dueAt} would
2243+
* have claimed — the trigger computes it, the engine never learns how.
2244+
*/
2245+
readonly dispatchKey: string;
2246+
/** The acting organization the flow declares, when it declares one. */
2247+
readonly organization?: string | null;
2248+
}
2249+
2250+
/**
2251+
* What {@link AutomationEngine.recordMissedRun} did with one due time:
2252+
* `recorded` — a `missed` entry is in the run history; `dispatched` — the
2253+
* ledger holds a claim for its window, so it was not missed; `unverifiable` —
2254+
* no ledger reading could be made, so nothing was recorded.
2255+
*/
2256+
export type MissedRunOutcome = 'recorded' | 'dispatched' | 'unverifiable';
2257+
21952258
/**
21962259
* Persisted claim ledger for trigger dispatch idempotency (#10220), extended
21972260
* by #14501 to record what each claim turned into.
@@ -2982,6 +3045,88 @@ export class AutomationEngine implements IAutomationService {
29823045
return readDispatch.call(host, key);
29833046
}
29843047

3048+
/**
3049+
* [#25235] How many missed due times one detection may record for one
3050+
* flow, or `null` when it may record none.
3051+
*
3052+
* `null` without a persisted dispatch ledger: "no claim for this window"
3053+
* is then not evidence that nothing ran, so the detection is skipped
3054+
* rather than inferred (the trigger says so, naming the flow).
3055+
*
3056+
* Otherwise the run history's own per-flow cap
3057+
* ({@link SuspendedRunStore.maxTerminalRunsPerFlow}): the store keeps only
3058+
* that many newest terminal rows per flow and deletes the rest in the same
3059+
* write, so recording more would be writing rows it removes at once. A
3060+
* store that declares no cap (or `0`, cap disabled) leaves the engine's
3061+
* own run buffer (`maxLogSize`) as the history bound, and that is read
3062+
* instead — never a number of this method's own.
3063+
*/
3064+
missedRunLimit(): number | null {
3065+
const ledger = this.flowDispatchStore;
3066+
if (!ledger || typeof ledger.read !== 'function') return null;
3067+
const cap = this.store?.maxTerminalRunsPerFlow;
3068+
return typeof cap === 'number' && cap > 0 ? cap : this.maxLogSize;
3069+
}
3070+
3071+
/**
3072+
* Record one scheduled due time that passed while the kernel was not
3073+
* ticking as a `missed` entry in the flow's run history — and run nothing
3074+
* for it. Record only, no replay: the ruling this implements is
3075+
* objectstack-ai/cloud#3149 item 5, filed here as #25235.
3076+
*
3077+
* The schedule trigger decides WHICH due times passed in the silence (it
3078+
* owns the schedule and the window math); this method decides whether one
3079+
* of them was really missed, against the dispatch ledger: a window whose
3080+
* key is claimed was dispatched — a fire that landed, or one that died in
3081+
* flight, which the replay door owns — and is not recorded.
3082+
*
3083+
* ⛔ It records nothing it cannot verify. With no persisted ledger, or a
3084+
* ledger read that throws, "unclaimed" would be a guess, and a `missed`
3085+
* entry for a window that actually ran is a false statement in the one
3086+
* place an operator goes to learn what ran. The trigger reports the
3087+
* `unverifiable` count instead.
3088+
*
3089+
* The entry's id is derived from the flow and the due time, so two
3090+
* replicas finding the same silence write one history row (the store's
3091+
* terminal write is an upsert by id). On the entry `startedAt` is the due
3092+
* time and `completedAt` the detection instant — `ExecutionStatus`
3093+
* documents that reading of the two for `missed`; no step ran, so `steps`
3094+
* is empty.
3095+
*/
3096+
async recordMissedRun(missed: MissedScheduledRun): Promise<MissedRunOutcome> {
3097+
const store = this.flowDispatchStore;
3098+
if (!store || typeof store.read !== 'function') return 'unverifiable';
3099+
try {
3100+
if (await store.read(missed.dispatchKey)) return 'dispatched';
3101+
} catch (err) {
3102+
this.logger.warn(
3103+
`[automation] missed-run check for flow '${missed.flowName}' due ${missed.dueAt} could not read ` +
3104+
`the dispatch ledger — NOT recording it as missed, since an unread claim may be a run that ` +
3105+
`landed. The store failure is in this record's meta.`,
3106+
describeThrownForLog(err),
3107+
);
3108+
return 'unverifiable';
3109+
}
3110+
const context: AutomationContext = {
3111+
event: 'schedule',
3112+
...(missed.organization ? { tenantId: missed.organization } : {}),
3113+
};
3114+
this.recordLog(
3115+
{
3116+
id: missedRunId(missed.flowName, missed.dueAt),
3117+
flowName: missed.flowName,
3118+
flowVersion: this.flows.get(missed.flowName)?.version,
3119+
status: 'missed',
3120+
startedAt: missed.dueAt,
3121+
completedAt: missed.detectedAt,
3122+
trigger: buildRunTrigger(context),
3123+
steps: [],
3124+
},
3125+
context,
3126+
);
3127+
return 'recorded';
3128+
}
3129+
29853130
/**
29863131
* Generate a process-unique run id. Includes a random component so ids do
29873132
* not collide with runs persisted by a previous process lifetime (a plain
@@ -3547,6 +3692,22 @@ export class AutomationEngine implements IAutomationService {
35473692
`Flow '${flowName}' is not armed on trigger '${resolved.triggerType}' — ${reason}`,
35483693
);
35493694
}
3695+
// [#25235] A refusal on purpose disarms what a previous kernel left
3696+
// armed — see {@link FlowTrigger.release}. On every pass, not once:
3697+
// the trigger may register after the flow, and a cancel by name of
3698+
// an already-disarmed job changes nothing.
3699+
const refusing = this.triggers.get(resolved.triggerType);
3700+
if (refusing && typeof refusing.release === 'function') {
3701+
try {
3702+
refusing.release(flowName);
3703+
} catch (err) {
3704+
this.logger.warn(
3705+
`Flow '${flowName}' is not armed on trigger '${resolved.triggerType}', and disarming what ` +
3706+
`an earlier start left armed for it failed. The failure is in this record's meta.`,
3707+
describeThrownForLog(err),
3708+
);
3709+
}
3710+
}
35503711
return;
35513712
}
35523713
// Past the gate the refusal no longer describes this flow: either it

‎packages/services/service-automation/src/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ export type {
8585
// [#22507] What `AutomationEngine.renderFlowTextSlot` takes — the slot a
8686
// builtin executor hands the engine's one translated-template pick.
8787
FlowTextSlotTranslation,
88+
// [#25235] What `AutomationEngine.recordMissedRun` takes and answers —
89+
// barrel-reachable through that method.
90+
MissedScheduledRun,
91+
MissedRunOutcome,
8892
} from './engine.js';
8993

9094
// [#11997] ADR-0005 overlay precedence for same-named flow definitions. The boot

0 commit comments

Comments
 (0)