@opengeni/core 2.6.4 → 2.7.5-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,13 @@ import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
2
2
  import {
3
3
  canonicalizeConfiguredModelId,
4
4
  configuredAllowedModels,
5
+ ORGANIZATION_GATEWAY_MODEL_ID_PREFIX,
6
+ ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX,
5
7
  resolveFirstPartyMcpToolPolicy,
6
8
  policyProviderIdForModel,
7
9
  resolveTurnExecutionPolicyV1,
8
10
  WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
11
+ WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
9
12
  XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
10
13
  type Settings,
11
14
  } from "@opengeni/config";
@@ -17,12 +20,15 @@ import {
17
20
  DraftTimelineAnnotations,
18
21
  FIRST_PARTY_MCP_TOOL_NAMES,
19
22
  OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
23
+ SessionSkills,
20
24
  SessionSpawnDenial,
21
25
  ServiceTurnInitiator,
22
26
  ServiceTurnInitiatorContext,
23
27
  evaluateWorkspaceModelPolicy,
24
28
  normalizeAutomaticSessionTitle,
25
29
  resolveWorkspaceSessionToolDefaults,
30
+ metadataWithTurnExecutionPolicyV1,
31
+ readTurnExecutionPolicyV1,
26
32
  stableJson,
27
33
  type AccessGrant,
28
34
  type ComposerDraft,
@@ -60,8 +66,10 @@ import {
60
66
  type XaiProviderAccountAuthoritySnapshotV1,
61
67
  } from "@opengeni/contracts";
62
68
  import {
69
+ assertExactNewSessionDraftInTransaction,
63
70
  createSession,
64
71
  createSessionWithIdempotencyKeyResult,
72
+ canonicalSessionCommandHash,
65
73
  encryptVariableSetValue,
66
74
  getAnySessionInGroup,
67
75
  getEnrollment,
@@ -70,12 +78,13 @@ import {
70
78
  getWorkspaceDefaultRigId,
71
79
  listDistinctVariableSetSelectionsInGroup,
72
80
  listDistinctRigVersionIdsInGroup,
81
+ listInstalledPortableSkills,
73
82
  getSandbox,
74
83
  getSession,
84
+ getInitializedSessionCreateReplay,
75
85
  getSessionAuthorityProjection,
76
86
  SessionIdConflictError,
77
87
  NewSessionDraftConflictError,
78
- getSessionSpawnDenialByIdempotencyKey,
79
88
  getWorkspaceControlEvent,
80
89
  getSessionLineage,
81
90
  getSessionTurn,
@@ -88,6 +97,9 @@ import {
88
97
  listSessionTurns,
89
98
  listSessionMcpServersForChildInheritance,
90
99
  requireSession,
100
+ setActiveSandbox,
101
+ setSubjectRlsContext,
102
+ replaySubmittedHumanPromptFromBoundaryReceipt,
91
103
  submitHumanPromptInTransaction,
92
104
  appendSessionEventsWithLockedSessionUpdate,
93
105
  updateSessionTitleWithEvent,
@@ -127,8 +139,19 @@ import {
127
139
  requireSessionAuthorization,
128
140
  SessionAuthorizationDeniedError,
129
141
  } from "../session-authorization";
130
- import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
142
+ import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
143
+ import {
144
+ preflightCreateTimeSandboxTarget,
145
+ swapActiveSandbox,
146
+ type FleetContext,
147
+ } from "../sandbox/fleet";
131
148
  import { managedSessionGroupBackend } from "../sandbox/runtime-settings";
149
+ import {
150
+ isWorkspaceCustomModelId,
151
+ lockActiveCustomModelForAdmission,
152
+ resolveWorkspaceCatalogSettings,
153
+ workspaceCustomModelReference,
154
+ } from "../model-catalog";
132
155
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
133
156
  import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
134
157
  import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
@@ -158,6 +181,15 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
158
181
  // Keep the durable snapshot below the shared event-preview array boundary so
159
182
  // the generic lossy projection cannot silently rewrite this audit fact.
160
183
  const maxToolPolicyAuditRefs = 40;
184
+
185
+ function isCatalogOverlayModel(modelId: string | null | undefined): boolean {
186
+ return (
187
+ modelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
188
+ modelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) === true ||
189
+ modelId?.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX) === true ||
190
+ modelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX) === true
191
+ );
192
+ }
161
193
  // RFC 9110 field-name token characters.
162
194
  const sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
163
195
 
@@ -470,6 +502,7 @@ function validateSessionMcpServersForCreate(
470
502
  const dbServers: CreateSessionMcpServerInput[] = [];
471
503
  const metadata: SessionMcpServerMetadata[] = [];
472
504
  for (const server of servers) {
505
+ assertHostMcpAuthoritySourceAdmissionEnabled(settings, server.connectionRef);
473
506
  if (seenIds.has(server.id)) {
474
507
  throw new HTTPException(422, {
475
508
  message: `duplicate session MCP server id: ${server.id}`,
@@ -656,6 +689,16 @@ export async function createAndStartSessionWithOutcome(input: {
656
689
  /** Internal database-only composition seam. The exact session shell and this
657
690
  * linkage commit together before its first event/turn can be initialized. */
658
691
  beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
692
+ /** The custom workspace model was frozen by an earlier accepted boundary or
693
+ * inherited from an existing session, so retirement must not invalidate it. */
694
+ retainWorkspaceGatewayModel?: boolean;
695
+ /** Provider-neutral successor to retainWorkspaceGatewayModel. */
696
+ retainWorkspaceCustomModel?: boolean;
697
+ /** The selected workspace Gateway product is backed by a mutable custom row,
698
+ * rather than deployment-curated Gateway membership. */
699
+ workspaceGatewayCustomModel?: boolean;
700
+ /** Provider-neutral successor to workspaceGatewayCustomModel. */
701
+ workspaceCustomModel?: boolean;
659
702
  accountId: string;
660
703
  workspaceId: string;
661
704
  visibility?: "user_private" | "workspace_shared";
@@ -683,7 +726,11 @@ export async function createAndStartSessionWithOutcome(input: {
683
726
  createdByActor?: Extract<SessionCommandActor, { type: "agent_attempt" }> | null;
684
727
  // Ordered low-to-high precedence. Names/ids only; session.created never
685
728
  // carries variable values.
686
- variableSets?: Array<{ id: string; name: string; scope: VariableSet["scope"] }>;
729
+ variableSets?: Array<{
730
+ id: string;
731
+ name: string;
732
+ scope: VariableSet["scope"];
733
+ }>;
687
734
  // The rig + frozen active rig version resolved at create (M3). Both null ⇒ a
688
735
  // rig-less session (byte-for-byte today's behavior). Frozen here so a later
689
736
  // rig promote never moves an existing session's version.
@@ -725,6 +772,9 @@ export async function createAndStartSessionWithOutcome(input: {
725
772
  // session. Every caller repairs or re-delivers the winner's one atomic start;
726
773
  // the durable initializer prevents duplicate events or turns.
727
774
  createIdempotencyKey?: string | null;
775
+ // Exact explicit installed-Skill selection. The database stores this only as
776
+ // keyed-create identity; runtime behavior comes from the frozen Skill content.
777
+ selectedInstalledSkillIds?: string[];
728
778
  // The shared-sandbox group this session's box joins (addendum 05 §D). Null/
729
779
  // omitted ⇒ a singleton group (the new row's own id, today's 1:1 behavior); a
730
780
  // shared/{groupId} spawn passes the resolved group so both run in ONE box.
@@ -735,11 +785,11 @@ export async function createAndStartSessionWithOutcome(input: {
735
785
  // OS-labeling surfaces honestly reflect the machine.
736
786
  sandboxOs?: Session["sandboxOs"];
737
787
  // Create-time machine targeting (A-2a, RACE-FREE): the enrolled machine (a
738
- // sandbox id) to run this session on. When set, the active-sandbox pointer is
739
- // resolved+validated+seeded (epoch-fenced) INSIDE finishStartSession, AFTER the
740
- // session row exists but BEFORE the first turn is enqueued/the workflow woken,
741
- // so the FIRST turn routes to the chosen machine. An invalid/unowned/offline
742
- // target fails the create (422) never a silent fall-back to the default box.
788
+ // sandbox id) to run this session on. When set, target liveness is preflighted
789
+ // before insertion, then the active-sandbox pointer is authority-checked and
790
+ // seeded (epoch-fenced) in the SAME transaction as the session row. The FIRST
791
+ // turn therefore routes to the chosen machine, while an invalid/unowned/offline
792
+ // target fails the create (422) without leaving a queued session shell.
743
793
  // `workingDir` (optional) is the path/cwd base the chosen machine runs under,
744
794
  // seeded alongside the pointer through the epoch-fenced CAS.
745
795
  seedTargetSandbox?: {
@@ -775,16 +825,117 @@ export async function createAndStartSessionWithOutcome(input: {
775
825
  allowNestedAgentDepthIncrease?: boolean;
776
826
  subjectId?: string | null;
777
827
  }): Promise<CreateSessionOutcome> {
778
- const sessionMetadata = {
779
- ...input.metadata,
780
- model: input.model,
781
- reasoningEffort: input.reasoningEffort,
782
- ...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
783
- };
828
+ const sessionMetadata = metadataWithTurnExecutionPolicyV1(
829
+ {
830
+ ...input.metadata,
831
+ model: input.model,
832
+ reasoningEffort: input.reasoningEffort,
833
+ ...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
834
+ },
835
+ input.turnExecutionPolicy,
836
+ );
784
837
  const frozenCreatedByContext = freezeAgentChildAutomaticTitleInCreatorContext(
785
838
  input.createdByContext,
786
839
  input.initialAutomaticTitle,
787
840
  );
841
+ const requiresActiveWorkspaceCustomModel =
842
+ (input.workspaceCustomModel === true || input.workspaceGatewayCustomModel === true) &&
843
+ input.retainWorkspaceCustomModel !== true &&
844
+ input.retainWorkspaceGatewayModel !== true;
845
+ const seedTargetForNewSession = input.seedTargetSandbox ?? null;
846
+ const preflightTarget = seedTargetForNewSession
847
+ ? await preflightCreateTimeSandboxTarget(
848
+ {
849
+ db: input.db,
850
+ settings: seedTargetForNewSession.settings,
851
+ bus: input.bus,
852
+ },
853
+ {
854
+ accountId: input.accountId,
855
+ workspaceId: input.workspaceId,
856
+ ...(seedTargetForNewSession.resourceSubjectId
857
+ ? { subjectId: seedTargetForNewSession.resourceSubjectId }
858
+ : {}),
859
+ },
860
+ seedTargetForNewSession.sandboxId,
861
+ seedTargetForNewSession.workingDir ?? null,
862
+ )
863
+ : null;
864
+ const targetPreflightFailureMessage =
865
+ preflightTarget && !preflightTarget.ok
866
+ ? `cannot target sandbox ${seedTargetForNewSession!.sandboxId}: ${preflightTarget.reason}`
867
+ : null;
868
+ let targetSeededBeforeCreateCommit = false;
869
+ const beforeCreateCommit =
870
+ requiresActiveWorkspaceCustomModel ||
871
+ input.consumeNewSessionDraft ||
872
+ input.beforeCreateCommit ||
873
+ preflightTarget ||
874
+ targetPreflightFailureMessage
875
+ ? async (tx: Database, sessionId: string, context?: { created: boolean }): Promise<void> => {
876
+ // A committed keyed replay already crossed this fence when its shell
877
+ // was first accepted. Revalidate only the transaction inserting a new
878
+ // session, while still running caller linkage on every replay.
879
+ if (targetPreflightFailureMessage && context?.created !== false) {
880
+ throw new HTTPException(422, {
881
+ message: targetPreflightFailureMessage,
882
+ });
883
+ }
884
+ if (requiresActiveWorkspaceCustomModel && context?.created !== false) {
885
+ const reference = {
886
+ scope: input.turnExecutionPolicy.providerId.startsWith("organization-")
887
+ ? ("organization" as const)
888
+ : ("workspace" as const),
889
+ providerKind: input.turnExecutionPolicy.providerId.includes("openrouter")
890
+ ? ("openrouter" as const)
891
+ : ("vercel_gateway" as const),
892
+ upstreamModelId: input.turnExecutionPolicy.upstreamModelId,
893
+ };
894
+ const active = await lockActiveCustomModelForAdmission(tx, {
895
+ accountId: input.accountId,
896
+ workspaceId: input.workspaceId,
897
+ reference,
898
+ });
899
+ if (!active) {
900
+ throw new HTTPException(422, {
901
+ message: `model is not available: ${input.model}`,
902
+ });
903
+ }
904
+ }
905
+ // Reject an already-stale browser draft before the newly inserted
906
+ // shell can commit. The initializer repeats this exact check while
907
+ // consuming the draft after it installs the first runnable unit.
908
+ if (input.consumeNewSessionDraft && context?.created !== false) {
909
+ await setSubjectRlsContext(tx, input.consumeNewSessionDraft.subjectId);
910
+ await assertExactNewSessionDraftInTransaction(tx, {
911
+ workspaceId: input.workspaceId,
912
+ subjectId: input.consumeNewSessionDraft.subjectId,
913
+ expectedRevision: input.consumeNewSessionDraft.expectedRevision,
914
+ expectedSnapshot: input.consumeNewSessionDraft.expectedSnapshot,
915
+ });
916
+ }
917
+ if (preflightTarget?.ok && context?.created !== false) {
918
+ const seeded = await setActiveSandbox(tx, {
919
+ accountId: input.accountId,
920
+ workspaceId: input.workspaceId,
921
+ sessionId,
922
+ targetSandboxId: preflightTarget.targetSandboxId,
923
+ expectedEpoch: 0,
924
+ ...(seedTargetForNewSession?.resourceSubjectId
925
+ ? { subjectId: seedTargetForNewSession.resourceSubjectId }
926
+ : {}),
927
+ workingDir: preflightTarget.workingDir,
928
+ });
929
+ if (!seeded.swapped) {
930
+ throw new HTTPException(422, {
931
+ message: `cannot target sandbox ${seedTargetForNewSession!.sandboxId}: target authority changed during session creation`,
932
+ });
933
+ }
934
+ targetSeededBeforeCreateCommit = true;
935
+ }
936
+ await input.beforeCreateCommit?.(tx, sessionId);
937
+ }
938
+ : undefined;
788
939
  // Keyed creation is intentionally handled only by the database admission
789
940
  // transaction below. Its workspace/key lock replays either the successful
790
941
  // session or the committed denial atomically; an application-side lookup
@@ -820,6 +971,7 @@ export async function createAndStartSessionWithOutcome(input: {
820
971
  policyRole: input.policyRole ?? null,
821
972
  parentSessionId: input.parentSessionId ?? null,
822
973
  createIdempotencyKey: input.createIdempotencyKey,
974
+ selectedInstalledSkillIds: input.selectedInstalledSkillIds ?? [],
823
975
  sandboxGroupId: input.sandboxGroupId ?? null,
824
976
  ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
825
977
  mcpServers: input.mcpServers ?? [],
@@ -834,15 +986,29 @@ export async function createAndStartSessionWithOutcome(input: {
834
986
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
835
987
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
836
988
  subjectId: input.subjectId ?? null,
837
- ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
989
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
838
990
  });
839
991
  if (keyedResult.denied) {
840
992
  throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
841
993
  }
842
994
  const { session: keyed, created } = keyedResult;
843
995
  if (!created) {
996
+ const persistedPolicy = readTurnExecutionPolicyV1(keyed.metadata);
844
997
  const finished = await finishStartSession(
845
- keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
998
+ keyed.temporalWorkflowId
999
+ ? {
1000
+ ...input,
1001
+ seedTargetSandbox: null,
1002
+ ...(persistedPolicy.kind === "valid"
1003
+ ? { turnExecutionPolicy: persistedPolicy.policy }
1004
+ : {}),
1005
+ }
1006
+ : {
1007
+ ...input,
1008
+ ...(persistedPolicy.kind === "valid"
1009
+ ? { turnExecutionPolicy: persistedPolicy.policy }
1010
+ : {}),
1011
+ },
846
1012
  keyed,
847
1013
  );
848
1014
  return {
@@ -852,7 +1018,10 @@ export async function createAndStartSessionWithOutcome(input: {
852
1018
  changed: finished.changed,
853
1019
  };
854
1020
  }
855
- const finished = await finishStartSession(input, keyed);
1021
+ const finished = await finishStartSession(
1022
+ targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
1023
+ keyed,
1024
+ );
856
1025
  return {
857
1026
  session: finished.session,
858
1027
  outcome: "created",
@@ -905,7 +1074,7 @@ export async function createAndStartSessionWithOutcome(input: {
905
1074
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
906
1075
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
907
1076
  subjectId: input.subjectId ?? null,
908
- ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
1077
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
909
1078
  });
910
1079
  } catch (error) {
911
1080
  if (error instanceof SessionSpawnDeniedDbError) {
@@ -913,7 +1082,10 @@ export async function createAndStartSessionWithOutcome(input: {
913
1082
  }
914
1083
  throw error;
915
1084
  }
916
- const finished = await finishStartSession(input, session);
1085
+ const finished = await finishStartSession(
1086
+ targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
1087
+ session,
1088
+ );
917
1089
  return {
918
1090
  session: finished.session,
919
1091
  outcome: "created",
@@ -951,7 +1123,11 @@ async function finishStartSession(
951
1123
  reasoningEffort: Settings["openaiReasoningEffort"];
952
1124
  turnExecutionPolicy: TurnExecutionPolicyV1;
953
1125
  sandboxBackend: Settings["sandboxBackend"];
954
- variableSets?: Array<{ id: string; name: string; scope: VariableSet["scope"] }>;
1126
+ variableSets?: Array<{
1127
+ id: string;
1128
+ name: string;
1129
+ scope: VariableSet["scope"];
1130
+ }>;
955
1131
  goal?: GoalSpec | null;
956
1132
  initialAutomaticTitle?: string | null;
957
1133
  sessionMcpServers?: SessionMcpServerMetadata[];
@@ -984,15 +1160,12 @@ async function finishStartSession(
984
1160
  ): Promise<{ session: CreateSessionResponse; changed: boolean }> {
985
1161
  // Create-time machine targeting (A-2a): seed the active-sandbox pointer BEFORE
986
1162
  // the atomic initial turn transaction, so the FIRST turn routes to the chosen
987
- // machine. swapActiveSandbox does
1163
+ // machine. Home backend and active route are independent: a backend:none
1164
+ // session has no managed home but may still attach a valid Connected Machine.
1165
+ // swapActiveSandbox does
988
1166
  // the same ownership+liveness validation as the live swap; an invalid/unowned/
989
1167
  // offline target FAILS the create (422) — never a silent fall-back to the box.
990
1168
  if (input.seedTargetSandbox) {
991
- if (session.sandboxBackend === "none") {
992
- throw new HTTPException(422, {
993
- message: "cannot target a machine for a session with no sandbox (backend: none)",
994
- });
995
- }
996
1169
  const ctx: FleetContext = {
997
1170
  accountId: session.accountId,
998
1171
  workspaceId: session.workspaceId,
@@ -1137,9 +1310,6 @@ export function canonicalConfiguredModel(
1137
1310
  ) {
1138
1311
  return canonicalModel;
1139
1312
  }
1140
- if (canonicalModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
1141
- return canonicalModel;
1142
- }
1143
1313
  throw new HTTPException(422, { message: `model is not available: ${model}` });
1144
1314
  }
1145
1315
 
@@ -1245,7 +1415,17 @@ export async function requireQueuedTurnForApi(
1245
1415
  * `session_send_message` tool so the two surfaces cannot drift. Callers own
1246
1416
  * resource/tool validation and the per-message usage limit before calling.
1247
1417
  */
1248
- export async function postUserMessageTurn(input: {
1418
+ type PostUserMessageTurnResult = {
1419
+ accepted: SessionEvent;
1420
+ turn: SessionTurn;
1421
+ draft: ComposerDraft | null;
1422
+ receipt: SessionCommandReceipt;
1423
+ routing: SessionPromptRouting;
1424
+ interruptionCount: number;
1425
+ replay: boolean;
1426
+ };
1427
+
1428
+ type PostUserMessageTurnInput = {
1249
1429
  db: Database;
1250
1430
  bus: EventBus;
1251
1431
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
@@ -1273,28 +1453,143 @@ export async function postUserMessageTurn(input: {
1273
1453
  commandActor?: SessionCommandActor;
1274
1454
  controlEtag?: string | null;
1275
1455
  expectedDraftRevision?: number | null;
1456
+ boundaryRequestHash?: string;
1276
1457
  reasoningEffortFallback?: Settings["openaiReasoningEffort"];
1277
1458
  turnExecutionPolicy: TurnExecutionPolicyV1;
1278
1459
  recordAgentRunUsage?: boolean;
1279
1460
  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;
1461
+ };
1462
+
1463
+ function finalizePostUserMessageTurn(
1464
+ input: Pick<
1465
+ PostUserMessageTurnInput,
1466
+ | "db"
1467
+ | "bus"
1468
+ | "workflowClient"
1469
+ | "accountId"
1470
+ | "workspaceId"
1471
+ | "sessionId"
1472
+ | "delivery"
1473
+ | "schedulePostCommit"
1474
+ >,
1475
+ result: Awaited<ReturnType<typeof submitHumanPromptInTransaction>>,
1476
+ ): PostUserMessageTurnResult {
1477
+ const { db, bus, workflowClient, accountId, workspaceId, sessionId } = input;
1478
+ const postCommitTask = async () => {
1479
+ await Promise.all([
1480
+ (async () => {
1481
+ try {
1482
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1483
+ if (result.workspaceControlEventId) {
1484
+ const controlEvent = await getWorkspaceControlEvent(
1485
+ db,
1486
+ workspaceId,
1487
+ result.workspaceControlEventId,
1488
+ );
1489
+ if (!controlEvent) {
1490
+ throw new Error(
1491
+ `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1492
+ );
1493
+ }
1494
+ await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1495
+ }
1496
+ } catch {
1497
+ console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1498
+ errorClass: "PromptEventFanoutOperationError",
1499
+ errorCode: "session_prompt_event_fanout_failed",
1500
+ origin: "core",
1501
+ });
1502
+ }
1503
+ })(),
1504
+ (async () => {
1505
+ try {
1506
+ await workflowClient.wakeSessionWorkflow({
1507
+ accountId,
1508
+ workspaceId,
1509
+ sessionId,
1510
+ workflowId: result.turn.temporalWorkflowId,
1511
+ wakeRevision: result.wakeRevision,
1512
+ ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1513
+ ? { interruptionRequested: true }
1514
+ : {}),
1515
+ });
1516
+ } catch {
1517
+ console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1518
+ errorClass: "WorkflowWakeOperationError",
1519
+ errorCode: "session_workflow_wake_failed",
1520
+ origin: "core",
1521
+ });
1522
+ }
1523
+ })(),
1524
+ ]);
1525
+ };
1526
+ const schedulePostCommit =
1527
+ input.schedulePostCommit ??
1528
+ ((task: () => Promise<void>) => {
1529
+ void task();
1530
+ });
1531
+ try {
1532
+ schedulePostCommit(postCommitTask);
1533
+ } catch {
1534
+ console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
1535
+ errorClass: "PromptPostCommitScheduleError",
1536
+ errorCode: "session_prompt_post_commit_schedule_failed",
1537
+ origin: "core",
1538
+ });
1539
+ }
1540
+ return {
1541
+ accepted: result.accepted,
1542
+ turn: result.turn,
1543
+ receipt: {
1544
+ id: result.receipt.id,
1545
+ action: result.receipt.action,
1546
+ operationKey: result.receipt.operationKey,
1547
+ targetSessionId: result.receipt.targetSessionId,
1548
+ targetTurnId: result.receipt.targetTurnId,
1549
+ appliedControlRevision: result.receipt.appliedControlRevision,
1550
+ appliedQueueVersion: result.receipt.appliedQueueVersion,
1551
+ appliedTurnVersion: result.receipt.appliedTurnVersion,
1552
+ appliedDraftRevision: result.receipt.appliedDraftRevision,
1553
+ createdAt: result.receipt.createdAt.toISOString(),
1554
+ },
1555
+ routing: result.routing,
1556
+ draft: result.draft
1557
+ ? {
1558
+ revision: result.draft.revision,
1559
+ text: result.draft.text,
1560
+ annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
1561
+ resources: result.draft.resources as ResourceRef[],
1562
+ model: result.draft.model,
1563
+ reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
1564
+ latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
1565
+ sourceTurnId: result.draft.sourceTurnId,
1566
+ sourceTurnVersion: result.draft.sourceTurnVersion,
1567
+ updatedAt: result.draft.updatedAt.toISOString(),
1568
+ }
1569
+ : null,
1570
+ interruptionCount: result.interruptionCount,
1571
+ replay: result.replay,
1572
+ };
1573
+ }
1574
+
1575
+ export async function postUserMessageTurn(
1576
+ input: PostUserMessageTurnInput,
1577
+ ): Promise<PostUserMessageTurnResult> {
1578
+ const { db, settings, accountId, workspaceId, sessionId } = input;
1290
1579
  const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
1291
1580
  const requestedReasoningEffort = input.reasoningEffort ?? null;
1292
1581
  // Reject an explicit per-message model the host does not expose; an omitted
1293
1582
  // model inherits the session's model downstream (always a configured id).
1294
1583
  assertConfiguredModel(settings, requestedModel);
1295
- await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
1296
1584
  const sessionForModelGate = await requireSession(db, workspaceId, sessionId);
1297
1585
  const effectiveModelForGate = requestedModel ?? sessionForModelGate.model;
1586
+ const freshWorkspaceCustomModel =
1587
+ requestedModel !== null &&
1588
+ isWorkspaceCustomModelId(settings, requestedModel) &&
1589
+ requestedModel !== sessionForModelGate.model
1590
+ ? requestedModel
1591
+ : null;
1592
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, effectiveModelForGate);
1298
1593
  try {
1299
1594
  assertSessionAllowsProductModel(sessionForModelGate, effectiveModelForGate);
1300
1595
  } catch (error) {
@@ -1329,6 +1624,9 @@ export async function postUserMessageTurn(input: {
1329
1624
  subjectId: input.actor ?? accountId,
1330
1625
  },
1331
1626
  operationKey,
1627
+ ...(input.boundaryRequestHash
1628
+ ? { boundaryRequestHash: input.boundaryRequestHash }
1629
+ : {}),
1332
1630
  delivery: input.delivery ?? "send",
1333
1631
  controlEtag: input.controlEtag ?? null,
1334
1632
  expectedDraftRevision: input.expectedDraftRevision ?? null,
@@ -1356,6 +1654,29 @@ export async function postUserMessageTurn(input: {
1356
1654
  }
1357
1655
  : {}),
1358
1656
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1657
+ ...(freshWorkspaceCustomModel
1658
+ ? {
1659
+ beforeFreshPromptCommit: async (tx: Database): Promise<void> => {
1660
+ const reference = workspaceCustomModelReference(
1661
+ settings,
1662
+ freshWorkspaceCustomModel,
1663
+ );
1664
+ if (!reference) {
1665
+ throw new Error("workspace custom model reference disappeared");
1666
+ }
1667
+ const active = await lockActiveCustomModelForAdmission(tx, {
1668
+ accountId,
1669
+ workspaceId,
1670
+ reference,
1671
+ });
1672
+ if (!active) {
1673
+ throw new HTTPException(422, {
1674
+ message: `model is not available: ${freshWorkspaceCustomModel}`,
1675
+ });
1676
+ }
1677
+ },
1678
+ }
1679
+ : {}),
1359
1680
  controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
1360
1681
  }),
1361
1682
  ),
@@ -1386,125 +1707,31 @@ export async function postUserMessageTurn(input: {
1386
1707
  }
1387
1708
  throw error;
1388
1709
  }
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
- };
1484
- }
1485
-
1486
- /**
1487
- * Full create-session flow shared by `POST /sessions` and the first-party MCP
1488
- * `session_create` tool: payload validation, resource/tool/variableSet
1489
- * checks, usage limits, session start, and usage recording. `rawPayload` is
1490
- * the unparsed request body so absent-vs-empty execution-context fields keep
1491
- * their meaning: a child inherits omitted resources/tools/mcpServers from its
1492
- * trusted immediate parent, while explicit arrays (including []) win. A
1493
- * top-level create with omitted tools applies workspace-default capability MCPs.
1494
- */
1495
- export function resolveChildGoalFromAcceptedSnapshot(
1496
- goal: GoalSpec,
1497
- parentGoalSnapshot: SessionGoalSnapshot,
1498
- ): GoalSpec {
1499
- const inheritedRootConstraints =
1500
- parentGoalSnapshot.state === "none" ? [] : parentGoalSnapshot.rootConstraints;
1501
- const requestedRootConstraints = goal.rootConstraints;
1502
- if (
1503
- requestedRootConstraints?.some((constraint) => !inheritedRootConstraints.includes(constraint))
1504
- ) {
1505
- throw new Error(
1506
- "child goal rootConstraints must be an exact subset of the calling turn's frozen root constraints",
1507
- );
1710
+ return finalizePostUserMessageTurn(input, result);
1711
+ }
1712
+
1713
+ /**
1714
+ * Full create-session flow shared by `POST /sessions` and the first-party MCP
1715
+ * `session_create` tool: payload validation, resource/tool/variableSet
1716
+ * checks, usage limits, session start, and usage recording. `rawPayload` is
1717
+ * the unparsed request body so absent-vs-empty execution-context fields keep
1718
+ * their meaning: a child inherits omitted resources/tools/mcpServers from its
1719
+ * trusted immediate parent, while explicit arrays (including []) win. A
1720
+ * top-level create with omitted tools applies workspace-default capability MCPs.
1721
+ */
1722
+ export function resolveChildGoalFromAcceptedSnapshot(
1723
+ goal: GoalSpec,
1724
+ parentGoalSnapshot: SessionGoalSnapshot,
1725
+ ): GoalSpec {
1726
+ const inheritedRootConstraints =
1727
+ parentGoalSnapshot.state === "none" ? [] : parentGoalSnapshot.rootConstraints;
1728
+ const requestedRootConstraints = goal.rootConstraints;
1729
+ if (
1730
+ requestedRootConstraints?.some((constraint) => !inheritedRootConstraints.includes(constraint))
1731
+ ) {
1732
+ throw new Error(
1733
+ "child goal rootConstraints must be an exact subset of the calling turn's frozen root constraints",
1734
+ );
1508
1735
  }
1509
1736
  return {
1510
1737
  ...goal,
@@ -1532,16 +1759,84 @@ export function resolveSessionCreateVisibility(input: {
1532
1759
  return input.requestedVisibility === "private" ? "user_private" : "workspace_shared";
1533
1760
  }
1534
1761
 
1762
+ async function resolveWorkspaceModelBoundarySettings(
1763
+ deps: Pick<ApiRouteDeps, "db" | "settings" | "catalogSourceSettings">,
1764
+ grant: AccessGrant,
1765
+ workspaceId: string,
1766
+ modelIds: readonly (string | null | undefined)[],
1767
+ retainedProductModelId?: string | null,
1768
+ ): Promise<Settings> {
1769
+ const retainedCatalogModel = isCatalogOverlayModel(retainedProductModelId);
1770
+ if (deps.catalogSourceSettings) {
1771
+ // The adapter already resolved one exact workspace catalog snapshot for
1772
+ // this request. Preserve it for fresh selections, but an existing session
1773
+ // may name a retired custom model that the active-only adapter snapshot
1774
+ // intentionally omitted. Re-open only the unoverlaid source for that
1775
+ // retention lookup; never feed the synthetic workspace provider back
1776
+ // through deployment validation.
1777
+ if (!retainedCatalogModel) return deps.settings;
1778
+ }
1779
+ const needsWorkspaceResolution =
1780
+ deps.settings.modelCatalogSource === "database" || modelIds.some(isCatalogOverlayModel);
1781
+ if (!needsWorkspaceResolution) return deps.settings;
1782
+ return (
1783
+ await resolveWorkspaceCatalogSettings(deps.db, deps.catalogSourceSettings ?? deps.settings, {
1784
+ accountId: grant.accountId,
1785
+ workspaceId,
1786
+ ...(retainedProductModelId !== undefined ? { retainedProductModelId } : {}),
1787
+ })
1788
+ ).settings;
1789
+ }
1790
+
1791
+ async function withSessionCreateUsageRecording(input: {
1792
+ deps: ApiRouteDeps;
1793
+ grant: AccessGrant;
1794
+ workspaceId: string;
1795
+ startMode: "realtime" | undefined;
1796
+ origin: "system" | "user";
1797
+ createOutcome: CreateSessionOutcome;
1798
+ }): Promise<CreateSessionRequestOutcome> {
1799
+ let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
1800
+ if (input.startMode !== "realtime") {
1801
+ try {
1802
+ await recordWorkspaceUsage(input.deps, {
1803
+ accountId: input.grant.accountId,
1804
+ workspaceId: input.workspaceId,
1805
+ subjectId: input.grant.subjectId,
1806
+ eventType: "agent_run.created",
1807
+ quantity: 1,
1808
+ unit: "run",
1809
+ sourceResourceType: "session",
1810
+ sourceResourceId: input.createOutcome.session.id,
1811
+ sessionId: input.createOutcome.session.id,
1812
+ initiator: input.createOutcome.session.createdBy,
1813
+ initiatorContext: input.createOutcome.session.createdByContext,
1814
+ origin: input.origin,
1815
+ idempotencyKey: `agent_run.created:${input.workspaceId}:${input.createOutcome.session.id}`,
1816
+ });
1817
+ } catch (error) {
1818
+ usageRecording = "failed";
1819
+ reportSessionUsageRecordingFailure(error);
1820
+ }
1821
+ }
1822
+ return { ...input.createOutcome, usageRecording };
1823
+ }
1824
+
1535
1825
  export async function createSessionForRequestWithOutcome(
1536
- deps: ApiRouteDeps,
1826
+ unresolvedDeps: ApiRouteDeps,
1537
1827
  grant: AccessGrant,
1538
1828
  workspaceId: string,
1539
1829
  rawPayload: unknown,
1540
1830
  authorization?: AccessGrantAuthorization,
1541
1831
  agentChildPresentation?: AgentChildSessionCreatePresentation,
1542
1832
  ): Promise<CreateSessionRequestOutcome> {
1543
- const { settings, db, bus, workflowClient, objectStorage } = deps;
1544
1833
  const payload = CreateSessionRequest.parse(rawPayload);
1834
+ if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
1835
+ throw new HTTPException(422, {
1836
+ message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
1837
+ });
1838
+ }
1839
+ const db = unresolvedDeps.db;
1545
1840
  const visibilityProvided = hasOwnProperty(rawPayload, "visibility");
1546
1841
  if (payload.visibility === "private" && !grant.metadata?.["sessionId"]) {
1547
1842
  if (!authorization) {
@@ -1549,39 +1844,13 @@ export async function createSessionForRequestWithOutcome(
1549
1844
  message: "managed human session required",
1550
1845
  });
1551
1846
  }
1552
- await requireManagedHumanPrivateSessionCreate(deps, authorization, workspaceId);
1847
+ await requireManagedHumanPrivateSessionCreate(unresolvedDeps, authorization, workspaceId);
1553
1848
  if (payload.sandbox === "shared" || typeof payload.sandbox === "object") {
1554
1849
  throw new HTTPException(422, {
1555
1850
  message: "Only-me sessions require their own sandbox",
1556
1851
  });
1557
1852
  }
1558
1853
  }
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
1854
  // Parent linkage and execution-context inheritance come ONLY from the
1586
1855
  // worker-signed sessionId claim. A caller cannot nominate a parent in the
1587
1856
  // payload, so inheriting an existing repository/tool/credential snapshot does
@@ -1592,7 +1861,7 @@ export async function createSessionForRequestWithOutcome(
1592
1861
  : null;
1593
1862
  if (parentSessionId) {
1594
1863
  try {
1595
- await requireSessionAuthorization(deps, grant, {
1864
+ await requireSessionAuthorization(unresolvedDeps, grant, {
1596
1865
  sessionId: parentSessionId,
1597
1866
  operation: "session.child.create",
1598
1867
  surface: "core",
@@ -1650,6 +1919,118 @@ export async function createSessionForRequestWithOutcome(
1650
1919
  message: "caller attempt does not belong to the parent session",
1651
1920
  });
1652
1921
  }
1922
+ const replayManagedHumanSubjectId = creationInitiator.actor
1923
+ ? (parentCallingTurn?.initiatingHumanSubjectId ??
1924
+ (parentCallingTurn?.initiator.kind === "subject"
1925
+ ? parentCallingTurn.initiator.subjectId
1926
+ : null))
1927
+ : authorization?.canonicalManagedHumanSession || grant.principalKind === "human_session"
1928
+ ? grant.subjectId
1929
+ : null;
1930
+ let retainedKeyedShellModel: string | null = null;
1931
+ if (
1932
+ payload.idempotencyKey &&
1933
+ (effectiveVisibility !== "user_private" || replayManagedHumanSubjectId !== null)
1934
+ ) {
1935
+ try {
1936
+ const initializedReplay = await getInitializedSessionCreateReplay(db, {
1937
+ accountId: grant.accountId,
1938
+ workspaceId,
1939
+ subjectId: replayManagedHumanSubjectId ?? grant.subjectId,
1940
+ ...(replayManagedHumanSubjectId
1941
+ ? { activeManagedHumanSubjectId: replayManagedHumanSubjectId }
1942
+ : {}),
1943
+ createIdempotencyKey: payload.idempotencyKey,
1944
+ selectedInstalledSkillIds: payload.installedSkillIds ?? [],
1945
+ ...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
1946
+ visibility: effectiveVisibility,
1947
+ variableSetIds: payload.variableSetIds ?? [],
1948
+ initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
1949
+ deferInitialTurn: payload.startMode === "realtime",
1950
+ });
1951
+ if (initializedReplay) {
1952
+ if (initializedReplay.outcome === "denied") {
1953
+ throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(initializedReplay.denial));
1954
+ }
1955
+ if (initializedReplay.outcome === "pending") {
1956
+ retainedKeyedShellModel = initializedReplay.session.model;
1957
+ } else {
1958
+ if (initializedReplay.workflowWakeRevision !== null) {
1959
+ await unresolvedDeps.workflowClient.wakeSessionWorkflow({
1960
+ accountId: grant.accountId,
1961
+ workspaceId,
1962
+ sessionId: initializedReplay.session.id,
1963
+ workflowId: initializedReplay.temporalWorkflowId,
1964
+ wakeRevision: initializedReplay.workflowWakeRevision,
1965
+ });
1966
+ }
1967
+ return await withSessionCreateUsageRecording({
1968
+ deps: unresolvedDeps,
1969
+ grant,
1970
+ workspaceId,
1971
+ startMode: payload.startMode,
1972
+ origin: creationInitiator.actor ? "system" : "user",
1973
+ createOutcome: {
1974
+ session: initializedReplay.session,
1975
+ outcome: initializedReplay.changed ? "repaired" : "replayed",
1976
+ replay: !initializedReplay.changed,
1977
+ changed: initializedReplay.changed,
1978
+ },
1979
+ });
1980
+ }
1981
+ }
1982
+ } catch (error) {
1983
+ if (error instanceof SessionIdConflictError) {
1984
+ throw new HTTPException(409, {
1985
+ message: "requested session id is already in use",
1986
+ });
1987
+ }
1988
+ if (error instanceof SessionCreateIdempotencyConflictError) {
1989
+ throw new HTTPException(409, { message: error.message, cause: error });
1990
+ }
1991
+ throw error;
1992
+ }
1993
+ }
1994
+ let settings = await resolveWorkspaceModelBoundarySettings(
1995
+ unresolvedDeps,
1996
+ grant,
1997
+ workspaceId,
1998
+ [payload.model],
1999
+ retainedKeyedShellModel,
2000
+ );
2001
+ let deps =
2002
+ settings === unresolvedDeps.settings
2003
+ ? unresolvedDeps
2004
+ : {
2005
+ ...unresolvedDeps,
2006
+ catalogSourceSettings: unresolvedDeps.catalogSourceSettings ?? unresolvedDeps.settings,
2007
+ settings,
2008
+ };
2009
+ const { bus, workflowClient, objectStorage } = deps;
2010
+ await requireAtomicPersonalResourceAttachment(
2011
+ deps,
2012
+ authorization,
2013
+ workspaceId,
2014
+ payload.personalResourceAttachment,
2015
+ false,
2016
+ );
2017
+ const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
2018
+ const effectiveModelId = payload.model ?? inheritedModel;
2019
+ const effectiveCatalogSettings = await resolveWorkspaceModelBoundarySettings(
2020
+ deps,
2021
+ grant,
2022
+ workspaceId,
2023
+ [effectiveModelId],
2024
+ parentSession ? inheritedModel : null,
2025
+ );
2026
+ if (effectiveCatalogSettings !== settings) {
2027
+ deps = {
2028
+ ...deps,
2029
+ catalogSourceSettings: deps.catalogSourceSettings ?? settings,
2030
+ settings: effectiveCatalogSettings,
2031
+ };
2032
+ settings = effectiveCatalogSettings;
2033
+ }
1653
2034
  let effectiveGoal = payload.goal;
1654
2035
  if (parentSession && payload.goal) {
1655
2036
  try {
@@ -1723,9 +2104,43 @@ export async function createSessionForRequestWithOutcome(
1723
2104
  ? payload.resources
1724
2105
  : (parentSession?.resources ?? payload.resources),
1725
2106
  );
1726
- const skills = hasOwnProperty(rawPayload, "skills")
2107
+ const inheritedOrSubmittedSkills = hasOwnProperty(rawPayload, "skills")
1727
2108
  ? payload.skills
1728
2109
  : (parentSession?.skills ?? payload.skills);
2110
+ const selectedInstalledSkillIds = payload.installedSkillIds ?? [];
2111
+ const selectedInstalledSkills: SessionSkill[] = [];
2112
+ if (selectedInstalledSkillIds.length > 0) {
2113
+ const installedSkills = await listInstalledPortableSkills(db, workspaceId, {
2114
+ includeSessionSelected: true,
2115
+ });
2116
+ const installedById = new Map(installedSkills.map((skill) => [skill.capabilityId, skill]));
2117
+ for (const capabilityId of selectedInstalledSkillIds) {
2118
+ const installed = installedById.get(capabilityId);
2119
+ if (!installed) {
2120
+ throw new HTTPException(422, {
2121
+ message: `Session-selected Skill is not installed in this workspace: ${capabilityId}`,
2122
+ });
2123
+ }
2124
+ if (installed.activationMode !== "session_selected") {
2125
+ throw new HTTPException(422, {
2126
+ message: `Installed Skill does not require explicit session selection: ${capabilityId}`,
2127
+ });
2128
+ }
2129
+ selectedInstalledSkills.push({
2130
+ name: installed.name,
2131
+ description: installed.description,
2132
+ files: installed.files.map((file) => ({ path: file.path, content: file.content })),
2133
+ });
2134
+ }
2135
+ }
2136
+ let skills: SessionSkill[];
2137
+ try {
2138
+ skills = SessionSkills.parse([...inheritedOrSubmittedSkills, ...selectedInstalledSkills]);
2139
+ } catch (error) {
2140
+ throw new HTTPException(422, {
2141
+ message: error instanceof Error ? error.message : "invalid session Skill selection",
2142
+ });
2143
+ }
1729
2144
  const toolsProvided = hasOwnProperty(rawPayload, "tools");
1730
2145
  // Visibility became durable draft state after older clients had already
1731
2146
  // written rows without it. Compare it only when the create request supplied
@@ -1860,8 +2275,7 @@ export async function createSessionForRequestWithOutcome(
1860
2275
  // managers: falling back to the deployment model would silently move a child
1861
2276
  // onto the OpenGeni-credits billing path. Legacy session-bound grants without
1862
2277
  // 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);
2278
+ const model = canonicalConfiguredModel(settings, effectiveModelId);
1865
2279
  if (model === null || model === undefined) {
1866
2280
  throw new Error("effective session model unexpectedly resolved to null");
1867
2281
  }
@@ -2025,7 +2439,10 @@ export async function createSessionForRequestWithOutcome(
2025
2439
  payload.firstPartyMcpTools,
2026
2440
  parentSession ? parentSession.firstPartyMcpTools : undefined,
2027
2441
  workspaceFirstPartyDefaults && !parentSession
2028
- ? { ...deploymentFirstPartyMcpToolPolicy, default: workspaceFirstPartyDefaults }
2442
+ ? {
2443
+ ...deploymentFirstPartyMcpToolPolicy,
2444
+ default: workspaceFirstPartyDefaults,
2445
+ }
2029
2446
  : deploymentFirstPartyMcpToolPolicy,
2030
2447
  );
2031
2448
  const googleDrivePublicationEnabled =
@@ -2101,7 +2518,10 @@ export async function createSessionForRequestWithOutcome(
2101
2518
  let sandboxGroupId: string | null = null;
2102
2519
  let inheritedBackend: Session["sandboxBackend"] | undefined;
2103
2520
  let inheritedSandboxOs: Session["sandboxOs"] | undefined;
2104
- let inheritedActiveTarget: { sandboxId: string; workingDir: string | null } | null = null;
2521
+ let inheritedActiveTarget: {
2522
+ sandboxId: string;
2523
+ workingDir: string | null;
2524
+ } | null = null;
2105
2525
  // ENV-AWARE GROUPING: under the CURRENT mechanics the workspace VariableSet is
2106
2526
  // creation-time box state — the box's manifest env is fixed when it is cold-
2107
2527
  // created, and the SDK's provided-session guard rejects any manifest-env delta
@@ -2405,9 +2825,12 @@ export async function createSessionForRequestWithOutcome(
2405
2825
  sessionMcpServers: sessionMcpServers.metadata,
2406
2826
  personalConnectionDelegations,
2407
2827
  initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
2828
+ workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
2829
+ retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
2408
2830
  ...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
2409
2831
  parentSessionId,
2410
2832
  createIdempotencyKey: payload.idempotencyKey ?? null,
2833
+ selectedInstalledSkillIds,
2411
2834
  maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
2412
2835
  allowNestedAgentDepthIncrease: hasPermission(grant.permissions, "workspace:admin"),
2413
2836
  subjectId: grant.subjectId,
@@ -2474,30 +2897,14 @@ export async function createSessionForRequestWithOutcome(
2474
2897
  }
2475
2898
  throw error;
2476
2899
  }
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 };
2900
+ return await withSessionCreateUsageRecording({
2901
+ deps,
2902
+ grant,
2903
+ workspaceId,
2904
+ startMode: payload.startMode,
2905
+ origin: creationInitiator.actor ? "system" : "user",
2906
+ createOutcome,
2907
+ });
2501
2908
  }
2502
2909
 
2503
2910
  /** @internal Fixed public projection; the committed session outcome remains authoritative. */
@@ -2525,6 +2932,53 @@ export async function createSessionForRequest(
2525
2932
  ).session;
2526
2933
  }
2527
2934
 
2935
+ function sessionPromptBoundaryRequestHash(input: {
2936
+ delivery: "send" | "steer";
2937
+ controlEtag: string | null;
2938
+ expectedDraftRevision: number | null;
2939
+ text: string;
2940
+ annotations: SubmittedTimelineAnnotation[];
2941
+ modelContext: string | null;
2942
+ resources: ResourceRef[];
2943
+ composerDraftResources?: ResourceRef[];
2944
+ model: string | null;
2945
+ reasoningEffort: ReasoningEffort | null;
2946
+ latencyMode: "standard" | "priority" | "fast" | null;
2947
+ source: "user" | "api";
2948
+ mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
2949
+ connectionAuthorities?: McpConnectionAuthoritySelection[];
2950
+ personalResourceAttachment?: PersonalResourceAttachmentIntent;
2951
+ commandActor: SessionCommandActor;
2952
+ }): string {
2953
+ return `prompt-boundary-v1:${canonicalSessionCommandHash({
2954
+ delivery: input.delivery,
2955
+ controlEtag: input.controlEtag,
2956
+ expectedDraftRevision: input.expectedDraftRevision,
2957
+ text: input.text,
2958
+ annotations: input.annotations,
2959
+ modelContext: input.modelContext,
2960
+ resources: input.resources,
2961
+ composerDraftResourcesProvided: input.composerDraftResources !== undefined,
2962
+ composerDraftResources: input.composerDraftResources ?? [],
2963
+ model: input.model,
2964
+ reasoningEffort: input.reasoningEffort,
2965
+ latencyMode: input.latencyMode,
2966
+ source: input.source,
2967
+ mcpCredentialUpdates: input.mcpCredentialUpdates,
2968
+ connectionAuthorities: input.connectionAuthorities ?? [],
2969
+ personalResourceAttachment: input.personalResourceAttachment ?? null,
2970
+ ...(input.commandActor.type === "service"
2971
+ ? {
2972
+ serviceInitiator: {
2973
+ subjectId: input.commandActor.subjectId,
2974
+ subjectLabel: input.commandActor.subjectLabel ?? null,
2975
+ context: input.commandActor.context ?? {},
2976
+ },
2977
+ }
2978
+ : {}),
2979
+ })}`;
2980
+ }
2981
+
2528
2982
  /**
2529
2983
  * Full accept-user-message flow shared by the `user.message` branch of
2530
2984
  * `POST /sessions/:id/events` and the first-party MCP `session_send_message`
@@ -2566,192 +3020,316 @@ export async function acceptSessionUserMessageWithOutcome(
2566
3020
  interruptionCount: number;
2567
3021
  replay: boolean;
2568
3022
  }> {
2569
- const { settings, db, bus, workflowClient, objectStorage } = deps;
3023
+ const { db, bus, workflowClient, objectStorage } = deps;
2570
3024
  const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
3025
+ const delivery = input.delivery ?? "send";
3026
+ const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
3027
+ const commandActor: SessionCommandActor = delegatedServiceInitiator
3028
+ ? {
3029
+ type: "service",
3030
+ subjectId: delegatedServiceInitiator.initiator.subjectId,
3031
+ ...(delegatedServiceInitiator.initiator.label
3032
+ ? { subjectLabel: delegatedServiceInitiator.initiator.label }
3033
+ : {}),
3034
+ context: delegatedServiceInitiator.context,
3035
+ }
3036
+ : { type: "human", subjectId: grant.subjectId };
2571
3037
  await requireSessionAuthorization(deps, grant, {
2572
3038
  sessionId,
2573
- operation: input.delivery === "steer" ? "session.steer" : "session.append",
3039
+ operation: delivery === "steer" ? "session.steer" : "session.append",
2574
3040
  surface: "core",
2575
3041
  });
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
3042
  const requestedResources = normalizeResources(input.resources ?? []);
2615
3043
  const composerDraftResources = input.composerDraftResources
2616
3044
  ? normalizeResources(input.composerDraftResources)
2617
3045
  : 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)),
3046
+ const boundaryRequestHash = input.clientEventId
3047
+ ? sessionPromptBoundaryRequestHash({
3048
+ delivery,
3049
+ controlEtag: input.controlEtag ?? null,
3050
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
3051
+ text: input.text,
3052
+ annotations: input.annotations ?? [],
3053
+ modelContext: input.modelContext ?? null,
3054
+ resources: requestedResources,
3055
+ ...(composerDraftResources ? { composerDraftResources } : {}),
3056
+ model: input.model ?? null,
3057
+ reasoningEffort: input.reasoningEffort ?? null,
3058
+ latencyMode: input.latencyMode ?? null,
3059
+ source,
3060
+ mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
3061
+ ...(input.connectionAuthorities
3062
+ ? { connectionAuthorities: input.connectionAuthorities }
3063
+ : {}),
3064
+ ...(input.personalResourceAttachment
3065
+ ? { personalResourceAttachment: input.personalResourceAttachment }
3066
+ : {}),
3067
+ commandActor,
3068
+ })
3069
+ : null;
3070
+ if (input.clientEventId && boundaryRequestHash) {
3071
+ const replay = await withWorkspaceSubjectSessionActivityRls(
3072
+ db,
3073
+ workspaceId,
3074
+ grant.subjectId,
3075
+ async (scopedDb) =>
3076
+ await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
3077
+ workspaceId,
3078
+ sessionId,
3079
+ subjectId: grant.subjectId,
3080
+ actor: commandActor,
3081
+ operationKey: input.clientEventId!,
3082
+ delivery,
3083
+ boundaryRequestHash,
3084
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
3085
+ }),
2622
3086
  );
2623
- if (unacceptedDraftResource) {
2624
- throw new HTTPException(422, {
2625
- message: "composer draft resources must be included in the accepted resource set",
2626
- });
3087
+ if (replay) {
3088
+ return finalizePostUserMessageTurn(
3089
+ {
3090
+ db,
3091
+ bus,
3092
+ workflowClient,
3093
+ accountId: grant.accountId,
3094
+ workspaceId,
3095
+ sessionId,
3096
+ delivery,
3097
+ ...(deps.schedulePromptPostCommit
3098
+ ? { schedulePostCommit: deps.schedulePromptPostCommit }
3099
+ : {}),
3100
+ },
3101
+ replay,
3102
+ );
2627
3103
  }
2628
3104
  }
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",
3105
+ try {
3106
+ await requireAtomicPersonalResourceAttachment(
3107
+ deps,
3108
+ input.authorization,
3109
+ workspaceId,
3110
+ input.personalResourceAttachment,
3111
+ true,
3112
+ );
3113
+ // Hoisted above requireLimit so the codex-billed predicate can resolve the
3114
+ // turn's effective model (a follow-up turn inherits the session's model). A
3115
+ // pure read with no side effects.
3116
+ const existingSession = await requireSession(db, workspaceId, sessionId);
3117
+ const settings = await resolveWorkspaceModelBoundarySettings(
3118
+ deps,
3119
+ grant,
3120
+ workspaceId,
3121
+ [input.model ?? existingSession.model],
3122
+ existingSession.model,
3123
+ );
3124
+ if (settings !== deps.settings) {
3125
+ deps = {
3126
+ ...deps,
3127
+ catalogSourceSettings: deps.catalogSourceSettings ?? deps.settings,
3128
+ settings,
3129
+ };
3130
+ }
3131
+ const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
3132
+ const effectiveModel =
3133
+ canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
3134
+ if (effectiveModel === null) {
3135
+ throw new Error("effective follow-up model unexpectedly resolved to null");
3136
+ }
3137
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
3138
+ try {
3139
+ assertSessionAllowsProductModel(existingSession, effectiveModel);
3140
+ } catch (error) {
3141
+ if (error instanceof CodexCompactionV2ProviderLockedError) {
3142
+ throw new HTTPException(422, { message: error.message, cause: error });
3143
+ }
3144
+ throw error;
3145
+ }
3146
+ const sessionReasoningEffort = existingSession.reasoningEffort;
3147
+ const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
3148
+ const sessionLatencyMode = existingSession.latencyMode;
3149
+ const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
3150
+ const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
3151
+ modelId: effectiveModel,
3152
+ requestedModelId: input.model ?? null,
3153
+ modelSource: input.model == null ? "session" : "explicit",
3154
+ reasoningEffort: effectiveReasoningEffort,
3155
+ reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
3156
+ latencyMode: effectiveLatencyMode,
3157
+ latencyModeSource: input.latencyMode == null ? "session" : "explicit",
2645
3158
  });
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({
3159
+ if (composerDraftResources) {
3160
+ const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
3161
+ const unacceptedDraftResource = composerDraftResources.find(
3162
+ (resource) => !acceptedResources.has(stableJson(resource)),
3163
+ );
3164
+ if (unacceptedDraftResource) {
3165
+ throw new HTTPException(422, {
3166
+ message: "composer draft resources must be included in the accepted resource set",
3167
+ });
3168
+ }
3169
+ }
3170
+ const annotations = await validateSubmittedTimelineAnnotations(
2706
3171
  db,
2707
- bus,
2708
- workflowClient,
2709
- settings,
2710
- accountId: grant.accountId,
2711
3172
  workspaceId,
2712
3173
  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
3174
+ input.annotations ?? [],
3175
+ );
3176
+ await requireLimit(deps, {
3177
+ accountId: grant.accountId,
3178
+ workspaceId,
3179
+ action: "agent_run:create",
3180
+ quantity: 1,
3181
+ model: effectiveModel,
3182
+ });
3183
+ if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
3184
+ throw new HTTPException(503, {
3185
+ message: "object storage is not configured",
3186
+ });
3187
+ }
3188
+ await validateFileResources(
3189
+ db,
3190
+ grant.accountId,
3191
+ workspaceId,
3192
+ grant.subjectId,
3193
+ requestedResources,
3194
+ );
3195
+ await validateGitHubRepositorySelection(db, workspaceId, [
3196
+ ...existingSession.resources,
3197
+ ...requestedResources,
3198
+ ]);
3199
+ const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
3200
+ settings,
3201
+ grant,
3202
+ session: existingSession,
3203
+ updates: input.mcpCredentialUpdates ?? [],
3204
+ });
3205
+ const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
3206
+ const inheritedPersonalConnectionDelegations =
3207
+ connectionDelegationSource.kind === "turn"
3208
+ ? await getSessionTurnPersonalConnectionDelegations(
3209
+ db,
3210
+ workspaceId,
3211
+ connectionDelegationSource.sessionId,
3212
+ connectionDelegationSource.turnId,
3213
+ )
3214
+ : null;
3215
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
3216
+ db,
3217
+ workspaceId,
3218
+ settings,
3219
+ inheritedPersonalConnectionDelegations
2733
3220
  ? {
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
- },
3221
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
2742
3222
  }
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
- : {}),
3223
+ : { subjectId: grant.subjectId },
3224
+ );
3225
+ const personalConnectionDelegations = await freezePersonalConnectionDelegations({
3226
+ db,
3227
+ workspaceId,
3228
+ settings: runtimeSettings,
3229
+ tools: existingSession.tools,
3230
+ resources: [...existingSession.resources, ...requestedResources],
3231
+ source: connectionDelegationSource,
3232
+ targetSessionId: sessionId,
3233
+ googleDrivePublicationEnabled:
3234
+ existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
3235
+ existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
3236
+ (!existingSession.firstPartyMcpPermissions?.length ||
3237
+ (existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
3238
+ existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
3239
+ atlassianEnabled:
3240
+ existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
3241
+ (!existingSession.firstPartyMcpPermissions?.length ||
3242
+ existingSession.firstPartyMcpPermissions.includes("connections:read")),
3243
+ ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
2753
3244
  });
2754
- return { accepted, turn, draft, receipt, routing, interruptionCount, replay };
3245
+ const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
3246
+ await postUserMessageTurn({
3247
+ db,
3248
+ bus,
3249
+ workflowClient,
3250
+ settings,
3251
+ accountId: grant.accountId,
3252
+ workspaceId,
3253
+ sessionId,
3254
+ text: input.text,
3255
+ annotations,
3256
+ modelContext: input.modelContext ?? null,
3257
+ resources: requestedResources,
3258
+ ...(composerDraftResources ? { composerDraftResources } : {}),
3259
+ model: input.model ?? null,
3260
+ reasoningEffort: input.reasoningEffort ?? null,
3261
+ latencyMode: input.latencyMode ?? null,
3262
+ reasoningEffortFallback: sessionReasoningEffort,
3263
+ turnExecutionPolicy,
3264
+ mcpCredentialUpdates,
3265
+ personalConnectionDelegations,
3266
+ ...(input.personalResourceAttachment
3267
+ ? { personalResourceAttachment: input.personalResourceAttachment }
3268
+ : {}),
3269
+ delivery,
3270
+ origin: source === "api" ? "operator" : "human",
3271
+ actor: grant.subjectId,
3272
+ ...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
3273
+ commandActor,
3274
+ ...(boundaryRequestHash ? { boundaryRequestHash } : {}),
3275
+ ...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
3276
+ ...(input.expectedDraftRevision !== undefined
3277
+ ? { expectedDraftRevision: input.expectedDraftRevision }
3278
+ : {}),
3279
+ ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
3280
+ recordAgentRunUsage: true,
3281
+ ...(deps.schedulePromptPostCommit
3282
+ ? { schedulePostCommit: deps.schedulePromptPostCommit }
3283
+ : {}),
3284
+ });
3285
+ return {
3286
+ accepted,
3287
+ turn,
3288
+ draft,
3289
+ receipt,
3290
+ routing,
3291
+ interruptionCount,
3292
+ replay,
3293
+ };
3294
+ } catch (error) {
3295
+ if (input.clientEventId && boundaryRequestHash) {
3296
+ const replay = await withWorkspaceSubjectSessionActivityRls(
3297
+ db,
3298
+ workspaceId,
3299
+ grant.subjectId,
3300
+ async (scopedDb) =>
3301
+ await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
3302
+ workspaceId,
3303
+ sessionId,
3304
+ subjectId: grant.subjectId,
3305
+ actor: commandActor,
3306
+ operationKey: input.clientEventId!,
3307
+ delivery,
3308
+ boundaryRequestHash,
3309
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
3310
+ serializeOperation: true,
3311
+ }),
3312
+ );
3313
+ if (replay) {
3314
+ return finalizePostUserMessageTurn(
3315
+ {
3316
+ db,
3317
+ bus,
3318
+ workflowClient,
3319
+ accountId: grant.accountId,
3320
+ workspaceId,
3321
+ sessionId,
3322
+ delivery,
3323
+ ...(deps.schedulePromptPostCommit
3324
+ ? { schedulePostCommit: deps.schedulePromptPostCommit }
3325
+ : {}),
3326
+ },
3327
+ replay,
3328
+ );
3329
+ }
3330
+ }
3331
+ throw error;
3332
+ }
2755
3333
  }
2756
3334
 
2757
3335
  /** Backward-compatible entity-returning path used by existing REST callers. */