@nylorun/runtime 0.12.0-beta → 0.14.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 (223) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/README.md +69 -15
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +38 -8
  5. package/dist/adapters/replication/pgoutput.d.ts +16 -0
  6. package/dist/adapters/replication/pgoutput.js +169 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +1071 -0
  9. package/dist/api/a2a/card.d.ts +15 -0
  10. package/dist/api/a2a/card.js +24 -0
  11. package/dist/api/a2a/endpoint.d.ts +8 -0
  12. package/dist/api/a2a/endpoint.js +81 -0
  13. package/dist/api/a2a/protocol.d.ts +144 -0
  14. package/dist/api/a2a/protocol.js +188 -0
  15. package/dist/api/a2a/routes.d.ts +19 -0
  16. package/dist/api/a2a/routes.js +289 -0
  17. package/dist/api/a2a/tasks.d.ts +50 -0
  18. package/dist/api/a2a/tasks.js +186 -0
  19. package/dist/api/ag-ui/endpoint.d.ts +8 -0
  20. package/dist/api/ag-ui/endpoint.js +126 -0
  21. package/dist/api/ag-ui/history.d.ts +7 -0
  22. package/dist/api/ag-ui/history.js +60 -0
  23. package/dist/api/ag-ui/routes.d.ts +32 -0
  24. package/dist/api/ag-ui/routes.js +305 -0
  25. package/dist/api/ag-ui/session-id.d.ts +1 -0
  26. package/dist/api/ag-ui/session-id.js +12 -0
  27. package/dist/api/ag-ui/sse.d.ts +9 -0
  28. package/dist/api/ag-ui/sse.js +7 -0
  29. package/dist/api/ag-ui/translate.d.ts +38 -0
  30. package/dist/api/ag-ui/translate.js +222 -0
  31. package/dist/api/components.d.ts +7825 -0
  32. package/dist/api/components.js +83 -0
  33. package/dist/api/http/app.d.ts +18 -0
  34. package/dist/api/http/app.js +58 -0
  35. package/dist/api/http/body.d.ts +4 -0
  36. package/dist/api/http/body.js +37 -0
  37. package/dist/api/http/define.d.ts +39 -0
  38. package/dist/api/http/define.js +130 -0
  39. package/dist/api/http/respond.d.ts +9 -0
  40. package/dist/api/http/respond.js +44 -0
  41. package/dist/api/http/routes/access.d.ts +24 -0
  42. package/dist/api/http/routes/access.js +259 -0
  43. package/dist/api/http/routes/actions.d.ts +8 -0
  44. package/dist/api/http/routes/actions.js +76 -0
  45. package/dist/api/http/routes/endpoints.d.ts +7 -0
  46. package/dist/api/http/routes/endpoints.js +100 -0
  47. package/dist/api/http/routes/sessions.d.ts +3 -0
  48. package/dist/api/http/routes/sessions.js +205 -0
  49. package/dist/api/http/routes/tenant.d.ts +10 -0
  50. package/dist/api/http/routes/tenant.js +172 -0
  51. package/dist/api/http/routes/vaults.d.ts +8 -0
  52. package/dist/api/http/routes/vaults.js +167 -0
  53. package/dist/api/openapi.d.ts +19 -0
  54. package/dist/api/openapi.js +138 -0
  55. package/dist/api/route.d.ts +12 -0
  56. package/dist/api/route.js +4 -0
  57. package/dist/core/bearer.d.ts +8 -0
  58. package/dist/core/bearer.js +11 -0
  59. package/dist/core/flow-host.d.ts +7 -15
  60. package/dist/core/flow-host.js +19 -41
  61. package/dist/core/sandbox-routes.d.ts +12 -7
  62. package/dist/core/sandbox-routes.js +24 -25
  63. package/dist/execution/memory.d.ts +8 -5
  64. package/dist/execution/memory.js +33 -13
  65. package/dist/execution/types.d.ts +38 -6
  66. package/dist/execution/types.js +12 -3
  67. package/dist/host/admin-api.d.ts +22 -0
  68. package/dist/host/admin-api.js +178 -0
  69. package/dist/host/app.d.ts +34 -0
  70. package/dist/host/app.js +185 -0
  71. package/dist/host/config-for.d.ts +2 -0
  72. package/dist/host/config-for.js +1 -0
  73. package/dist/host/config.d.ts +11 -0
  74. package/dist/host/cors.d.ts +27 -0
  75. package/dist/host/cors.js +64 -0
  76. package/dist/host/create-host.d.ts +31 -3
  77. package/dist/host/create-host.js +168 -327
  78. package/dist/host/http.d.ts +14 -3
  79. package/dist/host/http.js +64 -23
  80. package/dist/host/main.js +55 -4
  81. package/dist/host/stack-config.d.ts +20 -0
  82. package/dist/host/stack-config.js +55 -0
  83. package/dist/mcp/pool.d.ts +19 -1
  84. package/dist/mcp/pool.js +60 -10
  85. package/dist/model/catalog.d.ts +2 -10
  86. package/dist/model/classify.d.ts +13 -0
  87. package/dist/model/classify.js +61 -0
  88. package/dist/model/models.d.ts +10 -1
  89. package/dist/model/models.js +14 -6
  90. package/dist/model/pi-model.d.ts +17 -2
  91. package/dist/model/pi-model.js +366 -222
  92. package/dist/openapi.json +25696 -0
  93. package/dist/sandbox/manager.d.ts +13 -1
  94. package/dist/sandbox/manager.js +8 -11
  95. package/dist/sandbox/resolve.d.ts +33 -0
  96. package/dist/sandbox/resolve.js +81 -0
  97. package/dist/sandbox/session-sandbox.d.ts +11 -0
  98. package/dist/sandbox/session-sandbox.js +51 -0
  99. package/dist/sandbox/share.d.ts +14 -2
  100. package/dist/sandbox/share.js +33 -3
  101. package/dist/sandbox/tenant-config.d.ts +37 -0
  102. package/dist/sandbox/tenant-config.js +51 -0
  103. package/dist/sandbox/tools.d.ts +7 -9
  104. package/dist/sandbox/tools.js +11 -3
  105. package/dist/sandbox/types.d.ts +8 -2
  106. package/dist/store/event.d.ts +19 -0
  107. package/dist/store/event.js +49 -0
  108. package/dist/store/memory.d.ts +20 -8
  109. package/dist/store/memory.js +265 -100
  110. package/dist/store/postgres/connect.d.ts +13 -0
  111. package/dist/store/postgres/connect.js +5 -0
  112. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
  113. package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
  114. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
  115. package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
  116. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
  117. package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
  118. package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
  119. package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
  120. package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
  121. package/dist/store/postgres/migrations/007_event_time.js +14 -0
  122. package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
  123. package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
  124. package/dist/store/postgres/migrations/index.d.ts +5 -1
  125. package/dist/store/postgres/migrations/index.js +22 -2
  126. package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
  127. package/dist/store/postgres/migrations/shared/index.js +92 -0
  128. package/dist/store/postgres/record.d.ts +11 -0
  129. package/dist/store/postgres/record.js +64 -0
  130. package/dist/store/postgres/store.js +337 -95
  131. package/dist/store/postgres/tenants.js +14 -0
  132. package/dist/store/types.d.ts +176 -81
  133. package/dist/streams/basin.d.ts +18 -1
  134. package/dist/streams/basin.js +34 -2
  135. package/dist/streams/control.d.ts +18 -0
  136. package/dist/streams/control.js +29 -0
  137. package/dist/streams/memory.d.ts +3 -1
  138. package/dist/streams/memory.js +14 -8
  139. package/dist/streams/relay/core.d.ts +35 -0
  140. package/dist/streams/relay/core.js +366 -0
  141. package/dist/streams/relay/memory.d.ts +69 -0
  142. package/dist/streams/relay/memory.js +176 -0
  143. package/dist/streams/relay/types.d.ts +77 -0
  144. package/dist/streams/relay/types.js +7 -0
  145. package/dist/streams/types.d.ts +66 -36
  146. package/dist/streams/types.js +18 -53
  147. package/dist/tenant/access-policy.d.ts +30 -0
  148. package/dist/tenant/access-policy.js +52 -0
  149. package/dist/tenant/actions.d.ts +10 -149
  150. package/dist/tenant/actions.js +5 -167
  151. package/dist/tenant/advance.js +32 -0
  152. package/dist/tenant/auth.d.ts +19 -17
  153. package/dist/tenant/auth.js +90 -93
  154. package/dist/tenant/browser.d.ts +15 -0
  155. package/dist/tenant/browser.js +39 -0
  156. package/dist/tenant/commands.d.ts +18 -2
  157. package/dist/tenant/commands.js +121 -129
  158. package/dist/tenant/context.d.ts +75 -18
  159. package/dist/tenant/context.js +14 -10
  160. package/dist/tenant/delivery-token.d.ts +41 -0
  161. package/dist/tenant/delivery-token.js +105 -0
  162. package/dist/tenant/delivery.d.ts +62 -0
  163. package/dist/tenant/delivery.js +476 -0
  164. package/dist/tenant/effects.d.ts +5 -0
  165. package/dist/tenant/effects.js +138 -48
  166. package/dist/tenant/endpoints.d.ts +67 -0
  167. package/dist/tenant/endpoints.js +90 -0
  168. package/dist/tenant/ephemeral.d.ts +6 -0
  169. package/dist/tenant/ephemeral.js +60 -36
  170. package/dist/tenant/http.d.ts +8 -9
  171. package/dist/tenant/http.js +13 -27
  172. package/dist/tenant/jwt.d.ts +16 -0
  173. package/dist/tenant/jwt.js +50 -0
  174. package/dist/tenant/module.d.ts +3 -0
  175. package/dist/tenant/module.js +6 -12
  176. package/dist/tenant/outbound.d.ts +44 -0
  177. package/dist/tenant/outbound.js +127 -0
  178. package/dist/tenant/pool.d.ts +0 -2
  179. package/dist/tenant/pool.js +0 -18
  180. package/dist/tenant/reset.d.ts +0 -4
  181. package/dist/tenant/reset.js +0 -5
  182. package/dist/tenant/runtime.d.ts +10 -20
  183. package/dist/tenant/runtime.js +59 -42
  184. package/dist/tenant/scheduler.d.ts +7 -2
  185. package/dist/tenant/scheduler.js +7 -3
  186. package/dist/tenant/session-streams.d.ts +125 -0
  187. package/dist/tenant/session-streams.js +399 -0
  188. package/dist/tenant/sessions.d.ts +28 -4
  189. package/dist/tenant/sessions.js +118 -24
  190. package/dist/tenant/signing-keys.d.ts +55 -0
  191. package/dist/tenant/signing-keys.js +172 -0
  192. package/dist/tenant/slim.d.ts +8 -0
  193. package/dist/tenant/slim.js +21 -0
  194. package/dist/tenant/status.d.ts +0 -3
  195. package/dist/tenant/status.js +19 -12
  196. package/dist/tenant/streams.d.ts +60 -67
  197. package/dist/tenant/streams.js +140 -208
  198. package/dist/tenant/subject-limits.d.ts +16 -0
  199. package/dist/tenant/subject-limits.js +45 -0
  200. package/dist/tenant/sweep.d.ts +1 -10
  201. package/dist/tenant/sweep.js +29 -69
  202. package/dist/tenant/tokens.d.ts +14 -0
  203. package/dist/tenant/tokens.js +178 -0
  204. package/dist/tenant/transcript.d.ts +48 -5
  205. package/dist/tenant/transcript.js +58 -1
  206. package/dist/tenant/types.d.ts +29 -7
  207. package/dist/tenant/worker.d.ts +3 -1
  208. package/dist/tenant/worker.js +9 -3
  209. package/dist/vault/service.d.ts +18 -2
  210. package/dist/vault/service.js +57 -13
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +20 -8
  214. package/dist/core/executors.d.ts +0 -44
  215. package/dist/core/executors.js +0 -95
  216. package/dist/streams/relay.d.ts +0 -37
  217. package/dist/streams/relay.js +0 -254
  218. package/dist/tenant/live.d.ts +0 -92
  219. package/dist/tenant/live.js +0 -290
  220. package/dist/tenant/routes-tenant.d.ts +0 -12
  221. package/dist/tenant/routes-tenant.js +0 -137
  222. package/dist/tenant/routes.d.ts +0 -9
  223. package/dist/tenant/routes.js +0 -151
