@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.
- package/CHANGELOG.md +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +122 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +266 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +11 -0
- package/dist/api/http/routes/tenant.js +244 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +137 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +33 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Postgres Session Store (architecture §12.2):
|
|
3
|
-
*
|
|
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
|
|
8
|
-
* are addressed by
|
|
9
|
-
* never through `search_path
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
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,
|
|
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
|
|
33
|
-
* wrote, so every string round-trips, including U+0000 and
|
|
34
|
-
* surrogates that `jsonb` rejects
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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 {
|
|
40
|
-
import {
|
|
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 {
|
|
43
|
-
import {
|
|
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
|
-
|
|
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.
|
|
64
|
-
|
|
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.
|
|
81
|
-
t = new PostgresTx(
|
|
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 =
|
|
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
|
|
127
|
+
if (t.events.length > 0) {
|
|
98
128
|
const commit = {
|
|
99
129
|
events: t.events,
|
|
100
|
-
|
|
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.
|
|
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
|
|
155
|
-
epoch:
|
|
156
|
-
ownerExpiresAt: row.
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
schema;
|
|
208
|
+
db;
|
|
252
209
|
tenantId;
|
|
253
210
|
now;
|
|
254
211
|
closed = false;
|
|
255
212
|
events = [];
|
|
256
|
-
|
|
213
|
+
generations = [];
|
|
257
214
|
callbacks = [];
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
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.
|
|
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
|
|
322
|
-
// The
|
|
323
|
-
const [
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
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
|
-
|
|
336
|
-
|
|
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.
|
|
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
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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:
|
|
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.
|
|
388
|
-
|
|
389
|
-
|
|
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.
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
424
|
-
|
|
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
|
-
:
|
|
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.
|
|
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(
|
|
380
|
+
return this.bodies(definitions);
|
|
441
381
|
}
|
|
442
382
|
async listSandboxes() {
|
|
443
383
|
this.check();
|
|
444
|
-
return this.bodies(
|
|
384
|
+
return this.bodies(sandboxes);
|
|
445
385
|
}
|
|
446
|
-
async
|
|
447
|
-
this.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
417
|
+
async expiredDeliveries(now, limit) {
|
|
465
418
|
this.check();
|
|
466
|
-
const
|
|
467
|
-
const rows = await
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
528
|
-
const [row] = await sql `
|
|
474
|
+
const [row] = await this.db.execute(sql `
|
|
529
475
|
SELECT
|
|
530
|
-
(SELECT count(*) FROM ${
|
|
531
|
-
(SELECT count(*) FROM ${
|
|
532
|
-
|
|
533
|
-
(SELECT count(*) FROM ${
|
|
534
|
-
|
|
535
|
-
(SELECT count(*) FROM ${
|
|
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
|
-
// ---
|
|
546
|
-
async
|
|
493
|
+
// --- basin generations --------------------------------------------------
|
|
494
|
+
async basinGenerations() {
|
|
547
495
|
this.check();
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
|
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
|
-
|
|
588
|
-
|
|
504
|
+
await this.db.update(tenant).set({
|
|
505
|
+
retiredGenerations: sql `array_remove(${tenant.retiredGenerations}, ${generation}::int)`,
|
|
506
|
+
});
|
|
589
507
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return
|
|
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
|
|
556
|
+
async deleteEndpoint(agentId) {
|
|
597
557
|
this.check();
|
|
598
|
-
await this.
|
|
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
|
|
560
|
+
async recordEndpointHealth(agentId, update) {
|
|
615
561
|
this.check();
|
|
616
|
-
|
|
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.
|
|
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.
|
|
628
|
-
|
|
629
|
-
|
|
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.
|
|
634
|
-
|
|
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.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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.
|
|
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.
|
|
655
|
-
return 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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
|
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.
|
|
727
|
-
|
|
728
|
-
|
|
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.
|
|
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.
|
|
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
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return
|
|
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.
|
|
760
|
-
|
|
761
|
-
|
|
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.
|
|
766
|
-
|
|
767
|
-
|
|
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.
|
|
773
|
-
|
|
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.
|
|
779
|
-
|
|
780
|
-
|
|
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
|
|
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
|
|
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
|
|
944
|
+
await db.delete(sandboxes);
|
|
791
945
|
if (scope === "all") {
|
|
792
|
-
await
|
|
793
|
-
await
|
|
794
|
-
await
|
|
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
|
}
|