@intx/hub-sessions 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +23 -7
  3. package/dist/agent-repo.js +19 -6
  4. package/dist/agent-state-kind.js +18 -63
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +19 -97
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +52 -7
  10. package/dist/credential-push.js +178 -22
  11. package/dist/event-collector-registry.d.ts +3 -2
  12. package/dist/event-collector-registry.js +42 -13
  13. package/dist/event-collector.d.ts +12 -2
  14. package/dist/event-collector.js +45 -4
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +541 -86
  17. package/dist/hub-session-orchestrator.d.ts +2 -3
  18. package/dist/hub-session-orchestrator.js +22 -56
  19. package/dist/index.d.ts +19 -9
  20. package/dist/index.js +16 -7
  21. package/dist/reconciliation-scheduler.d.ts +14 -0
  22. package/dist/reconciliation-scheduler.js +55 -0
  23. package/dist/repo-store/index.d.ts +2 -1
  24. package/dist/repo-store/index.js +1 -0
  25. package/dist/repo-store/store.d.ts +1 -1
  26. package/dist/repo-store/store.js +138 -1
  27. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  28. package/dist/repo-store/subscribe-kind.js +42 -77
  29. package/dist/repo-store/types.d.ts +94 -6
  30. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  31. package/dist/repo-store/user-principal-gate.js +78 -0
  32. package/dist/session-service.d.ts +252 -126
  33. package/dist/session-service.js +851 -624
  34. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  35. package/dist/sidecar-allocation/capability-policy.js +124 -0
  36. package/dist/sidecar-allocation/contracts.d.ts +101 -0
  37. package/dist/sidecar-allocation/contracts.js +26 -0
  38. package/dist/sidecar-allocation/index.d.ts +5 -0
  39. package/dist/sidecar-allocation/index.js +4 -0
  40. package/dist/sidecar-allocation/operation.d.ts +10 -0
  41. package/dist/sidecar-allocation/operation.js +54 -0
  42. package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
  43. package/dist/sidecar-allocation/plugin-registry.js +61 -0
  44. package/dist/sidecar-allocation/reconciler.d.ts +54 -0
  45. package/dist/sidecar-allocation/reconciler.js +825 -0
  46. package/dist/skill-kind.js +12 -62
  47. package/dist/substrate.d.ts +3 -3
  48. package/dist/substrate.js +1 -1
  49. package/dist/workflow-allocation-service.d.ts +64 -0
  50. package/dist/workflow-allocation-service.js +554 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +42 -0
  56. package/dist/workflow-dispatch-service.js +209 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +29 -1
  60. package/dist/workflow-kind.js +143 -139
  61. package/dist/workflow-probe-gate.d.ts +286 -0
  62. package/dist/workflow-probe-gate.js +382 -0
  63. package/dist/workflow-run-kind.d.ts +239 -32
  64. package/dist/workflow-run-kind.js +874 -188
  65. package/dist/workflow-run-reader.d.ts +1 -1
  66. package/dist/workflow-run-reader.js +3 -7
  67. package/dist/workflow-run-restore.d.ts +16 -0
  68. package/dist/workflow-run-restore.js +30 -0
  69. package/dist/workflow-source-closure.d.ts +35 -0
  70. package/dist/workflow-source-closure.js +342 -0
  71. package/dist/workflow-source-pins.d.ts +8 -0
  72. package/dist/workflow-source-pins.js +14 -0
  73. package/dist/ws/index.d.ts +3 -3
  74. package/dist/ws/index.js +2 -2
  75. package/dist/ws/pending-tracker.d.ts +93 -0
  76. package/dist/ws/pending-tracker.js +132 -0
  77. package/dist/ws/sidecar-events.d.ts +139 -37
  78. package/dist/ws/sidecar-events.js +2 -2
  79. package/dist/ws/sidecar-handler.d.ts +218 -60
  80. package/dist/ws/sidecar-handler.js +1713 -732
  81. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  82. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  83. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  84. package/dist/ws/sidecar-token-authenticator.js +78 -7
  85. package/package.json +14 -13
  86. package/dist/available-skills-stanza.d.ts +0 -21
  87. package/dist/available-skills-stanza.js +0 -32
