@opengeni/db 3.6.0 → 3.7.4-canary.0

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 (92) hide show
  1. package/dist/automations.d.ts +4 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/{chunk-UGDVSQ6U.js → chunk-3ZMN4EXC.js} +2 -2
  4. package/dist/{chunk-744DFC65.js → chunk-6FIVOJ3V.js} +96 -6
  5. package/dist/chunk-6FIVOJ3V.js.map +1 -0
  6. package/dist/{chunk-UJ2EW3QI.js → chunk-DXJ2WY6W.js} +427 -85
  7. package/dist/chunk-DXJ2WY6W.js.map +1 -0
  8. package/dist/{chunk-CDMMSUWJ.js → chunk-JP6IYGYI.js} +3 -3
  9. package/dist/{chunk-Q5YUGQVK.js → chunk-JS3AWDKZ.js} +3 -3
  10. package/dist/{chunk-G3732M3T.js → chunk-LHKB62GC.js} +2 -2
  11. package/dist/{chunk-2NT476HD.js → chunk-QL4AHBYD.js} +2 -2
  12. package/dist/{chunk-IW6O6G7R.js → chunk-VDXHLVDE.js} +2 -2
  13. package/dist/{chunk-WW7AJ5IC.js → chunk-ZTSJPFDH.js} +5 -7
  14. package/dist/chunk-ZTSJPFDH.js.map +1 -0
  15. package/dist/codex-token-resolver.d.ts +12 -1
  16. package/dist/company-profile-agent-admin.d.ts +4 -2
  17. package/dist/company-profile-agent-policy.d.ts +20 -0
  18. package/dist/company-profile-schema.d.ts +573 -0
  19. package/dist/editable-artifact-durable-export.js +2 -2
  20. package/dist/editable-artifacts.js +3 -3
  21. package/dist/index.d.ts +487 -20
  22. package/dist/index.js +9489 -6041
  23. package/dist/index.js.map +1 -1
  24. package/dist/managed-auth-session-sets.js +3 -3
  25. package/dist/managed-human-provisioning.d.ts +3 -3
  26. package/dist/memory-domain.d.ts +5 -3
  27. package/dist/model-catalog.d.ts +153 -0
  28. package/dist/pr-review.d.ts +9 -0
  29. package/dist/provision-roles.js +1 -1
  30. package/dist/runtime-posture.d.ts +7 -7
  31. package/dist/schema.d.ts +1069 -6
  32. package/dist/schema.js +17 -1
  33. package/dist/session-background-commands.js +3 -3
  34. package/dist/session-control.d.ts +75 -5
  35. package/dist/session-queue-commands.d.ts +19 -0
  36. package/dist/session-tenancy.js +3 -3
  37. package/dist/user-resource-authority.d.ts +8 -0
  38. package/dist/video-generation.js +3 -3
  39. package/dist/xai-subscription.d.ts +8 -0
  40. package/drizzle/0369_local_human_personal_authority.sql +350 -0
  41. package/drizzle/0370_workspace_member_management_scope.sql +98 -0
  42. package/drizzle/0371_workspace_member_candidate_inventory.sql +128 -0
  43. package/drizzle/0372_ordered_variable_set_runtime_authority.sql +287 -0
  44. package/drizzle/0373_sandbox_shared_preparation.sql +59 -0
  45. package/drizzle/0374_session_event_cursors.sql +315 -0
  46. package/drizzle/0375_session_recovery_observability.sql +73 -0
  47. package/drizzle/0376_organization_workspace_inventory.sql +88 -0
  48. package/drizzle/0377_scoped_rig_health_projection.sql +80 -0
  49. package/drizzle/0378_scoped_rig_health_audit_timestamp.sql +77 -0
  50. package/drizzle/0379_session_event_raw_lane_activation.sql +518 -0
  51. package/drizzle/0380_autonomous_company_profile_agent_policy.sql +829 -0
  52. package/drizzle/0381_organization_codex_subscription_inheritance.sql +720 -0
  53. package/drizzle/0382_organization_api_key_provenance.sql +91 -0
  54. package/drizzle/0383_codex_cooldown_reconciliation.sql +51 -0
  55. package/drizzle/0384_codex_cooldown_revision_guard_privileges.sql +6 -0
  56. package/drizzle/0385_connected_machine_legacy_tilde_workdirs.sql +25 -0
  57. package/drizzle/0386_local_organization_administration.sql +157 -0
  58. package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
  59. package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
  60. package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
  61. package/package.json +6 -6
  62. package/src/automations.ts +9 -1
  63. package/src/codex-token-resolver.ts +26 -1
  64. package/src/company-brain-governed-writes.ts +30 -1
  65. package/src/company-profile-agent-admin.ts +68 -1
  66. package/src/company-profile-agent-policy.ts +126 -0
  67. package/src/company-profile-schema.ts +115 -1
  68. package/src/database.ts +3 -5
  69. package/src/index.ts +4473 -570
  70. package/src/insights.ts +21 -7
  71. package/src/managed-human-provisioning.ts +3 -3
  72. package/src/memory-domain.ts +9 -4
  73. package/src/model-catalog.ts +682 -0
  74. package/src/organization-membership-lifecycle.ts +0 -1
  75. package/src/pr-review.ts +19 -0
  76. package/src/provision-roles.ts +83 -4
  77. package/src/runtime-posture.ts +26 -0
  78. package/src/schema.ts +259 -10
  79. package/src/session-control.ts +347 -59
  80. package/src/session-queue-commands.ts +303 -122
  81. package/src/session-tool-call-settlement.ts +34 -7
  82. package/src/user-resource-authority.ts +56 -0
  83. package/src/xai-subscription.ts +66 -14
  84. package/dist/chunk-744DFC65.js.map +0 -1
  85. package/dist/chunk-UJ2EW3QI.js.map +0 -1
  86. package/dist/chunk-WW7AJ5IC.js.map +0 -1
  87. /package/dist/{chunk-UGDVSQ6U.js.map → chunk-3ZMN4EXC.js.map} +0 -0
  88. /package/dist/{chunk-CDMMSUWJ.js.map → chunk-JP6IYGYI.js.map} +0 -0
  89. /package/dist/{chunk-Q5YUGQVK.js.map → chunk-JS3AWDKZ.js.map} +0 -0
  90. /package/dist/{chunk-G3732M3T.js.map → chunk-LHKB62GC.js.map} +0 -0
  91. /package/dist/{chunk-2NT476HD.js.map → chunk-QL4AHBYD.js.map} +0 -0
  92. /package/dist/{chunk-IW6O6G7R.js.map → chunk-VDXHLVDE.js.map} +0 -0
