@nylorun/runtime 0.13.0-beta → 0.15.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 (309) hide show
  1. package/CHANGELOG.md +434 -0
  2. package/README.md +137 -41
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +40 -10
  5. package/dist/adapters/replication/pgoutput.d.ts +21 -0
  6. package/dist/adapters/replication/pgoutput.js +170 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +570 -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 +8065 -0
  32. package/dist/api/components.js +82 -0
  33. package/dist/api/gate/keys.d.ts +14 -0
  34. package/dist/api/gate/keys.js +59 -0
  35. package/dist/api/gate/routes.d.ts +34 -0
  36. package/dist/api/gate/routes.js +235 -0
  37. package/dist/api/http/app.d.ts +18 -0
  38. package/dist/api/http/app.js +58 -0
  39. package/dist/api/http/body.d.ts +4 -0
  40. package/dist/api/http/body.js +37 -0
  41. package/dist/api/http/define.d.ts +39 -0
  42. package/dist/api/http/define.js +122 -0
  43. package/dist/api/http/respond.d.ts +9 -0
  44. package/dist/api/http/respond.js +44 -0
  45. package/dist/api/http/routes/access.d.ts +24 -0
  46. package/dist/api/http/routes/access.js +266 -0
  47. package/dist/api/http/routes/actions.d.ts +8 -0
  48. package/dist/api/http/routes/actions.js +76 -0
  49. package/dist/api/http/routes/endpoints.d.ts +7 -0
  50. package/dist/api/http/routes/endpoints.js +100 -0
  51. package/dist/api/http/routes/sessions.d.ts +3 -0
  52. package/dist/api/http/routes/sessions.js +205 -0
  53. package/dist/api/http/routes/tenant.d.ts +11 -0
  54. package/dist/api/http/routes/tenant.js +244 -0
  55. package/dist/api/http/routes/vaults.d.ts +8 -0
  56. package/dist/api/http/routes/vaults.js +167 -0
  57. package/dist/api/openapi.d.ts +19 -0
  58. package/dist/api/openapi.js +137 -0
  59. package/dist/api/route.d.ts +12 -0
  60. package/dist/api/route.js +4 -0
  61. package/dist/core/bearer.d.ts +8 -0
  62. package/dist/core/bearer.js +11 -0
  63. package/dist/core/flow-host.d.ts +7 -15
  64. package/dist/core/flow-host.js +16 -41
  65. package/dist/core/sandbox-routes.d.ts +12 -7
  66. package/dist/core/sandbox-routes.js +24 -25
  67. package/dist/execution/memory.d.ts +8 -5
  68. package/dist/execution/memory.js +33 -13
  69. package/dist/execution/types.d.ts +38 -6
  70. package/dist/execution/types.js +12 -3
  71. package/dist/gates/contract.d.ts +51 -0
  72. package/dist/gates/contract.js +37 -0
  73. package/dist/gates/handler.d.ts +21 -0
  74. package/dist/gates/handler.js +55 -0
  75. package/dist/gates/http-client.d.ts +12 -0
  76. package/dist/gates/http-client.js +160 -0
  77. package/dist/gates/in-process.d.ts +32 -0
  78. package/dist/gates/in-process.js +42 -0
  79. package/dist/gates/inflight.d.ts +39 -0
  80. package/dist/gates/inflight.js +70 -0
  81. package/dist/gates/mcp-handler.d.ts +35 -0
  82. package/dist/gates/mcp-handler.js +142 -0
  83. package/dist/gates/meter.d.ts +24 -0
  84. package/dist/gates/meter.js +165 -0
  85. package/dist/gates/model-gate.d.ts +45 -0
  86. package/dist/gates/model-gate.js +1 -0
  87. package/dist/gates/tenant-vaults.d.ts +64 -0
  88. package/dist/gates/tenant-vaults.js +94 -0
  89. package/dist/gates/tool-calls.d.ts +51 -0
  90. package/dist/gates/tool-calls.js +64 -0
  91. package/dist/gates/tool-client.d.ts +12 -0
  92. package/dist/gates/tool-client.js +201 -0
  93. package/dist/gates/tool-contract.d.ts +116 -0
  94. package/dist/gates/tool-contract.js +64 -0
  95. package/dist/gates/tool-gate.d.ts +45 -0
  96. package/dist/gates/tool-gate.js +10 -0
  97. package/dist/host/admin-api.d.ts +23 -0
  98. package/dist/host/admin-api.js +74 -0
  99. package/dist/host/app.d.ts +34 -0
  100. package/dist/host/app.js +181 -0
  101. package/dist/host/config-for.d.ts +3 -1
  102. package/dist/host/config-for.js +3 -2
  103. package/dist/host/config.d.ts +11 -0
  104. package/dist/host/cors.d.ts +27 -0
  105. package/dist/host/cors.js +64 -0
  106. package/dist/host/create-host.d.ts +36 -8
  107. package/dist/host/create-host.js +161 -330
  108. package/dist/host/execution.d.ts +20 -18
  109. package/dist/host/execution.js +2 -2
  110. package/dist/host/gates.d.ts +58 -0
  111. package/dist/host/gates.js +127 -0
  112. package/dist/host/http.d.ts +20 -4
  113. package/dist/host/http.js +81 -23
  114. package/dist/host/index.d.ts +1 -1
  115. package/dist/host/main.js +189 -58
  116. package/dist/host/stack-config.d.ts +91 -19
  117. package/dist/host/stack-config.js +246 -17
  118. package/dist/infra/database.d.ts +2 -2
  119. package/dist/infra/execution.d.ts +5 -5
  120. package/dist/infra/execution.js +8 -6
  121. package/dist/infra/index.d.ts +1 -1
  122. package/dist/infra/workers.d.ts +17 -17
  123. package/dist/infra/workers.js +9 -9
  124. package/dist/keys/client.d.ts +9 -0
  125. package/dist/keys/client.js +70 -0
  126. package/dist/keys/contract.d.ts +24 -0
  127. package/dist/keys/contract.js +12 -0
  128. package/dist/keys/keys.d.ts +41 -0
  129. package/dist/keys/keys.js +50 -0
  130. package/dist/mcp/connect.d.ts +43 -3
  131. package/dist/mcp/connect.js +12 -5
  132. package/dist/mcp/pool.d.ts +39 -3
  133. package/dist/mcp/pool.js +74 -11
  134. package/dist/model/catalog.d.ts +2 -10
  135. package/dist/model/classify.d.ts +13 -0
  136. package/dist/model/classify.js +61 -0
  137. package/dist/model/models.d.ts +10 -1
  138. package/dist/model/models.js +14 -6
  139. package/dist/model/pi-model.d.ts +17 -2
  140. package/dist/model/pi-model.js +366 -222
  141. package/dist/openapi.json +25889 -0
  142. package/dist/record/append.d.ts +51 -0
  143. package/dist/record/append.js +20 -0
  144. package/dist/record/envelope.d.ts +19 -0
  145. package/dist/record/envelope.js +49 -0
  146. package/dist/record/index.d.ts +11 -0
  147. package/dist/record/index.js +11 -0
  148. package/dist/sandbox/manager.d.ts +13 -1
  149. package/dist/sandbox/manager.js +8 -11
  150. package/dist/sandbox/resolve.d.ts +33 -0
  151. package/dist/sandbox/resolve.js +81 -0
  152. package/dist/sandbox/session-sandbox.d.ts +11 -0
  153. package/dist/sandbox/session-sandbox.js +51 -0
  154. package/dist/sandbox/share.d.ts +14 -2
  155. package/dist/sandbox/share.js +33 -3
  156. package/dist/sandbox/tenant-config.d.ts +37 -0
  157. package/dist/sandbox/tenant-config.js +51 -0
  158. package/dist/sandbox/tools.d.ts +7 -9
  159. package/dist/sandbox/tools.js +11 -3
  160. package/dist/sandbox/types.d.ts +8 -2
  161. package/dist/store/postgres/connect.d.ts +25 -9
  162. package/dist/store/postgres/connect.js +27 -10
  163. package/dist/store/postgres/db.d.ts +16 -0
  164. package/dist/store/postgres/db.js +28 -0
  165. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  166. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  167. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  168. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  170. package/dist/store/postgres/migrate.d.ts +68 -0
  171. package/dist/store/postgres/migrate.js +183 -0
  172. package/dist/store/postgres/record-writer.d.ts +4 -0
  173. package/dist/store/postgres/record-writer.js +34 -0
  174. package/dist/store/postgres/record.d.ts +5 -0
  175. package/dist/store/postgres/record.js +60 -0
  176. package/dist/store/postgres/schema.d.ts +3096 -0
  177. package/dist/store/postgres/schema.js +363 -0
  178. package/dist/store/postgres/store.d.ts +2 -4
  179. package/dist/store/postgres/store.js +590 -434
  180. package/dist/store/postgres/tenant.d.ts +56 -0
  181. package/dist/store/postgres/tenant.js +154 -0
  182. package/dist/store/types.d.ts +183 -132
  183. package/dist/store/types.js +0 -1
  184. package/dist/streams/basin.d.ts +18 -1
  185. package/dist/streams/basin.js +34 -2
  186. package/dist/streams/control.d.ts +18 -0
  187. package/dist/streams/control.js +33 -0
  188. package/dist/streams/memory.d.ts +3 -1
  189. package/dist/streams/memory.js +14 -8
  190. package/dist/streams/relay/core.d.ts +35 -0
  191. package/dist/streams/relay/core.js +366 -0
  192. package/dist/streams/relay/types.d.ts +78 -0
  193. package/dist/streams/relay/types.js +8 -0
  194. package/dist/streams/types.d.ts +71 -36
  195. package/dist/streams/types.js +18 -53
  196. package/dist/tenant/access-policy.d.ts +30 -0
  197. package/dist/tenant/access-policy.js +52 -0
  198. package/dist/tenant/actions.d.ts +10 -149
  199. package/dist/tenant/actions.js +5 -167
  200. package/dist/tenant/advance.d.ts +7 -2
  201. package/dist/tenant/advance.js +125 -14
  202. package/dist/tenant/auth.d.ts +19 -17
  203. package/dist/tenant/auth.js +90 -93
  204. package/dist/tenant/browser.d.ts +15 -0
  205. package/dist/tenant/browser.js +39 -0
  206. package/dist/tenant/cause.d.ts +25 -0
  207. package/dist/tenant/cause.js +45 -0
  208. package/dist/tenant/commands.d.ts +18 -2
  209. package/dist/tenant/commands.js +111 -115
  210. package/dist/tenant/context.d.ts +101 -21
  211. package/dist/tenant/context.js +14 -10
  212. package/dist/tenant/delivery-token.d.ts +41 -0
  213. package/dist/tenant/delivery-token.js +104 -0
  214. package/dist/tenant/delivery.d.ts +62 -0
  215. package/dist/tenant/delivery.js +481 -0
  216. package/dist/tenant/effects.d.ts +20 -1
  217. package/dist/tenant/effects.js +159 -42
  218. package/dist/tenant/endpoints.d.ts +67 -0
  219. package/dist/tenant/endpoints.js +90 -0
  220. package/dist/tenant/ephemeral.d.ts +22 -7
  221. package/dist/tenant/ephemeral.js +83 -125
  222. package/dist/tenant/history.d.ts +64 -0
  223. package/dist/tenant/history.js +120 -0
  224. package/dist/tenant/http.d.ts +8 -9
  225. package/dist/tenant/http.js +13 -27
  226. package/dist/tenant/jwt.d.ts +16 -0
  227. package/dist/tenant/jwt.js +50 -0
  228. package/dist/tenant/model-setting.d.ts +2 -2
  229. package/dist/tenant/module.d.ts +17 -28
  230. package/dist/tenant/module.js +133 -279
  231. package/dist/tenant/outbound.d.ts +44 -0
  232. package/dist/tenant/outbound.js +127 -0
  233. package/dist/tenant/paths.d.ts +11 -16
  234. package/dist/tenant/paths.js +24 -68
  235. package/dist/tenant/pool.d.ts +1 -3
  236. package/dist/tenant/pool.js +1 -19
  237. package/dist/tenant/principals.d.ts +23 -11
  238. package/dist/tenant/principals.js +61 -49
  239. package/dist/tenant/reset.d.ts +0 -4
  240. package/dist/tenant/reset.js +0 -5
  241. package/dist/tenant/runtime.d.ts +28 -20
  242. package/dist/tenant/runtime.js +95 -64
  243. package/dist/tenant/scheduler.d.ts +10 -3
  244. package/dist/tenant/scheduler.js +9 -5
  245. package/dist/tenant/session-streams.d.ts +127 -0
  246. package/dist/tenant/session-streams.js +404 -0
  247. package/dist/tenant/sessions.d.ts +28 -4
  248. package/dist/tenant/sessions.js +118 -24
  249. package/dist/tenant/signing-keys.d.ts +55 -0
  250. package/dist/tenant/signing-keys.js +172 -0
  251. package/dist/tenant/slim.d.ts +8 -0
  252. package/dist/tenant/slim.js +21 -0
  253. package/dist/tenant/status.d.ts +3 -3
  254. package/dist/tenant/status.js +20 -13
  255. package/dist/tenant/store-pg.d.ts +9 -6
  256. package/dist/tenant/store-pg.js +60 -125
  257. package/dist/tenant/streams.d.ts +62 -85
  258. package/dist/tenant/streams.js +143 -226
  259. package/dist/tenant/subject-limits.d.ts +16 -0
  260. package/dist/tenant/subject-limits.js +45 -0
  261. package/dist/tenant/sweep.d.ts +1 -10
  262. package/dist/tenant/sweep.js +29 -69
  263. package/dist/tenant/tokens.d.ts +14 -0
  264. package/dist/tenant/tokens.js +180 -0
  265. package/dist/tenant/transcript.d.ts +48 -5
  266. package/dist/tenant/transcript.js +58 -1
  267. package/dist/tenant/types.d.ts +69 -98
  268. package/dist/tenant/types.js +4 -15
  269. package/dist/tenant/worker.d.ts +3 -1
  270. package/dist/tenant/worker.js +13 -7
  271. package/dist/vault/host-model.d.ts +31 -0
  272. package/dist/vault/host-model.js +57 -0
  273. package/dist/vault/service.d.ts +44 -17
  274. package/dist/vault/service.js +126 -121
  275. package/dist/version.d.ts +1 -1
  276. package/dist/version.js +1 -1
  277. package/package.json +25 -9
  278. package/dist/core/executors.d.ts +0 -44
  279. package/dist/core/executors.js +0 -95
  280. package/dist/store/memory.d.ts +0 -57
  281. package/dist/store/memory.js +0 -645
  282. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  283. package/dist/store/postgres/migrations/001_initial.js +0 -196
  284. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  285. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  286. package/dist/store/postgres/migrations/index.d.ts +0 -49
  287. package/dist/store/postgres/migrations/index.js +0 -76
  288. package/dist/store/postgres/names.d.ts +0 -11
  289. package/dist/store/postgres/names.js +0 -42
  290. package/dist/store/postgres/tenants.d.ts +0 -90
  291. package/dist/store/postgres/tenants.js +0 -214
  292. package/dist/streams/relay.d.ts +0 -37
  293. package/dist/streams/relay.js +0 -254
  294. package/dist/tenant/envelope.d.ts +0 -7
  295. package/dist/tenant/envelope.js +0 -22
  296. package/dist/tenant/live.d.ts +0 -92
  297. package/dist/tenant/live.js +0 -290
  298. package/dist/tenant/quarantine-error.d.ts +0 -6
  299. package/dist/tenant/quarantine-error.js +0 -9
  300. package/dist/tenant/quarantine.d.ts +0 -27
  301. package/dist/tenant/quarantine.js +0 -72
  302. package/dist/tenant/routes-tenant.d.ts +0 -12
  303. package/dist/tenant/routes-tenant.js +0 -137
  304. package/dist/tenant/routes.d.ts +0 -9
  305. package/dist/tenant/routes.js +0 -151
  306. package/dist/tenant/store-memory.d.ts +0 -16
  307. package/dist/tenant/store-memory.js +0 -111
  308. /package/dist/{store → record}/cursor.d.ts +0 -0
  309. /package/dist/{store → record}/cursor.js +0 -0
