@sixb/pg 0.1.19 → 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 (56) 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/auth-storage.d.ts.map +1 -1
  4. package/dist/auth-storage/magic-links.d.ts.map +1 -1
  5. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
  6. package/dist/auth-storage/rows.d.ts +0 -3
  7. package/dist/auth-storage/rows.d.ts.map +1 -1
  8. package/dist/auth-storage/sessions.d.ts +2 -11
  9. package/dist/auth-storage/sessions.d.ts.map +1 -1
  10. package/dist/auth-storage/shared.d.ts +1 -2
  11. package/dist/auth-storage/shared.d.ts.map +1 -1
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +984 -80
  15. package/dist/index.js.map +20 -18
  16. package/dist/migrations.d.ts.map +1 -1
  17. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
  18. package/dist/ontology-storage/sources.d.ts +2 -1
  19. package/dist/ontology-storage/sources.d.ts.map +1 -1
  20. package/dist/pg-action-run-storage.d.ts +4 -0
  21. package/dist/pg-action-run-storage.d.ts.map +1 -1
  22. package/dist/pg-activity-storage.d.ts +22 -0
  23. package/dist/pg-activity-storage.d.ts.map +1 -0
  24. package/dist/pg-artifact-storage.d.ts +19 -0
  25. package/dist/pg-artifact-storage.d.ts.map +1 -0
  26. package/dist/pg-execution-storage.d.ts +2 -1
  27. package/dist/pg-execution-storage.d.ts.map +1 -1
  28. package/dist/pg-workflow-run-storage.d.ts +4 -0
  29. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  30. package/dist/share-grant-storage.d.ts +2 -1
  31. package/dist/share-grant-storage.d.ts.map +1 -1
  32. package/package.json +3 -3
  33. package/src/agents/threads.ts +23 -0
  34. package/src/auth-storage/auth-storage.ts +1 -22
  35. package/src/auth-storage/magic-links.ts +0 -4
  36. package/src/auth-storage/oidc-attempts.ts +0 -4
  37. package/src/auth-storage/rows.ts +0 -6
  38. package/src/auth-storage/sessions.ts +18 -30
  39. package/src/auth-storage/shared.ts +3 -35
  40. package/src/index.ts +10 -0
  41. package/src/migrations/062-drop-auth-audience.sql +19 -0
  42. package/src/migrations/063-artifacts.sql +67 -0
  43. package/src/migrations/064-drop-output-diagnostics.sql +14 -0
  44. package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
  45. package/src/migrations/066-activity.sql +61 -0
  46. package/src/migrations/067-activity-timeline.sql +15 -0
  47. package/src/migrations/068-activity-interactions.sql +20 -0
  48. package/src/migrations.ts +20 -0
  49. package/src/ontology-storage/replacement-plans.ts +25 -7
  50. package/src/ontology-storage/sources.ts +37 -0
  51. package/src/pg-action-run-storage.ts +23 -0
  52. package/src/pg-activity-storage.ts +486 -0
  53. package/src/pg-artifact-storage.ts +325 -0
  54. package/src/pg-execution-storage.ts +52 -0
  55. package/src/pg-workflow-run-storage.ts +23 -0
  56. 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 };
