@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.
- package/README.md +21 -4
- package/dist/adapters/mail-part-store.d.ts +46 -0
- package/dist/adapters/mail-part-store.js +251 -0
- package/dist/adapters/repo-store.js +5 -14
- package/dist/adapters/spawn-child.d.ts +42 -6
- package/dist/adapters/spawn-child.js +8 -18
- package/dist/adapters/step-invoker.d.ts +52 -2
- package/dist/adapters/step-invoker.js +230 -60
- package/dist/adapters/substrate-mailbox-store.d.ts +80 -0
- package/dist/adapters/substrate-mailbox-store.js +404 -0
- package/dist/child/child-mailbox-reader.d.ts +10 -0
- package/dist/child/child-mailbox-reader.js +23 -0
- package/dist/child/credential-cell.d.ts +8 -0
- package/dist/child/credential-cell.js +66 -0
- package/dist/child/from-process-env.d.ts +12 -0
- package/dist/child/from-process-env.js +6 -0
- package/dist/child/index.d.ts +4 -1
- package/dist/child/index.js +4 -1
- package/dist/child/mailbox-mutation-bridge.d.ts +61 -0
- package/dist/child/mailbox-mutation-bridge.js +101 -0
- package/dist/child/mailbox-watch-registry.d.ts +17 -0
- package/dist/child/mailbox-watch-registry.js +61 -0
- package/dist/child/outbound-mail-bridge.d.ts +3 -2
- package/dist/child/outbound-mail-bridge.js +20 -32
- package/dist/child/pending-request.d.ts +89 -0
- package/dist/child/pending-request.js +80 -0
- package/dist/child/run-child.d.ts +69 -7
- package/dist/child/run-child.js +307 -75
- package/dist/child/substrate-write-bridge.d.ts +3 -2
- package/dist/child/substrate-write-bridge.js +21 -38
- package/dist/child/supervisor-backed-transport.d.ts +52 -6
- package/dist/child/supervisor-backed-transport.js +205 -62
- package/dist/child/warm-agent-cache.d.ts +44 -4
- package/dist/child/warm-agent-cache.js +41 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/ipc/control-channel.d.ts +93 -2
- package/dist/ipc/control-channel.js +147 -47
- package/dist/ipc/index.d.ts +1 -1
- package/dist/ipc/index.js +1 -1
- package/dist/run-body-then-cleanup.d.ts +17 -0
- package/dist/run-body-then-cleanup.js +38 -0
- package/dist/seams/scheduler.d.ts +12 -0
- package/dist/seams/scheduler.js +13 -4
- package/dist/supervisor/cancel-signing.js +3 -7
- package/dist/supervisor/credentials.d.ts +17 -5
- package/dist/supervisor/recycle.d.ts +5 -1
- package/dist/supervisor/run-event-compaction.d.ts +2 -2
- package/dist/supervisor/run-event-compaction.js +11 -16
- package/dist/supervisor/run-event-recovery.d.ts +34 -0
- package/dist/supervisor/run-event-recovery.js +45 -0
- package/dist/supervisor/supervisor.d.ts +27 -4
- package/dist/supervisor/supervisor.js +644 -58
- package/dist/supervisor/terminal-commit.js +3 -7
- package/dist/supervisor/types.d.ts +30 -0
- package/dist/testing/change-notifier.d.ts +12 -0
- package/dist/testing/change-notifier.js +63 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/log-capture.d.ts +52 -0
- package/dist/testing/log-capture.js +124 -0
- package/dist/testing/mail-bus.d.ts +22 -0
- package/dist/testing/mail-bus.js +78 -0
- package/dist/testing/memory-streams.d.ts +43 -0
- package/dist/testing/memory-streams.js +211 -0
- package/dist/testing/spawn-observer.d.ts +12 -0
- package/dist/testing/spawn-observer.js +36 -0
- package/dist/testing/stub-repo-store.d.ts +10 -0
- package/dist/testing/stub-repo-store.js +39 -0
- package/dist/testing/supervisor-reaper.d.ts +24 -0
- package/dist/testing/supervisor-reaper.js +49 -0
- package/dist/testing/upstream-frames.d.ts +47 -0
- package/dist/testing/upstream-frames.js +94 -0
- package/dist/workflow-definition-loader.d.ts +56 -0
- package/dist/workflow-definition-loader.js +106 -0
- package/package.json +17 -11
- package/dist/conversation-text.d.ts +0 -23
- package/dist/conversation-text.js +0 -56
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Child-side mailbox-mutation bridge (INBOUND half of mailbox ownership,
|
|
2
|
+
// §3b).
|
|
3
|
+
//
|
|
4
|
+
// The supervisor is the sole mail owner: it holds the long-lived
|
|
5
|
+
// substrate mailbox store and is the only writer to the workflow-run
|
|
6
|
+
// ref. A step agent reads its INBOX locally (the supervisor-backed
|
|
7
|
+
// transport's read surface opens fresh committed snapshots), but every
|
|
8
|
+
// MUTATION of the mailbox -- flag writes (`\Seen`, `\Deleted`, ...) and
|
|
9
|
+
// `expunge` -- routes up to the supervisor through this bridge rather
|
|
10
|
+
// than being flushed from the child. A second writer flushing the same
|
|
11
|
+
// ref from the child would race the supervisor's in-memory mirror and
|
|
12
|
+
// break uid / modseq monotonicity, so the child never writes the
|
|
13
|
+
// mailbox directly.
|
|
14
|
+
//
|
|
15
|
+
// Lifecycle of one mutation:
|
|
16
|
+
//
|
|
17
|
+
// 1. The agent's mail tool (flag or expunge) calls the
|
|
18
|
+
// supervisor-backed transport's `setFlags` / `clearFlags` /
|
|
19
|
+
// `expunge`. The transport calls `bridge.submit(mutation)`.
|
|
20
|
+
// 2. `submit` mints a `requestId`, registers a pending awaiter, and
|
|
21
|
+
// emits `mailbox.mutate.request` upstream carrying the op and its
|
|
22
|
+
// operands.
|
|
23
|
+
// 3. The supervisor applies the op to its owned mailbox store,
|
|
24
|
+
// flushes, and replies with `mailbox.mutate.response`. The reply is
|
|
25
|
+
// sent only after the flush, so the child's next committed read
|
|
26
|
+
// observes the mutation (the same flush-before-signal ordering
|
|
27
|
+
// `mailbox.notify` relies on).
|
|
28
|
+
// 4. The bridge resolves / rejects the pending awaiter; the
|
|
29
|
+
// transport method returns to the mail tool. A supervisor-side
|
|
30
|
+
// failure (unknown uid, substrate fault) surfaces as a rejection so
|
|
31
|
+
// the agent's mail-tool call fails loudly rather than silently
|
|
32
|
+
// dropping the mutation.
|
|
33
|
+
import { getLogger } from "@intx/log";
|
|
34
|
+
import { createPendingRequestCore } from "./pending-request.js";
|
|
35
|
+
const logger = getLogger(["workflow-host", "child", "mailbox-mutation-bridge"]);
|
|
36
|
+
/**
|
|
37
|
+
* Construct the child-side mailbox-mutation bridge. Pending mutations
|
|
38
|
+
* live in the shared pending-request core keyed by `requestId`; the
|
|
39
|
+
* bridge resolves the awaiter when the supervisor's matching
|
|
40
|
+
* `mailbox.mutate.response` lands.
|
|
41
|
+
*/
|
|
42
|
+
export function createChildMailboxMutationBridge(opts) {
|
|
43
|
+
const pending = createPendingRequestCore({
|
|
44
|
+
label: "workflow-child mailbox mutation",
|
|
45
|
+
allocatorPrefix: "mm",
|
|
46
|
+
allocateRequestId: opts.allocateRequestId,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
get pendingCount() {
|
|
50
|
+
return pending.pendingCount;
|
|
51
|
+
},
|
|
52
|
+
async submit(mutation) {
|
|
53
|
+
const { requestId, promise } = pending.register(undefined);
|
|
54
|
+
const data = mutation.op === "expunge"
|
|
55
|
+
? {
|
|
56
|
+
requestId,
|
|
57
|
+
runId: mutation.runId,
|
|
58
|
+
mailbox: mutation.mailbox,
|
|
59
|
+
op: mutation.op,
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
requestId,
|
|
63
|
+
runId: mutation.runId,
|
|
64
|
+
mailbox: mutation.mailbox,
|
|
65
|
+
op: mutation.op,
|
|
66
|
+
uid: mutation.uid,
|
|
67
|
+
flags: mutation.flags,
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
await opts.upstreamSender.send({
|
|
71
|
+
type: "mailbox.mutate.request",
|
|
72
|
+
data,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
pending.discard(requestId);
|
|
77
|
+
throw pending.sendFailedError(requestId, cause);
|
|
78
|
+
}
|
|
79
|
+
return promise;
|
|
80
|
+
},
|
|
81
|
+
handleResult(data) {
|
|
82
|
+
const entry = pending.settle(data.requestId);
|
|
83
|
+
if (entry === undefined) {
|
|
84
|
+
logger.warn `mailbox.mutate.response landed with no pending entry; requestId=${data.requestId} dropped`;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (data.result.ok) {
|
|
88
|
+
const result = {};
|
|
89
|
+
if (data.result.expungedUids !== undefined) {
|
|
90
|
+
result.expungedUids = data.result.expungedUids;
|
|
91
|
+
}
|
|
92
|
+
entry.resolve(result);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
entry.reject(pending.rejectedError(data.requestId, data.result.reason));
|
|
96
|
+
},
|
|
97
|
+
cancelAll(reason) {
|
|
98
|
+
pending.cancelAll(reason);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MailboxEvent, Unsubscribe } from "@intx/types/runtime";
|
|
2
|
+
export interface MailboxWatchRegistry {
|
|
3
|
+
/**
|
|
4
|
+
* Register a callback for a mailbox. Returns an `Unsubscribe` that removes
|
|
5
|
+
* it; after unsubscribe the callback observes no further events, including
|
|
6
|
+
* one whose `fire` preceded the unsubscribe but whose asynchronous delivery
|
|
7
|
+
* had not yet run.
|
|
8
|
+
*/
|
|
9
|
+
watch(mailbox: string, callback: (event: MailboxEvent) => void): Unsubscribe;
|
|
10
|
+
/**
|
|
11
|
+
* Deliver a `MailboxEvent` to every callback currently registered for the
|
|
12
|
+
* mailbox, each on its own microtask. A no-op when no callback is registered
|
|
13
|
+
* for the mailbox.
|
|
14
|
+
*/
|
|
15
|
+
fire(mailbox: string, event: MailboxEvent): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createMailboxWatchRegistry(): MailboxWatchRegistry;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Child-side mailbox watch registry (INBOUND half of mailbox ownership,
|
|
2
|
+
// design §3b).
|
|
3
|
+
//
|
|
4
|
+
// The supervisor is the sole mail owner: it commits an arrived message to the
|
|
5
|
+
// workflow-run substrate mailbox and fires a `mailbox.notify` control frame.
|
|
6
|
+
// The child's control loop routes that frame to this registry's `fire`, which
|
|
7
|
+
// delivers a typed `exists` `MailboxEvent` to every callback registered for the
|
|
8
|
+
// mailbox through `watch`. The step agent's supervisor-backed transport
|
|
9
|
+
// implements `MessageTransport.watch` over this registry, so `mail_wait`
|
|
10
|
+
// unblocks when new mail lands -- decoupled from the FIFO trigger dispatch that
|
|
11
|
+
// resolves a run's first input.
|
|
12
|
+
//
|
|
13
|
+
// Delivery is ASYNCHRONOUS. A `fire` never invokes a callback synchronously on
|
|
14
|
+
// the delivering call stack: it schedules each callback on a microtask, per the
|
|
15
|
+
// IMAP IDLE contract the `MailboxEvent` watcher models (MESSAGE.md § Real-Time
|
|
16
|
+
// Notification). Delivery re-checks registration at the microtask, so a watcher
|
|
17
|
+
// that unsubscribes between `fire` and delivery observes no event.
|
|
18
|
+
export function createMailboxWatchRegistry() {
|
|
19
|
+
const watchers = new Map();
|
|
20
|
+
return {
|
|
21
|
+
watch(mailbox, callback) {
|
|
22
|
+
let set = watchers.get(mailbox);
|
|
23
|
+
if (set === undefined) {
|
|
24
|
+
set = new Set();
|
|
25
|
+
watchers.set(mailbox, set);
|
|
26
|
+
}
|
|
27
|
+
set.add(callback);
|
|
28
|
+
let active = true;
|
|
29
|
+
return () => {
|
|
30
|
+
// Idempotent: a double-unsubscribe must not remove a same-identity
|
|
31
|
+
// callback a later `watch` re-registered.
|
|
32
|
+
if (!active)
|
|
33
|
+
return;
|
|
34
|
+
active = false;
|
|
35
|
+
const current = watchers.get(mailbox);
|
|
36
|
+
if (current === undefined)
|
|
37
|
+
return;
|
|
38
|
+
current.delete(callback);
|
|
39
|
+
if (current.size === 0)
|
|
40
|
+
watchers.delete(mailbox);
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
fire(mailbox, event) {
|
|
44
|
+
const set = watchers.get(mailbox);
|
|
45
|
+
if (set === undefined)
|
|
46
|
+
return;
|
|
47
|
+
// Snapshot the callbacks registered at fire time, then deliver each on
|
|
48
|
+
// its own microtask so no callback runs synchronously on this call
|
|
49
|
+
// stack. Re-check membership at delivery so a callback unsubscribed
|
|
50
|
+
// between now and its microtask does not receive the event.
|
|
51
|
+
for (const callback of [...set]) {
|
|
52
|
+
queueMicrotask(() => {
|
|
53
|
+
const current = watchers.get(mailbox);
|
|
54
|
+
if (current === undefined || !current.has(callback))
|
|
55
|
+
return;
|
|
56
|
+
callback(event);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -30,7 +30,8 @@ export interface CreateChildOutboundMailBridgeOpts {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Construct the child-side outbound-mail bridge. Pending sends live in
|
|
33
|
-
*
|
|
34
|
-
* supervisor's matching `outbound.result`
|
|
33
|
+
* the shared pending-request core keyed by `requestId`; the bridge
|
|
34
|
+
* resolves the awaiter when the supervisor's matching `outbound.result`
|
|
35
|
+
* lands.
|
|
35
36
|
*/
|
|
36
37
|
export declare function createChildOutboundMailBridge(opts: CreateChildOutboundMailBridgeOpts): ChildOutboundMailBridge;
|
|
@@ -32,24 +32,26 @@
|
|
|
32
32
|
// send.
|
|
33
33
|
import { getLogger } from "@intx/log";
|
|
34
34
|
import { base64Encode } from "@intx/types";
|
|
35
|
+
import { createPendingRequestCore } from "./pending-request.js";
|
|
35
36
|
const logger = getLogger(["workflow-host", "child", "outbound-mail-bridge"]);
|
|
36
37
|
/**
|
|
37
38
|
* Construct the child-side outbound-mail bridge. Pending sends live in
|
|
38
|
-
*
|
|
39
|
-
* supervisor's matching `outbound.result`
|
|
39
|
+
* the shared pending-request core keyed by `requestId`; the bridge
|
|
40
|
+
* resolves the awaiter when the supervisor's matching `outbound.result`
|
|
41
|
+
* lands.
|
|
40
42
|
*/
|
|
41
43
|
export function createChildOutboundMailBridge(opts) {
|
|
42
|
-
const pending =
|
|
43
|
-
|
|
44
|
+
const pending = createPendingRequestCore({
|
|
45
|
+
label: "workflow-child outbound mail",
|
|
46
|
+
allocatorPrefix: "om",
|
|
47
|
+
allocateRequestId: opts.allocateRequestId,
|
|
48
|
+
});
|
|
44
49
|
return {
|
|
45
50
|
get pendingCount() {
|
|
46
|
-
return pending.
|
|
51
|
+
return pending.pendingCount;
|
|
47
52
|
},
|
|
48
53
|
async submit(senderAddress, message) {
|
|
49
|
-
const requestId =
|
|
50
|
-
const resultPromise = new Promise((resolve, reject) => {
|
|
51
|
-
pending.set(requestId, { resolve, reject });
|
|
52
|
-
});
|
|
54
|
+
const { requestId, promise } = pending.register(undefined);
|
|
53
55
|
try {
|
|
54
56
|
await opts.upstreamSender.send({
|
|
55
57
|
type: "outbound.message",
|
|
@@ -61,19 +63,17 @@ export function createChildOutboundMailBridge(opts) {
|
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
65
|
catch (cause) {
|
|
64
|
-
pending.
|
|
65
|
-
|
|
66
|
-
throw new Error(`workflow-child outbound mail: upstream send failed for requestId ${requestId}: ${reason}`, { cause });
|
|
66
|
+
pending.discard(requestId);
|
|
67
|
+
throw pending.sendFailedError(requestId, cause);
|
|
67
68
|
}
|
|
68
|
-
return
|
|
69
|
+
return promise;
|
|
69
70
|
},
|
|
70
71
|
handleResult(data) {
|
|
71
|
-
const entry = pending.
|
|
72
|
+
const entry = pending.settle(data.requestId);
|
|
72
73
|
if (entry === undefined) {
|
|
73
74
|
logger.warn `outbound.result landed with no pending entry; requestId=${data.requestId} dropped`;
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
|
-
pending.delete(data.requestId);
|
|
77
77
|
if (data.result.ok) {
|
|
78
78
|
entry.resolve({
|
|
79
79
|
messageId: data.result.messageId,
|
|
@@ -81,13 +81,10 @@ export function createChildOutboundMailBridge(opts) {
|
|
|
81
81
|
});
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
|
-
entry.reject(
|
|
84
|
+
entry.reject(pending.rejectedError(data.requestId, data.result.reason));
|
|
85
85
|
},
|
|
86
86
|
cancelAll(reason) {
|
|
87
|
-
|
|
88
|
-
entry.reject(new Error(`workflow-child outbound mail (requestId=${requestId}) cancelled: ${reason}`));
|
|
89
|
-
}
|
|
90
|
-
pending.clear();
|
|
87
|
+
pending.cancelAll(reason);
|
|
91
88
|
},
|
|
92
89
|
};
|
|
93
90
|
}
|
|
@@ -114,6 +111,8 @@ function projectOutboundMessage(message) {
|
|
|
114
111
|
payload.summary = message.summary;
|
|
115
112
|
if (message.inReplyTo !== undefined)
|
|
116
113
|
payload.inReplyTo = message.inReplyTo;
|
|
114
|
+
if (message.references !== undefined)
|
|
115
|
+
payload.references = message.references;
|
|
117
116
|
if (message.correlationId !== undefined) {
|
|
118
117
|
payload.correlationId = message.correlationId;
|
|
119
118
|
}
|
|
@@ -125,19 +124,8 @@ function projectOutboundMessage(message) {
|
|
|
125
124
|
payload.attachments = message.attachments.map((a) => ({
|
|
126
125
|
name: a.name,
|
|
127
126
|
contentType: a.contentType,
|
|
128
|
-
dataBase64:
|
|
127
|
+
dataBase64: base64Encode(a.data),
|
|
129
128
|
}));
|
|
130
129
|
}
|
|
131
130
|
return payload;
|
|
132
131
|
}
|
|
133
|
-
function bytesToBase64(bytes) {
|
|
134
|
-
return base64Encode(bytes);
|
|
135
|
-
}
|
|
136
|
-
function defaultRequestIdAllocator() {
|
|
137
|
-
let counter = 0;
|
|
138
|
-
return () => {
|
|
139
|
-
counter += 1;
|
|
140
|
-
const rand = Math.random().toString(36).slice(2, 10);
|
|
141
|
-
return `om-${String(counter)}-${rand}`;
|
|
142
|
-
};
|
|
143
|
-
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A live pending entry as returned by {@link PendingRequestCore.get} and
|
|
3
|
+
* {@link PendingRequestCore.settle}. `meta` is the per-bridge payload the
|
|
4
|
+
* entry was registered with (e.g. the substrate-write request whose merge
|
|
5
|
+
* closure the merge round-trip invokes).
|
|
6
|
+
*/
|
|
7
|
+
export type PendingEntryHandle<Value, Meta> = {
|
|
8
|
+
meta: Meta;
|
|
9
|
+
resolve: (value: Value) => void;
|
|
10
|
+
reject: (err: Error) => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Options for {@link createPendingRequestCore}. `label` prefixes every
|
|
14
|
+
* error the core builds (`<label>: upstream send failed for requestId
|
|
15
|
+
* <id>: ...`, `<label> (requestId=<id>) rejected by supervisor: ...`,
|
|
16
|
+
* `<label> (requestId=<id>) cancelled: ...`, `<label>: no pending entry
|
|
17
|
+
* for requestId <id>`), so each bridge's observable error strings stay
|
|
18
|
+
* its own. `allocatorPrefix` seeds the default requestId allocator
|
|
19
|
+
* (`<prefix>-<counter>-<rand>`); tests inject `allocateRequestId` for a
|
|
20
|
+
* deterministic id.
|
|
21
|
+
*/
|
|
22
|
+
export type PendingRequestCoreOptions = {
|
|
23
|
+
label: string;
|
|
24
|
+
allocatorPrefix: string;
|
|
25
|
+
/** `undefined` is accepted explicitly so callers can forward an optional option verbatim. */
|
|
26
|
+
allocateRequestId?: (() => string) | undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The pending-request lifecycle surface a bridge composes into its own
|
|
30
|
+
* `submit` / `handle*` / `cancelAll` methods.
|
|
31
|
+
*/
|
|
32
|
+
export type PendingRequestCore<Value, Meta = undefined> = {
|
|
33
|
+
/** Number of registered-but-unsettled requests. */
|
|
34
|
+
readonly pendingCount: number;
|
|
35
|
+
/**
|
|
36
|
+
* Mint a requestId, register an awaiter keyed by it, and return the id
|
|
37
|
+
* plus the promise the bridge returns from its `submit`. The entry
|
|
38
|
+
* stays pending until `settle` or `cancelAll` acts on it, or `discard`
|
|
39
|
+
* removes it without settling (the upstream-send-failure path).
|
|
40
|
+
*/
|
|
41
|
+
register(meta: Meta): {
|
|
42
|
+
requestId: string;
|
|
43
|
+
promise: Promise<Value>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Remove a pending entry without settling its promise. Used when the
|
|
47
|
+
* upstream send itself fails, so the abandoned awaiter does not leak.
|
|
48
|
+
*/
|
|
49
|
+
discard(requestId: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Look up a pending entry WITHOUT removing it. Used by the
|
|
52
|
+
* substrate-write bridge's merge round-trip, which must reach the
|
|
53
|
+
* entry's merge closure while the entry stays alive for the terminal
|
|
54
|
+
* write response.
|
|
55
|
+
*/
|
|
56
|
+
get(requestId: string): PendingEntryHandle<Value, Meta> | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Look up a pending entry and remove it. Used by the response-frame
|
|
59
|
+
* handlers: the entry is gone before its promise resolves/rejects, so
|
|
60
|
+
* a later stale response or `cancelAll` finds nothing to act on.
|
|
61
|
+
*/
|
|
62
|
+
settle(requestId: string): PendingEntryHandle<Value, Meta> | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Reject every pending entry with `<label> (requestId=<id>) cancelled:
|
|
65
|
+
* <reason>` and clear the map. The control loop invokes this on any
|
|
66
|
+
* exit path so no awaiter leaks when the supervisor has torn the IPC
|
|
67
|
+
* down.
|
|
68
|
+
*/
|
|
69
|
+
cancelAll(reason: string): void;
|
|
70
|
+
/** `<label>: upstream send failed for requestId <id>: <cause message>` with `{ cause }`. */
|
|
71
|
+
sendFailedError(requestId: string, cause: unknown): Error;
|
|
72
|
+
/** `<label> (requestId=<id>) rejected by supervisor: <reason>`. */
|
|
73
|
+
rejectedError(requestId: string, reason: string): Error;
|
|
74
|
+
/** `<label>: no pending entry for requestId <id>` (substrate merge failure reply). */
|
|
75
|
+
noPendingError(requestId: string): Error;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Construct a pending-request core for one bridge. Pending entries live in
|
|
79
|
+
* a map keyed by `requestId`; the bridge's response handler settles the
|
|
80
|
+
* awaiter when the matching response frame lands.
|
|
81
|
+
*/
|
|
82
|
+
export declare function createPendingRequestCore<Value, Meta = undefined>(opts: PendingRequestCoreOptions): PendingRequestCore<Value, Meta>;
|
|
83
|
+
/**
|
|
84
|
+
* Default `requestId` allocator: a per-instance monotonic counter plus a
|
|
85
|
+
* random suffix, so ids are unique across bridge instances without any
|
|
86
|
+
* cross-instance coordination. The `prefix` names the owning bridge
|
|
87
|
+
* (`sw-`, `om-`, `mm-`) for triage in supervisor logs.
|
|
88
|
+
*/
|
|
89
|
+
export declare function defaultRequestIdAllocator(prefix: string): () => string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Generic pending-request core shared by the three child-side control-IPC
|
|
2
|
+
// bridges (substrate-write, outbound-mail, mailbox-mutation).
|
|
3
|
+
//
|
|
4
|
+
// Every bridge runs the same request/response round-trip over the control
|
|
5
|
+
// channel: `submit` mints a `requestId`, registers a pending awaiter keyed
|
|
6
|
+
// by that id, emits a request frame upstream, and resolves/rejects the
|
|
7
|
+
// awaiter when the supervisor's matching response frame lands. The three
|
|
8
|
+
// bridges differ only in the request payload they send, the value they
|
|
9
|
+
// resolve, the label their error messages carry, and (for the
|
|
10
|
+
// substrate-write bridge) an intermediate merge round-trip that must peek
|
|
11
|
+
// at the pending entry without settling it.
|
|
12
|
+
//
|
|
13
|
+
// This core owns that lifecycle in one place so the per-bridge files
|
|
14
|
+
// contain only their wire-specific parts. It never touches the control
|
|
15
|
+
// channel itself: each bridge performs its own `upstreamSender.send` so
|
|
16
|
+
// the frame payloads stay byte-identical to what the supervisor's
|
|
17
|
+
// `ControlPayload` validator expects.
|
|
18
|
+
/**
|
|
19
|
+
* Construct a pending-request core for one bridge. Pending entries live in
|
|
20
|
+
* a map keyed by `requestId`; the bridge's response handler settles the
|
|
21
|
+
* awaiter when the matching response frame lands.
|
|
22
|
+
*/
|
|
23
|
+
export function createPendingRequestCore(opts) {
|
|
24
|
+
const pending = new Map();
|
|
25
|
+
const allocate = opts.allocateRequestId ?? defaultRequestIdAllocator(opts.allocatorPrefix);
|
|
26
|
+
return {
|
|
27
|
+
get pendingCount() {
|
|
28
|
+
return pending.size;
|
|
29
|
+
},
|
|
30
|
+
register(meta) {
|
|
31
|
+
const requestId = allocate();
|
|
32
|
+
const promise = new Promise((resolve, reject) => {
|
|
33
|
+
pending.set(requestId, { meta, resolve, reject });
|
|
34
|
+
});
|
|
35
|
+
return { requestId, promise };
|
|
36
|
+
},
|
|
37
|
+
discard(requestId) {
|
|
38
|
+
pending.delete(requestId);
|
|
39
|
+
},
|
|
40
|
+
get(requestId) {
|
|
41
|
+
return pending.get(requestId);
|
|
42
|
+
},
|
|
43
|
+
settle(requestId) {
|
|
44
|
+
const entry = pending.get(requestId);
|
|
45
|
+
if (entry !== undefined)
|
|
46
|
+
pending.delete(requestId);
|
|
47
|
+
return entry;
|
|
48
|
+
},
|
|
49
|
+
cancelAll(reason) {
|
|
50
|
+
for (const [requestId, entry] of pending) {
|
|
51
|
+
entry.reject(new Error(`${opts.label} (requestId=${requestId}) cancelled: ${reason}`));
|
|
52
|
+
}
|
|
53
|
+
pending.clear();
|
|
54
|
+
},
|
|
55
|
+
sendFailedError(requestId, cause) {
|
|
56
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
57
|
+
return new Error(`${opts.label}: upstream send failed for requestId ${requestId}: ${reason}`, { cause });
|
|
58
|
+
},
|
|
59
|
+
rejectedError(requestId, reason) {
|
|
60
|
+
return new Error(`${opts.label} (requestId=${requestId}) rejected by supervisor: ${reason}`);
|
|
61
|
+
},
|
|
62
|
+
noPendingError(requestId) {
|
|
63
|
+
return new Error(`${opts.label}: no pending entry for requestId ${requestId}`);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Default `requestId` allocator: a per-instance monotonic counter plus a
|
|
69
|
+
* random suffix, so ids are unique across bridge instances without any
|
|
70
|
+
* cross-instance coordination. The `prefix` names the owning bridge
|
|
71
|
+
* (`sw-`, `om-`, `mm-`) for triage in supervisor logs.
|
|
72
|
+
*/
|
|
73
|
+
export function defaultRequestIdAllocator(prefix) {
|
|
74
|
+
let counter = 0;
|
|
75
|
+
return () => {
|
|
76
|
+
counter += 1;
|
|
77
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
78
|
+
return `${prefix}-${String(counter)}-${rand}`;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Principal, RepoId, RepoStore as SubstrateRepoStore } from "@intx/hub-sessions/substrate";
|
|
2
2
|
import type { AuthzCallResult } from "@intx/inference";
|
|
3
|
-
import type { RunResult, Scheduler, ReadParkedApprovalOps, StepInvokeRequest, StepInvokeResult, SpawnChildWorkflow, WorkflowAuthorizeFn, WorkflowPark } from "@intx/workflow";
|
|
3
|
+
import type { RunResult, Scheduler, ReadParkedApprovalOps, StepInvokeRequest, StepInvokeResult, SpawnChildWorkflow, WorkflowAuthorizeFn, WorkflowDefinition, WorkflowPark } from "@intx/workflow";
|
|
4
4
|
import { type WorkflowHostDrainController } from "../drain-controller.js";
|
|
5
|
-
import type { InferenceSource } from "@intx/types/runtime";
|
|
5
|
+
import type { InferenceSource, MailPartReader } from "@intx/types/runtime";
|
|
6
6
|
import type { CredentialDelivery } from "@intx/types/sidecar";
|
|
7
7
|
import type { RunSuspendableChild, RunChildWorkflow } from "../adapters/spawn-child.js";
|
|
8
8
|
import { type ControlChannelSender, type ControlPayload, type EventPayload, type FrameWriter, type NdjsonReader, type NdjsonWriter } from "../ipc/index.js";
|
|
@@ -11,6 +11,8 @@ import { hashGrants } from "../supervisor/credentials.js";
|
|
|
11
11
|
import type { SpawnTimeEnv } from "./env-bootstrap.js";
|
|
12
12
|
import { type LoadParkedApproval } from "./parked-correlations.js";
|
|
13
13
|
import type { ChildOutboundMailBridge } from "./outbound-mail-bridge.js";
|
|
14
|
+
import type { ChildMailboxMutationBridge } from "./mailbox-mutation-bridge.js";
|
|
15
|
+
import type { MailboxWatchRegistry } from "./mailbox-watch-registry.js";
|
|
14
16
|
import { type WarmAgentCache } from "./warm-agent-cache.js";
|
|
15
17
|
/**
|
|
16
18
|
* `WorkflowAuthorize` closure factory shape. The child's authorize
|
|
@@ -31,10 +33,12 @@ export type CredentialsSnapshotRef = {
|
|
|
31
33
|
};
|
|
32
34
|
/**
|
|
33
35
|
* The deployment's decrypted credential material and per-handle descriptors,
|
|
34
|
-
* held through a mutable reference
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* held through a mutable reference. A `credentials-updated` frame MERGES into
|
|
37
|
+
* this cell (materials by credentialId, bindings by (consumer, handle); an
|
|
38
|
+
* explicit `revoke` list drops entries) rather than replacing it wholesale,
|
|
39
|
+
* because the cell has several independently-scoped producers. The secret lives
|
|
40
|
+
* ONLY here -- read at tool-invoke time through the gated capability -- and is
|
|
41
|
+
* never copied into a snapshot, event, or state.
|
|
38
42
|
*/
|
|
39
43
|
export type CredentialMaterialRef = {
|
|
40
44
|
current: CredentialDelivery | null;
|
|
@@ -121,7 +125,7 @@ export interface CredentialWiring {
|
|
|
121
125
|
readonly materialRef: CredentialMaterialRef;
|
|
122
126
|
readonly resolveStepGrants: (stepId: string) => readonly unknown[];
|
|
123
127
|
}
|
|
124
|
-
export type ChildStepInvoker = (req: StepInvokeRequest, onEvent: (event: EventPayload) => void, authorize: WorkflowAuthorizeFn, warmCache: WarmAgentCache | undefined, sourcesRef: SourcesSnapshotRef, credentialWiring: CredentialWiring) => Promise<StepInvokeResult>;
|
|
128
|
+
export type ChildStepInvoker = (req: StepInvokeRequest, onEvent: (event: EventPayload) => void, authorize: WorkflowAuthorizeFn, warmCache: WarmAgentCache | undefined, sourcesRef: SourcesSnapshotRef, credentialWiring: CredentialWiring, mailPartReader: MailPartReader) => Promise<StepInvokeResult>;
|
|
125
129
|
/**
|
|
126
130
|
* Bindings the binary owns: per-deployment substrate identity,
|
|
127
131
|
* principal credentials, the runtime-supplied callbacks the
|
|
@@ -180,6 +184,24 @@ export interface RunWorkflowChildBindings {
|
|
|
180
184
|
* that runs no onTrigger section omits it.
|
|
181
185
|
*/
|
|
182
186
|
runSuspendableChild?: RunSuspendableChild;
|
|
187
|
+
/**
|
|
188
|
+
* Materialize a loop iteration run's own `runs/<childRunId>/grants.json`,
|
|
189
|
+
* inheriting the parent (container) run's grants capped to the loop body's
|
|
190
|
+
* declared resources. A loop iteration runs through the workflow-host-local
|
|
191
|
+
* loop executor (the inherited env), NOT the sidecar's `buildChildRunEnv`, so
|
|
192
|
+
* it is the one body birth path that does not otherwise write its own grants
|
|
193
|
+
* file -- and without it the body's `childWorkflow` grandchild spawn is
|
|
194
|
+
* refused as under-authorized. Wired by the sidecar (the grant-cap helpers
|
|
195
|
+
* live there); optional so a test or an in-process host that never spawns a
|
|
196
|
+
* grandchild from a loop body can omit it. `definition` MUST be the PRE-rewrite
|
|
197
|
+
* loop body (grandchild still inline) so the cap keeps the grandchild's
|
|
198
|
+
* declared resources.
|
|
199
|
+
*/
|
|
200
|
+
materializeLoopIterationGrants?: (args: {
|
|
201
|
+
parentRunId: string;
|
|
202
|
+
childRunId: string;
|
|
203
|
+
definition: WorkflowDefinition;
|
|
204
|
+
}) => Promise<void>;
|
|
183
205
|
/** Host-process scheduler singleton. The child consumes the same instance. */
|
|
184
206
|
scheduler: Scheduler;
|
|
185
207
|
/** Grant evaluator wired against the host's grant-rule grammar. */
|
|
@@ -225,6 +247,19 @@ export interface RunWorkflowChildBindings {
|
|
|
225
247
|
* invocation step settles as a terminal failure, the pre-recovery behavior.
|
|
226
248
|
*/
|
|
227
249
|
readParkedApprovalOps?: ReadParkedApprovalOps;
|
|
250
|
+
/**
|
|
251
|
+
* Mailbox watch registry backing the warm agent's `mail_wait` (INBOUND half
|
|
252
|
+
* of mailbox ownership, §3b). The host's substrate factory builds ONE
|
|
253
|
+
* instance at child boot, shares it with the step agent's supervisor-backed
|
|
254
|
+
* transport (whose `watch` registers callbacks into it), and exposes it here
|
|
255
|
+
* so the control loop routes each `mailbox.notify` frame to the same
|
|
256
|
+
* registry's `fire`. Optional: a deploy that wires no mail surface (and the
|
|
257
|
+
* recursive child-workflow adapter) omits it, and an inbound `mailbox.notify`
|
|
258
|
+
* frame is then logged and dropped. A test may instead inject a registry
|
|
259
|
+
* directly through `RunWorkflowChildOpts.mailboxWatchRegistry`, which takes
|
|
260
|
+
* precedence.
|
|
261
|
+
*/
|
|
262
|
+
mailboxWatchRegistry?: MailboxWatchRegistry;
|
|
228
263
|
/** Optional clock override; production wires `() => new Date()`. */
|
|
229
264
|
clock?: () => Date;
|
|
230
265
|
/** Optional id generator override; production wires a monotonic one. */
|
|
@@ -328,6 +363,33 @@ export interface RunWorkflowChildOpts {
|
|
|
328
363
|
* but no agent on the child side asked for an outbound send.
|
|
329
364
|
*/
|
|
330
365
|
outboundMailBridge?: ChildOutboundMailBridge;
|
|
366
|
+
/**
|
|
367
|
+
* Optional mailbox-mutation bridge (INBOUND half of mailbox ownership,
|
|
368
|
+
* §3b). The step agent's mail tools mutate the INBOX -- flag writes and
|
|
369
|
+
* `expunge` -- through a transport whose write methods route through
|
|
370
|
+
* this bridge: it emits a `mailbox.mutate.request` upstream control
|
|
371
|
+
* frame and resolves once the supervisor's matching
|
|
372
|
+
* `mailbox.mutate.response` lands. The control loop routes the
|
|
373
|
+
* downstream response frame to the bridge's `handleResult` and invokes
|
|
374
|
+
* `cancelAll` on any exit path so a pending mutation does not leak an
|
|
375
|
+
* awaiter after the supervisor tears the IPC down. When omitted,
|
|
376
|
+
* inbound `mailbox.mutate.response` frames are logged at warn-level and
|
|
377
|
+
* dropped -- the wire shape is well-formed but no agent on the child
|
|
378
|
+
* side asked for a mutation.
|
|
379
|
+
*/
|
|
380
|
+
mailboxMutationBridge?: ChildMailboxMutationBridge;
|
|
381
|
+
/**
|
|
382
|
+
* Optional mailbox watch registry (INBOUND half of mailbox ownership,
|
|
383
|
+
* design §3b). The supervisor -- the sole mail owner -- commits an arrived
|
|
384
|
+
* message to the workflow-run substrate mailbox and fires a `mailbox.notify`
|
|
385
|
+
* control frame; the control loop routes that frame to this registry's
|
|
386
|
+
* `fire`, which delivers a typed `exists` `MailboxEvent` to the callbacks the
|
|
387
|
+
* step agent's supervisor-backed transport registered through `watch`
|
|
388
|
+
* (backing `mail_wait`). When omitted, an inbound `mailbox.notify` frame is
|
|
389
|
+
* logged at warn-level and dropped -- the wire shape is well-formed but no
|
|
390
|
+
* watcher on the child side asked for inbound events.
|
|
391
|
+
*/
|
|
392
|
+
mailboxWatchRegistry?: MailboxWatchRegistry;
|
|
331
393
|
}
|
|
332
394
|
/**
|
|
333
395
|
* Narrow interface the child's control loop calls when downstream
|