@intx/workflow-host 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 (101) hide show
  1. package/README.md +77 -14
  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.d.ts +22 -1
  5. package/dist/adapters/repo-store.js +56 -65
  6. package/dist/adapters/spawn-child.d.ts +109 -44
  7. package/dist/adapters/spawn-child.js +77 -81
  8. package/dist/adapters/step-invoker.d.ts +52 -2
  9. package/dist/adapters/step-invoker.js +284 -37
  10. package/dist/adapters/substrate-mailbox-store.d.ts +80 -0
  11. package/dist/adapters/substrate-mailbox-store.js +404 -0
  12. package/dist/child/child-mailbox-reader.d.ts +10 -0
  13. package/dist/child/child-mailbox-reader.js +23 -0
  14. package/dist/child/credential-cell.d.ts +8 -0
  15. package/dist/child/credential-cell.js +66 -0
  16. package/dist/child/env-bootstrap.d.ts +20 -6
  17. package/dist/child/env-bootstrap.js +9 -1
  18. package/dist/child/from-process-env.d.ts +12 -0
  19. package/dist/child/from-process-env.js +6 -0
  20. package/dist/child/index.d.ts +6 -2
  21. package/dist/child/index.js +4 -1
  22. package/dist/child/mailbox-mutation-bridge.d.ts +61 -0
  23. package/dist/child/mailbox-mutation-bridge.js +101 -0
  24. package/dist/child/mailbox-watch-registry.d.ts +17 -0
  25. package/dist/child/mailbox-watch-registry.js +61 -0
  26. package/dist/child/outbound-mail-bridge.d.ts +3 -2
  27. package/dist/child/outbound-mail-bridge.js +20 -32
  28. package/dist/child/parked-correlations.d.ts +42 -0
  29. package/dist/child/parked-correlations.js +80 -0
  30. package/dist/child/pending-request.d.ts +89 -0
  31. package/dist/child/pending-request.js +80 -0
  32. package/dist/child/proxy-repo-store.d.ts +3 -2
  33. package/dist/child/proxy-repo-store.js +2 -0
  34. package/dist/child/run-child.d.ts +170 -14
  35. package/dist/child/run-child.js +569 -155
  36. package/dist/child/self-discovery.d.ts +10 -0
  37. package/dist/child/self-discovery.js +25 -1
  38. package/dist/child/substrate-write-bridge.d.ts +3 -2
  39. package/dist/child/substrate-write-bridge.js +21 -38
  40. package/dist/child/supervisor-backed-transport.d.ts +52 -6
  41. package/dist/child/supervisor-backed-transport.js +205 -62
  42. package/dist/child/verified-definition-loader.d.ts +33 -0
  43. package/dist/child/verified-definition-loader.js +43 -0
  44. package/dist/child/warm-agent-cache.d.ts +44 -4
  45. package/dist/child/warm-agent-cache.js +41 -10
  46. package/dist/index.d.ts +6 -4
  47. package/dist/index.js +6 -4
  48. package/dist/ipc/control-channel.d.ts +151 -2
  49. package/dist/ipc/control-channel.js +222 -29
  50. package/dist/ipc/event-channel.d.ts +32 -1
  51. package/dist/ipc/index.d.ts +1 -1
  52. package/dist/ipc/index.js +1 -1
  53. package/dist/mail-bus/hub-transport-adapter.d.ts +12 -7
  54. package/dist/mail-bus/hub-transport-adapter.js +9 -5
  55. package/dist/run-body-then-cleanup.d.ts +17 -0
  56. package/dist/run-body-then-cleanup.js +38 -0
  57. package/dist/seams/scheduler.d.ts +16 -6
  58. package/dist/seams/scheduler.js +87 -97
  59. package/dist/supervisor/cancel-signing.d.ts +2 -2
  60. package/dist/supervisor/cancel-signing.js +4 -8
  61. package/dist/supervisor/credentials.d.ts +28 -15
  62. package/dist/supervisor/credentials.js +7 -7
  63. package/dist/supervisor/dispatch-attribution.js +1 -1
  64. package/dist/supervisor/drain-timeout.d.ts +2 -2
  65. package/dist/supervisor/drain-timeout.js +1 -1
  66. package/dist/supervisor/index.d.ts +3 -3
  67. package/dist/supervisor/index.js +2 -2
  68. package/dist/supervisor/recycle.d.ts +10 -3
  69. package/dist/supervisor/recycle.js +18 -7
  70. package/dist/supervisor/run-event-compaction.d.ts +5 -5
  71. package/dist/supervisor/run-event-compaction.js +14 -19
  72. package/dist/supervisor/run-event-recovery.d.ts +34 -0
  73. package/dist/supervisor/run-event-recovery.js +45 -0
  74. package/dist/supervisor/spawn-env.d.ts +2 -2
  75. package/dist/supervisor/spawn-env.js +1 -1
  76. package/dist/supervisor/supervisor.d.ts +106 -26
  77. package/dist/supervisor/supervisor.js +1903 -414
  78. package/dist/supervisor/terminal-commit.d.ts +36 -0
  79. package/dist/supervisor/terminal-commit.js +130 -0
  80. package/dist/supervisor/types.d.ts +180 -23
  81. package/dist/testing/change-notifier.d.ts +12 -0
  82. package/dist/testing/change-notifier.js +63 -0
  83. package/dist/testing/index.d.ts +8 -0
  84. package/dist/testing/index.js +16 -0
  85. package/dist/testing/log-capture.d.ts +52 -0
  86. package/dist/testing/log-capture.js +124 -0
  87. package/dist/testing/mail-bus.d.ts +22 -0
  88. package/dist/testing/mail-bus.js +78 -0
  89. package/dist/testing/memory-streams.d.ts +43 -0
  90. package/dist/testing/memory-streams.js +211 -0
  91. package/dist/testing/spawn-observer.d.ts +12 -0
  92. package/dist/testing/spawn-observer.js +36 -0
  93. package/dist/testing/stub-repo-store.d.ts +10 -0
  94. package/dist/testing/stub-repo-store.js +39 -0
  95. package/dist/testing/supervisor-reaper.d.ts +24 -0
  96. package/dist/testing/supervisor-reaper.js +49 -0
  97. package/dist/testing/upstream-frames.d.ts +47 -0
  98. package/dist/testing/upstream-frames.js +94 -0
  99. package/dist/workflow-definition-loader.d.ts +187 -0
  100. package/dist/workflow-definition-loader.js +422 -0
  101. package/package.json +18 -11
