@intx/workflow-host 0.3.0 → 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 (78) hide show
  1. package/README.md +21 -4
  2. package/dist/adapters/mail-part-store.d.ts +46 -0
  3. package/dist/adapters/mail-part-store.js +251 -0
  4. package/dist/adapters/repo-store.js +5 -14
  5. package/dist/adapters/spawn-child.d.ts +42 -6
  6. package/dist/adapters/spawn-child.js +8 -18
  7. package/dist/adapters/step-invoker.d.ts +52 -2
  8. package/dist/adapters/step-invoker.js +230 -60
  9. package/dist/adapters/substrate-mailbox-store.d.ts +80 -0
  10. package/dist/adapters/substrate-mailbox-store.js +404 -0
  11. package/dist/child/child-mailbox-reader.d.ts +10 -0
  12. package/dist/child/child-mailbox-reader.js +23 -0
  13. package/dist/child/credential-cell.d.ts +8 -0
  14. package/dist/child/credential-cell.js +66 -0
  15. package/dist/child/from-process-env.d.ts +12 -0
  16. package/dist/child/from-process-env.js +6 -0
  17. package/dist/child/index.d.ts +4 -1
  18. package/dist/child/index.js +4 -1
  19. package/dist/child/mailbox-mutation-bridge.d.ts +61 -0
  20. package/dist/child/mailbox-mutation-bridge.js +101 -0
  21. package/dist/child/mailbox-watch-registry.d.ts +17 -0
  22. package/dist/child/mailbox-watch-registry.js +61 -0
  23. package/dist/child/outbound-mail-bridge.d.ts +3 -2
  24. package/dist/child/outbound-mail-bridge.js +20 -32
  25. package/dist/child/pending-request.d.ts +89 -0
  26. package/dist/child/pending-request.js +80 -0
  27. package/dist/child/run-child.d.ts +69 -7
  28. package/dist/child/run-child.js +307 -75
  29. package/dist/child/substrate-write-bridge.d.ts +3 -2
  30. package/dist/child/substrate-write-bridge.js +21 -38
  31. package/dist/child/supervisor-backed-transport.d.ts +52 -6
  32. package/dist/child/supervisor-backed-transport.js +205 -62
  33. package/dist/child/warm-agent-cache.d.ts +44 -4
  34. package/dist/child/warm-agent-cache.js +41 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +4 -3
  37. package/dist/ipc/control-channel.d.ts +93 -2
  38. package/dist/ipc/control-channel.js +147 -47
  39. package/dist/ipc/index.d.ts +1 -1
  40. package/dist/ipc/index.js +1 -1
  41. package/dist/run-body-then-cleanup.d.ts +17 -0
  42. package/dist/run-body-then-cleanup.js +38 -0
  43. package/dist/seams/scheduler.d.ts +12 -0
  44. package/dist/seams/scheduler.js +13 -4
  45. package/dist/supervisor/cancel-signing.js +3 -7
  46. package/dist/supervisor/credentials.d.ts +17 -5
  47. package/dist/supervisor/recycle.d.ts +5 -1
  48. package/dist/supervisor/run-event-compaction.d.ts +2 -2
  49. package/dist/supervisor/run-event-compaction.js +11 -16
  50. package/dist/supervisor/run-event-recovery.d.ts +34 -0
  51. package/dist/supervisor/run-event-recovery.js +45 -0
  52. package/dist/supervisor/supervisor.d.ts +27 -4
  53. package/dist/supervisor/supervisor.js +644 -58
  54. package/dist/supervisor/terminal-commit.js +3 -7
  55. package/dist/supervisor/types.d.ts +30 -0
  56. package/dist/testing/change-notifier.d.ts +12 -0
  57. package/dist/testing/change-notifier.js +63 -0
  58. package/dist/testing/index.d.ts +8 -0
  59. package/dist/testing/index.js +16 -0
  60. package/dist/testing/log-capture.d.ts +52 -0
  61. package/dist/testing/log-capture.js +124 -0
  62. package/dist/testing/mail-bus.d.ts +22 -0
  63. package/dist/testing/mail-bus.js +78 -0
  64. package/dist/testing/memory-streams.d.ts +43 -0
  65. package/dist/testing/memory-streams.js +211 -0
  66. package/dist/testing/spawn-observer.d.ts +12 -0
  67. package/dist/testing/spawn-observer.js +36 -0
  68. package/dist/testing/stub-repo-store.d.ts +10 -0
  69. package/dist/testing/stub-repo-store.js +39 -0
  70. package/dist/testing/supervisor-reaper.d.ts +24 -0
  71. package/dist/testing/supervisor-reaper.js +49 -0
  72. package/dist/testing/upstream-frames.d.ts +47 -0
  73. package/dist/testing/upstream-frames.js +94 -0
  74. package/dist/workflow-definition-loader.d.ts +56 -0
  75. package/dist/workflow-definition-loader.js +106 -0
  76. package/package.json +17 -11
  77. package/dist/conversation-text.d.ts +0 -23
  78. package/dist/conversation-text.js +0 -56
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Run `body`, then always run `cleanup`, without letting a `cleanup`
3
+ * failure mask a `body` failure.
4
+ *
5
+ * When `body` throws, `cleanup` still runs and a `cleanup` failure is
6
+ * handed to `onCleanupErrorAfterBodyError` (to log) and then dropped, so
7
+ * the original `body` error is what propagates. When `body` succeeds, a
8
+ * `cleanup` failure propagates -- there is no primary error to protect, so
9
+ * a failing teardown is the error worth surfacing.
10
+ *
11
+ * This exists so teardown in a `finally` (agent close, warm-cache
12
+ * eviction) can surface its own failure without a `throw` inside a
13
+ * `finally` block, which `no-unsafe-finally` forbids precisely because it
14
+ * silently swallows the in-flight exception -- the masking bug this guards
15
+ * against.
16
+ */
17
+ export declare function runBodyThenCleanup<T>(body: () => Promise<T>, cleanup: () => Promise<void>, onCleanupErrorAfterBodyError: (cause: unknown) => void): Promise<T>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Run `body`, then always run `cleanup`, without letting a `cleanup`
3
+ * failure mask a `body` failure.
4
+ *
5
+ * When `body` throws, `cleanup` still runs and a `cleanup` failure is
6
+ * handed to `onCleanupErrorAfterBodyError` (to log) and then dropped, so
7
+ * the original `body` error is what propagates. When `body` succeeds, a
8
+ * `cleanup` failure propagates -- there is no primary error to protect, so
9
+ * a failing teardown is the error worth surfacing.
10
+ *
11
+ * This exists so teardown in a `finally` (agent close, warm-cache
12
+ * eviction) can surface its own failure without a `throw` inside a
13
+ * `finally` block, which `no-unsafe-finally` forbids precisely because it
14
+ * silently swallows the in-flight exception -- the masking bug this guards
15
+ * against.
16
+ */
17
+ export async function runBodyThenCleanup(body, cleanup, onCleanupErrorAfterBodyError) {
18
+ let outcome;
19
+ try {
20
+ outcome = { ok: true, value: await body() };
21
+ }
22
+ catch (error) {
23
+ outcome = { ok: false, error };
24
+ }
25
+ try {
26
+ await cleanup();
27
+ }
28
+ catch (cleanupError) {
29
+ if (outcome.ok) {
30
+ throw cleanupError;
31
+ }
32
+ onCleanupErrorAfterBodyError(cleanupError);
33
+ }
34
+ if (!outcome.ok) {
35
+ throw outcome.error;
36
+ }
37
+ return outcome.value;
38
+ }
@@ -52,6 +52,18 @@ export type SchedulerOpts = {
52
52
  * callbacks and to skip past-due cron entries on recovery.
53
53
  */
54
54
  clock: () => Date;
55
+ /**
56
+ * Arms a one-shot timer and returns its canceller. Defaults to the global
57
+ * timer, which is what production wants.
58
+ *
59
+ * The clock above decides WHEN a timer should fire; this decides what
60
+ * actually fires it. Without both, a caller can compute a deterministic
61
+ * delay and still have to wait out the real interval to observe the
62
+ * firing -- which left this module's own tests waiting past a `fireAt`,
63
+ * and, in one case, waiting longer than a cancelled timer's delay to argue
64
+ * from silence that the cancel had worked.
65
+ */
66
+ scheduleTimeout?: (handler: () => void, ms: number) => () => void;
55
67
  };
