@opengeni/core 0.21.2 → 0.24.1

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 (123) hide show
  1. package/dist/access/index.d.ts +9 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +9 -4
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/chunk-6WKPWE5S.js +2380 -0
  6. package/dist/chunk-6WKPWE5S.js.map +1 -0
  7. package/dist/chunk-JJU6I5XD.js +2713 -0
  8. package/dist/chunk-JJU6I5XD.js.map +1 -0
  9. package/dist/chunk-NYPMQ7NO.js +2270 -0
  10. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  11. package/dist/dependencies.d.ts +26 -6
  12. package/dist/domain/capabilities.d.ts +35 -3
  13. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  14. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  15. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  16. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  17. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  18. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  19. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  20. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  21. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  22. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  23. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  24. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  25. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  26. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  27. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  28. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  29. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  30. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  31. package/dist/domain/memory-slack-publication.d.ts +0 -2
  32. package/dist/domain/packs.d.ts +29 -1
  33. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  34. package/dist/domain/scheduled-tasks.d.ts +27 -3
  35. package/dist/domain/session-tool-policy.d.ts +2 -2
  36. package/dist/domain/sessions.d.ts +44 -10
  37. package/dist/domain/skill-imports.d.ts +32 -0
  38. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  39. package/dist/domain/timeline-annotations.d.ts +5 -0
  40. package/dist/domain/video-generation-capabilities.d.ts +9 -0
  41. package/dist/domain/video-generation.d.ts +47 -0
  42. package/dist/editable-artifact-live/application.d.ts +69 -0
  43. package/dist/editable-artifact-live/errors.d.ts +1 -0
  44. package/dist/editable-artifact-live/index.d.ts +8 -0
  45. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  46. package/dist/editable-artifact-live/ports.d.ts +100 -0
  47. package/dist/editable-artifact-live/server.d.ts +71 -0
  48. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  49. package/dist/editable-artifact-live/types.d.ts +213 -0
  50. package/dist/editable-artifact-live/wire.d.ts +34 -0
  51. package/dist/editable-artifact-live.d.ts +2 -0
  52. package/dist/editable-artifact-live.js +58 -0
  53. package/dist/editable-artifact-live.js.map +1 -0
  54. package/dist/editable-artifacts.d.ts +2 -0
  55. package/dist/editable-artifacts.js +179 -0
  56. package/dist/editable-artifacts.js.map +1 -0
  57. package/dist/index.d.ts +35 -25
  58. package/dist/index.js +3184 -652
  59. package/dist/index.js.map +1 -1
  60. package/dist/managed-session.d.ts +53 -0
  61. package/dist/rigs/index.d.ts +1 -1
  62. package/dist/rigs/provider-images.d.ts +31 -0
  63. package/dist/sandbox/fleet.d.ts +9 -1
  64. package/dist/sandbox/routing.d.ts +1 -0
  65. package/dist/session-authorization.d.ts +10 -1
  66. package/dist/workflow-wake-contract.d.ts +7 -0
  67. package/package.json +18 -10
  68. package/src/access/index.ts +43 -3
  69. package/src/application/new-session-drafts.ts +2 -0
  70. package/src/application/session-commands.ts +217 -116
  71. package/src/dependencies.ts +30 -2
  72. package/src/domain/capabilities.ts +672 -218
  73. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  74. package/src/domain/durable-learning-slack-publication.ts +202 -0
  75. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  76. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  77. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  78. package/src/domain/editable-artifacts/errors.ts +115 -0
  79. package/src/domain/editable-artifacts/hash.ts +131 -0
  80. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  81. package/src/domain/editable-artifacts/index.ts +14 -0
  82. package/src/domain/editable-artifacts/ports.ts +462 -0
  83. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  84. package/src/domain/editable-artifacts/service.ts +2402 -0
  85. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  86. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  87. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  88. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  89. package/src/domain/editable-artifacts/types.ts +835 -0
  90. package/src/domain/insights.ts +136 -33
  91. package/src/domain/memory-slack-delivery.ts +204 -0
  92. package/src/domain/memory-slack-publication.ts +10 -19
  93. package/src/domain/packs.ts +266 -5
  94. package/src/domain/personal-connection-delegations.ts +97 -6
  95. package/src/domain/resources.ts +10 -20
  96. package/src/domain/scheduled-tasks.ts +365 -25
  97. package/src/domain/session-tool-policy.ts +8 -2
  98. package/src/domain/sessions.ts +388 -155
  99. package/src/domain/skill-imports.ts +350 -0
  100. package/src/domain/slack-publication-secret-safety.ts +42 -0
  101. package/src/domain/timeline-annotations.ts +204 -0
  102. package/src/domain/video-generation-capabilities.ts +69 -0
  103. package/src/domain/video-generation.ts +249 -0
  104. package/src/editable-artifact-live/application.ts +182 -0
  105. package/src/editable-artifact-live/errors.ts +4 -0
  106. package/src/editable-artifact-live/index.ts +8 -0
  107. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  108. package/src/editable-artifact-live/ports.ts +139 -0
  109. package/src/editable-artifact-live/server.ts +1707 -0
  110. package/src/editable-artifact-live/ticket.ts +217 -0
  111. package/src/editable-artifact-live/types.ts +286 -0
  112. package/src/editable-artifact-live/wire.ts +95 -0
  113. package/src/editable-artifact-live.ts +2 -0
  114. package/src/editable-artifacts.ts +2 -0
  115. package/src/index.ts +14 -0
  116. package/src/managed-auth-type.ts +4 -3
  117. package/src/managed-session.ts +36 -0
  118. package/src/rigs/index.ts +26 -19
  119. package/src/rigs/provider-images.ts +110 -0
  120. package/src/sandbox/fleet.ts +56 -1
  121. package/src/sandbox/routing.ts +32 -3
  122. package/src/session-authorization.ts +20 -0
  123. package/src/workflow-wake-contract.ts +8 -0