@@ -1,4 +1,4 @@
1
- import { type AuthorizeFn, type KindHandler, type Principal, type RepoId, type RepoStore } from "./repo-store/index.js";
1
+ import { type AuthorizeFn, type CommittedReads, type KindHandler, type Principal, type RepoId, type RepoStore } from "./repo-store/index.js";
2
2
  export type WorkflowRunHubPrincipal = {
3
3
  readonly kind: "hub";
4
4
  };
@@ -8,23 +8,38 @@ export type WorkflowRunSidecarPrincipal = {
8
8
  };
9
9
  export type WorkflowRunWorkflowProcessPrincipal = {
10
10
  readonly kind: "workflow-process";
11
- readonly deploymentId: string;
11
+ readonly anchorRunId: string;
12
12
  readonly runId?: string;
13
13
  };
14
14
  export type WorkflowRunSupervisorPrincipal = {
15
15
  readonly kind: "supervisor";
16
- readonly deploymentId: string;
16
+ readonly anchorRunId: string;
17
17
  };
18
18
  export type WorkflowRunPrincipal = WorkflowRunHubPrincipal | WorkflowRunSidecarPrincipal | WorkflowRunWorkflowProcessPrincipal | WorkflowRunSupervisorPrincipal;
19
19
  export declare const WORKFLOW_RUN_GITIGNORE_PATH = ".gitignore";
20
20
  export declare const WORKFLOW_RUN_RUNS_PREFIX = "runs";
21
21
  export declare const WORKFLOW_RUN_EVENTS_DIR = "events";
22
22
  export declare const WORKFLOW_RUN_BLOBS_DIR = "blobs";
23
+ export declare const WORKFLOW_RUN_GRANTS_FILE = "grants.json";
23
24
  export declare const WORKFLOW_RUN_ADDRESSES_PREFIX = "addresses";
24
25
  export declare const WORKFLOW_RUN_CONTROL_PREFIX = "control";
25
26
  export declare const WORKFLOW_RUN_INBOX_DIR = "inbox";
26
27
  export declare const WORKFLOW_RUN_PROCESSING_DIR = "processing";
27
28
  export declare const WORKFLOW_RUN_CONSUMED_DIR = "consumed";
