@opengeni/core 2.6.3-canary.0 → 2.6.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.
@@ -2,10 +2,11 @@ import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
2
2
  import {
3
3
  canonicalizeConfiguredModelId,
4
4
  configuredAllowedModels,
5
+ WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
6
+ WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
5
7
  resolveFirstPartyMcpToolPolicy,
6
8
  policyProviderIdForModel,
7
9
  resolveTurnExecutionPolicyV1,
8
- WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
9
10
  XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
10
11
  type Settings,
11
12
  } from "@opengeni/config";
@@ -23,6 +24,8 @@ import {
23
24
  evaluateWorkspaceModelPolicy,
24
25
  normalizeAutomaticSessionTitle,
25
26
  resolveWorkspaceSessionToolDefaults,
27
+ metadataWithTurnExecutionPolicyV1,
28
+ readTurnExecutionPolicyV1,
26
29
  stableJson,
27
30
  type AccessGrant,
28
31
  type ComposerDraft,
@@ -62,6 +65,7 @@ import {
62
65
  import {
63
66
  createSession,
64
67
  createSessionWithIdempotencyKeyResult,
68
+ canonicalSessionCommandHash,
65
69
  encryptVariableSetValue,
66
70
  getAnySessionInGroup,
67
71
  getEnrollment,
@@ -72,10 +76,10 @@ import {
72
76
  listDistinctRigVersionIdsInGroup,
73
77
  getSandbox,
74
78
  getSession,
79
+ getInitializedSessionCreateReplay,
75
80
  getSessionAuthorityProjection,
76
81
  SessionIdConflictError,
77
82
  NewSessionDraftConflictError,
78
- getSessionSpawnDenialByIdempotencyKey,
79
83
  getWorkspaceControlEvent,
80
84
  getSessionLineage,
81
85
  getSessionTurn,
@@ -87,7 +91,10 @@ import {
87
91
  initializeSessionStartAtomically,
88
92
  listSessionTurns,
89
93
  listSessionMcpServersForChildInheritance,
94
+ lockActiveWorkspaceGatewayCustomModelForAdmission,
95
+ lockActiveWorkspaceOpenRouterCustomModelForAdmission,
90
96
  requireSession,
97
+ replaySubmittedHumanPromptFromBoundaryReceipt,
91
98
  submitHumanPromptInTransaction,
92
99
  appendSessionEventsWithLockedSessionUpdate,
93
100
  updateSessionTitleWithEvent,
@@ -129,6 +136,11 @@ import {
129
136
  } from "../session-authorization";
130
137
  import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
131
138
  import { managedSessionGroupBackend } from "../sandbox/runtime-settings";
139
+ import {
140
+ isWorkspaceCustomModelId,
141
+ resolveWorkspaceCatalogSettings,
142
+ workspaceCustomModelReference,
143
+ } from "../model-catalog";
132
144
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
133
145
  import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
134
146
  import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
@@ -656,6 +668,16 @@ export async function createAndStartSessionWithOutcome(input: {
656
668
  /** Internal database-only composition seam. The exact session shell and this
657
669
  * linkage commit together before its first event/turn can be initialized. */
658
670
  beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
671
+ /** The custom workspace model was frozen by an earlier accepted boundary or
672
+ * inherited from an existing session, so retirement must not invalidate it. */
673
+ retainWorkspaceGatewayModel?: boolean;
674
+ /** Provider-neutral successor to retainWorkspaceGatewayModel. */
675
+ retainWorkspaceCustomModel?: boolean;
676
+ /** The selected workspace Gateway product is backed by a mutable custom row,
677
+ * rather than deployment-curated Gateway membership. */
678
+ workspaceGatewayCustomModel?: boolean;
679
+ /** Provider-neutral successor to workspaceGatewayCustomModel. */
680
+ workspaceCustomModel?: boolean;
659
681
  accountId: string;
660
682
  workspaceId: string;
661
683
  visibility?: "user_private" | "workspace_shared";
@@ -775,16 +797,56 @@ export async function createAndStartSessionWithOutcome(input: {
775
797
  allowNestedAgentDepthIncrease?: boolean;
776
798
  subjectId?: string | null;
777
799
  }): Promise<CreateSessionOutcome> {
778
- const sessionMetadata = {
779
- ...input.metadata,
780
- model: input.model,
781
- reasoningEffort: input.reasoningEffort,
782
- ...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
783
- };
800
+ const sessionMetadata = metadataWithTurnExecutionPolicyV1(
801
+ {
802
+ ...input.metadata,
803
+ model: input.model,
804
+ reasoningEffort: input.reasoningEffort,
805
+ ...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
806
+ },
807
+ input.turnExecutionPolicy,
808
+ );
784
809
  const frozenCreatedByContext = freezeAgentChildAutomaticTitleInCreatorContext(
785
810
  input.createdByContext,
786
811
  input.initialAutomaticTitle,
787
812
  );
813
+ const requiresActiveWorkspaceCustomModel =
814
+ (input.workspaceCustomModel === true || input.workspaceGatewayCustomModel === true) &&
815
+ input.retainWorkspaceCustomModel !== true &&
816
+ input.retainWorkspaceGatewayModel !== true;
817
+ const beforeCreateCommit =
818
+ requiresActiveWorkspaceCustomModel || input.beforeCreateCommit
819
+ ? async (tx: Database, sessionId: string, context?: { created: boolean }): Promise<void> => {
820
+ // A committed keyed replay already crossed this fence when its shell
821
+ // was first accepted. Revalidate only the transaction inserting a new
822
+ // session, while still running caller linkage on every replay.
823
+ if (requiresActiveWorkspaceCustomModel && context?.created !== false) {
824
+ const openRouter = input.model.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX);
825
+ const upstreamModelId = input.model.slice(
826
+ openRouter
827
+ ? WORKSPACE_OPENROUTER_MODEL_ID_PREFIX.length
828
+ : WORKSPACE_GATEWAY_MODEL_ID_PREFIX.length,
829
+ );
830
+ const active = openRouter
831
+ ? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
832
+ accountId: input.accountId,
833
+ workspaceId: input.workspaceId,
834
+ upstreamModelId,
835
+ })
836
+ : await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
837
+ accountId: input.accountId,
838
+ workspaceId: input.workspaceId,
839
+ upstreamModelId,
840
+ });
841
+ if (!active) {
842
+ throw new HTTPException(422, {
843
+ message: `model is not available: ${input.model}`,
844
+ });
845
+ }
846
+ }
847
+ await input.beforeCreateCommit?.(tx, sessionId);
848
+ }
849
+ : undefined;
788
850
  // Keyed creation is intentionally handled only by the database admission
789
851
  // transaction below. Its workspace/key lock replays either the successful
790
852
  // session or the committed denial atomically; an application-side lookup
@@ -834,15 +896,29 @@ export async function createAndStartSessionWithOutcome(input: {
834
896
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
835
897
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
836
898
  subjectId: input.subjectId ?? null,
837
- ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
899
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
838
900
  });
839
901
  if (keyedResult.denied) {
840
902
  throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
841
903
  }
842
904
  const { session: keyed, created } = keyedResult;
843
905
  if (!created) {
906
+ const persistedPolicy = readTurnExecutionPolicyV1(keyed.metadata);
844
907
  const finished = await finishStartSession(
845
- keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
908
+ keyed.temporalWorkflowId
909
+ ? {
910
+ ...input,
911
+ seedTargetSandbox: null,
912
+ ...(persistedPolicy.kind === "valid"
913
+ ? { turnExecutionPolicy: persistedPolicy.policy }
914
+ : {}),
915
+ }
916
+ : {
917
+ ...input,
918
+ ...(persistedPolicy.kind === "valid"
919
+ ? { turnExecutionPolicy: persistedPolicy.policy }
920
+ : {}),
921
+ },
846
922
  keyed,
847
923
  );
848
924
  return {
@@ -905,7 +981,7 @@ export async function createAndStartSessionWithOutcome(input: {
905
981
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
906
982
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
907
983
  subjectId: input.subjectId ?? null,
908
- ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
984
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
909
985
  });
910
986
  } catch (error) {
911
987
  if (error instanceof SessionSpawnDeniedDbError) {
@@ -1137,9 +1213,6 @@ export function canonicalConfiguredModel(
1137
1213
  ) {
1138
1214
  return canonicalModel;
1139
1215
  }
1140
- if (canonicalModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
1141
- return canonicalModel;
1142
- }
1143
1216
  throw new HTTPException(422, { message: `model is not available: ${model}` });
1144
1217
  }
1145
1218
 
@@ -1245,7 +1318,17 @@ export async function requireQueuedTurnForApi(
1245
1318
  * `session_send_message` tool so the two surfaces cannot drift. Callers own
1246
1319
  * resource/tool validation and the per-message usage limit before calling.
1247
1320
  */
1248
- export async function postUserMessageTurn(input: {
1321
+ type PostUserMessageTurnResult = {
1322
+ accepted: SessionEvent;
1323
+ turn: SessionTurn;
1324
+ draft: ComposerDraft | null;
1325
+ receipt: SessionCommandReceipt;
1326
+ routing: SessionPromptRouting;
1327
+ interruptionCount: number;
1328
+ replay: boolean;
1329
+ };
1330
+
1331
+ type PostUserMessageTurnInput = {
1249
1332
  db: Database;
1250
1333
  bus: EventBus;
1251
1334
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
@@ -1273,28 +1356,143 @@ export async function postUserMessageTurn(input: {
1273
1356
  commandActor?: SessionCommandActor;
1274
1357
  controlEtag?: string | null;
1275
1358
  expectedDraftRevision?: number | null;
1359
+ boundaryRequestHash?: string;
1276
1360
  reasoningEffortFallback?: Settings["openaiReasoningEffort"];
1277
1361
  turnExecutionPolicy: TurnExecutionPolicyV1;
1278
1362
  recordAgentRunUsage?: boolean;
1279
1363
  schedulePostCommit?: (task: () => Promise<void>) => void;
1280
- }): Promise<{
1281
- accepted: SessionEvent;
1282
- turn: SessionTurn;
1283
- draft: ComposerDraft | null;
1284
- receipt: SessionCommandReceipt;
1285
- routing: SessionPromptRouting;
1286
- interruptionCount: number;
1287
- replay: boolean;
1288
- }> {
1289
- const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
1364
+ };
1365
+
1366
+ function finalizePostUserMessageTurn(
1367
+ input: Pick<
1368
+ PostUserMessageTurnInput,
1369
+ | "db"
1370
+ | "bus"
1371
+ | "workflowClient"
1372
+ | "accountId"
1373
+ | "workspaceId"
1374
+ | "sessionId"
1375
+ | "delivery"
1376
+ | "schedulePostCommit"
1377
+ >,
1378
+ result: Awaited<ReturnType<typeof submitHumanPromptInTransaction>>,
1379
+ ): PostUserMessageTurnResult {
1380
+ const { db, bus, workflowClient, accountId, workspaceId, sessionId } = input;
1381
+ const postCommitTask = async () => {
1382
+ await Promise.all([
1383
+ (async () => {
1384
+ try {
1385
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1386
+ if (result.workspaceControlEventId) {
1387
+ const controlEvent = await getWorkspaceControlEvent(
1388
+ db,
1389
+ workspaceId,
1390
+ result.workspaceControlEventId,
1391
+ );
1392
+ if (!controlEvent) {
1393
+ throw new Error(
1394
+ `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1395
+ );
1396
+ }
1397
+ await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1398
+ }
1399
+ } catch {
1400
+ console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1401
+ errorClass: "PromptEventFanoutOperationError",
1402
+ errorCode: "session_prompt_event_fanout_failed",
1403
+ origin: "core",
1404
+ });
1405
+ }
1406
+ })(),
1407
+ (async () => {
1408
+ try {
1409
+ await workflowClient.wakeSessionWorkflow({
1410
+ accountId,
1411
+ workspaceId,
1412
+ sessionId,
1413
+ workflowId: result.turn.temporalWorkflowId,
1414
+ wakeRevision: result.wakeRevision,
1415
+ ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1416
+ ? { interruptionRequested: true }
1417
+ : {}),
1418
+ });
1419
+ } catch {
1420
+ console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1421
+ errorClass: "WorkflowWakeOperationError",
1422
+ errorCode: "session_workflow_wake_failed",
1423
+ origin: "core",
1424
+ });
1425
+ }
1426
+ })(),
1427
+ ]);
1428
+ };
1429
+ const schedulePostCommit =
1430
+ input.schedulePostCommit ??
1431
+ ((task: () => Promise<void>) => {
1432
+ void task();
1433
+ });
1434
+ try {
1435
+ schedulePostCommit(postCommitTask);
1436
+ } catch {
1437
+ console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
1438
+ errorClass: "PromptPostCommitScheduleError",
1439
+ errorCode: "session_prompt_post_commit_schedule_failed",
1440
+ origin: "core",
1441
+ });
1442
+ }
1443
+ return {
1444
+ accepted: result.accepted,
1445
+ turn: result.turn,
1446
+ receipt: {
1447
+ id: result.receipt.id,
1448
+ action: result.receipt.action,
1449
+ operationKey: result.receipt.operationKey,
1450
+ targetSessionId: result.receipt.targetSessionId,
1451
+ targetTurnId: result.receipt.targetTurnId,
1452
+ appliedControlRevision: result.receipt.appliedControlRevision,
1453
+ appliedQueueVersion: result.receipt.appliedQueueVersion,
1454
+ appliedTurnVersion: result.receipt.appliedTurnVersion,
1455
+ appliedDraftRevision: result.receipt.appliedDraftRevision,
1456
+ createdAt: result.receipt.createdAt.toISOString(),
1457
+ },
1458
+ routing: result.routing,
1459
+ draft: result.draft
1460
+ ? {
1461
+ revision: result.draft.revision,
1462
+ text: result.draft.text,
1463
+ annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
1464
+ resources: result.draft.resources as ResourceRef[],
1465
+ model: result.draft.model,
1466
+ reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
1467
+ latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
1468
+ sourceTurnId: result.draft.sourceTurnId,
1469
+ sourceTurnVersion: result.draft.sourceTurnVersion,
1470
+ updatedAt: result.draft.updatedAt.toISOString(),
1471
+ }
1472
+ : null,
1473
+ interruptionCount: result.interruptionCount,
1474
+ replay: result.replay,
1475
+ };
1476
+ }
1477
+
1478
+ export async function postUserMessageTurn(
1479
+ input: PostUserMessageTurnInput,
1480
+ ): Promise<PostUserMessageTurnResult> {
1481
+ const { db, settings, accountId, workspaceId, sessionId } = input;
1290
1482
  const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
1291
1483
  const requestedReasoningEffort = input.reasoningEffort ?? null;
1292
1484
  // Reject an explicit per-message model the host does not expose; an omitted
1293
1485
  // model inherits the session's model downstream (always a configured id).
1294
1486
  assertConfiguredModel(settings, requestedModel);
1295
- await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
1296
1487
  const sessionForModelGate = await requireSession(db, workspaceId, sessionId);
1297
1488
  const effectiveModelForGate = requestedModel ?? sessionForModelGate.model;
1489
+ const freshWorkspaceCustomModel =
1490
+ requestedModel !== null &&
1491
+ isWorkspaceCustomModelId(settings, requestedModel) &&
1492
+ requestedModel !== sessionForModelGate.model
1493
+ ? requestedModel
1494
+ : null;
1495
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, effectiveModelForGate);
1298
1496
  try {
1299
1497
  assertSessionAllowsProductModel(sessionForModelGate, effectiveModelForGate);
1300
1498
  } catch (error) {
@@ -1329,6 +1527,9 @@ export async function postUserMessageTurn(input: {
1329
1527
  subjectId: input.actor ?? accountId,
1330
1528
  },
1331
1529
  operationKey,
1530
+ ...(input.boundaryRequestHash
1531
+ ? { boundaryRequestHash: input.boundaryRequestHash }
1532
+ : {}),
1332
1533
  delivery: input.delivery ?? "send",
1333
1534
  controlEtag: input.controlEtag ?? null,
1334
1535
  expectedDraftRevision: input.expectedDraftRevision ?? null,
@@ -1356,6 +1557,36 @@ export async function postUserMessageTurn(input: {
1356
1557
  }
1357
1558
  : {}),
1358
1559
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1560
+ ...(freshWorkspaceCustomModel
1561
+ ? {
1562
+ beforeFreshPromptCommit: async (tx: Database): Promise<void> => {
1563
+ const reference = workspaceCustomModelReference(
1564
+ settings,
1565
+ freshWorkspaceCustomModel,
1566
+ );
1567
+ if (!reference) {
1568
+ throw new Error("workspace custom model reference disappeared");
1569
+ }
1570
+ const active =
1571
+ reference.providerKind === "openrouter"
1572
+ ? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
1573
+ accountId,
1574
+ workspaceId,
1575
+ upstreamModelId: reference.upstreamModelId,
1576
+ })
1577
+ : await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
1578
+ accountId,
1579
+ workspaceId,
1580
+ upstreamModelId: reference.upstreamModelId,
1581
+ });
1582
+ if (!active) {
1583
+ throw new HTTPException(422, {
1584
+ message: `model is not available: ${freshWorkspaceCustomModel}`,
1585
+ });
1586
+ }
1587
+ },
1588
+ }
1589
+ : {}),
1359
1590
  controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
1360
1591
  }),
1361
1592
  ),
@@ -1386,101 +1617,7 @@ export async function postUserMessageTurn(input: {
1386
1617
  }
1387
1618
  throw error;
1388
1619
  }
1389
- const postCommitTask = async () => {
1390
- await Promise.all([
1391
- (async () => {
1392
- try {
1393
- await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1394
- if (result.workspaceControlEventId) {
1395
- const controlEvent = await getWorkspaceControlEvent(
1396
- db,
1397
- workspaceId,
1398
- result.workspaceControlEventId,
1399
- );
1400
- if (!controlEvent) {
1401
- throw new Error(
1402
- `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1403
- );
1404
- }
1405
- await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1406
- }
1407
- } catch {
1408
- console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1409
- errorClass: "PromptEventFanoutOperationError",
1410
- errorCode: "session_prompt_event_fanout_failed",
1411
- origin: "core",
1412
- });
1413
- }
1414
- })(),
1415
- (async () => {
1416
- try {
1417
- await workflowClient.wakeSessionWorkflow({
1418
- accountId,
1419
- workspaceId,
1420
- sessionId,
1421
- workflowId: result.turn.temporalWorkflowId,
1422
- wakeRevision: result.wakeRevision,
1423
- ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1424
- ? { interruptionRequested: true }
1425
- : {}),
1426
- });
1427
- } catch {
1428
- console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1429
- errorClass: "WorkflowWakeOperationError",
1430
- errorCode: "session_workflow_wake_failed",
1431
- origin: "core",
1432
- });
1433
- }
1434
- })(),
1435
- ]);
1436
- };
1437
- const schedulePostCommit =
1438
- input.schedulePostCommit ??
1439
- ((task: () => Promise<void>) => {
1440
- void task();
1441
- });
1442
- try {
1443
- schedulePostCommit(postCommitTask);
1444
- } catch {
1445
- console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
1446
- errorClass: "PromptPostCommitScheduleError",
1447
- errorCode: "session_prompt_post_commit_schedule_failed",
1448
- origin: "core",
1449
- });
1450
- }
1451
- return {
1452
- accepted: result.accepted,
1453
- turn: result.turn,
1454
- receipt: {
1455
- id: result.receipt.id,
1456
- action: result.receipt.action,
1457
- operationKey: result.receipt.operationKey,
1458
- targetSessionId: result.receipt.targetSessionId,
1459
- targetTurnId: result.receipt.targetTurnId,
1460
- appliedControlRevision: result.receipt.appliedControlRevision,
1461
- appliedQueueVersion: result.receipt.appliedQueueVersion,
1462
- appliedTurnVersion: result.receipt.appliedTurnVersion,
1463
- appliedDraftRevision: result.receipt.appliedDraftRevision,
1464
- createdAt: result.receipt.createdAt.toISOString(),
1465
- },
1466
- routing: result.routing,
1467
- draft: result.draft
1468
- ? {
1469
- revision: result.draft.revision,
1470
- text: result.draft.text,
1471
- annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
1472
- resources: result.draft.resources as ResourceRef[],
1473
- model: result.draft.model,
1474
- reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
1475
- latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
1476
- sourceTurnId: result.draft.sourceTurnId,
1477
- sourceTurnVersion: result.draft.sourceTurnVersion,
1478
- updatedAt: result.draft.updatedAt.toISOString(),
1479
- }
1480
- : null,
1481
- interruptionCount: result.interruptionCount,
1482
- replay: result.replay,
1483
- };
1620
+ return finalizePostUserMessageTurn(input, result);
1484
1621
  }
1485
1622
 
1486
1623
  /**
@@ -1532,16 +1669,92 @@ export function resolveSessionCreateVisibility(input: {
1532
1669
  return input.requestedVisibility === "private" ? "user_private" : "workspace_shared";
1533
1670
  }
1534
1671
 
1672
+ async function resolveWorkspaceModelBoundarySettings(
1673
+ deps: Pick<ApiRouteDeps, "db" | "settings" | "catalogSourceSettings">,
1674
+ grant: AccessGrant,
1675
+ workspaceId: string,
1676
+ modelIds: readonly (string | null | undefined)[],
1677
+ retainedProductModelId?: string | null,
1678
+ ): Promise<Settings> {
1679
+ const retainedWorkspaceModel =
1680
+ retainedProductModelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
1681
+ retainedProductModelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) === true;
1682
+ if (deps.catalogSourceSettings) {
1683
+ // The adapter already resolved one exact workspace catalog snapshot for
1684
+ // this request. Preserve it for fresh selections, but an existing session
1685
+ // may name a retired custom model that the active-only adapter snapshot
1686
+ // intentionally omitted. Re-open only the unoverlaid source for that
1687
+ // retention lookup; never feed the synthetic workspace provider back
1688
+ // through deployment validation.
1689
+ if (!retainedWorkspaceModel) return deps.settings;
1690
+ }
1691
+ const workspaceModelIds = modelIds.filter(
1692
+ (modelId): modelId is string =>
1693
+ typeof modelId === "string" &&
1694
+ (modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) ||
1695
+ modelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)),
1696
+ );
1697
+ const needsWorkspaceResolution =
1698
+ deps.settings.modelCatalogSource === "database" || workspaceModelIds.length > 0;
1699
+ if (!needsWorkspaceResolution) return deps.settings;
1700
+ return (
1701
+ await resolveWorkspaceCatalogSettings(deps.db, deps.catalogSourceSettings ?? deps.settings, {
1702
+ accountId: grant.accountId,
1703
+ workspaceId,
1704
+ ...(retainedProductModelId !== undefined ? { retainedProductModelId } : {}),
1705
+ })
1706
+ ).settings;
1707
+ }
1708
+
1709
+ async function withSessionCreateUsageRecording(input: {
1710
+ deps: ApiRouteDeps;
1711
+ grant: AccessGrant;
1712
+ workspaceId: string;
1713
+ startMode: "realtime" | undefined;
1714
+ origin: "system" | "user";
1715
+ createOutcome: CreateSessionOutcome;
1716
+ }): Promise<CreateSessionRequestOutcome> {
1717
+ let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
1718
+ if (input.startMode !== "realtime") {
1719
+ try {
1720
+ await recordWorkspaceUsage(input.deps, {
1721
+ accountId: input.grant.accountId,
1722
+ workspaceId: input.workspaceId,
1723
+ subjectId: input.grant.subjectId,
1724
+ eventType: "agent_run.created",
1725
+ quantity: 1,
1726
+ unit: "run",
1727
+ sourceResourceType: "session",
1728
+ sourceResourceId: input.createOutcome.session.id,
1729
+ sessionId: input.createOutcome.session.id,
1730
+ initiator: input.createOutcome.session.createdBy,
1731
+ initiatorContext: input.createOutcome.session.createdByContext,
1732
+ origin: input.origin,
1733
+ idempotencyKey: `agent_run.created:${input.workspaceId}:${input.createOutcome.session.id}`,
1734
+ });
1735
+ } catch (error) {
1736
+ usageRecording = "failed";
1737
+ reportSessionUsageRecordingFailure(error);
1738
+ }
1739
+ }
1740
+ return { ...input.createOutcome, usageRecording };
1741
+ }
1742
+
1535
1743
  export async function createSessionForRequestWithOutcome(
1536
- deps: ApiRouteDeps,
1744
+ unresolvedDeps: ApiRouteDeps,
1537
1745
  grant: AccessGrant,
1538
1746
  workspaceId: string,
1539
1747
  rawPayload: unknown,
1540
1748
  authorization?: AccessGrantAuthorization,
1541
1749
  agentChildPresentation?: AgentChildSessionCreatePresentation,
1542
1750
  ): Promise<CreateSessionRequestOutcome> {
1543
- const { settings, db, bus, workflowClient, objectStorage } = deps;
1544
1751
  const payload = CreateSessionRequest.parse(rawPayload);
1752
+ if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
1753
+ throw new HTTPException(422, {
1754
+ message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
1755
+ });
1756
+ }
1757
+ const db = unresolvedDeps.db;
1545
1758
  const visibilityProvided = hasOwnProperty(rawPayload, "visibility");
1546
1759
  if (payload.visibility === "private" && !grant.metadata?.["sessionId"]) {
1547
1760
  if (!authorization) {
@@ -1549,39 +1762,13 @@ export async function createSessionForRequestWithOutcome(
1549
1762
  message: "managed human session required",
1550
1763
  });
1551
1764
  }
1552
- await requireManagedHumanPrivateSessionCreate(deps, authorization, workspaceId);
1765
+ await requireManagedHumanPrivateSessionCreate(unresolvedDeps, authorization, workspaceId);
1553
1766
  if (payload.sandbox === "shared" || typeof payload.sandbox === "object") {
1554
1767
  throw new HTTPException(422, {
1555
1768
  message: "Only-me sessions require their own sandbox",
1556
1769
  });
1557
1770
  }
1558
1771
  }
1559
- if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
1560
- throw new HTTPException(422, {
1561
- message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
1562
- });
1563
- }
1564
- // A committed keyed denial is the idempotent outcome even if mutable
1565
- // resources, policy, authorization, or budget have changed since the first
1566
- // attempt. Replay it before any of those checks, just as a keyed successful
1567
- // session is returned rather than recreated later in createAndStartSession.
1568
- if (payload.idempotencyKey) {
1569
- const denial = await getSessionSpawnDenialByIdempotencyKey(
1570
- db,
1571
- workspaceId,
1572
- payload.idempotencyKey,
1573
- );
1574
- if (denial) {
1575
- throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
1576
- }
1577
- }
1578
- await requireAtomicPersonalResourceAttachment(
1579
- deps,
1580
- authorization,
1581
- workspaceId,
1582
- payload.personalResourceAttachment,
1583
- false,
1584
- );
1585
1772
  // Parent linkage and execution-context inheritance come ONLY from the
1586
1773
  // worker-signed sessionId claim. A caller cannot nominate a parent in the
1587
1774
  // payload, so inheriting an existing repository/tool/credential snapshot does
@@ -1592,7 +1779,7 @@ export async function createSessionForRequestWithOutcome(
1592
1779
  : null;
1593
1780
  if (parentSessionId) {
1594
1781
  try {
1595
- await requireSessionAuthorization(deps, grant, {
1782
+ await requireSessionAuthorization(unresolvedDeps, grant, {
1596
1783
  sessionId: parentSessionId,
1597
1784
  operation: "session.child.create",
1598
1785
  surface: "core",
@@ -1650,6 +1837,117 @@ export async function createSessionForRequestWithOutcome(
1650
1837
  message: "caller attempt does not belong to the parent session",
1651
1838
  });
1652
1839
  }
1840
+ const replayManagedHumanSubjectId = creationInitiator.actor
1841
+ ? (parentCallingTurn?.initiatingHumanSubjectId ??
1842
+ (parentCallingTurn?.initiator.kind === "subject"
1843
+ ? parentCallingTurn.initiator.subjectId
1844
+ : null))
1845
+ : authorization?.canonicalManagedHumanSession || grant.principalKind === "human_session"
1846
+ ? grant.subjectId
1847
+ : null;
1848
+ let retainedKeyedShellModel: string | null = null;
1849
+ if (
1850
+ payload.idempotencyKey &&
1851
+ (effectiveVisibility !== "user_private" || replayManagedHumanSubjectId !== null)
1852
+ ) {
1853
+ try {
1854
+ const initializedReplay = await getInitializedSessionCreateReplay(db, {
1855
+ accountId: grant.accountId,
1856
+ workspaceId,
1857
+ subjectId: replayManagedHumanSubjectId ?? grant.subjectId,
1858
+ ...(replayManagedHumanSubjectId
1859
+ ? { activeManagedHumanSubjectId: replayManagedHumanSubjectId }
1860
+ : {}),
1861
+ createIdempotencyKey: payload.idempotencyKey,
1862
+ ...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
1863
+ visibility: effectiveVisibility,
1864
+ variableSetIds: payload.variableSetIds ?? [],
1865
+ initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
1866
+ deferInitialTurn: payload.startMode === "realtime",
1867
+ });
1868
+ if (initializedReplay) {
1869
+ if (initializedReplay.outcome === "denied") {
1870
+ throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(initializedReplay.denial));
1871
+ }
1872
+ if (initializedReplay.outcome === "pending") {
1873
+ retainedKeyedShellModel = initializedReplay.session.model;
1874
+ } else {
1875
+ if (initializedReplay.workflowWakeRevision !== null) {
1876
+ await unresolvedDeps.workflowClient.wakeSessionWorkflow({
1877
+ accountId: grant.accountId,
1878
+ workspaceId,
1879
+ sessionId: initializedReplay.session.id,
1880
+ workflowId: initializedReplay.temporalWorkflowId,
1881
+ wakeRevision: initializedReplay.workflowWakeRevision,
1882
+ });
1883
+ }
1884
+ return await withSessionCreateUsageRecording({
1885
+ deps: unresolvedDeps,
1886
+ grant,
1887
+ workspaceId,
1888
+ startMode: payload.startMode,
1889
+ origin: creationInitiator.actor ? "system" : "user",
1890
+ createOutcome: {
1891
+ session: initializedReplay.session,
1892
+ outcome: initializedReplay.changed ? "repaired" : "replayed",
1893
+ replay: !initializedReplay.changed,
1894
+ changed: initializedReplay.changed,
1895
+ },
1896
+ });
1897
+ }
1898
+ }
1899
+ } catch (error) {
1900
+ if (error instanceof SessionIdConflictError) {
1901
+ throw new HTTPException(409, {
1902
+ message: "requested session id is already in use",
1903
+ });
1904
+ }
1905
+ if (error instanceof SessionCreateIdempotencyConflictError) {
1906
+ throw new HTTPException(409, { message: error.message, cause: error });
1907
+ }
1908
+ throw error;
1909
+ }
1910
+ }
1911
+ let settings = await resolveWorkspaceModelBoundarySettings(
1912
+ unresolvedDeps,
1913
+ grant,
1914
+ workspaceId,
1915
+ [payload.model],
1916
+ retainedKeyedShellModel,
1917
+ );
1918
+ let deps =
1919
+ settings === unresolvedDeps.settings
1920
+ ? unresolvedDeps
1921
+ : {
1922
+ ...unresolvedDeps,
1923
+ catalogSourceSettings: unresolvedDeps.catalogSourceSettings ?? unresolvedDeps.settings,
1924
+ settings,
1925
+ };
1926
+ const { bus, workflowClient, objectStorage } = deps;
1927
+ await requireAtomicPersonalResourceAttachment(
1928
+ deps,
1929
+ authorization,
1930
+ workspaceId,
1931
+ payload.personalResourceAttachment,
1932
+ false,
1933
+ );
1934
+ const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
1935
+ const effectiveModelId = payload.model ?? inheritedModel;
1936
+ const effectiveCatalogSettings = await resolveWorkspaceModelBoundarySettings(
1937
+ deps,
1938
+ grant,
1939
+ workspaceId,
1940
+ [effectiveModelId],
1941
+ parentSession ? inheritedModel : null,
1942
+ );
1943
+ if (effectiveCatalogSettings !== settings) {
1944
+ deps = {
1945
+ ...deps,
1946
+ catalogSourceSettings: deps.catalogSourceSettings ?? settings,
1947
+ settings: effectiveCatalogSettings,
1948
+ };
1949
+ settings = effectiveCatalogSettings;
1950
+ }
1653
1951
  let effectiveGoal = payload.goal;
1654
1952
  if (parentSession && payload.goal) {
1655
1953
  try {
@@ -1860,8 +2158,7 @@ export async function createSessionForRequestWithOutcome(
1860
2158
  // managers: falling back to the deployment model would silently move a child
1861
2159
  // onto the OpenGeni-credits billing path. Legacy session-bound grants without
1862
2160
  // exact attempt claims fall back to the parent session's persisted defaults.
1863
- const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
1864
- const model = canonicalConfiguredModel(settings, payload.model ?? inheritedModel);
2161
+ const model = canonicalConfiguredModel(settings, effectiveModelId);
1865
2162
  if (model === null || model === undefined) {
1866
2163
  throw new Error("effective session model unexpectedly resolved to null");
1867
2164
  }
@@ -2405,6 +2702,8 @@ export async function createSessionForRequestWithOutcome(
2405
2702
  sessionMcpServers: sessionMcpServers.metadata,
2406
2703
  personalConnectionDelegations,
2407
2704
  initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
2705
+ workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
2706
+ retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
2408
2707
  ...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
2409
2708
  parentSessionId,
2410
2709
  createIdempotencyKey: payload.idempotencyKey ?? null,
@@ -2474,30 +2773,14 @@ export async function createSessionForRequestWithOutcome(
2474
2773
  }
2475
2774
  throw error;
2476
2775
  }
2477
- let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
2478
- if (payload.startMode !== "realtime") {
2479
- try {
2480
- await recordWorkspaceUsage(deps, {
2481
- accountId: grant.accountId,
2482
- workspaceId,
2483
- subjectId: grant.subjectId,
2484
- eventType: "agent_run.created",
2485
- quantity: 1,
2486
- unit: "run",
2487
- sourceResourceType: "session",
2488
- sourceResourceId: createOutcome.session.id,
2489
- sessionId: createOutcome.session.id,
2490
- initiator: createOutcome.session.createdBy,
2491
- initiatorContext: createOutcome.session.createdByContext,
2492
- origin: creationInitiator.actor ? "system" : "user",
2493
- idempotencyKey: `agent_run.created:${workspaceId}:${createOutcome.session.id}`,
2494
- });
2495
- } catch (error) {
2496
- usageRecording = "failed";
2497
- reportSessionUsageRecordingFailure(error);
2498
- }
2499
- }
2500
- return { ...createOutcome, usageRecording };
2776
+ return await withSessionCreateUsageRecording({
2777
+ deps,
2778
+ grant,
2779
+ workspaceId,
2780
+ startMode: payload.startMode,
2781
+ origin: creationInitiator.actor ? "system" : "user",
2782
+ createOutcome,
2783
+ });
2501
2784
  }
2502
2785
 
2503
2786
  /** @internal Fixed public projection; the committed session outcome remains authoritative. */
@@ -2525,6 +2808,53 @@ export async function createSessionForRequest(
2525
2808
  ).session;
2526
2809
  }
2527
2810
 
2811
+ function sessionPromptBoundaryRequestHash(input: {
2812
+ delivery: "send" | "steer";
2813
+ controlEtag: string | null;
2814
+ expectedDraftRevision: number | null;
2815
+ text: string;
2816
+ annotations: SubmittedTimelineAnnotation[];
2817
+ modelContext: string | null;
2818
+ resources: ResourceRef[];
2819
+ composerDraftResources?: ResourceRef[];
2820
+ model: string | null;
2821
+ reasoningEffort: ReasoningEffort | null;
2822
+ latencyMode: "standard" | "priority" | "fast" | null;
2823
+ source: "user" | "api";
2824
+ mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
2825
+ connectionAuthorities?: McpConnectionAuthoritySelection[];
2826
+ personalResourceAttachment?: PersonalResourceAttachmentIntent;
2827
+ commandActor: SessionCommandActor;
2828
+ }): string {
2829
+ return `prompt-boundary-v1:${canonicalSessionCommandHash({
2830
+ delivery: input.delivery,
2831
+ controlEtag: input.controlEtag,
2832
+ expectedDraftRevision: input.expectedDraftRevision,
2833
+ text: input.text,
2834
+ annotations: input.annotations,
2835
+ modelContext: input.modelContext,
2836
+ resources: input.resources,
2837
+ composerDraftResourcesProvided: input.composerDraftResources !== undefined,
2838
+ composerDraftResources: input.composerDraftResources ?? [],
2839
+ model: input.model,
2840
+ reasoningEffort: input.reasoningEffort,
2841
+ latencyMode: input.latencyMode,
2842
+ source: input.source,
2843
+ mcpCredentialUpdates: input.mcpCredentialUpdates,
2844
+ connectionAuthorities: input.connectionAuthorities ?? [],
2845
+ personalResourceAttachment: input.personalResourceAttachment ?? null,
2846
+ ...(input.commandActor.type === "service"
2847
+ ? {
2848
+ serviceInitiator: {
2849
+ subjectId: input.commandActor.subjectId,
2850
+ subjectLabel: input.commandActor.subjectLabel ?? null,
2851
+ context: input.commandActor.context ?? {},
2852
+ },
2853
+ }
2854
+ : {}),
2855
+ })}`;
2856
+ }
2857
+
2528
2858
  /**
2529
2859
  * Full accept-user-message flow shared by the `user.message` branch of
2530
2860
  * `POST /sessions/:id/events` and the first-party MCP `session_send_message`
@@ -2566,192 +2896,308 @@ export async function acceptSessionUserMessageWithOutcome(
2566
2896
  interruptionCount: number;
2567
2897
  replay: boolean;
2568
2898
  }> {
2569
- const { settings, db, bus, workflowClient, objectStorage } = deps;
2899
+ const { db, bus, workflowClient, objectStorage } = deps;
2570
2900
  const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
2901
+ const delivery = input.delivery ?? "send";
2902
+ const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
2903
+ const commandActor: SessionCommandActor = delegatedServiceInitiator
2904
+ ? {
2905
+ type: "service",
2906
+ subjectId: delegatedServiceInitiator.initiator.subjectId,
2907
+ ...(delegatedServiceInitiator.initiator.label
2908
+ ? { subjectLabel: delegatedServiceInitiator.initiator.label }
2909
+ : {}),
2910
+ context: delegatedServiceInitiator.context,
2911
+ }
2912
+ : { type: "human", subjectId: grant.subjectId };
2571
2913
  await requireSessionAuthorization(deps, grant, {
2572
2914
  sessionId,
2573
- operation: input.delivery === "steer" ? "session.steer" : "session.append",
2915
+ operation: delivery === "steer" ? "session.steer" : "session.append",
2574
2916
  surface: "core",
2575
2917
  });
2576
- await requireAtomicPersonalResourceAttachment(
2577
- deps,
2578
- input.authorization,
2579
- workspaceId,
2580
- input.personalResourceAttachment,
2581
- true,
2582
- );
2583
- // Hoisted above requireLimit so the codex-billed predicate can resolve the
2584
- // turn's effective model (a follow-up turn inherits the session's model). A
2585
- // pure read with no side effects.
2586
- const existingSession = await requireSession(db, workspaceId, sessionId);
2587
- const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
2588
- const effectiveModel =
2589
- canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
2590
- if (effectiveModel === null) {
2591
- throw new Error("effective follow-up model unexpectedly resolved to null");
2592
- }
2593
- try {
2594
- assertSessionAllowsProductModel(existingSession, effectiveModel);
2595
- } catch (error) {
2596
- if (error instanceof CodexCompactionV2ProviderLockedError) {
2597
- throw new HTTPException(422, { message: error.message, cause: error });
2598
- }
2599
- throw error;
2600
- }
2601
- const sessionReasoningEffort = existingSession.reasoningEffort;
2602
- const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
2603
- const sessionLatencyMode = existingSession.latencyMode;
2604
- const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
2605
- const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
2606
- modelId: effectiveModel,
2607
- requestedModelId: input.model ?? null,
2608
- modelSource: input.model == null ? "session" : "explicit",
2609
- reasoningEffort: effectiveReasoningEffort,
2610
- reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
2611
- latencyMode: effectiveLatencyMode,
2612
- latencyModeSource: input.latencyMode == null ? "session" : "explicit",
2613
- });
2614
2918
  const requestedResources = normalizeResources(input.resources ?? []);
2615
2919
  const composerDraftResources = input.composerDraftResources
2616
2920
  ? normalizeResources(input.composerDraftResources)
2617
2921
  : undefined;
2618
- if (composerDraftResources) {
2619
- const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
2620
- const unacceptedDraftResource = composerDraftResources.find(
2621
- (resource) => !acceptedResources.has(stableJson(resource)),
2922
+ const boundaryRequestHash = input.clientEventId
2923
+ ? sessionPromptBoundaryRequestHash({
2924
+ delivery,
2925
+ controlEtag: input.controlEtag ?? null,
2926
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
2927
+ text: input.text,
2928
+ annotations: input.annotations ?? [],
2929
+ modelContext: input.modelContext ?? null,
2930
+ resources: requestedResources,
2931
+ ...(composerDraftResources ? { composerDraftResources } : {}),
2932
+ model: input.model ?? null,
2933
+ reasoningEffort: input.reasoningEffort ?? null,
2934
+ latencyMode: input.latencyMode ?? null,
2935
+ source,
2936
+ mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
2937
+ ...(input.connectionAuthorities
2938
+ ? { connectionAuthorities: input.connectionAuthorities }
2939
+ : {}),
2940
+ ...(input.personalResourceAttachment
2941
+ ? { personalResourceAttachment: input.personalResourceAttachment }
2942
+ : {}),
2943
+ commandActor,
2944
+ })
2945
+ : null;
2946
+ if (input.clientEventId && boundaryRequestHash) {
2947
+ const replay = await withWorkspaceSubjectSessionActivityRls(
2948
+ db,
2949
+ workspaceId,
2950
+ grant.subjectId,
2951
+ async (scopedDb) =>
2952
+ await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
2953
+ workspaceId,
2954
+ sessionId,
2955
+ subjectId: grant.subjectId,
2956
+ actor: commandActor,
2957
+ operationKey: input.clientEventId!,
2958
+ delivery,
2959
+ boundaryRequestHash,
2960
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
2961
+ }),
2622
2962
  );
2623
- if (unacceptedDraftResource) {
2624
- throw new HTTPException(422, {
2625
- message: "composer draft resources must be included in the accepted resource set",
2626
- });
2963
+ if (replay) {
2964
+ return finalizePostUserMessageTurn(
2965
+ {
2966
+ db,
2967
+ bus,
2968
+ workflowClient,
2969
+ accountId: grant.accountId,
2970
+ workspaceId,
2971
+ sessionId,
2972
+ delivery,
2973
+ ...(deps.schedulePromptPostCommit
2974
+ ? { schedulePostCommit: deps.schedulePromptPostCommit }
2975
+ : {}),
2976
+ },
2977
+ replay,
2978
+ );
2627
2979
  }
2628
2980
  }
2629
- const annotations = await validateSubmittedTimelineAnnotations(
2630
- db,
2631
- workspaceId,
2632
- sessionId,
2633
- input.annotations ?? [],
2634
- );
2635
- await requireLimit(deps, {
2636
- accountId: grant.accountId,
2637
- workspaceId,
2638
- action: "agent_run:create",
2639
- quantity: 1,
2640
- model: effectiveModel,
2641
- });
2642
- if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
2643
- throw new HTTPException(503, {
2644
- message: "object storage is not configured",
2981
+ try {
2982
+ await requireAtomicPersonalResourceAttachment(
2983
+ deps,
2984
+ input.authorization,
2985
+ workspaceId,
2986
+ input.personalResourceAttachment,
2987
+ true,
2988
+ );
2989
+ // Hoisted above requireLimit so the codex-billed predicate can resolve the
2990
+ // turn's effective model (a follow-up turn inherits the session's model). A
2991
+ // pure read with no side effects.
2992
+ const existingSession = await requireSession(db, workspaceId, sessionId);
2993
+ const settings = await resolveWorkspaceModelBoundarySettings(
2994
+ deps,
2995
+ grant,
2996
+ workspaceId,
2997
+ [input.model ?? existingSession.model],
2998
+ existingSession.model,
2999
+ );
3000
+ if (settings !== deps.settings) {
3001
+ deps = {
3002
+ ...deps,
3003
+ catalogSourceSettings: deps.catalogSourceSettings ?? deps.settings,
3004
+ settings,
3005
+ };
3006
+ }
3007
+ const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
3008
+ const effectiveModel =
3009
+ canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
3010
+ if (effectiveModel === null) {
3011
+ throw new Error("effective follow-up model unexpectedly resolved to null");
3012
+ }
3013
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
3014
+ try {
3015
+ assertSessionAllowsProductModel(existingSession, effectiveModel);
3016
+ } catch (error) {
3017
+ if (error instanceof CodexCompactionV2ProviderLockedError) {
3018
+ throw new HTTPException(422, { message: error.message, cause: error });
3019
+ }
3020
+ throw error;
3021
+ }
3022
+ const sessionReasoningEffort = existingSession.reasoningEffort;
3023
+ const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
3024
+ const sessionLatencyMode = existingSession.latencyMode;
3025
+ const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
3026
+ const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
3027
+ modelId: effectiveModel,
3028
+ requestedModelId: input.model ?? null,
3029
+ modelSource: input.model == null ? "session" : "explicit",
3030
+ reasoningEffort: effectiveReasoningEffort,
3031
+ reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
3032
+ latencyMode: effectiveLatencyMode,
3033
+ latencyModeSource: input.latencyMode == null ? "session" : "explicit",
2645
3034
  });
2646
- }
2647
- await validateFileResources(
2648
- db,
2649
- grant.accountId,
2650
- workspaceId,
2651
- grant.subjectId,
2652
- requestedResources,
2653
- );
2654
- await validateGitHubRepositorySelection(db, workspaceId, [
2655
- ...existingSession.resources,
2656
- ...requestedResources,
2657
- ]);
2658
- const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
2659
- settings,
2660
- grant,
2661
- session: existingSession,
2662
- updates: input.mcpCredentialUpdates ?? [],
2663
- });
2664
- const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
2665
- const inheritedPersonalConnectionDelegations =
2666
- connectionDelegationSource.kind === "turn"
2667
- ? await getSessionTurnPersonalConnectionDelegations(
2668
- db,
2669
- workspaceId,
2670
- connectionDelegationSource.sessionId,
2671
- connectionDelegationSource.turnId,
2672
- )
2673
- : null;
2674
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
2675
- db,
2676
- workspaceId,
2677
- settings,
2678
- inheritedPersonalConnectionDelegations
2679
- ? {
2680
- personalConnectionDelegations: inheritedPersonalConnectionDelegations,
2681
- }
2682
- : { subjectId: grant.subjectId },
2683
- );
2684
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
2685
- db,
2686
- workspaceId,
2687
- settings: runtimeSettings,
2688
- tools: existingSession.tools,
2689
- resources: [...existingSession.resources, ...requestedResources],
2690
- source: connectionDelegationSource,
2691
- targetSessionId: sessionId,
2692
- googleDrivePublicationEnabled:
2693
- existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
2694
- existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
2695
- (!existingSession.firstPartyMcpPermissions?.length ||
2696
- (existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
2697
- existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
2698
- atlassianEnabled:
2699
- existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
2700
- (!existingSession.firstPartyMcpPermissions?.length ||
2701
- existingSession.firstPartyMcpPermissions.includes("connections:read")),
2702
- ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
2703
- });
2704
- const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
2705
- await postUserMessageTurn({
3035
+ if (composerDraftResources) {
3036
+ const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
3037
+ const unacceptedDraftResource = composerDraftResources.find(
3038
+ (resource) => !acceptedResources.has(stableJson(resource)),
3039
+ );
3040
+ if (unacceptedDraftResource) {
3041
+ throw new HTTPException(422, {
3042
+ message: "composer draft resources must be included in the accepted resource set",
3043
+ });
3044
+ }
3045
+ }
3046
+ const annotations = await validateSubmittedTimelineAnnotations(
2706
3047
  db,
2707
- bus,
2708
- workflowClient,
2709
- settings,
2710
- accountId: grant.accountId,
2711
3048
  workspaceId,
2712
3049
  sessionId,
2713
- text: input.text,
2714
- annotations,
2715
- modelContext: input.modelContext ?? null,
2716
- resources: requestedResources,
2717
- ...(composerDraftResources ? { composerDraftResources } : {}),
2718
- model: input.model ?? null,
2719
- reasoningEffort: input.reasoningEffort ?? null,
2720
- latencyMode: input.latencyMode ?? null,
2721
- reasoningEffortFallback: sessionReasoningEffort,
2722
- turnExecutionPolicy,
2723
- mcpCredentialUpdates,
2724
- personalConnectionDelegations,
2725
- ...(input.personalResourceAttachment
2726
- ? { personalResourceAttachment: input.personalResourceAttachment }
2727
- : {}),
2728
- delivery: input.delivery ?? "send",
2729
- origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
2730
- actor: grant.subjectId,
2731
- ...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
2732
- ...(delegatedServiceInitiator
3050
+ input.annotations ?? [],
3051
+ );
3052
+ await requireLimit(deps, {
3053
+ accountId: grant.accountId,
3054
+ workspaceId,
3055
+ action: "agent_run:create",
3056
+ quantity: 1,
3057
+ model: effectiveModel,
3058
+ });
3059
+ if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
3060
+ throw new HTTPException(503, {
3061
+ message: "object storage is not configured",
3062
+ });
3063
+ }
3064
+ await validateFileResources(
3065
+ db,
3066
+ grant.accountId,
3067
+ workspaceId,
3068
+ grant.subjectId,
3069
+ requestedResources,
3070
+ );
3071
+ await validateGitHubRepositorySelection(db, workspaceId, [
3072
+ ...existingSession.resources,
3073
+ ...requestedResources,
3074
+ ]);
3075
+ const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
3076
+ settings,
3077
+ grant,
3078
+ session: existingSession,
3079
+ updates: input.mcpCredentialUpdates ?? [],
3080
+ });
3081
+ const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
3082
+ const inheritedPersonalConnectionDelegations =
3083
+ connectionDelegationSource.kind === "turn"
3084
+ ? await getSessionTurnPersonalConnectionDelegations(
3085
+ db,
3086
+ workspaceId,
3087
+ connectionDelegationSource.sessionId,
3088
+ connectionDelegationSource.turnId,
3089
+ )
3090
+ : null;
3091
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
3092
+ db,
3093
+ workspaceId,
3094
+ settings,
3095
+ inheritedPersonalConnectionDelegations
2733
3096
  ? {
2734
- commandActor: {
2735
- type: "service" as const,
2736
- subjectId: delegatedServiceInitiator.initiator.subjectId,
2737
- ...(delegatedServiceInitiator.initiator.label
2738
- ? { subjectLabel: delegatedServiceInitiator.initiator.label }
2739
- : {}),
2740
- context: delegatedServiceInitiator.context,
2741
- },
3097
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
2742
3098
  }
2743
- : {}),
2744
- ...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
2745
- ...(input.expectedDraftRevision !== undefined
2746
- ? { expectedDraftRevision: input.expectedDraftRevision }
2747
- : {}),
2748
- ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
2749
- recordAgentRunUsage: true,
2750
- ...(deps.schedulePromptPostCommit
2751
- ? { schedulePostCommit: deps.schedulePromptPostCommit }
2752
- : {}),
3099
+ : { subjectId: grant.subjectId },
3100
+ );
3101
+ const personalConnectionDelegations = await freezePersonalConnectionDelegations({
3102
+ db,
3103
+ workspaceId,
3104
+ settings: runtimeSettings,
3105
+ tools: existingSession.tools,
3106
+ resources: [...existingSession.resources, ...requestedResources],
3107
+ source: connectionDelegationSource,
3108
+ targetSessionId: sessionId,
3109
+ googleDrivePublicationEnabled:
3110
+ existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
3111
+ existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
3112
+ (!existingSession.firstPartyMcpPermissions?.length ||
3113
+ (existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
3114
+ existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
3115
+ atlassianEnabled:
3116
+ existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
3117
+ (!existingSession.firstPartyMcpPermissions?.length ||
3118
+ existingSession.firstPartyMcpPermissions.includes("connections:read")),
3119
+ ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
2753
3120
  });
2754
- return { accepted, turn, draft, receipt, routing, interruptionCount, replay };
3121
+ const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
3122
+ await postUserMessageTurn({
3123
+ db,
3124
+ bus,
3125
+ workflowClient,
3126
+ settings,
3127
+ accountId: grant.accountId,
3128
+ workspaceId,
3129
+ sessionId,
3130
+ text: input.text,
3131
+ annotations,
3132
+ modelContext: input.modelContext ?? null,
3133
+ resources: requestedResources,
3134
+ ...(composerDraftResources ? { composerDraftResources } : {}),
3135
+ model: input.model ?? null,
3136
+ reasoningEffort: input.reasoningEffort ?? null,
3137
+ latencyMode: input.latencyMode ?? null,
3138
+ reasoningEffortFallback: sessionReasoningEffort,
3139
+ turnExecutionPolicy,
3140
+ mcpCredentialUpdates,
3141
+ personalConnectionDelegations,
3142
+ ...(input.personalResourceAttachment
3143
+ ? { personalResourceAttachment: input.personalResourceAttachment }
3144
+ : {}),
3145
+ delivery,
3146
+ origin: source === "api" ? "operator" : "human",
3147
+ actor: grant.subjectId,
3148
+ ...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
3149
+ commandActor,
3150
+ ...(boundaryRequestHash ? { boundaryRequestHash } : {}),
3151
+ ...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
3152
+ ...(input.expectedDraftRevision !== undefined
3153
+ ? { expectedDraftRevision: input.expectedDraftRevision }
3154
+ : {}),
3155
+ ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
3156
+ recordAgentRunUsage: true,
3157
+ ...(deps.schedulePromptPostCommit
3158
+ ? { schedulePostCommit: deps.schedulePromptPostCommit }
3159
+ : {}),
3160
+ });
3161
+ return { accepted, turn, draft, receipt, routing, interruptionCount, replay };
3162
+ } catch (error) {
3163
+ if (input.clientEventId && boundaryRequestHash) {
3164
+ const replay = await withWorkspaceSubjectSessionActivityRls(
3165
+ db,
3166
+ workspaceId,
3167
+ grant.subjectId,
3168
+ async (scopedDb) =>
3169
+ await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
3170
+ workspaceId,
3171
+ sessionId,
3172
+ subjectId: grant.subjectId,
3173
+ actor: commandActor,
3174
+ operationKey: input.clientEventId!,
3175
+ delivery,
3176
+ boundaryRequestHash,
3177
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
3178
+ serializeOperation: true,
3179
+ }),
3180
+ );
3181
+ if (replay) {
3182
+ return finalizePostUserMessageTurn(
3183
+ {
3184
+ db,
3185
+ bus,
3186
+ workflowClient,
3187
+ accountId: grant.accountId,
3188
+ workspaceId,
3189
+ sessionId,
3190
+ delivery,
3191
+ ...(deps.schedulePromptPostCommit
3192
+ ? { schedulePostCommit: deps.schedulePromptPostCommit }
3193
+ : {}),
3194
+ },
3195
+ replay,
3196
+ );
3197
+ }
3198
+ }
3199
+ throw error;
3200
+ }
2755
3201
  }
2756
3202
 
2757
3203
  /** Backward-compatible entity-returning path used by existing REST callers. */