@@ -25,5 +25,15 @@ export interface DiscoverRunsOpts {
25
25
  * terminal event. Runs that already terminated are skipped because
26
26
  * resume against a terminal log would still settle without progress
27
27
  * but would generate spurious "resume seed" reads for no benefit.
28
+ *
29
+ * Child runs are excluded. A run spawned by another run -- a
30
+ * `childWorkflow` step's child or an `onTrigger` section's per-event body,
31
+ * whichever committed a `ChildSpawned` naming it -- is driven by its
32
+ * PARENT's runtime, not on its own. Resuming a child here would re-run it
33
+ * under the deployment definition rather than the child's own definition,
34
+ * and for a body approval park it would hub-register the child's internal
35
+ * park that the parent already proxies up on the shared correlation. A
36
+ * child run is identified structurally, by appearing as a
37
+ * `ChildSpawned.childRunId` in some log, rather than by its id shape.
28
38
  */
29
39
  export declare function discoverInFlightRuns(opts: DiscoverRunsOpts): Promise<readonly DiscoveredRun[]>;
@@ -22,6 +22,16 @@ const RUNS_PREFIX = "runs";
22
22
  * terminal event. Runs that already terminated are skipped because
23
23
  * resume against a terminal log would still settle without progress
24
24
  * but would generate spurious "resume seed" reads for no benefit.
25
+ *
26
+ * Child runs are excluded. A run spawned by another run -- a
27
+ * `childWorkflow` step's child or an `onTrigger` section's per-event body,
28
+ * whichever committed a `ChildSpawned` naming it -- is driven by its
29
+ * PARENT's runtime, not on its own. Resuming a child here would re-run it
30
+ * under the deployment definition rather than the child's own definition,
31
+ * and for a body approval park it would hub-register the child's internal
32
+ * park that the parent already proxies up on the shared correlation. A
33
+ * child run is identified structurally, by appearing as a
34
+ * `ChildSpawned.childRunId` in some log, rather than by its id shape.
25
35
  */
26
36
  export async function discoverInFlightRuns(opts) {
27
37
  const fs = await import("node:fs/promises");
@@ -37,11 +47,25 @@ export async function discoverInFlightRuns(opts) {
37
47
  return [];
38
48
  throw cause;
39
49
  }
40
- const out = [];
50
+ // Read every run's log once, and collect the set of run ids that any log
51
+ // spawned as a child. The scan spans every run (terminal ones included) so
52
+ // a child whose parent already completed is still recognized as a child.
53
+ const logs = new Map();
54
+ const childRunIds = new Set();
41
55
  for (const runId of runDirs) {
42
56
  const events = await opts.runtimeRepoStore.read(runId);
43
57
  if (events.length === 0)
44
58
  continue;
59
+ logs.set(runId, events);
60
+ for (const event of events) {
61
+ if (event.kind === "ChildSpawned")
62
+ childRunIds.add(event.childRunId);
63
+ }
64
+ }
65
+ const out = [];
66
+ for (const [runId, events] of logs) {
67
+ if (childRunIds.has(runId))
68
+ continue;
45
69
  const resumed = resumeFromLog(runId, events);
46
70
  if (isTerminalRunPhase(resumed.phase))
47
71
  continue;
@@ -59,8 +59,9 @@ export interface CreateChildSubstrateWriteBridgeOpts {
59
59
  }
60
60
  /**
61
61
  * Construct the child-side substrate-write bridge. Pending writes
62
- * live in a map keyed by `requestId`; the bridge resolves the awaiter
63
- * when the supervisor's matching `substrate.write.response` lands.
62
+ * live in the shared pending-request core keyed by `requestId`; the
63
+ * bridge resolves the awaiter when the supervisor's matching
64
+ * `substrate.write.response` lands.
64
65
  *
65
66
  * The supervisor may emit zero or more `substrate.merge.request`
66
67
  * frames per pending write (the supervisor's merge callback may run
@@ -35,11 +35,13 @@
35
35
  // the closure produces, both base64-encoded.
36
36
  import { getLogger } from "@intx/log";
37
37
  import { base64Decode, base64Encode } from "@intx/types";
38
+ import { createPendingRequestCore } from "./pending-request.js";
38
39
  const logger = getLogger(["workflow-host", "child", "substrate-write-bridge"]);
39
40
  /**
40
41
  * Construct the child-side substrate-write bridge. Pending writes
41
- * live in a map keyed by `requestId`; the bridge resolves the awaiter
42
- * when the supervisor's matching `substrate.write.response` lands.
42
+ * live in the shared pending-request core keyed by `requestId`; the
43
+ * bridge resolves the awaiter when the supervisor's matching
44
+ * `substrate.write.response` lands.
43
45
  *
44
46
  * The supervisor may emit zero or more `substrate.merge.request`
45
47
  * frames per pending write (the supervisor's merge callback may run
@@ -49,17 +51,17 @@ const logger = getLogger(["workflow-host", "child", "substrate-write-bridge"]);
49
51
  * pending entry stays alive until the terminal write response lands.
50
52
  */
51
53
  export function createChildSubstrateWriteBridge(opts) {
52
- const pending = new Map();
53
- const allocate = opts.allocateRequestId ?? defaultRequestIdAllocator();
54
+ const pending = createPendingRequestCore({
55
+ label: "workflow-child substrate write",
56
+ allocatorPrefix: "sw",
57
+ allocateRequestId: opts.allocateRequestId,
58
+ });
54
59
  return {
55
60
  get pendingCount() {
56
- return pending.size;
61
+ return pending.pendingCount;
57
62
  },
58
63
  async submit(req) {
59
- const requestId = allocate();
60
- const resultPromise = new Promise((resolve, reject) => {
61
- pending.set(requestId, { req, resolve, reject });
62
- });
64
+ const { requestId, promise } = pending.register(req);
63
65
  try {
64
66
  await opts.upstreamSender.send({
65
67
  type: "substrate.write.request",
@@ -73,11 +75,10 @@ export function createChildSubstrateWriteBridge(opts) {
73
75
  });
74
76
  }
75
77
  catch (cause) {
76
- pending.delete(requestId);
77
- const message = cause instanceof Error ? cause.message : String(cause);
78
- throw new Error(`workflow-child substrate write: upstream send failed for requestId ${requestId}: ${message}`, { cause });
78
+ pending.discard(requestId);
79
+ throw pending.sendFailedError(requestId, cause);
79
80
  }
80
- return resultPromise;
81
+ return promise;
81
82
  },
82
83
  handleMergeRequest(data) {
83
84
  const entry = pending.get(data.requestId);
@@ -93,7 +94,7 @@ export function createChildSubstrateWriteBridge(opts) {
93
94
  requestId: data.requestId,
94
95
  result: {
95
96
  ok: false,
96
- reason: `workflow-child substrate write: no pending entry for requestId ${data.requestId}`,
97
+ reason: pending.noPendingError(data.requestId).message,
97
98
  },
98
99
  },
99
100
  })
@@ -106,7 +107,7 @@ export function createChildSubstrateWriteBridge(opts) {
106
107
  void (async () => {
107
108
  try {
108
109
  const existing = decodeMergeRequest(data.existing);
109
- const merged = await entry.req.merge(existing);
110
+ const merged = await entry.meta.merge(existing);
110
111
  const files = encodeFiles(merged);
111
112
  await opts.upstreamSender.send({
112
113
  type: "substrate.merge.response",
@@ -137,30 +138,26 @@ export function createChildSubstrateWriteBridge(opts) {
137
138
  })();
138
139
  },
139
140
  handleWriteResponse(data) {
140
- const entry = pending.get(data.requestId);
141
+ const entry = pending.settle(data.requestId);
141
142
  if (entry === undefined) {
142
143
  logger.warn `substrate.write.response landed with no pending entry; requestId=${data.requestId} dropped`;
143
144
  return;
144
145
  }
145
- pending.delete(data.requestId);
146
146
  if (data.result.ok) {
147
147
  entry.resolve({ commitSha: data.result.commitSha });
148
148
  return;
149
149
  }
150
- entry.reject(new Error(`workflow-child substrate write (requestId=${data.requestId}) rejected by supervisor: ${data.result.reason}`));
150
+ entry.reject(pending.rejectedError(data.requestId, data.result.reason));
151
151
  },
152
152
  cancelAll(reason) {
153
- for (const [requestId, entry] of pending) {
154
- entry.reject(new Error(`workflow-child substrate write (requestId=${requestId}) cancelled: ${reason}`));
155
- }
156
- pending.clear();
153
+ pending.cancelAll(reason);
157
154
  },
158
155
  };
159
156
  }
160
157
  function decodeMergeRequest(existing) {
161
158
  const out = new Map();
162
159
  for (const entry of existing) {
163
- out.set(entry.path, base64ToBytes(entry.contentBase64));
160
+ out.set(entry.path, base64Decode(entry.contentBase64));
164
161
  }
165
162
  return out;
166
163
  }
@@ -168,21 +165,7 @@ function encodeFiles(files) {
168
165
  const out = [];
169
166
  for (const [path, content] of Object.entries(files)) {
170
167
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
171
- out.push({ path, contentBase64: bytesToBase64(bytes) });
168
+ out.push({ path, contentBase64: base64Encode(bytes) });
172
169
  }
173
170
  return out;
174
171
  }
175
- function bytesToBase64(bytes) {
176
- return base64Encode(bytes);
177
- }
178
- function base64ToBytes(value) {
179
- return base64Decode(value);
180
- }
181
- function defaultRequestIdAllocator() {
182
- let counter = 0;
183
- return () => {
184
- counter += 1;
185
- const rand = Math.random().toString(36).slice(2, 10);
186
- return `sw-${String(counter)}-${rand}`;
187
- };
188
- }
@@ -1,10 +1,56 @@
1
- import type { MessageTransport } from "@intx/types/runtime";
1
+ import type { CryptoProvider, MessageTransport } from "@intx/types/runtime";
2
+ import type { ChildMailboxReader } from "./child-mailbox-reader.js";
3
+ import type { ChildMailboxMutationBridge } from "./mailbox-mutation-bridge.js";
4
+ import type { MailboxWatchRegistry } from "./mailbox-watch-registry.js";
2
5
  import type { ChildOutboundMailBridge } from "./outbound-mail-bridge.js";
6
+ /**
7
+ * The dependencies backing the transport's whole inbox capability: the local
8
+ * IMAP READ surface (`reader` / `watchRegistry` / `getCrypto`) that resolves
9
+ * against the deployment's substrate mailbox, plus the routed-WRITE channel
10
+ * (`mutationBridge`) that carries flag writes and expunge up to the supervisor.
11
+ * The sidecar wires the bundle only for a build that owns an inbound mailbox
12
+ * (the warm agent); a build without it has no inbox and every inbound method
13
+ * throws a clear "not wired" error. Reads are local; writes route upstream --
14
+ * the child never flushes the run ref, so it never races the supervisor.
15
+ */
16
+ export interface SupervisorBackedTransportInbound {
17
+ /**
18
+ * Opens a fresh committed snapshot of the deployment's substrate `INBOX`.
19
+ * Every inbound read opens a new snapshot, so a read taken after a
20
+ * `mailbox.notify` -- or after a routed write the supervisor flushed before
21
+ * replying -- observes the committed state.
22
+ */
23
+ reader: ChildMailboxReader;
24
+ /**
25
+ * The registry the child's control loop fires `mailbox.notify` into. It must
26
+ * be the same instance `runWorkflowChild` routes the frame to, so a `watch`
27
+ * installed here observes the supervisor's notification.
28
+ */
29
+ watchRegistry: MailboxWatchRegistry;
30
+ /**
31
+ * Resolve a sender address to its `CryptoProvider` so `fetchFull` can verify
32
+ * the message signature. Returns `undefined` when no key is known for the
33
+ * sender, in which case the signature status is reported as `unknown`.
34
+ */
35
+ getCrypto: (fromAddress: string) => CryptoProvider | undefined;
36
+ /**
37
+ * The upstream channel the write methods route through. `setFlags` /
38
+ * `clearFlags` / `expunge` call `mutationBridge.submit`, which emits a
39
+ * `mailbox.mutate.request` and resolves once the supervisor applies the
40
+ * mutation to its owned store and replies. Bundled with the read surface
41
+ * because the write methods and the reads share one presence condition:
42
+ * this agent owns an inbox, or it owns none.
43
+ */
44
+ mutationBridge: ChildMailboxMutationBridge;
45
+ }
3
46
  /**
4
47
  * Construct a `MessageTransport` whose outbound side routes through the
5
- * supervisor (via `bridge`) and whose inbound side is inert. `address`
6
- * is the agent's mail address; the supervisor signs the outbound mail as
7
- * this address through the host transport, so it must be the address the
8
- * host registered the agent's `CryptoProvider` against.
48
+ * supervisor (via `bridge`) and whose inbound side is a local IMAP read
49
+ * surface over `inbound`. `address` is the agent's mail address; the
50
+ * supervisor signs the outbound mail as this address through the host
51
+ * transport, so it must be the address the host registered the agent's
52
+ * `CryptoProvider` against. When `inbound` is omitted, the inbound methods
53
+ * throw a clear "not wired" error; the sidecar supplies it once the child's
54
+ * mailbox reader and watch registry are threaded through.
9
55
  */
10
- export declare function createSupervisorBackedTransport(bridge: ChildOutboundMailBridge, address: string): MessageTransport;
56
+ export declare function createSupervisorBackedTransport(bridge: ChildOutboundMailBridge, address: string, inbound?: SupervisorBackedTransportInbound): MessageTransport;
@@ -1,35 +1,80 @@
1
1
  // Supervisor-backed `MessageTransport` for a unified-host step agent
2
- // (OUTBOUND half of mailbox ownership, §3a).
2
+ // (both halves of mailbox ownership, §3a OUTBOUND and §3b INBOUND).
3
3
  //
4
4
  // Under the unified host the supervisor is the sole mail owner: it holds
5
5
  // the durable inbox and the host transport against which the agent's
6
6
  // address is registered with its signing key. The step agent therefore
7
- // does NOT subscribe its own transport for inbound mail (the supervisor
8
- // delivers inputs via the step path) and does NOT hold a signing key to
9
- // send outbound mail. Its mail tools are backed by this transport:
7
+ // does NOT hold a signing key to send outbound mail, and it does NOT own
8
+ // the host-side inbox directly. Its mail tools are backed by this
9
+ // transport:
10
10
  //
11
- // - INBOUND is a no-op. `watch` returns a no-op unsubscribe and never
12
- // fires; the supervisor delivers the agent's input as the step
13
- // input, not through the agent's own mailbox. The IMAP read surface
14
- // (`search`, `fetchFull`, `fetchHeaders`, ...) throws: the agent
15
- // owns no mailbox in the unified host, so a read against one is a
16
- // programming error, surfaced loudly rather than returning a
17
- // silently-empty result that would hide the missing inbound surface.
18
- // - OUTBOUND (`send` / `append`) routes through the supervisor over
19
- // the control IPC via the outbound-mail bridge. The supervisor
20
- // performs the actual signed send through the host transport, so the
21
- // outbound mail carries the agent's signature with full parity to
22
- // the in-process path. The agent never holds the key.
11
+ // - INBOUND is a functional local IMAP read surface once the sidecar
12
+ // wires it (the `inbound` constructor argument). The supervisor
13
+ // commits an arrived message to the deployment's workflow-run
14
+ // substrate mailbox (`mailbox/INBOX/`) and fires a `mailbox.notify`
15
+ // control frame. The read surface (`search`, `thread`,
16
+ // `fetchHeaders`, `fetchStructure`, `fetchPart`, `fetchFull`, `sync`,
17
+ // `getMailboxStatus`) answers by opening a fresh committed snapshot of
18
+ // that mailbox through the child mailbox reader and running the
19
+ // `@intx/mailbox` pure query functions over it -- local, no hub or
20
+ // IPC round-trip. `watch` registers into the child watch registry, so
21
+ // `mail_wait` unblocks when the routed `mailbox.notify` fires. The
22
+ // WRITE methods (`setFlags` / `clearFlags` / `expunge`) do NOT touch
23
+ // the local read surface: they route up to the supervisor through the
24
+ // mailbox-mutation bridge (a `mailbox.mutate.request` frame), which
25
+ // applies the mutation to the supervisor's owned store and replies.
26
+ // The child never flushes the run ref, so it never races the
27
+ // supervisor's mirror. The agent owns only the `INBOX`, so every
28
+ // inbound method rejects a request for any other mailbox rather than
29
+ // silently serving `INBOX` under the wrong name. When the sidecar
30
+ // constructs the transport without the `inbound` argument, the inbound
31
+ // methods throw a clear "not wired" error rather than answer against a
32
+ // missing surface.
33
+ // - OUTBOUND (`send`) routes through the supervisor over the control
34
+ // IPC via the outbound-mail bridge. The supervisor performs the
35
+ // actual signed send through the host transport, so the outbound mail
36
+ // carries the agent's signature with full parity to the in-process
37
+ // path. The agent never holds the key.
38
+ //
39
+ // A handful of methods stay unsupported and throw: they act on a resource
40
+ // the unified-host agent does not own. `append` and the mailbox-management
41
+ // methods (`listMailboxes` / `createMailbox` / `deleteMailbox`) target a
42
+ // mailbox the agent does not own; `move` / `copy` need a second mailbox it
43
+ // does not own; and the distribution-list methods are unimplemented across
44
+ // every transport.
45
+ import { executeSearch, executeThread, fetchFull as doFetchFull, fetchHeaders as doFetchHeaders, fetchPart as doFetchPart, fetchStructure as doFetchStructure, } from "@intx/mailbox";
46
+ import { deriveWorkflowRunId } from "@intx/types";
47
+ import { MAILBOX_INBOX_DIR } from "../adapters/substrate-mailbox-store.js";
23
48
  /**
24
49
  * Construct a `MessageTransport` whose outbound side routes through the
25
- * supervisor (via `bridge`) and whose inbound side is inert. `address`
26
- * is the agent's mail address; the supervisor signs the outbound mail as
27
- * this address through the host transport, so it must be the address the
28
- * host registered the agent's `CryptoProvider` against.
50
+ * supervisor (via `bridge`) and whose inbound side is a local IMAP read
51
+ * surface over `inbound`. `address` is the agent's mail address; the
52
+ * supervisor signs the outbound mail as this address through the host
53
+ * transport, so it must be the address the host registered the agent's
54
+ * `CryptoProvider` against. When `inbound` is omitted, the inbound methods
55
+ * throw a clear "not wired" error; the sidecar supplies it once the child's
56
+ * mailbox reader and watch registry are threaded through.
29
57
  */
30
- export function createSupervisorBackedTransport(bridge, address) {
31
- function inboundUnsupported(method) {
32
- throw new Error(`supervisor-backed transport: ${method} is not supported for unified-host step agent ${address}; the supervisor owns the mailbox and delivers inbound mail as the step input`);
58
+ export function createSupervisorBackedTransport(bridge, address, inbound) {
59
+ function unsupported(method) {
60
+ throw new Error(`supervisor-backed transport: ${method} is not supported for unified-host step agent ${address}; the supervisor owns the mailbox and the agent owns only its own ${MAILBOX_INBOX_DIR}`);
61
+ }
62
+ // Return the wired inbound surface, or fail loud when the sidecar
63
+ // constructed the transport without it -- an inbound read against a missing
64
+ // surface is a wiring error, not a silently-empty result.
65
+ function requireInbound(method) {
66
+ if (inbound === undefined) {
67
+ throw new Error(`supervisor-backed transport: ${method} needs the inbound surface, but it is not wired for unified-host step agent ${address}; the sidecar must construct the transport with its mailbox reader, watch registry, and crypto`);
68
+ }
69
+ return inbound;
70
+ }
71
+ // The unified-host agent owns exactly one mailbox, the substrate `INBOX`
72
+ // the reader opens. Reject any other name rather than serve `INBOX` under
73
+ // it, which would return the wrong mailbox's messages mislabeled.
74
+ function requireInbox(mailbox) {
75
+ if (mailbox !== MAILBOX_INBOX_DIR) {
76
+ throw new Error(`supervisor-backed transport: unified-host step agent ${address} owns only the "${MAILBOX_INBOX_DIR}" mailbox; "${mailbox}" is not available`);
77
+ }
33
78
  }
34
79
  return {
35
80
  async send(message, _signal) {
@@ -39,75 +84,173 @@ export function createSupervisorBackedTransport(bridge, address) {
39
84
  // `append` writes into a mailbox the agent owns; in the unified
40
85
  // host the agent owns none. The mail tools do not append (they
41
86
  // `send`), so a reachable `append` is a programming error.
42
- return inboundUnsupported("append");
87
+ return unsupported("append");
43
88
  },
44
89
  async listMailboxes(_signal) {
45
- return inboundUnsupported("listMailboxes");
90
+ return unsupported("listMailboxes");
46
91
  },
47
92
  async createMailbox(_name, _signal) {
48
- return inboundUnsupported("createMailbox");
93
+ return unsupported("createMailbox");
49
94
  },
50
95
  async deleteMailbox(_name, _signal) {
51
- return inboundUnsupported("deleteMailbox");
96
+ return unsupported("deleteMailbox");
52
97
  },
53
- async getMailboxStatus(_name, _signal) {
54
- return inboundUnsupported("getMailboxStatus");
98
+ async getMailboxStatus(name, _signal) {
99
+ const { reader } = requireInbound("getMailboxStatus");
100
+ requireInbox(name);
101
+ const store = await reader.open();
102
+ const unseen = store.messages.filter((m) => !m.flags.has("\\Seen")).length;
103
+ return {
104
+ total: store.messages.length,
105
+ unseen,
106
+ recent: 0,
107
+ uidNext: store.uidNext,
108
+ uidValidity: store.uidValidity,
109
+ highestModSeq: store.highestModSeq,
110
+ };
55
111
  },
56
- async search(_mailbox, _query, _signal) {
57
- return inboundUnsupported("search");
112
+ async search(mailbox, query, _signal) {
113
+ const { reader } = requireInbound("search");
114
+ requireInbox(mailbox);
115
+ const store = await reader.open();
116
+ return await executeSearch(mailbox, store, query);
58
117
  },
59
- async thread(_mailbox, _algorithm, _query, _signal) {
60
- return inboundUnsupported("thread");
118
+ async thread(mailbox, algorithm, query, _signal) {
119
+ const { reader } = requireInbound("thread");
120
+ requireInbox(mailbox);
121
+ const store = await reader.open();
122
+ return await executeThread(mailbox, store, algorithm, query);
61
123
  },
62
- async fetchHeaders(_ref, _signal) {
63
- return inboundUnsupported("fetchHeaders");
124
+ async fetchHeaders(ref, _signal) {
125
+ const { reader } = requireInbound("fetchHeaders");
126
+ requireInbox(ref.mailbox);
127
+ const store = await reader.open();
128
+ return await doFetchHeaders(ref, store);
64
129
  },
65
- async fetchStructure(_ref, _signal) {
66
- return inboundUnsupported("fetchStructure");
130
+ async fetchStructure(ref, _signal) {
131
+ const { reader } = requireInbound("fetchStructure");
132
+ requireInbox(ref.mailbox);
133
+ const store = await reader.open();
134
+ return await doFetchStructure(ref, store);
67
135
  },
68
- async fetchPart(_ref, _partPath, _signal) {
69
- return inboundUnsupported("fetchPart");
136
+ async fetchPart(ref, partPath, _signal) {
137
+ const { reader } = requireInbound("fetchPart");
138
+ requireInbox(ref.mailbox);
139
+ const store = await reader.open();
140
+ return await doFetchPart(ref, partPath, store);
70
141
  },
71
- async fetchFull(_ref, _signal) {
72
- return inboundUnsupported("fetchFull");
142
+ async fetchFull(ref, _signal) {
143
+ const { reader, getCrypto } = requireInbound("fetchFull");
144
+ requireInbox(ref.mailbox);
145
+ const store = await reader.open();
146
+ return await doFetchFull(ref, store, getCrypto);
73
147
  },
74
- async setFlags(_ref, _flags, _signal) {
75
- return inboundUnsupported("setFlags");
148
+ async setFlags(ref, flags, _signal) {
149
+ const { mutationBridge } = requireInbound("setFlags");
150
+ requireInbox(ref.mailbox);
151
+ // Route the flag write to the supervisor -- the sole mailbox writer --
152
+ // rather than flushing a second store against the run ref. `submit`
153
+ // resolves only after the supervisor flushes, so a subsequent read
154
+ // observes the flag.
155
+ await mutationBridge.submit({
156
+ runId: deriveWorkflowRunId(address),
157
+ mailbox: ref.mailbox,
158
+ op: "addFlags",
159
+ uid: ref.uid,
160
+ flags,
161
+ });
76
162
  },
77
- async clearFlags(_ref, _flags, _signal) {
78
- return inboundUnsupported("clearFlags");
163
+ async clearFlags(ref, flags, _signal) {
164
+ const { mutationBridge } = requireInbound("clearFlags");
165
+ requireInbox(ref.mailbox);
166
+ await mutationBridge.submit({
167
+ runId: deriveWorkflowRunId(address),
168
+ mailbox: ref.mailbox,
169
+ op: "removeFlags",
170
+ uid: ref.uid,
171
+ flags,
172
+ });
79
173
  },
80
174
  async move(_ref, _toMailbox, _signal) {
81
- return inboundUnsupported("move");
175
+ return unsupported("move");
82
176
  },
83
177
  async copy(_ref, _toMailbox, _signal) {
84
- return inboundUnsupported("copy");
178
+ return unsupported("copy");
85
179
  },
86
- async expunge(_mailbox, _signal) {
87
- return inboundUnsupported("expunge");
180
+ async expunge(mailbox, _signal) {
181
+ const { mutationBridge } = requireInbound("expunge");
182
+ requireInbox(mailbox);
183
+ // Route to the supervisor, which sweeps every `\Deleted` message out of
184
+ // its owned INBOX and returns the swept uids. The expunged bytes survive
185
+ // in git history (a workflow-run repo's objects are never GC'd), so the
186
+ // replication check permits the deletion. A caller expunging after a
187
+ // `setFlags(\Deleted)` MUST await the two in sequence -- the supervisor
188
+ // applies mutations in arrival order, so an unawaited (concurrent) pair
189
+ // could let the sweep run before the flag is set and miss the message.
190
+ const result = await mutationBridge.submit({
191
+ runId: deriveWorkflowRunId(address),
192
+ mailbox,
193
+ op: "expunge",
194
+ });
195
+ return { expungedUids: result.expungedUids ?? [] };
88
196
  },
89
- watch(_mailbox, _callback) {
90
- // Inbound delivery is a no-op: the supervisor delivers the agent's
91
- // input as the step input, not through the agent's mailbox. The
92
- // watch never fires; return a no-op unsubscribe so a mail tool that
93
- // installs a watch (mail_wait) does not throw at install time but
94
- // also never observes a spurious event.
95
- return () => undefined;
197
+ watch(mailbox, callback) {
198
+ const { watchRegistry } = requireInbound("watch");
199
+ requireInbox(mailbox);
200
+ // The supervisor -- the sole mail owner -- fires `mailbox.notify` into
201
+ // the registry when new mail lands; the registry delivers the typed
202
+ // event to this callback. `mail_wait` installs the watch and unblocks on
203
+ // the first delivery.
204
+ return watchRegistry.watch(mailbox, callback);
96
205
  },
97
- async sync(_mailbox, _knownState, _signal) {
98
- return inboundUnsupported("sync");
206
+ async sync(mailbox, knownState, _signal) {
207
+ const { reader } = requireInbound("sync");
208
+ requireInbox(mailbox);
209
+ const store = await reader.open();
210
+ const result = store.sync({
211
+ uidValidity: knownState.uidValidity,
212
+ highestModSeq: knownState.highestModSeq,
213
+ });
214
+ if (result.resync) {
215
+ return {
216
+ vanished: [],
217
+ changed: [],
218
+ newMessages: result.messages.map((m) => ({ uid: m.uid, mailbox })),
219
+ fullResyncRequired: true,
220
+ };
221
+ }
222
+ // The backing reports every message whose modseq advanced past the
223
+ // client's known state as `changed`. Split it against the client's known
224
+ // `uidNext`: a uid at or beyond it is a new arrival, one below it is a
225
+ // flag change on a message the client already held.
226
+ const newMessages = [];
227
+ const changed = [];
228
+ for (const m of result.changed) {
229
+ if (m.uid >= knownState.uidNext) {
230
+ newMessages.push({ uid: m.uid, mailbox });
231
+ }
232
+ else {
233
+ changed.push({ uid: m.uid, flags: Array.from(m.flags) });
234
+ }
235
+ }
236
+ return {
237
+ vanished: [...result.vanished],
238
+ changed,
239
+ newMessages,
240
+ fullResyncRequired: false,
241
+ };
99
242
  },
100
243
  async createList(_address, _name, _signal) {
101
- return inboundUnsupported("createList");
244
+ return unsupported("createList");
102
245
  },
103
246
  async listMembers(_address, _signal) {
104
- return inboundUnsupported("listMembers");
247
+ return unsupported("listMembers");
105
248
  },
106
249
  async subscribe(_listAddress, _subscriberAddress, _signal) {
107
- return inboundUnsupported("subscribe");
250
+ return unsupported("subscribe");
108
251
  },
109
252
  async unsubscribe(_listAddress, _subscriberAddress, _signal) {
110
- return inboundUnsupported("unsubscribe");
253
+ return unsupported("unsubscribe");
111
254
  },
112
255
  };
113
256
  }
@@ -0,0 +1,33 @@
1
+ import type { WorkflowDefinition } from "@intx/workflow";
2
+ export interface LoadVerifiedWorkflowDefinitionFromClosureOpts {
3
+ /**
4
+ * Sidecar-local directory of the materialized workflow-definition closure:
5
+ * the package dir holding `package.json` (with `interchange.workflow`) and
6
+ * its laid-out `node_modules/`. The sidecar computes this when it applies
7
+ * the frozen closure and threads it through the child's spawn env.
8
+ */
9
+ packageDir: string;
10
+ /**
11
+ * Hub-approved wire hash the re-verify must match. Sourced from the hub
12
+ * authority (`SpawnTimeEnv.definitionHash`). The recompute projects the
13
+ * evaluated LIVE definition back to its inert form and hashes that; a value
14
+ * that differs throws.
15
+ */
16
+ approvedHash: string;
17
+ /**
18
+ * Test seam forwarded to `loadWorkflowDefinitionFromClosure` for the
19
+ * workflow entry's dynamic import. Production omits it and the entry is
20
+ * imported natively.
21
+ */
22
+ importModule?: (importUrl: string) => Promise<unknown>;
23
+ }
24
+ /**
25
+ * Evaluate a source-ref deployment's pinned code closure to a live
26
+ * `WorkflowDefinition` and re-verify it by project-then-hash before returning
27
+ * it. The inert projection is a non-executable approval surface, so the runtime
28
+ * needs the live definition the closure evaluates to. The re-verify projects
29
+ * that live definition back to its inert form and hashes it
30
+ * (`computeLiveDefinitionHash`), matching the hub-approved wire hash by byte
31
+ * equality; a divergent closure fails closed here and never runs.
32
+ */
33
+ export declare function loadVerifiedWorkflowDefinitionFromClosure(opts: LoadVerifiedWorkflowDefinitionFromClosureOpts): Promise<WorkflowDefinition>;