@@ -35,6 +35,8 @@ import {
35
35
  type SessionMcpCredentialUpdateInput,
36
36
  type SessionMcpServerInput,
37
37
  type SessionMcpServerMetadata,
38
+ type SubmittedTimelineAnnotation,
39
+ type TimelineAnnotation,
38
40
  type UpdateSessionMcpApprovalPolicyResponse,
39
41
  type UpdateSessionToolPolicyRequest,
40
42
  type SessionAuthorizationPort,
@@ -59,7 +61,6 @@ import {
59
61
  getSession,
60
62
  SessionIdConflictError,
61
63
  getSessionSpawnDenialByIdempotencyKey,
62
- getSessionEvent,
63
64
  getWorkspaceControlEvent,
64
65
  getSessionLineage,
65
66
  getSessionTurn,
@@ -71,12 +72,13 @@ import {
71
72
  submitHumanPromptInTransaction,
72
73
  appendSessionEventsWithLockedSessionUpdate,
73
74
  updateSessionTitle as updateSessionTitleRow,
74
- withWorkspaceSubjectRls,
75
+ withWorkspaceSubjectSessionActivityRls,
75
76
  type CreateSessionMcpServerInput,
76
77
  type Database,
77
78
  type UpdateSessionMcpServerCredentialsInput,
78
79
  QueueCommandConflictError,
79
80
  AgentCommandAuthorityError,
81
+ runIdempotentPersistenceTransaction,
80
82
  SessionSpawnDeniedDbError,
81
83
  SessionControlConflictError,
82
84
  SessionToolPolicyVersionConflictError,
@@ -99,6 +101,7 @@ import type {
99
101
  import { requireSessionAuthorization } from "../session-authorization";
100
102
  import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
101
103
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
104
+ import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
102
105
  import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
103
106
  import {
104
107
  freezePersonalConnectionDelegations,
@@ -241,7 +244,9 @@ export function creationInitiatorForGrant(grant: AccessGrant): FrozenCreationIni
241
244
  !Number.isSafeInteger(callerExecutionGeneration) ||
242
245
  callerExecutionGeneration < 1
243
246
  ) {
244
- throw new HTTPException(403, { message: "caller attempt claims are incomplete" });
247
+ throw new HTTPException(403, {
248
+ message: "caller attempt claims are incomplete",
249
+ });
245
250
  }
246
251
  const actor = {
247
252
  type: "agent_attempt",
@@ -284,11 +289,15 @@ function normalizedSessionMcpCredentialHeaders(
284
289
  const seen = new Set<string>();
285
290
  for (const [name, value] of entries) {
286
291
  if (!sessionMcpCredentialHeaderName.test(name)) {
287
- throw new HTTPException(422, { message: `invalid credential header name: ${name}` });
292
+ throw new HTTPException(422, {
293
+ message: `invalid credential header name: ${name}`,
294
+ });
288
295
  }
289
296
  const lower = name.toLowerCase();
290
297
  if (seen.has(lower)) {
291
- throw new HTTPException(422, { message: `duplicate credential header name: ${name}` });
298
+ throw new HTTPException(422, {
299
+ message: `duplicate credential header name: ${name}`,
300
+ });
292
301
  }
293
302
  seen.add(lower);
294
303
  if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
@@ -388,11 +397,15 @@ function validateSessionMcpServersForCreate(
388
397
  const metadata: SessionMcpServerMetadata[] = [];
389
398
  for (const server of servers) {
390
399
  if (seenIds.has(server.id)) {
391
- throw new HTTPException(422, { message: `duplicate session MCP server id: ${server.id}` });
400
+ throw new HTTPException(422, {
401
+ message: `duplicate session MCP server id: ${server.id}`,
402
+ });
392
403
  }
393
404
  seenIds.add(server.id);
394
405
  if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
395
- throw new HTTPException(422, { message: `MCP server id already exists: ${server.id}` });
406
+ throw new HTTPException(422, {
407
+ message: `MCP server id already exists: ${server.id}`,
408
+ });
396
409
  }
397
410
  const headers = normalizedSessionMcpCredentialHeaders(server.headers);
398
411
  const headersEncrypted = Object.fromEntries(
@@ -490,7 +503,9 @@ function validateSessionMcpCredentialUpdates(input: {
490
503
  }
491
504
  seenIds.add(update.id);
492
505
  if (!knownIds.has(update.id)) {
493
- throw new HTTPException(422, { message: `unknown session MCP server id: ${update.id}` });
506
+ throw new HTTPException(422, {
507
+ message: `unknown session MCP server id: ${update.id}`,
508
+ });
494
509
  }
495
510
  const headers = normalizedSessionMcpCredentialHeaders(update.headers);
496
511
  return {
@@ -506,7 +521,22 @@ function validateSessionMcpCredentialUpdates(input: {
506
521
  return encryptedUpdates;
507
522
  }
508
523
 
509
- export async function createAndStartSession(input: {
524
+ export type CreateSessionOutcome = {
525
+ session: CreateSessionResponse;
526
+ /** The committed create/start effect represented by this request. */
527
+ outcome: "created" | "repaired" | "replayed";
528
+ /** Backward-compatible replay flag for existing entity-oriented callers. */
529
+ replay: boolean;
530
+ /** True when the request created/repaired start state or committed a new wake revision. */
531
+ changed: boolean;
532
+ };
533
+
534
+ export type CreateSessionRequestOutcome = CreateSessionOutcome & {
535
+ /** Billing telemetry is recorded after the committed session start. */
536
+ usageRecording: "recorded" | "failed";
537
+ };
538
+
539
+ export async function createAndStartSessionWithOutcome(input: {
510
540
  requestedSessionId?: string;
511
541
  db: Database;
512
542
  bus: EventBus;
@@ -587,17 +617,24 @@ export async function createAndStartSession(input: {
587
617
  // target fails the create (422) — never a silent fall-back to the default box.
588
618
  // `workingDir` (optional) is the path/cwd base the chosen machine runs under,
589
619
  // seeded alongside the pointer through the epoch-fenced CAS.
590
- seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
620
+ seedTargetSandbox?: {
621
+ sandboxId: string;
622
+ settings: Settings;
623
+ workingDir?: string | null;
624
+ } | null;
591
625
  // Exact actor-private pre-session draft represented by this create. The
592
626
  // initializer consumes it only after the first durable runnable unit commits.
593
- consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
627
+ consumeNewSessionDraft?: {
628
+ subjectId: string;
629
+ expectedRevision: number;
630
+ } | null;
594
631
  // A child may lower its inherited nested-agent depth limit freely; increases
595
632
  // are authorized by the caller's workspace:admin grant and checked again by
596
633
  // the database admission transaction.
597
634
  maxNestedAgentDepthOverride?: number | null;
598
635
  allowNestedAgentDepthIncrease?: boolean;
599
636
  subjectId?: string | null;
600
- }): Promise<CreateSessionResponse> {
637
+ }): Promise<CreateSessionOutcome> {
601
638
  const sessionMetadata = {
602
639
  ...input.metadata,
603
640
  model: input.model,
@@ -647,12 +684,24 @@ export async function createAndStartSession(input: {
647
684
  }
648
685
  const { session: keyed, created } = keyedResult;
649
686
  if (!created) {
650
- return await finishStartSession(
687
+ const finished = await finishStartSession(
651
688
  keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
652
689
  keyed,
653
690
  );
691
+ return {
692
+ session: finished.session,
693
+ outcome: finished.changed ? "repaired" : "replayed",
694
+ replay: !finished.changed,
695
+ changed: finished.changed,
696
+ };
654
697
  }
655
- return await finishStartSession(input, keyed);
698
+ const finished = await finishStartSession(input, keyed);
699
+ return {
700
+ session: finished.session,
701
+ outcome: "created",
702
+ replay: false,
703
+ changed: true,
704
+ };
656
705
  }
657
706
  let session: Session;
658
707
  try {
@@ -694,7 +743,20 @@ export async function createAndStartSession(input: {
694
743
  }
695
744
  throw error;
696
745
  }
697
- return await finishStartSession(input, session);
746
+ const finished = await finishStartSession(input, session);
747
+ return {
748
+ session: finished.session,
749
+ outcome: "created",
750
+ replay: false,
751
+ changed: true,
752
+ };
753
+ }
754
+
755
+ /** Backward-compatible entity-returning create path used by existing callers. */
756
+ export async function createAndStartSession(
757
+ input: Parameters<typeof createAndStartSessionWithOutcome>[0],
758
+ ): Promise<CreateSessionResponse> {
759
+ return (await createAndStartSessionWithOutcome(input)).session;
698
760
  }
699
761
 
700
762
  /**
@@ -727,10 +789,13 @@ async function finishStartSession(
727
789
  settings: Settings;
728
790
  workingDir?: string | null;
729
791
  } | null;
730
- consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
792
+ consumeNewSessionDraft?: {
793
+ subjectId: string;
794
+ expectedRevision: number;
795
+ } | null;
731
796
  },
732
797
  session: Session,
733
- ): Promise<CreateSessionResponse> {
798
+ ): Promise<{ session: CreateSessionResponse; changed: boolean }> {
734
799
  // Create-time machine targeting (A-2a): seed the active-sandbox pointer BEFORE
735
800
  // the atomic initial turn transaction, so the FIRST turn routes to the chosen
736
801
  // machine. swapActiveSandbox does
@@ -750,7 +815,11 @@ async function finishStartSession(
750
815
  sessionGroupId: session.sandboxGroupId,
751
816
  };
752
817
  const seeded = await swapActiveSandbox(
753
- { db: input.db, settings: input.seedTargetSandbox.settings, bus: input.bus },
818
+ {
819
+ db: input.db,
820
+ settings: input.seedTargetSandbox.settings,
821
+ bus: input.bus,
822
+ },
754
823
  ctx,
755
824
  input.seedTargetSandbox.sandboxId,
756
825
  // The working dir is committed in the SAME epoch-fenced CAS that seeds the
@@ -773,7 +842,10 @@ async function finishStartSession(
773
842
  createdEventPayload: {
774
843
  toolPolicy: input.toolPolicy,
775
844
  ...(input.variableSet
776
- ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
845
+ ? {
846
+ variableSetId: input.variableSet.id,
847
+ variableSetName: input.variableSet.name,
848
+ }
777
849
  : {}),
778
850
  ...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
779
851
  },
@@ -806,7 +878,10 @@ async function finishStartSession(
806
878
  started.turn?.id ??
807
879
  (await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ??
808
880
  null;
809
- return { ...persisted, initialTurnId };
881
+ return {
882
+ session: { ...persisted, initialTurnId },
883
+ changed: started.changed,
884
+ };
810
885
  }
811
886
 
812
887
  export function workflowIdForSession(sessionId: string): string {
@@ -820,10 +895,10 @@ export function workflowIdForSession(sessionId: string): string {
820
895
  * it cannot be resolved to a provider at run time, so we fail the request at
821
896
  * the API edge with 422 rather than enqueuing a turn the worker can't honor.
822
897
  *
823
- * `model` is the explicit, caller-supplied value (null/undefined when omitted).
824
- * An omitted model defaults to `settings.openaiModel` downstream which is
825
- * always first in `configuredAllowedModels` so only an explicit value is
826
- * checked. Centralized here so every model-carrying choke point
898
+ * `model` is the effective value selected by the caller's boundary. Top-level
899
+ * omission defaults to `settings.openaiModel`; child-session omission inherits
900
+ * the worker-signed calling turn before reaching this helper. Centralized here
901
+ * so every model-carrying choke point
827
902
  * (create-session, user-message/turn-accept, queued-turn update, and
828
903
  * scheduled-task agentConfig — a scheduled task is a session the worker runs
829
904
  * later) and the MCP surfaces that share them validate identically and cannot
@@ -980,6 +1055,7 @@ export async function postUserMessageTurn(input: {
980
1055
  workspaceId: string;
981
1056
  sessionId: string;
982
1057
  text: string;
1058
+ annotations?: TimelineAnnotation[];
983
1059
  turnInstructions?: string | null;
984
1060
  resources: ResourceRef[];
985
1061
  model?: string | null;
@@ -997,7 +1073,14 @@ export async function postUserMessageTurn(input: {
997
1073
  expectedDraftRevision?: number | null;
998
1074
  reasoningEffortFallback?: Settings["openaiReasoningEffort"];
999
1075
  turnExecutionPolicy: TurnExecutionPolicyV1;
1000
- }): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
1076
+ recordAgentRunUsage?: boolean;
1077
+ schedulePostCommit?: (task: () => Promise<void>) => void;
1078
+ }): Promise<{
1079
+ accepted: SessionEvent;
1080
+ turn: SessionTurn;
1081
+ interruptionCount: number;
1082
+ replay: boolean;
1083
+ }> {
1001
1084
  const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
1002
1085
  const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
1003
1086
  const requestedReasoningEffort = input.reasoningEffort ?? null;
@@ -1018,35 +1101,50 @@ export async function postUserMessageTurn(input: {
1018
1101
  const operationKey = input.clientEventId ?? crypto.randomUUID();
1019
1102
  let result;
1020
1103
  try {
1021
- result = await withWorkspaceSubjectRls(db, workspaceId, input.actor ?? accountId, (scoped) =>
1022
- scoped.transaction((tx) =>
1023
- submitHumanPromptInTransaction(tx as unknown as Database, {
1024
- accountId,
1104
+ result = await runIdempotentPersistenceTransaction(
1105
+ {
1106
+ stage: "session.prompt.submit",
1107
+ eventTypes: ["user.message", "turn.queued", "session.status.changed"],
1108
+ maxAttempts: 3,
1109
+ },
1110
+ async () =>
1111
+ await withWorkspaceSubjectSessionActivityRls(
1112
+ db,
1025
1113
  workspaceId,
1026
- sessionId,
1027
- subjectId: input.actor ?? accountId,
1028
- ...(input.actorLabel ? { subjectLabel: input.actorLabel } : {}),
1029
- actor: input.commandActor ?? {
1030
- type: "human",
1031
- subjectId: input.actor ?? accountId,
1032
- },
1033
- operationKey,
1034
- delivery: input.delivery ?? "send",
1035
- controlEtag: input.controlEtag ?? null,
1036
- expectedDraftRevision: input.expectedDraftRevision ?? null,
1037
- text: input.text,
1038
- turnInstructions: input.turnInstructions ?? null,
1039
- resources: input.resources,
1040
- model: requestedModel,
1041
- reasoningEffort: requestedReasoningEffort,
1042
- latencyMode: input.latencyMode ?? null,
1043
- reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
1044
- turnExecutionPolicy: input.turnExecutionPolicy,
1045
- source: input.origin === "operator" ? "api" : "user",
1046
- personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1047
- mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1048
- }),
1049
- ),
1114
+ input.actor ?? accountId,
1115
+ (scoped) =>
1116
+ submitHumanPromptInTransaction(scoped, {
1117
+ accountId,
1118
+ workspaceId,
1119
+ sessionId,
1120
+ subjectId: input.actor ?? accountId,
1121
+ ...(input.actorLabel ? { subjectLabel: input.actorLabel } : {}),
1122
+ actor: input.commandActor ?? {
1123
+ type: "human",
1124
+ subjectId: input.actor ?? accountId,
1125
+ },
1126
+ operationKey,
1127
+ delivery: input.delivery ?? "send",
1128
+ controlEtag: input.controlEtag ?? null,
1129
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
1130
+ text: input.text,
1131
+ annotations: input.annotations ?? [],
1132
+ turnInstructions: input.turnInstructions ?? null,
1133
+ resources: input.resources,
1134
+ model: requestedModel,
1135
+ reasoningEffort: requestedReasoningEffort,
1136
+ latencyMode: input.latencyMode ?? null,
1137
+ reasoningEffortFallback:
1138
+ input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
1139
+ turnExecutionPolicy: input.turnExecutionPolicy,
1140
+ source: input.origin === "operator" ? "api" : "user",
1141
+ ...(input.recordAgentRunUsage !== undefined
1142
+ ? { recordAgentRunUsage: input.recordAgentRunUsage }
1143
+ : {}),
1144
+ personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1145
+ mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1146
+ }),
1147
+ ),
1050
1148
  );
1051
1149
  } catch (error) {
1052
1150
  if (
@@ -1063,53 +1161,68 @@ export async function postUserMessageTurn(input: {
1063
1161
  }
1064
1162
  throw error;
1065
1163
  }
1066
- const events = await Promise.all(
1067
- result.eventIds.map((eventId) => getSessionEvent(db, workspaceId, eventId)),
1068
- );
1069
- if (events.some((event) => event === null)) {
1070
- throw new Error("Committed prompt events could not be reloaded");
1071
- }
1072
- const turn = await getSessionTurn(db, workspaceId, result.turnId);
1073
- if (!turn) throw new Error("Committed prompt turn could not be reloaded");
1074
- const accepted = events.find((event) => event?.id === result.acceptedEventId);
1075
- if (!accepted) throw new Error("Committed user.message event could not be reloaded");
1076
- await publishDurableSessionEvents(
1077
- bus,
1078
- workspaceId,
1079
- sessionId,
1080
- events.filter((event): event is SessionEvent => event !== null),
1081
- );
1082
- if (result.workspaceControlEventId) {
1083
- const controlEvent = await getWorkspaceControlEvent(
1084
- db,
1085
- workspaceId,
1086
- result.workspaceControlEventId,
1087
- );
1088
- if (!controlEvent) {
1089
- throw new Error(
1090
- `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1091
- );
1164
+ const postCommitTask = async () => {
1165
+ try {
1166
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1167
+ if (result.workspaceControlEventId) {
1168
+ const controlEvent = await getWorkspaceControlEvent(
1169
+ db,
1170
+ workspaceId,
1171
+ result.workspaceControlEventId,
1172
+ );
1173
+ if (!controlEvent) {
1174
+ throw new Error(
1175
+ `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1176
+ );
1177
+ }
1178
+ await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1179
+ }
1180
+ } catch {
1181
+ console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1182
+ errorClass: "PromptEventFanoutOperationError",
1183
+ errorCode: "session_prompt_event_fanout_failed",
1184
+ origin: "core",
1185
+ });
1092
1186
  }
1093
- await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1094
- }
1187
+ try {
1188
+ await workflowClient.wakeSessionWorkflow({
1189
+ accountId,
1190
+ workspaceId,
1191
+ sessionId,
1192
+ workflowId: result.turn.temporalWorkflowId,
1193
+ wakeRevision: result.wakeRevision,
1194
+ ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1195
+ ? { interruptionRequested: true }
1196
+ : {}),
1197
+ });
1198
+ } catch {
1199
+ console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1200
+ errorClass: "WorkflowWakeOperationError",
1201
+ errorCode: "session_workflow_wake_failed",
1202
+ origin: "core",
1203
+ });
1204
+ }
1205
+ };
1206
+ const schedulePostCommit =
1207
+ input.schedulePostCommit ??
1208
+ ((task: () => Promise<void>) => {
1209
+ void task();
1210
+ });
1095
1211
  try {
1096
- await workflowClient.wakeSessionWorkflow({
1097
- accountId,
1098
- workspaceId,
1099
- sessionId,
1100
- workflowId: turn.temporalWorkflowId,
1101
- wakeRevision: result.wakeRevision,
1102
- ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1103
- ? { interruptionRequested: true }
1104
- : {}),
1212
+ schedulePostCommit(postCommitTask);
1213
+ } catch {
1214
+ console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
1215
+ errorClass: "PromptPostCommitScheduleError",
1216
+ errorCode: "session_prompt_post_commit_schedule_failed",
1217
+ origin: "core",
1105
1218
  });
1106
- } catch (error) {
1107
- console.warn(
1108
- `[sessions] workflow wake failed for committed prompt ${workspaceId}/${sessionId}; durable outbox will retry`,
1109
- error,
1110
- );
1111
1219
  }
1112
- return { accepted, turn };
1220
+ return {
1221
+ accepted: result.accepted,
1222
+ turn: result.turn,
1223
+ interruptionCount: result.interruptionCount,
1224
+ replay: result.replay,
1225
+ };
1113
1226
  }
1114
1227
 
1115
1228
  /**
@@ -1121,12 +1234,12 @@ export async function postUserMessageTurn(input: {
1121
1234
  * trusted immediate parent, while explicit arrays (including []) win. A
1122
1235
  * top-level create with omitted tools applies workspace-default capability MCPs.
1123
1236
  */
1124
- export async function createSessionForRequest(
1237
+ export async function createSessionForRequestWithOutcome(
1125
1238
  deps: ApiRouteDeps,
1126
1239
  grant: AccessGrant,
1127
1240
  workspaceId: string,
1128
1241
  rawPayload: unknown,
1129
- ): Promise<Session> {
1242
+ ): Promise<CreateSessionRequestOutcome> {
1130
1243
  const { settings, db, bus, workflowClient, objectStorage } = deps;
1131
1244
  const payload = CreateSessionRequest.parse(rawPayload);
1132
1245
  if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
@@ -1169,10 +1282,24 @@ export async function createSessionForRequest(
1169
1282
  message: `parent session not found in workspace: ${parentSessionId}`,
1170
1283
  });
1171
1284
  }
1285
+ const creationInitiator = creationInitiatorForGrant(grant);
1286
+ const parentCallingTurn =
1287
+ parentSession && creationInitiator.actor
1288
+ ? await getSessionTurn(db, workspaceId, creationInitiator.actor.turnId)
1289
+ : null;
1290
+ if (
1291
+ creationInitiator.actor &&
1292
+ (!parentCallingTurn || parentCallingTurn.sessionId !== parentSession?.id)
1293
+ ) {
1294
+ throw new HTTPException(403, {
1295
+ message: "caller attempt does not belong to the parent session",
1296
+ });
1297
+ }
1172
1298
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
1173
1299
  db,
1174
1300
  workspaceId,
1175
1301
  settings,
1302
+ { subjectId: grant.subjectId },
1176
1303
  );
1177
1304
  const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers")
1178
1305
  ? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers)
@@ -1219,7 +1346,10 @@ export async function createSessionForRequest(
1219
1346
  "child tools may only narrow the parent session tool policy",
1220
1347
  );
1221
1348
  selectedTools = requestedTools;
1222
- toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
1349
+ toolPolicy = {
1350
+ mode: "explicit",
1351
+ inheritedFromSessionId: parentSession.id,
1352
+ };
1223
1353
  } else {
1224
1354
  selectedTools = parentEffective;
1225
1355
  toolPolicy = {
@@ -1252,7 +1382,9 @@ export async function createSessionForRequest(
1252
1382
  });
1253
1383
  await validateGitHubRepositorySelection(db, workspaceId, resources);
1254
1384
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
1255
- throw new HTTPException(503, { message: "object storage is not configured" });
1385
+ throw new HTTPException(503, {
1386
+ message: "object storage is not configured",
1387
+ });
1256
1388
  }
1257
1389
  await validateFileResources(db, workspaceId, resources);
1258
1390
  // VariableSet attachment requires variable-sets:use on the calling grant
@@ -1297,25 +1429,56 @@ export async function createSessionForRequest(
1297
1429
  frozenRigVersionId = rig.activeVersion.id;
1298
1430
  }
1299
1431
  }
1300
- const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
1432
+ // A spawned worker is causally part of the exact turn that created it. Omitted
1433
+ // execution policy fields therefore inherit that calling turn rather than the
1434
+ // deployment defaults. This is especially important for Codex subscription
1435
+ // managers: falling back to the deployment model would silently move a child
1436
+ // onto the OpenGeni-credits billing path. Legacy session-bound grants without
1437
+ // exact attempt claims fall back to the parent session's persisted defaults.
1438
+ const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
1439
+ const model = canonicalConfiguredModel(settings, payload.model ?? inheritedModel);
1301
1440
  if (model === null || model === undefined) {
1302
1441
  throw new Error("effective session model unexpectedly resolved to null");
1303
1442
  }
1304
- // Session creation persists the EFFECTIVE model — an omitted payload.model
1305
- // stamps the deployment default onto the session — so the policy must vet
1306
- // that effective value, not just explicit ones (a restricted workspace's
1307
- // default-model session would otherwise be born blocked).
1443
+ // Session creation persists the EFFECTIVE model — the explicit selection,
1444
+ // inherited calling-turn model, or deployment default — so the policy must
1445
+ // vet that effective value, not just explicit ones (a restricted workspace's
1446
+ // inherited/default-model session would otherwise be born blocked).
1308
1447
  await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
1309
- const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
1310
- const latencyMode = payload.latencyMode ?? "standard";
1448
+ const inheritedReasoningEffort =
1449
+ parentCallingTurn?.reasoningEffort ??
1450
+ (parentSession
1451
+ ? reasoningEffortForSession(parentSession.metadata, settings.openaiReasoningEffort)
1452
+ : settings.openaiReasoningEffort);
1453
+ const inheritedLatencyMode =
1454
+ parentCallingTurn?.latencyMode ??
1455
+ (parentSession ? latencyModeForSession(parentSession.metadata, "standard") : "standard");
1456
+ const reasoningEffort = payload.reasoningEffort ?? inheritedReasoningEffort;
1457
+ const latencyMode = payload.latencyMode ?? inheritedLatencyMode;
1458
+ const inheritedFromParent = parentSession !== null;
1311
1459
  const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
1312
1460
  modelId: model,
1313
1461
  requestedModelId: payload.model ?? null,
1314
- modelSource: payload.model === undefined ? "deployment" : "explicit",
1462
+ modelSource:
1463
+ payload.model === undefined
1464
+ ? inheritedFromParent
1465
+ ? "continuation"
1466
+ : "deployment"
1467
+ : "explicit",
1315
1468
  reasoningEffort,
1316
- reasoningSource: payload.reasoningEffort === undefined ? "deployment" : "explicit",
1469
+ reasoningSource:
1470
+ payload.reasoningEffort === undefined
1471
+ ? inheritedFromParent
1472
+ ? "continuation"
1473
+ : "deployment"
1474
+ : "explicit",
1317
1475
  latencyMode,
1318
- latencyModeSource: payload.latencyMode === undefined ? "deployment" : "explicit",
1476
+ latencyModeSource:
1477
+ payload.latencyMode === undefined
1478
+ ? inheritedFromParent
1479
+ ? "continuation"
1480
+ : "deployment"
1481
+ : "explicit",
1319
1482
  });
1320
1483
  // Parent linkage was resolved above, before context validation. A child with
1321
1484
  // no explicit permission override inherits the creating session's effective
@@ -1620,10 +1783,9 @@ export async function createSessionForRequest(
1620
1783
  model,
1621
1784
  });
1622
1785
  }
1623
- const creationInitiator = creationInitiatorForGrant(grant);
1624
- let session: CreateSessionResponse;
1786
+ let createOutcome: CreateSessionOutcome;
1625
1787
  try {
1626
- session = await createAndStartSession({
1788
+ createOutcome = await createAndStartSessionWithOutcome({
1627
1789
  ...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
1628
1790
  db,
1629
1791
  bus,
@@ -1683,7 +1845,11 @@ export async function createSessionForRequest(
1683
1845
  // (after the row exists, before the first turn dispatches). Validation
1684
1846
  // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
1685
1847
  seedTargetSandbox: payload.targetSandboxId
1686
- ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null }
1848
+ ? {
1849
+ sandboxId: payload.targetSandboxId,
1850
+ settings,
1851
+ workingDir: payload.workingDir ?? null,
1852
+ }
1687
1853
  : null,
1688
1854
  consumeNewSessionDraft:
1689
1855
  payload.expectedNewSessionDraftRevision !== undefined
@@ -1704,24 +1870,52 @@ export async function createSessionForRequest(
1704
1870
  }
1705
1871
  throw error;
1706
1872
  }
1873
+ let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
1707
1874
  if (payload.startMode !== "realtime") {
1708
- await recordWorkspaceUsage(deps, {
1709
- accountId: grant.accountId,
1710
- workspaceId,
1711
- subjectId: grant.subjectId,
1712
- eventType: "agent_run.created",
1713
- quantity: 1,
1714
- unit: "run",
1715
- sourceResourceType: "session",
1716
- sourceResourceId: session.id,
1717
- sessionId: session.id,
1718
- initiator: session.createdBy,
1719
- initiatorContext: session.createdByContext,
1720
- origin: creationInitiator.actor ? "system" : "user",
1721
- idempotencyKey: `agent_run.created:${workspaceId}:${session.id}`,
1722
- });
1875
+ try {
1876
+ await recordWorkspaceUsage(deps, {
1877
+ accountId: grant.accountId,
1878
+ workspaceId,
1879
+ subjectId: grant.subjectId,
1880
+ eventType: "agent_run.created",
1881
+ quantity: 1,
1882
+ unit: "run",
1883
+ sourceResourceType: "session",
1884
+ sourceResourceId: createOutcome.session.id,
1885
+ sessionId: createOutcome.session.id,
1886
+ initiator: createOutcome.session.createdBy,
1887
+ initiatorContext: createOutcome.session.createdByContext,
1888
+ origin: creationInitiator.actor ? "system" : "user",
1889
+ idempotencyKey: `agent_run.created:${workspaceId}:${createOutcome.session.id}`,
1890
+ });
1891
+ } catch (error) {
1892
+ usageRecording = "failed";
1893
+ reportSessionUsageRecordingFailure(error);
1894
+ }
1723
1895
  }
1724
- return session;
1896
+ return { ...createOutcome, usageRecording };
1897
+ }
1898
+
1899
+ /** @internal Fixed public projection; the committed session outcome remains authoritative. */
1900
+ export function reportSessionUsageRecordingFailure(_error: unknown): void {
1901
+ console.warn(
1902
+ "[sessions] usage recording failed after committed session create; returning committed outcome",
1903
+ {
1904
+ errorClass: "UsageRecordingError",
1905
+ errorCode: "session_create_usage_recording_failed",
1906
+ origin: "core",
1907
+ },
1908
+ );
1909
+ }
1910
+
1911
+ /** Backward-compatible entity-returning request path for REST and core callers. */
1912
+ export async function createSessionForRequest(
1913
+ deps: ApiRouteDeps,
1914
+ grant: AccessGrant,
1915
+ workspaceId: string,
1916
+ rawPayload: unknown,
1917
+ ): Promise<CreateSessionResponse> {
1918
+ return (await createSessionForRequestWithOutcome(deps, grant, workspaceId, rawPayload)).session;
1725
1919
  }
1726
1920
 
1727
1921
  /**
@@ -1731,13 +1925,14 @@ export async function createSessionForRequest(
1731
1925
  * enqueue, and usage recording. `toolsProvided: false` durably preserves an
1732
1926
  * Tool selection is durable session state and never rides a follow-up prompt.
1733
1927
  */
1734
- export async function acceptSessionUserMessage(
1928
+ export async function acceptSessionUserMessageWithOutcome(
1735
1929
  deps: AcceptSessionUserMessageDependencies,
1736
1930
  grant: AccessGrant,
1737
1931
  workspaceId: string,
1738
1932
  sessionId: string,
1739
1933
  input: {
1740
1934
  text: string;
1935
+ annotations?: SubmittedTimelineAnnotation[];
1741
1936
  turnInstructions?: string | null;
1742
1937
  resources?: ResourceRef[];
1743
1938
  model?: string | null;
@@ -1750,7 +1945,12 @@ export async function acceptSessionUserMessage(
1750
1945
  controlEtag?: string | null;
1751
1946
  expectedDraftRevision?: number | null;
1752
1947
  },
1753
- ): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
1948
+ ): Promise<{
1949
+ accepted: SessionEvent;
1950
+ turn: SessionTurn;
1951
+ interruptionCount: number;
1952
+ replay: boolean;
1953
+ }> {
1754
1954
  const { settings, db, bus, workflowClient, objectStorage } = deps;
1755
1955
  await requireSessionAuthorization(deps, grant, {
1756
1956
  sessionId,
@@ -1792,6 +1992,12 @@ export async function acceptSessionUserMessage(
1792
1992
  latencyModeSource: input.latencyMode == null ? "session" : "explicit",
1793
1993
  });
1794
1994
  const requestedResources = normalizeResources(input.resources ?? []);
1995
+ const annotations = await validateSubmittedTimelineAnnotations(
1996
+ db,
1997
+ workspaceId,
1998
+ sessionId,
1999
+ input.annotations ?? [],
2000
+ );
1795
2001
  await requireLimit(deps, {
1796
2002
  accountId: grant.accountId,
1797
2003
  workspaceId,
@@ -1800,7 +2006,9 @@ export async function acceptSessionUserMessage(
1800
2006
  model: effectiveModel,
1801
2007
  });
1802
2008
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
1803
- throw new HTTPException(503, { message: "object storage is not configured" });
2009
+ throw new HTTPException(503, {
2010
+ message: "object storage is not configured",
2011
+ });
1804
2012
  }
1805
2013
  await validateFileResources(db, workspaceId, requestedResources);
1806
2014
  await validateGitHubRepositorySelection(db, workspaceId, [
@@ -1813,7 +2021,9 @@ export async function acceptSessionUserMessage(
1813
2021
  session: existingSession,
1814
2022
  updates: input.mcpCredentialUpdates ?? [],
1815
2023
  });
1816
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
2024
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings, {
2025
+ subjectId: grant.subjectId,
2026
+ });
1817
2027
  const personalConnectionDelegations = await freezePersonalConnectionDelegations({
1818
2028
  db,
1819
2029
  workspaceId,
@@ -1822,7 +2032,7 @@ export async function acceptSessionUserMessage(
1822
2032
  source: personalConnectionDelegationSourceForGrant(grant),
1823
2033
  });
1824
2034
  const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
1825
- const { accepted, turn } = await postUserMessageTurn({
2035
+ const { accepted, turn, interruptionCount, replay } = await postUserMessageTurn({
1826
2036
  db,
1827
2037
  bus,
1828
2038
  workflowClient,
@@ -1831,6 +2041,7 @@ export async function acceptSessionUserMessage(
1831
2041
  workspaceId,
1832
2042
  sessionId,
1833
2043
  text: input.text,
2044
+ annotations,
1834
2045
  turnInstructions: input.turnInstructions ?? null,
1835
2046
  resources: requestedResources,
1836
2047
  model: input.model ?? null,
@@ -1861,24 +2072,33 @@ export async function acceptSessionUserMessage(
1861
2072
  ? { expectedDraftRevision: input.expectedDraftRevision }
1862
2073
  : {}),
1863
2074
  ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
2075
+ recordAgentRunUsage: true,
2076
+ ...(deps.schedulePromptPostCommit ? { schedulePostCommit: deps.schedulePromptPostCommit } : {}),
1864
2077
  });
1865
- await recordWorkspaceUsage(deps, {
1866
- accountId: grant.accountId,
2078
+ return { accepted, turn, interruptionCount, replay };
2079
+ }
2080
+
2081
+ /** Backward-compatible entity-returning path used by existing REST callers. */
2082
+ export async function acceptSessionUserMessage(
2083
+ deps: Parameters<typeof acceptSessionUserMessageWithOutcome>[0],
2084
+ grant: Parameters<typeof acceptSessionUserMessageWithOutcome>[1],
2085
+ workspaceId: Parameters<typeof acceptSessionUserMessageWithOutcome>[2],
2086
+ sessionId: Parameters<typeof acceptSessionUserMessageWithOutcome>[3],
2087
+ input: Parameters<typeof acceptSessionUserMessageWithOutcome>[4],
2088
+ ): Promise<{
2089
+ accepted: SessionEvent;
2090
+ turn: SessionTurn;
2091
+ interruptionCount: number;
2092
+ replay: boolean;
2093
+ }> {
2094
+ const { accepted, turn, interruptionCount, replay } = await acceptSessionUserMessageWithOutcome(
2095
+ deps,
2096
+ grant,
1867
2097
  workspaceId,
1868
- subjectId: grant.subjectId,
1869
- eventType: "agent_run.created",
1870
- quantity: 1,
1871
- unit: "run",
1872
- sourceResourceType: "session_turn",
1873
- sourceResourceId: turn.id,
1874
2098
  sessionId,
1875
- turnId: turn.id,
1876
- initiator: turn.initiator,
1877
- initiatorContext: turn.initiatorContext,
1878
- origin: turn.source,
1879
- idempotencyKey: `agent_run.created:${workspaceId}:${turn.id}`,
1880
- });
1881
- return { accepted, turn };
2099
+ input,
2100
+ );
2101
+ return { accepted, turn, interruptionCount, replay };
1882
2102
  }
1883
2103
 
1884
2104
  /**
@@ -1912,7 +2132,12 @@ export async function updateSessionTitle(
1912
2132
  surface: "core",
1913
2133
  });
1914
2134
  const workspaceId = grant.workspaceId;
1915
- const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
2135
+ const result = await updateSessionTitleRow(db, {
2136
+ workspaceId,
2137
+ sessionId,
2138
+ title,
2139
+ source,
2140
+ });
1916
2141
  if (result.updated) {
1917
2142
  await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
1918
2143
  {
@@ -1963,7 +2188,9 @@ export async function updateSessionMcpApprovalPolicy(
1963
2188
  async (_session, context) => {
1964
2189
  const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
1965
2190
  if (!result.server) {
1966
- throw new HTTPException(404, { message: "session MCP server not found" });
2191
+ throw new HTTPException(404, {
2192
+ message: "session MCP server not found",
2193
+ });
1967
2194
  }
1968
2195
  outcome.server = result.server;
1969
2196
  return {
@@ -1980,6 +2207,7 @@ export async function updateSessionMcpApprovalPolicy(
1980
2207
  : [],
1981
2208
  };
1982
2209
  },
2210
+ { activity: "semantic" },
1983
2211
  );
1984
2212
  const updatedServer = outcome.server;
1985
2213
  if (!updatedServer) {
@@ -2063,6 +2291,7 @@ export async function updateSessionToolPolicy(
2063
2291
  deps.db,
2064
2292
  grant.workspaceId,
2065
2293
  deps.settings,
2294
+ { subjectId: grant.subjectId },
2066
2295
  );
2067
2296
  const runtimeSettings = settingsWithSessionMcpServerMetadata(
2068
2297
  capabilityRuntimeSettings,
@@ -2078,7 +2307,9 @@ export async function updateSessionToolPolicy(
2078
2307
  (tool) => !validatedIds.has(`${tool.kind}:${tool.id}`),
2079
2308
  );
2080
2309
  if (unknown) {
2081
- throw new HTTPException(422, { message: `unknown MCP server id: ${unknown.id}` });
2310
+ throw new HTTPException(422, {
2311
+ message: `unknown MCP server id: ${unknown.id}`,
2312
+ });
2082
2313
  }
2083
2314
  return withFirstPartyTools(validatedTools, runtimeSettings);
2084
2315
  })()
@@ -2107,7 +2338,9 @@ export async function updateSessionToolPolicy(
2107
2338
  if (session.parentSessionId) {
2108
2339
  const parent = await context.getLockedSession(session.parentSessionId);
2109
2340
  if (!parent) {
2110
- throw new HTTPException(409, { message: "parent session is no longer available" });
2341
+ throw new HTTPException(409, {
2342
+ message: "parent session is no longer available",
2343
+ });
2111
2344
  }
2112
2345
  const parentTracksWorkspaceDefaults = parent.toolPolicy?.mode === "workspace_default";
2113
2346
  const parentEffective = withFirstPartyTools(
@@ -2220,7 +2453,7 @@ export async function updateSessionToolPolicy(
2220
2453
  },
2221
2454
  };
2222
2455
  },
2223
- { lockParentSession: true },
2456
+ { activity: "semantic", lockParentSession: true },
2224
2457
  );
2225
2458
  if (events.length > 0) {
2226
2459
  await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);