@opengeni/db 3.7.4 → 4.0.0-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 (113) hide show
  1. package/dist/automations.d.ts +4 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-integrations.d.ts +2 -0
  4. package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
  5. package/dist/chunk-2XEZK4LF.js.map +1 -0
  6. package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
  7. package/dist/chunk-3RI33YQN.js.map +1 -0
  8. package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
  9. package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
  10. package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
  11. package/dist/chunk-N5VXJ4I2.js.map +1 -0
  12. package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
  13. package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
  14. package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
  15. package/dist/chunk-QM6YOWHZ.js.map +1 -0
  16. package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
  17. package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
  18. package/dist/codemode-operations.d.ts +5 -0
  19. package/dist/codex-token-resolver.d.ts +6 -2
  20. package/dist/connection-token-resolver.d.ts +8 -0
  21. package/dist/editable-artifact-durable-export.js +2 -2
  22. package/dist/editable-artifacts.js +3 -3
  23. package/dist/generated-images.d.ts +15 -0
  24. package/dist/index.d.ts +393 -91
  25. package/dist/index.js +9992 -6069
  26. package/dist/index.js.map +1 -1
  27. package/dist/insights.d.ts +9 -0
  28. package/dist/managed-auth-session-sets.js +3 -3
  29. package/dist/managed-user-setup.d.ts +3 -0
  30. package/dist/mcp-oauth.d.ts +80 -0
  31. package/dist/migrate.d.ts +1 -0
  32. package/dist/migrate.js +3 -1
  33. package/dist/model-catalog.d.ts +153 -0
  34. package/dist/model-context-snapshots.d.ts +20 -0
  35. package/dist/new-session-drafts.d.ts +12 -0
  36. package/dist/organization-membership-lifecycle.d.ts +8 -1
  37. package/dist/organization-model-providers.d.ts +101 -0
  38. package/dist/pack-components.d.ts +1 -0
  39. package/dist/pr-review.d.ts +9 -0
  40. package/dist/provision-roles.js +1 -1
  41. package/dist/runtime-posture.d.ts +8 -8
  42. package/dist/schema.d.ts +7708 -5050
  43. package/dist/schema.js +25 -1
  44. package/dist/session-background-commands.d.ts +18 -6
  45. package/dist/session-background-commands.js +15 -9
  46. package/dist/session-control.d.ts +37 -2
  47. package/dist/session-goal-pacing.d.ts +3 -6
  48. package/dist/session-queue-commands.d.ts +17 -0
  49. package/dist/session-tenancy.js +3 -3
  50. package/dist/tool-gateway-approvals.d.ts +31 -0
  51. package/dist/video-generation.js +3 -3
  52. package/dist/workspace-artifacts.d.ts +17 -3
  53. package/dist/xai-subscription.d.ts +8 -0
  54. package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
  55. package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
  56. package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
  57. package/drizzle/0390_organization_model_provider_connections.sql +197 -0
  58. package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
  59. package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
  60. package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
  61. package/drizzle/0394_session_selected_skill_activation.sql +20 -0
  62. package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
  63. package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
  64. package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
  65. package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
  66. package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
  67. package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
  68. package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
  69. package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
  70. package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
  71. package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
  72. package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
  73. package/drizzle/0406_workspace_html_site_sources.sql +71 -0
  74. package/package.json +6 -6
  75. package/src/automations.ts +9 -1
  76. package/src/capability-integrations.ts +8 -0
  77. package/src/codemode-operations.ts +31 -21
  78. package/src/codex-token-resolver.ts +16 -6
  79. package/src/connection-token-resolver.ts +52 -3
  80. package/src/generated-images.ts +70 -6
  81. package/src/index.ts +3894 -994
  82. package/src/insights-model-bundle.ts +54 -5
  83. package/src/insights.ts +53 -7
  84. package/src/managed-user-setup.ts +14 -3
  85. package/src/mcp-oauth.ts +488 -0
  86. package/src/migrate.ts +9 -1
  87. package/src/model-catalog.ts +682 -0
  88. package/src/model-context-snapshots.ts +151 -0
  89. package/src/new-session-drafts.ts +66 -21
  90. package/src/organization-membership-lifecycle.ts +47 -5
  91. package/src/organization-model-providers.ts +614 -0
  92. package/src/pack-components.ts +15 -2
  93. package/src/pr-review.ts +19 -0
  94. package/src/provision-roles.ts +4 -0
  95. package/src/runtime-posture.ts +64 -0
  96. package/src/schema.ts +666 -61
  97. package/src/session-background-commands.ts +180 -78
  98. package/src/session-control.ts +181 -12
  99. package/src/session-goal-pacing.ts +4 -8
  100. package/src/session-queue-commands.ts +157 -82
  101. package/src/tool-gateway-approvals.ts +168 -0
  102. package/src/workspace-artifacts.ts +633 -178
  103. package/src/xai-subscription.ts +66 -14
  104. package/dist/chunk-4NAR7FBA.js.map +0 -1
  105. package/dist/chunk-6DPJA36C.js.map +0 -1
  106. package/dist/chunk-O56DM5GZ.js.map +0 -1
  107. package/dist/chunk-S3TDDCS2.js.map +0 -1
  108. /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
  109. /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
  110. /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
  111. /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
  112. /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
  113. /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