@@ -104,6 +104,18 @@ export type EffectiveSessionControl = {
104
104
  } | null;
105
105
  };
106
106
 
107
+ /**
108
+ * The bounded control projection required by a locked activity-write fence.
109
+ * Settlement and background-command summaries are read-model concerns and must
110
+ * not extend the session-event critical section when admission only needs the
111
+ * effective run state and blocker scope.
112
+ */
113
+ export type SessionWriteAdmissionControl = {
114
+ state: EffectiveControlState;
115
+ controlVersion: number;
116
+ primaryBlockerKind: EffectiveControlBlocker["kind"] | null;
117
+ };
118
+
107
119
  type SettlementAttemptCounts = {
108
120
  attemptCount: number;
109
121
  interruptionPendingCount: number;
@@ -627,6 +639,13 @@ export type SessionEventWriteLockInput = {
627
639
  controlLock: WorkspaceControlLockMode | "already_locked" | "none";
628
640
  /** Used only when a staged caller already established the workspace prefix. */
629
641
  workspaceLock?: "key_share" | "already_locked";
642
+ /**
643
+ * Semantic writers retain FOR NO KEY UPDATE. Cursor-authoritative raw-only
644
+ * appends need only the FK-compatible identity hold; KEY SHARE is compatible
645
+ * with semantic NO KEY UPDATE and therefore removes the wide session row as
646
+ * their serialization mutex without inverting the rolling lock order.
647
+ */
648
+ sessionLock?: "no_key_update" | "key_share";
630
649
  sessionIds?: string[];
631
650
  turnIds?: string[];
632
651
  attemptIds?: string[];
@@ -636,6 +655,7 @@ export type SessionEventWriteLocks = {
636
655
  control: WorkspaceControlRow | null;
637
656
  workspace: typeof schema.workspaces.$inferSelect | null;
638
657
  sessions: Array<typeof schema.sessions.$inferSelect>;
658
+ cursors: Array<typeof schema.sessionEventCursors.$inferSelect>;
639
659
  turns: Array<typeof schema.sessionTurns.$inferSelect>;
640
660
  attempts: Array<typeof schema.sessionTurnAttempts.$inferSelect>;
641
661
  };
@@ -646,7 +666,8 @@ export type SessionEventWriteLocks = {
646
666
  * workspace-control advisory lock + workspace_inference_controls row
647
667
  * (when control-aware; see `lockWorkspaceInferenceControl`)
648
668
  * -> actual workspaces row FOR KEY SHARE
649
- * -> session rows FOR NO KEY UPDATE, UUID ordered
669
+ * -> session rows FOR NO KEY UPDATE (semantic) or KEY SHARE (raw), UUID ordered
670
+ * -> session event cursor rows FOR UPDATE, UUID ordered
650
671
  * -> exact turn rows FOR UPDATE, UUID ordered
651
672
  * -> exact attempt rows FOR UPDATE, UUID ordered
652
673
  *
@@ -661,10 +682,12 @@ export type SessionEventWriteLocks = {
661
682
  * `FOR UPDATE` lock serialized unrelated sessions and inverted the activity
662
683
  * path's session -> implicit workspace-FK edge.
663
684
  *
664
- * `FOR NO KEY UPDATE` is equally deliberate for sessions. Session primary and
665
- * composite identity keys are immutable, so writers need to exclude only concurrent
666
- * non-key mutation. Keeping FK `FOR KEY SHARE` checks compatible prevents the
667
- * activity finalizer's workspace counter from participating in a lock cycle.
685
+ * `FOR NO KEY UPDATE` is equally deliberate for semantic session writers.
686
+ * Cursor-authoritative raw-only writers use `FOR KEY SHARE`: it preserves the
687
+ * session identity against deletion while remaining compatible with semantic
688
+ * `FOR NO KEY UPDATE`. Keeping this rolling session-before-cursor order avoids
689
+ * an inversion with old binaries and quiescent cascade deletion; the compact
690
+ * cursor row, not the wide session row, is the raw ordering mutex.
668
691
  *
669
692
  * Complex lifecycle transactions may acquire allocator/control locks before
670
693
  * this helper and may discover exact turn IDs only after locking the session.
@@ -698,7 +721,8 @@ export async function lockSessionEventWriteRows(
698
721
  }
699
722
 
700
723
  const sessionIds = [...new Set(input.sessionIds ?? [])].sort();
701
- const sessions =
724
+ const sessionLock = input.sessionLock ?? "no_key_update";
725
+ let sessions =
702
726
  sessionIds.length > 0
703
727
  ? await db
704
728
  .select()
@@ -710,8 +734,72 @@ export async function lockSessionEventWriteRows(
710
734
  ),
711
735
  )
712
736
  .orderBy(schema.sessions.id)
713
- .for("no key update")
737
+ .for(sessionLock === "key_share" ? "key share" : "no key update")
738
+ : [];
739
+
740
+ const cursors =
741
+ sessions.length > 0
742
+ ? await db
743
+ .select()
744
+ .from(schema.sessionEventCursors)
745
+ .where(
746
+ and(
747
+ eq(schema.sessionEventCursors.workspaceId, input.workspaceId),
748
+ inArray(
749
+ schema.sessionEventCursors.sessionId,
750
+ sessions.map((session) => session.id),
751
+ ),
752
+ ),
753
+ )
754
+ .orderBy(schema.sessionEventCursors.sessionId)
755
+ .for("update")
714
756
  : [];
757
+ if (cursors.length !== sessions.length) {
758
+ throw new SessionControlInvariantError(
759
+ `Session event cursor lock set was incomplete for workspace ${input.workspaceId}`,
760
+ );
761
+ }
762
+ if (sessionLock === "key_share" && sessions.length > 0) {
763
+ // KEY SHARE is intentionally compatible with a semantic writer's NO KEY
764
+ // UPDATE. If that writer reached the cursor first it may have committed a
765
+ // control/authority/session-state change after our initial identity read.
766
+ // Re-read only after owning the cursor so the attempt fence evaluates the
767
+ // latest committed session state while later semantic writers wait here.
768
+ sessions = await db
769
+ .select()
770
+ .from(schema.sessions)
771
+ .where(
772
+ and(
773
+ eq(schema.sessions.workspaceId, input.workspaceId),
774
+ inArray(schema.sessions.id, sessionIds),
775
+ ),
776
+ )
777
+ .orderBy(schema.sessions.id)
778
+ .for("key share");
779
+ }
780
+ const canonicalSessions = sessions.map((session, index) => {
781
+ const cursor = cursors[index]!;
782
+ if (
783
+ cursor.sessionId !== session.id ||
784
+ cursor.accountId !== session.accountId ||
785
+ cursor.workspaceId !== session.workspaceId
786
+ ) {
787
+ throw new SessionControlInvariantError(
788
+ `Session event cursor identity failed for session ${session.id}`,
789
+ );
790
+ }
791
+ // The cursor is the allocation authority. The wide session column remains
792
+ // a rolling compatibility/read projection until every reader is cut over.
793
+ // It may lag after the later pure-append activation, but it must never lead
794
+ // the durable cursor or an old writer could have committed an unverified
795
+ // sequence.
796
+ if (cursor.lastSequence < session.lastSequence) {
797
+ throw new SessionControlInvariantError(
798
+ `Session event cursor is behind session projection for session ${session.id}`,
799
+ );
800
+ }
801
+ return { ...session, lastSequence: cursor.lastSequence };
802
+ });
715
803
 
716
804
  const turnIds = [...new Set(input.turnIds ?? [])].sort();
717
805
  const turns =
@@ -745,7 +833,7 @@ export async function lockSessionEventWriteRows(
745
833
  .for("update")
746
834
  : [];
747
835
 
748
- return { control, workspace, sessions, turns, attempts };
836
+ return { control, workspace, sessions: canonicalSessions, cursors, turns, attempts };
749
837
  }
750
838
 
751
839
  export async function registerSessionTurnAttemptClaim(
@@ -1337,6 +1425,10 @@ async function settlementAttemptCounts(
1337
1425
  workspaceId: string,
1338
1426
  sessionIds: string[],
1339
1427
  ): Promise<Map<string, SettlementAttemptCounts>> {
1428
+ // Drive the projection from the requested targets through the indexed
1429
+ // parent_session_id edge. The inverse plan scanned every interruption in a
1430
+ // workspace before walking upward, making one session read proportional to
1431
+ // a high-volume orchestrator workspace's entire interruption history.
1340
1432
  const rows = await db.execute<{
1341
1433
  sessionId: string;
1342
1434
  attemptCount: number | string;
@@ -1344,9 +1436,24 @@ async function settlementAttemptCounts(
1344
1436
  quiescencePendingCount: number | string;
1345
1437
  }>(sql`
1346
1438
  with recursive targets(id) as (values ${targetValues(sessionIds)}),
1347
- interruptions as (
1439
+ descendant_sessions(target_id, session_id, depth, path) as (
1440
+ select target.id, target.id, 0::integer, array[target.id]::uuid[]
1441
+ from targets target
1442
+ union all
1443
+ select
1444
+ descendant.target_id,
1445
+ child.id,
1446
+ descendant.depth + 1,
1447
+ descendant.path || child.id
1448
+ from descendant_sessions descendant
1449
+ join ${schema.sessions} child
1450
+ on child.workspace_id = ${workspaceId}
1451
+ and child.parent_session_id = descendant.session_id
1452
+ where not child.id = any(descendant.path)
1453
+ and descendant.depth < ${SESSION_ANCESTRY_LIMIT}
1454
+ ), interruptions as (
1348
1455
  select
1349
- interruption.session_id,
1456
+ descendant.target_id,
1350
1457
  interruption.attempt_id,
1351
1458
  bool_or(interruption.state in ('pending', 'delivered', 'acknowledged'))
1352
1459
  as interruption_pending,
@@ -1354,62 +1461,31 @@ async function settlementAttemptCounts(
1354
1461
  interruption.state in ('settled', 'rejected_stale')
1355
1462
  and attempt.quiesced_at is null
1356
1463
  ) as quiescence_pending
1357
- from ${schema.sessionAttemptInterruptions} interruption
1464
+ from descendant_sessions descendant
1465
+ join ${schema.sessionAttemptInterruptions} interruption
1466
+ on interruption.workspace_id = ${workspaceId}
1467
+ and interruption.session_id = descendant.session_id
1358
1468
  join ${schema.sessionTurnAttempts} attempt
1359
1469
  on attempt.workspace_id = interruption.workspace_id
1360
1470
  and attempt.id = interruption.attempt_id
1361
- where interruption.workspace_id = ${workspaceId}
1362
- and (
1471
+ where (
1363
1472
  interruption.state in ('pending', 'delivered', 'acknowledged')
1364
1473
  or (
1365
1474
  interruption.state in ('settled', 'rejected_stale')
1366
1475
  and attempt.quiesced_at is null
1367
1476
  )
1368
1477
  )
1369
- group by interruption.session_id, interruption.attempt_id
1370
- ), interruption_ancestry(
1371
- session_id,
1372
- ancestor_id,
1373
- attempt_id,
1374
- interruption_pending,
1375
- quiescence_pending,
1376
- depth,
1377
- path
1378
- ) as (
1379
- select
1380
- interruption.session_id,
1381
- interruption.session_id,
1382
- interruption.attempt_id,
1383
- interruption.interruption_pending,
1384
- interruption.quiescence_pending,
1385
- 0::integer,
1386
- array[interruption.session_id]::uuid[]
1387
- from interruptions interruption
1388
- union all
1389
- select
1390
- ancestry.session_id,
1391
- current.parent_session_id,
1392
- ancestry.attempt_id,
1393
- ancestry.interruption_pending,
1394
- ancestry.quiescence_pending,
1395
- ancestry.depth + 1,
1396
- ancestry.path || current.parent_session_id
1397
- from interruption_ancestry ancestry
1398
- join ${schema.sessions} current
1399
- on current.workspace_id = ${workspaceId} and current.id = ancestry.ancestor_id
1400
- where current.parent_session_id is not null
1401
- and not current.parent_session_id = any(ancestry.path)
1402
- and ancestry.depth < ${SESSION_ANCESTRY_LIMIT}
1478
+ group by descendant.target_id, interruption.attempt_id
1403
1479
  )
1404
1480
  select
1405
1481
  target.id as "sessionId",
1406
- count(distinct ancestry.attempt_id)::integer as "attemptCount",
1407
- count(distinct ancestry.attempt_id)
1408
- filter (where ancestry.interruption_pending)::integer as "interruptionPendingCount",
1409
- count(distinct ancestry.attempt_id)
1410
- filter (where ancestry.quiescence_pending)::integer as "quiescencePendingCount"
1482
+ count(interruption.attempt_id)::integer as "attemptCount",
1483
+ count(interruption.attempt_id)
1484
+ filter (where interruption.interruption_pending)::integer as "interruptionPendingCount",
1485
+ count(interruption.attempt_id)
1486
+ filter (where interruption.quiescence_pending)::integer as "quiescencePendingCount"
1411
1487
  from targets target
1412
- join interruption_ancestry ancestry on ancestry.ancestor_id = target.id
1488
+ join interruptions interruption on interruption.target_id = target.id
1413
1489
  group by target.id
1414
1490
  `);
1415
1491
  return new Map(
@@ -1500,6 +1576,46 @@ export async function evaluateSessionControls(
1500
1576
  return result;
1501
1577
  }
1502
1578
 
1579
+ /**
1580
+ * Evaluate only the control facts needed to admit an already locked session
1581
+ * write. The workspace-control row must be reused when the caller established
1582
+ * the canonical prefix before session/turn/attempt locks.
1583
+ *
1584
+ * Deliberately omit settlementAttemptCounts and
1585
+ * stoppingBackgroundCommandCounts: those recursive summaries do not affect
1586
+ * effective pause state and their results are discarded by the write fence.
1587
+ */
1588
+ export async function evaluateSessionWriteAdmissionControl(
1589
+ db: Database,
1590
+ workspaceId: string,
1591
+ sessionId: string,
1592
+ options: {
1593
+ lock?: WorkspaceControlLockMode;
1594
+ workspaceControl?: WorkspaceControlRow | undefined;
1595
+ } = {},
1596
+ ): Promise<SessionWriteAdmissionControl> {
1597
+ if (options.workspaceControl && options.workspaceControl.workspaceId !== workspaceId) {
1598
+ throw new SessionControlInvariantError(
1599
+ `Locked workspace control ${options.workspaceControl.workspaceId} does not match ${workspaceId}`,
1600
+ );
1601
+ }
1602
+ const workspace =
1603
+ options.workspaceControl ??
1604
+ (await lockWorkspaceInferenceControl(db, workspaceId, options.lock ?? "share"));
1605
+ const projected = projectEffectiveControl(
1606
+ workspace,
1607
+ sessionId,
1608
+ await loadTargetAncestryRows(db, workspaceId, [sessionId]),
1609
+ NO_SETTLEMENT_ATTEMPTS,
1610
+ 0,
1611
+ );
1612
+ return {
1613
+ state: projected.state,
1614
+ controlVersion: projected.controlVersion,
1615
+ primaryBlockerKind: projected.primaryBlocker?.kind ?? null,
1616
+ };
1617
+ }
1618
+
1503
1619
  type SessionDiscoveryControlRow = {
1504
1620
  targetId: string;
1505
1621
  ancestryCount: number | string;
@@ -1768,6 +1884,174 @@ async function findCommandReceipt(
1768
1884
  return rows[0] ?? null;
1769
1885
  }
1770
1886
 
1887
+ export type SessionPromptCommandAction = "prompt.send" | "prompt.steer";
1888
+
1889
+ async function findPromptCommandReceipts(
1890
+ db: Database,
1891
+ input: {
1892
+ workspaceId: string;
1893
+ actor: SessionCommandActor;
1894
+ operationKey: string;
1895
+ lock?: "none" | "update";
1896
+ },
1897
+ ): Promise<SessionCommandReceiptRow[]> {
1898
+ const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
1899
+ const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
1900
+ const query = db
1901
+ .select()
1902
+ .from(schema.sessionCommandReceipts)
1903
+ .where(
1904
+ and(
1905
+ eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
1906
+ eq(schema.sessionCommandReceipts.actorType, input.actor.type),
1907
+ sql`${schema.sessionCommandReceipts.actorSubjectId} is not distinct from ${actorSubjectId}`,
1908
+ sql`${schema.sessionCommandReceipts.actorAttemptId} is not distinct from ${actorAttemptId}::uuid`,
1909
+ inArray(schema.sessionCommandReceipts.action, ["prompt.send", "prompt.steer"]),
1910
+ eq(schema.sessionCommandReceipts.operationKey, input.operationKey),
1911
+ ),
1912
+ );
1913
+ const rows = input.lock === "update" ? await query.for("update") : await query;
1914
+ return rows;
1915
+ }
1916
+
1917
+ function fingerprintedPromptCommandReceipt(
1918
+ receipts: readonly SessionCommandReceiptRow[],
1919
+ ): SessionCommandReceiptRow | null {
1920
+ const fingerprinted = receipts.filter(
1921
+ (receipt) => typeof receipt.result.boundaryRequestHash === "string",
1922
+ );
1923
+ if (fingerprinted.length > 1) {
1924
+ throw new SessionControlInvariantError(
1925
+ "Prompt operation key resolved to multiple fingerprinted receipts",
1926
+ );
1927
+ }
1928
+ return fingerprinted[0] ?? null;
1929
+ }
1930
+
1931
+ function assertPromptCommandReceiptIdentity(
1932
+ receipt: SessionCommandReceiptRow,
1933
+ input: {
1934
+ action: SessionPromptCommandAction;
1935
+ targetSessionId: string;
1936
+ boundaryRequestHash: string;
1937
+ },
1938
+ ): void {
1939
+ if (
1940
+ receipt.action !== input.action ||
1941
+ receipt.targetSessionId !== input.targetSessionId ||
1942
+ receipt.targetTurnId !== null ||
1943
+ (typeof receipt.result.boundaryRequestHash === "string" &&
1944
+ receipt.result.boundaryRequestHash !== input.boundaryRequestHash)
1945
+ ) {
1946
+ throw new SessionCommandIdempotencyError();
1947
+ }
1948
+ }
1949
+
1950
+ /**
1951
+ * Prompt operation keys are actor-scoped across target sessions and across the
1952
+ * Send/Steer action pair. The advisory lock closes the gap left by the legacy
1953
+ * action/target-specific unique index without changing old receipt identities.
1954
+ */
1955
+ export async function reserveSessionPromptCommandReceipt(
1956
+ db: Database,
1957
+ input: {
1958
+ accountId: string;
1959
+ workspaceId: string;
1960
+ actor: SessionCommandActor;
1961
+ action: SessionPromptCommandAction;
1962
+ targetSessionId: string;
1963
+ operationKey: string;
1964
+ canonicalRequestHash: string;
1965
+ boundaryRequestHash: string;
1966
+ },
1967
+ ): Promise<{ receipt: SessionCommandReceiptRow; replay: boolean }> {
1968
+ await lockSessionPromptCommandOperation(db, input);
1969
+ const existingReceipts = await findPromptCommandReceipts(db, {
1970
+ workspaceId: input.workspaceId,
1971
+ actor: input.actor,
1972
+ operationKey: input.operationKey,
1973
+ lock: "update",
1974
+ });
1975
+ const fingerprinted = fingerprintedPromptCommandReceipt(existingReceipts);
1976
+ if (fingerprinted) {
1977
+ assertPromptCommandReceiptIdentity(fingerprinted, input);
1978
+ return { receipt: fingerprinted, replay: true };
1979
+ }
1980
+ const legacyExact = existingReceipts.find(
1981
+ (receipt) =>
1982
+ receipt.action === input.action &&
1983
+ receipt.targetSessionId === input.targetSessionId &&
1984
+ receipt.targetTurnId === null,
1985
+ );
1986
+ if (legacyExact) {
1987
+ if (legacyExact.canonicalRequestHash !== input.canonicalRequestHash) {
1988
+ throw new SessionCommandIdempotencyError();
1989
+ }
1990
+ return { receipt: legacyExact, replay: true };
1991
+ }
1992
+ return await reserveSessionCommandReceipt(db, {
1993
+ accountId: input.accountId,
1994
+ workspaceId: input.workspaceId,
1995
+ actor: input.actor,
1996
+ action: input.action,
1997
+ targetSessionId: input.targetSessionId,
1998
+ targetTurnId: null,
1999
+ operationKey: input.operationKey,
2000
+ canonicalRequestHash: input.canonicalRequestHash,
2001
+ ...(existingReceipts.length === 0
2002
+ ? { initialResult: { boundaryRequestHash: input.boundaryRequestHash } }
2003
+ : {}),
2004
+ });
2005
+ }
2006
+
2007
+ /** Serialize one actor-scoped prompt operation before replay or reservation. */
2008
+ export async function lockSessionPromptCommandOperation(
2009
+ db: Database,
2010
+ input: {
2011
+ workspaceId: string;
2012
+ actor: SessionCommandActor;
2013
+ operationKey: string;
2014
+ },
2015
+ ): Promise<void> {
2016
+ const actorIdentity =
2017
+ input.actor.type === "agent_attempt"
2018
+ ? `attempt:${input.actor.attemptId}`
2019
+ : `${input.actor.type}:${input.actor.subjectId}`;
2020
+ await db.execute(
2021
+ sql`select pg_advisory_xact_lock(hashtextextended(${`session-prompt:${input.workspaceId}:${actorIdentity}:${input.operationKey}`}, 0))`,
2022
+ );
2023
+ }
2024
+
2025
+ /** Return only a fully committed exact prompt receipt with the new boundary fingerprint. */
2026
+ export async function getCompletedSessionPromptCommandReceipt(
2027
+ db: Database,
2028
+ input: {
2029
+ workspaceId: string;
2030
+ actor: SessionCommandActor;
2031
+ action: SessionPromptCommandAction;
2032
+ targetSessionId: string;
2033
+ operationKey: string;
2034
+ boundaryRequestHash: string;
2035
+ },
2036
+ ): Promise<SessionCommandReceiptRow | null> {
2037
+ const receipt = fingerprintedPromptCommandReceipt(
2038
+ await findPromptCommandReceipts(db, {
2039
+ workspaceId: input.workspaceId,
2040
+ actor: input.actor,
2041
+ operationKey: input.operationKey,
2042
+ }),
2043
+ );
2044
+ if (!receipt) return null;
2045
+ assertPromptCommandReceiptIdentity(receipt, input);
2046
+ if (
2047
+ typeof receipt.result.boundaryRequestHash !== "string" ||
2048
+ receipt.appliedQueueVersion === null
2049
+ ) {
2050
+ return null;
2051
+ }
2052
+ return receipt;
2053
+ }
2054
+
1771
2055
  export async function reserveSessionCommandReceipt(
1772
2056
  db: Database,
1773
2057
  input: {
@@ -1780,6 +2064,7 @@ export async function reserveSessionCommandReceipt(
1780
2064
  operationKey: string;
1781
2065
  canonicalRequestHash: string;
1782
2066
  identityScope?: "actor" | "goal_operation";
2067
+ initialResult?: Record<string, unknown>;
1783
2068
  },
1784
2069
  ): Promise<{ receipt: SessionCommandReceiptRow; replay: boolean }> {
1785
2070
  if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
@@ -1810,6 +2095,7 @@ export async function reserveSessionCommandReceipt(
1810
2095
  targetTurnId: input.targetTurnId,
1811
2096
  operationKey: input.operationKey,
1812
2097
  canonicalRequestHash: input.canonicalRequestHash,
2098
+ result: input.initialResult ?? {},
1813
2099
  })
1814
2100
  .onConflictDoNothing()
1815
2101
  .returning();
@@ -3250,6 +3536,11 @@ export async function mutateSessionControlInTransaction(
3250
3536
  }
3251
3537
  }
3252
3538
 
3539
+ const targetSession = locks.sessions.find((session) => session.id === input.sessionId);
3540
+ if (!targetSession) {
3541
+ throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
3542
+ }
3543
+
3253
3544
  const revision = nextRevision(workspace);
3254
3545
  await advanceWorkspaceRevision(db, input.workspaceId, revision);
3255
3546
  const updatedRows = await db
@@ -3290,10 +3581,7 @@ export async function mutateSessionControlInTransaction(
3290
3581
  : eq(schema.sessions.id, input.sessionId),
3291
3582
  ),
3292
3583
  )
3293
- .returning({
3294
- id: schema.sessions.id,
3295
- lastSequence: schema.sessions.lastSequence,
3296
- });
3584
+ .returning({ id: schema.sessions.id });
3297
3585
  const updated = updatedRows.find((row) => row.id === input.sessionId);
3298
3586
  if (!updated) throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
3299
3587
 
@@ -3356,7 +3644,7 @@ export async function mutateSessionControlInTransaction(
3356
3644
  accountId: input.accountId,
3357
3645
  workspaceId: input.workspaceId,
3358
3646
  sessionId: input.sessionId,
3359
- sequence: updated.lastSequence + 1,
3647
+ sequence: targetSession.lastSequence + 1,
3360
3648
  type:
3361
3649
  input.action === "pause" || input.action === "cancel"
3362
3650
  ? "session.control.paused"
@@ -3380,7 +3668,7 @@ export async function mutateSessionControlInTransaction(
3380
3668
  throw new SessionControlInvariantError("Session control event was not inserted");
3381
3669
  await db
3382
3670
  .update(schema.sessions)
3383
- .set({ lastSequence: updated.lastSequence + 1, updatedAt: new Date() })
3671
+ .set({ lastSequence: targetSession.lastSequence + 1, updatedAt: new Date() })
3384
3672
  .where(eq(schema.sessions.id, input.sessionId));
3385
3673
  await db.insert(schema.auditEvents).values(
3386
3674
  withLosslessContentWriteVersion(