@@ -19,7 +19,8 @@
19
19
  * ends. To stay deadlock-free:
20
20
  *
21
21
  * 1. A session-scoped transaction locks its session row before writing any
22
- * other row of that session (effects, actions, checkpoints, outbox).
22
+ * other row of that session (effects, actions, checkpoints, its record rows
23
+ * and log head).
23
24
  * 2. A transaction that touches several sessions locks them in ascending id
24
25
  * order, with `lockSessions` (`./locking.ts`), before writing any of them.
25
26
  *
@@ -29,7 +30,7 @@
29
30
  *
30
31
  * ## Values
31
32
  *
32
- * Document and outbox bodies are `json`, stored as the text `JSON.stringify`
33
+ * Document and event bodies are `json`, stored as the text `JSON.stringify`
33
34
  * wrote, so every string round-trips, including U+0000 and unpaired
34
35
  * surrogates that `jsonb` rejects (see `migrations/001_initial.ts`), and key
35
36
  * order is kept. Ids and ISO timestamps compared as text use `COLLATE "C"`.
@@ -37,10 +38,16 @@
37
38
  */
38
39
  import { AsyncLocalStorage } from "node:async_hooks";
39
40
  import { randomUUID } from "node:crypto";
41
+ import { buildEvent } from "../event.js";
40
42
  import { encodeCursor } from "../cursor.js";
