@opengeni/core 0.21.10 → 0.28.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 (132) hide show
  1. package/dist/access/index.d.ts +1 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +2 -2
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/canonical-human-identities.d.ts +12 -0
  6. package/dist/canonical-human-identities.js +23 -0
  7. package/dist/canonical-human-identities.js.map +1 -0
  8. package/dist/chunk-6WKPWE5S.js +2380 -0
  9. package/dist/chunk-6WKPWE5S.js.map +1 -0
  10. package/dist/chunk-IBOEYG6N.js +34 -0
  11. package/dist/chunk-IBOEYG6N.js.map +1 -0
  12. package/dist/chunk-JJU6I5XD.js +2713 -0
  13. package/dist/chunk-JJU6I5XD.js.map +1 -0
  14. package/dist/chunk-NYPMQ7NO.js +2270 -0
  15. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  16. package/dist/dependencies.d.ts +30 -6
  17. package/dist/domain/capabilities.d.ts +28 -3
  18. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  19. package/dist/domain/conversation-integrations.d.ts +225 -0
  20. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  21. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  22. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  23. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  24. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  25. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  26. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  27. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  28. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  29. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  30. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  31. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  32. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  33. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  34. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  35. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  36. package/dist/domain/fiken.d.ts +35 -0
  37. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  38. package/dist/domain/packs.d.ts +29 -1
  39. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  40. package/dist/domain/scheduled-tasks.d.ts +2 -2
  41. package/dist/domain/session-tool-policy.d.ts +2 -2
  42. package/dist/domain/sessions.d.ts +24 -8
  43. package/dist/domain/skill-imports.d.ts +32 -0
  44. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  45. package/dist/domain/timeline-annotations.d.ts +5 -0
  46. package/dist/domain/video-generation-capabilities.d.ts +10 -0
  47. package/dist/domain/video-generation.d.ts +47 -0
  48. package/dist/editable-artifact-live/application.d.ts +69 -0
  49. package/dist/editable-artifact-live/errors.d.ts +1 -0
  50. package/dist/editable-artifact-live/index.d.ts +8 -0
  51. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  52. package/dist/editable-artifact-live/ports.d.ts +100 -0
  53. package/dist/editable-artifact-live/server.d.ts +71 -0
  54. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  55. package/dist/editable-artifact-live/types.d.ts +213 -0
  56. package/dist/editable-artifact-live/wire.d.ts +34 -0
  57. package/dist/editable-artifact-live.d.ts +2 -0
  58. package/dist/editable-artifact-live.js +58 -0
  59. package/dist/editable-artifact-live.js.map +1 -0
  60. package/dist/editable-artifacts.d.ts +2 -0
  61. package/dist/editable-artifacts.js +179 -0
  62. package/dist/editable-artifacts.js.map +1 -0
  63. package/dist/index.d.ts +37 -26
  64. package/dist/index.js +4145 -959
  65. package/dist/index.js.map +1 -1
  66. package/dist/managed-session.d.ts +6 -2
  67. package/dist/rigs/index.d.ts +1 -1
  68. package/dist/rigs/provider-images.d.ts +31 -0
  69. package/dist/sandbox/fleet.d.ts +8 -5
  70. package/dist/sandbox/routing.d.ts +1 -0
  71. package/dist/session-authorization.d.ts +4 -2
  72. package/dist/transcription.d.ts +5 -0
  73. package/dist/workflow-wake-contract.d.ts +7 -0
  74. package/package.json +22 -10
  75. package/src/access/index.ts +14 -2
  76. package/src/application/new-session-drafts.ts +2 -0
  77. package/src/application/session-commands.ts +188 -107
  78. package/src/canonical-human-identities.ts +27 -0
  79. package/src/dependencies.ts +34 -2
  80. package/src/domain/capabilities.ts +729 -408
  81. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  82. package/src/domain/conversation-integrations.ts +1238 -0
  83. package/src/domain/durable-learning-slack-publication.ts +202 -0
  84. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  85. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  86. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  87. package/src/domain/editable-artifacts/errors.ts +115 -0
  88. package/src/domain/editable-artifacts/hash.ts +131 -0
  89. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  90. package/src/domain/editable-artifacts/index.ts +14 -0
  91. package/src/domain/editable-artifacts/ports.ts +462 -0
  92. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  93. package/src/domain/editable-artifacts/service.ts +2402 -0
  94. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  95. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  96. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  97. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  98. package/src/domain/editable-artifacts/types.ts +835 -0
  99. package/src/domain/fiken.ts +88 -0
  100. package/src/domain/insights.ts +163 -50
  101. package/src/domain/memory-slack-delivery.ts +204 -0
  102. package/src/domain/memory-slack-publication.ts +5 -2
  103. package/src/domain/packs.ts +266 -5
  104. package/src/domain/personal-connection-delegations.ts +97 -6
  105. package/src/domain/scheduled-tasks.ts +171 -29
  106. package/src/domain/session-tool-policy.ts +8 -2
  107. package/src/domain/sessions.ts +395 -148
  108. package/src/domain/skill-imports.ts +350 -0
  109. package/src/domain/slack-publication-secret-safety.ts +42 -0
  110. package/src/domain/timeline-annotations.ts +204 -0
  111. package/src/domain/video-generation-capabilities.ts +104 -0
  112. package/src/domain/video-generation.ts +249 -0
  113. package/src/editable-artifact-live/application.ts +182 -0
  114. package/src/editable-artifact-live/errors.ts +4 -0
  115. package/src/editable-artifact-live/index.ts +8 -0
  116. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  117. package/src/editable-artifact-live/ports.ts +139 -0
  118. package/src/editable-artifact-live/server.ts +1707 -0
  119. package/src/editable-artifact-live/ticket.ts +217 -0
  120. package/src/editable-artifact-live/types.ts +286 -0
  121. package/src/editable-artifact-live/wire.ts +95 -0
  122. package/src/editable-artifact-live.ts +2 -0
  123. package/src/editable-artifacts.ts +2 -0
  124. package/src/index.ts +15 -0
  125. package/src/managed-session.ts +19 -2
  126. package/src/rigs/index.ts +26 -19
  127. package/src/rigs/provider-images.ts +110 -0
  128. package/src/sandbox/fleet.ts +72 -66
  129. package/src/sandbox/routing.ts +6 -3
  130. package/src/session-authorization.ts +187 -52
  131. package/src/transcription.ts +5 -0
  132. package/src/workflow-wake-contract.ts +8 -0
@@ -2,9 +2,11 @@ import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
2
2
  import {
3
3
  canonicalizeConfiguredModelId,
4
4
  configuredAllowedModels,
5
+ resolveFirstPartyMcpToolPolicy,
5
6
  policyProviderIdForModel,
6
7
  resolveTurnExecutionPolicyV1,
7
8
  WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
9
+ XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
8
10
  type Settings,
9
11
  } from "@opengeni/config";
