@nylorun/runtime 0.22.0-beta → 0.24.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 (273) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +95 -30
  3. package/dist/adapters/execution/restate.d.ts +15 -3
  4. package/dist/adapters/execution/restate.js +78 -84
  5. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  6. package/dist/adapters/replication/pgoutput.js +6 -3
  7. package/dist/adapters/sandbox/local.d.ts +9 -1
  8. package/dist/adapters/sandbox/local.js +16 -4
  9. package/dist/adapters/sandbox/virtual.js +4 -1
  10. package/dist/adapters/streams/s2.js +1 -13
  11. package/dist/api/a2a/endpoint.js +5 -1
  12. package/dist/api/a2a/routes.d.ts +2 -0
  13. package/dist/api/a2a/routes.js +2 -2
  14. package/dist/api/a2a/tasks.js +2 -1
  15. package/dist/api/ag-ui/endpoint.js +4 -2
  16. package/dist/api/ag-ui/history.js +4 -1
  17. package/dist/api/ag-ui/routes.d.ts +4 -1
  18. package/dist/api/ag-ui/routes.js +11 -10
  19. package/dist/api/ag-ui/translate.js +3 -2
  20. package/dist/api/components.d.ts +4953 -1204
  21. package/dist/api/components.js +25 -16
  22. package/dist/api/gate/keys.js +7 -1
  23. package/dist/api/gate/routes.d.ts +6 -0
  24. package/dist/api/gate/routes.js +39 -3
  25. package/dist/api/http/app.js +4 -4
  26. package/dist/api/http/define.d.ts +6 -0
  27. package/dist/api/http/define.js +13 -1
  28. package/dist/api/http/respond.js +1 -1
  29. package/dist/api/http/routes/artifacts.js +35 -8
  30. package/dist/api/http/routes/coverage.d.ts +9 -0
  31. package/dist/api/http/routes/coverage.js +34 -0
  32. package/dist/api/http/routes/environments.d.ts +11 -0
  33. package/dist/api/http/routes/environments.js +283 -0
  34. package/dist/api/http/routes/mcp.d.ts +1 -1
  35. package/dist/api/http/routes/mcp.js +2 -2
  36. package/dist/api/http/routes/me.js +1 -1
  37. package/dist/api/http/routes/reads.d.ts +45 -9
  38. package/dist/api/http/routes/reads.js +54 -5
  39. package/dist/api/http/routes/sessions.js +11 -6
  40. package/dist/api/http/routes/tenant.js +32 -10
  41. package/dist/api/openapi.js +31 -26
  42. package/dist/artifacts/disk.d.ts +59 -0
  43. package/dist/artifacts/disk.js +563 -0
  44. package/dist/artifacts/export.d.ts +1 -1
  45. package/dist/artifacts/export.js +2 -3
  46. package/dist/artifacts/folders.d.ts +6 -3
  47. package/dist/artifacts/folders.js +17 -12
  48. package/dist/artifacts/service.d.ts +2 -1
  49. package/dist/artifacts/service.js +6 -1
  50. package/dist/artifacts/workspace.d.ts +1 -1
  51. package/dist/artifacts/workspace.js +1 -1
  52. package/dist/core/flow-host.d.ts +3 -22
  53. package/dist/core/flow-host.js +11 -37
  54. package/dist/core/provider.d.ts +3 -2
  55. package/dist/core/provider.js +5 -37
  56. package/dist/core/sandbox-routes.d.ts +2 -21
  57. package/dist/core/sandbox-routes.js +10 -43
  58. package/dist/execution/types.d.ts +11 -9
  59. package/dist/execution/types.js +5 -7
  60. package/dist/gates/contract.d.ts +3 -24
  61. package/dist/gates/contract.js +1 -36
  62. package/dist/gates/egress-credentials.d.ts +9 -0
  63. package/dist/gates/egress-credentials.js +11 -4
  64. package/dist/gates/egress.d.ts +28 -1
  65. package/dist/gates/egress.js +115 -10
  66. package/dist/gates/fixture-model.d.ts +28 -0
  67. package/dist/gates/fixture-model.js +92 -0
  68. package/dist/gates/handler.js +4 -4
  69. package/dist/gates/http-tool.d.ts +6 -2
  70. package/dist/gates/http-tool.js +3 -5
  71. package/dist/gates/in-process.d.ts +5 -0
  72. package/dist/gates/in-process.js +12 -1
  73. package/dist/gates/journal.d.ts +30 -0
  74. package/dist/gates/journal.js +52 -0
  75. package/dist/gates/mcp-handler.d.ts +4 -0
  76. package/dist/gates/mcp-handler.js +5 -1
  77. package/dist/gates/meter.d.ts +10 -2
  78. package/dist/gates/meter.js +20 -5
  79. package/dist/gates/policy.d.ts +49 -0
  80. package/dist/gates/policy.js +134 -0
  81. package/dist/gates/tenant-vaults.d.ts +7 -7
  82. package/dist/gates/tenant-vaults.js +5 -5
  83. package/dist/gates/tool-calls.d.ts +10 -5
  84. package/dist/gates/tool-calls.js +28 -10
  85. package/dist/gates/tool-client.d.ts +8 -0
  86. package/dist/gates/tool-client.js +27 -11
  87. package/dist/gates/tool-contract.d.ts +10 -68
  88. package/dist/gates/tool-contract.js +1 -85
  89. package/dist/gates/tool-gate.d.ts +1 -1
  90. package/dist/harness/calls.d.ts +12 -5
  91. package/dist/harness/calls.js +21 -12
  92. package/dist/harness/executors.js +12 -9
  93. package/dist/harness/main.js +18 -5
  94. package/dist/harness/pod.d.ts +34 -0
  95. package/dist/harness/pod.js +41 -0
  96. package/dist/harness/service.d.ts +0 -1
  97. package/dist/harness/service.js +13 -5
  98. package/dist/harness/setup.d.ts +27 -0
  99. package/dist/harness/setup.js +150 -0
  100. package/dist/harness/ws-client.js +3 -2
  101. package/dist/harness/ws-port.d.ts +1 -5
  102. package/dist/harness/ws-port.js +1 -5
  103. package/dist/harness-api/in-process.js +3 -3
  104. package/dist/harness-api/record.d.ts +15 -1
  105. package/dist/harness-api/record.js +174 -44
  106. package/dist/harness-api/redact.d.ts +3 -0
  107. package/dist/harness-api/redact.js +45 -0
  108. package/dist/harness-api/server.d.ts +8 -0
  109. package/dist/harness-api/server.js +93 -13
  110. package/dist/harness-api/start.d.ts +10 -3
  111. package/dist/harness-api/start.js +40 -10
  112. package/dist/harness-api/tool-journal.d.ts +96 -0
  113. package/dist/harness-api/tool-journal.js +102 -0
  114. package/dist/harness-api/tool-status.d.ts +13 -0
  115. package/dist/harness-api/tool-status.js +26 -0
  116. package/dist/harness-api/workspace.d.ts +6 -3
  117. package/dist/harness-api/workspace.js +27 -11
  118. package/dist/harness-api/ws-server.d.ts +11 -3
  119. package/dist/harness-api/ws-server.js +94 -15
  120. package/dist/host/execution.d.ts +8 -6
  121. package/dist/host/gates.d.ts +2 -1
  122. package/dist/host/gates.js +10 -2
  123. package/dist/host/main.js +64 -21
  124. package/dist/host/operate.js +67 -5
  125. package/dist/host/stack-config.d.ts +70 -0
  126. package/dist/host/stack-config.js +67 -1
  127. package/dist/infra/execution.d.ts +18 -1
  128. package/dist/infra/execution.js +28 -4
  129. package/dist/infra/index.d.ts +1 -1
  130. package/dist/keys/client.js +4 -1
  131. package/dist/keys/keys.d.ts +5 -5
  132. package/dist/keys/keys.js +5 -7
  133. package/dist/management-openapi.json +999 -2211
  134. package/dist/mcp/connect.d.ts +25 -20
  135. package/dist/mcp/connect.js +29 -30
  136. package/dist/mcp/pool.d.ts +2 -2
  137. package/dist/mcp/pool.js +1 -1
  138. package/dist/mcp/preview.d.ts +3 -3
  139. package/dist/mcp/preview.js +10 -20
  140. package/dist/mcp/snapshot.d.ts +5 -11
  141. package/dist/mcp/snapshot.js +6 -7
  142. package/dist/model/pi-model.js +8 -0
  143. package/dist/openapi.json +6955 -2526
  144. package/dist/reads/types.d.ts +16 -3
  145. package/dist/record/append.d.ts +3 -1
  146. package/dist/record/append.js +1 -0
  147. package/dist/record/envelope.d.ts +5 -1
  148. package/dist/record/envelope.js +7 -3
  149. package/dist/record/environment.d.ts +25 -0
  150. package/dist/record/environment.js +44 -0
  151. package/dist/record/index.d.ts +1 -1
  152. package/dist/record/index.js +1 -1
  153. package/dist/retry.d.ts +21 -0
  154. package/dist/retry.js +34 -0
  155. package/dist/sandbox/disk-fs.d.ts +95 -0
  156. package/dist/sandbox/disk-fs.js +206 -0
  157. package/dist/sandbox/join.d.ts +17 -18
  158. package/dist/sandbox/join.js +46 -20
  159. package/dist/sandbox/manager.d.ts +16 -8
  160. package/dist/sandbox/manager.js +33 -17
  161. package/dist/sandbox/placement.d.ts +13 -12
  162. package/dist/sandbox/placement.js +29 -12
  163. package/dist/sandbox/pods/client.d.ts +6 -1
  164. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  165. package/dist/sandbox/pods/lifecycle.js +14 -26
  166. package/dist/sandbox/pods/reconcile.js +21 -15
  167. package/dist/sandbox/pods/spec.d.ts +15 -13
  168. package/dist/sandbox/pods/spec.js +16 -20
  169. package/dist/sandbox/records.d.ts +2 -2
  170. package/dist/sandbox/records.js +3 -3
  171. package/dist/sandbox/resolve.d.ts +35 -27
  172. package/dist/sandbox/resolve.js +66 -41
  173. package/dist/sandbox/session-sandbox.js +1 -1
  174. package/dist/sandbox/setup.d.ts +61 -0
  175. package/dist/sandbox/setup.js +123 -0
  176. package/dist/sandbox/share.d.ts +11 -21
  177. package/dist/sandbox/share.js +12 -41
  178. package/dist/sandbox/tenant-config.d.ts +12 -14
  179. package/dist/sandbox/tenant-config.js +12 -17
  180. package/dist/sandbox/types.d.ts +6 -0
  181. package/dist/store/postgres/control.js +8 -1
  182. package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
  183. package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
  184. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  185. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  186. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  187. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  188. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  189. package/dist/store/postgres/reads.js +117 -17
  190. package/dist/store/postgres/record-writer.d.ts +4 -4
  191. package/dist/store/postgres/record-writer.js +9 -9
  192. package/dist/store/postgres/record.d.ts +7 -0
  193. package/dist/store/postgres/record.js +23 -2
  194. package/dist/store/postgres/schema.d.ts +1213 -779
  195. package/dist/store/postgres/schema.js +136 -76
  196. package/dist/store/postgres/store.js +283 -117
  197. package/dist/store/types.d.ts +206 -88
  198. package/dist/streams/relay/core.js +1 -13
  199. package/dist/tenant/advance.d.ts +7 -6
  200. package/dist/tenant/advance.js +121 -49
  201. package/dist/tenant/artifact-tool.js +3 -4
  202. package/dist/tenant/commands.d.ts +15 -2
  203. package/dist/tenant/commands.js +94 -14
  204. package/dist/tenant/context.d.ts +67 -33
  205. package/dist/tenant/context.js +4 -15
  206. package/dist/tenant/control.d.ts +8 -1
  207. package/dist/tenant/control.js +2 -0
  208. package/dist/tenant/effects.d.ts +1 -1
  209. package/dist/tenant/effects.js +37 -20
  210. package/dist/tenant/environments.d.ts +167 -0
  211. package/dist/tenant/environments.js +823 -0
  212. package/dist/tenant/harness-tokens.d.ts +21 -0
  213. package/dist/tenant/harness-tokens.js +61 -0
  214. package/dist/tenant/host-token.d.ts +1 -1
  215. package/dist/tenant/host-token.js +2 -4
  216. package/dist/tenant/identity-file.d.ts +4 -4
  217. package/dist/tenant/identity-file.js +17 -8
  218. package/dist/tenant/issuers.js +3 -3
  219. package/dist/tenant/principals.js +1 -1
  220. package/dist/tenant/reset.js +3 -3
  221. package/dist/tenant/runtime.d.ts +6 -1
  222. package/dist/tenant/runtime.js +30 -4
  223. package/dist/tenant/scheduler.d.ts +7 -0
  224. package/dist/tenant/scheduler.js +38 -7
  225. package/dist/tenant/session-streams.d.ts +0 -1
  226. package/dist/tenant/session-streams.js +1 -13
  227. package/dist/tenant/session.d.ts +3 -3
  228. package/dist/tenant/session.js +0 -14
  229. package/dist/tenant/sessions.d.ts +13 -13
  230. package/dist/tenant/sessions.js +61 -138
  231. package/dist/tenant/signing-keys.d.ts +5 -1
  232. package/dist/tenant/status.js +3 -3
  233. package/dist/tenant/streams.js +14 -29
  234. package/dist/tenant/sweep.d.ts +39 -12
  235. package/dist/tenant/sweep.js +168 -63
  236. package/dist/tenant/transcript.d.ts +210 -20
  237. package/dist/tenant/transcript.js +62 -11
  238. package/dist/tenant/turn-timing.d.ts +43 -0
  239. package/dist/tenant/turn-timing.js +39 -0
  240. package/dist/tenant/types.d.ts +2 -0
  241. package/dist/traces/ids.d.ts +42 -0
  242. package/dist/traces/ids.js +85 -0
  243. package/dist/traces/key.d.ts +6 -0
  244. package/dist/traces/key.js +37 -0
  245. package/dist/traces/map.d.ts +113 -0
  246. package/dist/traces/map.js +317 -0
  247. package/dist/traces/options.d.ts +7 -0
  248. package/dist/traces/options.js +1 -0
  249. package/dist/traces/otlp.d.ts +42 -0
  250. package/dist/traces/otlp.js +84 -0
  251. package/dist/traces/projector.d.ts +58 -0
  252. package/dist/traces/projector.js +200 -0
  253. package/dist/traces/read.d.ts +23 -0
  254. package/dist/traces/read.js +16 -0
  255. package/dist/vault/coverage.d.ts +26 -0
  256. package/dist/vault/coverage.js +64 -0
  257. package/dist/vault/error.d.ts +11 -1
  258. package/dist/vault/error.js +5 -1
  259. package/dist/vault/service.d.ts +47 -48
  260. package/dist/vault/service.js +321 -382
  261. package/dist/vault/sources.d.ts +5 -16
  262. package/dist/vault/sources.js +6 -8
  263. package/dist/version.d.ts +1 -1
  264. package/dist/version.js +2 -2
  265. package/package.json +14 -13
  266. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  267. package/dist/api/http/routes/sandboxes.js +0 -153
  268. package/dist/api/http/routes/vaults.d.ts +0 -10
  269. package/dist/api/http/routes/vaults.js +0 -139
  270. package/dist/record/sandbox.d.ts +0 -25
  271. package/dist/record/sandbox.js +0 -43
  272. package/dist/tenant/sandboxes.d.ts +0 -122
  273. package/dist/tenant/sandboxes.js +0 -562
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * Every `tx` is one READ COMMITTED transaction on a pooled connection (Drizzle's
17
17
  * `transaction` on postgres.js `begin`). Tables are addressed by schema-qualified names,
