@nylorun/runtime 0.9.0-beta → 0.11.0-beta

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 (192) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +110 -37
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/media.d.ts +2 -2
  6. package/dist/adapters/media.js +2 -2
  7. package/dist/adapters/observe.d.ts +2 -1
  8. package/dist/adapters/observe.js +2 -3
  9. package/dist/adapters/sandbox/virtual.js +1 -1
  10. package/dist/adapters/streams/s2.d.ts +18 -0
  11. package/dist/adapters/streams/s2.js +361 -0
  12. package/dist/assets.d.ts +2 -2
  13. package/dist/assets.js +5 -3
  14. package/dist/core/executors.d.ts +15 -3
  15. package/dist/core/executors.js +40 -5
  16. package/dist/core/flow-host.d.ts +225 -0
  17. package/dist/core/flow-host.js +518 -0
  18. package/dist/core/limits.d.ts +40 -0
  19. package/dist/core/limits.js +76 -0
  20. package/dist/core/runtime.d.ts +5 -90
  21. package/dist/core/runtime.js +3 -1447
  22. package/dist/core/sandbox-routes.d.ts +40 -0
  23. package/dist/core/sandbox-routes.js +106 -0
  24. package/dist/core/turn-manifest.d.ts +50 -0
  25. package/dist/core/turn-manifest.js +66 -0
  26. package/dist/execution/memory.d.ts +54 -0
  27. package/dist/execution/memory.js +217 -0
  28. package/dist/execution/types.d.ts +125 -0
  29. package/dist/execution/types.js +52 -0
  30. package/dist/host/config-for.d.ts +17 -0
  31. package/dist/host/config-for.js +27 -0
  32. package/dist/host/config.d.ts +16 -0
  33. package/dist/host/create-host.d.ts +56 -0
  34. package/dist/host/create-host.js +420 -0
  35. package/dist/host/environment.d.ts +13 -0
  36. package/dist/host/environment.js +53 -0
  37. package/dist/host/execution.d.ts +61 -0
  38. package/dist/host/execution.js +69 -0
  39. package/dist/host/http.d.ts +39 -0
  40. package/dist/host/http.js +142 -0
  41. package/dist/host/index.d.ts +8 -0
  42. package/dist/host/index.js +7 -0
  43. package/dist/host/logger.d.ts +3 -0
  44. package/dist/host/logger.js +18 -0
  45. package/dist/host/main.d.ts +3 -0
  46. package/dist/host/main.js +201 -0
  47. package/dist/host/stack-config.d.ts +72 -0
  48. package/dist/host/stack-config.js +193 -0
  49. package/dist/index.d.ts +7 -2
  50. package/dist/index.js +4 -2
  51. package/dist/infra/database.d.ts +16 -0
  52. package/dist/infra/database.js +31 -0
  53. package/dist/infra/execution.d.ts +39 -0
  54. package/dist/infra/execution.js +68 -0
  55. package/dist/infra/index.d.ts +42 -0
  56. package/dist/infra/index.js +56 -0
  57. package/dist/infra/readiness.d.ts +33 -0
  58. package/dist/infra/readiness.js +53 -0
  59. package/dist/infra/streams.d.ts +17 -0
  60. package/dist/infra/streams.js +33 -0
  61. package/dist/infra/workers.d.ts +39 -0
  62. package/dist/infra/workers.js +31 -0
  63. package/dist/mcp/connect.d.ts +1 -0
  64. package/dist/mcp/connect.js +2 -0
  65. package/dist/mcp/pool.d.ts +3 -1
  66. package/dist/mcp/pool.js +2 -1
  67. package/dist/mcp/stdio.d.ts +2 -0
  68. package/dist/mcp/stdio.js +3 -1
  69. package/dist/model/auth-store.d.ts +1 -1
  70. package/dist/model/auth-store.js +2 -2
  71. package/dist/model/models.d.ts +4 -0
  72. package/dist/model/models.js +22 -26
  73. package/dist/model/pi-model.d.ts +2 -2
  74. package/dist/model/pi-model.js +6 -6
  75. package/dist/model/settings.d.ts +5 -1
  76. package/dist/model/settings.js +6 -2
  77. package/dist/sandbox/manager.d.ts +26 -8
  78. package/dist/sandbox/manager.js +95 -47
  79. package/dist/sandbox/select.d.ts +5 -4
  80. package/dist/sandbox/select.js +18 -13
  81. package/dist/sandbox/share.d.ts +23 -0
  82. package/dist/sandbox/share.js +58 -0
  83. package/dist/sandbox/types.d.ts +2 -2
  84. package/dist/store/canonical.d.ts +5 -0
  85. package/dist/store/canonical.js +17 -0
  86. package/dist/store/cursor.d.ts +9 -0
  87. package/dist/store/cursor.js +18 -0
  88. package/dist/store/memory.d.ts +57 -0
  89. package/dist/store/memory.js +643 -0
  90. package/dist/store/ownership.d.ts +19 -0
  91. package/dist/store/ownership.js +33 -0
  92. package/dist/store/postgres/connect.d.ts +25 -0
  93. package/dist/store/postgres/connect.js +25 -0
  94. package/dist/store/postgres/locking.d.ts +16 -0
  95. package/dist/store/postgres/locking.js +22 -0
  96. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  97. package/dist/store/postgres/migrations/001_initial.js +196 -0
  98. package/dist/store/postgres/migrations/index.d.ts +49 -0
  99. package/dist/store/postgres/migrations/index.js +75 -0
  100. package/dist/store/postgres/names.d.ts +11 -0
  101. package/dist/store/postgres/names.js +42 -0
  102. package/dist/store/postgres/store.d.ts +11 -0
  103. package/dist/store/postgres/store.js +790 -0
  104. package/dist/store/postgres/tenants.d.ts +90 -0
  105. package/dist/store/postgres/tenants.js +207 -0
  106. package/dist/store/types.d.ts +440 -0
  107. package/dist/store/types.js +10 -0
  108. package/dist/streams/basin.d.ts +23 -0
  109. package/dist/streams/basin.js +51 -0
  110. package/dist/streams/memory.d.ts +25 -0
  111. package/dist/streams/memory.js +124 -0
  112. package/dist/streams/relay.d.ts +37 -0
  113. package/dist/streams/relay.js +254 -0
  114. package/dist/streams/types.d.ts +115 -0
  115. package/dist/streams/types.js +86 -0
  116. package/dist/tenant/actions.d.ts +153 -0
  117. package/dist/tenant/actions.js +191 -0
  118. package/dist/tenant/advance.d.ts +15 -0
  119. package/dist/tenant/advance.js +439 -0
  120. package/dist/tenant/auth.d.ts +11 -0
  121. package/dist/tenant/auth.js +34 -0
  122. package/dist/tenant/commands.d.ts +3 -0
  123. package/dist/tenant/commands.js +384 -0
  124. package/dist/tenant/context.d.ts +140 -0
  125. package/dist/tenant/context.js +40 -0
  126. package/dist/tenant/effects.d.ts +20 -0
  127. package/dist/tenant/effects.js +539 -0
  128. package/dist/tenant/envelope.d.ts +7 -0
  129. package/dist/tenant/envelope.js +22 -0
  130. package/dist/tenant/ephemeral.d.ts +42 -0
  131. package/dist/tenant/ephemeral.js +224 -0
  132. package/dist/tenant/http.d.ts +32 -0
  133. package/dist/tenant/http.js +51 -0
  134. package/dist/tenant/live.d.ts +92 -0
  135. package/dist/tenant/live.js +290 -0
  136. package/dist/tenant/logger.d.ts +9 -0
  137. package/dist/tenant/logger.js +25 -0
  138. package/dist/tenant/model-setting.d.ts +16 -0
  139. package/dist/tenant/model-setting.js +12 -0
  140. package/dist/tenant/module.d.ts +38 -0
  141. package/dist/tenant/module.js +319 -0
  142. package/dist/tenant/paths.d.ts +28 -0
  143. package/dist/tenant/paths.js +91 -0
  144. package/dist/tenant/pool.d.ts +10 -0
  145. package/dist/tenant/pool.js +54 -0
  146. package/dist/tenant/principals.d.ts +13 -0
  147. package/dist/tenant/principals.js +37 -0
  148. package/dist/tenant/quarantine-error.d.ts +6 -0
  149. package/dist/tenant/quarantine-error.js +9 -0
  150. package/dist/tenant/quarantine.d.ts +27 -0
  151. package/dist/tenant/quarantine.js +72 -0
  152. package/dist/tenant/reset.d.ts +22 -0
  153. package/dist/tenant/reset.js +81 -0
  154. package/dist/tenant/routes-tenant.d.ts +12 -0
  155. package/dist/tenant/routes-tenant.js +126 -0
  156. package/dist/tenant/routes.d.ts +9 -0
  157. package/dist/tenant/routes.js +142 -0
  158. package/dist/tenant/runtime.d.ts +81 -0
  159. package/dist/tenant/runtime.js +298 -0
  160. package/dist/tenant/scheduler.d.ts +25 -0
  161. package/dist/tenant/scheduler.js +84 -0
  162. package/dist/tenant/session.d.ts +60 -0
  163. package/dist/tenant/session.js +94 -0
  164. package/dist/tenant/sessions.d.ts +34 -0
  165. package/dist/tenant/sessions.js +149 -0
  166. package/dist/tenant/status.d.ts +37 -0
  167. package/dist/tenant/status.js +130 -0
  168. package/dist/tenant/store-memory.d.ts +16 -0
  169. package/dist/tenant/store-memory.js +111 -0
  170. package/dist/tenant/store-pg.d.ts +15 -0
  171. package/dist/tenant/store-pg.js +140 -0
  172. package/dist/tenant/streams.d.ts +126 -0
  173. package/dist/tenant/streams.js +419 -0
  174. package/dist/tenant/sweep.d.ts +15 -0
  175. package/dist/tenant/sweep.js +122 -0
  176. package/dist/tenant/types.d.ts +205 -0
  177. package/dist/tenant/types.js +23 -0
  178. package/dist/tenant/worker.d.ts +84 -0
  179. package/dist/tenant/worker.js +189 -0
  180. package/dist/vault/kek.d.ts +5 -2
  181. package/dist/vault/kek.js +7 -10
  182. package/dist/vault/service.d.ts +37 -31
  183. package/dist/vault/service.js +439 -359
  184. package/dist/version.d.ts +1 -1
  185. package/dist/version.js +1 -1
  186. package/package.json +14 -11
  187. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  188. package/dist/adapters/sandbox/microsandbox.js +0 -238
  189. package/dist/core/main.js +0 -35
  190. package/dist/core/store.d.ts +0 -30
  191. package/dist/core/store.js +0 -202
  192. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -0,0 +1,37 @@
