@sixb/pg 0.1.20 → 0.1.21

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 (43) hide show
  1. package/dist/agents/threads.d.ts +4 -0
  2. package/dist/agents/threads.d.ts.map +1 -1
  3. package/dist/auth-storage/sessions.d.ts +2 -1
  4. package/dist/auth-storage/sessions.d.ts.map +1 -1
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +959 -7
  8. package/dist/index.js.map +15 -13
  9. package/dist/migrations.d.ts.map +1 -1
  10. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
  11. package/dist/ontology-storage/sources.d.ts +2 -1
  12. package/dist/ontology-storage/sources.d.ts.map +1 -1
  13. package/dist/pg-action-run-storage.d.ts +4 -0
  14. package/dist/pg-action-run-storage.d.ts.map +1 -1
  15. package/dist/pg-activity-storage.d.ts +22 -0
  16. package/dist/pg-activity-storage.d.ts.map +1 -0
  17. package/dist/pg-artifact-storage.d.ts +19 -0
  18. package/dist/pg-artifact-storage.d.ts.map +1 -0
  19. package/dist/pg-execution-storage.d.ts +2 -1
  20. package/dist/pg-execution-storage.d.ts.map +1 -1
  21. package/dist/pg-workflow-run-storage.d.ts +4 -0
  22. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  23. package/dist/share-grant-storage.d.ts +2 -1
  24. package/dist/share-grant-storage.d.ts.map +1 -1
  25. package/package.json +3 -3
  26. package/src/agents/threads.ts +23 -0
  27. package/src/auth-storage/sessions.ts +20 -0
  28. package/src/index.ts +10 -0
  29. package/src/migrations/063-artifacts.sql +67 -0
  30. package/src/migrations/064-drop-output-diagnostics.sql +14 -0
  31. package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
  32. package/src/migrations/066-activity.sql +61 -0
  33. package/src/migrations/067-activity-timeline.sql +15 -0
  34. package/src/migrations/068-activity-interactions.sql +20 -0
  35. package/src/migrations.ts +18 -0
  36. package/src/ontology-storage/replacement-plans.ts +25 -7
  37. package/src/ontology-storage/sources.ts +37 -0
  38. package/src/pg-action-run-storage.ts +23 -0
  39. package/src/pg-activity-storage.ts +486 -0
  40. package/src/pg-artifact-storage.ts +325 -0
  41. package/src/pg-execution-storage.ts +52 -0
  42. package/src/pg-workflow-run-storage.ts +23 -0
  43. package/src/share-grant-storage.ts +21 -0
package/dist/index.js CHANGED
@@ -1142,6 +1142,23 @@ class PgAgentThreadStore {
1142
1142
  `;
1143
1143
  return row ? rowToThreadRecord(row) : null;
1144
1144
  }
1145
+ async getByIds(params) {
1146
+ const ids = [...new Set(params.ids)];
1147
+ if (ids.length === 0) {
1148
+ return [];
1149
+ }
1150
+ const rows = await this.sql`
1151
+ SELECT *
1152
+ FROM agent_threads
1153
+ WHERE project_id = ${params.projectId}
1154
+ AND id = ANY(${this.sql.array(ids)}::text[])
1155
+ `;
1156
+ const byId = new Map(rows.map((row) => [row.id, row]));
1157
+ return ids.flatMap((id) => {
1158
+ const row = byId.get(id);
1159
+ return row ? [rowToThreadRecord(row)] : [];
1160
+ });
1161
+ }
1145
1162
  async list(input) {
1146
1163
  if (input.statuses !== undefined && input.statuses.length === 0) {
1147
1164
  return { threads: [], hasMore: false, total: 0 };
@@ -2739,6 +2756,22 @@ class PgAuthSessionStore {
2739
2756
  `;
2740
2757
  return rows.map(rowToSessionRecord);
2741
2758
  }
