@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
|
@@ -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
|
|
42
|
-
* when the supervisor's matching
|
|
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 =
|
|
53
|
-
|
|
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.
|
|
61
|
+
return pending.pendingCount;
|
|
57
62
|
},
|
|
58
63
|
async submit(req) {
|
|
59
|
-
const requestId =
|
|
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.
|
|
77
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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(
|
|
150
|
+
entry.reject(pending.rejectedError(data.requestId, data.result.reason));
|
|
151
151
|
},
|
|
152
152
|
cancelAll(reason) {
|
|
153
|
-
|
|
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,
|
|
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:
|
|
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
|
|
6
|
-
* is the agent's mail address; the
|
|
7
|
-
*
|
|
8
|
-
* host registered the agent's
|
|
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
|
-
// (
|
|
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
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
|
26
|
-
* is the agent's mail address; the
|
|
27
|
-
*
|
|
28
|
-
* host registered the agent's
|
|
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
|
|
32
|
-
throw new Error(`supervisor-backed transport: ${method} is not supported for unified-host step agent ${address}; the supervisor owns the mailbox and
|
|
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
|
|
87
|
+
return unsupported("append");
|
|
43
88
|
},
|
|
44
89
|
async listMailboxes(_signal) {
|
|
45
|
-
return
|
|
90
|
+
return unsupported("listMailboxes");
|
|
46
91
|
},
|
|
47
92
|
async createMailbox(_name, _signal) {
|
|
48
|
-
return
|
|
93
|
+
return unsupported("createMailbox");
|
|
49
94
|
},
|
|
50
95
|
async deleteMailbox(_name, _signal) {
|
|
51
|
-
return
|
|
96
|
+
return unsupported("deleteMailbox");
|
|
52
97
|
},
|
|
53
|
-
async getMailboxStatus(
|
|
54
|
-
|
|
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(
|
|
57
|
-
|
|
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(
|
|
60
|
-
|
|
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(
|
|
63
|
-
|
|
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(
|
|
66
|
-
|
|
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(
|
|
69
|
-
|
|
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(
|
|
72
|
-
|
|
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(
|
|
75
|
-
|
|
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(
|
|
78
|
-
|
|
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
|
|
175
|
+
return unsupported("move");
|
|
82
176
|
},
|
|
83
177
|
async copy(_ref, _toMailbox, _signal) {
|
|
84
|
-
return
|
|
178
|
+
return unsupported("copy");
|
|
85
179
|
},
|
|
86
|
-
async expunge(
|
|
87
|
-
|
|
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(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
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(
|
|
98
|
-
|
|
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
|
|
244
|
+
return unsupported("createList");
|
|
102
245
|
},
|
|
103
246
|
async listMembers(_address, _signal) {
|
|
104
|
-
return
|
|
247
|
+
return unsupported("listMembers");
|
|
105
248
|
},
|
|
106
249
|
async subscribe(_listAddress, _subscriberAddress, _signal) {
|
|
107
|
-
return
|
|
250
|
+
return unsupported("subscribe");
|
|
108
251
|
},
|
|
109
252
|
async unsubscribe(_listAddress, _subscriberAddress, _signal) {
|
|
110
|
-
return
|
|
253
|
+
return unsupported("unsubscribe");
|
|
111
254
|
},
|
|
112
255
|
};
|
|
113
256
|
}
|
|
@@ -11,6 +11,35 @@ import type { InferenceEvent, InferenceSource } from "@intx/types/runtime";
|
|
|
11
11
|
export interface WarmEventSinkRef {
|
|
12
12
|
current: ((event: InferenceEvent) => void) | null;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Per-turn settle barrier the connector reply drain exposes to the warm
|
|
16
|
+
* step (design §3c durability). The step snapshots `replySeq()` before its
|
|
17
|
+
* `agent.send` and, for a turn that produced a reply, awaits
|
|
18
|
+
* `waitForReplyAfter(snapshot)` so the run parks -- and the supervisor
|
|
19
|
+
* consumes the inbound mail -- only after the reply is durably sent. This is
|
|
20
|
+
* the structural subset of the harness `ConnectorReplyDrain` the warm path
|
|
21
|
+
* needs; declaring it here keeps the workflow-host package independent of
|
|
22
|
+
* `@intx/harness`, while the drain the sidecar builds satisfies it.
|
|
23
|
+
*/
|
|
24
|
+
export type WarmReplySettlement = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
readonly ok: false;
|
|
28
|
+
readonly cause: unknown;
|
|
29
|
+
};
|
|
30
|
+
export interface WarmReplyDrive {
|
|
31
|
+
/** Settles when the drain loop exits at eviction. Folded into `eventForward`. */
|
|
32
|
+
readonly done: Promise<void>;
|
|
33
|
+
/** Monotonic count of replies that have settled (sent-and-acked or failed). */
|
|
34
|
+
replySeq(): number;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve once the reply at index `n` has settled, with its outcome. A
|
|
37
|
+
* failure outcome (or a drain that tears down before reply `n` arrives)
|
|
38
|
+
* carries `ok: false` so the caller fails the turn rather than treating the
|
|
39
|
+
* reply as sent.
|
|
40
|
+
*/
|
|
41
|
+
waitForReplyAfter(n: number): Promise<WarmReplySettlement>;
|
|
42
|
+
}
|
|
14
43
|
/**
|
|
15
44
|
* Per-address warm-agent cache. Keyed by the step's stable identity (the
|
|
16
45
|
* single step's id), so a long-lived agent resolves to the same entry on
|
|
@@ -28,11 +57,22 @@ export interface WarmAgentCache {
|
|
|
28
57
|
/**
|
|
29
58
|
* Cache a freshly-built warm agent under `key`. The `eventSinkRef` is
|
|
30
59
|
* the mutable sink the agent's stream forwarder reads; `eventForward`
|
|
31
|
-
* is the forwarder loop's settle promise.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
60
|
+
* is the forwarder loop's settle promise. `replyDrive` is the connector
|
|
61
|
+
* reply drain's per-turn barrier, or `null` when the deployment drives no
|
|
62
|
+
* threaded replies. Throws if an entry already exists for `key` -- a
|
|
63
|
+
* double-build is a step-invoker bug, not a silent overwrite that would
|
|
64
|
+
* leak the prior agent's LSP subprocess.
|
|
65
|
+
*/
|
|
66
|
+
store(key: string, agent: Agent, eventSinkRef: WarmEventSinkRef, eventForward: Promise<void>, replyDrive: WarmReplyDrive | null): void;
|
|
67
|
+
/**
|
|
68
|
+
* Return the connector reply drain's per-turn barrier cached for `key`, or
|
|
69
|
+
* `null` when the deployment drives no threaded replies. The step-invoker
|
|
70
|
+
* fetches it on every message -- the drain is established once at the
|
|
71
|
+
* first-message build but each message's send must snapshot and await it.
|
|
72
|
+
* Throws when no entry exists for `key`: a barrier fetch before the warm
|
|
73
|
+
* agent is stored is a step-invoker sequencing bug.
|
|
34
74
|
*/
|
|
35
|
-
|
|
75
|
+
getReplyDrive(key: string): WarmReplyDrive | null;
|
|
36
76
|
/**
|
|
37
77
|
* Point the warm agent's stream forwarder at the active step's event
|
|
38
78
|
* sink before its `agent.send`. Throws when no entry exists for
|