@@ -1,16 +1,25 @@
1
1
  /**
2
- * The Postgres Session Store (architecture §12.2): one Tenant schema, one
3
- * `SessionStore`. See `store/types.ts` for the invariants it keeps.
2
+ * The Postgres Session Store (architecture §12.2, session-store.md §2–§3): the `SessionStore`
3
+ * of the one Tenant a database holds, its state in the schema `nylorun` and its record in
4
+ * `nylorun_streams`, queried through Drizzle (`schema.ts`). See `store/types.ts` for the
5
+ * invariants it keeps.
6
+ *
7
+ * ## Queries
8
+ *
9
+ * Plain reads and writes use Drizzle's query builder: `.set(patch)` and `and(...)` skip
10
+ * `undefined`, rows come back in the camelCase of `schema.ts`. Statements whose logic is SQL
11
+ * stay `sql` fragments inside it: the endpoint upsert's `CASE` arms, the log-head upsert,
12
+ * `orphanedSessions`' ordering, `counts()`, advisory locks and array updates.
4
13
  *
5
14
  * ## Transactions
6
15
  *
7
- * Every `tx` is one READ COMMITTED transaction on a pooled connection. Tables
8
- * are addressed by fully qualified, quoted names (`"tenant_<id>"."sessions"`),
9
- * never through `search_path`, so a statement cannot reach another schema.
10
- * Commit listeners and `afterCommit` callbacks run after `COMMIT` returns and
11
- * never reject the committed `tx`. Nested `tx` calls are detected with
12
- * `AsyncLocalStorage` and rejected, and a `Tx` rejects every call once its
13
- * callback settles.
16
+ * Every `tx` is one READ COMMITTED transaction on a pooled connection (Drizzle's
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`
19
+ * returns and never reject the committed `tx`. Nested `tx` calls are detected with
20
+ * `AsyncLocalStorage` and rejected, and a `Tx` rejects every call once its callback settles.
21
+ * A failed statement rejects `tx` with the driver's error (`driverError`), not Drizzle's
22
+ * wrapper, so its SQLSTATE is `code`.
14
23
  *
15
24
  * ## Lock ordering
16
25
  *
@@ -19,7 +28,8 @@
19
28
  * ends. To stay deadlock-free:
20
29
  *
21
30
  * 1. A session-scoped transaction locks its session row before writing any
22
- * other row of that session (effects, actions, checkpoints, outbox).
31
+ * other row of that session (effects, actions, its record rows and log
32
+ * head).
23
33
  * 2. A transaction that touches several sessions locks them in ascending id
24
34
  * order, with `lockSessions` (`./locking.ts`), before writing any of them.
25
35
  *
@@ -29,27 +39,45 @@
29
39
  *
30
40
  * ## Values
31
41
  *
32
- * Document and outbox bodies are `json`, stored as the text `JSON.stringify`
33
- * wrote, so every string round-trips, including U+0000 and unpaired
34
- * surrogates that `jsonb` rejects (see `migrations/001_initial.ts`), and key
35
- * order is kept. Ids and ISO timestamps compared as text use `COLLATE "C"`.
36
- * Lease times in action bodies are compared as `timestamptz`.
42
+ * Document and event bodies are `json`, stored as the text `JSON.stringify`
43
+ * wrote (`jsonText` in `schema.ts`), so every string round-trips, including U+0000 and
44
+ * unpaired surrogates that `jsonb` rejects, and key order is kept. Ids and ISO timestamps
45
+ * compared as text are `COLLATE "C"`. Delivery deadlines in action bodies are compared as
46
+ * `timestamptz`.
37
47
  */