29
+ /**
30
+ * Per-run inbound mail-part subtree. Non-text inbound mail content
31
+ * (image/audio/video/document mail parts) is committed here as real
32
+ * files rather than inlined into the JSON event log, whose serialization
33
+ * boundary would corrupt binary bytes. The layout is
34
+ * `runs/<runId>/parts/<urlEncoded(messageId)>/<index>-<name>`: one
35
+ * directory per inbound message (so a long-lived run's successive turns
36
+ * never collide), and one file per mail part carrying its verbatim
37
+ * bytes. The workflow-host ingest writes the bytes and records a
38
+ * lightweight `{ name, contentType, ref }` reference into the run's
39
+ * trigger / signal payload; the step invoker reads the bytes back at
40
+ * `agent.send` time. Files are immutable once written, like `blobs/`.
41
+ */
42
+ export declare const WORKFLOW_RUN_PARTS_DIR = "parts";
28
43
  /**
29
44
  * Filename of the per-address retention watermark blob, a direct child
30
45
  * of `addresses/<urlEncoded>/` (a file, not a directory). Carries the
@@ -42,15 +57,28 @@ export declare const WORKFLOW_RUN_WATERMARK_FILE = "watermark.json";
42
57
  * deduped by a retained consumed entry, short enough that `consumed/`
43
58
  * reaches a bounded steady state of one day's message volume.
44
59
  *
45
- * INVARIANT (operator-owned): the horizon must be >= the longest
46
- * window in which the same `messageId` could legitimately be
47
- * re-submitted and still must be caught as a duplicate. There is no
48
- * automatic internal mail redelivery in the system today, so this is
49
- * the external re-submission window. If an at-least-once redelivery
50
- * source is ever added, the horizon must be >= its maximum redelivery
51
- * window or dedup breaks; a breach surfaces LOUDLY (a too-late
52
- * re-submission carrying an old `receivedAt` is refused at enqueue,
53
- * not silently reprocessed) rather than as silent double-processing.
60
+ * INVARIANT (operator-owned): the horizon must be >= the longest window in
61
+ * which the same `messageId` could legitimately be re-submitted and still must
62
+ * be caught as a duplicate. The hub now redelivers un-acked inbound mail
63
+ * (connected-window retry and reconnect-redelivery), so an at-least-once
64
+ * internal source DOES exist -- but the dedup guarantee against it does not
65
+ * rest on window arithmetic. It rests on a STRUCTURAL fact: `enqueueInbox` is
66
+ * only ever called with a freshly stamped `receivedAt` (a redelivery
67
+ * re-enters `onMailMessage` and re-stamps `Date.now()`, never carrying the
68
+ * original), and the watermark only ever advances to at most
69
+ * `consumedAt - retentionHorizonMs`, which is <= now, so a fresh `receivedAt`
70
+ * always sits a full horizon above the watermark and can never be stale-
71
+ * refused. A redelivery instead hits the `consumed/`/`processing/`/`inbox/`
72
+ * dedup index and is deduped there. The sole path that carries an original
73
+ * (old) `receivedAt` back into the queue is `replayProcessingToInbox`, which
74
+ * writes straight to `inbox/` and bypasses the stale gate entirely. So
75
+ * `claim_check_stale_enqueue` is unreachable via redelivery today. If any
76
+ * redelivery source is ever changed to carry the ORIGINAL `receivedAt` into
77
+ * `enqueueInbox`, stale becomes reachable, the horizon must then be >= that
78
+ * source's maximum redelivery window, and `StaleInboxEnqueueError`'s
79
+ * withhold-not-ack handling becomes load-bearing; a breach surfaces LOUDLY (an
80
+ * old-`receivedAt` re-submission is refused at enqueue) rather than as silent
81
+ * double-processing.
54
82
  */
55
83
  export declare const DEFAULT_CONSUMED_RETENTION_MS: number;
56
84
  /**
@@ -68,6 +96,69 @@ export declare const DEFAULT_CONSUMED_RETENTION_MS: number;
68
96
  * `<agentKey>` directory layer below the prefix).
69
97
  */
70
98
  export declare const WORKFLOW_RUN_AGENT_STATE_PREFIX = "agent-state";