@@ -1244,7 +1261,6 @@ function rowToSessionRecord(row) {
1244
1261
  projectId: row.project_id,
1245
1262
  userId: row.user_id,
1246
1263
  strategyId: row.strategy_id,
1247
- audience: row.audience,
1248
1264
  tokenHash: row.token_hash,
1249
1265
  createdAt: toDate(row.created_at),
1250
1266
  expiresAt: toDate(row.expires_at),
@@ -1311,7 +1327,6 @@ function rowToMagicLinkRecord(row) {
1311
1327
  id: row.id,
1312
1328
  projectId: row.project_id,
1313
1329
  strategyId: row.strategy_id,
1314
- audience: row.audience,
1315
1330
  email: row.email,
1316
1331
  tokenHash: row.token_hash,
1317
1332
  returnTo: row.return_to ?? undefined,
@@ -1326,7 +1341,6 @@ function rowToOidcAuthorizationAttemptRecord(row) {
1326
1341
  id: row.id,
1327
1342
  projectId: row.project_id,
1328
1343
  strategyId: row.strategy_id,
1329
- audience: row.audience,
1330
1344
  stateHash: row.state_hash,
1331
1345
  nonceHash: row.nonce_hash,
1332
1346
  codeVerifier: row.code_verifier,
@@ -1622,7 +1636,6 @@ async function consumeOidcAttempt(sql, params) {
1622
1636
  }
1623
1637
  async function validateCompleteSessionInput(sql, projectId, session) {
1624
1638
  const sessionId = assertNonEmpty(session.id, "Session id");
1625
- assertNonEmpty(session.audience, "Session audience");
1626
1639
  assertNonEmpty(session.tokenHash, "Session token hash");
1627
1640
  await assertSessionIdAvailable(sql, projectId, sessionId);
1628
1641
  }
@@ -1636,7 +1649,6 @@ async function createSession(sql, input) {
1636
1649
  const projectId = assertNonEmpty(input.projectId, "Project id");
1637
1650
  const userId = assertNonEmpty(input.userId, "User id");
1638
1651
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
1639
- const audience = assertNonEmpty(input.audience, "Session audience");
1640
1652
  const tokenHash = assertNonEmpty(input.tokenHash, "Session token hash");
1641
1653
  await requireUserById(sql, { projectId, id: userId });
1642
1654
  await assertSessionIdAvailable(sql, projectId, id);
@@ -1647,7 +1659,6 @@ async function createSession(sql, input) {
1647
1659
  id,
1648
1660
  user_id,
1649
1661
  strategy_id,
1650
- audience,
1651
1662
  token_hash,
1652
1663
  created_at,
1653
1664
  expires_at,
@@ -1662,7 +1673,6 @@ async function createSession(sql, input) {
1662
1673
  ${id},
1663
1674
  ${userId},
1664
1675
  ${strategyId},
1665
- ${audience},
1666
1676
  ${tokenHash},
1667
1677
  ${input.createdAt},
1668
1678
  ${input.expiresAt},
@@ -1681,50 +1691,26 @@ async function createSession(sql, input) {
1681
1691
  }
1682
1692
  }
1683
1693
  async function revokeActiveSessionsForUser(sql, params) {
1684
- const rows = params.audience === undefined ? await sql`
1685
- SELECT *
1686
- FROM auth_sessions
1687
- WHERE project_id = ${params.projectId}
1688
- AND user_id = ${params.userId}
1689
- AND revoked_at IS NULL
1690
- AND expires_at > ${params.revokedAt}
1691
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1692
- ORDER BY created_at ASC, id ASC
1693
- FOR UPDATE
1694
- ` : await sql`
1694
+ const rows = await sql`
1695
1695
  SELECT *
1696
1696
  FROM auth_sessions
1697
1697
  WHERE project_id = ${params.projectId}
1698
1698
  AND user_id = ${params.userId}
1699
- AND audience = ${params.audience}
1700
1699
  AND revoked_at IS NULL
1701
1700
  AND expires_at > ${params.revokedAt}
1702
1701
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1703
1702
  ORDER BY created_at ASC, id ASC
1704
1703
  FOR UPDATE
1705
1704
  `;
1706
- if (params.audience === undefined) {
1707
- await sql`
1708
- UPDATE auth_sessions
1709
- SET revoked_at = ${params.revokedAt}
1710
- WHERE project_id = ${params.projectId}
1711
- AND user_id = ${params.userId}
1712
- AND revoked_at IS NULL
1713
- AND expires_at > ${params.revokedAt}
1714
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1715
- `;
1716
- } else {
1717
- await sql`
1705
+ await sql`
1718
1706
  UPDATE auth_sessions
1719
1707
  SET revoked_at = ${params.revokedAt}
1720
1708
  WHERE project_id = ${params.projectId}
1721
1709
  AND user_id = ${params.userId}
1722
- AND audience = ${params.audience}
1723
1710
  AND revoked_at IS NULL
1724
1711
  AND expires_at > ${params.revokedAt}
1725
1712
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1726
1713
  `;
1727
- }
1728
1714
  return rows.map((row) => rowToSessionRecord({
1729
1715
  ...row,
1730
1716
  revoked_at: params.revokedAt
@@ -2384,7 +2370,6 @@ class PgAuthInvitationStore {
2384
2370
  }
2385
2371
 
2386
2372
  // src/auth-storage/magic-links.ts
2387
- import { resolveAuthSessionAudience } from "@sixb/core";
2388
2373
  import { AuthStorageError as AuthStorageError5 } from "@sixb/core/storage";
2389
2374
  class PgAuthMagicLinkStore {
2390
2375
  sql;
@@ -2396,7 +2381,6 @@ class PgAuthMagicLinkStore {
2396
2381
  const id = assertNonEmpty(input.id, "Magic link id");
2397
2382
  const projectId = assertNonEmpty(input.projectId, "Project id");
2398
2383
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
2399
- const audience = resolveAuthSessionAudience(input.audience);
2400
2384
  const email = normalizeEmail(input.email);
2401
2385
  const tokenHash = assertNonEmpty(input.tokenHash, "Magic link token hash");
2402
2386
  await lockAdvisoryKeys(tx, [authLockKey("magic-links", projectId, email)]);
@@ -2414,7 +2398,6 @@ class PgAuthMagicLinkStore {
2414
2398
  project_id,
2415
2399
  id,
2416
2400
  strategy_id,
2417
- audience,
2418
2401
  email,
2419
2402
  token_hash,
2420
2403
  return_to,
@@ -2424,7 +2407,6 @@ class PgAuthMagicLinkStore {
2424
2407
  ${projectId},
2425
2408
  ${id},
2426
2409
  ${strategyId},
2427
- ${audience},
2428
2410
  ${email},
2429
2411
  ${tokenHash},
2430
2412
  ${input.returnTo ?? null},
@@ -2472,7 +2454,6 @@ class PgAuthMagicLinkStore {
2472
2454
  }
2473
2455
 
2474
2456
  // src/auth-storage/oidc-attempts.ts
2475
- import { resolveAuthSessionAudience as resolveAuthSessionAudience2 } from "@sixb/core";
2476
2457
  import { AuthStorageError as AuthStorageError6 } from "@sixb/core/storage";
2477
2458
  class PgAuthOidcAuthorizationAttemptStore {
2478
2459
  sql;
@@ -2483,7 +2464,6 @@ class PgAuthOidcAuthorizationAttemptStore {
2483
2464
  const id = assertNonEmpty(input.id, "OIDC authorization attempt id");
2484
2465
  const projectId = assertNonEmpty(input.projectId, "Project id");
2485
2466
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
2486
- const audience = resolveAuthSessionAudience2(input.audience);
2487
2467
  const stateHash = assertNonEmpty(input.stateHash, "OIDC state hash");
2488
2468
  const nonceHash = assertNonEmpty(input.nonceHash, "OIDC nonce hash");
2489
2469
  const codeVerifier = assertNonEmpty(input.codeVerifier, "OIDC code verifier");
@@ -2496,7 +2476,6 @@ class PgAuthOidcAuthorizationAttemptStore {
2496
2476
  project_id,
2497
2477
  id,
2498
2478
  strategy_id,
2499
- audience,
2500
2479
  state_hash,
2501
2480
  nonce_hash,
2502
2481
  code_verifier,
@@ -2507,7 +2486,6 @@ class PgAuthOidcAuthorizationAttemptStore {
2507
2486
  ${projectId},
2508
2487
  ${id},
2509
2488
  ${strategyId},
2510
- ${audience},
2511
2489
  ${stateHash},
2512
2490
  ${nonceHash},
2513
2491
  ${codeVerifier},
@@ -2765,37 +2743,38 @@ class PgAuthSessionStore {
2765
2743
  async getById(params) {
2766
2744
  return getSessionById(this.sql, params);
2767
2745
  }
2768
- async getActiveByUserId(params) {
2769
- const [row] = await this.sql`
2746
+ async listActiveByUserId(params) {
2747
+ const rows = await this.sql`
2770
2748
  SELECT *
2771
2749
  FROM auth_sessions
2772
2750
  WHERE project_id = ${params.projectId}
2773
2751
  AND user_id = ${params.userId}
2774
- AND audience = ${params.audience}
2775
2752
  AND revoked_at IS NULL
2776
2753
  AND expires_at > ${params.now}
2777
2754
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.now})
2778
- ORDER BY created_at DESC, id DESC
2779
- LIMIT 1
2755
+ ORDER BY COALESCE(last_seen_at, created_at) DESC, created_at DESC, id DESC
2780
2756
  `;
2781
- return row ? rowToSessionRecord(row) : null;
2757
+ return rows.map(rowToSessionRecord);
2782
2758
  }
2783
- async listActiveByUserId(params) {
2759
+ async listByUserId(params) {
2784
2760
  const rows = await this.sql`
2785
2761
  SELECT *
2786
2762
  FROM auth_sessions
2787
2763
  WHERE project_id = ${params.projectId}
2788
2764
  AND user_id = ${params.userId}
2789
- AND revoked_at IS NULL
2790
- AND expires_at > ${params.now}
2791
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.now})
2792
- ORDER BY COALESCE(last_seen_at, created_at) DESC, created_at DESC, id DESC
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}
2793
2769
  `;
2794
- return rows.map(rowToSessionRecord);
2770
+ return {
2771
+ sessions: rows.slice(0, params.limit).map(rowToSessionRecord),
2772
+ hasMore: rows.length > params.limit
2773
+ };
2795
2774
  }
2796
2775
  async findValidByTokenHash(params) {
2797
2776
  const row = await getSessionRowById(this.sql, params);
2798
- if (!row || row.refresh_token_hash !== null || row.audience !== params.audience || row.token_hash !== params.tokenHash || !isLiveSessionRow(row, params.now)) {
2777
+ if (!row || row.refresh_token_hash !== null || row.token_hash !== params.tokenHash || !isLiveSessionRow(row, params.now)) {
2799
2778
  return null;
2800
2779
  }
2801
2780
  return rowToSessionRecord(row);
@@ -2855,7 +2834,6 @@ class PgAuthSessionStore {
2855
2834
  last_seen_at = ${params.now}
2856
2835
  WHERE project_id = ${params.projectId}
2857
2836
  AND id = ${params.id}
2858
- AND audience = ${params.audience}
2859
2837
  AND token_hash = ${params.tokenHash}
2860
2838
  AND refresh_token_hash IS NULL
2861
2839
  AND revoked_at IS NULL
@@ -2883,11 +2861,7 @@ class PgAuthSessionStore {
2883
2861
  }
2884
2862
  async revokeActiveForUser(params) {
2885
2863
  return runPgTransaction(this.sql, async (tx) => {
2886
- const locks = [authLockKey("sessions", params.projectId, params.userId)];
2887
- if (params.audience) {
2888
- locks.push(authLockKey("sessions", params.projectId, params.userId, params.audience));
2889
- }
2890
- await lockAdvisoryKeys(tx, locks);
2864
+ await lockAdvisoryKeys(tx, [authLockKey("sessions", params.projectId, params.userId)]);
2891
2865
  return revokeActiveSessionsForUser(tx, params);
2892
2866
  });
2893
2867
  }
@@ -3146,7 +3120,6 @@ class PgAuthStorage {
3146
3120
  };
3147
3121
  }
3148
3122
  await validateCompleteSessionInput(tx, projectId, input.session);
3149
- assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience);
3150
3123
  let newUserId;
3151
3124
  if (shouldCreateUser) {
3152
3125
  newUserId = assertNonEmpty(input.newUserId, "User id");
@@ -3251,7 +3224,6 @@ class PgAuthStorage {
3251
3224
  };
3252
3225
  }
3253
3226
  await validateCompleteSessionInput(tx, projectId, input.session);
3254
- assertSignInSessionAudience(projectId, input.session.audience, attempt.audience);
3255
3227
  let newUserId;
3256
3228
  if (shouldCreateUser) {
3257
3229
  newUserId = assertNonEmpty(input.newUserId, "User id");
@@ -3459,16 +3431,12 @@ class PgAuthStorage {
3459
3431
  return this.applyGroups(sql, { ...input, groupIds, source: "directory" });
3460
3432
  }
3461
3433
  async createSignInSession(sql, input) {
3462
- await lockAdvisoryKeys(sql, [
3463
- authLockKey("sessions", input.projectId, input.userId),
3464
- authLockKey("sessions", input.projectId, input.userId, input.session.audience)
3465
- ]);
3434
+ await lockAdvisoryKeys(sql, [authLockKey("sessions", input.projectId, input.userId)]);
3466
3435
  return createSession(sql, {
3467
3436
  id: input.session.id,
3468
3437
  projectId: input.projectId,
3469
3438
  userId: input.userId,
3470
3439
  strategyId: input.strategyId,
3471
- audience: input.session.audience,
3472
3440
  tokenHash: input.session.tokenHash,
3473
3441
  createdAt: input.session.createdAt,
3474
3442
  expiresAt: input.session.expiresAt,
@@ -3514,12 +3482,6 @@ class PgAuthStorage {
3514
3482
  return rowToIdentityRecord(row);
3515
3483
  }
3516
3484
  }
3517
- function assertSignInSessionAudience(projectId, sessionAudience, storedAudience) {
3518
- if (sessionAudience === storedAudience) {
3519
- return;
3520
- }
3521
- throw new AuthStorageError11("invalid_input", `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`);
3522
- }
3523
3485
  function assertCompletableDeviceAuthorization(authorization, input) {
3524
3486
  if (authorization.deviceCodeHash !== input.deviceCodeHash) {
3525
3487
  throw new AuthStorageError11("invalid_device_authorization", "[Sixb] Invalid device authorization.");
@@ -8168,6 +8130,223 @@ CREATE INDEX idx_action_runs_project_status_started
8168
8130
  ON action_runs (project_id, status, started_at DESC);
8169
8131
  `;
8170
8132
 
8133
+ // src/migrations/062-drop-auth-audience.sql
8134
+ var _062_drop_auth_audience_default = `-- A project has one browser app, so sessions, magic links and sign-in attempts no longer name the
8135
+ -- one they belong to. Atlas's browser sessions end here: their cookie is no longer read. Native
8136
+ -- sessions carried Atlas's name only because the column needed a value, so they stay.
8137
+ UPDATE auth_sessions
8138
+ SET revoked_at = now()
8139
+ WHERE audience = 'atlas'
8140
+ AND refresh_token_hash IS NULL
8141
+ AND revoked_at IS NULL;
8142
+
8143
+ -- Dropping the column drops the two indexes that name it.
8144
+ ALTER TABLE auth_sessions DROP COLUMN audience;
8145
+ CREATE INDEX idx_auth_sessions_user_active
8146
+ ON auth_sessions (project_id, user_id, expires_at DESC, created_at DESC, id DESC)
8147
+ WHERE revoked_at IS NULL;
8148
+ CREATE INDEX idx_auth_sessions_user_created
8149
+ ON auth_sessions (project_id, user_id, created_at DESC, id DESC);
8150
+
8151
+ ALTER TABLE auth_magic_links DROP COLUMN audience;
8152
+ ALTER TABLE auth_oidc_authorization_attempts DROP COLUMN audience;
8153
+ `;
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
+
8171
8350
  // src/migrations.ts
8172
8351
  var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
8173
8352
  function pgSql(id, sqlText) {
@@ -8395,7 +8574,14 @@ var postgresStorageMigrations = defineMigrations({
8395
8574
  pgSql("058-reranking-model-kind", _058_reranking_model_kind_default),
8396
8575
  pgSql("059-file-download-grants", _059_file_download_grants_default),
8397
8576
  pgSql("060-drained-source-versions", _060_drained_source_versions_default),
8398
- pgSql("061-terminal-action-runs", _061_terminal_action_runs_default)
8577
+ pgSql("061-terminal-action-runs", _061_terminal_action_runs_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)
8399
8585
  ]
8400
8586
  });
8401
8587
 
@@ -12723,7 +12909,7 @@ import {
12723
12909
  var PG_PLAN_WORK_TABLE = "ontology_replacement_plan_work";
12724
12910
  var IDENTITIES = "ontology_replacement_plan_identities";
12725
12911
  var SORT_KEY = "encode(convert_to(identity_key, 'UTF8'), 'hex')";
12726
- var ANALYZE_ROWS = 1e4;
12912
+ var ANALYZE_ROWS = 1000;
12727
12913
  function objectRevision(type, id) {
12728
12914
  return `jsonb_build_array(
12729
12915
  (SELECT jsonb_build_array(version, last_commit_id) FROM objects
@@ -12860,7 +13046,7 @@ class PgOntologyReplacementPlanStorage {
12860
13046
  }
12861
13047
  async* streamState(input) {
12862
13048
  assertPageRows(input.pageRows);
12863
- await runPgTransaction(this.sql, async (sql) => {
13049
+ const versionId = await runPgTransaction(this.sql, async (sql) => {
12864
13050
  const { candidate } = await requirePlan(sql, input);
12865
13051
  if (input.entityKind === "object" && candidate.projection_kind !== "object") {
12866
13052
  throw new MaterializationConflictError4("source-materialization", "Link projection replacement cannot stream object state.");
@@ -12873,6 +13059,7 @@ class PgOntologyReplacementPlanStorage {
12873
13059
  if (expanded >= ANALYZE_ROWS && !isWithinPgTransaction(this.sql))
12874
13060
  await analyzePlans(sql);
12875
13061
  }
13062
+ return candidate.version_id;
12876
13063
  });
12877
13064
  let after = "";
12878
13065
  let streamed = 0;
@@ -12902,8 +13089,8 @@ class PgOntologyReplacementPlanStorage {
12902
13089
  return state;
12903
13090
  }, { isolation: "repeatableRead" });
12904
13091
  if (!page) {
12905
- if (streamed >= ANALYZE_ROWS && !isWithinPgTransaction(this.sql)) {
12906
- await analyzePlans(this.sql);
13092
+ if (streamed > 0 && !isWithinPgTransaction(this.sql)) {
13093
+ await analyzeStagedWork(this.sql, versionId);
12907
13094
  }
12908
13095
  return;
12909
13096
  }
@@ -13431,6 +13618,16 @@ async function purgeTouches(sql, projectId, limit) {
13431
13618
  async function analyzePlans(sql) {
13432
13619
  await sql`ANALYZE ${sql(IDENTITIES)}, ${sql(PG_PLAN_WORK_TABLE)}`;
13433
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
+ }
13434
13631
  async function dropUnplannedWork(sql, versionId, kind) {
13435
13632
  await sql`
13436
13633
  DELETE FROM ${sql(PG_PLAN_WORK_TABLE)}
@@ -14698,6 +14895,7 @@ import { ONTOLOGY_OUTBOX_FAILURE_CODES as ONTOLOGY_OUTBOX_FAILURE_CODES2 } from
14698
14895
 
14699
14896
  // src/pg-execution-storage.ts
14700
14897
  import {
14898
+ compareExecutionCreation,
14701
14899
  executionRecordFromStorageRow,
14702
14900
  executionRecordToStorageRow,
14703
14901
  prepareExecutionRecord
@@ -14791,6 +14989,37 @@ class PgExecutionStorage {
14791
14989
  `;
14792
14990
  return row ? pgExecutionRecord(row) : null;
14793
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
+ }
14794
15023
  }
14795
15024
  function pgExecutionRecord(row) {
14796
15025
  return executionRecordFromStorageRow(toStorageRow(row));
@@ -15263,6 +15492,26 @@ class PgOntologySourceStorage {
15263
15492
  return row ? sourceRecord(row) : null;
15264
15493
  });
15265
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
+ }
15266
15515
  async adopt(input) {
15267
15516
  return this.runRootOperation(async (sql) => {
15268
15517
  assertProjectAndSource(input);
@@ -15934,6 +16183,23 @@ class PgActionRunStorage {
15934
16183
  `;
15935
16184
  return row ? rowToActionRunRecord(row) : null;
15936
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
+ }
15937
16203
  async list(input) {
15938
16204
  if (input.statuses && input.statuses.length === 0 || input.actionIds?.length === 0) {
15939
16205
  return {
@@ -16097,6 +16363,350 @@ function rowToActionSubject(row) {
16097
16363
  };
16098
16364
  }
16099
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
+
16100
16710
  // src/pg-ai-cost-storage.ts
16101
16711
  import {
16102
16712
  aiModelCallCostDetails,
@@ -17580,6 +18190,260 @@ function invalidState(reservation, operation) {
17580
18190
  return new AiLimitStorageError("invalid_reservation_state", `[SixbPg] Cannot ${operation} AI model-call reservation '${reservation.callId}' in state '${reservation.state}'.`);
17581
18191
  }
17582
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
+
17583
18447
  // src/pg-client.ts
17584
18448
  import { isIP } from "net";
17585
18449
  import { connect as connectTls } from "tls";
@@ -20004,6 +20868,23 @@ class PgWorkflowRunStorage {
20004
20868
  `;
20005
20869
  return row ? rowToWorkflowRunRecord(row) : null;
20006
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
+ }
20007
20888
  async list(input) {
20008
20889
  if (hasEmptyStatuses(input) || input.workflowIds?.length === 0) {
20009
20890
  return {
@@ -20607,6 +21488,23 @@ class PgShareGrantStorage {
20607
21488
  `;
20608
21489
  return row ? recordFromRow(row) : null;
20609
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
+ }
20610
21508
  async list(input) {
20611
21509
  const normalized = normalizeListShareGrantsInput(input);
20612
21510
  const where = ["project_id = $1"];
@@ -20826,6 +21724,7 @@ class PostgresStorage {
20826
21724
  auth;
20827
21725
  executions;
20828
21726
  agents;
21727
+ artifacts;
20829
21728
  aiUsage;
20830
21729
  aiCosts;
20831
21730
  aiLimits;
@@ -20843,6 +21742,7 @@ class PostgresStorage {
20843
21742
  shareSessions;
20844
21743
  fileUploadSessions;
20845
21744
  fileDownloadGrants;
21745
+ activity;
20846
21746
  migrators;
20847
21747
  sql;
20848
21748
  schemaName;
@@ -20884,6 +21784,7 @@ class PostgresStorage {
20884
21784
  this.auth = createAuthOperationScope(stores.auth, scope);
20885
21785
  this.executions = createOperationScopedFacade(stores.executions, scope);
20886
21786
  this.agents = createAgentOperationScope(stores.agents, scope);
21787
+ this.artifacts = createOperationScopedFacade(stores.artifacts, scope);
20887
21788
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope);
20888
21789
  this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope);
20889
21790
  this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope);
@@ -20901,6 +21802,7 @@ class PostgresStorage {
20901
21802
  this.shareSessions = createOperationScopedFacade(stores.shareSessions, scope);
20902
21803
  this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope);
20903
21804
  this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope);
21805
+ this.activity = createOperationScopedFacade(stores.activity, scope);
20904
21806
  registerPostgresStorageTestingAdapter(this, (durationMs) => stores.connectorConnections.advanceTimeForTesting(durationMs));
20905
21807
  }
20906
21808
  async ping() {
@@ -20993,6 +21895,7 @@ function createPostgresStores(sql, options) {
20993
21895
  auth,
20994
21896
  executions,
20995
21897
  agents: new PgAgentStorage({ sql, executions }),
21898
+ artifacts: new PgArtifactStorage(sql),
20996
21899
  aiUsage: new PgAiUsageStorage(sql),
20997
21900
  aiCosts: new PgAiCostStorage(sql),
20998
21901
  aiLimits: new PgAiLimitStorage(sql),
@@ -21009,7 +21912,8 @@ function createPostgresStores(sql, options) {
21009
21912
  shareGrants,
21010
21913
  shareSessions,
21011
21914
  fileUploadSessions: new PgFileUploadSessionStorage(sql),
21012
- fileDownloadGrants: new PgFileDownloadGrantStorage(sql)
21915
+ fileDownloadGrants: new PgFileDownloadGrantStorage(sql),
21916
+ activity: new PgActivityStorage(sql)
21013
21917
  };
21014
21918
  }
21015
21919
  function resolveTimeoutMillis(value, label) {
@@ -21026,4 +21930,4 @@ export {
21026
21930
  migratePostgresStorage
21027
21931
  };
21028
21932
 
21029
- //# debugId=295D429EF5EE4F7864756E2164756E21
21933
+ //# debugId=E2B2D974990D4A3064756E2164756E21