@@ -4,8 +4,8 @@ import type {
4
4
  } from "@opengeni/contracts";
5
5
  import { and, desc, eq, inArray, sql } from "drizzle-orm";
6
6
 
7
- import type { Database } from "./database";
8
- import { withRlsContext } from "./database";
7
+ import type { Database, SessionActivityDatabase } from "./database";
8
+ import { withRlsContext, withSessionActivityRlsContext } from "./database";
9
9
  import * as schema from "./schema";
10
10
 
11
11
  export type ConnectedMachineBackgroundCommandProof = {
@@ -15,6 +15,27 @@ export type ConnectedMachineBackgroundCommandProof = {
15
15
  observedAt: Date;
16
16
  };
17
17
 
18
+ export const SESSION_BACKGROUND_COMMAND_REASON_MAX_BYTES = 512;
19
+
20
+ export function boundedSessionBackgroundCommandReason(value: string, label: string): string {
21
+ const normalized = value.trim();
22
+ let bounded = "";
23
+ let bytes = 0;
24
+ for (const character of normalized) {
25
+ const characterBytes = Buffer.byteLength(character, "utf8");
26
+ if (bytes + characterBytes > SESSION_BACKGROUND_COMMAND_REASON_MAX_BYTES) break;
27
+ bounded += character;
28
+ bytes += characterBytes;
29
+ }
30
+ if (!bounded) throw new Error(`${label} must not be empty`);
31
+ return bounded;
32
+ }
33
+
34
+ export type SessionBackgroundCommandTerminalMutation = {
35
+ prepare: (tx: SessionActivityDatabase) => Promise<void>;
36
+ commit: (tx: SessionActivityDatabase, command: SessionBackgroundCommand) => Promise<void>;
37
+ };
38
+
18
39
  export type ConnectedMachineBackgroundCommandClaim = {
19
40
  commandId: string;
20
41
  accountId: string;
@@ -360,8 +381,10 @@ export async function recordConnectedMachineBackgroundCommandProof(
360
381
  proof: ConnectedMachineBackgroundCommandProof;
361
382
  },
362
383
  ): Promise<void> {
363
- const reason = input.proof.reason.trim().slice(0, 512);
364
- if (!reason) throw new Error("Connected command proof reason must not be empty");
384
+ const reason = boundedSessionBackgroundCommandReason(
385
+ input.proof.reason,
386
+ "Connected command proof reason",
387
+ );
365
388
  if (input.proof.outcome === "exited" && input.proof.exitCode === null) {
366
389
  throw new Error("Connected command exit proof requires an exit code");
367
390
  }
@@ -444,44 +467,54 @@ export async function deferConnectedMachineBackgroundCommandReconciliation(
444
467
  }
445
468
 
446
469
  /** Settle only from the proof already checkpointed under this exact claim. */
447
- export async function settleClaimedConnectedMachineBackgroundCommand(
470
+ export async function settleClaimedConnectedMachineBackgroundCommandWithMutation(
448
471
  db: Database,
449
472
  input: { claim: ConnectedMachineBackgroundCommandClaim },
473
+ mutateTerminal: SessionBackgroundCommandTerminalMutation,
450
474
  ): Promise<boolean> {
451
- return await withRlsContext(
475
+ return await withSessionActivityRlsContext(
452
476
  db,
453
477
  { accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
454
- async (scopedDb) => {
455
- const rows = await scopedDb.execute<{ id: string }>(sql`
456
- update ${schema.sessionBackgroundCommands} command set
457
- state = command.reconcile_proof_outcome,
458
- exit_code = case
459
- when command.reconcile_proof_outcome = 'exited'
460
- then command.reconcile_proof_exit_code
461
- else null
462
- end,
463
- settlement_reason = command.reconcile_proof_reason,
464
- settled_at = command.reconcile_proof_observed_at,
465
- reconcile_claim_id = null,
466
- reconcile_claimed_at = null,
467
- last_reconcile_outcome = 'settled_' || command.reconcile_proof_outcome,
468
- updated_at = clock_timestamp()
469
- where command.id = ${input.claim.commandId}
470
- and command.account_id = ${input.claim.accountId}
471
- and command.workspace_id = ${input.claim.workspaceId}
472
- and command.session_id = ${input.claim.sessionId}
473
- and command.provider = 'connected_machine'
474
- and command.control_workspace_id = ${input.claim.controlWorkspaceId}
475
- and command.enrollment_id = ${input.claim.enrollmentId}
476
- and command.connection_instance_id = ${input.claim.connectionInstanceId}
477
- and command.op_id = ${input.claim.opId}
478
- and command.reconcile_claim_id = ${input.claim.claimId}
479
- and command.state in ('running', 'stopping')
480
- and command.reconcile_proof_outcome in ('exited', 'lost')
481
- and command.reconcile_proof_observed_at is not null
482
- returning command.id
483
- `);
484
- return rows.length === 1;
478
+ async (tx) => {
479
+ await mutateTerminal.prepare(tx);
480
+ const rows = await tx.execute<{ id: string }>(sql`
481
+ update ${schema.sessionBackgroundCommands} command set
482
+ state = command.reconcile_proof_outcome,
483
+ exit_code = case
484
+ when command.reconcile_proof_outcome = 'exited'
485
+ then command.reconcile_proof_exit_code
486
+ else null
487
+ end,
488
+ settlement_reason = command.reconcile_proof_reason,
489
+ settled_at = command.reconcile_proof_observed_at,
490
+ reconcile_claim_id = null,
491
+ reconcile_claimed_at = null,
492
+ last_reconcile_outcome = 'settled_' || command.reconcile_proof_outcome,
493
+ updated_at = clock_timestamp()
494
+ where command.id = ${input.claim.commandId}
495
+ and command.account_id = ${input.claim.accountId}
496
+ and command.workspace_id = ${input.claim.workspaceId}
497
+ and command.session_id = ${input.claim.sessionId}
498
+ and command.provider = 'connected_machine'
499
+ and command.control_workspace_id = ${input.claim.controlWorkspaceId}
500
+ and command.enrollment_id = ${input.claim.enrollmentId}
501
+ and command.connection_instance_id = ${input.claim.connectionInstanceId}
502
+ and command.op_id = ${input.claim.opId}
503
+ and command.reconcile_claim_id = ${input.claim.claimId}
504
+ and command.state in ('running', 'stopping')
505
+ and command.reconcile_proof_outcome in ('exited', 'lost')
506
+ and command.reconcile_proof_observed_at is not null
507
+ returning command.id
508
+ `);
509
+ if (rows.length !== 1) return false;
510
+ const [row] = await tx
511
+ .select()
512
+ .from(schema.sessionBackgroundCommands)
513
+ .where(eq(schema.sessionBackgroundCommands.id, input.claim.commandId))
514
+ .limit(1);
515
+ if (!row) throw new Error("Settled Connected Machine command disappeared");
516
+ await mutateTerminal.commit(tx, mapCommand(row));
517
+ return true;
485
518
  },
486
519
  );
487
520
  }
@@ -652,8 +685,8 @@ export async function requestWorkspaceBackgroundCommandCancellationsInTransactio
652
685
  return Number(rows[0]?.commandCount ?? 0);
653
686
  }
654
687
 
655
- export async function settleSessionBackgroundCommandForRetainedProcess(
656
- db: Database,
688
+ export async function settleSessionBackgroundCommandForRetainedProcessInTransaction(
689
+ tx: SessionActivityDatabase,
657
690
  input: {
658
691
  accountId: string;
659
692
  workspaceId: string;
@@ -663,38 +696,76 @@ export async function settleSessionBackgroundCommandForRetainedProcess(
663
696
  exitCode: number | null;
664
697
  reason: string;
665
698
  },
699
+ mutateTerminal: SessionBackgroundCommandTerminalMutation,
666
700
  ): Promise<SessionBackgroundCommand | null> {
667
- return await withRlsContext(
668
- db,
669
- { accountId: input.accountId, workspaceId: input.workspaceId },
670
- async (scopedDb) => {
671
- const [row] = await scopedDb
672
- .update(schema.sessionBackgroundCommands)
673
- .set({
674
- state: input.outcome,
675
- exitCode: input.outcome === "exited" ? input.exitCode : null,
676
- settlementReason: input.reason.slice(0, 512),
677
- settledAt: new Date(),
678
- reconcileClaimId: null,
679
- reconcileClaimedAt: null,
680
- lastReconcileOutcome: `settled_${input.outcome}`,
681
- updatedAt: new Date(),
682
- })
683
- .where(
684
- and(
685
- eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
686
- eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
687
- eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
688
- inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
689
- ),
690
- )
691
- .returning();
692
- return row ? mapCommand(row) : null;
693
- },
701
+ const reason = boundedSessionBackgroundCommandReason(
702
+ input.reason,
703
+ "Managed command settlement reason",
694
704
  );
705
+ await mutateTerminal.prepare(tx);
706
+ const [updatedRow] = await tx
707
+ .update(schema.sessionBackgroundCommands)
708
+ .set({
709
+ state: input.outcome,
710
+ exitCode: input.outcome === "exited" ? input.exitCode : null,
711
+ settlementReason: reason,
712
+ settledAt: new Date(),
713
+ reconcileClaimId: null,
714
+ reconcileClaimedAt: null,
715
+ lastReconcileOutcome: `settled_${input.outcome}`,
716
+ updatedAt: new Date(),
717
+ })
718
+ .where(
719
+ and(
720
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
721
+ eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
722
+ eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
723
+ inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
724
+ ),
725
+ )
726
+ .returning();
727
+ let row = updatedRow;
728
+ if (!row) {
729
+ const [current] = await tx
730
+ .select()
731
+ .from(schema.sessionBackgroundCommands)
732
+ .where(
733
+ and(
734
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
735
+ eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
736
+ eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
737
+ ),
738
+ )
739
+ .for("update")
740
+ .limit(1);
741
+ if (!current) return null;
742
+ if (
743
+ current.state !== input.outcome ||
744
+ current.exitCode !== (input.outcome === "exited" ? input.exitCode : null)
745
+ ) {
746
+ throw new Error("Managed background command terminal state conflicts with process proof");
747
+ }
748
+ const [existingEvent] = await tx
749
+ .select({ id: schema.sessionEvents.id })
750
+ .from(schema.sessionEvents)
751
+ .where(
752
+ and(
753
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
754
+ eq(schema.sessionEvents.sessionId, input.sessionId),
755
+ eq(schema.sessionEvents.type, "session.command.finished"),
756
+ sql`${schema.sessionEvents.payload} ->> 'commandId' = ${current.id}`,
757
+ ),
758
+ )
759
+ .limit(1);
760
+ if (existingEvent) return mapCommand(current);
761
+ row = current;
762
+ }
763
+ const command = mapCommand(row);
764
+ await mutateTerminal.commit(tx, command);
765
+ return command;
695
766
  }
696
767
 
697
- export async function settleConnectedMachineSessionBackgroundCommand(
768
+ export async function settleConnectedMachineSessionBackgroundCommandWithMutation(
698
769
  db: Database,
699
770
  input: {
700
771
  accountId: string;
@@ -709,21 +780,25 @@ export async function settleConnectedMachineSessionBackgroundCommand(
709
780
  exitCode: number | null;
710
781
  reason: string;
711
782
  },
783
+ mutateTerminal: SessionBackgroundCommandTerminalMutation,
712
784
  ): Promise<SessionBackgroundCommand | null> {
713
- return await withRlsContext(
785
+ const reason = boundedSessionBackgroundCommandReason(
786
+ input.reason,
787
+ "Connected command settlement reason",
788
+ );
789
+ if (input.outcome === "exited" && input.exitCode === null) {
790
+ throw new Error("Connected command exit settlement requires an exit code");
791
+ }
792
+ if (input.outcome === "lost" && input.exitCode !== null) {
793
+ throw new Error("Connected command loss settlement cannot carry an exit code");
794
+ }
795
+ return await withSessionActivityRlsContext(
714
796
  db,
715
797
  { accountId: input.accountId, workspaceId: input.workspaceId },
716
- async (scopedDb) => {
798
+ async (tx) => {
799
+ await mutateTerminal.prepare(tx);
717
800
  const settledAt = new Date();
718
- const reason = input.reason.trim().slice(0, 512);
719
- if (!reason) throw new Error("Connected command settlement reason must not be empty");
720
- if (input.outcome === "exited" && input.exitCode === null) {
721
- throw new Error("Connected command exit settlement requires an exit code");
722
- }
723
- if (input.outcome === "lost" && input.exitCode !== null) {
724
- throw new Error("Connected command loss settlement cannot carry an exit code");
725
- }
726
- const [row] = await scopedDb
801
+ const [row] = await tx
727
802
  .update(schema.sessionBackgroundCommands)
728
803
  .set({
729
804
  state: input.outcome,
@@ -753,6 +828,33 @@ export async function settleConnectedMachineSessionBackgroundCommand(
753
828
  ),
754
829
  )
755
830
  .returning();
831
+ if (!row) return null;
832
+ const command = mapCommand(row);
833
+ await mutateTerminal.commit(tx, command);
834
+ return command;
835
+ },
836
+ );
837
+ }
838
+
839
+ export async function getSessionBackgroundCommand(
840
+ db: Database,
841
+ input: { accountId: string; workspaceId: string; sessionId: string; commandId: string },
842
+ ): Promise<SessionBackgroundCommand | null> {
843
+ return await withRlsContext(
844
+ db,
845
+ { accountId: input.accountId, workspaceId: input.workspaceId },
846
+ async (scopedDb) => {
847
+ const [row] = await scopedDb
848
+ .select()
849
+ .from(schema.sessionBackgroundCommands)
850
+ .where(
851
+ and(
852
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
853
+ eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
854
+ eq(schema.sessionBackgroundCommands.id, input.commandId),
855
+ ),
856
+ )
857
+ .limit(1);
756
858
  return row ? mapCommand(row) : null;
757
859
  },
758
860
  );
@@ -244,11 +244,11 @@ export async function assertAgentCommandAuthorityInTransaction(
244
244
  workspaceId: string;
245
245
  actor: Extract<SessionCommandActor, { type: "agent_attempt" }>;
246
246
  targetSessionId: string;
247
- action: "pause" | "resume" | "steer" | "message" | "goal";
247
+ action: "pause" | "resume" | "steer" | "message" | "goal" | "wait";
248
248
  },
249
249
  ): Promise<void> {
250
- if (input.action === "goal" && input.targetSessionId !== input.actor.sessionId) {
251
- throw new SessionControlInvariantError("An agent goal command must target its own session");
250
+ if (["goal", "wait"].includes(input.action) && input.targetSessionId !== input.actor.sessionId) {
251
+ throw new SessionControlInvariantError("An agent self command must target its own session");
252
252
  }
253
253
  // Every command caller establishes the control/workspace prefix first.
254
254
  // Reusing the event-write helper here keeps cross-session actor authority on
@@ -1855,13 +1855,13 @@ async function findCommandReceipt(
1855
1855
  targetSessionId: string | null;
1856
1856
  targetTurnId: string | null;
1857
1857
  operationKey: string;
1858
- identityScope: "actor" | "goal_operation";
1858
+ identityScope: "actor" | "target_operation";
1859
1859
  },
1860
1860
  ): Promise<SessionCommandReceiptRow | null> {
1861
1861
  const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
1862
1862
  const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
1863
1863
  const identity =
1864
- input.identityScope === "goal_operation"
1864
+ input.identityScope === "target_operation"
1865
1865
  ? and(
1866
1866
  eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
1867
1867
  eq(schema.sessionCommandReceipts.actorType, "agent_attempt"),
@@ -1884,6 +1884,174 @@ async function findCommandReceipt(
1884
1884
  return rows[0] ?? null;
1885
1885
  }
1886
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
+
1887
2055
  export async function reserveSessionCommandReceipt(
1888
2056
  db: Database,
1889
2057
  input: {
@@ -1895,20 +2063,21 @@ export async function reserveSessionCommandReceipt(
1895
2063
  targetTurnId: string | null;
1896
2064
  operationKey: string;
1897
2065
  canonicalRequestHash: string;
1898
- identityScope?: "actor" | "goal_operation";
2066
+ identityScope?: "actor" | "target_operation";
2067
+ initialResult?: Record<string, unknown>;
1899
2068
  },
1900
2069
  ): Promise<{ receipt: SessionCommandReceiptRow; replay: boolean }> {
1901
2070
  if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
1902
2071
  const identityScope = input.identityScope ?? "actor";
1903
2072
  if (
1904
- identityScope === "goal_operation" &&
2073
+ identityScope === "target_operation" &&
1905
2074
  (input.actor.type !== "agent_attempt" ||
1906
- !["goal.update", "goal.progress", "goal.wait"].includes(input.action) ||
2075
+ !["goal.update", "goal.progress", "session.wait_for_input"].includes(input.action) ||
1907
2076
  input.targetSessionId === null ||
1908
2077
  input.targetTurnId !== null)
1909
2078
  ) {
1910
2079
  throw new SessionControlInvariantError(
1911
- "Target-scoped receipt identity is reserved for agent goal commands",
2080
+ "Target-scoped receipt identity is reserved for agent self commands",
1912
2081
  );
1913
2082
  }
1914
2083
  const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
@@ -1926,6 +2095,7 @@ export async function reserveSessionCommandReceipt(
1926
2095
  targetTurnId: input.targetTurnId,
1927
2096
  operationKey: input.operationKey,
1928
2097
  canonicalRequestHash: input.canonicalRequestHash,
2098
+ result: input.initialResult ?? {},
1929
2099
  })
1930
2100
  .onConflictDoNothing()
1931
2101
  .returning();
@@ -1990,13 +2160,12 @@ async function registerContinuableWakes(
1990
2160
  )
1991
2161
  ), upserted as (
1992
2162
  insert into ${schema.sessionWorkflowWakeOutbox} (
1993
- session_id, account_id, workspace_id, temporal_workflow_id, reason, control_revision
2163
+ session_id, account_id, workspace_id, temporal_workflow_id, reason
1994
2164
  )
1995
- select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}, 1
2165
+ select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
1996
2166
  from eligible
1997
2167
  on conflict (session_id) do update set
1998
2168
  wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
1999
- control_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
2000
2169
  temporal_workflow_id = excluded.temporal_workflow_id,
2001
2170
  reason = excluded.reason,
2002
2171
  attempts = 0,
@@ -2,14 +2,10 @@ import { and, eq } from "drizzle-orm";
2
2
  import type { Database } from "./database";
3
3
  import * as schema from "./schema";
4
4
 
5
- /** Column reset shared by every goal head mutation that retires a `goal_wait` hold. */
6
- export const SESSION_GOAL_HOLD_CLEARED = {
7
- continuationHoldTurnId: null,
8
- continuationHoldUntil: null,
9
- continuationHoldReason: null,
10
- continuationHoldSetAt: null,
5
+ /** Clear a terminal unchanged-input continuation fence after newer goal truth. */
6
+ export const SESSION_GOAL_SUPPRESSION_CLEARED = {
7
+ continuationSuppressedTurnId: null,
11
8
  } as const;
12
-
13
9
  /** The pause reason written when a per-goal or deployment continuation ceiling is reached. */
14
10
  export const SESSION_GOAL_CAP_PAUSED_REASON = "max_auto_continuations";
15
11
 
@@ -103,7 +99,7 @@ export async function autoResumeGoalPausedByCapInTransaction(
103
99
  version: existing.version + 1,
104
100
  continuationWakeRevision: existing.continuationWakeRevision + 1,
105
101
  ...SESSION_GOAL_CONTINUATION_EPOCH_RESET,
106
- ...SESSION_GOAL_HOLD_CLEARED,
102
+ ...SESSION_GOAL_SUPPRESSION_CLEARED,
107
103
  updatedAt: input.now,
108
104
  })
109
105
  .where(eq(schema.sessionGoals.id, existing.id))