56
68
  export type SchedulerHandle = {
57
69
  /**
@@ -63,6 +63,15 @@ export const TimerEventEnvelope = type({
63
63
  "+": "ignore",
64
64
  });
65
65
  export function createWorkflowHostScheduler(opts) {
66
+ // Production arms the global timer; a caller supplying its own can fire a
67
+ // queued timer on demand instead of waiting out its delay.
68
+ const schedule = opts.scheduleTimeout ??
69
+ ((handler, ms) => {
70
+ const handle = setTimeout(handler, ms);
71
+ return () => {
72
+ clearTimeout(handle);
73
+ };
74
+ });
66
75
  const queues = new Map();
67
76
  const liveSubscriptions = [];
68
77
  let started = false;
@@ -87,7 +96,7 @@ export function createWorkflowHostScheduler(opts) {
87
96
  if (queues.has(key))
88
97
  return; // idempotent
89
98
  const delayMs = Math.max(0, fireAtMs - opts.clock().getTime());
90
- const timeout = setTimeout(() => {
99
+ const cancelTimeout = schedule(() => {
91
100
  void fireTimer(runId, timerId).catch((cause) => {
92
101
  // The scheduler's commit failed. Surface as unhandled so
93
102
  // operators see it; the runtime body's awaiter will hang
@@ -97,7 +106,7 @@ export function createWorkflowHostScheduler(opts) {
97
106
  : new Error(`scheduler ${String(repoId.id)}/${runId}/${timerId} commit failed: ${String(cause)}`);
98
107
  });
99
108
  }, delayMs);
100
- queues.set(key, { runId, timerId, fireAtMs, timeout, cron });
109
+ queues.set(key, { runId, timerId, fireAtMs, cancelTimeout, cron });
101
110
  }
102
111
  function startLiveSubscription(repoId) {
103
112
  const abort = new AbortController();
@@ -202,7 +211,7 @@ export function createWorkflowHostScheduler(opts) {
202
211
  return;
203
212
  stopped = true;
204
213
  for (const t of queues.values())
205
- clearTimeout(t.timeout);
214
+ t.cancelTimeout();
206
215
  queues.clear();
207
216
  for (const sub of liveSubscriptions.splice(0)) {
208
217
  sub.abort.abort();
@@ -216,7 +225,7 @@ export function createWorkflowHostScheduler(opts) {
216
225
  const entry = queues.get(key);
217
226
  if (entry === undefined)
218
227
  return;
219
- clearTimeout(entry.timeout);
228
+ entry.cancelTimeout();
220
229
  queues.delete(key);
221
230
  },
222
231
  queuedTimers() {
@@ -24,6 +24,7 @@
24
24
  // `supervisor`-kind principal), which is the cross-check the
25
25
  // supervisor's runtime-side signing keeps coherent.
26
26
  import { type } from "arktype";
27
+ import { parseEventSeq } from "@intx/hub-sessions/substrate";
27
28
  import { hexEncode } from "@intx/types";
28
29
  /**
29
30
  * Path inside the workflow-run repo each `CancelRequested` event
@@ -40,7 +41,6 @@ const EVENTS_DIR = "events";
40
41
  * push principal and enforces the principal-vs-origin map.
41
42
  */
42
43
  export const SUPERVISOR_PRINCIPAL_KIND = "supervisor";
43
- const EVENT_FILENAME_RE = /^(0|[1-9][0-9]*)\.json$/;
44
44
  const OnDiskEnvelope = type({
45
45
  seq: "number >= 0",
46
46
  type: "string",
@@ -85,13 +85,9 @@ export async function commitCancelRequested(opts) {
85
85
  let maxSeq = -1;
86
86
  for (const filepath of existing.keys()) {
87
87
  const name = filepath.slice(prefix.length);
88
- const match = EVENT_FILENAME_RE.exec(name);
89
- if (match === null)
88
+ const seq = parseEventSeq(name);
89
+ if (seq === null)
90
90
  continue;
91
- const seqStr = match[1];
92
- if (seqStr === undefined)
93
- continue;
94
- const seq = Number.parseInt(seqStr, 10);
95
91
  if (seq > maxSeq)
96
92
  maxSeq = seq;
97
93
  }
@@ -14,7 +14,11 @@ export declare const STEP_GRANTS_PATH = "state/grants.json";
14
14
  */
15
15
  export declare const STEP_GRANTS_REF = "refs/heads/main";
16
16
  export type CredentialsSnapshotStep = {
17
- /** Workflow step id from `WorkflowDefinition.stepOrder`. */
17
+ /**
18
+ * Workflow step id: a `WorkflowDefinition.stepOrder` entry, or a step id
19
+ * from one of the definition's `loop` bodies (a loop body shares the
20
+ * enclosing definition's flat step-id namespace).
21
+ */
18
22
  stepId: string;
19
23
  /** Mail address the step's agent presents to the bus. */
20
24
  address: string;
@@ -24,7 +28,7 @@ export type CredentialsSnapshotStep = {
24
28
  contentHash: string;
25
29
  };
26
30
  export type CredentialsSnapshot = {
27
- /** Step-id keyed entries in `stepOrder` traversal order. */
31
+ /** Step-id keyed entries in the caller's traversal order. */
28
32
  steps: readonly CredentialsSnapshotStep[];
29
33
  };
30
34
  /**
@@ -55,9 +59,17 @@ export type AssembleCredentialsSnapshotOpts = {
55
59
  /** Principal presented for each step's read. */
56
60
  principal: Principal;
57
61
  /**
58
- * Step ids in the deployment's `stepOrder`. The trivial workflow
59
- * passes a single entry; multi-step deployments pass every step in
60
- * the order the workflow asset declared.
62
+ * Every step id the snapshot must carry an entry for. The trivial
63
+ * workflow passes a single entry; multi-step deployments pass every
64
+ * step in the order the workflow asset declared.
65
+ *
66
+ * This is the deployment's flat step-id namespace, which is WIDER than
67
+ * the definition's own `stepOrder`: a `loop` body runs in-process as a
68
+ * child run inheriting the parent's env, so a body step authorizes
69
+ * against this same snapshot under its own plain step id. The caller
70
+ * owns that widening -- it is the layer that holds the definition --
71
+ * and the snapshot is total over whatever it passes, because the
72
+ * child's authorize treats a missing entry as unrecoverable.
61
73
  */
62
74
  stepOrder: readonly string[];
63
75
  /** Anchor run id used in agent-state repo identity and address derivation. */
@@ -56,7 +56,11 @@ export interface ChildWiring {
56
56
  export interface RecycleContext {
57
57
  /** The supervisor's full bindings, reused on respawn for credentials and spawn. */
58
58
  readonly bindings: WorkflowSupervisorBindings;
59
- /** Step ids in this deployment's `stepOrder` for credentials re-assembly. */
59
+ /**
60
+ * Every step id in this deployment's flat step-id namespace -- the
61
+ * definition's own `stepOrder` plus the step ids of every `loop` body it
62
+ * carries -- for credentials re-assembly.
63
+ */
60
64
  readonly stepOrder: readonly string[];
61
65
  /** Definition hash carried on respawn env (unchanged across recycle). */
62
66
  readonly definitionHash: string;
@@ -20,8 +20,8 @@ export type CompactRunEventsOpts = {
20
20
  * Idempotent and terminal-only: a run already sealed (no `events/` subtree)
21
21
  * or one whose latest event is not terminal is left untouched, so the call
22
22
  * is safe to repeat. The live caller invokes it once per run, right after the
23
- * run terminates; a bounded recovery sweep that would retry the fold for a run
24
- * whose compaction a crash interrupted is not yet implemented.
23
+ * run terminates; `recoverInterruptedCompactions` re-runs it for a run whose
24
+ * fold a crash interrupted before it could seal.
25
25
  *
26
26
  * The combined file is the verbatim byte concatenation of the per-event
27
27
  * blobs in seq order (`encodeCombinedEventLog`), the exact shape the
@@ -7,16 +7,10 @@
7
7
  // without losing any event. The fold writes under the substrate's
8
8
  // per-repo lock as the `supervisor` principal, whose `anchorRunId`
9
9
  // the workflow-run kind handler checks against `repoId.id`.
10
- import { WORKFLOW_RUN_EVENTS_FILE, encodeCombinedEventLog, } from "@intx/hub-sessions/substrate";
10
+ import { classifyTerminalEvent, parseEventSeq, WORKFLOW_RUN_EVENTS_FILE, encodeCombinedEventLog, } from "@intx/hub-sessions/substrate";
11
11
  import { SUPERVISOR_PRINCIPAL_KIND } from "./cancel-signing.js";
12
12
  const RUNS_PREFIX = "runs";
13
13
  const EVENTS_DIR = "events";
14
- const EVENT_FILENAME_RE = /^(0|[1-9][0-9]*)\.json$/;
15
- const TERMINAL_EVENT_TYPES = new Set([
16
- "RunCompleted",
17
- "RunFailed",
18
- "RunCancelled",
19
- ]);
20
14
  /**
21
15
  * Fold a terminated run's per-event `events/<seq>.json` blobs into one
22
16
  * combined `events.jsonl`, dropping the per-event files. This shrinks the
@@ -26,8 +20,8 @@ const TERMINAL_EVENT_TYPES = new Set([
26
20
  * Idempotent and terminal-only: a run already sealed (no `events/` subtree)
27
21
  * or one whose latest event is not terminal is left untouched, so the call
28
22
  * is safe to repeat. The live caller invokes it once per run, right after the
29
- * run terminates; a bounded recovery sweep that would retry the fold for a run
30
- * whose compaction a crash interrupted is not yet implemented.
23
+ * run terminates; `recoverInterruptedCompactions` re-runs it for a run whose
24
+ * fold a crash interrupted before it could seal.
31
25
  *
32
26
  * The combined file is the verbatim byte concatenation of the per-event
33
27
  * blobs in seq order (`encodeCombinedEventLog`), the exact shape the
@@ -55,10 +49,10 @@ export async function compactRunEvents(opts) {
55
49
  }
56
50
  const seqs = [];
57
51
  for (const name of filenames) {
58
- const match = EVENT_FILENAME_RE.exec(name);
59
- if (match === null || match[1] === undefined)
52
+ const seq = parseEventSeq(name);
53
+ if (seq === null)
60
54
  continue;
61
- seqs.push(Number.parseInt(match[1], 10));
55
+ seqs.push(seq);
62
56
  }
63
57
  if (seqs.length === 0)
64
58
  return { compacted: false };
@@ -74,7 +68,8 @@ export async function compactRunEvents(opts) {
74
68
  return { compacted: false };
75
69
  }
76
70
  const lastType = parsed.type;
77
- if (typeof lastType !== "string" || !TERMINAL_EVENT_TYPES.has(lastType)) {
71
+ if (typeof lastType !== "string" ||
72
+ !classifyTerminalEvent(lastType).terminal) {
78
73
  return { compacted: false };
79
74
  }
80
75
  const prefix = `${RUNS_PREFIX}/${opts.runId}/${EVENTS_DIR}/`;
@@ -90,11 +85,11 @@ export async function compactRunEvents(opts) {
90
85
  const entries = [];
91
86
  for (const [filepath, bytes] of existing) {
92
87
  const name = filepath.slice(prefix.length);
93
- const match = EVENT_FILENAME_RE.exec(name);
94
- if (match === null || match[1] === undefined) {
88
+ const seq = parseEventSeq(name);
89
+ if (seq === null) {
95
90
  throw new Error(`supervisor run-event-compaction: unexpected non-event file ${filepath} under run ${opts.runId}; refusing to compact`);
96
91
  }
97
- entries.push({ seq: Number.parseInt(match[1], 10), bytes });
92
+ entries.push({ seq, bytes });
98
93
  }
99
94
  if (entries.length === 0)
100
95
  return {};
@@ -0,0 +1,34 @@
1
+ import type { RepoId, RepoStore as SubstrateRepoStore } from "@intx/hub-sessions/substrate";
2
+ export type RecoverInterruptedCompactionsOpts = {
3
+ /** Substrate handle the supervisor writes through. */
4
+ substrate: SubstrateRepoStore;
5
+ /** Workflow-run repo for this deployment. */
6
+ repoId: RepoId;
7
+ /** Events ref the workflow-run repo writes to. */
8
+ ref: string;
9
+ /** Anchor run id used to construct the supervisor principal. */
10
+ anchorRunId: string;
11
+ /** Runs the boot scan proposes as terminal-but-per-event. */
12
+ pendingSealRunIds: readonly string[];
13
+ };
14
+ /** A run whose recovery fold threw, paired with the failure cause. */
15
+ export type RecoveryFoldFailure = {
16
+ runId: string;
17
+ message: string;
18
+ };
19
+ /**
20
+ * Re-seal runs a crash left terminal but still in per-event form, by re-running
21
+ * the idempotent `compactRunEvents` for each proposed run. `compactRunEvents`
22
+ * is authoritative: it no-ops a run that is already sealed or whose latest
23
+ * event is not terminal, so a stale or mistaken proposal is a harmless no-op.
24
+ *
25
+ * Folds run serially. Every fold contends the same per-repo write lock that
26
+ * live dispatch also takes, so folding one run at a time drains the backlog
27
+ * without a thundering herd on that lock. One run's failure is caught so it
28
+ * cannot abort the rest; the failed run id and its cause are returned -- not
29
+ * logged here -- so the caller owns how to surface the aggregate.
30
+ */
31
+ export declare function recoverInterruptedCompactions(opts: RecoverInterruptedCompactionsOpts): Promise<{
32
+ sealed: number;
33
+ failed: RecoveryFoldFailure[];
34
+ }>;
@@ -0,0 +1,45 @@
1
+ // Bounded recovery for run-event compaction folds a crash interrupted.
2
+ //
3
+ // When a run terminates, the supervisor fires `compactRunEvents` in the
4
+ // background. A crash between the terminal commit and that fold leaves the
5
+ // run terminal but still in per-event form, and the terminal signal never
6
+ // fires again for it. At the next spawn the boot scan proposes those runs;
7
+ // this sweep re-runs the idempotent fold for each so the leaked per-event
8
+ // file count is reclaimed.
9
+ import { compactRunEvents } from "./run-event-compaction.js";
10
+ /**
11
+ * Re-seal runs a crash left terminal but still in per-event form, by re-running
12
+ * the idempotent `compactRunEvents` for each proposed run. `compactRunEvents`
13
+ * is authoritative: it no-ops a run that is already sealed or whose latest
14
+ * event is not terminal, so a stale or mistaken proposal is a harmless no-op.
15
+ *
16
+ * Folds run serially. Every fold contends the same per-repo write lock that
17
+ * live dispatch also takes, so folding one run at a time drains the backlog
18
+ * without a thundering herd on that lock. One run's failure is caught so it
19
+ * cannot abort the rest; the failed run id and its cause are returned -- not
20
+ * logged here -- so the caller owns how to surface the aggregate.
21
+ */
22
+ export async function recoverInterruptedCompactions(opts) {
23
+ let sealed = 0;
24
+ const failed = [];
25
+ for (const runId of opts.pendingSealRunIds) {
26
+ try {
27
+ const { compacted } = await compactRunEvents({
28
+ substrate: opts.substrate,
29
+ repoId: opts.repoId,
30
+ ref: opts.ref,
31
+ anchorRunId: opts.anchorRunId,
32
+ runId,
33
+ });
34
+ if (compacted)
35
+ sealed += 1;
36
+ }
37
+ catch (cause) {
38
+ failed.push({
39
+ runId,
40
+ message: cause instanceof Error ? cause.message : String(cause),
41
+ });
42
+ }
43
+ }
44
+ return { sealed, failed };
45
+ }
@@ -135,6 +135,19 @@ export interface WorkflowSupervisor {
135
135
  * credential is delivered by omitting its material so the child evicts it.
136
136
  */
137
137
  deliverCredentials(opts: DeliverCredentialsOpts): Promise<void>;
138
+ /**
139
+ * Refresh a live run's grant floor mid-run by re-reading its durable
140
+ * `runs/<runId>/grants.json` and pushing it as a `grants-updated` frame. The
141
+ * enforcement path for a standing (`scope: "always"`) approval that lowers a
142
+ * tool's `ask` to `allow` in that file. Unlike `deliverSignal`/
143
+ * `deliverSources`, a refresh for a non-live child is normal, so this
144
+ * NO-OPS (`skipped`) instead of throwing, and a send failure to a live child
145
+ * is logged loudly but stays non-fatal -- the durable file governs the next
146
+ * barrier/respawn. It pushes only that file's contents, never caller-supplied
147
+ * grants, so it can only tighten or refresh a floor. Returns whether a live
148
+ * push happened.
149
+ */
150
+ deliverGrants(runId: string): Promise<"pushed" | "skipped">;
138
151
  /**
139
152
  * Re-register every correlation the child is currently parked on by
140
153
  * querying it for its parked correlations and re-emitting each through
@@ -158,7 +171,11 @@ export interface WorkflowSupervisor {
158
171
  getCredentialsSnapshot(): CredentialsSnapshot | null;
159
172
  }
160
173
  export type SpawnOpts = {
161
- /** Step ids in this deployment's `stepOrder` for credentials assembly. */
174
+ /**
175
+ * Every step id in this deployment's flat step-id namespace -- the
176
+ * definition's own `stepOrder` plus the step ids of every `loop` body it
177
+ * carries -- for credentials assembly.
178
+ */
162
179
  stepOrder: readonly string[];
163
180
  /** Content hash of the deployment's workflow definition. */
164
181
  definitionHash: string;
@@ -235,11 +252,17 @@ export type DeliverSourcesOpts = {
235
252
  };
236
253
  export type DeliverCredentialsOpts = {
237
254
  /**
238
- * The refreshed credential material and per-handle descriptors. A revoked
239
- * credential is delivered by omitting its material entry, so the child's
240
- * wholesale swap evicts it.
255
+ * The refreshed credential material and per-handle descriptors. The child
256
+ * MERGES this into its cell (materials upsert by credentialId, bindings by
257
+ * consumer-and-handle); it does not evict by omission.
241
258
  */
242
259
  delivery: CredentialDelivery;
260
+ /**
261
+ * CredentialIds to drop from the child's cell (a deletion or a deliberate
262
+ * revocation). The child removes each id's material and any binding that
263
+ * references it. A pure revocation pairs an empty `delivery` with these ids.
264
+ */
265
+ revoke?: string[];
243
266
  };
244
267
  export type RecycleOpts = {
245
268
  reason: string;