99
+ /**
100
+ * Conversational-mailbox subtree for the warm single-step agent. The
101
+ * substrate mailbox backing commits the agent's durable inbox under
102
+ * `mailbox/INBOX/` so the full message history replicates to the hub
103
+ * alongside the run state. The layout is:
104
+ *
105
+ * - `mailbox/INBOX/index.json` — the mailbox index. MUTABLE: the
106
+ * backing rewrites it on every flush, so it is exempt from the
107
+ * retained-blob byte-equality walk the `<uid>.eml` blobs are subject
108
+ * to. It must nonetheless PERSIST once it existed: dropping it drops
109
+ * the whole mailbox and resets uidValidity/uidNext on the next open.
110
+ * - `mailbox/INBOX/<uid>.eml` — one message per file, carrying the
111
+ * raw signed message bytes. `<uid>` is a decimal integer >= 1. A
112
+ * RETAINED `<uid>.eml` (present in both prior and prospective) is
113
+ * opaque and IMMUTABLE: it must reappear byte-identically, exactly
114
+ * like `runs/<runId>/blobs/`. A prior `<uid>.eml` may be ABSENT from
115
+ * the prospective tree — that is the warm agent expunging a message.
116
+ * The raw bytes are not lost: they stay reachable through the parent
117
+ * commit, and a `workflow-run` repo's objects are never GC'd (its
118
+ * kind is off the GC allow-list), so an expunged message survives in
119
+ * history for the life of the run repo. The audit trail rests on
120
+ * "these objects are never pruned", not on the live tree being
121
+ * monotonic.
122
+ *
123
+ * The only entries permitted under `mailbox/` are the `INBOX/`
124
+ * directory; the only entries permitted under `mailbox/INBOX/` are
125
+ * `index.json` and `<uid>.eml` message files. Anything else fails the
126
+ * push.
127
+ */
128
+ export declare const WORKFLOW_RUN_MAILBOX_PREFIX = "mailbox";
129
+ export declare const WORKFLOW_RUN_MAILBOX_INBOX_DIR = "INBOX";
130
+ export declare const WORKFLOW_RUN_MAILBOX_INDEX_FILE = "index.json";
131
+ /**
132
+ * Parse the seq from a per-event log filename `<seq>.json` under
133
+ * `runs/<runId>/events/`. Returns the non-negative integer seq, or
134
+ * `null` when the name is not a legal per-event filename. This is the
135
+ * one place the filename shape is defined; every reader of the event log
136
+ * narrows names through it rather than re-encoding the regex. Callers
137
+ * decide what an illegal name means -- a foreign entry to skip, or a
138
+ * substrate-invariant violation to surface -- since `validatePush` is
139
+ * the authority that keeps illegal names from landing in the first place.
140
+ */
141
+ export declare function parseEventSeq(filename: string): number | null;
142
+ /**
143
+ * Narrow a per-event filename to its seq, throwing when it is illegal.
144
+ * A reader that enumerates the committed event log to act on its entries
145
+ * uses this rather than `parseEventSeq`: `validatePush` is the authority
146
+ * that keeps an illegal name from ever landing under
147
+ * `runs/<runId>/events/`, so a name that reaches a reader is corruption,
148
+ * and silently skipping it would drop an event from processing. `context`
149
+ * is the repo-root-relative blob path, surfaced in the error so the
150
+ * offending entry is identifiable.
151
+ */
152
+ export declare function requireEventSeq(filename: string, context: string): number;
153
+ /**
154
+ * Maximum byte length of a single mail part path component (the
155
+ * URL-encoded message segment, and each `<index>-<name>` filename).
156
+ * messageIds and mail part names arrive from untrusted inbound mail;
157
+ * an over-long RFC 5322 message-id URL-encodes past the filesystem's
158
+ * 255-byte component limit and would otherwise fail at disk-write time,
159
+ * downstream of validation. Reject it at the boundary instead.
160
+ */
161
+ export declare const MAX_MAIL_PART_PATH_COMPONENT_BYTES = 255;
71
162
  /**
72
163
  * JSON envelope carried by inbox and processing entries. Keys:
73
164
  * - `messageId`: dedup key for the inbound message.
@@ -115,6 +206,10 @@ declare const ConsumedEnvelope: import("arktype/internal/variants/object.ts").Ob
115
206
  store: string;
116
207
  path: string;
117
208
  };
209
+ rejection?: {
210
+ code: string;
211
+ message: string;
212
+ };
118
213
  }, {}>;
119
214
  /**
120
215
  * JSON envelope carried by the per-address `watermark.json` blob. The
@@ -129,6 +224,35 @@ declare const WatermarkEnvelope: import("arktype/internal/variants/object.ts").O
129
224
  export type ClaimCheckEnvelope = typeof ClaimCheckEnvelope.infer;
130
225
  export type ConsumedEnvelope = typeof ConsumedEnvelope.infer;
131
226
  export type WatermarkEnvelope = typeof WatermarkEnvelope.infer;
227
+ /**
228
+ * Terminal event discriminators mapped to the `workflow_run.status` value
229
+ * each settles the run into. A run whose log contains an entry with one of
230
+ * these `type` values must not receive any event with a strictly greater
231
+ * seq.
232
+ *
233
+ * This map is a hand-rolled copy of the runtime's terminal-run vocabulary
234
+ * (`isTerminalRunPhase` in `@intx/workflow` state-machine `state.ts`,
235
+ * re-exported from the state-machine index and consumed by `transition.ts`),
236
+ * duplicated here because `@intx/hub-sessions` must not depend on
237
+ * `@intx/workflow`. It is the sole authority for that vocabulary and MUST
238
+ * stay in sync with the canonical runtime definition:
239
+ * if the runtime adds or removes a terminal run phase, update this map too.
240
+ * Drift silently reopens the restore-time double-driver collision that
241
+ * `scanRunsForBoot` (below) exists to prevent.
242
+ */
243
+ type TerminalRunStatus = "completed" | "failed" | "cancelled";
244
+ /**
245
+ * Classify a workflow-run event type against the terminal-status vocabulary.
246
+ * `TERMINAL_EVENT_STATUS` is the sole authority (see above), so a type absent
247
+ * from it is by definition not terminal: no separate membership set is
248
+ * consulted and no "unmapped terminal type" case can arise.
249
+ */
250
+ export declare function classifyTerminalEvent(eventType: string): {
251
+ terminal: true;
252
+ status: TerminalRunStatus;
253
+ } | {
254
+ terminal: false;
255
+ };
132
256
  export declare const workflowRunKindHandler: KindHandler;
