@opengeni/core 2.5.3 → 2.6.4-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/index.d.ts +24 -0
- package/dist/billing/limits.d.ts +5 -0
- package/dist/canonical-human-identities.js +2 -2
- package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
- package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
- package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
- package/dist/dependencies.d.ts +10 -1
- package/dist/domain/company-brain-governed-writes.d.ts +15 -6
- package/dist/domain/company-profile-agent-admin.d.ts +3 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/memory-slack-delivery.d.ts +4 -1
- package/dist/domain/personal-connection-delegations.d.ts +1 -0
- package/dist/domain/pr-review.d.ts +1 -1
- package/dist/domain/scheduled-tasks.d.ts +12 -0
- package/dist/domain/sessions.d.ts +37 -11
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1706 -667
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +18 -0
- package/dist/managed-auth-session-sets.js +3 -1
- package/dist/model-catalog.d.ts +89 -0
- package/dist/sandbox/fleet.d.ts +6 -4
- package/dist/sandbox/routing.d.ts +7 -2
- package/dist/sandbox/runtime-settings.d.ts +17 -1
- package/package.json +10 -10
- package/src/access/index.ts +140 -5
- package/src/application/user-resource-grants.ts +31 -2
- package/src/billing/limits.ts +57 -24
- package/src/dependencies.ts +15 -1
- package/src/domain/company-brain-governed-writes.ts +29 -13
- package/src/domain/company-profile-agent-admin.ts +3 -2
- package/src/domain/environments.ts +6 -34
- package/src/domain/memory-slack-delivery.ts +30 -0
- package/src/domain/personal-connection-delegations.ts +40 -7
- package/src/domain/remember.ts +5 -6
- package/src/domain/scheduled-tasks.ts +146 -1
- package/src/domain/sessions.ts +912 -358
- package/src/domain/workspace-members.ts +34 -2
- package/src/index.ts +1 -0
- package/src/managed-auth-session-sets.ts +38 -11
- package/src/model-catalog.ts +565 -0
- package/src/sandbox/fleet.ts +20 -17
- package/src/sandbox/routing.ts +18 -4
- package/src/sandbox/runtime-settings.ts +32 -0
- /package/dist/{chunk-ZVZJTMSV.js.map → chunk-OF65T3PM.js.map} +0 -0
package/src/domain/sessions.ts
CHANGED
|
@@ -2,14 +2,16 @@ import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
|
|
|
2
2
|
import {
|
|
3
3
|
canonicalizeConfiguredModelId,
|
|
4
4
|
configuredAllowedModels,
|
|
5
|
+
WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
|
|
6
|
+
WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
|
|
5
7
|
resolveFirstPartyMcpToolPolicy,
|
|
6
8
|
policyProviderIdForModel,
|
|
7
9
|
resolveTurnExecutionPolicyV1,
|
|
8
|
-
WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
|
|
9
10
|
XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
|
|
10
11
|
type Settings,
|
|
11
12
|
} from "@opengeni/config";
|
|
12
13
|
import {
|
|
14
|
+
AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
13
15
|
CreateSessionRequest,
|
|
14
16
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
15
17
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
@@ -20,7 +22,10 @@ import {
|
|
|
20
22
|
ServiceTurnInitiator,
|
|
21
23
|
ServiceTurnInitiatorContext,
|
|
22
24
|
evaluateWorkspaceModelPolicy,
|
|
25
|
+
normalizeAutomaticSessionTitle,
|
|
23
26
|
resolveWorkspaceSessionToolDefaults,
|
|
27
|
+
metadataWithTurnExecutionPolicyV1,
|
|
28
|
+
readTurnExecutionPolicyV1,
|
|
24
29
|
stableJson,
|
|
25
30
|
type AccessGrant,
|
|
26
31
|
type ComposerDraft,
|
|
@@ -60,6 +65,7 @@ import {
|
|
|
60
65
|
import {
|
|
61
66
|
createSession,
|
|
62
67
|
createSessionWithIdempotencyKeyResult,
|
|
68
|
+
canonicalSessionCommandHash,
|
|
63
69
|
encryptVariableSetValue,
|
|
64
70
|
getAnySessionInGroup,
|
|
65
71
|
getEnrollment,
|
|
@@ -70,10 +76,10 @@ import {
|
|
|
70
76
|
listDistinctRigVersionIdsInGroup,
|
|
71
77
|
getSandbox,
|
|
72
78
|
getSession,
|
|
79
|
+
getInitializedSessionCreateReplay,
|
|
73
80
|
getSessionAuthorityProjection,
|
|
74
81
|
SessionIdConflictError,
|
|
75
82
|
NewSessionDraftConflictError,
|
|
76
|
-
getSessionSpawnDenialByIdempotencyKey,
|
|
77
83
|
getWorkspaceControlEvent,
|
|
78
84
|
getSessionLineage,
|
|
79
85
|
getSessionTurn,
|
|
@@ -85,7 +91,10 @@ import {
|
|
|
85
91
|
initializeSessionStartAtomically,
|
|
86
92
|
listSessionTurns,
|
|
87
93
|
listSessionMcpServersForChildInheritance,
|
|
94
|
+
lockActiveWorkspaceGatewayCustomModelForAdmission,
|
|
95
|
+
lockActiveWorkspaceOpenRouterCustomModelForAdmission,
|
|
88
96
|
requireSession,
|
|
97
|
+
replaySubmittedHumanPromptFromBoundaryReceipt,
|
|
89
98
|
submitHumanPromptInTransaction,
|
|
90
99
|
appendSessionEventsWithLockedSessionUpdate,
|
|
91
100
|
updateSessionTitleWithEvent,
|
|
@@ -126,6 +135,12 @@ import {
|
|
|
126
135
|
SessionAuthorizationDeniedError,
|
|
127
136
|
} from "../session-authorization";
|
|
128
137
|
import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
138
|
+
import { managedSessionGroupBackend } from "../sandbox/runtime-settings";
|
|
139
|
+
import {
|
|
140
|
+
isWorkspaceCustomModelId,
|
|
141
|
+
resolveWorkspaceCatalogSettings,
|
|
142
|
+
workspaceCustomModelReference,
|
|
143
|
+
} from "../model-catalog";
|
|
129
144
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
130
145
|
import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
|
|
131
146
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
@@ -607,6 +622,44 @@ export type CreateSessionRequestOutcome = CreateSessionOutcome & {
|
|
|
607
622
|
usageRecording: "recorded" | "failed";
|
|
608
623
|
};
|
|
609
624
|
|
|
625
|
+
type AgentChildSessionCreatePresentation = {
|
|
626
|
+
/** Model-authored title candidate from the first-party `session_create` tool.
|
|
627
|
+
* This is deliberately separate from the public REST request contract. */
|
|
628
|
+
automaticTitleCandidate?: string | null;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY = "agentChildAutomaticTitle" as const;
|
|
632
|
+
|
|
633
|
+
/** @internal Exported for the keyed-create repair regression. */
|
|
634
|
+
export function freezeAgentChildAutomaticTitleInCreatorContext(
|
|
635
|
+
context: TurnInitiatorContext | undefined,
|
|
636
|
+
title: string | null | undefined,
|
|
637
|
+
): TurnInitiatorContext | undefined {
|
|
638
|
+
return title ? { ...(context ?? {}), [AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY]: title } : context;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** @internal Keyed repair must use the committed winner, not the retry payload. */
|
|
642
|
+
export function initialAutomaticTitleForSessionStart(
|
|
643
|
+
session: Pick<Session, "createdByContext">,
|
|
644
|
+
requestedTitle: string | null | undefined,
|
|
645
|
+
): string | null {
|
|
646
|
+
const frozenTitle = session.createdByContext[AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY];
|
|
647
|
+
return typeof frozenTitle === "string" ? frozenTitle : (requestedTitle ?? null);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function automaticTitleForAgentChildCreate(
|
|
651
|
+
presentation: AgentChildSessionCreatePresentation,
|
|
652
|
+
goal: GoalSpec | null | undefined,
|
|
653
|
+
initialMessage: string | null | undefined,
|
|
654
|
+
): string | null {
|
|
655
|
+
for (const candidate of [presentation.automaticTitleCandidate, goal?.text, initialMessage]) {
|
|
656
|
+
if (typeof candidate !== "string") continue;
|
|
657
|
+
const normalized = normalizeAutomaticSessionTitle(candidate);
|
|
658
|
+
if (normalized && normalized !== AUTOMATIC_SESSION_TITLE_FALLBACK) return normalized;
|
|
659
|
+
}
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
|
|
610
663
|
export async function createAndStartSessionWithOutcome(input: {
|
|
611
664
|
requestedSessionId?: string;
|
|
612
665
|
db: Database;
|
|
@@ -615,6 +668,16 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
615
668
|
/** Internal database-only composition seam. The exact session shell and this
|
|
616
669
|
* linkage commit together before its first event/turn can be initialized. */
|
|
617
670
|
beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
|
|
671
|
+
/** The custom workspace model was frozen by an earlier accepted boundary or
|
|
672
|
+
* inherited from an existing session, so retirement must not invalidate it. */
|
|
673
|
+
retainWorkspaceGatewayModel?: boolean;
|
|
674
|
+
/** Provider-neutral successor to retainWorkspaceGatewayModel. */
|
|
675
|
+
retainWorkspaceCustomModel?: boolean;
|
|
676
|
+
/** The selected workspace Gateway product is backed by a mutable custom row,
|
|
677
|
+
* rather than deployment-curated Gateway membership. */
|
|
678
|
+
workspaceGatewayCustomModel?: boolean;
|
|
679
|
+
/** Provider-neutral successor to workspaceGatewayCustomModel. */
|
|
680
|
+
workspaceCustomModel?: boolean;
|
|
618
681
|
accountId: string;
|
|
619
682
|
workspaceId: string;
|
|
620
683
|
visibility?: "user_private" | "workspace_shared";
|
|
@@ -652,6 +715,9 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
652
715
|
// resolved workspace-scoped by the caller). Null/omitted ⇒ unfiled (inbox).
|
|
653
716
|
channelId?: string | null;
|
|
654
717
|
goal?: GoalSpec | null;
|
|
718
|
+
/** Trusted sensitive-safe automatic title for an agent-created child. The
|
|
719
|
+
* atomic initializer commits the row mutation and `session.title_set`. */
|
|
720
|
+
initialAutomaticTitle?: string | null;
|
|
655
721
|
// Per-session agent persona/system instructions (org-visible metadata, not a
|
|
656
722
|
// secret). Persisted on the session row and composed system-level AFTER the
|
|
657
723
|
// workspace agentInstructions at turn time; never emitted as a timeline event.
|
|
@@ -731,12 +797,56 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
731
797
|
allowNestedAgentDepthIncrease?: boolean;
|
|
732
798
|
subjectId?: string | null;
|
|
733
799
|
}): Promise<CreateSessionOutcome> {
|
|
734
|
-
const sessionMetadata =
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
800
|
+
const sessionMetadata = metadataWithTurnExecutionPolicyV1(
|
|
801
|
+
{
|
|
802
|
+
...input.metadata,
|
|
803
|
+
model: input.model,
|
|
804
|
+
reasoningEffort: input.reasoningEffort,
|
|
805
|
+
...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
|
|
806
|
+
},
|
|
807
|
+
input.turnExecutionPolicy,
|
|
808
|
+
);
|
|
809
|
+
const frozenCreatedByContext = freezeAgentChildAutomaticTitleInCreatorContext(
|
|
810
|
+
input.createdByContext,
|
|
811
|
+
input.initialAutomaticTitle,
|
|
812
|
+
);
|
|
813
|
+
const requiresActiveWorkspaceCustomModel =
|
|
814
|
+
(input.workspaceCustomModel === true || input.workspaceGatewayCustomModel === true) &&
|
|
815
|
+
input.retainWorkspaceCustomModel !== true &&
|
|
816
|
+
input.retainWorkspaceGatewayModel !== true;
|
|
817
|
+
const beforeCreateCommit =
|
|
818
|
+
requiresActiveWorkspaceCustomModel || input.beforeCreateCommit
|
|
819
|
+
? async (tx: Database, sessionId: string, context?: { created: boolean }): Promise<void> => {
|
|
820
|
+
// A committed keyed replay already crossed this fence when its shell
|
|
821
|
+
// was first accepted. Revalidate only the transaction inserting a new
|
|
822
|
+
// session, while still running caller linkage on every replay.
|
|
823
|
+
if (requiresActiveWorkspaceCustomModel && context?.created !== false) {
|
|
824
|
+
const openRouter = input.model.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX);
|
|
825
|
+
const upstreamModelId = input.model.slice(
|
|
826
|
+
openRouter
|
|
827
|
+
? WORKSPACE_OPENROUTER_MODEL_ID_PREFIX.length
|
|
828
|
+
: WORKSPACE_GATEWAY_MODEL_ID_PREFIX.length,
|
|
829
|
+
);
|
|
830
|
+
const active = openRouter
|
|
831
|
+
? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
|
|
832
|
+
accountId: input.accountId,
|
|
833
|
+
workspaceId: input.workspaceId,
|
|
834
|
+
upstreamModelId,
|
|
835
|
+
})
|
|
836
|
+
: await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
|
|
837
|
+
accountId: input.accountId,
|
|
838
|
+
workspaceId: input.workspaceId,
|
|
839
|
+
upstreamModelId,
|
|
840
|
+
});
|
|
841
|
+
if (!active) {
|
|
842
|
+
throw new HTTPException(422, {
|
|
843
|
+
message: `model is not available: ${input.model}`,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
await input.beforeCreateCommit?.(tx, sessionId);
|
|
848
|
+
}
|
|
849
|
+
: undefined;
|
|
740
850
|
// Keyed creation is intentionally handled only by the database admission
|
|
741
851
|
// transaction below. Its workspace/key lock replays either the successful
|
|
742
852
|
// session or the committed denial atomically; an application-side lookup
|
|
@@ -755,7 +865,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
755
865
|
toolPolicy: input.toolPolicy,
|
|
756
866
|
metadata: sessionMetadata,
|
|
757
867
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
758
|
-
...(
|
|
868
|
+
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
759
869
|
createdByActor: input.createdByActor ?? null,
|
|
760
870
|
model: input.model,
|
|
761
871
|
reasoningEffort: input.reasoningEffort,
|
|
@@ -786,15 +896,29 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
786
896
|
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
787
897
|
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
788
898
|
subjectId: input.subjectId ?? null,
|
|
789
|
-
...(
|
|
899
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
790
900
|
});
|
|
791
901
|
if (keyedResult.denied) {
|
|
792
902
|
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
|
|
793
903
|
}
|
|
794
904
|
const { session: keyed, created } = keyedResult;
|
|
795
905
|
if (!created) {
|
|
906
|
+
const persistedPolicy = readTurnExecutionPolicyV1(keyed.metadata);
|
|
796
907
|
const finished = await finishStartSession(
|
|
797
|
-
keyed.temporalWorkflowId
|
|
908
|
+
keyed.temporalWorkflowId
|
|
909
|
+
? {
|
|
910
|
+
...input,
|
|
911
|
+
seedTargetSandbox: null,
|
|
912
|
+
...(persistedPolicy.kind === "valid"
|
|
913
|
+
? { turnExecutionPolicy: persistedPolicy.policy }
|
|
914
|
+
: {}),
|
|
915
|
+
}
|
|
916
|
+
: {
|
|
917
|
+
...input,
|
|
918
|
+
...(persistedPolicy.kind === "valid"
|
|
919
|
+
? { turnExecutionPolicy: persistedPolicy.policy }
|
|
920
|
+
: {}),
|
|
921
|
+
},
|
|
798
922
|
keyed,
|
|
799
923
|
);
|
|
800
924
|
return {
|
|
@@ -827,7 +951,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
827
951
|
toolPolicy: input.toolPolicy,
|
|
828
952
|
metadata: sessionMetadata,
|
|
829
953
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
830
|
-
...(
|
|
954
|
+
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
831
955
|
createdByActor: input.createdByActor ?? null,
|
|
832
956
|
model: input.model,
|
|
833
957
|
reasoningEffort: input.reasoningEffort,
|
|
@@ -857,7 +981,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
857
981
|
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
858
982
|
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
859
983
|
subjectId: input.subjectId ?? null,
|
|
860
|
-
...(
|
|
984
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
861
985
|
});
|
|
862
986
|
} catch (error) {
|
|
863
987
|
if (error instanceof SessionSpawnDeniedDbError) {
|
|
@@ -905,6 +1029,7 @@ async function finishStartSession(
|
|
|
905
1029
|
sandboxBackend: Settings["sandboxBackend"];
|
|
906
1030
|
variableSets?: Array<{ id: string; name: string; scope: VariableSet["scope"] }>;
|
|
907
1031
|
goal?: GoalSpec | null;
|
|
1032
|
+
initialAutomaticTitle?: string | null;
|
|
908
1033
|
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
909
1034
|
seedTargetSandbox?: {
|
|
910
1035
|
sandboxId: string;
|
|
@@ -1009,6 +1134,10 @@ async function finishStartSession(
|
|
|
1009
1134
|
: {}),
|
|
1010
1135
|
}
|
|
1011
1136
|
: null,
|
|
1137
|
+
initialAutomaticTitle: initialAutomaticTitleForSessionStart(
|
|
1138
|
+
session,
|
|
1139
|
+
input.initialAutomaticTitle,
|
|
1140
|
+
),
|
|
1012
1141
|
consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
|
|
1013
1142
|
rememberNewSessionSelection: input.rememberNewSessionSelection ?? null,
|
|
1014
1143
|
deferInitialTurn: input.deferInitialTurn === true,
|
|
@@ -1084,9 +1213,6 @@ export function canonicalConfiguredModel(
|
|
|
1084
1213
|
) {
|
|
1085
1214
|
return canonicalModel;
|
|
1086
1215
|
}
|
|
1087
|
-
if (canonicalModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
|
|
1088
|
-
return canonicalModel;
|
|
1089
|
-
}
|
|
1090
1216
|
throw new HTTPException(422, { message: `model is not available: ${model}` });
|
|
1091
1217
|
}
|
|
1092
1218
|
|
|
@@ -1192,7 +1318,17 @@ export async function requireQueuedTurnForApi(
|
|
|
1192
1318
|
* `session_send_message` tool so the two surfaces cannot drift. Callers own
|
|
1193
1319
|
* resource/tool validation and the per-message usage limit before calling.
|
|
1194
1320
|
*/
|
|
1195
|
-
|
|
1321
|
+
type PostUserMessageTurnResult = {
|
|
1322
|
+
accepted: SessionEvent;
|
|
1323
|
+
turn: SessionTurn;
|
|
1324
|
+
draft: ComposerDraft | null;
|
|
1325
|
+
receipt: SessionCommandReceipt;
|
|
1326
|
+
routing: SessionPromptRouting;
|
|
1327
|
+
interruptionCount: number;
|
|
1328
|
+
replay: boolean;
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
type PostUserMessageTurnInput = {
|
|
1196
1332
|
db: Database;
|
|
1197
1333
|
bus: EventBus;
|
|
1198
1334
|
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
@@ -1220,28 +1356,143 @@ export async function postUserMessageTurn(input: {
|
|
|
1220
1356
|
commandActor?: SessionCommandActor;
|
|
1221
1357
|
controlEtag?: string | null;
|
|
1222
1358
|
expectedDraftRevision?: number | null;
|
|
1359
|
+
boundaryRequestHash?: string;
|
|
1223
1360
|
reasoningEffortFallback?: Settings["openaiReasoningEffort"];
|
|
1224
1361
|
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
1225
1362
|
recordAgentRunUsage?: boolean;
|
|
1226
1363
|
schedulePostCommit?: (task: () => Promise<void>) => void;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
function finalizePostUserMessageTurn(
|
|
1367
|
+
input: Pick<
|
|
1368
|
+
PostUserMessageTurnInput,
|
|
1369
|
+
| "db"
|
|
1370
|
+
| "bus"
|
|
1371
|
+
| "workflowClient"
|
|
1372
|
+
| "accountId"
|
|
1373
|
+
| "workspaceId"
|
|
1374
|
+
| "sessionId"
|
|
1375
|
+
| "delivery"
|
|
1376
|
+
| "schedulePostCommit"
|
|
1377
|
+
>,
|
|
1378
|
+
result: Awaited<ReturnType<typeof submitHumanPromptInTransaction>>,
|
|
1379
|
+
): PostUserMessageTurnResult {
|
|
1380
|
+
const { db, bus, workflowClient, accountId, workspaceId, sessionId } = input;
|
|
1381
|
+
const postCommitTask = async () => {
|
|
1382
|
+
await Promise.all([
|
|
1383
|
+
(async () => {
|
|
1384
|
+
try {
|
|
1385
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
|
|
1386
|
+
if (result.workspaceControlEventId) {
|
|
1387
|
+
const controlEvent = await getWorkspaceControlEvent(
|
|
1388
|
+
db,
|
|
1389
|
+
workspaceId,
|
|
1390
|
+
result.workspaceControlEventId,
|
|
1391
|
+
);
|
|
1392
|
+
if (!controlEvent) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
|
|
1398
|
+
}
|
|
1399
|
+
} catch {
|
|
1400
|
+
console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
|
|
1401
|
+
errorClass: "PromptEventFanoutOperationError",
|
|
1402
|
+
errorCode: "session_prompt_event_fanout_failed",
|
|
1403
|
+
origin: "core",
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
})(),
|
|
1407
|
+
(async () => {
|
|
1408
|
+
try {
|
|
1409
|
+
await workflowClient.wakeSessionWorkflow({
|
|
1410
|
+
accountId,
|
|
1411
|
+
workspaceId,
|
|
1412
|
+
sessionId,
|
|
1413
|
+
workflowId: result.turn.temporalWorkflowId,
|
|
1414
|
+
wakeRevision: result.wakeRevision,
|
|
1415
|
+
...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
|
|
1416
|
+
? { interruptionRequested: true }
|
|
1417
|
+
: {}),
|
|
1418
|
+
});
|
|
1419
|
+
} catch {
|
|
1420
|
+
console.warn("[sessions] workflow wake failed; durable outbox will retry", {
|
|
1421
|
+
errorClass: "WorkflowWakeOperationError",
|
|
1422
|
+
errorCode: "session_workflow_wake_failed",
|
|
1423
|
+
origin: "core",
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
})(),
|
|
1427
|
+
]);
|
|
1428
|
+
};
|
|
1429
|
+
const schedulePostCommit =
|
|
1430
|
+
input.schedulePostCommit ??
|
|
1431
|
+
((task: () => Promise<void>) => {
|
|
1432
|
+
void task();
|
|
1433
|
+
});
|
|
1434
|
+
try {
|
|
1435
|
+
schedulePostCommit(postCommitTask);
|
|
1436
|
+
} catch {
|
|
1437
|
+
console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
|
|
1438
|
+
errorClass: "PromptPostCommitScheduleError",
|
|
1439
|
+
errorCode: "session_prompt_post_commit_schedule_failed",
|
|
1440
|
+
origin: "core",
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
accepted: result.accepted,
|
|
1445
|
+
turn: result.turn,
|
|
1446
|
+
receipt: {
|
|
1447
|
+
id: result.receipt.id,
|
|
1448
|
+
action: result.receipt.action,
|
|
1449
|
+
operationKey: result.receipt.operationKey,
|
|
1450
|
+
targetSessionId: result.receipt.targetSessionId,
|
|
1451
|
+
targetTurnId: result.receipt.targetTurnId,
|
|
1452
|
+
appliedControlRevision: result.receipt.appliedControlRevision,
|
|
1453
|
+
appliedQueueVersion: result.receipt.appliedQueueVersion,
|
|
1454
|
+
appliedTurnVersion: result.receipt.appliedTurnVersion,
|
|
1455
|
+
appliedDraftRevision: result.receipt.appliedDraftRevision,
|
|
1456
|
+
createdAt: result.receipt.createdAt.toISOString(),
|
|
1457
|
+
},
|
|
1458
|
+
routing: result.routing,
|
|
1459
|
+
draft: result.draft
|
|
1460
|
+
? {
|
|
1461
|
+
revision: result.draft.revision,
|
|
1462
|
+
text: result.draft.text,
|
|
1463
|
+
annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
|
|
1464
|
+
resources: result.draft.resources as ResourceRef[],
|
|
1465
|
+
model: result.draft.model,
|
|
1466
|
+
reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
|
|
1467
|
+
latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
|
|
1468
|
+
sourceTurnId: result.draft.sourceTurnId,
|
|
1469
|
+
sourceTurnVersion: result.draft.sourceTurnVersion,
|
|
1470
|
+
updatedAt: result.draft.updatedAt.toISOString(),
|
|
1471
|
+
}
|
|
1472
|
+
: null,
|
|
1473
|
+
interruptionCount: result.interruptionCount,
|
|
1474
|
+
replay: result.replay,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
export async function postUserMessageTurn(
|
|
1479
|
+
input: PostUserMessageTurnInput,
|
|
1480
|
+
): Promise<PostUserMessageTurnResult> {
|
|
1481
|
+
const { db, settings, accountId, workspaceId, sessionId } = input;
|
|
1237
1482
|
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
1238
1483
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
1239
1484
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
1240
1485
|
// model inherits the session's model downstream (always a configured id).
|
|
1241
1486
|
assertConfiguredModel(settings, requestedModel);
|
|
1242
|
-
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
1243
1487
|
const sessionForModelGate = await requireSession(db, workspaceId, sessionId);
|
|
1244
1488
|
const effectiveModelForGate = requestedModel ?? sessionForModelGate.model;
|
|
1489
|
+
const freshWorkspaceCustomModel =
|
|
1490
|
+
requestedModel !== null &&
|
|
1491
|
+
isWorkspaceCustomModelId(settings, requestedModel) &&
|
|
1492
|
+
requestedModel !== sessionForModelGate.model
|
|
1493
|
+
? requestedModel
|
|
1494
|
+
: null;
|
|
1495
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, effectiveModelForGate);
|
|
1245
1496
|
try {
|
|
1246
1497
|
assertSessionAllowsProductModel(sessionForModelGate, effectiveModelForGate);
|
|
1247
1498
|
} catch (error) {
|
|
@@ -1276,6 +1527,9 @@ export async function postUserMessageTurn(input: {
|
|
|
1276
1527
|
subjectId: input.actor ?? accountId,
|
|
1277
1528
|
},
|
|
1278
1529
|
operationKey,
|
|
1530
|
+
...(input.boundaryRequestHash
|
|
1531
|
+
? { boundaryRequestHash: input.boundaryRequestHash }
|
|
1532
|
+
: {}),
|
|
1279
1533
|
delivery: input.delivery ?? "send",
|
|
1280
1534
|
controlEtag: input.controlEtag ?? null,
|
|
1281
1535
|
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
@@ -1303,6 +1557,36 @@ export async function postUserMessageTurn(input: {
|
|
|
1303
1557
|
}
|
|
1304
1558
|
: {}),
|
|
1305
1559
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
1560
|
+
...(freshWorkspaceCustomModel
|
|
1561
|
+
? {
|
|
1562
|
+
beforeFreshPromptCommit: async (tx: Database): Promise<void> => {
|
|
1563
|
+
const reference = workspaceCustomModelReference(
|
|
1564
|
+
settings,
|
|
1565
|
+
freshWorkspaceCustomModel,
|
|
1566
|
+
);
|
|
1567
|
+
if (!reference) {
|
|
1568
|
+
throw new Error("workspace custom model reference disappeared");
|
|
1569
|
+
}
|
|
1570
|
+
const active =
|
|
1571
|
+
reference.providerKind === "openrouter"
|
|
1572
|
+
? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
|
|
1573
|
+
accountId,
|
|
1574
|
+
workspaceId,
|
|
1575
|
+
upstreamModelId: reference.upstreamModelId,
|
|
1576
|
+
})
|
|
1577
|
+
: await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
|
|
1578
|
+
accountId,
|
|
1579
|
+
workspaceId,
|
|
1580
|
+
upstreamModelId: reference.upstreamModelId,
|
|
1581
|
+
});
|
|
1582
|
+
if (!active) {
|
|
1583
|
+
throw new HTTPException(422, {
|
|
1584
|
+
message: `model is not available: ${freshWorkspaceCustomModel}`,
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
}
|
|
1589
|
+
: {}),
|
|
1306
1590
|
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
1307
1591
|
}),
|
|
1308
1592
|
),
|
|
@@ -1333,101 +1617,7 @@ export async function postUserMessageTurn(input: {
|
|
|
1333
1617
|
}
|
|
1334
1618
|
throw error;
|
|
1335
1619
|
}
|
|
1336
|
-
|
|
1337
|
-
await Promise.all([
|
|
1338
|
-
(async () => {
|
|
1339
|
-
try {
|
|
1340
|
-
await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
|
|
1341
|
-
if (result.workspaceControlEventId) {
|
|
1342
|
-
const controlEvent = await getWorkspaceControlEvent(
|
|
1343
|
-
db,
|
|
1344
|
-
workspaceId,
|
|
1345
|
-
result.workspaceControlEventId,
|
|
1346
|
-
);
|
|
1347
|
-
if (!controlEvent) {
|
|
1348
|
-
throw new Error(
|
|
1349
|
-
`Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
|
|
1353
|
-
}
|
|
1354
|
-
} catch {
|
|
1355
|
-
console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
|
|
1356
|
-
errorClass: "PromptEventFanoutOperationError",
|
|
1357
|
-
errorCode: "session_prompt_event_fanout_failed",
|
|
1358
|
-
origin: "core",
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1361
|
-
})(),
|
|
1362
|
-
(async () => {
|
|
1363
|
-
try {
|
|
1364
|
-
await workflowClient.wakeSessionWorkflow({
|
|
1365
|
-
accountId,
|
|
1366
|
-
workspaceId,
|
|
1367
|
-
sessionId,
|
|
1368
|
-
workflowId: result.turn.temporalWorkflowId,
|
|
1369
|
-
wakeRevision: result.wakeRevision,
|
|
1370
|
-
...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
|
|
1371
|
-
? { interruptionRequested: true }
|
|
1372
|
-
: {}),
|
|
1373
|
-
});
|
|
1374
|
-
} catch {
|
|
1375
|
-
console.warn("[sessions] workflow wake failed; durable outbox will retry", {
|
|
1376
|
-
errorClass: "WorkflowWakeOperationError",
|
|
1377
|
-
errorCode: "session_workflow_wake_failed",
|
|
1378
|
-
origin: "core",
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
})(),
|
|
1382
|
-
]);
|
|
1383
|
-
};
|
|
1384
|
-
const schedulePostCommit =
|
|
1385
|
-
input.schedulePostCommit ??
|
|
1386
|
-
((task: () => Promise<void>) => {
|
|
1387
|
-
void task();
|
|
1388
|
-
});
|
|
1389
|
-
try {
|
|
1390
|
-
schedulePostCommit(postCommitTask);
|
|
1391
|
-
} catch {
|
|
1392
|
-
console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
|
|
1393
|
-
errorClass: "PromptPostCommitScheduleError",
|
|
1394
|
-
errorCode: "session_prompt_post_commit_schedule_failed",
|
|
1395
|
-
origin: "core",
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
return {
|
|
1399
|
-
accepted: result.accepted,
|
|
1400
|
-
turn: result.turn,
|
|
1401
|
-
receipt: {
|
|
1402
|
-
id: result.receipt.id,
|
|
1403
|
-
action: result.receipt.action,
|
|
1404
|
-
operationKey: result.receipt.operationKey,
|
|
1405
|
-
targetSessionId: result.receipt.targetSessionId,
|
|
1406
|
-
targetTurnId: result.receipt.targetTurnId,
|
|
1407
|
-
appliedControlRevision: result.receipt.appliedControlRevision,
|
|
1408
|
-
appliedQueueVersion: result.receipt.appliedQueueVersion,
|
|
1409
|
-
appliedTurnVersion: result.receipt.appliedTurnVersion,
|
|
1410
|
-
appliedDraftRevision: result.receipt.appliedDraftRevision,
|
|
1411
|
-
createdAt: result.receipt.createdAt.toISOString(),
|
|
1412
|
-
},
|
|
1413
|
-
routing: result.routing,
|
|
1414
|
-
draft: result.draft
|
|
1415
|
-
? {
|
|
1416
|
-
revision: result.draft.revision,
|
|
1417
|
-
text: result.draft.text,
|
|
1418
|
-
annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
|
|
1419
|
-
resources: result.draft.resources as ResourceRef[],
|
|
1420
|
-
model: result.draft.model,
|
|
1421
|
-
reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
|
|
1422
|
-
latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
|
|
1423
|
-
sourceTurnId: result.draft.sourceTurnId,
|
|
1424
|
-
sourceTurnVersion: result.draft.sourceTurnVersion,
|
|
1425
|
-
updatedAt: result.draft.updatedAt.toISOString(),
|
|
1426
|
-
}
|
|
1427
|
-
: null,
|
|
1428
|
-
interruptionCount: result.interruptionCount,
|
|
1429
|
-
replay: result.replay,
|
|
1430
|
-
};
|
|
1620
|
+
return finalizePostUserMessageTurn(input, result);
|
|
1431
1621
|
}
|
|
1432
1622
|
|
|
1433
1623
|
/**
|
|
@@ -1479,15 +1669,92 @@ export function resolveSessionCreateVisibility(input: {
|
|
|
1479
1669
|
return input.requestedVisibility === "private" ? "user_private" : "workspace_shared";
|
|
1480
1670
|
}
|
|
1481
1671
|
|
|
1482
|
-
|
|
1483
|
-
deps: ApiRouteDeps,
|
|
1672
|
+
async function resolveWorkspaceModelBoundarySettings(
|
|
1673
|
+
deps: Pick<ApiRouteDeps, "db" | "settings" | "catalogSourceSettings">,
|
|
1484
1674
|
grant: AccessGrant,
|
|
1485
1675
|
workspaceId: string,
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
): Promise<
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1676
|
+
modelIds: readonly (string | null | undefined)[],
|
|
1677
|
+
retainedProductModelId?: string | null,
|
|
1678
|
+
): Promise<Settings> {
|
|
1679
|
+
const retainedWorkspaceModel =
|
|
1680
|
+
retainedProductModelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
|
|
1681
|
+
retainedProductModelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) === true;
|
|
1682
|
+
if (deps.catalogSourceSettings) {
|
|
1683
|
+
// The adapter already resolved one exact workspace catalog snapshot for
|
|
1684
|
+
// this request. Preserve it for fresh selections, but an existing session
|
|
1685
|
+
// may name a retired custom model that the active-only adapter snapshot
|
|
1686
|
+
// intentionally omitted. Re-open only the unoverlaid source for that
|
|
1687
|
+
// retention lookup; never feed the synthetic workspace provider back
|
|
1688
|
+
// through deployment validation.
|
|
1689
|
+
if (!retainedWorkspaceModel) return deps.settings;
|
|
1690
|
+
}
|
|
1691
|
+
const workspaceModelIds = modelIds.filter(
|
|
1692
|
+
(modelId): modelId is string =>
|
|
1693
|
+
typeof modelId === "string" &&
|
|
1694
|
+
(modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) ||
|
|
1695
|
+
modelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)),
|
|
1696
|
+
);
|
|
1697
|
+
const needsWorkspaceResolution =
|
|
1698
|
+
deps.settings.modelCatalogSource === "database" || workspaceModelIds.length > 0;
|
|
1699
|
+
if (!needsWorkspaceResolution) return deps.settings;
|
|
1700
|
+
return (
|
|
1701
|
+
await resolveWorkspaceCatalogSettings(deps.db, deps.catalogSourceSettings ?? deps.settings, {
|
|
1702
|
+
accountId: grant.accountId,
|
|
1703
|
+
workspaceId,
|
|
1704
|
+
...(retainedProductModelId !== undefined ? { retainedProductModelId } : {}),
|
|
1705
|
+
})
|
|
1706
|
+
).settings;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async function withSessionCreateUsageRecording(input: {
|
|
1710
|
+
deps: ApiRouteDeps;
|
|
1711
|
+
grant: AccessGrant;
|
|
1712
|
+
workspaceId: string;
|
|
1713
|
+
startMode: "realtime" | undefined;
|
|
1714
|
+
origin: "system" | "user";
|
|
1715
|
+
createOutcome: CreateSessionOutcome;
|
|
1716
|
+
}): Promise<CreateSessionRequestOutcome> {
|
|
1717
|
+
let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
|
|
1718
|
+
if (input.startMode !== "realtime") {
|
|
1719
|
+
try {
|
|
1720
|
+
await recordWorkspaceUsage(input.deps, {
|
|
1721
|
+
accountId: input.grant.accountId,
|
|
1722
|
+
workspaceId: input.workspaceId,
|
|
1723
|
+
subjectId: input.grant.subjectId,
|
|
1724
|
+
eventType: "agent_run.created",
|
|
1725
|
+
quantity: 1,
|
|
1726
|
+
unit: "run",
|
|
1727
|
+
sourceResourceType: "session",
|
|
1728
|
+
sourceResourceId: input.createOutcome.session.id,
|
|
1729
|
+
sessionId: input.createOutcome.session.id,
|
|
1730
|
+
initiator: input.createOutcome.session.createdBy,
|
|
1731
|
+
initiatorContext: input.createOutcome.session.createdByContext,
|
|
1732
|
+
origin: input.origin,
|
|
1733
|
+
idempotencyKey: `agent_run.created:${input.workspaceId}:${input.createOutcome.session.id}`,
|
|
1734
|
+
});
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
usageRecording = "failed";
|
|
1737
|
+
reportSessionUsageRecordingFailure(error);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return { ...input.createOutcome, usageRecording };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
export async function createSessionForRequestWithOutcome(
|
|
1744
|
+
unresolvedDeps: ApiRouteDeps,
|
|
1745
|
+
grant: AccessGrant,
|
|
1746
|
+
workspaceId: string,
|
|
1747
|
+
rawPayload: unknown,
|
|
1748
|
+
authorization?: AccessGrantAuthorization,
|
|
1749
|
+
agentChildPresentation?: AgentChildSessionCreatePresentation,
|
|
1750
|
+
): Promise<CreateSessionRequestOutcome> {
|
|
1751
|
+
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1752
|
+
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
1753
|
+
throw new HTTPException(422, {
|
|
1754
|
+
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
const db = unresolvedDeps.db;
|
|
1491
1758
|
const visibilityProvided = hasOwnProperty(rawPayload, "visibility");
|
|
1492
1759
|
if (payload.visibility === "private" && !grant.metadata?.["sessionId"]) {
|
|
1493
1760
|
if (!authorization) {
|
|
@@ -1495,39 +1762,13 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1495
1762
|
message: "managed human session required",
|
|
1496
1763
|
});
|
|
1497
1764
|
}
|
|
1498
|
-
await requireManagedHumanPrivateSessionCreate(
|
|
1765
|
+
await requireManagedHumanPrivateSessionCreate(unresolvedDeps, authorization, workspaceId);
|
|
1499
1766
|
if (payload.sandbox === "shared" || typeof payload.sandbox === "object") {
|
|
1500
1767
|
throw new HTTPException(422, {
|
|
1501
1768
|
message: "Only-me sessions require their own sandbox",
|
|
1502
1769
|
});
|
|
1503
1770
|
}
|
|
1504
1771
|
}
|
|
1505
|
-
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
1506
|
-
throw new HTTPException(422, {
|
|
1507
|
-
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
1508
|
-
});
|
|
1509
|
-
}
|
|
1510
|
-
// A committed keyed denial is the idempotent outcome even if mutable
|
|
1511
|
-
// resources, policy, authorization, or budget have changed since the first
|
|
1512
|
-
// attempt. Replay it before any of those checks, just as a keyed successful
|
|
1513
|
-
// session is returned rather than recreated later in createAndStartSession.
|
|
1514
|
-
if (payload.idempotencyKey) {
|
|
1515
|
-
const denial = await getSessionSpawnDenialByIdempotencyKey(
|
|
1516
|
-
db,
|
|
1517
|
-
workspaceId,
|
|
1518
|
-
payload.idempotencyKey,
|
|
1519
|
-
);
|
|
1520
|
-
if (denial) {
|
|
1521
|
-
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
await requireAtomicPersonalResourceAttachment(
|
|
1525
|
-
deps,
|
|
1526
|
-
authorization,
|
|
1527
|
-
workspaceId,
|
|
1528
|
-
payload.personalResourceAttachment,
|
|
1529
|
-
false,
|
|
1530
|
-
);
|
|
1531
1772
|
// Parent linkage and execution-context inheritance come ONLY from the
|
|
1532
1773
|
// worker-signed sessionId claim. A caller cannot nominate a parent in the
|
|
1533
1774
|
// payload, so inheriting an existing repository/tool/credential snapshot does
|
|
@@ -1538,7 +1779,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1538
1779
|
: null;
|
|
1539
1780
|
if (parentSessionId) {
|
|
1540
1781
|
try {
|
|
1541
|
-
await requireSessionAuthorization(
|
|
1782
|
+
await requireSessionAuthorization(unresolvedDeps, grant, {
|
|
1542
1783
|
sessionId: parentSessionId,
|
|
1543
1784
|
operation: "session.child.create",
|
|
1544
1785
|
surface: "core",
|
|
@@ -1596,6 +1837,117 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1596
1837
|
message: "caller attempt does not belong to the parent session",
|
|
1597
1838
|
});
|
|
1598
1839
|
}
|
|
1840
|
+
const replayManagedHumanSubjectId = creationInitiator.actor
|
|
1841
|
+
? (parentCallingTurn?.initiatingHumanSubjectId ??
|
|
1842
|
+
(parentCallingTurn?.initiator.kind === "subject"
|
|
1843
|
+
? parentCallingTurn.initiator.subjectId
|
|
1844
|
+
: null))
|
|
1845
|
+
: authorization?.canonicalManagedHumanSession || grant.principalKind === "human_session"
|
|
1846
|
+
? grant.subjectId
|
|
1847
|
+
: null;
|
|
1848
|
+
let retainedKeyedShellModel: string | null = null;
|
|
1849
|
+
if (
|
|
1850
|
+
payload.idempotencyKey &&
|
|
1851
|
+
(effectiveVisibility !== "user_private" || replayManagedHumanSubjectId !== null)
|
|
1852
|
+
) {
|
|
1853
|
+
try {
|
|
1854
|
+
const initializedReplay = await getInitializedSessionCreateReplay(db, {
|
|
1855
|
+
accountId: grant.accountId,
|
|
1856
|
+
workspaceId,
|
|
1857
|
+
subjectId: replayManagedHumanSubjectId ?? grant.subjectId,
|
|
1858
|
+
...(replayManagedHumanSubjectId
|
|
1859
|
+
? { activeManagedHumanSubjectId: replayManagedHumanSubjectId }
|
|
1860
|
+
: {}),
|
|
1861
|
+
createIdempotencyKey: payload.idempotencyKey,
|
|
1862
|
+
...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
|
|
1863
|
+
visibility: effectiveVisibility,
|
|
1864
|
+
variableSetIds: payload.variableSetIds ?? [],
|
|
1865
|
+
initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
|
|
1866
|
+
deferInitialTurn: payload.startMode === "realtime",
|
|
1867
|
+
});
|
|
1868
|
+
if (initializedReplay) {
|
|
1869
|
+
if (initializedReplay.outcome === "denied") {
|
|
1870
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(initializedReplay.denial));
|
|
1871
|
+
}
|
|
1872
|
+
if (initializedReplay.outcome === "pending") {
|
|
1873
|
+
retainedKeyedShellModel = initializedReplay.session.model;
|
|
1874
|
+
} else {
|
|
1875
|
+
if (initializedReplay.workflowWakeRevision !== null) {
|
|
1876
|
+
await unresolvedDeps.workflowClient.wakeSessionWorkflow({
|
|
1877
|
+
accountId: grant.accountId,
|
|
1878
|
+
workspaceId,
|
|
1879
|
+
sessionId: initializedReplay.session.id,
|
|
1880
|
+
workflowId: initializedReplay.temporalWorkflowId,
|
|
1881
|
+
wakeRevision: initializedReplay.workflowWakeRevision,
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
return await withSessionCreateUsageRecording({
|
|
1885
|
+
deps: unresolvedDeps,
|
|
1886
|
+
grant,
|
|
1887
|
+
workspaceId,
|
|
1888
|
+
startMode: payload.startMode,
|
|
1889
|
+
origin: creationInitiator.actor ? "system" : "user",
|
|
1890
|
+
createOutcome: {
|
|
1891
|
+
session: initializedReplay.session,
|
|
1892
|
+
outcome: initializedReplay.changed ? "repaired" : "replayed",
|
|
1893
|
+
replay: !initializedReplay.changed,
|
|
1894
|
+
changed: initializedReplay.changed,
|
|
1895
|
+
},
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
if (error instanceof SessionIdConflictError) {
|
|
1901
|
+
throw new HTTPException(409, {
|
|
1902
|
+
message: "requested session id is already in use",
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
if (error instanceof SessionCreateIdempotencyConflictError) {
|
|
1906
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
1907
|
+
}
|
|
1908
|
+
throw error;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
let settings = await resolveWorkspaceModelBoundarySettings(
|
|
1912
|
+
unresolvedDeps,
|
|
1913
|
+
grant,
|
|
1914
|
+
workspaceId,
|
|
1915
|
+
[payload.model],
|
|
1916
|
+
retainedKeyedShellModel,
|
|
1917
|
+
);
|
|
1918
|
+
let deps =
|
|
1919
|
+
settings === unresolvedDeps.settings
|
|
1920
|
+
? unresolvedDeps
|
|
1921
|
+
: {
|
|
1922
|
+
...unresolvedDeps,
|
|
1923
|
+
catalogSourceSettings: unresolvedDeps.catalogSourceSettings ?? unresolvedDeps.settings,
|
|
1924
|
+
settings,
|
|
1925
|
+
};
|
|
1926
|
+
const { bus, workflowClient, objectStorage } = deps;
|
|
1927
|
+
await requireAtomicPersonalResourceAttachment(
|
|
1928
|
+
deps,
|
|
1929
|
+
authorization,
|
|
1930
|
+
workspaceId,
|
|
1931
|
+
payload.personalResourceAttachment,
|
|
1932
|
+
false,
|
|
1933
|
+
);
|
|
1934
|
+
const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
|
|
1935
|
+
const effectiveModelId = payload.model ?? inheritedModel;
|
|
1936
|
+
const effectiveCatalogSettings = await resolveWorkspaceModelBoundarySettings(
|
|
1937
|
+
deps,
|
|
1938
|
+
grant,
|
|
1939
|
+
workspaceId,
|
|
1940
|
+
[effectiveModelId],
|
|
1941
|
+
parentSession ? inheritedModel : null,
|
|
1942
|
+
);
|
|
1943
|
+
if (effectiveCatalogSettings !== settings) {
|
|
1944
|
+
deps = {
|
|
1945
|
+
...deps,
|
|
1946
|
+
catalogSourceSettings: deps.catalogSourceSettings ?? settings,
|
|
1947
|
+
settings: effectiveCatalogSettings,
|
|
1948
|
+
};
|
|
1949
|
+
settings = effectiveCatalogSettings;
|
|
1950
|
+
}
|
|
1599
1951
|
let effectiveGoal = payload.goal;
|
|
1600
1952
|
if (parentSession && payload.goal) {
|
|
1601
1953
|
try {
|
|
@@ -1612,6 +1964,14 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1612
1964
|
});
|
|
1613
1965
|
}
|
|
1614
1966
|
}
|
|
1967
|
+
const initialAutomaticTitle =
|
|
1968
|
+
parentSession && agentChildPresentation
|
|
1969
|
+
? automaticTitleForAgentChildCreate(
|
|
1970
|
+
agentChildPresentation,
|
|
1971
|
+
effectiveGoal,
|
|
1972
|
+
payload.initialMessage,
|
|
1973
|
+
)
|
|
1974
|
+
: null;
|
|
1615
1975
|
const personalResourceSubjectId = creationInitiator.actor
|
|
1616
1976
|
? (await requireLiveAgentAttemptAuthorization(db, grant, creationInitiator.actor.sessionId))
|
|
1617
1977
|
.initiatingHumanSubjectId
|
|
@@ -1798,8 +2158,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1798
2158
|
// managers: falling back to the deployment model would silently move a child
|
|
1799
2159
|
// onto the OpenGeni-credits billing path. Legacy session-bound grants without
|
|
1800
2160
|
// exact attempt claims fall back to the parent session's persisted defaults.
|
|
1801
|
-
const
|
|
1802
|
-
const model = canonicalConfiguredModel(settings, payload.model ?? inheritedModel);
|
|
2161
|
+
const model = canonicalConfiguredModel(settings, effectiveModelId);
|
|
1803
2162
|
if (model === null || model === undefined) {
|
|
1804
2163
|
throw new Error("effective session model unexpectedly resolved to null");
|
|
1805
2164
|
}
|
|
@@ -2038,6 +2397,8 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2038
2397
|
payload.sandbox ?? (payload.targetSandboxId ? "new" : parentSessionId ? "shared" : "new");
|
|
2039
2398
|
let sandboxGroupId: string | null = null;
|
|
2040
2399
|
let inheritedBackend: Session["sandboxBackend"] | undefined;
|
|
2400
|
+
let inheritedSandboxOs: Session["sandboxOs"] | undefined;
|
|
2401
|
+
let inheritedActiveTarget: { sandboxId: string; workingDir: string | null } | null = null;
|
|
2041
2402
|
// ENV-AWARE GROUPING: under the CURRENT mechanics the workspace VariableSet is
|
|
2042
2403
|
// creation-time box state — the box's manifest env is fixed when it is cold-
|
|
2043
2404
|
// created, and the SDK's provided-session guard rejects any manifest-env delta
|
|
@@ -2112,6 +2473,17 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2112
2473
|
} else {
|
|
2113
2474
|
sandboxGroupId = parent.sandboxGroupId;
|
|
2114
2475
|
inheritedBackend = parent.sandboxBackend;
|
|
2476
|
+
inheritedSandboxOs = parent.sandboxOs;
|
|
2477
|
+
// A Connected Machine route is session-local even when two sessions share
|
|
2478
|
+
// one logical sandbox group. Copy the trusted parent's exact active route
|
|
2479
|
+
// so an omitted child sandbox really does share the creator's current box
|
|
2480
|
+
// instead of creating a selfhosted row with no bound agent.
|
|
2481
|
+
inheritedActiveTarget = parent.activeSandboxId
|
|
2482
|
+
? {
|
|
2483
|
+
sandboxId: parent.activeSandboxId,
|
|
2484
|
+
workingDir: parent.workingDir,
|
|
2485
|
+
}
|
|
2486
|
+
: null;
|
|
2115
2487
|
}
|
|
2116
2488
|
} else if (typeof sandboxChoice === "object") {
|
|
2117
2489
|
const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
|
|
@@ -2160,6 +2532,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2160
2532
|
}
|
|
2161
2533
|
sandboxGroupId = sandboxChoice.groupId;
|
|
2162
2534
|
inheritedBackend = member.sandboxBackend;
|
|
2535
|
+
inheritedSandboxOs = member.sandboxOs;
|
|
2163
2536
|
}
|
|
2164
2537
|
// else "new": leave sandboxGroupId null → own singleton group (group ≡ id).
|
|
2165
2538
|
// A working dir is only meaningful for a TARGETED machine (it is the chosen
|
|
@@ -2172,6 +2545,19 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2172
2545
|
"workingDir requires targetSandboxId (it is the targeted machine's working directory)",
|
|
2173
2546
|
});
|
|
2174
2547
|
}
|
|
2548
|
+
// A registry-built selfhosted client is deliberately inert: it has no live
|
|
2549
|
+
// agent identity until a concrete Connected Machine is named. Reject an own
|
|
2550
|
+
// targetless home before persisting a session whose first turn can only fail.
|
|
2551
|
+
// Shared children retain their already-bound group through inheritedBackend.
|
|
2552
|
+
if (
|
|
2553
|
+
inheritedBackend === undefined &&
|
|
2554
|
+
!payload.targetSandboxId &&
|
|
2555
|
+
(payload.sandboxBackend ?? settings.sandboxBackend) === "selfhosted"
|
|
2556
|
+
) {
|
|
2557
|
+
throw new HTTPException(422, {
|
|
2558
|
+
message: "selfhosted sessions require targetSandboxId; select an online Connected Machine",
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2175
2561
|
// Honest-label (Stage-D closure): a session TARGETED at a Connected
|
|
2176
2562
|
// Machine (a selfhosted sandbox) runs machine-primary every turn, so its HOME
|
|
2177
2563
|
// sandbox_backend must read "selfhosted" — not the deployment cloud default —
|
|
@@ -2230,6 +2616,25 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2230
2616
|
}
|
|
2231
2617
|
}
|
|
2232
2618
|
}
|
|
2619
|
+
const effectiveSandboxBackend =
|
|
2620
|
+
inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend;
|
|
2621
|
+
const effectiveSandboxOs = inheritedSandboxOs ?? machineHomeOs;
|
|
2622
|
+
const effectiveSeedTarget = payload.targetSandboxId
|
|
2623
|
+
? {
|
|
2624
|
+
sandboxId: payload.targetSandboxId,
|
|
2625
|
+
workingDir: payload.workingDir ?? null,
|
|
2626
|
+
}
|
|
2627
|
+
: inheritedActiveTarget;
|
|
2628
|
+
if (
|
|
2629
|
+
effectiveSandboxBackend === "selfhosted" &&
|
|
2630
|
+
effectiveSeedTarget === null &&
|
|
2631
|
+
managedSessionGroupBackend(settings.sandboxBackend, effectiveSandboxBackend) === null
|
|
2632
|
+
) {
|
|
2633
|
+
throw new HTTPException(422, {
|
|
2634
|
+
message:
|
|
2635
|
+
"self-hosted execution runs on a Connected Machine, but no machine was selected or inherited; connect the parent session to a machine or provide machineTarget",
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2233
2638
|
if (payload.startMode !== "realtime") {
|
|
2234
2639
|
await requireLimit(deps, {
|
|
2235
2640
|
accountId: grant.accountId,
|
|
@@ -2266,11 +2671,10 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2266
2671
|
// machine-targeted create (top-level or own-box child) labels the home
|
|
2267
2672
|
// "selfhosted" (machineHomeBackend), overriding the caller/deployment
|
|
2268
2673
|
// default so the row matches where the session actually runs.
|
|
2269
|
-
sandboxBackend:
|
|
2270
|
-
inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
|
|
2674
|
+
sandboxBackend: effectiveSandboxBackend,
|
|
2271
2675
|
// Mirror the backend relabel on the OS axis: a machine-targeted own-box
|
|
2272
|
-
// create carries a derived OS; shared spawns
|
|
2273
|
-
...(
|
|
2676
|
+
// create carries a derived OS; shared spawns inherit the exact parent box.
|
|
2677
|
+
...(effectiveSandboxOs ? { sandboxOs: effectiveSandboxOs } : {}),
|
|
2274
2678
|
sandboxGroupId,
|
|
2275
2679
|
metadata: payload.metadata,
|
|
2276
2680
|
...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
|
|
@@ -2286,6 +2690,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2286
2690
|
rigVersionId: frozenRigVersionId,
|
|
2287
2691
|
channelId,
|
|
2288
2692
|
goal: effectiveGoal ?? null,
|
|
2693
|
+
initialAutomaticTitle,
|
|
2289
2694
|
// Per-session persona instructions (already trimmed/validated by the
|
|
2290
2695
|
// contracts schema). Persisted on the row; composed system-level at turn
|
|
2291
2696
|
// time. Not surfaced as an event.
|
|
@@ -2297,6 +2702,8 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2297
2702
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
2298
2703
|
personalConnectionDelegations,
|
|
2299
2704
|
initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
|
|
2705
|
+
workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
|
|
2706
|
+
retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
|
|
2300
2707
|
...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
|
|
2301
2708
|
parentSessionId,
|
|
2302
2709
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
@@ -2307,11 +2714,11 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2307
2714
|
// active-sandbox pointer is seeded race-free inside createAndStartSession
|
|
2308
2715
|
// (after the row exists, before the first turn dispatches). Validation
|
|
2309
2716
|
// (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
|
|
2310
|
-
seedTargetSandbox:
|
|
2717
|
+
seedTargetSandbox: effectiveSeedTarget
|
|
2311
2718
|
? {
|
|
2312
|
-
sandboxId:
|
|
2719
|
+
sandboxId: effectiveSeedTarget.sandboxId,
|
|
2313
2720
|
settings,
|
|
2314
|
-
workingDir:
|
|
2721
|
+
workingDir: effectiveSeedTarget.workingDir,
|
|
2315
2722
|
resourceSubjectId: personalResourceSubjectId,
|
|
2316
2723
|
}
|
|
2317
2724
|
: null,
|
|
@@ -2366,30 +2773,14 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2366
2773
|
}
|
|
2367
2774
|
throw error;
|
|
2368
2775
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
quantity: 1,
|
|
2378
|
-
unit: "run",
|
|
2379
|
-
sourceResourceType: "session",
|
|
2380
|
-
sourceResourceId: createOutcome.session.id,
|
|
2381
|
-
sessionId: createOutcome.session.id,
|
|
2382
|
-
initiator: createOutcome.session.createdBy,
|
|
2383
|
-
initiatorContext: createOutcome.session.createdByContext,
|
|
2384
|
-
origin: creationInitiator.actor ? "system" : "user",
|
|
2385
|
-
idempotencyKey: `agent_run.created:${workspaceId}:${createOutcome.session.id}`,
|
|
2386
|
-
});
|
|
2387
|
-
} catch (error) {
|
|
2388
|
-
usageRecording = "failed";
|
|
2389
|
-
reportSessionUsageRecordingFailure(error);
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
return { ...createOutcome, usageRecording };
|
|
2776
|
+
return await withSessionCreateUsageRecording({
|
|
2777
|
+
deps,
|
|
2778
|
+
grant,
|
|
2779
|
+
workspaceId,
|
|
2780
|
+
startMode: payload.startMode,
|
|
2781
|
+
origin: creationInitiator.actor ? "system" : "user",
|
|
2782
|
+
createOutcome,
|
|
2783
|
+
});
|
|
2393
2784
|
}
|
|
2394
2785
|
|
|
2395
2786
|
/** @internal Fixed public projection; the committed session outcome remains authoritative. */
|
|
@@ -2417,6 +2808,53 @@ export async function createSessionForRequest(
|
|
|
2417
2808
|
).session;
|
|
2418
2809
|
}
|
|
2419
2810
|
|
|
2811
|
+
function sessionPromptBoundaryRequestHash(input: {
|
|
2812
|
+
delivery: "send" | "steer";
|
|
2813
|
+
controlEtag: string | null;
|
|
2814
|
+
expectedDraftRevision: number | null;
|
|
2815
|
+
text: string;
|
|
2816
|
+
annotations: SubmittedTimelineAnnotation[];
|
|
2817
|
+
modelContext: string | null;
|
|
2818
|
+
resources: ResourceRef[];
|
|
2819
|
+
composerDraftResources?: ResourceRef[];
|
|
2820
|
+
model: string | null;
|
|
2821
|
+
reasoningEffort: ReasoningEffort | null;
|
|
2822
|
+
latencyMode: "standard" | "priority" | "fast" | null;
|
|
2823
|
+
source: "user" | "api";
|
|
2824
|
+
mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
|
|
2825
|
+
connectionAuthorities?: McpConnectionAuthoritySelection[];
|
|
2826
|
+
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
2827
|
+
commandActor: SessionCommandActor;
|
|
2828
|
+
}): string {
|
|
2829
|
+
return `prompt-boundary-v1:${canonicalSessionCommandHash({
|
|
2830
|
+
delivery: input.delivery,
|
|
2831
|
+
controlEtag: input.controlEtag,
|
|
2832
|
+
expectedDraftRevision: input.expectedDraftRevision,
|
|
2833
|
+
text: input.text,
|
|
2834
|
+
annotations: input.annotations,
|
|
2835
|
+
modelContext: input.modelContext,
|
|
2836
|
+
resources: input.resources,
|
|
2837
|
+
composerDraftResourcesProvided: input.composerDraftResources !== undefined,
|
|
2838
|
+
composerDraftResources: input.composerDraftResources ?? [],
|
|
2839
|
+
model: input.model,
|
|
2840
|
+
reasoningEffort: input.reasoningEffort,
|
|
2841
|
+
latencyMode: input.latencyMode,
|
|
2842
|
+
source: input.source,
|
|
2843
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates,
|
|
2844
|
+
connectionAuthorities: input.connectionAuthorities ?? [],
|
|
2845
|
+
personalResourceAttachment: input.personalResourceAttachment ?? null,
|
|
2846
|
+
...(input.commandActor.type === "service"
|
|
2847
|
+
? {
|
|
2848
|
+
serviceInitiator: {
|
|
2849
|
+
subjectId: input.commandActor.subjectId,
|
|
2850
|
+
subjectLabel: input.commandActor.subjectLabel ?? null,
|
|
2851
|
+
context: input.commandActor.context ?? {},
|
|
2852
|
+
},
|
|
2853
|
+
}
|
|
2854
|
+
: {}),
|
|
2855
|
+
})}`;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2420
2858
|
/**
|
|
2421
2859
|
* Full accept-user-message flow shared by the `user.message` branch of
|
|
2422
2860
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
@@ -2458,192 +2896,308 @@ export async function acceptSessionUserMessageWithOutcome(
|
|
|
2458
2896
|
interruptionCount: number;
|
|
2459
2897
|
replay: boolean;
|
|
2460
2898
|
}> {
|
|
2461
|
-
const {
|
|
2899
|
+
const { db, bus, workflowClient, objectStorage } = deps;
|
|
2462
2900
|
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
2901
|
+
const delivery = input.delivery ?? "send";
|
|
2902
|
+
const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
|
|
2903
|
+
const commandActor: SessionCommandActor = delegatedServiceInitiator
|
|
2904
|
+
? {
|
|
2905
|
+
type: "service",
|
|
2906
|
+
subjectId: delegatedServiceInitiator.initiator.subjectId,
|
|
2907
|
+
...(delegatedServiceInitiator.initiator.label
|
|
2908
|
+
? { subjectLabel: delegatedServiceInitiator.initiator.label }
|
|
2909
|
+
: {}),
|
|
2910
|
+
context: delegatedServiceInitiator.context,
|
|
2911
|
+
}
|
|
2912
|
+
: { type: "human", subjectId: grant.subjectId };
|
|
2463
2913
|
await requireSessionAuthorization(deps, grant, {
|
|
2464
2914
|
sessionId,
|
|
2465
|
-
operation:
|
|
2915
|
+
operation: delivery === "steer" ? "session.steer" : "session.append",
|
|
2466
2916
|
surface: "core",
|
|
2467
2917
|
});
|
|
2468
|
-
await requireAtomicPersonalResourceAttachment(
|
|
2469
|
-
deps,
|
|
2470
|
-
input.authorization,
|
|
2471
|
-
workspaceId,
|
|
2472
|
-
input.personalResourceAttachment,
|
|
2473
|
-
true,
|
|
2474
|
-
);
|
|
2475
|
-
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
2476
|
-
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
2477
|
-
// pure read with no side effects.
|
|
2478
|
-
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
2479
|
-
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
2480
|
-
const effectiveModel =
|
|
2481
|
-
canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
2482
|
-
if (effectiveModel === null) {
|
|
2483
|
-
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
2484
|
-
}
|
|
2485
|
-
try {
|
|
2486
|
-
assertSessionAllowsProductModel(existingSession, effectiveModel);
|
|
2487
|
-
} catch (error) {
|
|
2488
|
-
if (error instanceof CodexCompactionV2ProviderLockedError) {
|
|
2489
|
-
throw new HTTPException(422, { message: error.message, cause: error });
|
|
2490
|
-
}
|
|
2491
|
-
throw error;
|
|
2492
|
-
}
|
|
2493
|
-
const sessionReasoningEffort = existingSession.reasoningEffort;
|
|
2494
|
-
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
2495
|
-
const sessionLatencyMode = existingSession.latencyMode;
|
|
2496
|
-
const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
|
|
2497
|
-
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
2498
|
-
modelId: effectiveModel,
|
|
2499
|
-
requestedModelId: input.model ?? null,
|
|
2500
|
-
modelSource: input.model == null ? "session" : "explicit",
|
|
2501
|
-
reasoningEffort: effectiveReasoningEffort,
|
|
2502
|
-
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
2503
|
-
latencyMode: effectiveLatencyMode,
|
|
2504
|
-
latencyModeSource: input.latencyMode == null ? "session" : "explicit",
|
|
2505
|
-
});
|
|
2506
2918
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
2507
2919
|
const composerDraftResources = input.composerDraftResources
|
|
2508
2920
|
? normalizeResources(input.composerDraftResources)
|
|
2509
2921
|
: undefined;
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2922
|
+
const boundaryRequestHash = input.clientEventId
|
|
2923
|
+
? sessionPromptBoundaryRequestHash({
|
|
2924
|
+
delivery,
|
|
2925
|
+
controlEtag: input.controlEtag ?? null,
|
|
2926
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
2927
|
+
text: input.text,
|
|
2928
|
+
annotations: input.annotations ?? [],
|
|
2929
|
+
modelContext: input.modelContext ?? null,
|
|
2930
|
+
resources: requestedResources,
|
|
2931
|
+
...(composerDraftResources ? { composerDraftResources } : {}),
|
|
2932
|
+
model: input.model ?? null,
|
|
2933
|
+
reasoningEffort: input.reasoningEffort ?? null,
|
|
2934
|
+
latencyMode: input.latencyMode ?? null,
|
|
2935
|
+
source,
|
|
2936
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
2937
|
+
...(input.connectionAuthorities
|
|
2938
|
+
? { connectionAuthorities: input.connectionAuthorities }
|
|
2939
|
+
: {}),
|
|
2940
|
+
...(input.personalResourceAttachment
|
|
2941
|
+
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
2942
|
+
: {}),
|
|
2943
|
+
commandActor,
|
|
2944
|
+
})
|
|
2945
|
+
: null;
|
|
2946
|
+
if (input.clientEventId && boundaryRequestHash) {
|
|
2947
|
+
const replay = await withWorkspaceSubjectSessionActivityRls(
|
|
2948
|
+
db,
|
|
2949
|
+
workspaceId,
|
|
2950
|
+
grant.subjectId,
|
|
2951
|
+
async (scopedDb) =>
|
|
2952
|
+
await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
|
|
2953
|
+
workspaceId,
|
|
2954
|
+
sessionId,
|
|
2955
|
+
subjectId: grant.subjectId,
|
|
2956
|
+
actor: commandActor,
|
|
2957
|
+
operationKey: input.clientEventId!,
|
|
2958
|
+
delivery,
|
|
2959
|
+
boundaryRequestHash,
|
|
2960
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
2961
|
+
}),
|
|
2514
2962
|
);
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2963
|
+
if (replay) {
|
|
2964
|
+
return finalizePostUserMessageTurn(
|
|
2965
|
+
{
|
|
2966
|
+
db,
|
|
2967
|
+
bus,
|
|
2968
|
+
workflowClient,
|
|
2969
|
+
accountId: grant.accountId,
|
|
2970
|
+
workspaceId,
|
|
2971
|
+
sessionId,
|
|
2972
|
+
delivery,
|
|
2973
|
+
...(deps.schedulePromptPostCommit
|
|
2974
|
+
? { schedulePostCommit: deps.schedulePromptPostCommit }
|
|
2975
|
+
: {}),
|
|
2976
|
+
},
|
|
2977
|
+
replay,
|
|
2978
|
+
);
|
|
2519
2979
|
}
|
|
2520
2980
|
}
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2981
|
+
try {
|
|
2982
|
+
await requireAtomicPersonalResourceAttachment(
|
|
2983
|
+
deps,
|
|
2984
|
+
input.authorization,
|
|
2985
|
+
workspaceId,
|
|
2986
|
+
input.personalResourceAttachment,
|
|
2987
|
+
true,
|
|
2988
|
+
);
|
|
2989
|
+
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
2990
|
+
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
2991
|
+
// pure read with no side effects.
|
|
2992
|
+
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
2993
|
+
const settings = await resolveWorkspaceModelBoundarySettings(
|
|
2994
|
+
deps,
|
|
2995
|
+
grant,
|
|
2996
|
+
workspaceId,
|
|
2997
|
+
[input.model ?? existingSession.model],
|
|
2998
|
+
existingSession.model,
|
|
2999
|
+
);
|
|
3000
|
+
if (settings !== deps.settings) {
|
|
3001
|
+
deps = {
|
|
3002
|
+
...deps,
|
|
3003
|
+
catalogSourceSettings: deps.catalogSourceSettings ?? deps.settings,
|
|
3004
|
+
settings,
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
3008
|
+
const effectiveModel =
|
|
3009
|
+
canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
3010
|
+
if (effectiveModel === null) {
|
|
3011
|
+
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
3012
|
+
}
|
|
3013
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
3014
|
+
try {
|
|
3015
|
+
assertSessionAllowsProductModel(existingSession, effectiveModel);
|
|
3016
|
+
} catch (error) {
|
|
3017
|
+
if (error instanceof CodexCompactionV2ProviderLockedError) {
|
|
3018
|
+
throw new HTTPException(422, { message: error.message, cause: error });
|
|
3019
|
+
}
|
|
3020
|
+
throw error;
|
|
3021
|
+
}
|
|
3022
|
+
const sessionReasoningEffort = existingSession.reasoningEffort;
|
|
3023
|
+
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
3024
|
+
const sessionLatencyMode = existingSession.latencyMode;
|
|
3025
|
+
const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
|
|
3026
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
3027
|
+
modelId: effectiveModel,
|
|
3028
|
+
requestedModelId: input.model ?? null,
|
|
3029
|
+
modelSource: input.model == null ? "session" : "explicit",
|
|
3030
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
3031
|
+
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
3032
|
+
latencyMode: effectiveLatencyMode,
|
|
3033
|
+
latencyModeSource: input.latencyMode == null ? "session" : "explicit",
|
|
2537
3034
|
});
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
2551
|
-
settings,
|
|
2552
|
-
grant,
|
|
2553
|
-
session: existingSession,
|
|
2554
|
-
updates: input.mcpCredentialUpdates ?? [],
|
|
2555
|
-
});
|
|
2556
|
-
const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
|
|
2557
|
-
const inheritedPersonalConnectionDelegations =
|
|
2558
|
-
connectionDelegationSource.kind === "turn"
|
|
2559
|
-
? await getSessionTurnPersonalConnectionDelegations(
|
|
2560
|
-
db,
|
|
2561
|
-
workspaceId,
|
|
2562
|
-
connectionDelegationSource.sessionId,
|
|
2563
|
-
connectionDelegationSource.turnId,
|
|
2564
|
-
)
|
|
2565
|
-
: null;
|
|
2566
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
2567
|
-
db,
|
|
2568
|
-
workspaceId,
|
|
2569
|
-
settings,
|
|
2570
|
-
inheritedPersonalConnectionDelegations
|
|
2571
|
-
? {
|
|
2572
|
-
personalConnectionDelegations: inheritedPersonalConnectionDelegations,
|
|
2573
|
-
}
|
|
2574
|
-
: { subjectId: grant.subjectId },
|
|
2575
|
-
);
|
|
2576
|
-
const personalConnectionDelegations = await freezePersonalConnectionDelegations({
|
|
2577
|
-
db,
|
|
2578
|
-
workspaceId,
|
|
2579
|
-
settings: runtimeSettings,
|
|
2580
|
-
tools: existingSession.tools,
|
|
2581
|
-
resources: [...existingSession.resources, ...requestedResources],
|
|
2582
|
-
source: connectionDelegationSource,
|
|
2583
|
-
targetSessionId: sessionId,
|
|
2584
|
-
googleDrivePublicationEnabled:
|
|
2585
|
-
existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
|
|
2586
|
-
existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
|
|
2587
|
-
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
2588
|
-
(existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
|
|
2589
|
-
existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
|
|
2590
|
-
atlassianEnabled:
|
|
2591
|
-
existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
2592
|
-
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
2593
|
-
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
2594
|
-
...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
|
|
2595
|
-
});
|
|
2596
|
-
const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
|
|
2597
|
-
await postUserMessageTurn({
|
|
3035
|
+
if (composerDraftResources) {
|
|
3036
|
+
const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
|
|
3037
|
+
const unacceptedDraftResource = composerDraftResources.find(
|
|
3038
|
+
(resource) => !acceptedResources.has(stableJson(resource)),
|
|
3039
|
+
);
|
|
3040
|
+
if (unacceptedDraftResource) {
|
|
3041
|
+
throw new HTTPException(422, {
|
|
3042
|
+
message: "composer draft resources must be included in the accepted resource set",
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
const annotations = await validateSubmittedTimelineAnnotations(
|
|
2598
3047
|
db,
|
|
2599
|
-
bus,
|
|
2600
|
-
workflowClient,
|
|
2601
|
-
settings,
|
|
2602
|
-
accountId: grant.accountId,
|
|
2603
3048
|
workspaceId,
|
|
2604
3049
|
sessionId,
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
3050
|
+
input.annotations ?? [],
|
|
3051
|
+
);
|
|
3052
|
+
await requireLimit(deps, {
|
|
3053
|
+
accountId: grant.accountId,
|
|
3054
|
+
workspaceId,
|
|
3055
|
+
action: "agent_run:create",
|
|
3056
|
+
quantity: 1,
|
|
3057
|
+
model: effectiveModel,
|
|
3058
|
+
});
|
|
3059
|
+
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
3060
|
+
throw new HTTPException(503, {
|
|
3061
|
+
message: "object storage is not configured",
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
await validateFileResources(
|
|
3065
|
+
db,
|
|
3066
|
+
grant.accountId,
|
|
3067
|
+
workspaceId,
|
|
3068
|
+
grant.subjectId,
|
|
3069
|
+
requestedResources,
|
|
3070
|
+
);
|
|
3071
|
+
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3072
|
+
...existingSession.resources,
|
|
3073
|
+
...requestedResources,
|
|
3074
|
+
]);
|
|
3075
|
+
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
3076
|
+
settings,
|
|
3077
|
+
grant,
|
|
3078
|
+
session: existingSession,
|
|
3079
|
+
updates: input.mcpCredentialUpdates ?? [],
|
|
3080
|
+
});
|
|
3081
|
+
const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
|
|
3082
|
+
const inheritedPersonalConnectionDelegations =
|
|
3083
|
+
connectionDelegationSource.kind === "turn"
|
|
3084
|
+
? await getSessionTurnPersonalConnectionDelegations(
|
|
3085
|
+
db,
|
|
3086
|
+
workspaceId,
|
|
3087
|
+
connectionDelegationSource.sessionId,
|
|
3088
|
+
connectionDelegationSource.turnId,
|
|
3089
|
+
)
|
|
3090
|
+
: null;
|
|
3091
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3092
|
+
db,
|
|
3093
|
+
workspaceId,
|
|
3094
|
+
settings,
|
|
3095
|
+
inheritedPersonalConnectionDelegations
|
|
2625
3096
|
? {
|
|
2626
|
-
|
|
2627
|
-
type: "service" as const,
|
|
2628
|
-
subjectId: delegatedServiceInitiator.initiator.subjectId,
|
|
2629
|
-
...(delegatedServiceInitiator.initiator.label
|
|
2630
|
-
? { subjectLabel: delegatedServiceInitiator.initiator.label }
|
|
2631
|
-
: {}),
|
|
2632
|
-
context: delegatedServiceInitiator.context,
|
|
2633
|
-
},
|
|
3097
|
+
personalConnectionDelegations: inheritedPersonalConnectionDelegations,
|
|
2634
3098
|
}
|
|
2635
|
-
: {}
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
...
|
|
2643
|
-
|
|
2644
|
-
|
|
3099
|
+
: { subjectId: grant.subjectId },
|
|
3100
|
+
);
|
|
3101
|
+
const personalConnectionDelegations = await freezePersonalConnectionDelegations({
|
|
3102
|
+
db,
|
|
3103
|
+
workspaceId,
|
|
3104
|
+
settings: runtimeSettings,
|
|
3105
|
+
tools: existingSession.tools,
|
|
3106
|
+
resources: [...existingSession.resources, ...requestedResources],
|
|
3107
|
+
source: connectionDelegationSource,
|
|
3108
|
+
targetSessionId: sessionId,
|
|
3109
|
+
googleDrivePublicationEnabled:
|
|
3110
|
+
existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
|
|
3111
|
+
existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
|
|
3112
|
+
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
3113
|
+
(existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
|
|
3114
|
+
existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
|
|
3115
|
+
atlassianEnabled:
|
|
3116
|
+
existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
3117
|
+
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
3118
|
+
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
3119
|
+
...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
|
|
2645
3120
|
});
|
|
2646
|
-
|
|
3121
|
+
const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
|
|
3122
|
+
await postUserMessageTurn({
|
|
3123
|
+
db,
|
|
3124
|
+
bus,
|
|
3125
|
+
workflowClient,
|
|
3126
|
+
settings,
|
|
3127
|
+
accountId: grant.accountId,
|
|
3128
|
+
workspaceId,
|
|
3129
|
+
sessionId,
|
|
3130
|
+
text: input.text,
|
|
3131
|
+
annotations,
|
|
3132
|
+
modelContext: input.modelContext ?? null,
|
|
3133
|
+
resources: requestedResources,
|
|
3134
|
+
...(composerDraftResources ? { composerDraftResources } : {}),
|
|
3135
|
+
model: input.model ?? null,
|
|
3136
|
+
reasoningEffort: input.reasoningEffort ?? null,
|
|
3137
|
+
latencyMode: input.latencyMode ?? null,
|
|
3138
|
+
reasoningEffortFallback: sessionReasoningEffort,
|
|
3139
|
+
turnExecutionPolicy,
|
|
3140
|
+
mcpCredentialUpdates,
|
|
3141
|
+
personalConnectionDelegations,
|
|
3142
|
+
...(input.personalResourceAttachment
|
|
3143
|
+
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
3144
|
+
: {}),
|
|
3145
|
+
delivery,
|
|
3146
|
+
origin: source === "api" ? "operator" : "human",
|
|
3147
|
+
actor: grant.subjectId,
|
|
3148
|
+
...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
|
|
3149
|
+
commandActor,
|
|
3150
|
+
...(boundaryRequestHash ? { boundaryRequestHash } : {}),
|
|
3151
|
+
...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
|
|
3152
|
+
...(input.expectedDraftRevision !== undefined
|
|
3153
|
+
? { expectedDraftRevision: input.expectedDraftRevision }
|
|
3154
|
+
: {}),
|
|
3155
|
+
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
3156
|
+
recordAgentRunUsage: true,
|
|
3157
|
+
...(deps.schedulePromptPostCommit
|
|
3158
|
+
? { schedulePostCommit: deps.schedulePromptPostCommit }
|
|
3159
|
+
: {}),
|
|
3160
|
+
});
|
|
3161
|
+
return { accepted, turn, draft, receipt, routing, interruptionCount, replay };
|
|
3162
|
+
} catch (error) {
|
|
3163
|
+
if (input.clientEventId && boundaryRequestHash) {
|
|
3164
|
+
const replay = await withWorkspaceSubjectSessionActivityRls(
|
|
3165
|
+
db,
|
|
3166
|
+
workspaceId,
|
|
3167
|
+
grant.subjectId,
|
|
3168
|
+
async (scopedDb) =>
|
|
3169
|
+
await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
|
|
3170
|
+
workspaceId,
|
|
3171
|
+
sessionId,
|
|
3172
|
+
subjectId: grant.subjectId,
|
|
3173
|
+
actor: commandActor,
|
|
3174
|
+
operationKey: input.clientEventId!,
|
|
3175
|
+
delivery,
|
|
3176
|
+
boundaryRequestHash,
|
|
3177
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
3178
|
+
serializeOperation: true,
|
|
3179
|
+
}),
|
|
3180
|
+
);
|
|
3181
|
+
if (replay) {
|
|
3182
|
+
return finalizePostUserMessageTurn(
|
|
3183
|
+
{
|
|
3184
|
+
db,
|
|
3185
|
+
bus,
|
|
3186
|
+
workflowClient,
|
|
3187
|
+
accountId: grant.accountId,
|
|
3188
|
+
workspaceId,
|
|
3189
|
+
sessionId,
|
|
3190
|
+
delivery,
|
|
3191
|
+
...(deps.schedulePromptPostCommit
|
|
3192
|
+
? { schedulePostCommit: deps.schedulePromptPostCommit }
|
|
3193
|
+
: {}),
|
|
3194
|
+
},
|
|
3195
|
+
replay,
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
throw error;
|
|
3200
|
+
}
|
|
2647
3201
|
}
|
|
2648
3202
|
|
|
2649
3203
|
/** Backward-compatible entity-returning path used by existing REST callers. */
|