10
12
  import {
@@ -35,6 +37,8 @@ import {
35
37
  type SessionMcpCredentialUpdateInput,
36
38
  type SessionMcpServerInput,
37
39
  type SessionMcpServerMetadata,
40
+ type SubmittedTimelineAnnotation,
41
+ type TimelineAnnotation,
38
42
  type UpdateSessionMcpApprovalPolicyResponse,
39
43
  type UpdateSessionToolPolicyRequest,
40
44
  type SessionAuthorizationPort,
@@ -44,6 +48,7 @@ import {
44
48
  type TurnInitiator,
45
49
  type TurnInitiatorContext,
46
50
  type TurnExecutionPolicyV1,
51
+ type XaiProviderAccountAuthoritySnapshotV1,
47
52
  } from "@opengeni/contracts";
48
53
  import {
49
54
  createSession,
@@ -51,6 +56,7 @@ import {
51
56
  encryptVariableSetValue,
52
57
  getAnySessionInGroup,
53
58
  getEnrollment,
59
+ getChannel,
54
60
  getRig,
55
61
  getWorkspaceDefaultRigId,
56
62
  listDistinctVariableSetIdsInGroup,
@@ -59,10 +65,10 @@ import {
59
65
  getSession,
60
66
  SessionIdConflictError,
61
67
  getSessionSpawnDenialByIdempotencyKey,
62
- getSessionEvent,
63
68
  getWorkspaceControlEvent,
64
69
  getSessionLineage,
65
70
  getSessionTurn,
71
+ getSessionTurnXaiProviderAccountAuthoritySnapshot,
66
72
  getWorkspaceModelPolicy,
67
73
  initializeSessionStartAtomically,
68
74
  listSessionTurns,
@@ -71,12 +77,13 @@ import {
71
77
  submitHumanPromptInTransaction,
72
78
  appendSessionEventsWithLockedSessionUpdate,
73
79
  updateSessionTitle as updateSessionTitleRow,
74
- withWorkspaceSubjectRls,
80
+ withWorkspaceSubjectSessionActivityRls,
75
81
  type CreateSessionMcpServerInput,
76
82
  type Database,
77
83
  type UpdateSessionMcpServerCredentialsInput,
78
84
  QueueCommandConflictError,
79
85
  AgentCommandAuthorityError,
86
+ runIdempotentPersistenceTransaction,
80
87
  SessionSpawnDeniedDbError,
81
88
  SessionControlConflictError,
82
89
  SessionToolPolicyVersionConflictError,
@@ -96,9 +103,13 @@ import type {
96
103
  ApiRouteDeps,
97
104
  SessionWorkflowClient,
98
105
  } from "../dependencies";
99
- import { requireSessionAuthorization } from "../session-authorization";
106
+ import {
107
+ requireSessionAuthorization,
108
+ SessionAuthorizationDeniedError,
109
+ } from "../session-authorization";
100
110
  import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
101
111
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
112
+ import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
102
113
  import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
103
114
  import {
104
115
  freezePersonalConnectionDelegations,
@@ -145,10 +156,18 @@ export class SessionSpawnDeniedError extends Error {
145
156
  export function resolveFirstPartyMcpToolsForCreate(
146
157
  requested: FirstPartyMcpToolName[] | undefined,
147
158
  parentStored: FirstPartyMcpToolName[] | null | undefined,
159
+ policy: {
160
+ default: readonly FirstPartyMcpToolName[];
161
+ allowed: readonly FirstPartyMcpToolName[];
162
+ } = {
163
+ default: DEFAULT_FIRST_PARTY_MCP_TOOLS,
164
+ allowed: FIRST_PARTY_MCP_TOOL_NAMES,
165
+ },
148
166
  ): FirstPartyMcpToolName[] {
149
167
  if (requested !== undefined) return [...requested];
150
- if (parentStored === undefined) return [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
151
- return [...(parentStored ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)];
168
+ const allowed = new Set(policy.allowed);
169
+ const inherited = parentStored === undefined ? policy.default : (parentStored ?? policy.default);
170
+ return [...inherited].filter((tool) => allowed.has(tool));
152
171
  }
153
172
 
154
173
  function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
@@ -241,7 +260,9 @@ export function creationInitiatorForGrant(grant: AccessGrant): FrozenCreationIni
241
260
  !Number.isSafeInteger(callerExecutionGeneration) ||
242
261
  callerExecutionGeneration < 1
243
262
  ) {
244
- throw new HTTPException(403, { message: "caller attempt claims are incomplete" });
263
+ throw new HTTPException(403, {
264
+ message: "caller attempt claims are incomplete",
265
+ });
245
266
  }
246
267
  const actor = {
247
268
  type: "agent_attempt",
@@ -284,11 +305,15 @@ function normalizedSessionMcpCredentialHeaders(
284
305
  const seen = new Set<string>();
285
306
  for (const [name, value] of entries) {
286
307
  if (!sessionMcpCredentialHeaderName.test(name)) {
287
- throw new HTTPException(422, { message: `invalid credential header name: ${name}` });
308
+ throw new HTTPException(422, {
309
+ message: `invalid credential header name: ${name}`,
310
+ });
288
311
  }
289
312
  const lower = name.toLowerCase();
290
313
  if (seen.has(lower)) {
291
- throw new HTTPException(422, { message: `duplicate credential header name: ${name}` });
314
+ throw new HTTPException(422, {
315
+ message: `duplicate credential header name: ${name}`,
316
+ });
292
317
  }
293
318
  seen.add(lower);
294
319
  if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
@@ -388,11 +413,15 @@ function validateSessionMcpServersForCreate(
388
413
  const metadata: SessionMcpServerMetadata[] = [];
389
414
  for (const server of servers) {
390
415
  if (seenIds.has(server.id)) {
391
- throw new HTTPException(422, { message: `duplicate session MCP server id: ${server.id}` });
416
+ throw new HTTPException(422, {
417
+ message: `duplicate session MCP server id: ${server.id}`,
418
+ });
392
419
  }
393
420
  seenIds.add(server.id);
394
421
  if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
395
- throw new HTTPException(422, { message: `MCP server id already exists: ${server.id}` });
422
+ throw new HTTPException(422, {
423
+ message: `MCP server id already exists: ${server.id}`,
424
+ });
396
425
  }
397
426
  const headers = normalizedSessionMcpCredentialHeaders(server.headers);
398
427
  const headersEncrypted = Object.fromEntries(
@@ -490,7 +519,9 @@ function validateSessionMcpCredentialUpdates(input: {
490
519
  }
491
520
  seenIds.add(update.id);
492
521
  if (!knownIds.has(update.id)) {
493
- throw new HTTPException(422, { message: `unknown session MCP server id: ${update.id}` });
522
+ throw new HTTPException(422, {
523
+ message: `unknown session MCP server id: ${update.id}`,
524
+ });
494
525
  }
495
526
  const headers = normalizedSessionMcpCredentialHeaders(update.headers);
496
527
  return {
@@ -525,7 +556,10 @@ export async function createAndStartSessionWithOutcome(input: {
525
556
  requestedSessionId?: string;
526
557
  db: Database;
527
558
  bus: EventBus;
528
- workflowClient: SessionWorkflowClient;
559
+ workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
560
+ /** Internal database-only composition seam. The exact session shell and this
561
+ * linkage commit together before its first event/turn can be initialized. */
562
+ beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
529
563
  accountId: string;
530
564
  workspaceId: string;
531
565
  initialMessage: string;
@@ -557,6 +591,9 @@ export async function createAndStartSessionWithOutcome(input: {
557
591
  // rig promote never moves an existing session's version.
558
592
  rigId?: string | null;
559
593
  rigVersionId?: string | null;
594
+ // The workspace channel the session is filed under (rail organization only;
595
+ // resolved workspace-scoped by the caller). Null/omitted ⇒ unfiled (inbox).
596
+ channelId?: string | null;
560
597
  goal?: GoalSpec | null;
561
598
  // Per-session agent persona/system instructions (org-visible metadata, not a
562
599
  // secret). Persisted on the session row and composed system-level AFTER the
@@ -576,6 +613,7 @@ export async function createAndStartSessionWithOutcome(input: {
576
613
  mcpServers?: CreateSessionMcpServerInput[];
577
614
  sessionMcpServers?: SessionMcpServerMetadata[];
578
615
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
616
+ xaiProviderAccountAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1;
579
617
  // The manager session spawning this worker (a worker-signed sessionId claim
580
618
  // on the creating grant); null for direct API creates and scheduled runs.
581
619
  // When set, the worker's terminal-for-now transitions wake this parent.
@@ -602,10 +640,17 @@ export async function createAndStartSessionWithOutcome(input: {
602
640
  // target fails the create (422) — never a silent fall-back to the default box.
603
641
  // `workingDir` (optional) is the path/cwd base the chosen machine runs under,
604
642
  // seeded alongside the pointer through the epoch-fenced CAS.
605
- seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
643
+ seedTargetSandbox?: {
644
+ sandboxId: string;
645
+ settings: Settings;
646
+ workingDir?: string | null;
647
+ } | null;
606
648
  // Exact actor-private pre-session draft represented by this create. The
607
649
  // initializer consumes it only after the first durable runnable unit commits.
608
- consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
650
+ consumeNewSessionDraft?: {
651
+ subjectId: string;
652
+ expectedRevision: number;
653
+ } | null;
609
654
  // A child may lower its inherited nested-agent depth limit freely; increases
610
655
  // are authorized by the caller's workspace:admin grant and checked again by
611
656
  // the database admission transaction.
@@ -643,6 +688,7 @@ export async function createAndStartSessionWithOutcome(input: {
643
688
  variableSetId: input.variableSet?.id ?? null,
644
689
  rigId: input.rigId ?? null,
645
690
  rigVersionId: input.rigVersionId ?? null,
691
+ channelId: input.channelId ?? null,
646
692
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
647
693
  firstPartyMcpTools: input.firstPartyMcpTools,
648
694
  instructions: input.instructions ?? null,
@@ -653,9 +699,15 @@ export async function createAndStartSessionWithOutcome(input: {
653
699
  ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
654
700
  mcpServers: input.mcpServers ?? [],
655
701
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
702
+ ...(input.xaiProviderAccountAuthoritySnapshot
703
+ ? {
704
+ initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
705
+ }
706
+ : {}),
656
707
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
657
708
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
658
709
  subjectId: input.subjectId ?? null,
710
+ ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
659
711
  });
660
712
  if (keyedResult.denied) {
661
713
  throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
@@ -702,6 +754,7 @@ export async function createAndStartSessionWithOutcome(input: {
702
754
  variableSetId: input.variableSet?.id ?? null,
703
755
  rigId: input.rigId ?? null,
704
756
  rigVersionId: input.rigVersionId ?? null,
757
+ channelId: input.channelId ?? null,
705
758
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
706
759
  firstPartyMcpTools: input.firstPartyMcpTools,
707
760
  instructions: input.instructions ?? null,
@@ -711,9 +764,15 @@ export async function createAndStartSessionWithOutcome(input: {
711
764
  ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
712
765
  mcpServers: input.mcpServers ?? [],
713
766
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
767
+ ...(input.xaiProviderAccountAuthoritySnapshot
768
+ ? {
769
+ initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
770
+ }
771
+ : {}),
714
772
  maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
715
773
  allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
716
774
  subjectId: input.subjectId ?? null,
775
+ ...(input.beforeCreateCommit ? { beforeCreateCommit: input.beforeCreateCommit } : {}),
717
776
  });
718
777
  } catch (error) {
719
778
  if (error instanceof SessionSpawnDeniedDbError) {
@@ -747,7 +806,7 @@ async function finishStartSession(
747
806
  input: {
748
807
  db: Database;
749
808
  bus: EventBus;
750
- workflowClient: SessionWorkflowClient;
809
+ workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
751
810
  initialMessage: string;
752
811
  deferInitialTurn?: boolean;
753
812
  turnInstructions?: string | null;
@@ -767,7 +826,10 @@ async function finishStartSession(
767
826
  settings: Settings;
768
827
  workingDir?: string | null;
769
828
  } | null;
770
- consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
829
+ consumeNewSessionDraft?: {
830
+ subjectId: string;
831
+ expectedRevision: number;
832
+ } | null;
771
833
  },
772
834
  session: Session,
773
835
  ): Promise<{ session: CreateSessionResponse; changed: boolean }> {
@@ -790,7 +852,11 @@ async function finishStartSession(
790
852
  sessionGroupId: session.sandboxGroupId,
791
853
  };
792
854
  const seeded = await swapActiveSandbox(
793
- { db: input.db, settings: input.seedTargetSandbox.settings, bus: input.bus },
855
+ {
856
+ db: input.db,
857
+ settings: input.seedTargetSandbox.settings,
858
+ bus: input.bus,
859
+ },
794
860
  ctx,
795
861
  input.seedTargetSandbox.sandboxId,
796
862
  // The working dir is committed in the SAME epoch-fenced CAS that seeds the
@@ -813,7 +879,10 @@ async function finishStartSession(
813
879
  createdEventPayload: {
814
880
  toolPolicy: input.toolPolicy,
815
881
  ...(input.variableSet
816
- ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
882
+ ? {
883
+ variableSetId: input.variableSet.id,
884
+ variableSetName: input.variableSet.name,
885
+ }
817
886
  : {}),
818
887
  ...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
819
888
  },
@@ -863,10 +932,10 @@ export function workflowIdForSession(sessionId: string): string {
863
932
  * it cannot be resolved to a provider at run time, so we fail the request at
864
933
  * the API edge with 422 rather than enqueuing a turn the worker can't honor.
865
934
  *
866
- * `model` is the explicit, caller-supplied value (null/undefined when omitted).
867
- * An omitted model defaults to `settings.openaiModel` downstream which is
868
- * always first in `configuredAllowedModels` so only an explicit value is
869
- * checked. Centralized here so every model-carrying choke point
935
+ * `model` is the effective value selected by the caller's boundary. Top-level
936
+ * omission defaults to `settings.openaiModel`; child-session omission inherits
937
+ * the worker-signed calling turn before reaching this helper. Centralized here
938
+ * so every model-carrying choke point
870
939
  * (create-session, user-message/turn-accept, queued-turn update, and
871
940
  * scheduled-task agentConfig — a scheduled task is a session the worker runs
872
941
  * later) and the MCP surfaces that share them validate identically and cannot
@@ -892,6 +961,16 @@ export function canonicalConfiguredModel(
892
961
  if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
893
962
  return canonicalModel;
894
963
  }
964
+ // SuperGrok subscription models are also discovered per workspace rather
965
+ // than stored in the deployment-global allow-list. Connection availability
966
+ // is enforced by the workspace policy and worker; this edge guard only needs
967
+ // to admit the product-model namespace when the feature is enabled.
968
+ if (
969
+ settings.supergrokSubscriptionEnabled &&
970
+ canonicalModel.startsWith(XAI_SUBSCRIPTION_MODEL_ID_PREFIX)
971
+ ) {
972
+ return canonicalModel;
973
+ }
895
974
  if (canonicalModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
896
975
  return canonicalModel;
897
976
  }
@@ -1023,6 +1102,7 @@ export async function postUserMessageTurn(input: {
1023
1102
  workspaceId: string;
1024
1103
  sessionId: string;
1025
1104
  text: string;
1105
+ annotations?: TimelineAnnotation[];
1026
1106
  turnInstructions?: string | null;
1027
1107
  resources: ResourceRef[];
1028
1108
  model?: string | null;
@@ -1040,7 +1120,14 @@ export async function postUserMessageTurn(input: {
1040
1120
  expectedDraftRevision?: number | null;
1041
1121
  reasoningEffortFallback?: Settings["openaiReasoningEffort"];
1042
1122
  turnExecutionPolicy: TurnExecutionPolicyV1;
1043
- }): Promise<{ accepted: SessionEvent; turn: SessionTurn; replay: boolean }> {
1123
+ recordAgentRunUsage?: boolean;
1124
+ schedulePostCommit?: (task: () => Promise<void>) => void;
1125
+ }): Promise<{
1126
+ accepted: SessionEvent;
1127
+ turn: SessionTurn;
1128
+ interruptionCount: number;
1129
+ replay: boolean;
1130
+ }> {
1044
1131
  const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
1045
1132
  const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
1046
1133
  const requestedReasoningEffort = input.reasoningEffort ?? null;
@@ -1061,35 +1148,50 @@ export async function postUserMessageTurn(input: {
1061
1148
  const operationKey = input.clientEventId ?? crypto.randomUUID();
1062
1149
  let result;
1063
1150
  try {
1064
- result = await withWorkspaceSubjectRls(db, workspaceId, input.actor ?? accountId, (scoped) =>
1065
- scoped.transaction((tx) =>
1066
- submitHumanPromptInTransaction(tx as unknown as Database, {
1067
- accountId,
1151
+ result = await runIdempotentPersistenceTransaction(
1152
+ {
1153
+ stage: "session.prompt.submit",
1154
+ eventTypes: ["user.message", "turn.queued", "session.status.changed"],
1155
+ maxAttempts: 3,
1156
+ },
1157
+ async () =>
1158
+ await withWorkspaceSubjectSessionActivityRls(
1159
+ db,
1068
1160
  workspaceId,
1069
- sessionId,
1070
- subjectId: input.actor ?? accountId,
1071
- ...(input.actorLabel ? { subjectLabel: input.actorLabel } : {}),
1072
- actor: input.commandActor ?? {
1073
- type: "human",
1074
- subjectId: input.actor ?? accountId,
1075
- },
1076
- operationKey,
1077
- delivery: input.delivery ?? "send",
1078
- controlEtag: input.controlEtag ?? null,
1079
- expectedDraftRevision: input.expectedDraftRevision ?? null,
1080
- text: input.text,
1081
- turnInstructions: input.turnInstructions ?? null,
1082
- resources: input.resources,
1083
- model: requestedModel,
1084
- reasoningEffort: requestedReasoningEffort,
1085
- latencyMode: input.latencyMode ?? null,
1086
- reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
1087
- turnExecutionPolicy: input.turnExecutionPolicy,
1088
- source: input.origin === "operator" ? "api" : "user",
1089
- personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1090
- mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1091
- }),
1092
- ),
1161
+ input.actor ?? accountId,
1162
+ (scoped) =>
1163
+ submitHumanPromptInTransaction(scoped, {
1164
+ accountId,
1165
+ workspaceId,
1166
+ sessionId,
1167
+ subjectId: input.actor ?? accountId,
1168
+ ...(input.actorLabel ? { subjectLabel: input.actorLabel } : {}),
1169
+ actor: input.commandActor ?? {
1170
+ type: "human",
1171
+ subjectId: input.actor ?? accountId,
1172
+ },
1173
+ operationKey,
1174
+ delivery: input.delivery ?? "send",
1175
+ controlEtag: input.controlEtag ?? null,
1176
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
1177
+ text: input.text,
1178
+ annotations: input.annotations ?? [],
1179
+ turnInstructions: input.turnInstructions ?? null,
1180
+ resources: input.resources,
1181
+ model: requestedModel,
1182
+ reasoningEffort: requestedReasoningEffort,
1183
+ latencyMode: input.latencyMode ?? null,
1184
+ reasoningEffortFallback:
1185
+ input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
1186
+ turnExecutionPolicy: input.turnExecutionPolicy,
1187
+ source: input.origin === "operator" ? "api" : "user",
1188
+ ...(input.recordAgentRunUsage !== undefined
1189
+ ? { recordAgentRunUsage: input.recordAgentRunUsage }
1190
+ : {}),
1191
+ personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1192
+ mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1193
+ }),
1194
+ ),
1093
1195
  );
1094
1196
  } catch (error) {
1095
1197
  if (
@@ -1106,54 +1208,68 @@ export async function postUserMessageTurn(input: {
1106
1208
  }
1107
1209
  throw error;
1108
1210
  }
1109
- const events = await Promise.all(
1110
- result.eventIds.map((eventId) => getSessionEvent(db, workspaceId, eventId)),
1111
- );
1112
- if (events.some((event) => event === null)) {
1113
- throw new Error("Committed prompt events could not be reloaded");
1114
- }
1115
- const turn = await getSessionTurn(db, workspaceId, result.turnId);
1116
- if (!turn) throw new Error("Committed prompt turn could not be reloaded");
1117
- const accepted = events.find((event) => event?.id === result.acceptedEventId);
1118
- if (!accepted) throw new Error("Committed user.message event could not be reloaded");
1119
- await publishDurableSessionEvents(
1120
- bus,
1121
- workspaceId,
1122
- sessionId,
1123
- events.filter((event): event is SessionEvent => event !== null),
1124
- );
1125
- if (result.workspaceControlEventId) {
1126
- const controlEvent = await getWorkspaceControlEvent(
1127
- db,
1128
- workspaceId,
1129
- result.workspaceControlEventId,
1130
- );
1131
- if (!controlEvent) {
1132
- throw new Error(
1133
- `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1134
- );
1211
+ const postCommitTask = async () => {
1212
+ try {
1213
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1214
+ if (result.workspaceControlEventId) {
1215
+ const controlEvent = await getWorkspaceControlEvent(
1216
+ db,
1217
+ workspaceId,
1218
+ result.workspaceControlEventId,
1219
+ );
1220
+ if (!controlEvent) {
1221
+ throw new Error(
1222
+ `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1223
+ );
1224
+ }
1225
+ await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1226
+ }
1227
+ } catch {
1228
+ console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1229
+ errorClass: "PromptEventFanoutOperationError",
1230
+ errorCode: "session_prompt_event_fanout_failed",
1231
+ origin: "core",
1232
+ });
1135
1233
  }
1136
- await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1137
- }
1138
- try {
1139
- await workflowClient.wakeSessionWorkflow({
1140
- accountId,
1141
- workspaceId,
1142
- sessionId,
1143
- workflowId: turn.temporalWorkflowId,
1144
- wakeRevision: result.wakeRevision,
1145
- ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1146
- ? { interruptionRequested: true }
1147
- : {}),
1234
+ try {
1235
+ await workflowClient.wakeSessionWorkflow({
1236
+ accountId,
1237
+ workspaceId,
1238
+ sessionId,
1239
+ workflowId: result.turn.temporalWorkflowId,
1240
+ wakeRevision: result.wakeRevision,
1241
+ ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1242
+ ? { interruptionRequested: true }
1243
+ : {}),
1244
+ });
1245
+ } catch {
1246
+ console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1247
+ errorClass: "WorkflowWakeOperationError",
1248
+ errorCode: "session_workflow_wake_failed",
1249
+ origin: "core",
1250
+ });
1251
+ }
1252
+ };
1253
+ const schedulePostCommit =
1254
+ input.schedulePostCommit ??
1255
+ ((task: () => Promise<void>) => {
1256
+ void task();
1148
1257
  });
1258
+ try {
1259
+ schedulePostCommit(postCommitTask);
1149
1260
  } catch {
1150
- console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1151
- errorClass: "WorkflowWakeOperationError",
1152
- errorCode: "session_workflow_wake_failed",
1261
+ console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
1262
+ errorClass: "PromptPostCommitScheduleError",
1263
+ errorCode: "session_prompt_post_commit_schedule_failed",
1153
1264
  origin: "core",
1154
1265
  });
1155
1266
  }
1156
- return { accepted, turn, replay: result.replay };
1267
+ return {
1268
+ accepted: result.accepted,
1269
+ turn: result.turn,
1270
+ interruptionCount: result.interruptionCount,
1271
+ replay: result.replay,
1272
+ };
1157
1273
  }
1158
1274
 
1159
1275
  /**
@@ -1201,11 +1317,18 @@ export async function createSessionForRequestWithOutcome(
1201
1317
  ? (grant.metadata["sessionId"] as string)
1202
1318
  : null;
1203
1319
  if (parentSessionId) {
1204
- await requireSessionAuthorization(deps, grant, {
1205
- sessionId: parentSessionId,
1206
- operation: "session.child.create",
1207
- surface: "core",
1208
- });
1320
+ try {
1321
+ await requireSessionAuthorization(deps, grant, {
1322
+ sessionId: parentSessionId,
1323
+ operation: "session.child.create",
1324
+ surface: "core",
1325
+ });
1326
+ } catch (error) {
1327
+ if (error instanceof SessionAuthorizationDeniedError) {
1328
+ throw new HTTPException(403, { message: error.message, cause: error });
1329
+ }
1330
+ throw error;
1331
+ }
1209
1332
  }
1210
1333
  const parentSession = parentSessionId ? await getSession(db, workspaceId, parentSessionId) : null;
1211
1334
  if (parentSessionId && !parentSession) {
@@ -1213,10 +1336,35 @@ export async function createSessionForRequestWithOutcome(
1213
1336
  message: `parent session not found in workspace: ${parentSessionId}`,
1214
1337
  });
1215
1338
  }
1339
+ const creationInitiator = creationInitiatorForGrant(grant);
1340
+ const parentCallingTurn =
1341
+ parentSession && creationInitiator.actor
1342
+ ? await getSessionTurn(db, workspaceId, creationInitiator.actor.turnId)
1343
+ : null;
1344
+ if (
1345
+ creationInitiator.actor &&
1346
+ (!parentCallingTurn || parentCallingTurn.sessionId !== parentSession?.id)
1347
+ ) {
1348
+ throw new HTTPException(403, {
1349
+ message: "caller attempt does not belong to the parent session",
1350
+ });
1351
+ }
1352
+ const xaiProviderAccountAuthoritySnapshot =
1353
+ parentSession && creationInitiator.actor
1354
+ ? await getSessionTurnXaiProviderAccountAuthoritySnapshot(
1355
+ db,
1356
+ workspaceId,
1357
+ parentSession.id,
1358
+ creationInitiator.actor.turnId,
1359
+ )
1360
+ : undefined;
1216
1361
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
1217
1362
  db,
1218
1363
  workspaceId,
1219
1364
  settings,
1365
+ {
1366
+ subjectId: grant.subjectId,
1367
+ },
1220
1368
  );
1221
1369
  const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers")
1222
1370
  ? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers)
@@ -1263,7 +1411,10 @@ export async function createSessionForRequestWithOutcome(
1263
1411
  "child tools may only narrow the parent session tool policy",
1264
1412
  );
1265
1413
  selectedTools = requestedTools;
1266
- toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
1414
+ toolPolicy = {
1415
+ mode: "explicit",
1416
+ inheritedFromSessionId: parentSession.id,
1417
+ };
1267
1418
  } else {
1268
1419
  selectedTools = parentEffective;
1269
1420
  toolPolicy = {
@@ -1296,7 +1447,9 @@ export async function createSessionForRequestWithOutcome(
1296
1447
  });
1297
1448
  await validateGitHubRepositorySelection(db, workspaceId, resources);
1298
1449
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
1299
- throw new HTTPException(503, { message: "object storage is not configured" });
1450
+ throw new HTTPException(503, {
1451
+ message: "object storage is not configured",
1452
+ });
1300
1453
  }
1301
1454
  await validateFileResources(db, workspaceId, resources);
1302
1455
  // VariableSet attachment requires variable-sets:use on the calling grant
@@ -1341,25 +1494,70 @@ export async function createSessionForRequestWithOutcome(
1341
1494
  frozenRigVersionId = rig.activeVersion.id;
1342
1495
  }
1343
1496
  }
1344
- const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
1497
+ // CHANNEL FILING. Pure rail organization: a UUID files the session into that
1498
+ // workspace channel, omission/null leaves it unfiled (inbox). Resolved
1499
+ // workspace-scoped so a foreign channel id can never attach; an explicit
1500
+ // unknown channelId is a caller error → 422.
1501
+ let channelId: string | null = null;
1502
+ if (payload.channelId) {
1503
+ const channel = await getChannel(db, workspaceId, payload.channelId);
1504
+ if (!channel) {
1505
+ throw new HTTPException(422, {
1506
+ message: `unknown channelId: ${payload.channelId}`,
1507
+ });
1508
+ }
1509
+ channelId = channel.id;
1510
+ }
1511
+ // A spawned worker is causally part of the exact turn that created it. Omitted
1512
+ // execution policy fields therefore inherit that calling turn rather than the
1513
+ // deployment defaults. This is especially important for Codex subscription
1514
+ // managers: falling back to the deployment model would silently move a child
1515
+ // onto the OpenGeni-credits billing path. Legacy session-bound grants without
1516
+ // exact attempt claims fall back to the parent session's persisted defaults.
1517
+ const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
1518
+ const model = canonicalConfiguredModel(settings, payload.model ?? inheritedModel);
1345
1519
  if (model === null || model === undefined) {
1346
1520
  throw new Error("effective session model unexpectedly resolved to null");
1347
1521
  }
1348
- // Session creation persists the EFFECTIVE model — an omitted payload.model
1349
- // stamps the deployment default onto the session — so the policy must vet
1350
- // that effective value, not just explicit ones (a restricted workspace's
1351
- // default-model session would otherwise be born blocked).
1522
+ // Session creation persists the EFFECTIVE model — the explicit selection,
1523
+ // inherited calling-turn model, or deployment default — so the policy must
1524
+ // vet that effective value, not just explicit ones (a restricted workspace's
1525
+ // inherited/default-model session would otherwise be born blocked).
1352
1526
  await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
1353
- const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
1354
- const latencyMode = payload.latencyMode ?? "standard";
1527
+ const inheritedReasoningEffort =
1528
+ parentCallingTurn?.reasoningEffort ??
1529
+ (parentSession
1530
+ ? reasoningEffortForSession(parentSession.metadata, settings.openaiReasoningEffort)
1531
+ : settings.openaiReasoningEffort);
1532
+ const inheritedLatencyMode =
1533
+ parentCallingTurn?.latencyMode ??
1534
+ (parentSession ? latencyModeForSession(parentSession.metadata, "standard") : "standard");
1535
+ const reasoningEffort = payload.reasoningEffort ?? inheritedReasoningEffort;
1536
+ const latencyMode = payload.latencyMode ?? inheritedLatencyMode;
1537
+ const inheritedFromParent = parentSession !== null;
1355
1538
  const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
1356
1539
  modelId: model,
1357
1540
  requestedModelId: payload.model ?? null,
1358
- modelSource: payload.model === undefined ? "deployment" : "explicit",
1541
+ modelSource:
1542
+ payload.model === undefined
1543
+ ? inheritedFromParent
1544
+ ? "continuation"
1545
+ : "deployment"
1546
+ : "explicit",
1359
1547
  reasoningEffort,
1360
- reasoningSource: payload.reasoningEffort === undefined ? "deployment" : "explicit",
1548
+ reasoningSource:
1549
+ payload.reasoningEffort === undefined
1550
+ ? inheritedFromParent
1551
+ ? "continuation"
1552
+ : "deployment"
1553
+ : "explicit",
1361
1554
  latencyMode,
1362
- latencyModeSource: payload.latencyMode === undefined ? "deployment" : "explicit",
1555
+ latencyModeSource:
1556
+ payload.latencyMode === undefined
1557
+ ? inheritedFromParent
1558
+ ? "continuation"
1559
+ : "deployment"
1560
+ : "explicit",
1363
1561
  });
1364
1562
  // Parent linkage was resolved above, before context validation. A child with
1365
1563
  // no explicit permission override inherits the creating session's effective
@@ -1429,12 +1627,22 @@ export async function createSessionForRequestWithOutcome(
1429
1627
  // Tool visibility is independent from permission authority. A child that
1430
1628
  // omits the field inherits the parent's exact effective selection; a
1431
1629
  // top-level omission selects the safe non-connector default catalog.
1630
+ const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(settings);
1631
+ const disallowedFirstPartyMcpTool = payload.firstPartyMcpTools?.find(
1632
+ (tool) => !deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
1633
+ );
1634
+ if (disallowedFirstPartyMcpTool) {
1635
+ throw new HTTPException(422, {
1636
+ message: `first-party MCP tool is disabled by deployment policy: ${disallowedFirstPartyMcpTool}`,
1637
+ });
1638
+ }
1432
1639
  const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
1433
1640
  payload.firstPartyMcpTools,
1434
1641
  parentSession ? parentSession.firstPartyMcpTools : undefined,
1642
+ deploymentFirstPartyMcpToolPolicy,
1435
1643
  );
1436
1644
  if (payload.goal) {
1437
- const missingGoalTools = ["goal_update", "goal_complete", "goal_pause"].filter(
1645
+ const missingGoalTools = ["goal_update", "goal_progress", "goal_complete", "goal_pause"].filter(
1438
1646
  (name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
1439
1647
  );
1440
1648
  if (missingGoalTools.length > 0) {
@@ -1664,7 +1872,6 @@ export async function createSessionForRequestWithOutcome(
1664
1872
  model,
1665
1873
  });
1666
1874
  }
1667
- const creationInitiator = creationInitiatorForGrant(grant);
1668
1875
  let createOutcome: CreateSessionOutcome;
1669
1876
  try {
1670
1877
  createOutcome = await createAndStartSessionWithOutcome({
@@ -1706,6 +1913,7 @@ export async function createSessionForRequestWithOutcome(
1706
1913
  // Frozen rig binding (M3): both null for a rig-less session (today's path).
1707
1914
  rigId: frozenRigId,
1708
1915
  rigVersionId: frozenRigVersionId,
1916
+ channelId,
1709
1917
  goal: payload.goal ?? null,
1710
1918
  // Per-session persona instructions (already trimmed/validated by the
1711
1919
  // contracts schema). Persisted on the row; composed system-level at turn
@@ -1717,6 +1925,7 @@ export async function createSessionForRequestWithOutcome(
1717
1925
  mcpServers: sessionMcpServers.dbServers,
1718
1926
  sessionMcpServers: sessionMcpServers.metadata,
1719
1927
  personalConnectionDelegations,
1928
+ ...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
1720
1929
  parentSessionId,
1721
1930
  createIdempotencyKey: payload.idempotencyKey ?? null,
1722
1931
  maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
@@ -1727,7 +1936,11 @@ export async function createSessionForRequestWithOutcome(
1727
1936
  // (after the row exists, before the first turn dispatches). Validation
1728
1937
  // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
1729
1938
  seedTargetSandbox: payload.targetSandboxId
1730
- ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null }
1939
+ ? {
1940
+ sandboxId: payload.targetSandboxId,
1941
+ settings,
1942
+ workingDir: payload.workingDir ?? null,
1943
+ }
1731
1944
  : null,
1732
1945
  consumeNewSessionDraft:
1733
1946
  payload.expectedNewSessionDraftRevision !== undefined
@@ -1810,6 +2023,7 @@ export async function acceptSessionUserMessageWithOutcome(
1810
2023
  sessionId: string,
1811
2024
  input: {
1812
2025
  text: string;
2026
+ annotations?: SubmittedTimelineAnnotation[];
1813
2027
  turnInstructions?: string | null;
1814
2028
  resources?: ResourceRef[];
1815
2029
  model?: string | null;
@@ -1822,8 +2036,14 @@ export async function acceptSessionUserMessageWithOutcome(
1822
2036
  controlEtag?: string | null;
1823
2037
  expectedDraftRevision?: number | null;
1824
2038
  },
1825
- ): Promise<{ accepted: SessionEvent; turn: SessionTurn; replay: boolean }> {
2039
+ ): Promise<{
2040
+ accepted: SessionEvent;
2041
+ turn: SessionTurn;
2042
+ interruptionCount: number;
2043
+ replay: boolean;
2044
+ }> {
1826
2045
  const { settings, db, bus, workflowClient, objectStorage } = deps;
2046
+ const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
1827
2047
  await requireSessionAuthorization(deps, grant, {
1828
2048
  sessionId,
1829
2049
  operation: input.delivery === "steer" ? "session.steer" : "session.append",
@@ -1864,6 +2084,12 @@ export async function acceptSessionUserMessageWithOutcome(
1864
2084
  latencyModeSource: input.latencyMode == null ? "session" : "explicit",
1865
2085
  });
1866
2086
  const requestedResources = normalizeResources(input.resources ?? []);
2087
+ const annotations = await validateSubmittedTimelineAnnotations(
2088
+ db,
2089
+ workspaceId,
2090
+ sessionId,
2091
+ input.annotations ?? [],
2092
+ );
1867
2093
  await requireLimit(deps, {
1868
2094
  accountId: grant.accountId,
1869
2095
  workspaceId,
@@ -1872,7 +2098,9 @@ export async function acceptSessionUserMessageWithOutcome(
1872
2098
  model: effectiveModel,
1873
2099
  });
1874
2100
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
1875
- throw new HTTPException(503, { message: "object storage is not configured" });
2101
+ throw new HTTPException(503, {
2102
+ message: "object storage is not configured",
2103
+ });
1876
2104
  }
1877
2105
  await validateFileResources(db, workspaceId, requestedResources);
1878
2106
  await validateGitHubRepositorySelection(db, workspaceId, [
@@ -1885,7 +2113,9 @@ export async function acceptSessionUserMessageWithOutcome(
1885
2113
  session: existingSession,
1886
2114
  updates: input.mcpCredentialUpdates ?? [],
1887
2115
  });
1888
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
2116
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings, {
2117
+ subjectId: grant.subjectId,
2118
+ });
1889
2119
  const personalConnectionDelegations = await freezePersonalConnectionDelegations({
1890
2120
  db,
1891
2121
  workspaceId,
@@ -1893,8 +2123,7 @@ export async function acceptSessionUserMessageWithOutcome(
1893
2123
  tools: existingSession.tools,
1894
2124
  source: personalConnectionDelegationSourceForGrant(grant),
1895
2125
  });
1896
- const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
1897
- const { accepted, turn, replay } = await postUserMessageTurn({
2126
+ const { accepted, turn, interruptionCount, replay } = await postUserMessageTurn({
1898
2127
  db,
1899
2128
  bus,
1900
2129
  workflowClient,
@@ -1903,6 +2132,7 @@ export async function acceptSessionUserMessageWithOutcome(
1903
2132
  workspaceId,
1904
2133
  sessionId,
1905
2134
  text: input.text,
2135
+ annotations,
1906
2136
  turnInstructions: input.turnInstructions ?? null,
1907
2137
  resources: requestedResources,
1908
2138
  model: input.model ?? null,
@@ -1933,24 +2163,10 @@ export async function acceptSessionUserMessageWithOutcome(
1933
2163
  ? { expectedDraftRevision: input.expectedDraftRevision }
1934
2164
  : {}),
1935
2165
  ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
2166
+ recordAgentRunUsage: true,
2167
+ ...(deps.schedulePromptPostCommit ? { schedulePostCommit: deps.schedulePromptPostCommit } : {}),
1936
2168
  });
1937
- await recordWorkspaceUsage(deps, {
1938
- accountId: grant.accountId,
1939
- workspaceId,
1940
- subjectId: grant.subjectId,
1941
- eventType: "agent_run.created",
1942
- quantity: 1,
1943
- unit: "run",
1944
- sourceResourceType: "session_turn",
1945
- sourceResourceId: turn.id,
1946
- sessionId,
1947
- turnId: turn.id,
1948
- initiator: turn.initiator,
1949
- initiatorContext: turn.initiatorContext,
1950
- origin: turn.source,
1951
- idempotencyKey: `agent_run.created:${workspaceId}:${turn.id}`,
1952
- });
1953
- return { accepted, turn, replay };
2169
+ return { accepted, turn, interruptionCount, replay };
1954
2170
  }
1955
2171
 
1956
2172
  /** Backward-compatible entity-returning path used by existing REST callers. */
@@ -1960,15 +2176,20 @@ export async function acceptSessionUserMessage(
1960
2176
  workspaceId: Parameters<typeof acceptSessionUserMessageWithOutcome>[2],
1961
2177
  sessionId: Parameters<typeof acceptSessionUserMessageWithOutcome>[3],
1962
2178
  input: Parameters<typeof acceptSessionUserMessageWithOutcome>[4],
1963
- ): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
1964
- const { accepted, turn } = await acceptSessionUserMessageWithOutcome(
2179
+ ): Promise<{
2180
+ accepted: SessionEvent;
2181
+ turn: SessionTurn;
2182
+ interruptionCount: number;
2183
+ replay: boolean;
2184
+ }> {
2185
+ const { accepted, turn, interruptionCount, replay } = await acceptSessionUserMessageWithOutcome(
1965
2186
  deps,
1966
2187
  grant,
1967
2188
  workspaceId,
1968
2189
  sessionId,
1969
2190
  input,
1970
2191
  );
1971
- return { accepted, turn };
2192
+ return { accepted, turn, interruptionCount, replay };
1972
2193
  }
1973
2194
 
1974
2195
  /**
@@ -2002,7 +2223,12 @@ export async function updateSessionTitle(
2002
2223
  surface: "core",
2003
2224
  });
2004
2225
  const workspaceId = grant.workspaceId;
2005
- const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
2226
+ const result = await updateSessionTitleRow(db, {
2227
+ workspaceId,
2228
+ sessionId,
2229
+ title,
2230
+ source,
2231
+ });
2006
2232
  if (result.updated) {
2007
2233
  await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
2008
2234
  {
@@ -2053,7 +2279,9 @@ export async function updateSessionMcpApprovalPolicy(
2053
2279
  async (_session, context) => {
2054
2280
  const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
2055
2281
  if (!result.server) {
2056
- throw new HTTPException(404, { message: "session MCP server not found" });
2282
+ throw new HTTPException(404, {
2283
+ message: "session MCP server not found",
2284
+ });
2057
2285
  }
2058
2286
  outcome.server = result.server;
2059
2287
  return {
@@ -2070,6 +2298,7 @@ export async function updateSessionMcpApprovalPolicy(
2070
2298
  : [],
2071
2299
  };
2072
2300
  },
2301
+ { activity: "semantic" },
2073
2302
  );
2074
2303
  const updatedServer = outcome.server;
2075
2304
  if (!updatedServer) {
@@ -2153,6 +2382,7 @@ export async function updateSessionToolPolicy(
2153
2382
  deps.db,
2154
2383
  grant.workspaceId,
2155
2384
  deps.settings,
2385
+ { subjectId: grant.subjectId },
2156
2386
  );
2157
2387
  const runtimeSettings = settingsWithSessionMcpServerMetadata(
2158
2388
  capabilityRuntimeSettings,
@@ -2168,7 +2398,9 @@ export async function updateSessionToolPolicy(
2168
2398
  (tool) => !validatedIds.has(`${tool.kind}:${tool.id}`),
2169
2399
  );
2170
2400
  if (unknown) {
2171
- throw new HTTPException(422, { message: `unknown MCP server id: ${unknown.id}` });
2401
+ throw new HTTPException(422, {
2402
+ message: `unknown MCP server id: ${unknown.id}`,
2403
+ });
2172
2404
  }
2173
2405
  return withFirstPartyTools(validatedTools, runtimeSettings);
2174
2406
  })()
@@ -2176,11 +2408,20 @@ export async function updateSessionToolPolicy(
2176
2408
  const explicitRequestedFirstPartyTools = explicitRequest
2177
2409
  ? [...explicitRequest.firstPartyMcpTools]
2178
2410
  : null;
2411
+ const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(deps.settings);
2412
+ const disallowedFirstPartyMcpTool = explicitRequestedFirstPartyTools?.find(
2413
+ (tool) => !deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
2414
+ );
2415
+ if (disallowedFirstPartyMcpTool) {
2416
+ throw new HTTPException(422, {
2417
+ message: `first-party MCP tool is disabled by deployment policy: ${disallowedFirstPartyMcpTool}`,
2418
+ });
2419
+ }
2179
2420
  const workspaceDefaultTools = withFirstPartyTools(
2180
2421
  withDefaultEnabledCapabilityMcpTools([], deps.settings, capabilityRuntimeSettings),
2181
2422
  runtimeSettings,
2182
2423
  );
2183
- const workspaceDefaultFirstPartyTools = [...FIRST_PARTY_MCP_TOOL_NAMES];
2424
+ const workspaceDefaultFirstPartyTools = [...deploymentFirstPartyMcpToolPolicy.default];
2184
2425
  const events = await appendSessionEventsWithLockedSessionUpdate(
2185
2426
  deps.db,
2186
2427
  grant.workspaceId,
@@ -2197,7 +2438,9 @@ export async function updateSessionToolPolicy(
2197
2438
  if (session.parentSessionId) {
2198
2439
  const parent = await context.getLockedSession(session.parentSessionId);
2199
2440
  if (!parent) {
2200
- throw new HTTPException(409, { message: "parent session is no longer available" });
2441
+ throw new HTTPException(409, {
2442
+ message: "parent session is no longer available",
2443
+ });
2201
2444
  }
2202
2445
  const parentTracksWorkspaceDefaults = parent.toolPolicy?.mode === "workspace_default";
2203
2446
  const parentEffective = withFirstPartyTools(
@@ -2210,9 +2453,12 @@ export async function updateSessionToolPolicy(
2210
2453
  : parent.tools,
2211
2454
  runtimeSettings,
2212
2455
  );
2456
+ const deploymentAllowedFirstPartyMcpTools = new Set(
2457
+ deploymentFirstPartyMcpToolPolicy.allowed,
2458
+ );
2213
2459
  const parentFirstPartyMcpTools = [
2214
- ...(parent.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS),
2215
- ];
2460
+ ...(parent.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default),
2461
+ ].filter((tool) => deploymentAllowedFirstPartyMcpTools.has(tool));
2216
2462
  if (requestedMode === "workspace_default") {
2217
2463
  if (!parentTracksWorkspaceDefaults) {
2218
2464
  throw new HTTPException(403, {
@@ -2266,7 +2512,8 @@ export async function updateSessionToolPolicy(
2266
2512
  const unchanged =
2267
2513
  stableJson({
2268
2514
  tools: session.tools,
2269
- firstPartyMcpTools: session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS,
2515
+ firstPartyMcpTools:
2516
+ session.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default,
2270
2517
  policy: currentPolicy,
2271
2518
  }) ===
2272
2519
  stableJson({
@@ -2287,7 +2534,7 @@ export async function updateSessionToolPolicy(
2287
2534
  before: toolPolicyAuditSnapshot(
2288
2535
  session,
2289
2536
  session.tools,
2290
- [...(session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)],
2537
+ [...(session.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default)],
2291
2538
  currentPolicy,
2292
2539
  ),
2293
2540
  after: toolPolicyAuditSnapshot(
@@ -2310,7 +2557,7 @@ export async function updateSessionToolPolicy(
2310
2557
  },
2311
2558
  };
2312
2559
  },
2313
- { lockParentSession: true },
2560
+ { activity: "semantic", lockParentSession: true },
2314
2561
  );
2315
2562
  if (events.length > 0) {
2316
2563
  await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);