133
257
  export declare const workflowRunAuthorize: AuthorizeFn;
134
258
  export type EnqueueInboxArgs = {
@@ -153,6 +277,54 @@ export type EnqueueInboxResult = {
153
277
  inboxKey: string;
154
278
  envelope: ClaimCheckEnvelope;
155
279
  };
280
+ /**
281
+ * Which already-present state an `enqueueInbox` call found the messageId
282
+ * in. Every value is POSITIVE evidence the message's bytes are durably on
283
+ * disk (inbox/processing) or were already consumed -- so a caller gating a
284
+ * receipt on the enqueue may safely acknowledge on any of them.
285
+ */
286
+ export type EnqueueAlreadyPresentReason = "duplicate" | "already_inbox" | "processing" | "consumed";
287
+ /**
288
+ * Outcome of an `enqueueInbox` call. Modeled as a value (not an exception)
289
+ * precisely because the return/throw boundary is the ack/withhold boundary
290
+ * for a caller gating a durable-receipt ack: a returned outcome is safe to
291
+ * acknowledge (the bytes are on disk -- freshly written or already present),
292
+ * a throw is not (the write could not complete or its disposition cannot be
293
+ * decided). `enqueued` is the only outcome that added a new inbox entry, so
294
+ * it is the only one a dispatch-driving caller wakes its loop on.
295
+ */
296
+ export type EnqueueInboxOutcome = ({
297
+ outcome: "enqueued";
298
+ } & EnqueueInboxResult) | {
299
+ outcome: "already-present";
300
+ reason: EnqueueAlreadyPresentReason;
301
+ };
302
+ /**
303
+ * Thrown by `enqueueInbox` when the inbound's `receivedAt` is strictly below
304
+ * the address's retention watermark. This is refusal under UNCERTAINTY, not
305
+ * proof of prior receipt: the consumed dedup entry that would rule out a
306
+ * duplicate may have been pruned, so the substrate can no longer tell a
307
+ * duplicate from a never-processed message and refuses rather than risk
308
+ * reprocessing. A caller gating a durable-receipt ack MUST NOT acknowledge on
309
+ * this -- acking an "I cannot tell" would terminally drop a message that was
310
+ * never written. It is its own type (not a generic Error) so that a caller,
311
+ * and monitoring, can surface it as a distinct loud signal rather than
312
+ * blending it into ordinary I/O-failure noise.
313
+ *
314
+ * Structurally unreachable on the mail-inbound path today: `enqueueInbox` is
315
+ * only ever called with a freshly stamped `receivedAt` (a redelivery
316
+ * re-stamps `Date.now()` rather than carrying the original), and the watermark
317
+ * only ever advances to at most `consumedAt - retentionHorizonMs <= now`, so a
318
+ * fresh `receivedAt` sits a full horizon above it. The sole path that carries
319
+ * an original (old) `receivedAt` back into the queue is
320
+ * `replayProcessingToInbox`, which writes straight to `inbox/` and bypasses
321
+ * this gate entirely. If any redelivery source is ever changed to carry the
322
+ * original `receivedAt` into `enqueueInbox`, this becomes reachable and its
323
+ * withhold-not-ack handling becomes load-bearing.
324
+ */
325
+ export declare class StaleInboxEnqueueError extends Error {
326
+ constructor(message: string);
327
+ }
156
328
  /**
157
329
  * Append a new inbox entry for `address`. The merge callback reads
158
330
  * the address subtree under the per-repo lock, augments the inbox
@@ -166,7 +338,7 @@ export type EnqueueInboxResult = {
166
338
  * invariant here also catches the concurrent-enqueue race that the
167
339
  * per-repo lock alone cannot surface.
168
340
  */
169
- export declare function enqueueInbox(store: RepoStore, principal: Principal, repoId: RepoId, args: EnqueueInboxArgs): Promise<EnqueueInboxResult>;
341
+ export declare function enqueueInbox(store: RepoStore, principal: Principal, repoId: RepoId, args: EnqueueInboxArgs): Promise<EnqueueInboxOutcome>;
170
342
  export type DequeueToProcessingResult = {
171
343
  commitSha: string;
172
344
  key: string;
@@ -208,7 +380,7 @@ export type ReadProcessingEntryResult = {
208
380
  * committed -- which is exactly when the supervisor forwards
209
381
  * `trigger.fired` -- observes the processing entry. Reading the working tree (rather than walking the
210
382
  * committed git tree) matches the workflow-process child's sibling
211
- * reads of `workflow.json` and `runs/<runId>/events/`. Because the
383
+ * read of `runs/<runId>/events/`. Because the
212
384
  * read issues no commit it cannot race the supervisor's `markConsumed`
213
385
  * write; it returns a point-in-time snapshot of the directory.
214
386
  */
@@ -218,6 +390,16 @@ export type MarkConsumedArgs = {
218
390
  messageId: string;
219
391
  runId: string;
220
392
  consumedAt: number;
393
+ /**
394
+ * Present when the supervisor deliberately refused the message instead of
395
+ * delivering it to the run. The consumed entry remains the durable dedup
396
+ * record, while Hub projection uses this detail to fail (rather than settle)
397
+ * an allocation-dispatch row.
398
+ */
399
+ rejection?: {
400
+ code: string;
401
+ message: string;
402
+ };
221
403
  /**
222
404
  * Retention horizon for the consumed dedup index, in milliseconds.
223
405
  * The commit advances the per-address watermark to
@@ -263,27 +445,52 @@ export type ReplayProcessingToInboxResult = {
263
445
  commitSha: string;
264
446
  replayedKeys: string[];
265
447
  };
448
+ export type ScanRunsForBootResult = {
449
+ ownedMessageIds: Set<string>;
450
+ pendingSealRunIds: string[];
451
+ };
452
+ /**
453
+ * Walk `runs/` once and return the two boot-recovery inputs the supervisor's
454
+ * spawn needs, from a single traversal of the working tree via `getRepoDir`:
455
+ *
456
+ * - `ownedMessageIds`: the `consumedMessageId`s of NON-terminal runs -- the
457
+ * messages a live run still owns. Spawn feeds this into
458
+ * `replayProcessingToInbox`'s `ownedMessageIds` so a parked run's message is
459
+ * not re-admitted to inbox and dispatched a second time while the run is
460
+ * recovered by re-driving its durable log. Without this, the re-drive AND the
461
+ * re-triggered fresh run both re-park the same awaitSignal gate on the same
462
+ * runId, and the two concurrent runtime bodies race to a corrupt terminal.
463
+ * - `pendingSealRunIds`: runs that are terminal but still in per-event form --
464
+ * an interrupted fold left them unsealed. Spawn hands these to the recovery
465
+ * sweep, which re-runs the idempotent fold. A terminal event is a *proposal*:
466
+ * the authoritative decision is `compactRunEvents`, which independently
467
+ * re-checks the run's max-seq event and no-ops a run that is not actually
468
+ * terminal, so this scan may be loose.
469
+ *
470
+ * The working tree tracks the run-event ref (`refs/heads/main`); the
471
+ * claim-check ref (`refs/heads/events`) cannot see it, which is why this lives
472
+ * at the caller rather than inside `replayProcessingToInbox`'s single-ref
473
+ * delta. A run whose log is sealed (combined `events.jsonl`, only permitted for
474
+ * a terminated run) contributes to neither set; an absent `runs/` directory
475
+ * yields empty results.
476
+ */
477
+ export declare function scanRunsForBoot(store: RepoStore, repoId: RepoId): Promise<ScanRunsForBootResult>;
478
+ export type WorkflowRunLifecycle = "absent" | "live" | "terminal";
479
+ /** Read one run's lifecycle from a committed workflow-run tree. */
480
+ export declare function readCommittedWorkflowRunLifecycle(reads: CommittedReads | null, runId: string): Promise<WorkflowRunLifecycle>;
266
481
  /**
267
- * Read the run event logs under `runs/` and return the set of
268
- * `consumedMessageId`s belonging to NON-terminal runs -- the messages a
269
- * live run still owns. The caller (the supervisor's spawn-time replay)
270
- * feeds this into `replayProcessingToInbox`'s `ownedMessageIds` so a
271
- * parked run's message is not re-admitted to inbox and dispatched a
272
- * second time while the run is recovered by re-driving its durable log.
273
- * Without this, the re-drive AND the re-triggered fresh run both re-park
274
- * the same awaitSignal gate on the same runId, and the two concurrent
275
- * runtime bodies race to a corrupt terminal.
482
+ * Read the durable lifecycle of one run from the workflow-run working tree.
483
+ * `grants.json` alone is still an absent run: grants are staged before the
484
+ * first trigger, while the first event is the durable proof that the run was
485
+ * fired. A sealed event log is terminal by the kind handler's compaction
486
+ * invariant.
276
487
  *
277
- * Reads the substrate's working tree via `getRepoDir`, mirroring the
278
- * child's `discoverInFlightRuns`. The working tree tracks the run-event
279
- * ref (`refs/heads/main`); the claim-check ref (`refs/heads/events`)
280
- * cannot see it, which is why this lives at the caller rather than inside
281
- * `replayProcessingToInbox`'s single-ref delta. A run whose log is sealed
282
- * (combined `events.json`, only permitted for a terminated run) or
283
- * carries a terminal event is excluded; an absent `runs/` directory
284
- * yields an empty set.
488
+ * The supervisor uses this when in-memory cohort membership is empty. That
489
+ * happens both for a genuinely new deployment and briefly during recovery,
490
+ * so treating both states as "fire" would start a second driver for a live
491
+ * log or reuse a terminal run.
285
492
  */
286
- export declare function readOwnedMessageIds(store: RepoStore, repoId: RepoId): Promise<Set<string>>;
493
+ export declare function readWorkflowRunLifecycle(store: RepoStore, repoId: RepoId, runId: string): Promise<WorkflowRunLifecycle>;
287
494
  export type ReplayProcessingToInboxOpts = {
288
495
  /**
289
496
  * MessageIds whose run is still LIVE (non-terminal) and therefore owns