@opengeni/core 2.5.3 → 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.
Files changed (46) hide show
  1. package/dist/access/index.d.ts +24 -0
  2. package/dist/billing/limits.d.ts +5 -0
  3. package/dist/canonical-human-identities.js +2 -2
  4. package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
  5. package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
  6. package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
  7. package/dist/dependencies.d.ts +10 -1
  8. package/dist/domain/company-brain-governed-writes.d.ts +15 -6
  9. package/dist/domain/company-profile-agent-admin.d.ts +3 -2
  10. package/dist/domain/environments.d.ts +1 -1
  11. package/dist/domain/memory-slack-delivery.d.ts +4 -1
  12. package/dist/domain/personal-connection-delegations.d.ts +1 -0
  13. package/dist/domain/pr-review.d.ts +1 -1
  14. package/dist/domain/scheduled-tasks.d.ts +12 -0
  15. package/dist/domain/sessions.d.ts +37 -11
  16. package/dist/domain/workspace-members.d.ts +8 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1706 -667
  19. package/dist/index.js.map +1 -1
  20. package/dist/managed-auth-session-sets.d.ts +18 -0
  21. package/dist/managed-auth-session-sets.js +3 -1
  22. package/dist/model-catalog.d.ts +89 -0
  23. package/dist/sandbox/fleet.d.ts +6 -4
  24. package/dist/sandbox/routing.d.ts +7 -2
  25. package/dist/sandbox/runtime-settings.d.ts +17 -1
  26. package/package.json +10 -10
  27. package/src/access/index.ts +140 -5
  28. package/src/application/user-resource-grants.ts +31 -2
  29. package/src/billing/limits.ts +57 -24
  30. package/src/dependencies.ts +15 -1
  31. package/src/domain/company-brain-governed-writes.ts +29 -13
  32. package/src/domain/company-profile-agent-admin.ts +3 -2
  33. package/src/domain/environments.ts +6 -34
  34. package/src/domain/memory-slack-delivery.ts +30 -0
  35. package/src/domain/personal-connection-delegations.ts +40 -7
  36. package/src/domain/remember.ts +5 -6
  37. package/src/domain/scheduled-tasks.ts +146 -1
  38. package/src/domain/sessions.ts +912 -358
  39. package/src/domain/workspace-members.ts +34 -2
  40. package/src/index.ts +1 -0
  41. package/src/managed-auth-session-sets.ts +38 -11
  42. package/src/model-catalog.ts +565 -0
  43. package/src/sandbox/fleet.ts +20 -17
  44. package/src/sandbox/routing.ts +18 -4
  45. package/src/sandbox/runtime-settings.ts +32 -0
  46. /package/dist/{chunk-ZVZJTMSV.js.map → chunk-OF65T3PM.js.map} +0 -0