41
43
  import { OwnershipLostError } from "../ownership.js";
42
44
  import { POSTGRES_SCHEMA_VERSION, readSchemaVersion } from "./migrations/index.js";
43
45
  import { assertIdentifier, tenantSchemaName } from "./names.js";
46
+ import { STREAMS_SCHEMA } from "./migrations/shared/index.js";
47
+ import { createPostgresRecordReader } from "./record.js";
48
+ /** The shared record (Durable Streams §6): every Tenant's events and log heads. */
49
+ const SESSION_EVENTS = `${STREAMS_SCHEMA}.session_events`;
50
+ const LOG_HEADS = `${STREAMS_SCHEMA}.session_log_heads`;
44
51
  export function createPostgresSessionStore(options) {
45
52
  return new PostgresSessionStore(options);
46
53
  }
@@ -94,11 +101,10 @@ class PostgresSessionStore {
94
101
  finally {
95
102
  this.inflight.delete(run);
96
103
  }
97
- if (t.events.length > 0 || t.workAvailable) {
104
+ if (t.events.length > 0) {
98
105
  const commit = {
99
106
  events: t.events,
100
- incarnations: t.incarnations,
101
- workAvailable: t.workAvailable,
107
+ generations: t.generations,
102
108
  };
103
109
  for (const listener of [...this.listeners]) {
104
110
  try {
@@ -123,6 +129,9 @@ class PostgresSessionStore {
123
129
  this.listeners.add(listener);
124
130
  return () => this.listeners.delete(listener);
125
131
  }
132
+ record() {
133
+ return createPostgresRecordReader(this.sql, { tenantId: this.tenantId });
134
+ }
126
135
  async health() {
127
136
  const expectedSchemaVersion = this.schemaVersion;
128
137
  if (this.closed)
@@ -166,13 +175,23 @@ function toJson(value) {
166
175
  }
167
176
  const bytes = (value) => Buffer.from(value.buffer, value.byteOffset, value.byteLength);
168
177
  const fromBytes = (value) => new Uint8Array(value);
169
- function executorRow(row) {
178
+ function endpointRow(row) {
179
+ const optional = (key, value) => value === null ? {} : { [key]: value };
170
180
  return {
171
181
  agentId: row.agent_id,
172
- tokenHash: row.token_hash,
182
+ url: row.url,
173
183
  implementationVersion: row.implementation_version,
174
- ...(row.manifest_hash !== null ? { manifestHash: row.manifest_hash } : {}),
175
- ...(row.principal_id !== null ? { principalId: row.principal_id } : {}),
184
+ ...optional("manifestHash", row.manifest_hash),
185
+ timeoutMs: row.timeout_ms,
186
+ maxConcurrent: row.max_concurrent,
187
+ ...optional("principalId", row.principal_id),
188
+ ...optional("lastDeliveryAt", row.last_delivery_at),
189
+ ...optional("lastSuccessAt", row.last_success_at),
190
+ ...optional("lastErrorCode", row.last_error_code),
191
+ ...optional("lastErrorMessage", row.last_error_message),
192
+ consecutiveFailures: row.consecutive_failures,
193
+ ...optional("servedImplementationVersion", row.served_implementation_version),
194
+ ...optional("servedManifestHash", row.served_manifest_hash),
176
195
  createdAt: row.created_at,
177
196
  updatedAt: row.updated_at,
178
197
  };
@@ -186,6 +205,38 @@ function principalRow(row) {
186
205
  createdAt: row.created_at,
187
206
  };
188
207
  }
208
+ function signingKeyRow(row) {
209
+ return {
210
+ id: row.id,
211
+ state: row.state,
212
+ alg: row.alg,
213
+ publicJwk: row.public_jwk,
214
+ createdAt: row.created_at,
215
+ activatedAt: row.activated_at,
216
+ retiredAt: row.retired_at,
217
+ revokedAt: row.revoked_at,
218
+ kekId: row.kek_id,
219
+ nonce: fromBytes(row.nonce),
220
+ ciphertext: fromBytes(row.ciphertext),
221
+ wrappedDek: fromBytes(row.wrapped_dek),
222
+ };
223
+ }
224
+ function publishableKeyRow(row) {
225
+ return {
226
+ id: row.id,
227
+ key: row.key,
228
+ name: row.name,
229
+ originsJson: row.origins_json,
230
+ createdAt: row.created_at,
231
+ revokedAt: row.revoked_at,
232
+ };
233
+ }
234
+ /** The time column a signing key's new state stamps. */
235
+ const SIGNING_KEY_STAMP = {
236
+ current: "activated_at",
237
+ previous: "retired_at",
238
+ revoked: "revoked_at",
239
+ };
189
240
  function vaultRow(row) {
190
241
  return {
191
242
  id: row.id,
@@ -244,7 +295,6 @@ const SESSION_TABLES = [
244
295
  "effects",
245
296
  "actions",
246
297
  "links",
247
- "outbox",
248
298
  ];
249
299
  class PostgresTx {
250
300
  sql;
@@ -253,9 +303,8 @@ class PostgresTx {
253
303
  now;
254
304
  closed = false;
255
305
  events = [];
256
- incarnations = [];
306
+ generations = [];
257
307
  callbacks = [];
258
- workAvailable = false;
259
308
  constructor(sql, schema, tenantId, now) {
260
309
  this.sql = sql;
261
310
  this.schema = schema;
@@ -295,10 +344,9 @@ class PostgresTx {
295
344
  }
296
345
  async delete(table, id) {
297
346
  this.check();
347
+ // A session's record rows and log head stay: a session created again with this id
348
+ // continues its log (per-session record deletion is deferred, Durable Streams §15).
298
349
  await this.sql `DELETE FROM ${this.t(table)} WHERE id = ${id}`;
299
- // The session's stream is abandoned with it (a new incarnation starts at 0).
300
- if (table === "sessions")
301
- await this.sql `DELETE FROM ${this.t("outbox")} WHERE session_id = ${id}`;
302
350
  }
303
351
  async sessionRows(where, tail) {
304
352
  const sql = this.sql;
@@ -319,39 +367,43 @@ class PostgresTx {
319
367
  async event(sessionId, turnId, type, payload) {
320
368
  this.check();
321
369
  const sql = this.sql;
322
- // The UPDATE takes the session row lock and allocates under it.
323
- const [row] = await sql `
324
- UPDATE ${this.t("sessions")} SET next_event_seq = next_event_seq + 1
325
- WHERE id = ${sessionId}
326
- RETURNING next_event_seq - 1 AS seq, stream_incarnation AS incarnation`;
327
- if (!row)
370
+ // The session row lock orders the session's events; the log head allocates under it.
371
+ const [session] = await sql `
372
+ SELECT epoch FROM ${this.t("sessions")} WHERE id = ${sessionId} FOR UPDATE`;
373
+ if (!session)
328
374
  throw new Error(`Session ${sessionId} not found`);
375
+ const [row] = await sql `
376
+ INSERT INTO ${sql(LOG_HEADS)} (tenant_id, session_id, generation, head)
377
+ VALUES (
378
+ ${this.tenantId}, ${sessionId},
379
+ coalesce((SELECT basin_generation FROM ${this.t("tenant")}), 0), 1)
380
+ ON CONFLICT (tenant_id, session_id)
381
+ DO UPDATE SET head = ${sql(LOG_HEADS)}.head + 1
382
+ RETURNING head - 1 AS seq, generation`;
329
383
  const seq = Number(row.seq);
330
- const event = JSON.parse(JSON.stringify({
331
- eventId: randomUUID(),
332
- sessionId,
384
+ const generation = Number(row.generation);
385
+ const event = buildEvent({
333
386
  tenantId: this.tenantId,
387
+ sessionId,
334
388
  turnId,
335
- cursor: encodeCursor(sessionId, seq),
336
- createdAt: this.now().toISOString(),
389
+ seq,
390
+ epoch: Number(session.epoch),
391
+ time: this.now(),
337
392
  type,
338
393
  payload,
339
- }));
394
+ });
340
395
  await sql `
341
- INSERT INTO ${this.t("outbox")} (session_id, seq, body)
342
- VALUES (${sessionId}, ${seq}, ${JSON.stringify(event)}::text::json)`;
396
+ INSERT INTO ${sql(SESSION_EVENTS)} (tenant_id, session_id, seq, generation, type, body)
397
+ VALUES (${this.tenantId}, ${sessionId}, ${seq}, ${generation}, ${type},
398
+ ${JSON.stringify(event)}::text::json)`;
343
399
  this.events.push(event);
344
- this.incarnations.push(row.incarnation ?? null);
400
+ this.generations.push(generation);
345
401
  return structuredClone(event);
346
402
  }
347
403
  afterCommit(fn) {
348
404
  this.check();
349
405
  this.callbacks.push(fn);
350
406
  }
351
- signalWork() {
352
- this.check();
353
- this.workAvailable = true;
354
- }
355
407
  // --- ownership -----------------------------------------------------------
356
408
  async takeOwnership(sessionId, claim) {
357
409
  this.check();
@@ -443,22 +495,39 @@ class PostgresTx {
443
495
  this.check();
444
496
  return this.bodies("sandboxes");
445
497
  }
446
- async expiredClaims(now, limit) {
498
+ async pendingActions(agentId) {
447
499
  this.check();
448
500
  const rows = await this.sql `
449
501
  SELECT body FROM ${this.t("actions")}
450
- WHERE status = 'claimed' AND lease_expires_at IS NOT NULL
451
- AND lease_expires_at::timestamptz <= ${now}
452
- ORDER BY lease_expires_at::timestamptz, id
502
+ WHERE agent_id = ${agentId} AND status = 'pending'
503
+ ORDER BY id`;
504
+ return rows.map((row) => row.body);
505
+ }
506
+ async deliveringCount(agentId) {
507
+ this.check();
508
+ const [row] = await this.sql `
509
+ SELECT count(*)::int AS n FROM ${this.t("actions")}
510
+ WHERE agent_id = ${agentId} AND status = 'delivering'`;
511
+ return row.n;
512
+ }
513
+ async pendingActionsWithEndpoint(limit) {
514
+ this.check();
515
+ const rows = await this.sql `
516
+ SELECT a.body FROM ${this.t("actions")} a
517
+ JOIN ${this.t("endpoints")} e ON e.agent_id = a.agent_id
518
+ WHERE a.status = 'pending'
519
+ ORDER BY a.id
453
520
  LIMIT ${limit}`;
454
521
  return rows.map((row) => row.body);
455
522
  }
456
- async pendingActions(agentId) {
523
+ async expiredDeliveries(now, limit) {
457
524
  this.check();
458
525
  const rows = await this.sql `
459
526
  SELECT body FROM ${this.t("actions")}
460
- WHERE agent_id = ${agentId} AND status = 'pending'
461
- ORDER BY id`;
527
+ WHERE status = 'delivering' AND deadline_at IS NOT NULL
528
+ AND deadline_at::timestamptz <= ${now}
529
+ ORDER BY deadline_at::timestamptz, id
530
+ LIMIT ${limit}`;
462
531
  return rows.map((row) => row.body);
463
532
  }
464
533
  async actionsForSession(sessionId, filter = {}) {
@@ -529,7 +598,7 @@ class PostgresTx {
529
598
  SELECT
530
599
  (SELECT count(*) FROM ${this.t("sessions")})::int AS sessions,
531
600
  (SELECT count(*) FROM ${this.t("sessions")} WHERE status = ANY(${OPEN_SESSION}))::int AS running,
532
- (SELECT count(*) FROM ${this.t("actions")} WHERE status IN ('pending', 'claimed'))::int AS actions,
601
+ (SELECT count(*) FROM ${this.t("actions")} WHERE status IN ('pending', 'delivering'))::int AS actions,
533
602
  (SELECT count(*) FROM ${this.t("effects")} WHERE status = 'uncertain')::int AS uncertain,
534
603
  (SELECT count(*) FROM ${this.t("sandboxes")})::int AS sandboxes,
535
604
  (SELECT count(*) FROM ${this.t("definitions")})::int AS definitions`;
@@ -542,78 +611,96 @@ class PostgresTx {
542
611
  definitions: row.definitions,
543
612
  };
544
613
  }
545
- // --- outbox --------------------------------------------------------------
546
- async outbox(limit, filter = {}) {
547
- this.check();
548
- const sql = this.sql;
549
- const rows = await sql `
550
- SELECT session_id, seq, body FROM ${this.t("outbox")}
551
- ${filter.sessionId === undefined ? sql `` : sql `WHERE session_id = ${filter.sessionId}`}
552
- ORDER BY session_id, seq
553
- LIMIT ${limit}`;
554
- return rows.map((row) => ({
555
- sessionId: row.session_id,
556
- seq: Number(row.seq),
557
- event: row.body,
558
- }));
559
- }
560
- async deleteOutbox(sessionId, throughSeq, incarnation) {
561
- this.check();
562
- const sql = this.sql;
563
- // One statement: the session and the rows are read from the same snapshot.
564
- const result = await sql `
565
- DELETE FROM ${this.t("outbox")}
566
- WHERE session_id = ${sessionId} AND seq <= ${throughSeq}
567
- ${incarnation === undefined
568
- ? sql ``
569
- : sql `AND EXISTS (
570
- SELECT 1 FROM ${this.t("sessions")} WHERE id = ${sessionId}
571
- AND stream_incarnation IS NOT DISTINCT FROM ${incarnation})`}`;
572
- return result.count;
573
- }
574
- async outboxStats() {
614
+ // --- basin generations --------------------------------------------------
615
+ async basinGenerations() {
575
616
  this.check();
576
617
  const [row] = await this.sql `
577
- SELECT count(*)::int AS depth, min(created_at) AS oldest
578
- FROM ${this.t("outbox")}`;
618
+ SELECT basin_generation, retired_generations FROM ${this.t("tenant")}`;
619
+ // A schema without its Tenant row (store tests) is at generation 0.
620
+ if (!row)
621
+ return { current: 0, retired: [] };
579
622
  return {
580
- depth: row.depth,
581
- oldestCreatedAt: row.oldest ?? null,
623
+ current: Number(row.basin_generation),
624
+ retired: row.retired_generations.map(Number),
582
625
  };
583
626
  }
584
- // --- executors -----------------------------------------------------------
585
- async listExecutors() {
627
+ async forgetRetiredGeneration(generation) {
628
+ this.check();
629
+ await this.sql `
630
+ UPDATE ${this.t("tenant")}
631
+ SET retired_generations = array_remove(retired_generations, ${generation}::int)`;
632
+ }
633
+ // --- Action endpoints -----------------------------------------------------
634
+ async listEndpoints() {
586
635
  this.check();
587
- const rows = await this.sql `SELECT * FROM ${this.t("executors")} ORDER BY agent_id`;
588
- return rows.map(executorRow);
636
+ const rows = await this.sql `SELECT * FROM ${this.t("endpoints")} ORDER BY agent_id`;
637
+ return rows.map(endpointRow);
589
638
  }
590
- async getExecutor(agentId) {
639
+ async getEndpoint(agentId) {
591
640
  this.check();
592
641
  const [row] = await this.sql `
593
- SELECT * FROM ${this.t("executors")} WHERE agent_id = ${agentId}`;
594
- return row && executorRow(row);
642
+ SELECT * FROM ${this.t("endpoints")} WHERE agent_id = ${agentId}`;
643
+ return row && endpointRow(row);
595
644
  }
596
- async putExecutor(row) {
645
+ async putEndpoint(row) {
597
646
  this.check();
647
+ // Health belongs to a URL: a new URL starts with none.
598
648
  await this.sql `
599
- INSERT INTO ${this.t("executors")} (
600
- agent_id, token_hash, implementation_version, manifest_hash,
601
- principal_id, created_at, updated_at
649
+ INSERT INTO ${this.t("endpoints")} AS e (
650
+ agent_id, url, implementation_version, manifest_hash, timeout_ms,
651
+ max_concurrent, principal_id, consecutive_failures, created_at, updated_at
602
652
  ) VALUES (
603
- ${row.agentId}, ${row.tokenHash}, ${row.implementationVersion},
604
- ${row.manifestHash ?? null}, ${row.principalId ?? null},
653
+ ${row.agentId}, ${row.url}, ${row.implementationVersion}, ${row.manifestHash ?? null},
654
+ ${row.timeoutMs}, ${row.maxConcurrent}, ${row.principalId ?? null}, 0,
605
655
  ${row.updatedAt}, ${row.updatedAt}
606
656
  )
607
657
  ON CONFLICT (agent_id) DO UPDATE SET
608
- token_hash = excluded.token_hash,
658
+ url = excluded.url,
609
659
  implementation_version = excluded.implementation_version,
610
660
  manifest_hash = excluded.manifest_hash,
661
+ timeout_ms = excluded.timeout_ms,
662
+ max_concurrent = excluded.max_concurrent,
611
663
  principal_id = excluded.principal_id,
612
- updated_at = excluded.updated_at`;
664
+ updated_at = excluded.updated_at,
665
+ last_delivery_at = CASE WHEN e.url = excluded.url THEN e.last_delivery_at END,
666
+ last_success_at = CASE WHEN e.url = excluded.url THEN e.last_success_at END,
667
+ last_error_code = CASE WHEN e.url = excluded.url THEN e.last_error_code END,
668
+ last_error_message = CASE WHEN e.url = excluded.url THEN e.last_error_message END,
669
+ consecutive_failures =
670
+ CASE WHEN e.url = excluded.url THEN e.consecutive_failures ELSE 0 END,
671
+ served_implementation_version =
672
+ CASE WHEN e.url = excluded.url THEN e.served_implementation_version END,
673
+ served_manifest_hash =
674
+ CASE WHEN e.url = excluded.url THEN e.served_manifest_hash END`;
675
+ }
676
+ async deleteEndpoint(agentId) {
677
+ this.check();
678
+ await this.sql `DELETE FROM ${this.t("endpoints")} WHERE agent_id = ${agentId}`;
613
679
  }
614
- async deleteExecutor(agentId) {
680
+ async recordEndpointHealth(agentId, update) {
615
681
  this.check();
616
- await this.sql `DELETE FROM ${this.t("executors")} WHERE agent_id = ${agentId}`;
682
+ const table = this.t("endpoints");
683
+ switch (update.kind) {
684
+ case "success":
685
+ await this.sql `
686
+ UPDATE ${table} SET last_delivery_at = ${update.at}, last_success_at = ${update.at},
687
+ consecutive_failures = 0, last_error_code = NULL, last_error_message = NULL
688
+ WHERE agent_id = ${agentId}`;
689
+ return;
690
+ case "failure":
691
+ await this.sql `
692
+ UPDATE ${table} SET last_delivery_at = ${update.at}, last_error_code = ${update.code},
693
+ last_error_message = ${update.message},
694
+ consecutive_failures = consecutive_failures + 1
695
+ WHERE agent_id = ${agentId}`;
696
+ return;
697
+ case "served":
698
+ await this.sql `
699
+ UPDATE ${table} SET served_implementation_version = ${update.implementationVersion},
700
+ served_manifest_hash = ${update.manifestHash ?? null}
701
+ WHERE agent_id = ${agentId}`;
702
+ return;
703
+ }
617
704
  }
618
705
  // --- principals ----------------------------------------------------------
619
706
  async insertPrincipal(row) {
@@ -766,6 +853,151 @@ class PostgresTx {
766
853
  INSERT INTO ${this.t("vault_idempotency")} (id, body_hash, response)
767
854
  VALUES (${row.id}, ${row.bodyHash}, ${row.response})`;
768
855
  }
856
+ // --- subject tokens ------------------------------------------------------
857
+ async lockSigningKeys() {
858
+ this.check();
859
+ const key = `${this.schema}.signing_keys`;
860
+ await this.sql `SELECT pg_advisory_xact_lock(hashtext(${key}))`;
861
+ }
862
+ async insertSigningKey(row) {
863
+ this.check();
864
+ await this.sql `
865
+ INSERT INTO ${this.t("signing_keys")} (
866
+ id, state, alg, public_jwk, kek_id, nonce, ciphertext, wrapped_dek,
867
+ created_at, activated_at, retired_at, revoked_at
868
+ ) VALUES (
869
+ ${row.id}, ${row.state}, ${row.alg}, ${row.publicJwk}, ${row.kekId},
870
+ ${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)},
871
+ ${row.createdAt}, ${row.activatedAt}, ${row.retiredAt}, ${row.revokedAt}
872
+ )`;
873
+ }
874
+ async signingKey(id) {
875
+ this.check();
876
+ const [row] = await this.sql `
877
+ SELECT * FROM ${this.t("signing_keys")} WHERE id = ${id}`;
878
+ return row && signingKeyRow(row);
879
+ }
880
+ async signingKeys(states) {
881
+ this.check();
882
+ const sql = this.sql;
883
+ const rows = await sql `
884
+ SELECT * FROM ${this.t("signing_keys")}
885
+ ${states === undefined ? sql `` : sql `WHERE state = ANY(${states})`}
886
+ ORDER BY created_at, id`;
887
+ return rows.map(signingKeyRow);
888
+ }
889
+ async setSigningKeyState(id, from, to, at) {
890
+ this.check();
891
+ const sql = this.sql;
892
+ const column = SIGNING_KEY_STAMP[to];
893
+ const rows = await sql `
894
+ UPDATE ${this.t("signing_keys")}
895
+ SET state = ${to}${column ? sql `, ${sql(column)} = ${at}` : sql ``}
896
+ WHERE id = ${id} AND state = ${from}
897
+ RETURNING id`;
898
+ return rows.length === 1;
899
+ }
900
+ async countSigningKeys() {
901
+ this.check();
902
+ const [row] = await this.sql `
903
+ SELECT count(*)::int AS n FROM ${this.t("signing_keys")}`;
904
+ return row.n;
905
+ }
906
+ async subjectEpoch(subject) {
907
+ this.check();
908
+ const [row] = await this.sql `
909
+ SELECT epoch FROM ${this.t("subject_epochs")} WHERE subject = ${subject}`;
910
+ return row ? Number(row.epoch) : 0;
911
+ }
912
+ async subjectEpochs(subjects) {
913
+ this.check();
914
+ if (subjects.length === 0)
915
+ return new Map();
916
+ const rows = await this.sql `
917
+ SELECT subject, epoch FROM ${this.t("subject_epochs")}
918
+ WHERE subject = ANY(${subjects})`;
919
+ return new Map(rows.map((row) => [row.subject, Number(row.epoch)]));
920
+ }
921
+ async bumpSubjectEpoch(subject, at) {
922
+ this.check();
923
+ const [row] = await this.sql `
924
+ INSERT INTO ${this.t("subject_epochs")} (subject, epoch, revoked_at)
925
+ VALUES (${subject}, 1, ${at})
926
+ ON CONFLICT (subject) DO UPDATE
927
+ SET epoch = ${this.t("subject_epochs")}.epoch + 1, revoked_at = excluded.revoked_at
928
+ RETURNING epoch`;
929
+ return Number(row.epoch);
930
+ }
931
+ async lockSubjectUsage(initial) {
932
+ this.check();
933
+ await this.sql `
934
+ INSERT INTO ${this.t("subject_usage")} (subject, turn_tokens, refilled_at)
935
+ VALUES (${initial.subject}, ${initial.turnTokens}, ${initial.refilledAt})
936
+ ON CONFLICT (subject) DO NOTHING`;
937
+ const [row] = await this.sql `
938
+ SELECT subject, turn_tokens, refilled_at FROM ${this.t("subject_usage")}
939
+ WHERE subject = ${initial.subject} FOR UPDATE`;
940
+ return {
941
+ subject: row.subject,
942
+ turnTokens: Number(row.turn_tokens),
943
+ refilledAt: row.refilled_at,
944
+ };
945
+ }
946
+ async putSubjectUsage(row) {
947
+ this.check();
948
+ await this.sql `
949
+ INSERT INTO ${this.t("subject_usage")} (subject, turn_tokens, refilled_at)
950
+ VALUES (${row.subject}, ${row.turnTokens}, ${row.refilledAt})
951
+ ON CONFLICT (subject) DO UPDATE
952
+ SET turn_tokens = excluded.turn_tokens, refilled_at = excluded.refilled_at`;
953
+ }
954
+ async countOwnerSessions(ownerUserId, statuses) {
955
+ this.check();
956
+ const [row] = await this.sql `
957
+ SELECT count(*)::int AS n FROM ${this.t("sessions")}
958
+ WHERE owner_user_id = ${ownerUserId} AND status = ANY(${statuses})`;
959
+ return row.n;
960
+ }
961
+ // --- publishable keys ----------------------------------------------------
962
+ async insertPublishableKey(row) {
963
+ this.check();
964
+ await this.sql `
965
+ INSERT INTO ${this.t("publishable_keys")} (id, key, name, origins_json, created_at, revoked_at)
966
+ VALUES (${row.id}, ${row.key}, ${row.name}, ${row.originsJson}, ${row.createdAt}, ${row.revokedAt})`;
967
+ }
968
+ async publishableKeyByKey(key) {
969
+ this.check();
970
+ const [row] = await this.sql `
971
+ SELECT * FROM ${this.t("publishable_keys")} WHERE key = ${key}`;
972
+ return row && publishableKeyRow(row);
973
+ }
974
+ async publishableKey(id) {
975
+ this.check();
976
+ const [row] = await this.sql `
977
+ SELECT * FROM ${this.t("publishable_keys")} WHERE id = ${id}`;
978
+ return row && publishableKeyRow(row);
979
+ }
980
+ async publishableKeys() {
981
+ this.check();
982
+ const rows = await this.sql `
983
+ SELECT * FROM ${this.t("publishable_keys")} ORDER BY created_at, id`;
984
+ return rows.map(publishableKeyRow);
985
+ }
986
+ async updatePublishableKey(id, patch) {
987
+ this.check();
988
+ const sql = this.sql;
989
+ const sets = [
990
+ ...(patch.originsJson === undefined ? [] : [sql `origins_json = ${patch.originsJson}`]),
991
+ ...(patch.revokedAt === undefined ? [] : [sql `revoked_at = ${patch.revokedAt}`]),
992
+ ];
993
+ if (sets.length === 0)
994
+ return (await this.publishableKey(id)) !== undefined;
995
+ const rows = await sql `
996
+ UPDATE ${this.t("publishable_keys")}
997
+ SET ${sets.reduce((all, next) => sql `${all}, ${next}`)}
998
+ WHERE id = ${id} RETURNING id`;
999
+ return rows.length === 1;
1000
+ }
769
1001
  // --- settings ------------------------------------------------------------
770
1002
  async getSetting(key) {
771
1003
  this.check();
@@ -783,14 +1015,24 @@ class PostgresTx {
783
1015
  async reset(scope) {
784
1016
  this.check();
785
1017
  const sql = this.sql;
786
- if (scope === "sessions" || scope === "all")
1018
+ if (scope === "sessions" || scope === "all") {
787
1019
  for (const table of SESSION_TABLES)
788
1020
  await sql `DELETE FROM ${this.t(table)}`;
1021
+ await sql `DELETE FROM ${this.t("subject_usage")}`;
1022
+ // The record goes with the sessions, and the Tenant moves to a new basin: the ids it
1023
+ // frees start again in an empty one (Durable Streams §8.1).
1024
+ await sql `DELETE FROM ${sql(SESSION_EVENTS)} WHERE tenant_id = ${this.tenantId}`;
1025
+ await sql `DELETE FROM ${sql(LOG_HEADS)} WHERE tenant_id = ${this.tenantId}`;
1026
+ await sql `
1027
+ UPDATE ${this.t("tenant")}
1028
+ SET retired_generations = array_append(retired_generations, basin_generation),
1029
+ basin_generation = basin_generation + 1`;
1030
+ }
789
1031
  if (scope === "sandboxes" || scope === "all")
790
1032
  await sql `DELETE FROM ${this.t("sandboxes")}`;
791
1033
  if (scope === "all") {
792
1034
  await sql `DELETE FROM ${this.t("definitions")}`;
793
- await sql `DELETE FROM ${this.t("executors")}`;
1035
+ await sql `DELETE FROM ${this.t("endpoints")}`;
794
1036
  await sql `DELETE FROM ${this.t("vaults")} WHERE scope <> 'host'`;
795
1037
  }
796
1038
  }
@@ -5,6 +5,7 @@ import { TenantConflictError, asQuarantine, quarantine, } from "../../tenant/qua
5
5
  import { MIGRATIONS, lockSchema, migrateSchemaInTx, readSchemaVersion, schemaExists, } from "./migrations/index.js";
6
6
  import { TENANT_SCHEMA_PREFIX, tenantIdFromSchema, tenantSchemaName } from "./names.js";
7
7
  import { createPostgresSessionStore } from "./store.js";
8
+ import { STREAMS_SCHEMA, migrateStreamsSchema } from "./migrations/shared/index.js";
8
9
  /**
9
10
  * Whether Postgres rejected a statement (a SQLSTATE), rather than the connection or the
10
11
  * server failing (classes 08, 53, 57, and the driver's own connection errors). Only the
@@ -19,6 +20,12 @@ function isStatementError(error) {
19
20
  export function createPostgresTenantCatalog(options) {
20
21
  const { sql } = options;
21
22
  const now = options.now ?? (() => new Date());
23
+ // The shared record (`nylorun_streams`) exists before any Tenant is created or opened.
24
+ let streamsReady;
25
+ const ensureStreams = () => (streamsReady ??= migrateStreamsSchema(sql).catch((error) => {
26
+ streamsReady = undefined;
27
+ throw error;
28
+ }));
22
29
  const migrations = options.migrations ?? MIGRATIONS;
23
30
  const latest = migrations.length;
24
31
  async function readEnvelopeWith(q, id) {
@@ -104,6 +111,7 @@ export function createPostgresTenantCatalog(options) {
104
111
  return readEnvelopeWith(sql, id);
105
112
  },
106
113
  async createTenant({ envelope, principals }) {
114
+ await ensureStreams();
107
115
  const input = TenantEnvelopeSchema.parse(envelope);
108
116
  const schema = tenantSchemaName(input.id);
109
117
  const outcome = await sql.begin(async (tx) => {
@@ -145,6 +153,7 @@ export function createPostgresTenantCatalog(options) {
145
153
  async openTenant(id, storeOptions = {}) {
146
154
  if (!isTenantId(id))
147
155
  return { status: "not-found" };
156
+ await ensureStreams();
148
157
  const schema = tenantSchemaName(id);
149
158
  const version = await readSchemaVersion(sql, schema);
150
159
  if (version === undefined)
@@ -206,6 +215,11 @@ export function createPostgresTenantCatalog(options) {
206
215
  if (!(await schemaExists(tx, schema)))
207
216
  return false;
208
217
  await tx `DROP SCHEMA ${tx(schema)} CASCADE`;
218
+ // The Tenant's share of the record goes with it (Durable Streams §15).
219
+ if ((await schemaExists(tx, STREAMS_SCHEMA))) {
220
+ await tx `DELETE FROM ${tx(`${STREAMS_SCHEMA}.session_events`)} WHERE tenant_id = ${id}`;
221
+ await tx `DELETE FROM ${tx(`${STREAMS_SCHEMA}.session_log_heads`)} WHERE tenant_id = ${id}`;
222
+ }
209
223
  return true;
210
224
  });
211
225
  },