18
- * never through `search_path`. Commit listeners and `afterCommit` callbacks run after `COMMIT`
18
+ * never through `search_path`. Commit listeners and outbox deliveries run after `COMMIT`
19
19
  * returns and never reject the committed `tx`. Nested `tx` calls are detected with
20
20
  * `AsyncLocalStorage` and rejected, and a `Tx` rejects every call once its callback settles.
21
21
  * A failed statement rejects `tx` with the driver's error (`driverError`), not Drizzle's
@@ -32,7 +32,7 @@
32
32
  * head).
33
33
  * 2. A transaction that touches several sessions locks them in ascending id
34
34
  * order, with `lockSessions` (`./locking.ts`), before writing any of them.
35
- * 3. A sandbox resource's row (`sandboxResource(id, { lock: true })`, which orders its
35
+ * 3. An Environment's row (`environment(id, { lock: true })`, which orders its
36
36
  * lifecycle stream) is locked after every session row the transaction locks, never
37
37
  * before one.
38
38
  *
@@ -50,15 +50,15 @@
50
50
  */
51
51
  import { AsyncLocalStorage } from "node:async_hooks";
52
52
  import { randomUUID } from "node:crypto";
53
- import { and, count, eq, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
54
- import { appendEvent, appendSandboxEvent } from "../../record/index.js";
53
+ import { and, count, eq, getTableColumns, gte, gt, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
54
+ import { appendEnvironmentEvent, appendEvent } from "../../record/index.js";
55
55
  import { OwnershipLostError, PrincipalRoleConflict } from "../ownership.js";
56
56
  import { followControlSignals, pruneSignals, writeSignal } from "./control.js";
57
57
  import { database, driverError } from "./db.js";
58
58
  import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
59
59
  import { createPostgresRecordReader } from "./record.js";
60
- import { postgresRecordWriter, postgresSandboxRecordWriter } from "./record-writer.js";
61
- import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, definitions, effects, links, modelBudgets, modelUsage, principals, sandboxes, sandboxEvents, sandboxResources, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, wakes, } from "./schema.js";
60
+ import { postgresEnvironmentRecordWriter, postgresRecordWriter } from "./record-writer.js";
61
+ import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, definitions, effects, links, modelBudgets, modelUsage, principals, sandboxes, environmentEvents, environments, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, sandboxSignals, wakes, } from "./schema.js";
62
62
  export function createPostgresSessionStore(options) {
63
63
  return new PostgresSessionStore(options);
64
64
  }