1
+ import type { SessionStore } from "../store/types.js";
2
+ import { type DurableStreams } from "./types.js";
3
+ /** A session's outbox starts past the end of its stream: events are missing from the stream. */
4
+ export declare class StreamGapError extends Error {
5
+ constructor(sessionId: string, first: number, tail: number);
6
+ }
7
+ export interface RelayOptions {
8
+ store: SessionStore;
9
+ streams: DurableStreams;
10
+ /** The Tenant whose basin receives the events; must match `store.tenantId`. */
11
+ tenantId: string;
12
+ /** Receives append and delete failures. The rows they concern stay in the outbox. */
13
+ onError?: (error: unknown) => void;
14
+ }
15
+ export interface Relay {
16
+ /**
17
+ * Appends leftover outbox rows (at most `limit`, default 1000) per session in
18
+ * sequence order: the Tenant sweep's recovery after a crash or an S2 outage.
19
+ * Resolves with the number of rows now in S2 and removed from the outbox.
20
+ * Per-session failures go to `onError`; those rows stay.
21
+ */
22
+ drain(limit?: number): Promise<number>;
23
+ /** Resolves when every relay started so far has settled. */
24
+ idle(): Promise<void>;
25
+ /** Stops listening to commits and waits for in-flight relays. */
26
+ close(): Promise<void>;
27
+ }
28
+ export declare function createRelay(options: RelayOptions): Relay;
29
+ /** Appends a `work_available` signal to `tenant/work`. Not relayed through the outbox. */
30
+ export declare function signalWork(streams: DurableStreams, tenantId: string): Promise<void>;
31
+ /** Appends a `session.cancel` signal to `tenant/control`. Not relayed through the outbox. */
32
+ export declare function signalCancel(streams: DurableStreams, tenantId: string, sessionId: string): Promise<void>;
33
+ /**
34
+ * Appends a `sessions.reset` signal to `tenant/control`: every process with the Tenant open
35
+ * checks its session feeds against the Session Store.
36
+ */
37
+ export declare function signalSessionsReset(streams: DurableStreams, tenantId: string): Promise<void>;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The outbox relay (architecture §12.4, "Writing: outbox and relay").
3
+ *
4
+ * The Session Store writes every event to its outbox in the same transaction
5
+ * as the state change. After the commit the relay appends the events to the
6
+ * stream of their session's incarnation (`sessions/<id>/<incarnation>`) with
7
+ * `matchSeq` set to each batch's first sequence, and deletes the rows S2 has:
8
+ *
9
+ * - **The right incarnation.** A committed event comes with the incarnation
10
+ * its session had when the event was allocated (`Commit.incarnations`).
11
+ * Leftover rows (`drain`, and a commit that finds earlier rows still
12
+ * waiting) are read with the session's incarnation in one transaction that
13
+ * locks the session; deleting a session deletes its outbox rows in the same
14
+ * transaction, so those rows belong to the incarnation read. Either way a
15
+ * session deleted and created again with the same id never gets the old
16
+ * incarnation's events, or the reverse.
17
+ * - **ok**: the rows are deleted through the last appended sequence, only
18
+ * while the session still has the incarnation appended to (one statement,
19
+ * `deleteOutbox` with `incarnation`).
20
+ * - **`seq_mismatch` with the tail past the batch start**: an earlier attempt
21
+ * (this process or another) already appended those events. The rows below
22
+ * the tail are deleted, and whatever is left of the batch is appended from
23
+ * the tail.
24
+ * - **`seq_mismatch` with the tail before the batch start**: earlier events of
25
+ * the session are still in the outbox (an earlier append failed), and the
26
+ * session's outbox is relayed in sequence order, which includes this batch.
27
+ * If the outbox's first row is past the tail, events are missing from the
28
+ * stream: the rows stay and the error goes to `onError`.
29
+ * - **append throws** (S2 unreachable, basin missing): the rows stay in the
30
+ * outbox and the error goes to `onError`. The next commit of the session, or
31
+ * the Tenant sweep's `drain`, appends them in order.
32
+ *
33
+ * Conditional appends make this exactly-once and in order per session however
34
+ * many processes relay the same rows. Within one process the relay also runs
35
+ * one relay at a time per session.
36
+ *
37
+ * `work_available`, `session.cancel` and `sessions.reset` are signals, not
38
+ * canonical events: they skip the outbox (`signalWork`, `signalCancel`,
39
+ * `signalSessionsReset`). A commit that called `t.signalWork()` is signalled
40
+ * by the relay after its events.
41
+ */
42
+ import { decodeCursor } from "../store/cursor.js";
43
+ import { CONTROL_STREAM, WORK_AVAILABLE, WORK_STREAM, streamOfSession, } from "./types.js";
44
+ /** A session's outbox starts past the end of its stream: events are missing from the stream. */
45
+ export class StreamGapError extends Error {
46
+ constructor(sessionId, first, tail) {
47
+ super(`Outbox of session ${sessionId} starts at ${first}, past the end of its stream at ${tail}; events before it are missing`);
48
+ this.name = "StreamGapError";
49
+ }
50
+ }
51
+ /** At most this many records per append (S2 allows 1000). */
52
+ const MAX_BATCH_RECORDS = 500;
53
+ /** At most this many bytes of JSON per append (S2 allows 1 MiB metered). */
54
+ const MAX_BATCH_BYTES = 768 * 1024;
55
+ /** Outbox rows read per page while relaying one session. */
56
+ const DRAIN_PAGE = 1000;
57
+ export function createRelay(options) {
58
+ const { store, streams, tenantId } = options;
59
+ if (store.tenantId !== tenantId)
60
+ throw new Error(`Relay tenant ${tenantId} does not match store tenant ${store.tenantId}`);
61
+ const onError = options.onError ?? (() => { });
62
+ /** sessionId → tail of that session's relay chain. */
63
+ const chains = new Map();
64
+ const pending = new Set();
65
+ let closed = false;
66
+ /** Runs `task` after every earlier task of the same session. */
67
+ function serialize(sessionId, task) {
68
+ const previous = chains.get(sessionId) ?? Promise.resolve();
69
+ const run = previous.then(task, task);
70
+ const settled = run.then(() => undefined, () => undefined);
71
+ chains.set(sessionId, settled);
72
+ pending.add(settled);
73
+ void settled.then(() => {
74
+ pending.delete(settled);
75
+ if (chains.get(sessionId) === settled)
76
+ chains.delete(sessionId);
77
+ });
78
+ return run;
79
+ }
80
+ /**
81
+ * Appends `rows` (one session, ascending, contiguous) to the stream of `incarnation`
82
+ * with `matchSeq`, and deletes what S2 has while the session still has that incarnation.
83
+ * Throws a `StreamGapError` when the stream's tail is before the first row still to append.
84
+ */
85
+ async function appendRows(sessionId, incarnation, rows) {
86
+ const stream = streamOfSession({
87
+ id: sessionId,
88
+ ...(incarnation === null ? {} : { streamIncarnation: incarnation }),
89
+ });
90
+ const deleteThrough = (seq) => store.tx((t) => t.deleteOutbox(sessionId, seq, incarnation));
91
+ let rest = rows;
92
+ while (rest.length > 0) {
93
+ const batch = takeBatch(rest);
94
+ const first = batch[0].seq;
95
+ const last = batch[batch.length - 1].seq;
96
+ const result = await streams.append(tenantId, stream, batch.map((row) => row.event), { matchSeq: first });
97
+ if (result.status === "ok") {
98
+ await deleteThrough(last);
99
+ rest = rest.slice(batch.length);
100
+ }
101
+ else if (result.tail > first) {
102
+ // Already appended by an earlier attempt: S2 has everything below the tail.
103
+ await deleteThrough(result.tail - 1);
104
+ rest = rest.filter((row) => row.seq >= result.tail);
105
+ }
106
+ else {
107
+ throw new StreamGapError(sessionId, first, result.tail);
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * Appends the session's outbox rows in sequence order, at most `limit`, reading them with
113
+ * the session's incarnation under its lock. Resolves with the number relayed.
114
+ */
115
+ async function relaySession(sessionId, limit = Infinity) {
116
+ let relayed = 0;
117
+ while (relayed < limit) {
118
+ const page = Math.min(DRAIN_PAGE, limit - relayed);
119
+ const read = await store.tx(async (t) => {
120
+ const session = await t.lockSession(sessionId);
121
+ // A deleted session took its outbox rows with it.
122
+ if (!session)
123
+ return undefined;
124
+ return {
125
+ incarnation: session.streamIncarnation ?? null,
126
+ rows: await t.outbox(page, { sessionId }),
127
+ };
128
+ });
129
+ if (!read || read.rows.length === 0)
130
+ return relayed;
131
+ const run = contiguous(read.rows);
132
+ await appendRows(sessionId, read.incarnation, run);
133
+ relayed += run.length;
134
+ // A gap inside the outbox: the next page starts at it and reports it.
135
+ if (read.rows.length < page && run.length === read.rows.length)
136
+ return relayed;
137
+ }
138
+ return relayed;
139
+ }
140
+ /**
141
+ * Appends one commit's rows of a session to the stream of the incarnation they were
142
+ * written under. When earlier rows are still in the outbox, relays the whole outbox.
143
+ */
144
+ async function relayCommitted(sessionId, incarnation, rows) {
145
+ try {
146
+ await appendRows(sessionId, incarnation, contiguous(rows));
147
+ }
148
+ catch (error) {
149
+ if (!(error instanceof StreamGapError))
150
+ throw error;
151
+ await relaySession(sessionId);
152
+ }
153
+ }
154
+ function onCommit(commit) {
155
+ if (closed)
156
+ return;
157
+ /** `sessionId\0incarnation` → the commit's rows of that incarnation. */
158
+ const groups = new Map();
159
+ commit.events.forEach((event, i) => {
160
+ let seq;
161
+ try {
162
+ seq = decodeCursor(event.sessionId, event.cursor);
163
+ }
164
+ catch {
165
+ onError(new Error(`Event ${event.eventId} has an invalid cursor; left for the sweep`));
166
+ return;
167
+ }
168
+ const incarnation = commit.incarnations[i] ?? null;
169
+ const key = `${event.sessionId}\u0000${incarnation ?? ""}`;
170
+ let group = groups.get(key);
171
+ if (!group)
172
+ groups.set(key, (group = { sessionId: event.sessionId, incarnation, rows: [] }));
173
+ group.rows.push({ sessionId: event.sessionId, seq, event });
174
+ });
175
+ const relays = [...groups.values()].map(({ sessionId, incarnation, rows }) => {
176
+ rows.sort((a, b) => a.seq - b.seq);
177
+ return serialize(sessionId, () => relayCommitted(sessionId, incarnation, rows)).catch(onError);
178
+ });
179
+ if (commit.workAvailable) {
180
+ const signalled = Promise.all(relays)
181
+ .then(() => signalWork(streams, tenantId))
182
+ .catch(onError);
183
+ pending.add(signalled);
184
+ void signalled.then(() => pending.delete(signalled));
185
+ }
186
+ }
187
+ const unsubscribe = store.onCommit(onCommit);
188
+ async function idle() {
189
+ while (pending.size > 0)
190
+ await Promise.all([...pending]);
191
+ }
192
+ return {
193
+ async drain(limit = 1000) {
194
+ const rows = await store.tx((t) => t.outbox(limit));
195
+ const bySession = new Map();
196
+ for (const row of rows)
197
+ bySession.set(row.sessionId, (bySession.get(row.sessionId) ?? 0) + 1);
198
+ const counts = await Promise.all([...bySession].map(([sessionId, count]) =>
199
+ // Rows may have been relayed since they were read; the conditional
200
+ // appends skip those.
201
+ serialize(sessionId, () => relaySession(sessionId, count)).catch((error) => {
202
+ onError(error);
203
+ return 0;
204
+ })));
205
+ return counts.reduce((sum, n) => sum + n, 0);
206
+ },
207
+ idle,
208
+ async close() {
209
+ closed = true;
210
+ unsubscribe();
211
+ await idle();
212
+ },
213
+ };
214
+ }
215
+ /** Appends a `work_available` signal to `tenant/work`. Not relayed through the outbox. */
216
+ export async function signalWork(streams, tenantId) {
217
+ await streams.append(tenantId, WORK_STREAM, [WORK_AVAILABLE]);
218
+ }
219
+ /** Appends a `session.cancel` signal to `tenant/control`. Not relayed through the outbox. */
220
+ export async function signalCancel(streams, tenantId, sessionId) {
221
+ if (!sessionId)
222
+ throw new Error("sessionId is required");
223
+ const signal = { type: "session.cancel", sessionId };
224
+ await streams.append(tenantId, CONTROL_STREAM, [signal]);
225
+ }
226
+ /**
227
+ * Appends a `sessions.reset` signal to `tenant/control`: every process with the Tenant open
228
+ * checks its session feeds against the Session Store.
229
+ */
230
+ export async function signalSessionsReset(streams, tenantId) {
231
+ const signal = { type: "sessions.reset" };
232
+ await streams.append(tenantId, CONTROL_STREAM, [signal]);
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ /** The leading run of `rows` (ascending) without a sequence gap. */
236
+ function contiguous(rows) {
237
+ let n = 1;
238
+ while (n < rows.length && rows[n].seq === rows[n - 1].seq + 1)
239
+ n += 1;
240
+ return rows.slice(0, n);
241
+ }
242
+ /** The leading rows that fit one append. Always at least one row. */
243
+ function takeBatch(rows) {
244
+ let bytes = 0;
245
+ let n = 0;
246
+ while (n < rows.length && n < MAX_BATCH_RECORDS) {
247
+ const size = JSON.stringify(rows[n].event).length + 8;
248
+ if (n > 0 && bytes + size > MAX_BATCH_BYTES)
249
+ break;
250
+ bytes += size;
251
+ n += 1;
252
+ }
253
+ return rows.slice(0, n);
254
+ }
@@ -0,0 +1,115 @@
1
+ /** A record as read from a stream. */
2
+ export interface StreamRecord<T = unknown> {
3
+ seq: number;
4
+ /** Milliseconds since the epoch, as assigned by the stream at append. */
5
+ timestamp: number;
6
+ body: T;
7
+ }
8
+ export interface AppendOptions {
9
+ /** Append only when the stream's tail equals this sequence. */
10
+ matchSeq?: number;
11
+ }
12
+ export interface AppendAck {
13
+ status: "ok";
14
+ /** Sequence of the first appended record. */
15
+ start: number;
16
+ /** Sequence after the last appended record (the new tail). */
17
+ end: number;
18
+ }
19
+ /** A conditional append found the stream elsewhere. Nothing was written. */
20
+ export interface SeqMismatch {
21
+ status: "seq_mismatch";
22
+ /** The stream's current tail. */
23
+ tail: number;
24
+ }
25
+ export type AppendResult = AppendAck | SeqMismatch;
26
+ export interface ReadOptions {
27
+ /** Ends the read. Iteration then finishes without throwing. */
28
+ signal?: AbortSignal;
29
+ /**
30
+ * `true` (default) keeps reading live records after the history. `false`
31
+ * stops at the tail observed when the read started (history requests).
32
+ */
33
+ follow?: boolean;
34
+ }
35
+ export interface DurableStreams {
36
+ /**
37
+ * Appends `records` as one batch. Rejects when the Tenant has no basin or
38
+ * `records` is empty.
39
+ */
40
+ append(tenantId: string, stream: string, records: readonly unknown[], options?: AppendOptions): Promise<AppendResult>;
41
+ /**
42
+ * Reads `stream` from `fromSeq`: history first, then (with `follow`) live
43
+ * records as they are appended, until `signal` aborts. Reading a stream that
44
+ * does not exist yet waits for its first record.
45
+ */
46
+ read<T = unknown>(tenantId: string, stream: string, fromSeq: number, options?: ReadOptions): AsyncIterable<StreamRecord<T>>;
47
+ /** The sequence the next record will get; 0 for a missing stream. */
48
+ tail(tenantId: string, stream: string): Promise<number>;
49
+ /** Creates the Tenant's basin. Idempotent. */
50
+ ensureTenant(tenantId: string): Promise<void>;
51
+ /** Deletes the Tenant's basin and all its streams. Idempotent. */
52
+ deleteTenant(tenantId: string): Promise<void>;
53
+ /** Deletes one stream (an abandoned session incarnation). Idempotent. */
54
+ deleteStream(tenantId: string, stream: string): Promise<void>;
55
+ /**
56
+ * Names of the Tenant's streams starting with `prefix`, in name order,
57
+ * without streams being deleted. Empty when the Tenant has no basin.
58
+ */
59
+ listStreams(tenantId: string, prefix: string): Promise<string[]>;
60
+ close(): Promise<void>;
61
+ /**
62
+ * Resolves when the backing service answers, rejects otherwise (readiness,
63
+ * `infra/streams.ts`). Absent for in-process implementations, which are
64
+ * always reachable.
65
+ */
66
+ probe?(signal: AbortSignal): Promise<void>;
67
+ }
68
+ export declare const SESSION_STREAM_PREFIX = "sessions/";
69
+ /** `work_available` signals for executor connections. */
70
+ export declare const WORK_STREAM = "tenant/work";
71
+ /** `session.cancel` and `sessions.reset` signals for every process with the Tenant open. */
72
+ export declare const CONTROL_STREAM = "tenant/control";
73
+ /**
74
+ * The incarnation of a session document written before incarnations existed.
75
+ * Random incarnations are 12 characters, so never this.
76
+ */
77
+ export declare const LEGACY_INCARNATION = "0";
78
+ /** A new session incarnation: 12 random base64url characters. */
79
+ export declare function newStreamIncarnation(): string;
80
+ /** The stream of one incarnation of a session: `sessions/<sessionId>/<incarnation>`. */
81
+ export declare function sessionStream(sessionId: string, incarnation: string): string;
82
+ /** The fields of a session document that name its stream. */
83
+ export interface SessionStreamRef {
84
+ id: string;
85
+ /** Set when the session is created (`newStreamIncarnation`); never changed. */
86
+ streamIncarnation?: string;
87
+ }
88
+ /** The stream of a session as stored now. */
89
+ export declare function streamOfSession(session: SessionStreamRef): string;
90
+ /** The session id and incarnation of a session stream, or undefined for other streams. */
91
+ export declare function parseSessionStream(stream: string): {
92
+ sessionId: string;
93
+ incarnation: string;
94
+ } | undefined;
95
+ /**
96
+ * Signals are not canonical events: they are appended without the outbox, and
97
+ * a lost signal costs latency, not correctness.
98
+ */
99
+ export interface WorkSignal {
100
+ type: "work_available";
101
+ }
102
+ /** Ends the advance of `sessionId` on the process running it. */
103
+ export interface SessionCancelSignal {
104
+ type: "session.cancel";
105
+ sessionId: string;
106
+ }
107
+ /**
108
+ * Session streams were abandoned (a Tenant reset): each process checks its
109
+ * session feeds and ends those whose session is gone or has a new incarnation.
110
+ */
111
+ export interface SessionsResetSignal {
112
+ type: "sessions.reset";
113
+ }
114
+ export type ControlSignal = SessionCancelSignal | SessionsResetSignal;
115
+ export declare const WORK_AVAILABLE: Readonly<WorkSignal>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The Durable Streams seam (architecture §12.4).
3
+ *
4
+ * Durable Streams tell everyone else what the Session Store recorded. Each
5
+ * Tenant has one S2 basin; inside it:
6
+ *
7
+ * | Stream | Contents | Written by | Read by |
8
+ * | --- | --- | --- | --- |
9
+ * | `sessions/<sessionId>/<incarnation>` | every `LiveEvent` of one incarnation of the session, in sequence | the relay, from the outbox | history and session SSE |
10
+ * | `tenant/work` | `work_available` signals | API nodes and Workers after committing pending Actions | executor SSE on every API node |
11
+ * | `tenant/control` | `session.cancel` and `sessions.reset` signals | API nodes | every process with the Tenant open |
12
+ *
13
+ * **Incarnations.** A session's stream name carries an incarnation, a random
14
+ * id stored on the session document when the session is created
15
+ * (`streamIncarnation`, see `streamOfSession`). A session deleted by a reset
16
+ * and created again with the same id gets a new incarnation, so a new stream
17
+ * starting at sequence 0: nothing ever deletes and re-creates the same stream.
18
+ * Abandoned streams are deleted best effort afterwards
19
+ * (`collectSessionStreams` in `tenant/streams.ts`). The cursor stays
20
+ * `base64url("<sessionId>:<seq>")`; readers resolve the incarnation from the
21
+ * session.
22
+ *
23
+ * S2 is the supported implementation (`adapters/streams/s2.ts`, the only file
24
+ * importing the S2 SDK); `streams/memory.ts` is the in-memory fake.
25
+ *
26
+ * ## Guarantees every implementation keeps
27
+ *
28
+ * - **Sequence numbers.** Each stream numbers its records from 0 without gaps.
29
+ * The tail is the sequence the next appended record gets (0 for a stream
30
+ * that does not exist yet).
31
+ * - **Conditional appends.** With `matchSeq`, a batch is appended only when the
32
+ * tail equals `matchSeq`; otherwise nothing is written and the result is a
33
+ * `SeqMismatch` carrying the current tail. The relay appends a session
34
+ * stream with `matchSeq` set to the outbox row's sequence, so a retried
35
+ * append after an unacknowledged success is detected and never duplicated.
36
+ * - **Batches are atomic.** A batch is appended entirely or not at all.
37
+ * - **Reads resume.** `read` from `fromSeq` yields every record with
38
+ * `seq >= fromSeq` in order, with no gap between history and the live tail.
39
+ * - **Tenant scope.** `ensureTenant` creates the Tenant's basin (idempotent)
40
+ * and must run before appends; streams are created on first append.
41
+ * `deleteTenant` removes the basin and every stream in it.
42
+ * - **Records are JSON.** Bodies are JSON-serializable values; implementations
43
+ * encode them as they need.
44
+ */
45
+ import { randomBytes } from "node:crypto";
46
+ // ---------------------------------------------------------------------------
47
+ // Stream names and signal records
48
+ export const SESSION_STREAM_PREFIX = "sessions/";
49
+ /** `work_available` signals for executor connections. */
50
+ export const WORK_STREAM = "tenant/work";
51
+ /** `session.cancel` and `sessions.reset` signals for every process with the Tenant open. */
52
+ export const CONTROL_STREAM = "tenant/control";
53
+ /**
54
+ * The incarnation of a session document written before incarnations existed.
55
+ * Random incarnations are 12 characters, so never this.
56
+ */
57
+ export const LEGACY_INCARNATION = "0";
58
+ /** A new session incarnation: 12 random base64url characters. */
59
+ export function newStreamIncarnation() {
60
+ return randomBytes(9).toString("base64url");
61
+ }
62
+ /** The stream of one incarnation of a session: `sessions/<sessionId>/<incarnation>`. */
63
+ export function sessionStream(sessionId, incarnation) {
64
+ if (!sessionId)
65
+ throw new Error("sessionId is required");
66
+ if (!incarnation || incarnation.includes("/"))
67
+ throw new Error("incarnation must be non-empty and contain no '/'");
68
+ return `${SESSION_STREAM_PREFIX}${sessionId}/${incarnation}`;
69
+ }
70
+ /** The stream of a session as stored now. */
71
+ export function streamOfSession(session) {
72
+ return sessionStream(session.id, session.streamIncarnation ?? LEGACY_INCARNATION);
73
+ }
74
+ /** The session id and incarnation of a session stream, or undefined for other streams. */
75
+ export function parseSessionStream(stream) {
76
+ if (!stream.startsWith(SESSION_STREAM_PREFIX))
77
+ return undefined;
78
+ const rest = stream.slice(SESSION_STREAM_PREFIX.length);
79
+ const slash = rest.lastIndexOf("/");
80
+ if (slash <= 0 || slash === rest.length - 1)
81
+ return undefined;
82
+ return { sessionId: rest.slice(0, slash), incarnation: rest.slice(slash + 1) };
83
+ }
84
+ export const WORK_AVAILABLE = Object.freeze({
85
+ type: "work_available",
86
+ });
@@ -0,0 +1,153 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import { type RegisterExecutorsRequestSchema } from "@nylorun/core/contracts";
3
+ import { type ExecutorRecord } from "../core/executors.js";
4
+ import { type SandboxRouteDeps } from "../core/sandbox-routes.js";
5
+ import { type AuthScope, type TenantContext } from "./context.js";
6
+ type RegisterExecutorsRequest = ReturnType<typeof RegisterExecutorsRequestSchema.parse>;
7
+ /** Session and Action lookups for the sandbox route handlers. */
8
+ export declare function sandboxRouteDeps(ctx: TenantContext): SandboxRouteDeps;
9
+ /** `GET /v1/actions`: the pending Actions of the executor's agent. */
10
+ export declare function listPendingActions(ctx: TenantContext, executor: ExecutorRecord): Promise<{
11
+ actions: ({
12
+ actionId: string;
13
+ sessionId: string;
14
+ turnId: string;
15
+ agentId: string;
16
+ manifestHash: string;
17
+ implementationVersion: string;
18
+ input: unknown;
19
+ context: Record<string, unknown>;
20
+ status: "cancelled" | "claimed" | "completed" | "pending" | "uncertain";
21
+ generation: number;
22
+ claimId: string | null;
23
+ leaseExpiresAt: string | null;
24
+ agent?: {
25
+ id: string;
26
+ path: string;
27
+ delegationId?: string | undefined;
28
+ } | undefined;
29
+ kind: "tool";
30
+ capabilityId: string;
31
+ toolName: string;
32
+ inputSchema?: Record<string, unknown> | undefined;
33
+ outputSchema?: Record<string, unknown> | undefined;
34
+ } | {
35
+ actionId: string;
36
+ sessionId: string;
37
+ turnId: string;
38
+ agentId: string;
39
+ manifestHash: string;
40
+ implementationVersion: string;
41
+ input: unknown;
42
+ context: Record<string, unknown>;
43
+ status: "cancelled" | "claimed" | "completed" | "pending" | "uncertain";
44
+ generation: number;
45
+ claimId: string | null;
46
+ leaseExpiresAt: string | null;
47
+ agent?: {
48
+ id: string;
49
+ path: string;
50
+ delegationId?: string | undefined;
51
+ } | undefined;
52
+ kind: "tool";
53
+ path: string;
54
+ key: string;
55
+ inputSchema?: Record<string, unknown> | undefined;
56
+ outputSchema?: Record<string, unknown> | undefined;
57
+ } | {
58
+ actionId: string;
59
+ sessionId: string;
60
+ turnId: string;
61
+ agentId: string;
62
+ manifestHash: string;
63
+ implementationVersion: string;
64
+ input: unknown;
65
+ context: Record<string, unknown>;
66
+ status: "cancelled" | "claimed" | "completed" | "pending" | "uncertain";
67
+ generation: number;
68
+ claimId: string | null;
69
+ leaseExpiresAt: string | null;
70
+ agent?: {
71
+ id: string;
72
+ path: string;
73
+ delegationId?: string | undefined;
74
+ } | undefined;
75
+ kind: "hook";
76
+ hook: {
77
+ at: "after" | "before";
78
+ scope: "step" | "turn";
79
+ capabilityIds: string[];
80
+ };
81
+ } | {
82
+ actionId: string;
83
+ sessionId: string;
84
+ turnId: string;
85
+ agentId: string;
86
+ manifestHash: string;
87
+ implementationVersion: string;
88
+ input: unknown;
89
+ context: Record<string, unknown>;
90
+ status: "cancelled" | "claimed" | "completed" | "pending" | "uncertain";
91
+ generation: number;
92
+ claimId: string | null;
93
+ leaseExpiresAt: string | null;
94
+ agent?: {
95
+ id: string;
96
+ path: string;
97
+ delegationId?: string | undefined;
98
+ } | undefined;
99
+ kind: "fn";
100
+ path: string;
101
+ key: string;
102
+ } | {
103
+ actionId: string;
104
+ sessionId: string;
105
+ turnId: string;
106
+ agentId: string;
107
+ manifestHash: string;
108
+ implementationVersion: string;
109
+ input: unknown;
110
+ context: Record<string, unknown>;
111
+ status: "cancelled" | "claimed" | "completed" | "pending" | "uncertain";
112
+ generation: number;
113
+ claimId: string | null;
114
+ leaseExpiresAt: string | null;
115
+ agent?: {
116
+ id: string;
117
+ path: string;
118
+ delegationId?: string | undefined;
119
+ } | undefined;
120
+ kind: "verify";
121
+ path: string;
122
+ key: string;
123
+ })[];
124
+ }>;
125
+ /** `POST /v1/actions/:id/sandbox/:tool`: a claim-scoped sandbox tool call. */
126
+ export declare function actionSandboxTool(ctx: TenantContext, scope: AuthScope, actionId: string, toolName: string, request: IncomingMessage): Promise<import("../sandbox/tools.js").SandboxToolOutcome>;
127
+ /** `POST /v1/actions/:id/claim` and `/heartbeat`; anything else under the Action is a 404. */
128
+ export declare function updateAction(ctx: TenantContext, scope: AuthScope, actionId: string, method: string | undefined, operation: string | undefined, body: unknown): Promise<unknown>;
129
+ /** `GET /v1/executors`. */
130
+ export declare function listExecutors(ctx: TenantContext): {
131
+ executors: {
132
+ agentId: string;
133
+ implementationVersion: string;
134
+ manifestHash?: string | undefined;
135
+ connected: boolean;
136
+ updatedAt: string;
137
+ }[];
138
+ };
139
+ /** `PUT /v1/executors`: persist the batch, then update the registry and end rotated streams. */
140
+ export declare function registerExecutors(ctx: TenantContext, principalId: string, body: RegisterExecutorsRequest): Promise<{
141
+ executors: {
142
+ agentId: string;
143
+ implementationVersion: string;
144
+ rotated: boolean;
145
+ replacedBy?: "different-credential" | undefined;
146
+ }[];
147
+ }>;
148
+ /** `DELETE /v1/executors/:agentId`: forget the executor and end its streams. */
149
+ export declare function deleteExecutor(ctx: TenantContext, agentId: string): Promise<{
150
+ agentId: string;
151
+ deleted: boolean;
152
+ }>;
153
+ export {};