@@ -2,14 +2,16 @@ 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";
12
13
  import {
14
+ AUTOMATIC_SESSION_TITLE_FALLBACK,
13
15
  CreateSessionRequest,
14
16
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
15
17
  DEFAULT_FIRST_PARTY_MCP_TOOLS,
@@ -20,7 +22,10 @@ import {
20
22
  ServiceTurnInitiator,
21
23
  ServiceTurnInitiatorContext,
22
24
  evaluateWorkspaceModelPolicy,
25
+ normalizeAutomaticSessionTitle,
23
26
  resolveWorkspaceSessionToolDefaults,
27
+ metadataWithTurnExecutionPolicyV1,
28
+ readTurnExecutionPolicyV1,
24
29
  stableJson,
25
30
  type AccessGrant,
26
31
  type ComposerDraft,
@@ -60,6 +65,7 @@ import {
60
65
  import {
61
66
  createSession,
62
67
  createSessionWithIdempotencyKeyResult,
68
+ canonicalSessionCommandHash,
63
69
  encryptVariableSetValue,
64
70
  getAnySessionInGroup,
65
71
  getEnrollment,
@@ -70,10 +76,10 @@ import {
70
76
  listDistinctRigVersionIdsInGroup,
71
77
  getSandbox,
72
78
  getSession,
79
+ getInitializedSessionCreateReplay,
73
80
  getSessionAuthorityProjection,
74
81
  SessionIdConflictError,
75
82
  NewSessionDraftConflictError,
76
- getSessionSpawnDenialByIdempotencyKey,
77
83
  getWorkspaceControlEvent,
78
84
  getSessionLineage,
79
85
  getSessionTurn,
@@ -85,7 +91,10 @@ import {
85
91
  initializeSessionStartAtomically,
86
92
  listSessionTurns,
87
93
  listSessionMcpServersForChildInheritance,
94
+ lockActiveWorkspaceGatewayCustomModelForAdmission,
95
+ lockActiveWorkspaceOpenRouterCustomModelForAdmission,
88
96
  requireSession,
97
+ replaySubmittedHumanPromptFromBoundaryReceipt,
89
98
  submitHumanPromptInTransaction,
90
99
  appendSessionEventsWithLockedSessionUpdate,
91
100
  updateSessionTitleWithEvent,
@@ -126,6 +135,12 @@ import {
126
135
  SessionAuthorizationDeniedError,
127
136
  } from "../session-authorization";
128
137
  import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
138
+ import { managedSessionGroupBackend } from "../sandbox/runtime-settings";
139
+ import {
140
+ isWorkspaceCustomModelId,
141
+ resolveWorkspaceCatalogSettings,
142
+ workspaceCustomModelReference,
143
+ } from "../model-catalog";
129
144
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
130
145
  import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
131
146
  import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
@@ -607,6 +622,44 @@ export type CreateSessionRequestOutcome = CreateSessionOutcome & {
607
622
  usageRecording: "recorded" | "failed";
608
623
  };
609
624
 
625
+ type AgentChildSessionCreatePresentation = {
626
+ /** Model-authored title candidate from the first-party `session_create` tool.
627
+ * This is deliberately separate from the public REST request contract. */
628
+ automaticTitleCandidate?: string | null;
629
+ };
630
+
631
+ const AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY = "agentChildAutomaticTitle" as const;
632
+
633
+ /** @internal Exported for the keyed-create repair regression. */
634
+ export function freezeAgentChildAutomaticTitleInCreatorContext(
635
+ context: TurnInitiatorContext | undefined,
636
+ title: string | null | undefined,
637
+ ): TurnInitiatorContext | undefined {
638
+ return title ? { ...(context ?? {}), [AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY]: title } : context;
639
+ }
640
+
641
+ /** @internal Keyed repair must use the committed winner, not the retry payload. */
642
+ export function initialAutomaticTitleForSessionStart(
643
+ session: Pick<Session, "createdByContext">,
644
+ requestedTitle: string | null | undefined,
645
+ ): string | null {
646
+ const frozenTitle = session.createdByContext[AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY];
647
+ return typeof frozenTitle === "string" ? frozenTitle : (requestedTitle ?? null);
648
+ }
649
+
650
+ function automaticTitleForAgentChildCreate(
651
+ presentation: AgentChildSessionCreatePresentation,
652
+ goal: GoalSpec | null | undefined,
653
+ initialMessage: string | null | undefined,
654
+ ): string | null {
655
+ for (const candidate of [presentation.automaticTitleCandidate, goal?.text, initialMessage]) {
656
+ if (typeof candidate !== "string") continue;
657
+ const normalized = normalizeAutomaticSessionTitle(candidate);
658
+ if (normalized && normalized !== AUTOMATIC_SESSION_TITLE_FALLBACK) return normalized;
659
+ }
660
+ return null;
661
+ }
662
+
610
663
  export async function createAndStartSessionWithOutcome(input: {
611
664
  requestedSessionId?: string;
612
665
  db: Database;
@@ -615,6 +668,16 @@ export async function createAndStartSessionWithOutcome(input: {
615
668
  /** Internal database-only composition seam. The exact session shell and this
616
669
  * linkage commit together before its first event/turn can be initialized. */
617
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;
618
681
  accountId: string;
619
682
  workspaceId: string;
620
683
  visibility?: "user_private" | "workspace_shared";
@@ -652,6 +715,9 @@ export async function createAndStartSessionWithOutcome(input: {
652
715
  // resolved workspace-scoped by the caller). Null/omitted ⇒ unfiled (inbox).
653
716
  channelId?: string | null;
654
717
  goal?: GoalSpec | null;
718
+ /** Trusted sensitive-safe automatic title for an agent-created child. The
719
+ * atomic initializer commits the row mutation and `session.title_set`. */
720
+ initialAutomaticTitle?: string | null;
655
721
  // Per-session agent persona/system instructions (org-visible metadata, not a
656
722
  // secret). Persisted on the session row and composed system-level AFTER the
657
723
  // workspace agentInstructions at turn time; never emitted as a timeline event.
@@ -731,12 +797,56 @@ export async function createAndStartSessionWithOutcome(input: {
731
797
  allowNestedAgentDepthIncrease?: boolean;
732
798
  subjectId?: string | null;
733
799
  }): Promise<CreateSessionOutcome> {
734
- const sessionMetadata = {
735
- ...input.metadata,
736
- model: input.model,
737
- reasoningEffort: input.reasoningEffort,
738
- ...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
739
- };
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
+ );
809
+ const frozenCreatedByContext = freezeAgentChildAutomaticTitleInCreatorContext(
810
+ input.createdByContext,
811
+ input.initialAutomaticTitle,
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;
740
850
  // Keyed creation is intentionally handled only by the database admission
741
851
  // transaction below. Its workspace/key lock replays either the successful
742
852
  // session or the committed denial atomically; an application-side lookup
@@ -755,7 +865,7 @@ export async function createAndStartSessionWithOutcome(input: {
755
865
  toolPolicy: input.toolPolicy,
756
866
  metadata: sessionMetadata,
757
867
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
758
- ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
868
+ ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
759
869
  createdByActor: input.createdByActor ?? null,
760
870
  model: input.model,
761
871
  reasoningEffort: input.reasoningEffort,
@@ -786,15 +896,29 @@ export async function createAndStartSessionWithOutcome(input: {
786
896
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
787
897
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
788
898
  subjectId: input.subjectId ?? null,
789
- ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
899
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
790
900
  });
791
901
  if (keyedResult.denied) {
792
902
  throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
793
903
  }
794
904
  const { session: keyed, created } = keyedResult;
795
905
  if (!created) {
906
+ const persistedPolicy = readTurnExecutionPolicyV1(keyed.metadata);
796
907
  const finished = await finishStartSession(
797
- 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
+ },
798
922
  keyed,
799
923
  );
800
924
  return {
@@ -827,7 +951,7 @@ export async function createAndStartSessionWithOutcome(input: {
827
951
  toolPolicy: input.toolPolicy,
828
952
  metadata: sessionMetadata,
829
953
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
830
- ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
954
+ ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
831
955
  createdByActor: input.createdByActor ?? null,
832
956
  model: input.model,
833
957
  reasoningEffort: input.reasoningEffort,
@@ -857,7 +981,7 @@ export async function createAndStartSessionWithOutcome(input: {
857
981
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
858
982
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
859
983
  subjectId: input.subjectId ?? null,
860
- ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
984
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
861
985
  });
862
986
  } catch (error) {
863
987
  if (error instanceof SessionSpawnDeniedDbError) {
@@ -905,6 +1029,7 @@ async function finishStartSession(
905
1029
  sandboxBackend: Settings["sandboxBackend"];
906
1030
  variableSets?: Array<{ id: string; name: string; scope: VariableSet["scope"] }>;
907
1031
  goal?: GoalSpec | null;
1032
+ initialAutomaticTitle?: string | null;
908
1033
  sessionMcpServers?: SessionMcpServerMetadata[];
909
1034
  seedTargetSandbox?: {
910
1035
  sandboxId: string;
@@ -1009,6 +1134,10 @@ async function finishStartSession(
1009
1134
  : {}),
1010
1135
  }
1011
1136
  : null,
1137
+ initialAutomaticTitle: initialAutomaticTitleForSessionStart(
1138
+ session,
1139
+ input.initialAutomaticTitle,
1140
+ ),
1012
1141
  consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
1013
1142
  rememberNewSessionSelection: input.rememberNewSessionSelection ?? null,
1014
1143
  deferInitialTurn: input.deferInitialTurn === true,
@@ -1084,9 +1213,6 @@ export function canonicalConfiguredModel(
1084
1213
  ) {
1085
1214
  return canonicalModel;
1086
1215
  }
1087
- if (canonicalModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
1088
- return canonicalModel;
1089
- }
1090
1216
  throw new HTTPException(422, { message: `model is not available: ${model}` });
1091
1217
  }
1092
1218
 
@@ -1192,7 +1318,17 @@ export async function requireQueuedTurnForApi(
1192
1318
  * `session_send_message` tool so the two surfaces cannot drift. Callers own
1193
1319
  * resource/tool validation and the per-message usage limit before calling.
1194
1320
  */
1195
- 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 = {
1196
1332
  db: Database;
1197
1333
  bus: EventBus;
1198
1334
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
@@ -1220,28 +1356,143 @@ export async function postUserMessageTurn(input: {
1220
1356
  commandActor?: SessionCommandActor;
1221
1357
  controlEtag?: string | null;
1222
1358
  expectedDraftRevision?: number | null;
1359
+ boundaryRequestHash?: string;
1223
1360
  reasoningEffortFallback?: Settings["openaiReasoningEffort"];
1224
1361
  turnExecutionPolicy: TurnExecutionPolicyV1;
1225
1362
  recordAgentRunUsage?: boolean;
1226
1363
  schedulePostCommit?: (task: () => Promise<void>) => void;
1227
- }): Promise<{
1228
- accepted: SessionEvent;
1229
- turn: SessionTurn;
1230
- draft: ComposerDraft | null;
1231
- receipt: SessionCommandReceipt;
1232
- routing: SessionPromptRouting;
1233
- interruptionCount: number;
1234
- replay: boolean;
1235
- }> {
1236
- 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;
1237
1482
  const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
1238
1483
  const requestedReasoningEffort = input.reasoningEffort ?? null;
1239
1484
  // Reject an explicit per-message model the host does not expose; an omitted
1240
1485
  // model inherits the session's model downstream (always a configured id).
1241
1486
  assertConfiguredModel(settings, requestedModel);
1242
- await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
1243
1487
  const sessionForModelGate = await requireSession(db, workspaceId, sessionId);
1244
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);
1245
1496
  try {
1246
1497
  assertSessionAllowsProductModel(sessionForModelGate, effectiveModelForGate);
1247
1498
  } catch (error) {
@@ -1276,6 +1527,9 @@ export async function postUserMessageTurn(input: {
1276
1527
  subjectId: input.actor ?? accountId,
1277
1528
  },
1278
1529
  operationKey,
1530
+ ...(input.boundaryRequestHash
1531
+ ? { boundaryRequestHash: input.boundaryRequestHash }
1532
+ : {}),
1279
1533
  delivery: input.delivery ?? "send",
1280
1534
  controlEtag: input.controlEtag ?? null,
1281
1535
  expectedDraftRevision: input.expectedDraftRevision ?? null,
@@ -1303,6 +1557,36 @@ export async function postUserMessageTurn(input: {
1303
1557
  }
1304
1558
  : {}),
1305
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
+ : {}),
1306
1590
  controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
1307
1591
  }),
1308
1592
  ),
@@ -1333,101 +1617,7 @@ export async function postUserMessageTurn(input: {
1333
1617
  }
1334
1618
  throw error;
1335
1619
  }
1336
- const postCommitTask = async () => {
1337
- await Promise.all([
1338
- (async () => {
1339
- try {
1340
- await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1341
- if (result.workspaceControlEventId) {
1342
- const controlEvent = await getWorkspaceControlEvent(
1343
- db,
1344
- workspaceId,
1345
- result.workspaceControlEventId,
1346
- );
1347
- if (!controlEvent) {
1348
- throw new Error(
1349
- `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1350
- );
1351
- }
1352
- await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1353
- }
1354
- } catch {
1355
- console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1356
- errorClass: "PromptEventFanoutOperationError",
1357
- errorCode: "session_prompt_event_fanout_failed",
1358
- origin: "core",
1359
- });
1360
- }
1361
- })(),
1362
- (async () => {
1363
- try {
1364
- await workflowClient.wakeSessionWorkflow({
1365
- accountId,
1366
- workspaceId,
1367
- sessionId,
1368
- workflowId: result.turn.temporalWorkflowId,
1369
- wakeRevision: result.wakeRevision,
1370
- ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1371
- ? { interruptionRequested: true }
1372
- : {}),
1373
- });
1374
- } catch {
1375
- console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1376
- errorClass: "WorkflowWakeOperationError",
1377
- errorCode: "session_workflow_wake_failed",
1378
- origin: "core",
1379
- });
1380
- }
1381
- })(),
1382
- ]);
1383
- };
1384
- const schedulePostCommit =
1385
- input.schedulePostCommit ??
1386
- ((task: () => Promise<void>) => {
1387
- void task();
1388
- });
1389
- try {
1390
- schedulePostCommit(postCommitTask);
1391
- } catch {
1392
- console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
1393
- errorClass: "PromptPostCommitScheduleError",
1394
- errorCode: "session_prompt_post_commit_schedule_failed",
1395
- origin: "core",
1396
- });
1397
- }
1398
- return {
1399
- accepted: result.accepted,
1400
- turn: result.turn,
1401
- receipt: {
1402
- id: result.receipt.id,
1403
- action: result.receipt.action,
1404
- operationKey: result.receipt.operationKey,
1405
- targetSessionId: result.receipt.targetSessionId,
1406
- targetTurnId: result.receipt.targetTurnId,
1407
- appliedControlRevision: result.receipt.appliedControlRevision,
1408
- appliedQueueVersion: result.receipt.appliedQueueVersion,
1409
- appliedTurnVersion: result.receipt.appliedTurnVersion,
1410
- appliedDraftRevision: result.receipt.appliedDraftRevision,
1411
- createdAt: result.receipt.createdAt.toISOString(),
1412
- },
1413
- routing: result.routing,
1414
- draft: result.draft
1415
- ? {
1416
- revision: result.draft.revision,
1417
- text: result.draft.text,
1418
- annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
1419
- resources: result.draft.resources as ResourceRef[],
1420
- model: result.draft.model,
1421
- reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
1422
- latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
1423
- sourceTurnId: result.draft.sourceTurnId,
1424
- sourceTurnVersion: result.draft.sourceTurnVersion,
1425
- updatedAt: result.draft.updatedAt.toISOString(),
1426
- }
1427
- : null,
1428
- interruptionCount: result.interruptionCount,
1429
- replay: result.replay,
1430
- };
1620
+ return finalizePostUserMessageTurn(input, result);
1431
1621
  }
1432
1622
 
1433
1623
  /**
@@ -1479,15 +1669,92 @@ export function resolveSessionCreateVisibility(input: {
1479
1669
  return input.requestedVisibility === "private" ? "user_private" : "workspace_shared";
1480
1670
  }
1481
1671
 
1482
- export async function createSessionForRequestWithOutcome(
1483
- deps: ApiRouteDeps,
1672
+ async function resolveWorkspaceModelBoundarySettings(
1673
+ deps: Pick<ApiRouteDeps, "db" | "settings" | "catalogSourceSettings">,
1484
1674
  grant: AccessGrant,
1485
1675
  workspaceId: string,
1486
- rawPayload: unknown,
1487
- authorization?: AccessGrantAuthorization,
1488
- ): Promise<CreateSessionRequestOutcome> {
1489
- const { settings, db, bus, workflowClient, objectStorage } = deps;
1490
- const payload = CreateSessionRequest.parse(rawPayload);
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
+
1743
+ export async function createSessionForRequestWithOutcome(
1744
+ unresolvedDeps: ApiRouteDeps,
1745
+ grant: AccessGrant,
1746
+ workspaceId: string,
1747
+ rawPayload: unknown,
1748
+ authorization?: AccessGrantAuthorization,
1749
+ agentChildPresentation?: AgentChildSessionCreatePresentation,
1750
+ ): Promise<CreateSessionRequestOutcome> {
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;
1491
1758
  const visibilityProvided = hasOwnProperty(rawPayload, "visibility");
1492
1759
  if (payload.visibility === "private" && !grant.metadata?.["sessionId"]) {
1493
1760
  if (!authorization) {
@@ -1495,39 +1762,13 @@ export async function createSessionForRequestWithOutcome(
1495
1762
  message: "managed human session required",
1496
1763
  });
1497
1764
  }
1498
- await requireManagedHumanPrivateSessionCreate(deps, authorization, workspaceId);
1765
+ await requireManagedHumanPrivateSessionCreate(unresolvedDeps, authorization, workspaceId);
1499
1766
  if (payload.sandbox === "shared" || typeof payload.sandbox === "object") {
1500
1767
  throw new HTTPException(422, {
1501
1768
  message: "Only-me sessions require their own sandbox",
1502
1769
  });
1503
1770
  }
1504
1771
  }
1505
- if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
1506
- throw new HTTPException(422, {
1507
- message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
1508
- });
1509
- }
1510
- // A committed keyed denial is the idempotent outcome even if mutable
1511
- // resources, policy, authorization, or budget have changed since the first
1512
- // attempt. Replay it before any of those checks, just as a keyed successful
1513
- // session is returned rather than recreated later in createAndStartSession.
1514
- if (payload.idempotencyKey) {
1515
- const denial = await getSessionSpawnDenialByIdempotencyKey(
1516
- db,
1517
- workspaceId,
1518
- payload.idempotencyKey,
1519
- );
1520
- if (denial) {
1521
- throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
1522
- }
1523
- }
1524
- await requireAtomicPersonalResourceAttachment(
1525
- deps,
1526
- authorization,
1527
- workspaceId,
1528
- payload.personalResourceAttachment,
1529
- false,
1530
- );
1531
1772
  // Parent linkage and execution-context inheritance come ONLY from the
1532
1773
  // worker-signed sessionId claim. A caller cannot nominate a parent in the
1533
1774
  // payload, so inheriting an existing repository/tool/credential snapshot does
@@ -1538,7 +1779,7 @@ export async function createSessionForRequestWithOutcome(
1538
1779
  : null;
1539
1780
  if (parentSessionId) {
1540
1781
  try {
1541
- await requireSessionAuthorization(deps, grant, {
1782
+ await requireSessionAuthorization(unresolvedDeps, grant, {
1542
1783
  sessionId: parentSessionId,
1543
1784
  operation: "session.child.create",
1544
1785
  surface: "core",
@@ -1596,6 +1837,117 @@ export async function createSessionForRequestWithOutcome(
1596
1837
  message: "caller attempt does not belong to the parent session",
1597
1838
  });
1598
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
+ }
1599
1951
  let effectiveGoal = payload.goal;
1600
1952
  if (parentSession && payload.goal) {
1601
1953
  try {
@@ -1612,6 +1964,14 @@ export async function createSessionForRequestWithOutcome(
1612
1964
  });
1613
1965
  }
1614
1966
  }
1967
+ const initialAutomaticTitle =
1968
+ parentSession && agentChildPresentation
1969
+ ? automaticTitleForAgentChildCreate(
1970
+ agentChildPresentation,
1971
+ effectiveGoal,
1972
+ payload.initialMessage,
1973
+ )
1974
+ : null;
1615
1975
  const personalResourceSubjectId = creationInitiator.actor
1616
1976
  ? (await requireLiveAgentAttemptAuthorization(db, grant, creationInitiator.actor.sessionId))
1617
1977
  .initiatingHumanSubjectId
@@ -1798,8 +2158,7 @@ export async function createSessionForRequestWithOutcome(
1798
2158
  // managers: falling back to the deployment model would silently move a child
1799
2159
  // onto the OpenGeni-credits billing path. Legacy session-bound grants without
1800
2160
  // exact attempt claims fall back to the parent session's persisted defaults.
1801
- const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
1802
- const model = canonicalConfiguredModel(settings, payload.model ?? inheritedModel);
2161
+ const model = canonicalConfiguredModel(settings, effectiveModelId);
1803
2162
  if (model === null || model === undefined) {
1804
2163
  throw new Error("effective session model unexpectedly resolved to null");
1805
2164
  }
@@ -2038,6 +2397,8 @@ export async function createSessionForRequestWithOutcome(
2038
2397
  payload.sandbox ?? (payload.targetSandboxId ? "new" : parentSessionId ? "shared" : "new");
2039
2398
  let sandboxGroupId: string | null = null;
2040
2399
  let inheritedBackend: Session["sandboxBackend"] | undefined;
2400
+ let inheritedSandboxOs: Session["sandboxOs"] | undefined;
2401
+ let inheritedActiveTarget: { sandboxId: string; workingDir: string | null } | null = null;
2041
2402
  // ENV-AWARE GROUPING: under the CURRENT mechanics the workspace VariableSet is
2042
2403
  // creation-time box state — the box's manifest env is fixed when it is cold-
2043
2404
  // created, and the SDK's provided-session guard rejects any manifest-env delta
@@ -2112,6 +2473,17 @@ export async function createSessionForRequestWithOutcome(
2112
2473
  } else {
2113
2474
  sandboxGroupId = parent.sandboxGroupId;
2114
2475
  inheritedBackend = parent.sandboxBackend;
2476
+ inheritedSandboxOs = parent.sandboxOs;
2477
+ // A Connected Machine route is session-local even when two sessions share
2478
+ // one logical sandbox group. Copy the trusted parent's exact active route
2479
+ // so an omitted child sandbox really does share the creator's current box
2480
+ // instead of creating a selfhosted row with no bound agent.
2481
+ inheritedActiveTarget = parent.activeSandboxId
2482
+ ? {
2483
+ sandboxId: parent.activeSandboxId,
2484
+ workingDir: parent.workingDir,
2485
+ }
2486
+ : null;
2115
2487
  }
2116
2488
  } else if (typeof sandboxChoice === "object") {
2117
2489
  const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
@@ -2160,6 +2532,7 @@ export async function createSessionForRequestWithOutcome(
2160
2532
  }
2161
2533
  sandboxGroupId = sandboxChoice.groupId;
2162
2534
  inheritedBackend = member.sandboxBackend;
2535
+ inheritedSandboxOs = member.sandboxOs;
2163
2536
  }
2164
2537
  // else "new": leave sandboxGroupId null → own singleton group (group ≡ id).
2165
2538
  // A working dir is only meaningful for a TARGETED machine (it is the chosen
@@ -2172,6 +2545,19 @@ export async function createSessionForRequestWithOutcome(
2172
2545
  "workingDir requires targetSandboxId (it is the targeted machine's working directory)",
2173
2546
  });
2174
2547
  }
2548
+ // A registry-built selfhosted client is deliberately inert: it has no live
2549
+ // agent identity until a concrete Connected Machine is named. Reject an own
2550
+ // targetless home before persisting a session whose first turn can only fail.
2551
+ // Shared children retain their already-bound group through inheritedBackend.
2552
+ if (
2553
+ inheritedBackend === undefined &&
2554
+ !payload.targetSandboxId &&
2555
+ (payload.sandboxBackend ?? settings.sandboxBackend) === "selfhosted"
2556
+ ) {
2557
+ throw new HTTPException(422, {
2558
+ message: "selfhosted sessions require targetSandboxId; select an online Connected Machine",
2559
+ });
2560
+ }
2175
2561
  // Honest-label (Stage-D closure): a session TARGETED at a Connected
2176
2562
  // Machine (a selfhosted sandbox) runs machine-primary every turn, so its HOME
2177
2563
  // sandbox_backend must read "selfhosted" — not the deployment cloud default —
@@ -2230,6 +2616,25 @@ export async function createSessionForRequestWithOutcome(
2230
2616
  }
2231
2617
  }
2232
2618
  }
2619
+ const effectiveSandboxBackend =
2620
+ inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend;
2621
+ const effectiveSandboxOs = inheritedSandboxOs ?? machineHomeOs;
2622
+ const effectiveSeedTarget = payload.targetSandboxId
2623
+ ? {
2624
+ sandboxId: payload.targetSandboxId,
2625
+ workingDir: payload.workingDir ?? null,
2626
+ }
2627
+ : inheritedActiveTarget;
2628
+ if (
2629
+ effectiveSandboxBackend === "selfhosted" &&
2630
+ effectiveSeedTarget === null &&
2631
+ managedSessionGroupBackend(settings.sandboxBackend, effectiveSandboxBackend) === null
2632
+ ) {
2633
+ throw new HTTPException(422, {
2634
+ message:
2635
+ "self-hosted execution runs on a Connected Machine, but no machine was selected or inherited; connect the parent session to a machine or provide machineTarget",
2636
+ });
2637
+ }
2233
2638
  if (payload.startMode !== "realtime") {
2234
2639
  await requireLimit(deps, {
2235
2640
  accountId: grant.accountId,
@@ -2266,11 +2671,10 @@ export async function createSessionForRequestWithOutcome(
2266
2671
  // machine-targeted create (top-level or own-box child) labels the home
2267
2672
  // "selfhosted" (machineHomeBackend), overriding the caller/deployment
2268
2673
  // default so the row matches where the session actually runs.
2269
- sandboxBackend:
2270
- inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
2674
+ sandboxBackend: effectiveSandboxBackend,
2271
2675
  // Mirror the backend relabel on the OS axis: a machine-targeted own-box
2272
- // create carries a derived OS; shared spawns keep the parent-box behavior.
2273
- ...(machineHomeOs ? { sandboxOs: machineHomeOs } : {}),
2676
+ // create carries a derived OS; shared spawns inherit the exact parent box.
2677
+ ...(effectiveSandboxOs ? { sandboxOs: effectiveSandboxOs } : {}),
2274
2678
  sandboxGroupId,
2275
2679
  metadata: payload.metadata,
2276
2680
  ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
@@ -2286,6 +2690,7 @@ export async function createSessionForRequestWithOutcome(
2286
2690
  rigVersionId: frozenRigVersionId,
2287
2691
  channelId,
2288
2692
  goal: effectiveGoal ?? null,
2693
+ initialAutomaticTitle,
2289
2694
  // Per-session persona instructions (already trimmed/validated by the
2290
2695
  // contracts schema). Persisted on the row; composed system-level at turn
2291
2696
  // time. Not surfaced as an event.
@@ -2297,6 +2702,8 @@ export async function createSessionForRequestWithOutcome(
2297
2702
  sessionMcpServers: sessionMcpServers.metadata,
2298
2703
  personalConnectionDelegations,
2299
2704
  initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
2705
+ workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
2706
+ retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
2300
2707
  ...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
2301
2708
  parentSessionId,
2302
2709
  createIdempotencyKey: payload.idempotencyKey ?? null,
@@ -2307,11 +2714,11 @@ export async function createSessionForRequestWithOutcome(
2307
2714
  // active-sandbox pointer is seeded race-free inside createAndStartSession
2308
2715
  // (after the row exists, before the first turn dispatches). Validation
2309
2716
  // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
2310
- seedTargetSandbox: payload.targetSandboxId
2717
+ seedTargetSandbox: effectiveSeedTarget
2311
2718
  ? {
2312
- sandboxId: payload.targetSandboxId,
2719
+ sandboxId: effectiveSeedTarget.sandboxId,
2313
2720
  settings,
2314
- workingDir: payload.workingDir ?? null,
2721
+ workingDir: effectiveSeedTarget.workingDir,
2315
2722
  resourceSubjectId: personalResourceSubjectId,
2316
2723
  }
2317
2724
  : null,
@@ -2366,30 +2773,14 @@ export async function createSessionForRequestWithOutcome(
2366
2773
  }
2367
2774
  throw error;
2368
2775
  }
2369
- let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
2370
- if (payload.startMode !== "realtime") {
2371
- try {
2372
- await recordWorkspaceUsage(deps, {
2373
- accountId: grant.accountId,
2374
- workspaceId,
2375
- subjectId: grant.subjectId,
2376
- eventType: "agent_run.created",
2377
- quantity: 1,
2378
- unit: "run",
2379
- sourceResourceType: "session",
2380
- sourceResourceId: createOutcome.session.id,
2381
- sessionId: createOutcome.session.id,
2382
- initiator: createOutcome.session.createdBy,
2383
- initiatorContext: createOutcome.session.createdByContext,
2384
- origin: creationInitiator.actor ? "system" : "user",
2385
- idempotencyKey: `agent_run.created:${workspaceId}:${createOutcome.session.id}`,
2386
- });
2387
- } catch (error) {
2388
- usageRecording = "failed";
2389
- reportSessionUsageRecordingFailure(error);
2390
- }
2391
- }
2392
- 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
+ });
2393
2784
  }
2394
2785
 
2395
2786
  /** @internal Fixed public projection; the committed session outcome remains authoritative. */
@@ -2417,6 +2808,53 @@ export async function createSessionForRequest(
2417
2808
  ).session;
2418
2809
  }
2419
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
+
2420
2858
  /**
2421
2859
  * Full accept-user-message flow shared by the `user.message` branch of
2422
2860
  * `POST /sessions/:id/events` and the first-party MCP `session_send_message`
@@ -2458,192 +2896,308 @@ export async function acceptSessionUserMessageWithOutcome(
2458
2896
  interruptionCount: number;
2459
2897
  replay: boolean;
2460
2898
  }> {
2461
- const { settings, db, bus, workflowClient, objectStorage } = deps;
2899
+ const { db, bus, workflowClient, objectStorage } = deps;
2462
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 };
2463
2913
  await requireSessionAuthorization(deps, grant, {
2464
2914
  sessionId,
2465
- operation: input.delivery === "steer" ? "session.steer" : "session.append",
2915
+ operation: delivery === "steer" ? "session.steer" : "session.append",
2466
2916
  surface: "core",
2467
2917
  });
2468
- await requireAtomicPersonalResourceAttachment(
2469
- deps,
2470
- input.authorization,
2471
- workspaceId,
2472
- input.personalResourceAttachment,
2473
- true,
2474
- );
2475
- // Hoisted above requireLimit so the codex-billed predicate can resolve the
2476
- // turn's effective model (a follow-up turn inherits the session's model). A
2477
- // pure read with no side effects.
2478
- const existingSession = await requireSession(db, workspaceId, sessionId);
2479
- const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
2480
- const effectiveModel =
2481
- canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
2482
- if (effectiveModel === null) {
2483
- throw new Error("effective follow-up model unexpectedly resolved to null");
2484
- }
2485
- try {
2486
- assertSessionAllowsProductModel(existingSession, effectiveModel);
2487
- } catch (error) {
2488
- if (error instanceof CodexCompactionV2ProviderLockedError) {
2489
- throw new HTTPException(422, { message: error.message, cause: error });
2490
- }
2491
- throw error;
2492
- }
2493
- const sessionReasoningEffort = existingSession.reasoningEffort;
2494
- const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
2495
- const sessionLatencyMode = existingSession.latencyMode;
2496
- const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
2497
- const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
2498
- modelId: effectiveModel,
2499
- requestedModelId: input.model ?? null,
2500
- modelSource: input.model == null ? "session" : "explicit",
2501
- reasoningEffort: effectiveReasoningEffort,
2502
- reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
2503
- latencyMode: effectiveLatencyMode,
2504
- latencyModeSource: input.latencyMode == null ? "session" : "explicit",
2505
- });
2506
2918
  const requestedResources = normalizeResources(input.resources ?? []);
2507
2919
  const composerDraftResources = input.composerDraftResources
2508
2920
  ? normalizeResources(input.composerDraftResources)
2509
2921
  : undefined;
2510
- if (composerDraftResources) {
2511
- const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
2512
- const unacceptedDraftResource = composerDraftResources.find(
2513
- (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
+ }),
2514
2962
  );
2515
- if (unacceptedDraftResource) {
2516
- throw new HTTPException(422, {
2517
- message: "composer draft resources must be included in the accepted resource set",
2518
- });
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
+ );
2519
2979
  }
2520
2980
  }
2521
- const annotations = await validateSubmittedTimelineAnnotations(
2522
- db,
2523
- workspaceId,
2524
- sessionId,
2525
- input.annotations ?? [],
2526
- );
2527
- await requireLimit(deps, {
2528
- accountId: grant.accountId,
2529
- workspaceId,
2530
- action: "agent_run:create",
2531
- quantity: 1,
2532
- model: effectiveModel,
2533
- });
2534
- if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
2535
- throw new HTTPException(503, {
2536
- 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",
2537
3034
  });
2538
- }
2539
- await validateFileResources(
2540
- db,
2541
- grant.accountId,
2542
- workspaceId,
2543
- grant.subjectId,
2544
- requestedResources,
2545
- );
2546
- await validateGitHubRepositorySelection(db, workspaceId, [
2547
- ...existingSession.resources,
2548
- ...requestedResources,
2549
- ]);
2550
- const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
2551
- settings,
2552
- grant,
2553
- session: existingSession,
2554
- updates: input.mcpCredentialUpdates ?? [],
2555
- });
2556
- const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
2557
- const inheritedPersonalConnectionDelegations =
2558
- connectionDelegationSource.kind === "turn"
2559
- ? await getSessionTurnPersonalConnectionDelegations(
2560
- db,
2561
- workspaceId,
2562
- connectionDelegationSource.sessionId,
2563
- connectionDelegationSource.turnId,
2564
- )
2565
- : null;
2566
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
2567
- db,
2568
- workspaceId,
2569
- settings,
2570
- inheritedPersonalConnectionDelegations
2571
- ? {
2572
- personalConnectionDelegations: inheritedPersonalConnectionDelegations,
2573
- }
2574
- : { subjectId: grant.subjectId },
2575
- );
2576
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
2577
- db,
2578
- workspaceId,
2579
- settings: runtimeSettings,
2580
- tools: existingSession.tools,
2581
- resources: [...existingSession.resources, ...requestedResources],
2582
- source: connectionDelegationSource,
2583
- targetSessionId: sessionId,
2584
- googleDrivePublicationEnabled:
2585
- existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
2586
- existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
2587
- (!existingSession.firstPartyMcpPermissions?.length ||
2588
- (existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
2589
- existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
2590
- atlassianEnabled:
2591
- existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
2592
- (!existingSession.firstPartyMcpPermissions?.length ||
2593
- existingSession.firstPartyMcpPermissions.includes("connections:read")),
2594
- ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
2595
- });
2596
- const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
2597
- 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(
2598
3047
  db,
2599
- bus,
2600
- workflowClient,
2601
- settings,
2602
- accountId: grant.accountId,
2603
3048
  workspaceId,
2604
3049
  sessionId,
2605
- text: input.text,
2606
- annotations,
2607
- modelContext: input.modelContext ?? null,
2608
- resources: requestedResources,
2609
- ...(composerDraftResources ? { composerDraftResources } : {}),
2610
- model: input.model ?? null,
2611
- reasoningEffort: input.reasoningEffort ?? null,
2612
- latencyMode: input.latencyMode ?? null,
2613
- reasoningEffortFallback: sessionReasoningEffort,
2614
- turnExecutionPolicy,
2615
- mcpCredentialUpdates,
2616
- personalConnectionDelegations,
2617
- ...(input.personalResourceAttachment
2618
- ? { personalResourceAttachment: input.personalResourceAttachment }
2619
- : {}),
2620
- delivery: input.delivery ?? "send",
2621
- origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
2622
- actor: grant.subjectId,
2623
- ...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
2624
- ...(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
2625
3096
  ? {
2626
- commandActor: {
2627
- type: "service" as const,
2628
- subjectId: delegatedServiceInitiator.initiator.subjectId,
2629
- ...(delegatedServiceInitiator.initiator.label
2630
- ? { subjectLabel: delegatedServiceInitiator.initiator.label }
2631
- : {}),
2632
- context: delegatedServiceInitiator.context,
2633
- },
3097
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
2634
3098
  }
2635
- : {}),
2636
- ...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
2637
- ...(input.expectedDraftRevision !== undefined
2638
- ? { expectedDraftRevision: input.expectedDraftRevision }
2639
- : {}),
2640
- ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
2641
- recordAgentRunUsage: true,
2642
- ...(deps.schedulePromptPostCommit
2643
- ? { schedulePostCommit: deps.schedulePromptPostCommit }
2644
- : {}),
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 } : {}),
2645
3120
  });
2646
- 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
+ }
2647
3201
  }
2648
3202
 
2649
3203
  /** Backward-compatible entity-returning path used by existing REST callers. */