@@ -88,6 +88,9 @@ class PostgresSessionStore {
88
88
  active = new AsyncLocalStorage();
89
89
  listeners = new Set();
90
90
  inflight = new Set();
91
+ /** Deliveries of committed outbox requests still running (`deliverTo`). */
92
+ deliveries = new Set();
93
+ deliver;
91
94
  now;
92
95
  onError;
93
96
  closed = false;
@@ -111,7 +114,7 @@ class PostgresSessionStore {
111
114
  let t;
112
115
  // Wrapped so `begin` does not treat an array result as queries to await.
113
116
  const run = this.db.transaction(async (db) => {
114
- t = new PostgresTx(db, this.tenantId, this.now, this.deleteWake);
117
+ t = new PostgresTx(db, this.tenantId, this.now);
115
118
  try {
116
119
  return { value: await this.active.run(this, () => fn(t)) };
117
120
  }
@@ -144,20 +147,51 @@ class PostgresSessionStore {
144
147
  }
145
148
  }
146
149
  }
147
- for (const callback of t.callbacks) {
148
- try {
149
- await callback();
150
- }
151
- catch (error) {
152
- this.onError(error);
153
- }
154
- }
150
+ if (t.outbox.length > 0)
151
+ this.send(t.outbox);
155
152
  return result.value;
156
153
  }
157
- /** Deletes a delivered outbox request, outside any transaction (`Tx.wake`). */
158
- deleteWake = async (id) => {
159
- await this.db.delete(wakes).where(eq(wakes.id, id));
160
- };
154
+ deliverTo(deliver) {
155
+ this.deliver = deliver;
156
+ }
157
+ async delivered() {
158
+ while (this.deliveries.size > 0)
159
+ await Promise.allSettled([...this.deliveries]);
160
+ }
161
+ /**
162
+ * Hands a commit's outbox requests to the delivery, one after the other, without the caller
163
+ * waiting: a delivered one's row is deleted, and one that was not is due at the sweep's next
164
+ * pass.
165
+ */
166
+ send(committed) {
167
+ const deliver = this.deliver;
168
+ if (!deliver)
169
+ return;
170
+ const run = (async () => {
171
+ for (const { kind, id, request } of committed) {
172
+ let sent = false;
173
+ try {
174
+ sent = await deliver(request);
175
+ }
176
+ catch (error) {
177
+ this.onError(error);
178
+ }
179
+ const table = OUTBOX[kind];
180
+ try {
181
+ if (sent === false)
182
+ await this.db.update(table).set({ retryAt: sql `clock_timestamp()` }).where(eq(table.id, id));
183
+ else
184
+ await this.db.delete(table).where(eq(table.id, id));
185
+ }
186
+ catch (error) {
187
+ // The row stays as it was: the sweep sends it again, under the same key.
188
+ this.onError(error);
189
+ }
190
+ }
191
+ })();
192
+ this.deliveries.add(run);
193
+ void run.finally(() => this.deliveries.delete(run));
194
+ }
161
195
  onCommit(listener) {
162
196
  this.listeners.add(listener);
163
197
  return () => this.listeners.delete(listener);
@@ -186,10 +220,14 @@ class PostgresSessionStore {
186
220
  throw new Error("SessionStore is closed");
187
221
  return followControlSignals(this.sql, this.db, onSignal, options);
188
222
  }
189
- /** Rejects new transactions and waits for running ones. Does not end the pool. */
223
+ /**
224
+ * Rejects new transactions and waits for running ones, and for the deliveries of committed
225
+ * outbox requests. Does not end the pool.
226
+ */
190
227
  async close() {
191
228
  this.closed = true;
192
229
  await Promise.allSettled([...this.inflight]);
230
+ await this.delivered();
193
231
  }
194
232
  }
195
233
  function storedSession(row) {
@@ -202,14 +240,21 @@ function ownership(row) {
202
240
  ownerExpiresAt: row.ownerExpiresAt?.toISOString() ?? null,
203
241
  };
204
242
  }
205
- function sandboxResourceOf(row) {
243
+ function environmentOf(row) {
206
244
  return {
207
245
  id: row.id,
208
246
  kind: row.kind,
209
- spec: row.spec,
210
247
  labels: row.labels,
248
+ recipe: row.recipe,
249
+ applied: row.applied,
250
+ version: row.version,
251
+ appliedVersion: row.appliedVersion,
252
+ ...(row.ownerUserId !== null ? { ownerUserId: row.ownerUserId } : {}),
253
+ lastUsedAt: new Date(row.lastUsedAt).toISOString(),
211
254
  createdAt: row.createdAt,
212
255
  updatedAt: row.updatedAt,
256
+ ...(row.diskOp ? { diskOp: row.diskOp } : {}),
257
+ ...(row.diskSeed ? { diskSeed: row.diskSeed } : {}),
213
258
  ...(row.k8sName !== null && row.desired !== null && row.observed !== null
214
259
  ? {
215
260
  pod: {
@@ -222,7 +267,6 @@ function sandboxResourceOf(row) {
222
267
  ...(row.joinTokenHash !== null ? { joinTokenHash: row.joinTokenHash } : {}),
223
268
  rev: row.rev,
224
269
  ...(row.lastActiveAt ? { lastActiveAt: row.lastActiveAt.toISOString() } : {}),
225
- ...(row.expiresAt ? { expiresAt: row.expiresAt.toISOString() } : {}),
226
270
  ...(row.startedAt ? { startedAt: row.startedAt.toISOString() } : {}),
227
271
  ...(row.reason !== null ? { reason: row.reason } : {}),
228
272
  ...(row.retiring !== null ? { retiring: row.retiring } : {}),
@@ -245,7 +289,6 @@ function podColumns(patch) {
245
289
  joinTokenHash: patch.joinTokenHash,
246
290
  rev: patch.rev,
247
291
  lastActiveAt: date(patch.lastActiveAt),
248
- expiresAt: date(patch.expiresAt),
249
292
  startedAt: date(patch.startedAt),
250
293
  reason: patch.reason,
251
294
  retiring: patch.retiring,
@@ -262,26 +305,49 @@ const SIGNING_KEY_STAMP = {
262
305
  /** The audit columns a row has (`ord` only orders them). */
263
306
  const { ord: _ord, ...AUDIT } = getTableColumns(vaultAudit);
264
307
  const SESSION_TABLES = [sessions, commands, effects, links, wakes];
265
- /** An outbox row as it is delivered: the wake's own dedupe key, or one made from the row id. */
308
+ /**
309
+ * The outbox's tables, by the kind of request each holds. Their delivery columns (`id`,
310
+ * `created_at`, `attempts`, `retry_at`, `parked_at`) are the same.
311
+ */
312
+ const OUTBOX = {
313
+ wake: wakes,
314
+ sandbox: sandboxSignals,
315
+ };
316
+ /** A wake outbox row as it is delivered: its own dedupe key, or one made from the row id. */
266
317
  function outboxWake(id, reason, dedupeKey) {
267
318
  return { reason, dedupeKey: dedupeKey ?? `wake:${id}` };
268
319
  }
320
+ /** A sandbox signal outbox row as it is delivered: a reconcile is keyed by the row. */
321
+ function outboxSignal(id, row) {
322
+ return row.kind === "arm"
323
+ ? { kind: "arm", timer: row.timer, at: row.at }
324
+ : { kind: "reconcile", key: `signal:${id}` };
325
+ }
326
+ /** When an outbox row of `table` is due, after a grace of `graceMs` (the database's clock). */
327
+ function dueAt(table, graceMs) {
328
+ const grace = sql `make_interval(secs => ${Math.max(0, graceMs) / 1000})`;
329
+ return sql `coalesce(${table.retryAt}, ${table.createdAt} + ${grace})`;
330
+ }
331
+ /** Due rows of `table`, soonest first: their epoch seconds, to merge two tables' rows. */
332
+ const DUE = (table, graceMs) => ({
333
+ due: dueAt(table, graceMs),
334
+ dueSeconds: sql `extract(epoch from ${dueAt(table, graceMs)})::float8`,
335
+ });
269
336
  /** The inserted row's `column` (`ON CONFLICT … DO UPDATE`). */
270
337
  const excluded = (column) => sql.raw(`excluded.${column}`);
271
338
  class PostgresTx {
272
339
  db;
273
340
  tenantId;
274
341
  now;
275
- deleteWake;
276
342
  closed = false;
277
343
  events = [];
278
344
  generations = [];
279
- callbacks = [];
280
- constructor(db, tenantId, now, deleteWake) {
345
+ /** The outbox requests this transaction wrote, delivered after it commits. */
346
+ outbox = [];
347
+ constructor(db, tenantId, now) {
281
348
  this.db = db;
282
349
  this.tenantId = tenantId;
283
350
  this.now = now;
284
- this.deleteWake = deleteWake;
285
351
  }
286
352
  check() {
287
353
  if (this.closed)
@@ -328,7 +394,7 @@ class PostgresTx {
328
394
  const [row] = await this.sessions().where(eq(sessions.id, id)).for("update");
329
395
  return row && storedSession(row);
330
396
  }
331
- async event(sessionId, turnId, type, payload) {
397
+ async event(sessionId, turnId, type, payload, writer) {
332
398
  this.check();
333
399
  const db = this.db;
334
400
  // The session row lock orders the session's events; the log head allocates under it.
@@ -347,17 +413,14 @@ class PostgresTx {
347
413
  time: this.now(),
348
414
  type,
349
415
  payload,
416
+ ...(writer ? { writer } : {}),
350
417
  });
351
418
  this.events.push(event);
352
419
  this.generations.push(generation);
353
420
  return structuredClone(event);
354
421
  }
355
- afterCommit(fn) {
356
- this.check();
357
- this.callbacks.push(fn);
358
- }
359
422
  // --- wake outbox ---------------------------------------------------------
360
- async wake(sessionId, wake, deliver) {
423
+ async wake(sessionId, wake) {
361
424
  this.check();
362
425
  const id = randomUUID();
363
426
  await this.db.insert(wakes).values({
@@ -366,34 +429,90 @@ class PostgresTx {
366
429
  reason: wake.reason,
367
430
  dedupeKey: wake.dedupeKey ?? null,
368
431
  });
369
- const delivered = outboxWake(id, wake.reason, wake.dedupeKey ?? null);
370
- // A rejection goes to the store's error hook (as every callback's), and the row stays.
371
- this.callbacks.push(async () => {
372
- if ((await deliver(sessionId, delivered)) === false)
373
- return;
374
- await this.deleteWake(id);
432
+ this.outbox.push({
433
+ kind: "wake",
434
+ id,
435
+ request: { kind: "wake", sessionId, wake: outboxWake(id, wake.reason, wake.dedupeKey ?? null) },
375
436
  });
376
437
  }
377
- async pendingWakes(before, limit) {
438
+ async signalSandbox(sandboxId, signal) {
378
439
  this.check();
379
- const rows = await this.db
380
- .select()
440
+ const id = randomUUID();
441
+ const row = {
442
+ kind: signal.kind,
443
+ timer: signal.kind === "arm" ? signal.timer : null,
444
+ at: signal.kind === "arm" ? signal.at : null,
445
+ };
446
+ await this.db.insert(sandboxSignals).values({ id, sandboxId, ...row });
447
+ this.outbox.push({
448
+ kind: "sandbox",
449
+ id,
450
+ request: { kind: "sandbox", sandboxId, signal: outboxSignal(id, row) },
451
+ });
452
+ }
453
+ async pendingOutbox(graceMs, limit) {
454
+ this.check();
455
+ // Compared on the database's clock, which wrote `created_at` and `retry_at`.
456
+ const now = sql `clock_timestamp()`;
457
+ const wake = DUE(wakes, graceMs);
458
+ const signal = DUE(OUTBOX.sandbox, graceMs);
459
+ const wakeRows = await this.db
460
+ .select({ ...getTableColumns(wakes), dueSeconds: wake.dueSeconds })
381
461
  .from(wakes)
382
- .where(lt(wakes.createdAt, before))
383
- .orderBy(wakes.createdAt, wakes.id)
462
+ .where(and(isNull(wakes.parkedAt), lte(wake.due, now)))
463
+ .orderBy(wake.due, wakes.id)
384
464
  .limit(limit);
385
- return rows.map((row) => ({
386
- id: row.id,
387
- sessionId: row.sessionId,
388
- wake: outboxWake(row.id, row.reason, row.dedupeKey),
389
- createdAt: row.createdAt.toISOString(),
390
- }));
465
+ const signalRows = await this.db
466
+ .select({ ...getTableColumns(sandboxSignals), dueSeconds: signal.dueSeconds })
467
+ .from(sandboxSignals)
468
+ .where(and(isNull(sandboxSignals.parkedAt), lte(signal.due, now)))
469
+ .orderBy(signal.due, sandboxSignals.id)
470
+ .limit(limit);
471
+ const rows = [
472
+ ...wakeRows.map((row) => ({
473
+ kind: "wake",
474
+ id: row.id,
475
+ request: {
476
+ kind: "wake",
477
+ sessionId: row.sessionId,
478
+ wake: outboxWake(row.id, row.reason, row.dedupeKey),
479
+ },
480
+ createdAt: row.createdAt.toISOString(),
481
+ attempts: row.attempts,
482
+ due: row.dueSeconds,
483
+ })),
484
+ ...signalRows.map((row) => ({
485
+ kind: "sandbox",
486
+ id: row.id,
487
+ request: { kind: "sandbox", sandboxId: row.sandboxId, signal: outboxSignal(row.id, row) },
488
+ createdAt: row.createdAt.toISOString(),
489
+ attempts: row.attempts,
490
+ due: row.dueSeconds,
491
+ })),
492
+ ];
493
+ rows.sort((a, b) => a.due - b.due || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
494
+ return rows.slice(0, limit).map(({ due: _due, ...row }) => row);
495
+ }
496
+ async deleteOutbox(rows) {
497
+ this.check();
498
+ for (const kind of ["wake", "sandbox"]) {
499
+ const ids = rows.filter((row) => row.kind === kind).map((row) => row.id);
500
+ if (ids.length > 0)
501
+ await this.db.delete(OUTBOX[kind]).where(inArray(OUTBOX[kind].id, ids));
502
+ }
391
503
  }
392
- async deleteWakes(ids) {
504
+ async failOutbox(row, retry) {
393
505
  this.check();
394
- if (ids.length === 0)
395
- return;
396
- await this.db.delete(wakes).where(inArray(wakes.id, [...ids]));
506
+ const table = OUTBOX[row.kind];
507
+ const secs = Math.max(0, retry.retryInMs) / 1000;
508
+ await this.db
509
+ .update(table)
510
+ .set({
511
+ attempts: sql `${table.attempts} + 1`,
512
+ retryAt: sql `clock_timestamp() + make_interval(secs => ${secs})`,
513
+ ...(retry.park ? { parkedAt: sql `clock_timestamp()` } : {}),
514
+ })
515
+ .where(eq(table.id, row.id));
397
516
  }
398
517
  // --- ownership -----------------------------------------------------------
399
518
  async takeOwnership(sessionId, claim) {
@@ -470,12 +589,11 @@ class PostgresTx {
470
589
  .orderBy(sessions.id);
471
590
  return rows.map((row) => storedSession(row));
472
591
  }
473
- async orphanedSessions(now, limit) {
592
+ async orphanedSessions(now, limit, after) {
474
593
  this.check();
475
594
  const rows = await this.sessions()
476
- .where(and(inArray(sessions.status, OPEN_SESSION), or(isNull(sessions.owner), isNull(sessions.ownerExpiresAt), lte(sessions.ownerExpiresAt, now))))
477
- // Never-owned sessions first, then the longest expired.
478
- .orderBy(sql `CASE WHEN ${sessions.owner} IS NULL THEN NULL ELSE ${sessions.ownerExpiresAt} END ASC NULLS FIRST`, sessions.id)
595
+ .where(and(inArray(sessions.status, OPEN_SESSION), or(isNull(sessions.owner), isNull(sessions.ownerExpiresAt), lte(sessions.ownerExpiresAt, now)), after === undefined ? undefined : gt(sessions.id, after)))
596
+ .orderBy(sessions.id)
479
597
  .limit(limit);
480
598
  return rows.map((row) => storedSession(row));
481
599
  }
@@ -500,88 +618,92 @@ class PostgresTx {
500
618
  this.check();
501
619
  return this.bodies(sandboxes);
502
620
  }
503
- // --- sandbox resources ---------------------------------------------------
504
- async sandboxResource(id, options = {}) {
621
+ // --- Environments ----------------------------------------------------------
622
+ async environment(id, options = {}) {
505
623
  this.check();
506
- const query = this.db.select().from(sandboxResources).where(eq(sandboxResources.id, id));
624
+ const query = this.db.select().from(environments).where(eq(environments.id, id));
507
625
  const [row] = options.lock ? await query.for("update") : await query;
508
- return row && sandboxResourceOf(row);
626
+ return row && environmentOf(row);
509
627
  }
510
- async createSandboxResource(row, limit) {
628
+ async createEnvironment(row, limit) {
511
629
  this.check();
512
630
  // Every create takes this lock, so two never both see room for one more.
513
- await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.sandbox_resources'))`);
631
+ await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.environments'))`);
514
632
  const [found] = await this.db
515
- .select({ id: sandboxResources.id })
516
- .from(sandboxResources)
517
- .where(eq(sandboxResources.id, row.id));
633
+ .select({ id: environments.id })
634
+ .from(environments)
635
+ .where(eq(environments.id, row.id));
518
636
  if (found)
519
637
  return "exists";
520
- const [counted] = await this.db.select({ n: count() }).from(sandboxResources);
638
+ const [counted] = await this.db.select({ n: count() }).from(environments);
521
639
  if ((counted?.n ?? 0) >= limit)
522
640
  return "limit";
523
- const { pod, ...resource } = row;
524
- await this.db.insert(sandboxResources).values({ ...resource, ...(pod ? podColumns(pod) : {}) });
641
+ const { pod, ownerUserId, diskOp, diskSeed, ...environment } = row;
642
+ await this.db.insert(environments).values({
643
+ ...environment,
644
+ ownerUserId: ownerUserId ?? null,
645
+ diskOp: diskOp ?? null,
646
+ diskSeed: diskSeed ?? null,
647
+ ...(pod ? podColumns(pod) : {}),
648
+ });
525
649
  return "created";
526
650
  }
527
- async updateSandboxSpec(id, spec, updatedAt) {
528
- this.check();
529
- await this.db.update(sandboxResources).set({ spec, updatedAt }).where(eq(sandboxResources.id, id));
530
- }
531
- async updateSandboxPod(id, patch, updatedAt) {
651
+ async updateEnvironment(id, patch, updatedAt) {
532
652
  this.check();
653
+ const columns = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined));
533
654
  await this.db
534
- .update(sandboxResources)
535
- .set({ ...podColumns(patch), updatedAt })
536
- .where(eq(sandboxResources.id, id));
655
+ .update(environments)
656
+ .set({ ...columns, updatedAt })
657
+ .where(eq(environments.id, id));
537
658
  }
538
- async updateSandboxLabels(id, labels, updatedAt) {
659
+ async updateEnvironmentPod(id, patch, updatedAt) {
539
660
  this.check();
540
661
  await this.db
541
- .update(sandboxResources)
542
- .set({ labels, updatedAt })
543
- .where(eq(sandboxResources.id, id));
662
+ .update(environments)
663
+ .set({ ...podColumns(patch), updatedAt })
664
+ .where(eq(environments.id, id));
544
665
  }
545
- async deleteSandboxResource(id) {
666
+ async deleteEnvironment(id) {
546
667
  this.check();
547
- await this.db.delete(sandboxResources).where(eq(sandboxResources.id, id));
668
+ // Its vault and credentials go with it (ON DELETE CASCADE).
669
+ await this.db.delete(environments).where(eq(environments.id, id));
548
670
  }
549
- async listSandboxResources(filter = {}) {
671
+ async listEnvironments(filter = {}) {
550
672
  this.check();
551
673
  const labels = filter.labels;
552
674
  const rows = await this.db
553
675
  .select()
554
- .from(sandboxResources)
676
+ .from(environments)
555
677
  .where(labels === undefined || Object.keys(labels).length === 0
556
678
  ? undefined
557
- : sql `nylorun.doc(${sandboxResources.labels}) @> ${toJson(labels)}::jsonb`)
558
- .orderBy(sandboxResources.id);
559
- return rows.map(sandboxResourceOf);
679
+ : sql `nylorun.doc(${environments.labels}) @> ${toJson(labels)}::jsonb`)
680
+ .orderBy(environments.id);
681
+ return rows.map(environmentOf);
560
682
  }
561
- async sessionsOnSandbox(sandboxId) {
683
+ async sessionsOnEnvironment(environmentId) {
562
684
  this.check();
563
685
  const rows = await this.sessions()
564
- .where(eq(sessions.sandboxId, sandboxId))
686
+ .where(eq(sessions.environmentId, environmentId))
565
687
  .orderBy(sessions.id);
566
688
  return rows.map((row) => storedSession(row));
567
689
  }
568
- async sandboxEvent(sandboxId, type, payload) {
690
+ async environmentEvent(environmentId, type, payload) {
569
691
  this.check();
570
- return appendSandboxEvent(postgresSandboxRecordWriter(this.db), {
692
+ return appendEnvironmentEvent(postgresEnvironmentRecordWriter(this.db), {
571
693
  tenantId: this.tenantId,
572
- sandboxId,
694
+ environmentId,
573
695
  time: this.now(),
574
696
  type,
575
697
  payload,
576
698
  });
577
699
  }
578
- async sandboxEvents(sandboxId, options = {}) {
700
+ async environmentEvents(environmentId, options = {}) {
579
701
  this.check();
580
702
  const rows = await this.db
581
- .select({ body: sandboxEvents.body })
582
- .from(sandboxEvents)
583
- .where(and(eq(sandboxEvents.sandboxId, sandboxId), options.fromSeq === undefined ? undefined : gte(sandboxEvents.seq, options.fromSeq)))
584
- .orderBy(sandboxEvents.seq)
703
+ .select({ body: environmentEvents.body })
704
+ .from(environmentEvents)
705
+ .where(and(eq(environmentEvents.environmentId, environmentId), options.fromSeq === undefined ? undefined : gte(environmentEvents.seq, options.fromSeq)))
706
+ .orderBy(environmentEvents.seq)
585
707
  .limit(options.limit ?? 1000);
586
708
  return rows.map((row) => row.body);
587
709
  }
@@ -719,21 +841,13 @@ class PostgresTx {
719
841
  const [row] = await this.db.select().from(vaults).where(eq(vaults.id, id));
720
842
  return row;
721
843
  }
722
- async vaultsByOwner(ownerUserId) {
723
- this.check();
724
- return this.db
725
- .select()
726
- .from(vaults)
727
- .where(and(eq(vaults.ownerUserId, ownerUserId), eq(vaults.scope, "user")))
728
- .orderBy(vaults.createdAt, vaults.id);
729
- }
730
- async installationVaults() {
844
+ async vaultOfEnvironment(environmentId) {
731
845
  this.check();
732
- return this.db
846
+ const [row] = await this.db
733
847
  .select()
734
848
  .from(vaults)
735
- .where(eq(vaults.scope, "installation"))
736
- .orderBy(vaults.createdAt, vaults.id);
849
+ .where(eq(vaults.environmentId, environmentId));
850
+ return row;
737
851
  }
738
852
  async updateVaultMetadata(id, metadataJson) {
739
853
  this.check();
@@ -756,6 +870,14 @@ class PostgresTx {
756
870
  .where(eq(vaultCredentials.id, id));
757
871
  return row;
758
872
  }
873
+ async credentialByName(vaultId, name) {
874
+ this.check();
875
+ const [row] = await this.db
876
+ .select()
877
+ .from(vaultCredentials)
878
+ .where(and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.name, name)));
879
+ return row;
880
+ }
759
881
  async credentialsForVault(vaultId, filter = {}) {
760
882
  this.check();
761
883
  return this.db
@@ -864,6 +986,16 @@ class PostgresTx {
864
986
  .returning();
865
987
  return inserted;
866
988
  }
989
+ async modelUsageOf(effectKey) {
990
+ this.check();
991
+ const [row] = await this.db
992
+ .select()
993
+ .from(modelUsage)
994
+ .where(and(eq(modelUsage.effectKey, effectKey), eq(modelUsage.duplicate, false)))
995
+ .orderBy(modelUsage.createdAt, modelUsage.id)
996
+ .limit(1);
997
+ return row;
998
+ }
867
999
  async modelUsageTotals(query) {
868
1000
  this.check();
869
1001
  const [row] = await this.db
@@ -904,6 +1036,15 @@ class PostgresTx {
904
1036
  .returning({ key: toolCrossings.key });
905
1037
  return inserted.length === 1;
906
1038
  }
1039
+ async restartToolCrossing(row) {
1040
+ this.check();
1041
+ const restarted = await this.db
1042
+ .update(toolCrossings)
1043
+ .set({ startedAt: row.startedAt })
1044
+ .where(and(eq(toolCrossings.key, row.key), eq(toolCrossings.hash, row.hash), isNull(toolCrossings.settledAt)))
1045
+ .returning({ key: toolCrossings.key });
1046
+ return restarted.length === 1;
1047
+ }
907
1048
  async settleToolCrossing(key, answer, settledAt) {
908
1049
  this.check();
909
1050
  await this.db
@@ -926,6 +1067,12 @@ class PostgresTx {
926
1067
  const [row] = options.lock ? await query.for("update") : await query;
927
1068
  return row;
928
1069
  }
1070
+ async diskArtifact(environmentId, options = {}) {
1071
+ this.check();
1072
+ const query = this.db.select().from(artifacts).where(eq(artifacts.environmentId, environmentId));
1073
+ const [row] = options.lock ? await query.for("update") : await query;
1074
+ return row;
1075
+ }
929
1076
  async listArtifacts(filter = {}) {
930
1077
  this.check();
931
1078
  return this.db
@@ -976,6 +1123,24 @@ class PostgresTx {
976
1123
  await this.db.delete(artifacts).where(eq(artifacts.id, id));
977
1124
  return deleted.map((row) => row.blobKey);
978
1125
  }
1126
+ async deleteArtifactVersion(artifactId, version) {
1127
+ this.check();
1128
+ const [deleted] = await this.db
1129
+ .delete(artifactVersions)
1130
+ .where(and(eq(artifactVersions.artifactId, artifactId), eq(artifactVersions.version, version)))
1131
+ .returning({ blobKey: artifactVersions.blobKey });
1132
+ if (!deleted)
1133
+ return undefined;
1134
+ const [left] = await this.db
1135
+ .select({ latest: sql `max(${artifactVersions.version})` })
1136
+ .from(artifactVersions)
1137
+ .where(eq(artifactVersions.artifactId, artifactId));
1138
+ await this.db
1139
+ .update(artifacts)
1140
+ .set({ latestVersion: Number(left?.latest ?? 0), updatedAt: new Date().toISOString() })
1141
+ .where(eq(artifacts.id, artifactId));
1142
+ return deleted.blobKey;
1143
+ }
979
1144
  async lockArtifactQuota() {
980
1145
  this.check();
981
1146
  await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.artifacts'))`);
@@ -1116,10 +1281,12 @@ class PostgresTx {
1116
1281
  basinGeneration: sql `${tenant.basinGeneration} + 1`,
1117
1282
  });
1118
1283
  }
1119
- if (scope === "sandboxes" || scope === "all") {
1284
+ if (scope === "environments" || scope === "all") {
1120
1285
  await db.delete(sandboxes);
1121
- await db.delete(sandboxResources);
1122
- await db.delete(sandboxEvents);
1286
+ // Their vaults and credentials go with them (ON DELETE CASCADE).
1287
+ await db.delete(environments);
1288
+ await db.delete(environmentEvents);
1289
+ await db.delete(sandboxSignals);
1123
1290
  }
1124
1291
  if (scope === "all") {
1125
1292
  await db.delete(definitions);
@@ -1128,7 +1295,6 @@ class PostgresTx {
1128
1295
  await db.delete(modelUsage);
1129
1296
  await db.delete(modelBudgets);
1130
1297
  await db.delete(artifacts);
1131
- await db.delete(vaults).where(ne(vaults.scope, "host"));
1132
1298
  }
1133
1299
  }
1134
1300
  }