2759
+ async listByUserId(params) {
2760
+ const rows = await this.sql`
2761
+ SELECT *
2762
+ FROM auth_sessions
2763
+ WHERE project_id = ${params.projectId}
2764
+ AND user_id = ${params.userId}
2765
+ ${params.before ? this.sql`AND created_at <= ${params.before.createdAt}` : this.sql``}
2766
+ ORDER BY created_at DESC, id DESC
2767
+ LIMIT ${params.limit + 1}
2768
+ OFFSET ${params.before?.skip ?? 0}
2769
+ `;
2770
+ return {
2771
+ sessions: rows.slice(0, params.limit).map(rowToSessionRecord),
2772
+ hasMore: rows.length > params.limit
2773
+ };
2774
+ }
2742
2775
  async findValidByTokenHash(params) {
2743
2776
  const row = await getSessionRowById(this.sql, params);
2744
2777
  if (!row || row.refresh_token_hash !== null || row.token_hash !== params.tokenHash || !isLiveSessionRow(row, params.now)) {
@@ -8119,6 +8152,201 @@ ALTER TABLE auth_magic_links DROP COLUMN audience;
8119
8152
  ALTER TABLE auth_oidc_authorization_attempts DROP COLUMN audience;
8120
8153
  `;
8121
8154
 
8155
+ // src/migrations/063-artifacts.sql
8156
+ var _063_artifacts_default = `-- Artifacts: versioned documents, sheets, decks, and files. An artifact's kind never
8157
+ -- changes. Versions take \`next_version\`, which only grows, so a number is never reused after its
8158
+ -- version is deleted.
8159
+ CREATE TABLE artifacts (
8160
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
8161
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
8162
+ kind TEXT NOT NULL CHECK (kind IN ('document', 'sheet', 'slides', 'file')),
8163
+ title TEXT NOT NULL CHECK (length(trim(title)) > 0),
8164
+ owner_principal_type TEXT NOT NULL
8165
+ CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
8166
+ owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
8167
+ latest_version INTEGER NOT NULL CHECK (latest_version > 0),
8168
+ next_version INTEGER NOT NULL CHECK (next_version > latest_version),
8169
+ created_at TIMESTAMPTZ NOT NULL,
8170
+ updated_at TIMESTAMPTZ NOT NULL,
8171
+ PRIMARY KEY (project_id, id)
8172
+ );
8173
+
8174
+ CREATE INDEX idx_artifacts_project_updated ON artifacts (project_id, updated_at DESC, id);
8175
+ CREATE INDEX idx_artifacts_owner_updated
8176
+ ON artifacts (project_id, owner_principal_type, owner_principal_id, updated_at DESC, id);
8177
+
8178
+ -- \`refs\`, not \`references\`: that word is reserved in SQL.
8179
+ CREATE TABLE artifact_versions (
8180
+ project_id TEXT NOT NULL,
8181
+ artifact_id TEXT NOT NULL,
8182
+ version INTEGER NOT NULL CHECK (version > 0),
8183
+ title TEXT NOT NULL CHECK (length(trim(title)) > 0),
8184
+ entry TEXT NOT NULL CHECK (length(entry) > 0),
8185
+ files JSONB NOT NULL CHECK (jsonb_typeof(files) = 'array'),
8186
+ outline JSONB NOT NULL CHECK (jsonb_typeof(outline) = 'object'),
8187
+ refs JSONB NOT NULL CHECK (jsonb_typeof(refs) = 'array'),
8188
+ author_principal_type TEXT NOT NULL
8189
+ CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
8190
+ author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
8191
+ origin_thread_id TEXT,
8192
+ origin_run_id TEXT,
8193
+ created_at TIMESTAMPTZ NOT NULL,
8194
+ PRIMARY KEY (project_id, artifact_id, version),
8195
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
8196
+ CHECK ((origin_thread_id IS NULL) = (origin_run_id IS NULL))
8197
+ );
8198
+
8199
+ CREATE INDEX idx_artifact_versions_origin_run
8200
+ ON artifact_versions (project_id, origin_run_id) WHERE origin_run_id IS NOT NULL;
8201
+ CREATE INDEX idx_artifact_versions_origin_thread
8202
+ ON artifact_versions (project_id, origin_thread_id, artifact_id) WHERE origin_thread_id IS NOT NULL;
8203
+
8204
+ -- Short-lived links that a sandboxed frame loads a deck's files through. Only the SHA-256 of each
8205
+ -- link's token is stored. \`targets\` is what the frame may show from other artifacts, decided for
8206
+ -- its reader when the link was issued: a JSON array of { address, artifactId, version }.
8207
+ CREATE TABLE artifact_frame_grants (
8208
+ project_id TEXT NOT NULL,
8209
+ token_hash TEXT NOT NULL CHECK (token_hash ~ '^[0-9a-f]{64}$'),
8210
+ artifact_id TEXT NOT NULL,
8211
+ version INTEGER NOT NULL CHECK (version > 0),
8212
+ targets JSONB NOT NULL CHECK (jsonb_typeof(targets) = 'array'),
8213
+ created_at TIMESTAMPTZ NOT NULL,
8214
+ expires_at TIMESTAMPTZ NOT NULL,
8215
+ PRIMARY KEY (project_id, token_hash),
8216
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
8217
+ CHECK (expires_at > created_at)
8218
+ );
8219
+
8220
+ CREATE INDEX idx_artifact_frame_grants_expiry ON artifact_frame_grants (project_id, expires_at);
8221
+ -- Deleting an artifact cascades here, and Postgres does not index a foreign key by itself.
8222
+ CREATE INDEX idx_artifact_frame_grants_artifact ON artifact_frame_grants (project_id, artifact_id);
8223
+ `;
8224
+
8225
+ // src/migrations/064-drop-output-diagnostics.sql
8226
+ var _064_drop_output_diagnostics_default = `-- The Agent delivers files by publishing artifacts, so a run no longer collects an output
8227
+ -- directory or reports on it. Stored \`output_*\` diagnostics go; a run left with none stores none.
8228
+ UPDATE agent_runs
8229
+ SET diagnostics = (
8230
+ SELECT jsonb_agg(item.value ORDER BY item.position)
8231
+ FROM jsonb_array_elements(agent_runs.diagnostics) WITH ORDINALITY AS item(value, position)
8232
+ WHERE item.value->>'code' NOT LIKE 'output\\_%'
8233
+ )
8234
+ WHERE jsonb_typeof(diagnostics) = 'array'
8235
+ AND EXISTS (
8236
+ SELECT 1
8237
+ FROM jsonb_array_elements(agent_runs.diagnostics) AS item(value)
8238
+ WHERE item.value->>'code' LIKE 'output\\_%'
8239
+ );
8240
+ `;
8241
+
8242
+ // src/migrations/065-plan-work-payload-statistics.sql
8243
+ var _065_plan_work_payload_statistics_default = "-- A plan's work is analyzed whenever a stream has staged enough of it, and most of that ANALYZE\n-- went into the statistics of `payload`, which no query reads: the plan queries filter on the\n-- typed columns and only extract values from `payload`, and an extracted value has no statistics.\n-- A query that filters `payload` itself (`@>`, `?`) would need them back.\nALTER TABLE ontology_replacement_plan_work ALTER COLUMN payload SET STATISTICS 0;\n";
8244
+
8245
+ // src/migrations/066-activity.sql
8246
+ var _066_activity_default = `-- App activity: one source per verified session, the visits it opened, and their page views.
8247
+ -- Foreground activity moves a visit's \`last_seen_at\` and stores no row, unless it opens a visit.
8248
+ -- A session id names an \`auth_sessions\` row for a user and a \`share_sessions\` row for a share; it
8249
+ -- is not a secret, and no foreign key holds it, so history outlives its sessions. Ids compare
8250
+ -- byte by byte, as the other providers sort them.
8251
+ CREATE TABLE activity_sources (
8252
+ project_id TEXT NOT NULL,
8253
+ actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
8254
+ session_id TEXT NOT NULL CHECK (length(session_id) > 0),
8255
+ visit_id TEXT NOT NULL,
8256
+ last_seen_at TIMESTAMPTZ NOT NULL,
8257
+ sequence INTEGER NOT NULL CHECK (sequence >= 0),
8258
+ window_start TIMESTAMPTZ NOT NULL,
8259
+ window_count INTEGER NOT NULL CHECK (window_count >= 0),
8260
+ PRIMARY KEY (project_id, actor_type, session_id)
8261
+ );
8262
+
8263
+ CREATE TABLE activity_visits (
8264
+ project_id TEXT NOT NULL,
8265
+ id TEXT COLLATE "C" NOT NULL,
8266
+ actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
8267
+ user_id TEXT COLLATE "C",
8268
+ grant_id TEXT,
8269
+ session_id TEXT NOT NULL CHECK (length(session_id) > 0),
8270
+ started_at TIMESTAMPTZ NOT NULL,
8271
+ last_seen_at TIMESTAMPTZ NOT NULL CHECK (last_seen_at >= started_at),
8272
+ page_views INTEGER NOT NULL CHECK (page_views >= 0),
8273
+ attribution JSONB CHECK (attribution IS NULL OR jsonb_typeof(attribution) = 'object'),
8274
+ PRIMARY KEY (project_id, id),
8275
+ CHECK (
8276
+ (actor_type = 'user' AND user_id IS NOT NULL AND grant_id IS NULL)
8277
+ OR (actor_type = 'share' AND user_id IS NULL AND grant_id IS NOT NULL)
8278
+ )
8279
+ );
8280
+
8281
+ -- A period reads the visits it overlaps. A visit lasts less than a day, so they started in
8282
+ -- \`[from - 1 day, to)\`: both period indexes bound \`started_at\` on both sides. \`last_seen_at\` is
8283
+ -- left out of every index, so the update each batch makes touches none of them. The user index
8284
+ -- also serves deleting one user's history.
8285
+ CREATE INDEX idx_activity_visits_started
8286
+ ON activity_visits (project_id, started_at, id);
8287
+ CREATE INDEX idx_activity_visits_user
8288
+ ON activity_visits (project_id, user_id, started_at, id);
8289
+ -- A session's visits, to join them with the session and the executions it authorized.
8290
+ CREATE INDEX idx_activity_visits_session
8291
+ ON activity_visits (project_id, actor_type, session_id);
8292
+
8293
+ CREATE TABLE activity_page_views (
8294
+ project_id TEXT NOT NULL,
8295
+ visit_id TEXT COLLATE "C" NOT NULL,
8296
+ sequence INTEGER NOT NULL CHECK (sequence > 0),
8297
+ id TEXT NOT NULL,
8298
+ route TEXT NOT NULL,
8299
+ received_at TIMESTAMPTZ NOT NULL,
8300
+ attribution JSONB CHECK (attribution IS NULL OR jsonb_typeof(attribution) = 'object'),
8301
+ PRIMARY KEY (project_id, visit_id, sequence),
8302
+ FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
8303
+ );
8304
+
8305
+ -- Finds a retried page view by the id its browser chose.
8306
+ CREATE INDEX idx_activity_page_views_id ON activity_page_views (project_id, id);
8307
+ `;
8308
+
8309
+ // src/migrations/067-activity-timeline.sql
8310
+ var _067_activity_timeline_default = `-- A visit's timeline reads what its session started: the roots it authorized in a window, then
8311
+ -- everything they caused. A signed-in session is the credential of its roots, a shared link's
8312
+ -- session their delegation; a child names its parent. Agent executions copy their parent's
8313
+ -- session: the root indexes hold only executions without a parent, so they never see those, and
8314
+ -- end with \`id\` so the root read's order needs no sort. A user's connection history reads
8315
+ -- \`idx_auth_sessions_user_created\`, which already exists.
8316
+ CREATE INDEX idx_executions_session_roots
8317
+ ON executions (project_id, authority_session_id, created_at, id)
8318
+ WHERE authority_session_id IS NOT NULL AND parent_execution_id IS NULL;
8319
+ CREATE INDEX idx_executions_delegation_roots
8320
+ ON executions (project_id, authority_delegation_session_id, created_at, id)
8321
+ WHERE authority_delegation_session_id IS NOT NULL AND parent_execution_id IS NULL;
8322
+ CREATE INDEX idx_executions_parent
8323
+ ON executions (project_id, parent_execution_id)
8324
+ WHERE parent_execution_id IS NOT NULL;
8325
+ `;
8326
+
8327
+ // src/migrations/068-activity-interactions.sql
8328
+ var _068_activity_interactions_default = `-- Interactions an app marks, such as a button: their name and route, never a value. They share
8329
+ -- the visit's sequence with its page views, so a visit reads both in the order they happened, and
8330
+ -- cascade with the visit, which counts them as it counts its page views.
8331
+ ALTER TABLE activity_visits
8332
+ ADD COLUMN interactions INTEGER NOT NULL DEFAULT 0 CHECK (interactions >= 0);
8333
+
8334
+ CREATE TABLE activity_interactions (
8335
+ project_id TEXT NOT NULL,
8336
+ visit_id TEXT COLLATE "C" NOT NULL,
8337
+ sequence INTEGER NOT NULL CHECK (sequence > 0),
8338
+ id TEXT NOT NULL,
8339
+ name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 100),
8340
+ route TEXT NOT NULL,
8341
+ received_at TIMESTAMPTZ NOT NULL,
8342
+ PRIMARY KEY (project_id, visit_id, sequence),
8343
+ FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
8344
+ );
8345
+
8346
+ -- Finds a retried interaction by the id its browser chose.
8347
+ CREATE INDEX idx_activity_interactions_id ON activity_interactions (project_id, id);
8348
+ `;
8349
+
8122
8350
  // src/migrations.ts
8123
8351
  var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
8124
8352
  function pgSql(id, sqlText) {
@@ -8347,7 +8575,13 @@ var postgresStorageMigrations = defineMigrations({
8347
8575
  pgSql("059-file-download-grants", _059_file_download_grants_default),
8348
8576
  pgSql("060-drained-source-versions", _060_drained_source_versions_default),
8349
8577
  pgSql("061-terminal-action-runs", _061_terminal_action_runs_default),
8350
- pgSql("062-drop-auth-audience", _062_drop_auth_audience_default)
8578
+ pgSql("062-drop-auth-audience", _062_drop_auth_audience_default),
8579
+ pgSql("063-artifacts", _063_artifacts_default),
8580
+ pgSql("064-drop-output-diagnostics", _064_drop_output_diagnostics_default),
8581
+ pgSql("065-plan-work-payload-statistics", _065_plan_work_payload_statistics_default),
8582
+ pgSql("066-activity", _066_activity_default),
8583
+ pgSql("067-activity-timeline", _067_activity_timeline_default),
8584
+ pgSql("068-activity-interactions", _068_activity_interactions_default)
8351
8585
  ]
8352
8586
  });
8353
8587
 
@@ -12675,7 +12909,7 @@ import {
12675
12909
  var PG_PLAN_WORK_TABLE = "ontology_replacement_plan_work";
12676
12910
  var IDENTITIES = "ontology_replacement_plan_identities";
12677
12911
  var SORT_KEY = "encode(convert_to(identity_key, 'UTF8'), 'hex')";
12678
- var ANALYZE_ROWS = 1e4;
12912
+ var ANALYZE_ROWS = 1000;
12679
12913
  function objectRevision(type, id) {
12680
12914
  return `jsonb_build_array(
12681
12915
  (SELECT jsonb_build_array(version, last_commit_id) FROM objects
@@ -12812,7 +13046,7 @@ class PgOntologyReplacementPlanStorage {
12812
13046
  }
12813
13047
  async* streamState(input) {
12814
13048
  assertPageRows(input.pageRows);
12815
- await runPgTransaction(this.sql, async (sql) => {
13049
+ const versionId = await runPgTransaction(this.sql, async (sql) => {
12816
13050
  const { candidate } = await requirePlan(sql, input);
12817
13051
  if (input.entityKind === "object" && candidate.projection_kind !== "object") {
12818
13052
  throw new MaterializationConflictError4("source-materialization", "Link projection replacement cannot stream object state.");
@@ -12825,6 +13059,7 @@ class PgOntologyReplacementPlanStorage {
12825
13059
  if (expanded >= ANALYZE_ROWS && !isWithinPgTransaction(this.sql))
12826
13060
  await analyzePlans(sql);
12827
13061
  }
13062
+ return candidate.version_id;
12828
13063
  });
12829
13064
  let after = "";
12830
13065
  let streamed = 0;
@@ -12854,8 +13089,8 @@ class PgOntologyReplacementPlanStorage {
12854
13089
  return state;
12855
13090
  }, { isolation: "repeatableRead" });
12856
13091
  if (!page) {
12857
- if (streamed >= ANALYZE_ROWS && !isWithinPgTransaction(this.sql)) {
12858
- await analyzePlans(this.sql);
13092
+ if (streamed > 0 && !isWithinPgTransaction(this.sql)) {
13093
+ await analyzeStagedWork(this.sql, versionId);
12859
13094
  }
12860
13095
  return;
12861
13096
  }
@@ -13383,6 +13618,16 @@ async function purgeTouches(sql, projectId, limit) {
13383
13618
  async function analyzePlans(sql) {
13384
13619
  await sql`ANALYZE ${sql(IDENTITIES)}, ${sql(PG_PLAN_WORK_TABLE)}`;
13385
13620
  }
13621
+ async function analyzeStagedWork(sql, versionId) {
13622
+ const [staged] = await sql`
13623
+ SELECT COUNT(*) AS rows FROM (
13624
+ SELECT 1 FROM ${sql(PG_PLAN_WORK_TABLE)} WHERE work_id = ${versionId} LIMIT ${ANALYZE_ROWS}
13625
+ ) AS work
13626
+ `;
13627
+ if (Number(staged?.rows ?? 0) < ANALYZE_ROWS)
13628
+ return;
13629
+ await analyzePlans(sql);
13630
+ }
13386
13631
  async function dropUnplannedWork(sql, versionId, kind) {
13387
13632
  await sql`
13388
13633
  DELETE FROM ${sql(PG_PLAN_WORK_TABLE)}
@@ -14650,6 +14895,7 @@ import { ONTOLOGY_OUTBOX_FAILURE_CODES as ONTOLOGY_OUTBOX_FAILURE_CODES2 } from
14650
14895
 
14651
14896
  // src/pg-execution-storage.ts
14652
14897
  import {
14898
+ compareExecutionCreation,
14653
14899
  executionRecordFromStorageRow,
14654
14900
  executionRecordToStorageRow,
14655
14901
  prepareExecutionRecord
@@ -14743,6 +14989,37 @@ class PgExecutionStorage {
14743
14989
  `;
14744
14990
  return row ? pgExecutionRecord(row) : null;
14745
14991
  }
14992
+ async listBySession(input) {
14993
+ const session = input.session.type === "user" ? this.sql`authority_session_id = ${input.session.id}` : this.sql`authority_delegation_session_id = ${input.session.id}`;
14994
+ const rows = await this.sql`
14995
+ WITH RECURSIVE tree AS (
14996
+ (
14997
+ SELECT *, 0 AS depth
14998
+ FROM executions
14999
+ WHERE project_id = ${input.projectId}
15000
+ AND ${session}
15001
+ AND parent_execution_id IS NULL
15002
+ AND created_at >= ${input.from}
15003
+ AND created_at < ${input.to}
15004
+ ORDER BY created_at, id
15005
+ LIMIT ${input.limit + 1}
15006
+ )
15007
+ UNION ALL
15008
+ SELECT child.*, tree.depth + 1
15009
+ FROM tree
15010
+ JOIN executions child
15011
+ ON child.project_id = tree.project_id
15012
+ AND child.parent_execution_id = tree.id
15013
+ )
15014
+ SELECT * FROM tree
15015
+ LIMIT ${input.limit + 1}
15016
+ `;
15017
+ const records = rows.map((row) => ({ depth: row.depth, record: pgExecutionRecord(row) })).sort((left, right) => left.depth - right.depth || compareExecutionCreation(left.record, right.record)).slice(0, input.limit).map(({ record }) => record);
15018
+ return {
15019
+ executions: records.sort(compareExecutionCreation),
15020
+ truncated: rows.length > input.limit
15021
+ };
15022
+ }
14746
15023
  }
14747
15024
  function pgExecutionRecord(row) {
14748
15025
  return executionRecordFromStorageRow(toStorageRow(row));
@@ -15215,6 +15492,26 @@ class PgOntologySourceStorage {
15215
15492
  return row ? sourceRecord(row) : null;
15216
15493
  });
15217
15494
  }
15495
+ async listActive(input) {
15496
+ return this.runRootOperation(async (sql) => {
15497
+ assertNonblank(input.projectId, "Source project id");
15498
+ const rows = await sql`
15499
+ SELECT source_id, dataset_id, dataset_version_id, dataset_version_created_at,
15500
+ projection_revision
15501
+ FROM ontology_sources
15502
+ WHERE project_id = ${input.projectId} AND status = 'active'
15503
+ `;
15504
+ return rows.map((row) => ({
15505
+ source: { projectionId: row.source_id },
15506
+ datasetVersion: {
15507
+ datasetId: row.dataset_id,
15508
+ versionId: row.dataset_version_id,
15509
+ createdAt: toIsoString2(row.dataset_version_created_at)
15510
+ },
15511
+ projectionRevision: row.projection_revision
15512
+ }));
15513
+ });
15514
+ }
15218
15515
  async adopt(input) {
15219
15516
  return this.runRootOperation(async (sql) => {
15220
15517
  assertProjectAndSource(input);
@@ -15886,6 +16183,23 @@ class PgActionRunStorage {
15886
16183
  `;
15887
16184
  return row ? rowToActionRunRecord(row) : null;
15888
16185
  }
16186
+ async getByExecutionIds(params) {
16187
+ const executionIds = [...new Set(params.executionIds)];
16188
+ if (executionIds.length === 0) {
16189
+ return [];
16190
+ }
16191
+ const rows = await this.sql`
16192
+ SELECT *
16193
+ FROM action_runs
16194
+ WHERE project_id = ${params.projectId}
16195
+ AND execution_id = ANY(${this.sql.array(executionIds)}::text[])
16196
+ `;
16197
+ const byExecution = new Map(rows.map((row) => [row.execution_id, row]));
16198
+ return executionIds.flatMap((executionId) => {
16199
+ const row = byExecution.get(executionId);
16200
+ return row ? [rowToActionRunRecord(row)] : [];
16201
+ });
16202
+ }
15889
16203
  async list(input) {
15890
16204
  if (input.statuses && input.statuses.length === 0 || input.actionIds?.length === 0) {
15891
16205
  return {
@@ -16049,6 +16363,350 @@ function rowToActionSubject(row) {
16049
16363
  };
16050
16364
  }
16051
16365
 
16366
+ // src/pg-activity-storage.ts
16367
+ import {
16368
+ ACTIVITY_MAX_VISIT_MS,
16369
+ activityActor,
16370
+ activityCalendarDays,
16371
+ parseActivityAttribution,
16372
+ planActivityAppend,
16373
+ serializeActivityAttribution,
16374
+ validateActivityAppend
16375
+ } from "@sixb/core/internal/storage";
16376
+ var NO_CURSOR = { at: new Date("9999-12-31T23:59:59.999Z"), id: "" };
16377
+
16378
+ class PgActivityStorage {
16379
+ sql;
16380
+ constructor(sql) {
16381
+ this.sql = sql;
16382
+ }
16383
+ async append(input) {
16384
+ validateActivityAppend(input);
16385
+ return runPgTransaction(this.sql, async (sql) => {
16386
+ await lockAdvisoryKeys(sql, [
16387
+ JSON.stringify(["activity", input.projectId, input.actor.type, input.sessionId])
16388
+ ]);
16389
+ const plan = planActivityAppend(input, await readSource(sql, input), await readStoredEvents(sql, input));
16390
+ if (plan.status !== "append") {
16391
+ return { status: plan.status === "duplicate" ? "accepted" : "throttled", accepted: 0 };
16392
+ }
16393
+ const { state } = plan;
16394
+ const lastSeenAt = new Date(state.lastSeenAt);
16395
+ if (plan.newVisit) {
16396
+ const attribution = plan.newVisit.attribution === null ? null : serializeActivityAttribution(plan.newVisit.attribution);
16397
+ await sql`
16398
+ INSERT INTO activity_visits (
16399
+ project_id, id, actor_type, user_id, grant_id, session_id,
16400
+ started_at, last_seen_at, page_views, interactions, attribution
16401
+ ) VALUES (
16402
+ ${input.projectId}, ${state.visitId}, ${input.actor.type},
16403
+ ${input.actor.type === "user" ? input.actor.userId : null},
16404
+ ${input.actor.type === "share" ? input.actor.grantId : null},
16405
+ ${input.sessionId}, ${lastSeenAt}, ${lastSeenAt}, ${plan.pageViews.length},
16406
+ ${plan.interactions.length}, ${attribution}::text::jsonb
16407
+ )
16408
+ `;
16409
+ } else {
16410
+ await sql`
16411
+ UPDATE activity_visits
16412
+ SET
16413
+ last_seen_at = ${lastSeenAt},
16414
+ page_views = page_views + ${plan.pageViews.length},
16415
+ interactions = interactions + ${plan.interactions.length}
16416
+ WHERE project_id = ${input.projectId} AND id = ${state.visitId}
16417
+ `;
16418
+ }
16419
+ for (const pageView of plan.pageViews) {
16420
+ await sql`
16421
+ INSERT INTO activity_page_views (
16422
+ project_id, visit_id, sequence, id, route, received_at, attribution
16423
+ ) VALUES (
16424
+ ${input.projectId}, ${pageView.visitId}, ${pageView.sequence}, ${pageView.id},
16425
+ ${pageView.route}, ${pageView.receivedAt},
16426
+ ${serializeActivityAttribution(pageView.attribution)}::text::jsonb
16427
+ )
16428
+ `;
16429
+ }
16430
+ for (const interaction of plan.interactions) {
16431
+ await sql`
16432
+ INSERT INTO activity_interactions (
16433
+ project_id, visit_id, sequence, id, name, route, received_at
16434
+ ) VALUES (
16435
+ ${input.projectId}, ${interaction.visitId}, ${interaction.sequence},
16436
+ ${interaction.id}, ${interaction.name}, ${interaction.route}, ${interaction.receivedAt}
16437
+ )
16438
+ `;
16439
+ }
16440
+ await sql`
16441
+ INSERT INTO activity_sources (
16442
+ project_id, actor_type, session_id, visit_id, last_seen_at, sequence,
16443
+ window_start, window_count
16444
+ ) VALUES (
16445
+ ${input.projectId}, ${input.actor.type}, ${input.sessionId}, ${state.visitId},
16446
+ ${lastSeenAt}, ${state.sequence}, ${new Date(state.windowStart)}, ${state.windowCount}
16447
+ )
16448
+ ON CONFLICT (project_id, actor_type, session_id) DO UPDATE SET
16449
+ visit_id = excluded.visit_id,
16450
+ last_seen_at = excluded.last_seen_at,
16451
+ sequence = excluded.sequence,
16452
+ window_start = excluded.window_start,
16453
+ window_count = excluded.window_count
16454
+ `;
16455
+ return { status: "accepted", accepted: plan.accepted };
16456
+ });
16457
+ }
16458
+ async list(input) {
16459
+ const before = input.before ?? NO_CURSOR;
16460
+ const rows = await this.sql`
16461
+ SELECT ${this.visitColumns()}
16462
+ FROM activity_visits v
16463
+ WHERE ${this.overlapping(input)}
16464
+ AND (
16465
+ v.last_seen_at < ${before.at}
16466
+ OR (v.last_seen_at = ${before.at} AND v.id < ${before.id})
16467
+ )
16468
+ ORDER BY v.last_seen_at DESC, v.id DESC
16469
+ LIMIT ${input.limit + 1}
16470
+ `;
16471
+ return {
16472
+ visits: rows.slice(0, input.limit).map(visitFromRow),
16473
+ hasMore: rows.length > input.limit
16474
+ };
16475
+ }
16476
+ async users(input) {
16477
+ const before = input.before ?? NO_CURSOR;
16478
+ const rows = await this.sql`
16479
+ SELECT
16480
+ v.user_id,
16481
+ MAX(v.last_seen_at) AS last_seen_at,
16482
+ COUNT(DISTINCT v.id) AS visits,
16483
+ COUNT(p.id) AS page_views
16484
+ FROM activity_visits v
16485
+ LEFT JOIN activity_page_views p
16486
+ ON p.project_id = v.project_id
16487
+ AND p.visit_id = v.id
16488
+ AND p.received_at >= ${input.from}
16489
+ AND p.received_at < ${input.to}
16490
+ WHERE ${this.overlapping(input)} AND v.actor_type = 'user'
16491
+ GROUP BY v.user_id
16492
+ HAVING MAX(v.last_seen_at) < ${before.at}
16493
+ OR (MAX(v.last_seen_at) = ${before.at} AND v.user_id < ${before.id})
16494
+ ORDER BY last_seen_at DESC, v.user_id DESC
16495
+ LIMIT ${input.limit + 1}
16496
+ `;
16497
+ return {
16498
+ users: rows.slice(0, input.limit).map((row) => ({
16499
+ userId: row.user_id,
16500
+ lastSeenAt: new Date(row.last_seen_at),
16501
+ visits: Number(row.visits),
16502
+ pageViews: Number(row.page_views)
16503
+ })),
16504
+ hasMore: rows.length > input.limit
16505
+ };
16506
+ }
16507
+ async summarize(input) {
16508
+ const [row] = await this.sql`
16509
+ SELECT
16510
+ COUNT(DISTINCT v.user_id) AS users,
16511
+ COUNT(DISTINCT v.id) AS visits,
16512
+ COUNT(DISTINCT v.id) FILTER (WHERE v.actor_type = 'share') AS shared_visits,
16513
+ COUNT(p.id) AS page_views
16514
+ FROM activity_visits v
16515
+ LEFT JOIN activity_page_views p
16516
+ ON p.project_id = v.project_id
16517
+ AND p.visit_id = v.id
16518
+ AND p.received_at >= ${input.from}
16519
+ AND p.received_at < ${input.to}
16520
+ WHERE ${this.overlapping(input)}
16521
+ `;
16522
+ return {
16523
+ users: Number(row?.users ?? 0),
16524
+ visits: Number(row?.visits ?? 0),
16525
+ sharedVisits: Number(row?.shared_visits ?? 0),
16526
+ pageViews: Number(row?.page_views ?? 0)
16527
+ };
16528
+ }
16529
+ async calendar(input) {
16530
+ const visits = await this.sql`
16531
+ SELECT v.started_at, v.last_seen_at
16532
+ FROM activity_visits v
16533
+ WHERE ${this.overlapping(input)}
16534
+ `;
16535
+ const days = await this.sql`
16536
+ SELECT
16537
+ to_char(p.received_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day,
16538
+ COUNT(*) AS page_views
16539
+ FROM activity_visits v
16540
+ JOIN activity_page_views p ON p.project_id = v.project_id AND p.visit_id = v.id
16541
+ WHERE ${this.overlapping(input)}
16542
+ AND p.received_at >= ${input.from}
16543
+ AND p.received_at < ${input.to}
16544
+ GROUP BY day
16545
+ `;
16546
+ return activityCalendarDays(input, visits.map((visit) => ({
16547
+ startedAt: new Date(visit.started_at),
16548
+ lastSeenAt: new Date(visit.last_seen_at)
16549
+ })), new Map(days.map((row) => [row.day, Number(row.page_views)])));
16550
+ }
16551
+ async read(input) {
16552
+ const [visit] = await this.sql`
16553
+ SELECT ${this.visitColumns()}, v.session_id
16554
+ FROM activity_visits v
16555
+ WHERE v.project_id = ${input.projectId} AND v.id = ${input.id}
16556
+ `;
16557
+ if (!visit) {
16558
+ return null;
16559
+ }
16560
+ const rows = await this.sql`
16561
+ SELECT id, visit_id, sequence, route, NULL::text AS name, received_at
16562
+ FROM activity_page_views
16563
+ WHERE project_id = ${input.projectId}
16564
+ AND visit_id = ${input.id}
16565
+ UNION ALL
16566
+ SELECT id, visit_id, sequence, route, name, received_at
16567
+ FROM activity_interactions
16568
+ WHERE project_id = ${input.projectId}
16569
+ AND visit_id = ${input.id}
16570
+ ORDER BY sequence
16571
+ LIMIT ${input.limit + 1}
16572
+ `;
16573
+ const pageViews = [];
16574
+ const interactions = [];
16575
+ for (const row of rows.slice(0, input.limit)) {
16576
+ if (row.name === null) {
16577
+ pageViews.push(pageViewFromRow(row));
16578
+ } else {
16579
+ interactions.push({ ...pageViewFromRow(row), name: row.name });
16580
+ }
16581
+ }
16582
+ return {
16583
+ visit: visitFromRow(visit),
16584
+ sessionId: visit.session_id,
16585
+ pageViews,
16586
+ interactions,
16587
+ hasMore: rows.length > input.limit
16588
+ };
16589
+ }
16590
+ async sessionVisits(input) {
16591
+ if (input.sessionIds.length === 0) {
16592
+ return [];
16593
+ }
16594
+ const rows = await this.sql`
16595
+ SELECT *
16596
+ FROM (
16597
+ SELECT
16598
+ ${this.visitColumns()},
16599
+ v.session_id,
16600
+ ROW_NUMBER() OVER (
16601
+ PARTITION BY v.session_id ORDER BY v.started_at DESC, v.id DESC
16602
+ ) AS position,
16603
+ COUNT(*) OVER (PARTITION BY v.session_id) AS total
16604
+ FROM activity_visits v
16605
+ WHERE v.project_id = ${input.projectId}
16606
+ AND v.actor_type = 'user'
16607
+ AND v.session_id = ANY(${this.sql.array([...input.sessionIds])}::text[])
16608
+ AND v.user_id = ${input.userId}
16609
+ ) ranked
16610
+ WHERE position <= ${input.limit}
16611
+ ORDER BY session_id, position
16612
+ `;
16613
+ const bySession = new Map;
16614
+ for (const row of rows) {
16615
+ const session = bySession.get(row.session_id) ?? { visits: Number(row.total), latest: [] };
16616
+ session.latest.push(visitFromRow(row));
16617
+ bySession.set(row.session_id, session);
16618
+ }
16619
+ return [...bySession].map(([sessionId, session]) => ({ sessionId, ...session }));
16620
+ }
16621
+ overlapping(input) {
16622
+ const user = input.userId === undefined ? this.sql`` : this.sql`AND v.user_id = ${input.userId}`;
16623
+ const earliestStart = new Date(input.from.getTime() - ACTIVITY_MAX_VISIT_MS);
16624
+ return this.sql`
16625
+ v.project_id = ${input.projectId}
16626
+ AND v.started_at >= ${earliestStart}
16627
+ AND v.started_at < ${input.to}
16628
+ AND v.last_seen_at >= ${input.from}
16629
+ ${user}
16630
+ `;
16631
+ }
16632
+ visitColumns() {
16633
+ return this.sql`
16634
+ v.id, v.actor_type, v.user_id, v.grant_id, v.started_at, v.last_seen_at, v.page_views,
16635
+ v.interactions, v.attribution::text AS attribution
16636
+ `;
16637
+ }
16638
+ }
16639
+ async function readSource(sql, input) {
16640
+ const [row] = await sql`
16641
+ SELECT
16642
+ s.visit_id, v.started_at AS visit_started_at, s.last_seen_at, s.sequence,
16643
+ s.window_start, s.window_count
16644
+ FROM activity_sources s
16645
+ LEFT JOIN activity_visits v ON v.project_id = s.project_id AND v.id = s.visit_id
16646
+ WHERE s.project_id = ${input.projectId}
16647
+ AND s.actor_type = ${input.actor.type}
16648
+ AND s.session_id = ${input.sessionId}
16649
+ `;
16650
+ if (!row) {
16651
+ return;
16652
+ }
16653
+ return {
16654
+ visitId: row.visit_id,
16655
+ visitStartedAt: row.visit_started_at === null ? null : new Date(row.visit_started_at).getTime(),
16656
+ lastSeenAt: new Date(row.last_seen_at).getTime(),
16657
+ sequence: Number(row.sequence),
16658
+ windowStart: new Date(row.window_start).getTime(),
16659
+ windowCount: Number(row.window_count)
16660
+ };
16661
+ }
16662
+ async function readStoredEvents(sql, input) {
16663
+ const ids = input.events.map((event) => event.id);
16664
+ const rows = await sql`
16665
+ SELECT p.id, p.route, NULL::text AS name, p.attribution::text AS attribution
16666
+ FROM activity_page_views p
16667
+ JOIN activity_visits v ON v.project_id = p.project_id AND v.id = p.visit_id
16668
+ WHERE p.project_id = ${input.projectId}
16669
+ AND p.id = ANY(${sql.array(ids)}::text[])
16670
+ AND v.actor_type = ${input.actor.type}
16671
+ AND v.session_id = ${input.sessionId}
16672
+ UNION ALL
16673
+ SELECT i.id, i.route, i.name, NULL::text AS attribution
16674
+ FROM activity_interactions i
16675
+ JOIN activity_visits v ON v.project_id = i.project_id AND v.id = i.visit_id
16676
+ WHERE i.project_id = ${input.projectId}
16677
+ AND i.id = ANY(${sql.array(ids)}::text[])
16678
+ AND v.actor_type = ${input.actor.type}
16679
+ AND v.session_id = ${input.sessionId}
16680
+ `;
16681
+ return rows.map((row) => {
16682
+ if (row.name !== null) {
16683
+ return { id: row.id, route: row.route, name: row.name };
16684
+ }
16685
+ const attribution = parseActivityAttribution(row.attribution);
16686
+ return { id: row.id, route: row.route, ...attribution ? { attribution } : {} };
16687
+ });
16688
+ }
16689
+ function visitFromRow(row) {
16690
+ return {
16691
+ id: row.id,
16692
+ actor: activityActor(row.actor_type, row.user_id, row.grant_id),
16693
+ attribution: parseActivityAttribution(row.attribution),
16694
+ startedAt: new Date(row.started_at),
16695
+ lastSeenAt: new Date(row.last_seen_at),
16696
+ pageViews: Number(row.page_views),
16697
+ interactions: Number(row.interactions)
16698
+ };
16699
+ }
16700
+ function pageViewFromRow(row) {
16701
+ return {
16702
+ id: row.id,
16703
+ visitId: row.visit_id,
16704
+ sequence: Number(row.sequence),
16705
+ route: row.route,
16706
+ receivedAt: new Date(row.received_at)
16707
+ };
16708
+ }
16709
+
16052
16710
  // src/pg-ai-cost-storage.ts
16053
16711
  import {
16054
16712
  aiModelCallCostDetails,
@@ -17532,6 +18190,260 @@ function invalidState(reservation, operation) {
17532
18190
  return new AiLimitStorageError("invalid_reservation_state", `[SixbPg] Cannot ${operation} AI model-call reservation '${reservation.callId}' in state '${reservation.state}'.`);
17533
18191
  }
17534
18192
 
18193
+ // src/pg-artifact-storage.ts
18194
+ import {
18195
+ artifactStorageErrors,
18196
+ parseArtifactFrameGrantRow,
18197
+ parseArtifactRow,
18198
+ parseArtifactVersionRow
18199
+ } from "@sixb/core/internal/storage";
18200
+ var ARTIFACT_COLUMNS = `
18201
+ project_id AS "projectId", id, kind, title,
18202
+ owner_principal_type AS "ownerPrincipalType", owner_principal_id AS "ownerPrincipalId",
18203
+ latest_version AS "latestVersion", created_at AS "createdAt", updated_at AS "updatedAt"
18204
+ `;
18205
+ var VERSION_COLUMNS = `
18206
+ project_id AS "projectId", artifact_id AS "artifactId", version, title, entry, files, outline,
18207
+ refs, author_principal_type AS "authorPrincipalType", author_principal_id AS "authorPrincipalId",
18208
+ origin_thread_id AS "originThreadId", origin_run_id AS "originRunId", created_at AS "createdAt"
18209
+ `;
18210
+ var FRAME_GRANT_COLUMNS = `
18211
+ project_id AS "projectId", token_hash AS "tokenHash", artifact_id AS "artifactId", version,
18212
+ targets, created_at AS "createdAt", expires_at AS "expiresAt"
18213
+ `;
18214
+
18215
+ class PgArtifactStorage {
18216
+ sql;
18217
+ constructor(sql) {
18218
+ this.sql = sql;
18219
+ }
18220
+ async create(input) {
18221
+ return runPgTransaction(this.sql, async (tx) => {
18222
+ const [artifact] = await tx`
18223
+ INSERT INTO artifacts (
18224
+ project_id, id, kind, title, owner_principal_type, owner_principal_id,
18225
+ latest_version, next_version, created_at, updated_at
18226
+ ) VALUES (
18227
+ ${input.projectId}, ${input.id}, ${input.kind}, ${input.version.title},
18228
+ ${input.owner.type}, ${input.owner.id}, 1, 2,
18229
+ ${input.version.createdAt}, ${input.version.createdAt}
18230
+ )
18231
+ ON CONFLICT (project_id, id) DO NOTHING
18232
+ RETURNING ${tx.unsafe(ARTIFACT_COLUMNS)}
18233
+ `;
18234
+ if (!artifact)
18235
+ throw artifactStorageErrors.exists(input.id);
18236
+ return {
18237
+ artifact: parseArtifactRow(artifact),
18238
+ version: await insertVersion(tx, input.projectId, input.id, 1, input.version)
18239
+ };
18240
+ });
18241
+ }
18242
+ async publish(input) {
18243
+ return runPgTransaction(this.sql, async (tx) => {
18244
+ const [current] = await tx`
18245
+ SELECT kind, next_version FROM artifacts
18246
+ WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
18247
+ FOR UPDATE
18248
+ `;
18249
+ if (!current)
18250
+ throw artifactStorageErrors.notFound(input.artifactId);
18251
+ const kind = current.kind;
18252
+ if (kind !== input.kind) {
18253
+ throw artifactStorageErrors.kindMismatch(input.artifactId, kind, input.kind);
18254
+ }
18255
+ const version = await insertVersion(tx, input.projectId, input.artifactId, Number(current.next_version), input.version);
18256
+ const [artifact] = await tx`
18257
+ UPDATE artifacts
18258
+ SET title = ${input.version.title}, latest_version = ${version.version},
18259
+ next_version = ${version.version + 1}, updated_at = ${input.version.createdAt}
18260
+ WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
18261
+ RETURNING ${tx.unsafe(ARTIFACT_COLUMNS)}
18262
+ `;
18263
+ return { artifact: parseArtifactRow(artifact), version };
18264
+ });
18265
+ }
18266
+ async get(input) {
18267
+ const [row] = await this.sql`
18268
+ SELECT ${this.sql.unsafe(ARTIFACT_COLUMNS)} FROM artifacts
18269
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
18270
+ `;
18271
+ return row ? parseArtifactRow(row) : null;
18272
+ }
18273
+ async list(input) {
18274
+ const where = ["a.project_id = $1"];
18275
+ const params = [input.projectId];
18276
+ if (input.owner) {
18277
+ where.push(`a.owner_principal_type = $${params.length + 1}`);
18278
+ params.push(input.owner.type);
18279
+ where.push(`a.owner_principal_id = $${params.length + 1}`);
18280
+ params.push(input.owner.id);
18281
+ }
18282
+ if (input.originThreadId !== undefined) {
18283
+ where.push(`EXISTS (
18284
+ SELECT 1 FROM artifact_versions v
18285
+ WHERE v.project_id = a.project_id AND v.artifact_id = a.id
18286
+ AND v.origin_thread_id = $${params.length + 1}
18287
+ )`);
18288
+ params.push(input.originThreadId);
18289
+ }
18290
+ if (input.kind !== undefined) {
18291
+ where.push(`a.kind = $${params.length + 1}`);
18292
+ params.push(input.kind);
18293
+ }
18294
+ const clause = where.join(" AND ");
18295
+ const [counted] = await this.sql.unsafe(`SELECT COUNT(*)::int AS total FROM artifacts a WHERE ${clause}`, params);
18296
+ const offset = input.offset ?? 0;
18297
+ const rows = await this.sql.unsafe(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
18298
+ ORDER BY a.updated_at DESC, a.id ASC
18299
+ LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, input.limit ?? null, offset]);
18300
+ const total = counted?.total ?? 0;
18301
+ return { artifacts: rows.map(parseArtifactRow), hasMore: offset + rows.length < total, total };
18302
+ }
18303
+ async getVersion(input) {
18304
+ const [row] = await this.sql`
18305
+ SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions
18306
+ WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
18307
+ AND version = ${input.version}
18308
+ `;
18309
+ return row ? parseArtifactVersionRow(row) : null;
18310
+ }
18311
+ async listVersions(input) {
18312
+ const [counted] = await this.sql`
18313
+ SELECT COUNT(*)::int AS total FROM artifact_versions
18314
+ WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
18315
+ `;
18316
+ const offset = input.offset ?? 0;
18317
+ const rows = await this.sql`
18318
+ SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions
18319
+ WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
18320
+ ORDER BY version DESC
18321
+ LIMIT ${input.limit ?? null} OFFSET ${offset}
18322
+ `;
18323
+ const total = counted?.total ?? 0;
18324
+ return {
18325
+ versions: rows.map(parseArtifactVersionRow),
18326
+ hasMore: offset + rows.length < total,
18327
+ total
18328
+ };
18329
+ }
18330
+ async listReferencingVersions(input) {
18331
+ const prefix = `artifact:${input.artifactId}`;
18332
+ const rows = await this.sql`
18333
+ SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions v
18334
+ WHERE project_id = ${input.projectId} AND (artifact_id, version) IN (
18335
+ SELECT id, latest_version FROM artifacts WHERE project_id = ${input.projectId}
18336
+ ) AND EXISTS (
18337
+ SELECT 1 FROM jsonb_array_elements(v.refs) r
18338
+ WHERE left(r ->> 'address', ${prefix.length}::int) = ${prefix}
18339
+ AND substr(r ->> 'address', ${prefix.length + 1}::int, 1) IN ('', '@', '/', '#')
18340
+ )
18341
+ ORDER BY artifact_id
18342
+ `;
18343
+ return rows.map(parseArtifactVersionRow);
18344
+ }
18345
+ async createFrameGrant(record) {
18346
+ const rows = await this.sql`
18347
+ INSERT INTO artifact_frame_grants
18348
+ (project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
18349
+ SELECT ${record.projectId}, ${record.tokenHash}, ${record.artifactId}, ${record.version},
18350
+ ${JSON.stringify(record.targets)}::text::jsonb, ${record.createdAt},
18351
+ ${record.expiresAt}
18352
+ WHERE EXISTS (
18353
+ SELECT 1 FROM artifacts WHERE project_id = ${record.projectId} AND id = ${record.artifactId}
18354
+ )
18355
+ RETURNING token_hash
18356
+ `;
18357
+ if (rows.length === 0)
18358
+ throw artifactStorageErrors.notFound(record.artifactId);
18359
+ }
18360
+ async findFrameGrant(input) {
18361
+ const [row] = await this.sql`
18362
+ SELECT ${this.sql.unsafe(FRAME_GRANT_COLUMNS)} FROM artifact_frame_grants
18363
+ WHERE project_id = ${input.projectId} AND token_hash = ${input.tokenHash}
18364
+ `;
18365
+ return row ? parseArtifactFrameGrantRow(row) : null;
18366
+ }
18367
+ async deleteExpiredFrameGrants(input) {
18368
+ const rows = await this.sql`
18369
+ DELETE FROM artifact_frame_grants
18370
+ WHERE (project_id, token_hash) IN (
18371
+ SELECT project_id, token_hash FROM artifact_frame_grants
18372
+ WHERE project_id = ${input.projectId} AND expires_at < ${input.expiredBefore}
18373
+ ORDER BY expires_at LIMIT ${input.limit}
18374
+ )
18375
+ RETURNING token_hash
18376
+ `;
18377
+ return rows.length;
18378
+ }
18379
+ async delete(input) {
18380
+ const rows = await this.sql`
18381
+ DELETE FROM artifacts WHERE project_id = ${input.projectId} AND id = ${input.id}
18382
+ RETURNING id
18383
+ `;
18384
+ return rows.length > 0;
18385
+ }
18386
+ async deleteVersionsByOrigin(input) {
18387
+ return runPgTransaction(this.sql, async (tx) => {
18388
+ const locked = await tx`
18389
+ SELECT id FROM artifacts
18390
+ WHERE project_id = ${input.projectId} AND id IN (
18391
+ SELECT artifact_id FROM artifact_versions
18392
+ WHERE project_id = ${input.projectId} AND origin_run_id = ${input.runId}
18393
+ )
18394
+ ORDER BY id
18395
+ FOR UPDATE
18396
+ `;
18397
+ const affected = locked.map((row) => row.id);
18398
+ if (affected.length === 0)
18399
+ return 0;
18400
+ const deleted = await tx`
18401
+ DELETE FROM artifact_versions
18402
+ WHERE project_id = ${input.projectId} AND origin_run_id = ${input.runId}
18403
+ AND artifact_id IN ${tx(affected)}
18404
+ RETURNING version
18405
+ `;
18406
+ await tx`
18407
+ DELETE FROM artifacts a
18408
+ WHERE a.project_id = ${input.projectId} AND a.id IN ${tx(affected)}
18409
+ AND NOT EXISTS (
18410
+ SELECT 1 FROM artifact_versions v
18411
+ WHERE v.project_id = a.project_id AND v.artifact_id = a.id
18412
+ )
18413
+ `;
18414
+ await tx`
18415
+ UPDATE artifacts a
18416
+ SET latest_version = latest.version, title = latest.title, updated_at = latest.created_at
18417
+ FROM (
18418
+ SELECT DISTINCT ON (artifact_id) artifact_id, version, title, created_at
18419
+ FROM artifact_versions
18420
+ WHERE project_id = ${input.projectId} AND artifact_id IN ${tx(affected)}
18421
+ ORDER BY artifact_id, version DESC
18422
+ ) latest
18423
+ WHERE a.project_id = ${input.projectId} AND a.id = latest.artifact_id
18424
+ `;
18425
+ return deleted.length;
18426
+ });
18427
+ }
18428
+ }
18429
+ async function insertVersion(sql, projectId, artifactId, version, input) {
18430
+ const [row] = await sql`
18431
+ INSERT INTO artifact_versions (
18432
+ project_id, artifact_id, version, title, entry, files, outline, refs,
18433
+ author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
18434
+ ) VALUES (
18435
+ ${projectId}, ${artifactId}, ${version}, ${input.title}, ${input.entry},
18436
+ ${JSON.stringify(input.files)}::text::jsonb,
18437
+ ${JSON.stringify(input.outline)}::text::jsonb,
18438
+ ${JSON.stringify(input.references)}::text::jsonb,
18439
+ ${input.author.type}, ${input.author.id},
18440
+ ${input.origin?.threadId ?? null}, ${input.origin?.runId ?? null}, ${input.createdAt}
18441
+ )
18442
+ RETURNING ${sql.unsafe(VERSION_COLUMNS)}
18443
+ `;
18444
+ return parseArtifactVersionRow(row);
18445
+ }
18446
+
17535
18447
  // src/pg-client.ts
17536
18448
  import { isIP } from "net";
17537
18449
  import { connect as connectTls } from "tls";
@@ -19956,6 +20868,23 @@ class PgWorkflowRunStorage {
19956
20868
  `;
19957
20869
  return row ? rowToWorkflowRunRecord(row) : null;
19958
20870
  }
20871
+ async getByExecutionIds(params) {
20872
+ const executionIds = [...new Set(params.executionIds)];
20873
+ if (executionIds.length === 0) {
20874
+ return [];
20875
+ }
20876
+ const rows = await this.sql`
20877
+ SELECT *
20878
+ FROM workflow_runs
20879
+ WHERE project_id = ${params.projectId}
20880
+ AND execution_id = ANY(${this.sql.array(executionIds)}::text[])
20881
+ `;
20882
+ const byExecution = new Map(rows.map((row) => [row.execution_id, row]));
20883
+ return executionIds.flatMap((executionId) => {
20884
+ const row = byExecution.get(executionId);
20885
+ return row ? [rowToWorkflowRunRecord(row)] : [];
20886
+ });
20887
+ }
19959
20888
  async list(input) {
19960
20889
  if (hasEmptyStatuses(input) || input.workflowIds?.length === 0) {
19961
20890
  return {
@@ -20559,6 +21488,23 @@ class PgShareGrantStorage {
20559
21488
  `;
20560
21489
  return row ? recordFromRow(row) : null;
20561
21490
  }
21491
+ async getByIds(input) {
21492
+ const ids = [...new Set(input.ids)];
21493
+ if (ids.length === 0) {
21494
+ return [];
21495
+ }
21496
+ const rows = await this.sql`
21497
+ SELECT *
21498
+ FROM share_grants
21499
+ WHERE project_id = ${input.projectId}
21500
+ AND id = ANY(${this.sql.array(ids)}::text[])
21501
+ `;
21502
+ const byId = new Map(rows.map((row) => [row.id, row]));
21503
+ return ids.flatMap((id) => {
21504
+ const row = byId.get(id);
21505
+ return row ? [recordFromRow(row)] : [];
21506
+ });
21507
+ }
20562
21508
  async list(input) {
20563
21509
  const normalized = normalizeListShareGrantsInput(input);
20564
21510
  const where = ["project_id = $1"];
@@ -20778,6 +21724,7 @@ class PostgresStorage {
20778
21724
  auth;
20779
21725
  executions;
20780
21726
  agents;
21727
+ artifacts;
20781
21728
  aiUsage;
20782
21729
  aiCosts;
20783
21730
  aiLimits;
@@ -20795,6 +21742,7 @@ class PostgresStorage {
20795
21742
  shareSessions;
20796
21743
  fileUploadSessions;
20797
21744
  fileDownloadGrants;
21745
+ activity;
20798
21746
  migrators;
20799
21747
  sql;
20800
21748
  schemaName;
@@ -20836,6 +21784,7 @@ class PostgresStorage {
20836
21784
  this.auth = createAuthOperationScope(stores.auth, scope);
20837
21785
  this.executions = createOperationScopedFacade(stores.executions, scope);
20838
21786
  this.agents = createAgentOperationScope(stores.agents, scope);
21787
+ this.artifacts = createOperationScopedFacade(stores.artifacts, scope);
20839
21788
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope);
20840
21789
  this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope);
20841
21790
  this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope);
@@ -20853,6 +21802,7 @@ class PostgresStorage {
20853
21802
  this.shareSessions = createOperationScopedFacade(stores.shareSessions, scope);
20854
21803
  this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope);
20855
21804
  this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope);
21805
+ this.activity = createOperationScopedFacade(stores.activity, scope);
20856
21806
  registerPostgresStorageTestingAdapter(this, (durationMs) => stores.connectorConnections.advanceTimeForTesting(durationMs));
20857
21807
  }
20858
21808
  async ping() {
@@ -20945,6 +21895,7 @@ function createPostgresStores(sql, options) {
20945
21895
  auth,
20946
21896
  executions,
20947
21897
  agents: new PgAgentStorage({ sql, executions }),
21898
+ artifacts: new PgArtifactStorage(sql),
20948
21899
  aiUsage: new PgAiUsageStorage(sql),
20949
21900
  aiCosts: new PgAiCostStorage(sql),
20950
21901
  aiLimits: new PgAiLimitStorage(sql),
@@ -20961,7 +21912,8 @@ function createPostgresStores(sql, options) {
20961
21912
  shareGrants,
20962
21913
  shareSessions,
20963
21914
  fileUploadSessions: new PgFileUploadSessionStorage(sql),
20964
- fileDownloadGrants: new PgFileDownloadGrantStorage(sql)
21915
+ fileDownloadGrants: new PgFileDownloadGrantStorage(sql),
21916
+ activity: new PgActivityStorage(sql)
20965
21917
  };
20966
21918
  }
20967
21919
  function resolveTimeoutMillis(value, label) {
@@ -20978,4 +21930,4 @@ export {
20978
21930
  migratePostgresStorage
20979
21931
  };
20980
21932
 
20981
- //# debugId=971E14D4D5198C8B64756E2164756E21
21933
+ //# debugId=E2B2D974990D4A3064756E2164756E21