38
48
  import { AsyncLocalStorage } from "node:async_hooks";
39
- import { randomUUID } from "node:crypto";
40
- import { encodeCursor } from "../cursor.js";
49
+ import { and, count, eq, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
50
+ import { appendEvent } from "../../record/index.js";
41
51
  import { OwnershipLostError } from "../ownership.js";
42
- import { POSTGRES_SCHEMA_VERSION, readSchemaVersion } from "./migrations/index.js";
43
- import { assertIdentifier, tenantSchemaName } from "./names.js";
52
+ import { database, driverError } from "./db.js";
53
+ import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
54
+ import { createPostgresRecordReader } from "./record.js";
55
+ import { postgresRecordWriter } from "./record-writer.js";
56
+ import { actions, commands, definitions, effects, endpoints, links, modelBudgets, modelUsage, principals, publishableKeys, sandboxes, sessionEvents, sessionLogHeads, sessions, toolCrossings, signingKeys, subjectEpochs, subjectUsage, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, } from "./schema.js";
44
57
  export function createPostgresSessionStore(options) {
45
58
  return new PostgresSessionStore(options);
46
59
  }
47
- const OWNERSHIP_KEYS = new Set(["owner", "epoch", "ownerExpiresAt"]);
48
60
  const OPEN_SESSION = ["running", "runnable"];
61
+ const DOCUMENTS = {
62
+ definitions,
63
+ sessions: sessions,
64
+ commands,
65
+ effects: effects,
66
+ actions: actions,
67
+ sandboxes,
68
+ links: links,
69
+ };
70
+ /** A session row as `StoredSession` reads it: the body and the ownership columns. */
71
+ const SESSION = {
72
+ body: sessions.body,
73
+ owner: sessions.owner,
74
+ epoch: sessions.epoch,
75
+ ownerExpiresAt: sessions.ownerExpiresAt,
76
+ };
49
77
  class PostgresSessionStore {
50
78
  tenantId;
51
79
  sql;
52
- schema;
80
+ db;
53
81
  schemaVersion;
54
82
  active = new AsyncLocalStorage();
55
83
  listeners = new Set();
@@ -60,9 +88,8 @@ class PostgresSessionStore {
60
88
  constructor(options) {
61
89
  this.tenantId = options.tenantId;
62
90
  this.sql = options.sql;
63
- this.schema = options.schema ?? tenantSchemaName(options.tenantId);
64
- assertIdentifier(this.schema);
65
- this.schemaVersion = options.schemaVersion ?? POSTGRES_SCHEMA_VERSION;
91
+ this.db = database(options.sql);
92
+ this.schemaVersion = options.schemaVersion ?? expectedSchemaVersion();
66
93
  this.now = options.now ?? (() => new Date());
67
94
  this.onError =
68
95
  options.onError ??
@@ -77,28 +104,30 @@ class PostgresSessionStore {
77
104
  throw new Error("Nested SessionStore.tx is not allowed");
78
105
  let t;
79
106
  // Wrapped so `begin` does not treat an array result as queries to await.
80
- const run = this.sql.begin("isolation level read committed", async (sql) => {
81
- t = new PostgresTx(sql, this.schema, this.tenantId, this.now);
107
+ const run = this.db.transaction(async (db) => {
108
+ t = new PostgresTx(db, this.tenantId, this.now);
82
109
  try {
83
110
  return { value: await this.active.run(this, () => fn(t)) };
84
111
  }
85
112
  finally {
86
113
  t.closed = true;
87
114
  }
88
- });
115
+ }, { isolationLevel: "read committed" });
89
116
  this.inflight.add(run);
90
117
  let result;
91
118
  try {
92
- result = (await run);
119
+ result = await run;
120
+ }
121
+ catch (error) {
122
+ throw driverError(error);
93
123
  }
94
124
  finally {
95
125
  this.inflight.delete(run);
96
126
  }
97
- if (t.events.length > 0 || t.workAvailable) {
127
+ if (t.events.length > 0) {
98
128
  const commit = {
99
129
  events: t.events,
100
- incarnations: t.incarnations,
101
- workAvailable: t.workAvailable,
130
+ generations: t.generations,
102
131
  };
103
132
  for (const listener of [...this.listeners]) {
104
133
  try {
@@ -123,12 +152,15 @@ class PostgresSessionStore {
123
152
  this.listeners.add(listener);
124
153
  return () => this.listeners.delete(listener);
125
154
  }
155
+ record() {
156
+ return createPostgresRecordReader(this.sql, { tenantId: this.tenantId });
157
+ }
126
158
  async health() {
127
159
  const expectedSchemaVersion = this.schemaVersion;
128
160
  if (this.closed)
129
161
  return { ok: false, schemaVersion: 0, expectedSchemaVersion };
130
162
  try {
131
- const schemaVersion = (await readSchemaVersion(this.sql, this.schema)) ?? 0;
163
+ const schemaVersion = (await readSchemaVersion(this.db)) ?? 0;
132
164
  return {
133
165
  ok: schemaVersion === expectedSchemaVersion,
134
166
  schemaVersion,
@@ -145,120 +177,43 @@ class PostgresSessionStore {
145
177
  await Promise.allSettled([...this.inflight]);
146
178
  }
147
179
  }
148
- // ---------------------------------------------------------------------------
149
180
  function storedSession(row) {
150
181
  return { ...row.body, ...ownership(row) };
151
182
  }
152
183
  function ownership(row) {
153
184
  return {
154
- owner: row.owner ?? null,
155
- epoch: Number(row.epoch),
156
- ownerExpiresAt: row.owner_expires_at
157
- ? new Date(row.owner_expires_at).toISOString()
158
- : null,
185
+ owner: row.owner,
186
+ epoch: row.epoch,
187
+ ownerExpiresAt: row.ownerExpiresAt?.toISOString() ?? null,
159
188
  };
160
189
  }
161
- function toJson(value) {
162
- const json = JSON.stringify(value);
163
- if (json === undefined)
164
- throw new Error("Document body must be JSON");
165
- return json;
190
+ /** An endpoint row with its unset (null) columns left out, as `EndpointRow` has them. */
191
+ function endpointRow(row) {
192
+ return Object.fromEntries(Object.entries(row).filter(([, value]) => value !== null));
166
193
  }
167
- const bytes = (value) => Buffer.from(value.buffer, value.byteOffset, value.byteLength);
168
- const fromBytes = (value) => new Uint8Array(value);
169
- function executorRow(row) {
170
- return {
171
- agentId: row.agent_id,
172
- tokenHash: row.token_hash,
173
- implementationVersion: row.implementation_version,
174
- ...(row.manifest_hash !== null ? { manifestHash: row.manifest_hash } : {}),
175
- ...(row.principal_id !== null ? { principalId: row.principal_id } : {}),
176
- createdAt: row.created_at,
177
- updatedAt: row.updated_at,
178
- };
179
- }
180
- function principalRow(row) {
181
- return {
182
- id: row.id,
183
- role: row.role,
184
- tokenHash: row.token_hash,
185
- idempotencyKey: row.idempotency_key,
186
- createdAt: row.created_at,
187
- };
188
- }
189
- function vaultRow(row) {
190
- return {
191
- id: row.id,
192
- name: row.name,
193
- ownerUserId: row.owner_user_id,
194
- metadataJson: row.metadata_json,
195
- createdAt: row.created_at,
196
- scope: row.scope,
197
- };
198
- }
199
- function credentialRow(row) {
200
- return {
201
- id: row.id,
202
- vaultId: row.vault_id,
203
- name: row.name,
204
- type: row.type,
205
- bindingJson: row.binding_json,
206
- expiresAt: row.expires_at,
207
- createdAt: row.created_at,
208
- rotatedAt: row.rotated_at,
209
- kekId: row.kek_id,
210
- nonce: fromBytes(row.nonce),
211
- ciphertext: fromBytes(row.ciphertext),
212
- wrappedDek: fromBytes(row.wrapped_dek),
213
- };
214
- }
215
- function auditRow(row) {
216
- return {
217
- id: row.id,
218
- at: row.at,
219
- actor: row.actor,
220
- action: row.action,
221
- vaultId: row.vault_id,
222
- credentialId: row.credential_id,
223
- sessionId: row.session_id,
224
- target: row.target,
225
- outcome: row.outcome,
226
- };
227
- }
228
- /** `VaultCredentialPatch` field → column. */
229
- const CREDENTIAL_COLUMNS = {
230
- name: "name",
231
- type: "type",
232
- bindingJson: "binding_json",
233
- expiresAt: "expires_at",
234
- rotatedAt: "rotated_at",
235
- kekId: "kek_id",
236
- nonce: "nonce",
237
- ciphertext: "ciphertext",
238
- wrappedDek: "wrapped_dek",
194
+ /** The time column a signing key's new state stamps. */
195
+ const SIGNING_KEY_STAMP = {
196
+ current: "activatedAt",
197
+ previous: "retiredAt",
198
+ revoked: "revokedAt",
239
199
  };
240
- const SESSION_TABLES = [
241
- "sessions",
242
- "commands",
243
- "checkpoints",
244
- "effects",
245
- "actions",
246
- "links",
247
- "outbox",
248
- ];
200
+ /** The audit columns a row has (`ord` only orders them). */
201
+ const { ord: _ord, ...AUDIT } = getTableColumns(vaultAudit);
202
+ const SESSION_TABLES = [sessions, commands, effects, actions, links];
203
+ /** The inserted row's `column` (`ON CONFLICT … DO UPDATE`). */
204
+ const excluded = (column) => sql.raw(`excluded.${column}`);
205
+ /** Keeps an endpoint's health `column` when its URL stays; a new URL starts with `fresh`. */
206
+ const sameUrl = (column, fresh = sql `NULL`) => sql `CASE WHEN ${endpoints.url} = excluded.url THEN ${column} ELSE ${fresh} END`;
249
207
  class PostgresTx {
250
- sql;
251
- schema;
208
+ db;
252
209
  tenantId;
253
210
  now;
254
211
  closed = false;
255
212
  events = [];
256
- incarnations = [];
213
+ generations = [];
257
214
  callbacks = [];
258
- workAvailable = false;
259
- constructor(sql, schema, tenantId, now) {
260
- this.sql = sql;
261
- this.schema = schema;
215
+ constructor(db, tenantId, now) {
216
+ this.db = db;
262
217
  this.tenantId = tenantId;
263
218
  this.now = now;
264
219
  }
@@ -266,99 +221,79 @@ class PostgresTx {
266
221
  if (this.closed)
267
222
  throw new Error("Tx used after its transaction ended");
268
223
  }
269
- /** The quoted, schema-qualified table. */
270
- t(table) {
271
- return this.sql(`${this.schema}.${table}`);
272
- }
273
224
  // --- documents -----------------------------------------------------------
274
225
  async get(table, id) {
275
226
  this.check();
276
- const sql = this.sql;
277
227
  if (table === "sessions") {
278
- const [row] = await sql `
279
- SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
280
- WHERE id = ${id}`;
228
+ const [row] = await this.sessions().where(eq(sessions.id, id));
281
229
  return row && storedSession(row);
282
230
  }
283
- const [row] = await sql `SELECT body FROM ${this.t(table)} WHERE id = ${id}`;
231
+ const t = DOCUMENTS[table];
232
+ const [row] = await this.db.select({ body: t.body }).from(t).where(eq(t.id, id));
284
233
  return row?.body;
285
234
  }
286
235
  async put(table, id, body) {
287
236
  this.check();
288
237
  let value = body;
289
- if (table === "sessions" && body && typeof body === "object")
290
- value = Object.fromEntries(Object.entries(body).filter(([key]) => !OWNERSHIP_KEYS.has(key)));
291
- const json = toJson(value);
292
- await this.sql `
293
- INSERT INTO ${this.t(table)} (id, body) VALUES (${id}, ${json}::text::json)
294
- ON CONFLICT (id) DO UPDATE SET body = excluded.body`;
238
+ if (table === "sessions" && body && typeof body === "object") {
239
+ // Ownership is store-managed: never in the body.
240
+ const { owner: _owner, epoch: _epoch, ownerExpiresAt: _expires, ...rest } = body;
241
+ value = rest;
242
+ }
243
+ const t = DOCUMENTS[table];
244
+ await this.db
245
+ .insert(t)
246
+ .values({ id, body: value })
247
+ .onConflictDoUpdate({ target: t.id, set: { body: excluded("body") } });
295
248
  }
296
249
  async delete(table, id) {
297
250
  this.check();
298
- 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
- }
303
- async sessionRows(where, tail) {
304
- const sql = this.sql;
305
- const rows = await sql `
306
- SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
307
- ${where ? sql `WHERE ${where}` : sql ``}
308
- ${tail ?? sql `ORDER BY id`}`;
309
- return rows.map((row) => storedSession(row));
251
+ // A session's record rows and log head stay: a session created again with this id
252
+ // continues its log (per-session record deletion is deferred, Durable Streams §15).
253
+ const t = DOCUMENTS[table];
254
+ await this.db.delete(t).where(eq(t.id, id));
255
+ }
256
+ sessions() {
257
+ return this.db.select(SESSION).from(sessions).$dynamic();
310
258
  }
311
259
  // --- ordering ------------------------------------------------------------
312
260
  async lockSession(id) {
313
261
  this.check();
314
- const [row] = await this.sql `
315
- SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
316
- WHERE id = ${id} FOR UPDATE`;
262
+ const [row] = await this.sessions().where(eq(sessions.id, id)).for("update");
317
263
  return row && storedSession(row);
318
264
  }
319
265
  async event(sessionId, turnId, type, payload) {
320
266
  this.check();
321
- 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)
267
+ const db = this.db;
268
+ // The session row lock orders the session's events; the log head allocates under it.
269
+ const [session] = await db
270
+ .select({ epoch: sessions.epoch })
271
+ .from(sessions)
272
+ .where(eq(sessions.id, sessionId))
273
+ .for("update");
274
+ if (!session)
328
275
  throw new Error(`Session ${sessionId} not found`);
329
- const seq = Number(row.seq);
330
- const event = JSON.parse(JSON.stringify({
331
- eventId: randomUUID(),
332
- sessionId,
276
+ const { event, generation } = await appendEvent(postgresRecordWriter(db), {
333
277
  tenantId: this.tenantId,
278
+ sessionId,
334
279
  turnId,
335
- cursor: encodeCursor(sessionId, seq),
336
- createdAt: this.now().toISOString(),
280
+ epoch: session.epoch,
281
+ time: this.now(),
337
282
  type,
338
283
  payload,
339
- }));
340
- await sql `
341
- INSERT INTO ${this.t("outbox")} (session_id, seq, body)
342
- VALUES (${sessionId}, ${seq}, ${JSON.stringify(event)}::text::json)`;
284
+ });
343
285
  this.events.push(event);
344
- this.incarnations.push(row.incarnation ?? null);
286
+ this.generations.push(generation);
345
287
  return structuredClone(event);
346
288
  }
347
289
  afterCommit(fn) {
348
290
  this.check();
349
291
  this.callbacks.push(fn);
350
292
  }
351
- signalWork() {
352
- this.check();
353
- this.workAvailable = true;
354
- }
355
293
  // --- ownership -----------------------------------------------------------
356
294
  async takeOwnership(sessionId, claim) {
357
295
  this.check();
358
- const sql = this.sql;
359
- const [row] = await sql `
360
- SELECT owner, epoch, owner_expires_at FROM ${this.t("sessions")}
361
- WHERE id = ${sessionId} FOR UPDATE`;
296
+ const [row] = await this.sessions().where(eq(sessions.id, sessionId)).for("update");
362
297
  if (!row)
363
298
  return { status: "missing" };
364
299
  const previous = ownership(row);
@@ -370,30 +305,36 @@ class PostgresTx {
370
305
  owner: previous.owner,
371
306
  ownerExpiresAt: previous.ownerExpiresAt,
372
307
  };
373
- const expires = new Date(claim.now.getTime() + claim.leaseMs);
374
- const [updated] = await sql `
375
- UPDATE ${this.t("sessions")}
376
- SET owner = ${claim.owner}, epoch = epoch + 1, owner_expires_at = ${expires}
377
- WHERE id = ${sessionId} RETURNING epoch`;
308
+ const [updated] = await this.db
309
+ .update(sessions)
310
+ .set({
311
+ owner: claim.owner,
312
+ epoch: sql `${sessions.epoch} + 1`,
313
+ ownerExpiresAt: new Date(claim.now.getTime() + claim.leaseMs),
314
+ })
315
+ .where(eq(sessions.id, sessionId))
316
+ .returning({ epoch: sessions.epoch });
378
317
  return {
379
318
  status: "owned",
380
- epoch: Number(updated.epoch),
319
+ epoch: updated.epoch,
381
320
  takeover: previous.owner !== null,
382
321
  previous,
383
322
  };
384
323
  }
385
324
  async renewOwnership(sessionId, owner, epoch, until) {
386
325
  this.check();
387
- const result = await this.sql `
388
- UPDATE ${this.t("sessions")} SET owner_expires_at = ${until}
389
- WHERE id = ${sessionId} AND owner = ${owner} AND epoch = ${epoch}`;
326
+ const result = await this.db
327
+ .update(sessions)
328
+ .set({ ownerExpiresAt: until })
329
+ .where(and(eq(sessions.id, sessionId), eq(sessions.owner, owner), eq(sessions.epoch, epoch)));
390
330
  return result.count > 0;
391
331
  }
392
332
  async releaseOwnership(sessionId, owner, epoch) {
393
333
  this.check();
394
- const result = await this.sql `
395
- UPDATE ${this.t("sessions")} SET owner = NULL, owner_expires_at = NULL
396
- WHERE id = ${sessionId} AND owner = ${owner} AND epoch = ${epoch}`;
334
+ const result = await this.db
335
+ .update(sessions)
336
+ .set({ owner: null, ownerExpiresAt: null })
337
+ .where(and(eq(sessions.id, sessionId), eq(sessions.owner, owner), eq(sessions.epoch, epoch)));
397
338
  return result.count > 0;
398
339
  }
399
340
  async assertEpoch(sessionId, epoch) {
@@ -407,79 +348,97 @@ class PostgresTx {
407
348
  // --- typed queries -------------------------------------------------------
408
349
  async sessionsWithStatus(statuses) {
409
350
  this.check();
410
- return this.sessionRows(this.sql `status = ANY(${[...statuses]})`);
351
+ const rows = await this.sessions()
352
+ .where(inArray(sessions.status, statuses))
353
+ .orderBy(sessions.id);
354
+ return rows.map((row) => storedSession(row));
411
355
  }
412
356
  async orphanedSessions(now, limit) {
413
357
  this.check();
414
- const sql = this.sql;
415
- return this.sessionRows(sql `status = ANY(${OPEN_SESSION})
416
- AND (owner IS NULL OR owner_expires_at IS NULL OR owner_expires_at <= ${now})`, sql `ORDER BY
417
- CASE WHEN owner IS NULL THEN NULL ELSE owner_expires_at END ASC NULLS FIRST,
418
- id
419
- LIMIT ${limit}`);
358
+ const rows = await this.sessions()
359
+ .where(and(inArray(sessions.status, OPEN_SESSION), or(isNull(sessions.owner), isNull(sessions.ownerExpiresAt), lte(sessions.ownerExpiresAt, now))))
360
+ // Never-owned sessions first, then the longest expired.
361
+ .orderBy(sql `CASE WHEN ${sessions.owner} IS NULL THEN NULL ELSE ${sessions.ownerExpiresAt} END ASC NULLS FIRST`, sessions.id)
362
+ .limit(limit);
363
+ return rows.map((row) => storedSession(row));
420
364
  }
421
365
  async listSessions(filter = {}) {
422
366
  this.check();
423
- const sql = this.sql;
424
- const conditions = [
425
- ...(filter.agentId === undefined ? [] : [sql `agent_id = ${filter.agentId}`]),
426
- ...(filter.ownerUserId === undefined
427
- ? []
428
- : [sql `owner_user_id = ${filter.ownerUserId}`]),
429
- ];
430
- return this.sessionRows(conditions.length === 0
367
+ const rows = await this.sessions()
368
+ .where(and(filter.agentId === undefined ? undefined : eq(sessions.agentId, filter.agentId), filter.ownerUserId === undefined
431
369
  ? undefined
432
- : conditions.reduce((all, next) => sql `${all} AND ${next}`));
370
+ : eq(sessions.ownerUserId, filter.ownerUserId)))
371
+ .orderBy(sessions.id);
372
+ return rows.map((row) => storedSession(row));
433
373
  }
434
374
  async bodies(table) {
435
- const rows = await this.sql `SELECT body FROM ${this.t(table)} ORDER BY id`;
375
+ const rows = await this.db.select({ body: table.body }).from(table).orderBy(table.id);
436
376
  return rows.map((row) => row.body);
437
377
  }
438
378
  async listDefinitions() {
439
379
  this.check();
440
- return this.bodies("definitions");
380
+ return this.bodies(definitions);
441
381
  }
442
382
  async listSandboxes() {
443
383
  this.check();
444
- return this.bodies("sandboxes");
384
+ return this.bodies(sandboxes);
445
385
  }
446
- async expiredClaims(now, limit) {
447
- this.check();
448
- const rows = await this.sql `
449
- 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
453
- LIMIT ${limit}`;
386
+ async actionBodies(where) {
387
+ const rows = await this.db
388
+ .select({ body: actions.body })
389
+ .from(actions)
390
+ .where(where)
391
+ .orderBy(actions.id);
454
392
  return rows.map((row) => row.body);
455
393
  }
456
394
  async pendingActions(agentId) {
457
395
  this.check();
458
- const rows = await this.sql `
459
- SELECT body FROM ${this.t("actions")}
460
- WHERE agent_id = ${agentId} AND status = 'pending'
461
- ORDER BY id`;
396
+ return this.actionBodies(and(eq(actions.agentId, agentId), eq(actions.status, "pending")));
397
+ }
398
+ async deliveringCount(agentId) {
399
+ this.check();
400
+ const [row] = await this.db
401
+ .select({ n: count() })
402
+ .from(actions)
403
+ .where(and(eq(actions.agentId, agentId), eq(actions.status, "delivering")));
404
+ return row.n;
405
+ }
406
+ async pendingActionsWithEndpoint(limit) {
407
+ this.check();
408
+ const rows = await this.db
409
+ .select({ body: actions.body })
410
+ .from(actions)
411
+ .innerJoin(endpoints, eq(endpoints.agentId, actions.agentId))
412
+ .where(eq(actions.status, "pending"))
413
+ .orderBy(actions.id)
414
+ .limit(limit);
462
415
  return rows.map((row) => row.body);
463
416
  }
464
- async actionsForSession(sessionId, filter = {}) {
417
+ async expiredDeliveries(now, limit) {
465
418
  this.check();
466
- const sql = this.sql;
467
- const rows = await sql `
468
- SELECT body FROM ${this.t("actions")}
469
- WHERE session_id = ${sessionId}
470
- ${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
471
- ${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
472
- ORDER BY id`;
419
+ const deadline = sql `${actions.deadlineAt}::timestamptz`;
420
+ const rows = await this.db
421
+ .select({ body: actions.body })
422
+ .from(actions)
423
+ .where(and(eq(actions.status, "delivering"), isNotNull(actions.deadlineAt), sql `${deadline} <= ${now.toISOString()}::timestamptz`))
424
+ .orderBy(deadline, actions.id)
425
+ .limit(limit);
473
426
  return rows.map((row) => row.body);
474
427
  }
428
+ async actionsForSession(sessionId, filter = {}) {
429
+ this.check();
430
+ return this.actionBodies(and(eq(actions.sessionId, sessionId), filter.turnId === undefined ? undefined : eq(actions.turnId, filter.turnId), filter.statuses === undefined ? undefined : inArray(actions.status, filter.statuses)));
431
+ }
475
432
  async actionsWithStatus(statuses, filter = {}) {
476
433
  this.check();
477
- const sql = this.sql;
478
- const rows = await sql `
479
- SELECT body FROM ${this.t("actions")}
480
- WHERE status = ANY(${[...statuses]})
481
- ${filter.kinds === undefined ? sql `` : sql `AND kind = ANY(${[...filter.kinds]})`}
482
- ORDER BY id`;
434
+ return this.actionBodies(and(inArray(actions.status, statuses), filter.kinds === undefined ? undefined : inArray(actions.kind, filter.kinds)));
435
+ }
436
+ async effectBodies(where) {
437
+ const rows = await this.db
438
+ .select({ body: effects.body })
439
+ .from(effects)
440
+ .where(where)
441
+ .orderBy(effects.id);
483
442
  return rows.map((row) => row.body);
484
443
  }
485
444
  async invokingEffects(sessionId) {
@@ -487,35 +446,23 @@ class PostgresTx {
487
446
  }
488
447
  async effectsForSession(sessionId, filter = {}) {
489
448
  this.check();
490
- const sql = this.sql;
491
- const rows = await sql `
492
- SELECT body FROM ${this.t("effects")}
493
- WHERE session_id = ${sessionId}
494
- ${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
495
- ${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
496
- ORDER BY id`;
497
- return rows.map((row) => row.body);
449
+ return this.effectBodies(and(eq(effects.sessionId, sessionId), filter.turnId === undefined ? undefined : eq(effects.turnId, filter.turnId), filter.statuses === undefined ? undefined : inArray(effects.status, filter.statuses)));
498
450
  }
499
451
  async effectsForTurn(sessionId, turnId, statuses) {
500
452
  return this.effectsForSession(sessionId, { turnId, statuses });
501
453
  }
502
454
  async effectsWithStatus(statuses, filter = {}) {
503
455
  this.check();
504
- const sql = this.sql;
505
- const rows = await sql `
506
- SELECT body FROM ${this.t("effects")}
507
- WHERE status = ANY(${[...statuses]})
508
- ${filter.kinds === undefined ? sql `` : sql `AND kind = ANY(${[...filter.kinds]})`}
509
- ORDER BY id`;
510
- return rows.map((row) => row.body);
456
+ return this.effectBodies(and(inArray(effects.status, statuses), filter.kinds === undefined ? undefined : inArray(effects.kind, filter.kinds)));
511
457
  }
512
458
  async linkedSessions(workflowSessionId) {
513
459
  this.check();
514
- const rows = await this.sql `
515
- SELECT l.id, l.body AS link, s.body, s.owner, s.epoch, s.owner_expires_at
516
- FROM ${this.t("links")} l JOIN ${this.t("sessions")} s ON s.id = l.id
517
- WHERE l.workflow_session_id = ${workflowSessionId}
518
- ORDER BY l.id`;
460
+ const rows = await this.db
461
+ .select({ id: links.id, link: links.body, ...SESSION })
462
+ .from(links)
463
+ .innerJoin(sessions, eq(sessions.id, links.id))
464
+ .where(eq(links.workflowSessionId, workflowSessionId))
465
+ .orderBy(links.id);
519
466
  return rows.map((row) => ({
520
467
  agentSessionId: row.id,
521
468
  link: row.link,
@@ -524,15 +471,16 @@ class PostgresTx {
524
471
  }
525
472
  async counts() {
526
473
  this.check();
527
- const sql = this.sql;
528
- const [row] = await sql `
474
+ const [row] = await this.db.execute(sql `
529
475
  SELECT
530
- (SELECT count(*) FROM ${this.t("sessions")})::int AS sessions,
531
- (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,
533
- (SELECT count(*) FROM ${this.t("effects")} WHERE status = 'uncertain')::int AS uncertain,
534
- (SELECT count(*) FROM ${this.t("sandboxes")})::int AS sandboxes,
535
- (SELECT count(*) FROM ${this.t("definitions")})::int AS definitions`;
476
+ (SELECT count(*) FROM ${sessions})::int AS sessions,
477
+ (SELECT count(*) FROM ${sessions}
478
+ WHERE ${sessions.status} IN ('running', 'runnable'))::int AS running,
479
+ (SELECT count(*) FROM ${actions}
480
+ WHERE ${actions.status} IN ('pending', 'delivering'))::int AS actions,
481
+ (SELECT count(*) FROM ${effects} WHERE ${effects.status} = 'uncertain')::int AS uncertain,
482
+ (SELECT count(*) FROM ${sandboxes})::int AS sandboxes,
483
+ (SELECT count(*) FROM ${definitions})::int AS definitions`);
536
484
  return {
537
485
  sessions: row.sessions,
538
486
  runningSessions: row.running,
@@ -542,256 +490,464 @@ class PostgresTx {
542
490
  definitions: row.definitions,
543
491
  };
544
492
  }
545
- // --- outbox --------------------------------------------------------------
546
- async outbox(limit, filter = {}) {
493
+ // --- basin generations --------------------------------------------------
494
+ async basinGenerations() {
547
495
  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
- }));
496
+ const [row] = await this.db
497
+ .select({ current: tenant.basinGeneration, retired: tenant.retiredGenerations })
498
+ .from(tenant);
499
+ // A database whose Tenant row is not written yet is at generation 0.
500
+ return row ?? { current: 0, retired: [] };
559
501
  }
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() {
575
- this.check();
576
- const [row] = await this.sql `
577
- SELECT count(*)::int AS depth, min(created_at) AS oldest
578
- FROM ${this.t("outbox")}`;
579
- return {
580
- depth: row.depth,
581
- oldestCreatedAt: row.oldest ?? null,
582
- };
583
- }
584
- // --- executors -----------------------------------------------------------
585
- async listExecutors() {
502
+ async forgetRetiredGeneration(generation) {
586
503
  this.check();
587
- const rows = await this.sql `SELECT * FROM ${this.t("executors")} ORDER BY agent_id`;
588
- return rows.map(executorRow);
504
+ await this.db.update(tenant).set({
505
+ retiredGenerations: sql `array_remove(${tenant.retiredGenerations}, ${generation}::int)`,
506
+ });
589
507
  }
590
- async getExecutor(agentId) {
591
- this.check();
592
- const [row] = await this.sql `
593
- SELECT * FROM ${this.t("executors")} WHERE agent_id = ${agentId}`;
594
- return row && executorRow(row);
508
+ // --- Action endpoints -----------------------------------------------------
509
+ async listEndpoints() {
510
+ this.check();
511
+ const rows = await this.db.select().from(endpoints).orderBy(endpoints.agentId);
512
+ return rows.map(endpointRow);
513
+ }
514
+ async getEndpoint(agentId) {
515
+ this.check();
516
+ const [row] = await this.db.select().from(endpoints).where(eq(endpoints.agentId, agentId));
517
+ return row && endpointRow(row);
518
+ }
519
+ async putEndpoint(row) {
520
+ this.check();
521
+ // Health belongs to a URL: a new URL starts with none.
522
+ await this.db
523
+ .insert(endpoints)
524
+ .values({
525
+ agentId: row.agentId,
526
+ url: row.url,
527
+ implementationVersion: row.implementationVersion,
528
+ manifestHash: row.manifestHash ?? null,
529
+ timeoutMs: row.timeoutMs,
530
+ maxConcurrent: row.maxConcurrent,
531
+ principalId: row.principalId ?? null,
532
+ consecutiveFailures: 0,
533
+ createdAt: row.updatedAt,
534
+ updatedAt: row.updatedAt,
535
+ })
536
+ .onConflictDoUpdate({
537
+ target: endpoints.agentId,
538
+ set: {
539
+ url: excluded("url"),
540
+ implementationVersion: excluded("implementation_version"),
541
+ manifestHash: excluded("manifest_hash"),
542
+ timeoutMs: excluded("timeout_ms"),
543
+ maxConcurrent: excluded("max_concurrent"),
544
+ principalId: excluded("principal_id"),
545
+ updatedAt: excluded("updated_at"),
546
+ lastDeliveryAt: sameUrl(sql `${endpoints.lastDeliveryAt}`),
547
+ lastSuccessAt: sameUrl(sql `${endpoints.lastSuccessAt}`),
548
+ lastErrorCode: sameUrl(sql `${endpoints.lastErrorCode}`),
549
+ lastErrorMessage: sameUrl(sql `${endpoints.lastErrorMessage}`),
550
+ consecutiveFailures: sameUrl(sql `${endpoints.consecutiveFailures}`, sql `0`),
551
+ servedImplementationVersion: sameUrl(sql `${endpoints.servedImplementationVersion}`),
552
+ servedManifestHash: sameUrl(sql `${endpoints.servedManifestHash}`),
553
+ },
554
+ });
595
555
  }
596
- async putExecutor(row) {
556
+ async deleteEndpoint(agentId) {
597
557
  this.check();
598
- 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
602
- ) VALUES (
603
- ${row.agentId}, ${row.tokenHash}, ${row.implementationVersion},
604
- ${row.manifestHash ?? null}, ${row.principalId ?? null},
605
- ${row.updatedAt}, ${row.updatedAt}
606
- )
607
- ON CONFLICT (agent_id) DO UPDATE SET
608
- token_hash = excluded.token_hash,
609
- implementation_version = excluded.implementation_version,
610
- manifest_hash = excluded.manifest_hash,
611
- principal_id = excluded.principal_id,
612
- updated_at = excluded.updated_at`;
558
+ await this.db.delete(endpoints).where(eq(endpoints.agentId, agentId));
613
559
  }
614
- async deleteExecutor(agentId) {
560
+ async recordEndpointHealth(agentId, update) {
615
561
  this.check();
616
- await this.sql `DELETE FROM ${this.t("executors")} WHERE agent_id = ${agentId}`;
562
+ const set = update.kind === "success"
563
+ ? {
564
+ lastDeliveryAt: update.at,
565
+ lastSuccessAt: update.at,
566
+ consecutiveFailures: 0,
567
+ lastErrorCode: null,
568
+ lastErrorMessage: null,
569
+ }
570
+ : update.kind === "failure"
571
+ ? {
572
+ lastDeliveryAt: update.at,
573
+ lastErrorCode: update.code,
574
+ lastErrorMessage: update.message,
575
+ consecutiveFailures: sql `${endpoints.consecutiveFailures} + 1`,
576
+ }
577
+ : {
578
+ servedImplementationVersion: update.implementationVersion,
579
+ servedManifestHash: update.manifestHash ?? null,
580
+ };
581
+ await this.db.update(endpoints).set(set).where(eq(endpoints.agentId, agentId));
617
582
  }
618
583
  // --- principals ----------------------------------------------------------
619
584
  async insertPrincipal(row) {
620
585
  this.check();
621
- await this.sql `
622
- INSERT INTO ${this.t("principals")} (id, role, token_hash, idempotency_key, created_at)
623
- VALUES (${row.id}, ${row.role}, ${row.tokenHash}, ${row.idempotencyKey}, ${row.createdAt})`;
586
+ await this.db.insert(principals).values(row);
624
587
  }
625
588
  async principalByTokenHash(tokenHash) {
626
589
  this.check();
627
- const [row] = await this.sql `
628
- SELECT * FROM ${this.t("principals")} WHERE token_hash = ${tokenHash}`;
629
- return row && principalRow(row);
590
+ const [row] = await this.db
591
+ .select()
592
+ .from(principals)
593
+ .where(eq(principals.tokenHash, tokenHash));
594
+ return row;
630
595
  }
631
596
  async principalById(id) {
632
597
  this.check();
633
- const [row] = await this.sql `
634
- SELECT * FROM ${this.t("principals")} WHERE id = ${id}`;
635
- return row && principalRow(row);
598
+ const [row] = await this.db.select().from(principals).where(eq(principals.id, id));
599
+ return row;
636
600
  }
637
601
  async applicationTokenHashes() {
638
602
  this.check();
639
- const rows = await this.sql `
640
- SELECT token_hash FROM ${this.t("principals")}
641
- WHERE role = 'application' ORDER BY id`;
642
- return rows.map((row) => row.token_hash);
603
+ const rows = await this.db
604
+ .select({ tokenHash: principals.tokenHash })
605
+ .from(principals)
606
+ .where(eq(principals.role, "application"))
607
+ .orderBy(principals.id);
608
+ return rows.map((row) => row.tokenHash);
643
609
  }
644
610
  // --- vault ---------------------------------------------------------------
645
611
  async insertVault(row) {
646
612
  this.check();
647
- await this.sql `
648
- INSERT INTO ${this.t("vaults")} (id, name, owner_user_id, metadata_json, created_at, scope)
649
- VALUES (${row.id}, ${row.name}, ${row.ownerUserId}, ${row.metadataJson},
650
- ${row.createdAt}, ${row.scope})`;
613
+ await this.db.insert(vaults).values(row);
651
614
  }
652
615
  async getVault(id) {
653
616
  this.check();
654
- const [row] = await this.sql `SELECT * FROM ${this.t("vaults")} WHERE id = ${id}`;
655
- return row && vaultRow(row);
617
+ const [row] = await this.db.select().from(vaults).where(eq(vaults.id, id));
618
+ return row;
656
619
  }
657
620
  async vaultsByOwner(ownerUserId) {
658
621
  this.check();
659
- const rows = await this.sql `
660
- SELECT * FROM ${this.t("vaults")}
661
- WHERE owner_user_id = ${ownerUserId} AND scope = 'user'
662
- ORDER BY created_at, id`;
663
- return rows.map(vaultRow);
622
+ return this.db
623
+ .select()
624
+ .from(vaults)
625
+ .where(and(eq(vaults.ownerUserId, ownerUserId), eq(vaults.scope, "user")))
626
+ .orderBy(vaults.createdAt, vaults.id);
664
627
  }
665
628
  async updateVaultMetadata(id, metadataJson) {
666
629
  this.check();
667
- await this.sql `
668
- UPDATE ${this.t("vaults")} SET metadata_json = ${metadataJson} WHERE id = ${id}`;
630
+ await this.db.update(vaults).set({ metadataJson }).where(eq(vaults.id, id));
669
631
  }
670
632
  async deleteVault(id) {
671
633
  this.check();
672
634
  // Credentials go with it (ON DELETE CASCADE).
673
- await this.sql `DELETE FROM ${this.t("vaults")} WHERE id = ${id}`;
635
+ await this.db.delete(vaults).where(eq(vaults.id, id));
674
636
  }
675
637
  async insertCredential(row) {
676
638
  this.check();
677
- await this.sql `
678
- INSERT INTO ${this.t("vault_credentials")} (
679
- id, vault_id, name, type, binding_json, expires_at, created_at,
680
- rotated_at, kek_id, nonce, ciphertext, wrapped_dek
681
- ) VALUES (
682
- ${row.id}, ${row.vaultId}, ${row.name}, ${row.type}, ${row.bindingJson},
683
- ${row.expiresAt}, ${row.createdAt}, ${row.rotatedAt}, ${row.kekId},
684
- ${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)}
685
- )`;
639
+ await this.db.insert(vaultCredentials).values(row);
686
640
  }
687
641
  async getCredential(id) {
688
642
  this.check();
689
- const [row] = await this.sql `
690
- SELECT * FROM ${this.t("vault_credentials")} WHERE id = ${id}`;
691
- return row && credentialRow(row);
643
+ const [row] = await this.db
644
+ .select()
645
+ .from(vaultCredentials)
646
+ .where(eq(vaultCredentials.id, id));
647
+ return row;
692
648
  }
693
649
  async credentialsForVault(vaultId, filter = {}) {
694
650
  this.check();
695
- const sql = this.sql;
696
- const rows = await sql `
697
- SELECT * FROM ${this.t("vault_credentials")}
698
- WHERE vault_id = ${vaultId}
699
- ${filter.type === undefined ? sql `` : sql `AND type = ${filter.type}`}
700
- ORDER BY created_at, id`;
701
- return rows.map(credentialRow);
651
+ return this.db
652
+ .select()
653
+ .from(vaultCredentials)
654
+ .where(and(eq(vaultCredentials.vaultId, vaultId), filter.type === undefined ? undefined : eq(vaultCredentials.type, filter.type)))
655
+ .orderBy(vaultCredentials.createdAt, vaultCredentials.id);
702
656
  }
703
657
  async updateCredential(vaultId, id, patch) {
704
658
  this.check();
705
- const sql = this.sql;
706
- const columns = {};
707
- for (const [field, column] of Object.entries(CREDENTIAL_COLUMNS)) {
708
- const value = patch[field];
709
- if (value === undefined)
710
- continue;
711
- columns[column] = value instanceof Uint8Array ? bytes(value) : value;
712
- }
713
- if (Object.keys(columns).length === 0) {
714
- const rows = await sql `
715
- SELECT 1 FROM ${this.t("vault_credentials")}
716
- WHERE vault_id = ${vaultId} AND id = ${id}`;
659
+ const where = and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.id, id));
660
+ if (Object.values(patch).every((value) => value === undefined)) {
661
+ const rows = await this.db
662
+ .select({ id: vaultCredentials.id })
663
+ .from(vaultCredentials)
664
+ .where(where);
717
665
  return rows.length > 0;
718
666
  }
719
- const result = await sql `
720
- UPDATE ${this.t("vault_credentials")} SET ${sql(columns)}
721
- WHERE vault_id = ${vaultId} AND id = ${id}`;
667
+ const result = await this.db.update(vaultCredentials).set(patch).where(where);
722
668
  return result.count > 0;
723
669
  }
724
670
  async deleteCredential(vaultId, id) {
725
671
  this.check();
726
- const result = await this.sql `
727
- DELETE FROM ${this.t("vault_credentials")}
728
- WHERE vault_id = ${vaultId} AND id = ${id}`;
672
+ const result = await this.db
673
+ .delete(vaultCredentials)
674
+ .where(and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.id, id)));
729
675
  return result.count > 0;
730
676
  }
731
677
  async countCredentials() {
732
678
  this.check();
733
- const [row] = await this.sql `
734
- SELECT count(*)::int AS n FROM ${this.t("vault_credentials")}`;
679
+ const [row] = await this.db.select({ n: count() }).from(vaultCredentials);
735
680
  return row.n;
736
681
  }
737
682
  async insertVaultAudit(row) {
738
683
  this.check();
739
- await this.sql `
740
- INSERT INTO ${this.t("vault_audit")} (
741
- id, at, actor, action, vault_id, credential_id, session_id, target, outcome
742
- ) VALUES (
743
- ${row.id}, ${row.at}, ${row.actor}, ${row.action}, ${row.vaultId},
744
- ${row.credentialId}, ${row.sessionId}, ${row.target}, ${row.outcome}
745
- )`;
684
+ await this.db.insert(vaultAudit).values(row);
746
685
  }
747
686
  async vaultAudit(filter = {}) {
748
687
  this.check();
749
- const sql = this.sql;
750
- const rows = await sql `
751
- SELECT * FROM ${this.t("vault_audit")}
752
- ${filter.vaultId === undefined ? sql `` : sql `WHERE vault_id = ${filter.vaultId}`}
753
- ORDER BY ord
754
- ${filter.limit === undefined ? sql `` : sql `LIMIT ${filter.limit}`}`;
755
- return rows.map(auditRow);
688
+ const query = this.db
689
+ .select(AUDIT)
690
+ .from(vaultAudit)
691
+ .where(filter.vaultId === undefined ? undefined : eq(vaultAudit.vaultId, filter.vaultId))
692
+ .orderBy(vaultAudit.ord)
693
+ .$dynamic();
694
+ return filter.limit === undefined ? query : query.limit(filter.limit);
756
695
  }
757
696
  async getVaultIdempotency(id) {
758
697
  this.check();
759
- const [row] = await this.sql `
760
- SELECT id, body_hash, response FROM ${this.t("vault_idempotency")} WHERE id = ${id}`;
761
- return row && { id: row.id, bodyHash: row.body_hash, response: row.response };
698
+ const [row] = await this.db
699
+ .select()
700
+ .from(vaultIdempotency)
701
+ .where(eq(vaultIdempotency.id, id));
702
+ return row;
762
703
  }
763
704
  async insertVaultIdempotency(row) {
764
705
  this.check();
765
- await this.sql `
766
- INSERT INTO ${this.t("vault_idempotency")} (id, body_hash, response)
767
- VALUES (${row.id}, ${row.bodyHash}, ${row.response})`;
706
+ await this.db.insert(vaultIdempotency).values(row);
707
+ }
708
+ // --- subject tokens ------------------------------------------------------
709
+ async lockSigningKeys() {
710
+ this.check();
711
+ await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.signing_keys'))`);
712
+ }
713
+ async insertSigningKey(row) {
714
+ this.check();
715
+ await this.db.insert(signingKeys).values(row);
716
+ }
717
+ async signingKey(id) {
718
+ this.check();
719
+ const [row] = await this.db.select().from(signingKeys).where(eq(signingKeys.id, id));
720
+ return row;
721
+ }
722
+ async signingKeys(states) {
723
+ this.check();
724
+ return this.db
725
+ .select()
726
+ .from(signingKeys)
727
+ .where(states === undefined ? undefined : inArray(signingKeys.state, states))
728
+ .orderBy(signingKeys.createdAt, signingKeys.id);
729
+ }
730
+ async setSigningKeyState(id, from, to, at) {
731
+ this.check();
732
+ const stamp = SIGNING_KEY_STAMP[to];
733
+ const rows = await this.db
734
+ .update(signingKeys)
735
+ .set({ state: to, ...(stamp ? { [stamp]: at } : {}) })
736
+ .where(and(eq(signingKeys.id, id), eq(signingKeys.state, from)))
737
+ .returning({ id: signingKeys.id });
738
+ return rows.length === 1;
739
+ }
740
+ async countSigningKeys() {
741
+ this.check();
742
+ const [row] = await this.db.select({ n: count() }).from(signingKeys);
743
+ return row.n;
744
+ }
745
+ async subjectEpoch(subject) {
746
+ this.check();
747
+ const [row] = await this.db
748
+ .select({ epoch: subjectEpochs.epoch })
749
+ .from(subjectEpochs)
750
+ .where(eq(subjectEpochs.subject, subject));
751
+ return row?.epoch ?? 0;
752
+ }
753
+ async subjectEpochs(subjects) {
754
+ this.check();
755
+ if (subjects.length === 0)
756
+ return new Map();
757
+ const rows = await this.db
758
+ .select({ subject: subjectEpochs.subject, epoch: subjectEpochs.epoch })
759
+ .from(subjectEpochs)
760
+ .where(inArray(subjectEpochs.subject, subjects));
761
+ return new Map(rows.map((row) => [row.subject, row.epoch]));
762
+ }
763
+ async bumpSubjectEpoch(subject, at) {
764
+ this.check();
765
+ const [row] = await this.db
766
+ .insert(subjectEpochs)
767
+ .values({ subject, epoch: 1, revokedAt: at })
768
+ .onConflictDoUpdate({
769
+ target: subjectEpochs.subject,
770
+ set: { epoch: sql `${subjectEpochs.epoch} + 1`, revokedAt: excluded("revoked_at") },
771
+ })
772
+ .returning({ epoch: subjectEpochs.epoch });
773
+ return row.epoch;
774
+ }
775
+ async lockSubjectUsage(initial) {
776
+ this.check();
777
+ await this.db.insert(subjectUsage).values(initial).onConflictDoNothing();
778
+ const [row] = await this.db
779
+ .select()
780
+ .from(subjectUsage)
781
+ .where(eq(subjectUsage.subject, initial.subject))
782
+ .for("update");
783
+ return row;
784
+ }
785
+ async putSubjectUsage(row) {
786
+ this.check();
787
+ await this.db
788
+ .insert(subjectUsage)
789
+ .values(row)
790
+ .onConflictDoUpdate({
791
+ target: subjectUsage.subject,
792
+ set: { turnTokens: excluded("turn_tokens"), refilledAt: excluded("refilled_at") },
793
+ });
794
+ }
795
+ async countOwnerSessions(ownerUserId, statuses) {
796
+ this.check();
797
+ const [row] = await this.db
798
+ .select({ n: count() })
799
+ .from(sessions)
800
+ .where(and(eq(sessions.ownerUserId, ownerUserId), inArray(sessions.status, statuses)));
801
+ return row.n;
802
+ }
803
+ // --- publishable keys ----------------------------------------------------
804
+ async insertPublishableKey(row) {
805
+ this.check();
806
+ await this.db.insert(publishableKeys).values(row);
807
+ }
808
+ async publishableKeyByKey(key) {
809
+ this.check();
810
+ const [row] = await this.db
811
+ .select()
812
+ .from(publishableKeys)
813
+ .where(eq(publishableKeys.key, key));
814
+ return row;
815
+ }
816
+ async publishableKey(id) {
817
+ this.check();
818
+ const [row] = await this.db
819
+ .select()
820
+ .from(publishableKeys)
821
+ .where(eq(publishableKeys.id, id));
822
+ return row;
823
+ }
824
+ async publishableKeys() {
825
+ this.check();
826
+ return this.db
827
+ .select()
828
+ .from(publishableKeys)
829
+ .orderBy(publishableKeys.createdAt, publishableKeys.id);
830
+ }
831
+ async updatePublishableKey(id, patch) {
832
+ this.check();
833
+ if (patch.originsJson === undefined && patch.revokedAt === undefined)
834
+ return (await this.publishableKey(id)) !== undefined;
835
+ const rows = await this.db
836
+ .update(publishableKeys)
837
+ .set(patch)
838
+ .where(eq(publishableKeys.id, id))
839
+ .returning({ id: publishableKeys.id });
840
+ return rows.length === 1;
841
+ }
842
+ // --- model usage ---------------------------------------------------------
843
+ async recordModelUsage(row) {
844
+ this.check();
845
+ const [inserted] = await this.db
846
+ .insert(modelUsage)
847
+ .values({
848
+ ...row,
849
+ duplicate: sql `EXISTS (SELECT 1 FROM ${modelUsage} WHERE ${modelUsage.effectKey} = ${row.effectKey})`,
850
+ })
851
+ .returning({ duplicate: modelUsage.duplicate });
852
+ return { ...row, duplicate: inserted.duplicate };
853
+ }
854
+ async modelUsageTotals(query) {
855
+ this.check();
856
+ const [row] = await this.db
857
+ .select({
858
+ calls: count(),
859
+ tokens: sql `coalesce(sum(${modelUsage.totalTokens}), 0)::bigint`.mapWith(Number),
860
+ costUsd: sql `coalesce(sum(${modelUsage.costUsd}), 0)::double precision`.mapWith(Number),
861
+ })
862
+ .from(modelUsage)
863
+ .where(and(query.scope === "agent" ? eq(modelUsage.agentId, query.id ?? "") : undefined, query.scope === "turn" ? eq(modelUsage.turnId, query.id ?? "") : undefined, query.since !== undefined ? gte(modelUsage.createdAt, query.since) : undefined));
864
+ return row;
865
+ }
866
+ async listModelBudgets() {
867
+ this.check();
868
+ return this.db
869
+ .select()
870
+ .from(modelBudgets)
871
+ .orderBy(sql `${modelBudgets.scope} COLLATE "C"`, modelBudgets.scopeId);
872
+ }
873
+ async putModelBudgets(rows) {
874
+ this.check();
875
+ await this.db.delete(modelBudgets);
876
+ if (rows.length > 0)
877
+ await this.db.insert(modelBudgets).values([...rows]);
878
+ }
879
+ // --- tool crossings --------------------------------------------------------
880
+ async toolCrossing(key) {
881
+ this.check();
882
+ const [row] = await this.db.select().from(toolCrossings).where(eq(toolCrossings.key, key));
883
+ return row;
884
+ }
885
+ async startToolCrossing(row) {
886
+ this.check();
887
+ const inserted = await this.db
888
+ .insert(toolCrossings)
889
+ .values({ ...row, settledAt: null, answer: null })
890
+ .onConflictDoNothing()
891
+ .returning({ key: toolCrossings.key });
892
+ return inserted.length === 1;
893
+ }
894
+ async settleToolCrossing(key, answer, settledAt) {
895
+ this.check();
896
+ await this.db
897
+ .update(toolCrossings)
898
+ .set({ answer, settledAt })
899
+ .where(eq(toolCrossings.key, key));
900
+ }
901
+ async pruneToolCrossings(before) {
902
+ this.check();
903
+ const deleted = await this.db
904
+ .delete(toolCrossings)
905
+ .where(lt(toolCrossings.settledAt, before))
906
+ .returning({ key: toolCrossings.key });
907
+ return deleted.length;
768
908
  }
769
909
  // --- settings ------------------------------------------------------------
770
910
  async getSetting(key) {
771
911
  this.check();
772
- const [row] = await this.sql `
773
- SELECT value FROM ${this.t("tenant_settings")} WHERE key = ${key}`;
912
+ const [row] = await this.db
913
+ .select({ value: tenantSettings.value })
914
+ .from(tenantSettings)
915
+ .where(eq(tenantSettings.key, key));
774
916
  return row?.value;
775
917
  }
776
918
  async putSetting(key, value) {
777
919
  this.check();
778
- await this.sql `
779
- INSERT INTO ${this.t("tenant_settings")} (key, value) VALUES (${key}, ${value})
780
- ON CONFLICT (key) DO UPDATE SET value = excluded.value`;
920
+ await this.db
921
+ .insert(tenantSettings)
922
+ .values({ key, value })
923
+ .onConflictDoUpdate({ target: tenantSettings.key, set: { value: excluded("value") } });
781
924
  }
782
925
  // --- reset ---------------------------------------------------------------
783
926
  async reset(scope) {
784
927
  this.check();
785
- const sql = this.sql;
786
- if (scope === "sessions" || scope === "all")
928
+ const db = this.db;
929
+ if (scope === "sessions" || scope === "all") {
787
930
  for (const table of SESSION_TABLES)
788
- await sql `DELETE FROM ${this.t(table)}`;
931
+ await db.delete(table);
932
+ await db.delete(subjectUsage);
933
+ await db.delete(toolCrossings);
934
+ // The record goes with the sessions, and the Tenant moves to a new basin: the ids it
935
+ // frees start again in an empty one (Durable Streams §8.1).
936
+ await db.delete(sessionEvents);
937
+ await db.delete(sessionLogHeads);
938
+ await db.update(tenant).set({
939
+ retiredGenerations: sql `array_append(${tenant.retiredGenerations}, ${tenant.basinGeneration})`,
940
+ basinGeneration: sql `${tenant.basinGeneration} + 1`,
941
+ });
942
+ }
789
943
  if (scope === "sandboxes" || scope === "all")
790
- await sql `DELETE FROM ${this.t("sandboxes")}`;
944
+ await db.delete(sandboxes);
791
945
  if (scope === "all") {
792
- await sql `DELETE FROM ${this.t("definitions")}`;
793
- await sql `DELETE FROM ${this.t("executors")}`;
794
- await sql `DELETE FROM ${this.t("vaults")} WHERE scope <> 'host'`;
946
+ await db.delete(definitions);
947
+ await db.delete(endpoints);
948
+ await db.delete(modelUsage);
949
+ await db.delete(modelBudgets);
950
+ await db.delete(vaults).where(ne(vaults.scope, "host"));
795
951
  }
796
952
  }
797
953
  }