@opengeni/core 0.21.2 → 0.21.10
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 +8 -0
- package/dist/application/session-commands.d.ts +7 -2
- package/dist/domain/capabilities.d.ts +7 -0
- package/dist/domain/memory-slack-publication.d.ts +0 -2
- package/dist/domain/scheduled-tasks.d.ts +25 -1
- package/dist/domain/sessions.d.ts +30 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +451 -102
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +53 -0
- package/dist/sandbox/fleet.d.ts +9 -1
- package/dist/session-authorization.d.ts +9 -0
- package/package.json +10 -10
- package/src/access/index.ts +43 -3
- package/src/application/session-commands.ts +29 -9
- package/src/domain/capabilities.ts +99 -29
- package/src/domain/memory-slack-publication.ts +5 -17
- package/src/domain/resources.ts +10 -20
- package/src/domain/scheduled-tasks.ts +221 -7
- package/src/domain/sessions.ts +127 -37
- package/src/index.ts +1 -0
- package/src/managed-auth-type.ts +4 -3
- package/src/managed-session.ts +36 -0
- package/src/sandbox/fleet.ts +56 -1
- package/src/sandbox/routing.ts +26 -0
- package/src/session-authorization.ts +17 -0
|
@@ -4,6 +4,9 @@ import type {
|
|
|
4
4
|
McpPersonalConnectionDelegation,
|
|
5
5
|
ScheduledTask,
|
|
6
6
|
ScheduledTaskAgentConfig,
|
|
7
|
+
Session,
|
|
8
|
+
SessionAuthorizationPort,
|
|
9
|
+
SessionAuthorizationSurface,
|
|
7
10
|
CreateScheduledTaskRequest as CreateScheduledTaskPayload,
|
|
8
11
|
UpdateScheduledTaskRequest as UpdateScheduledTaskPayload,
|
|
9
12
|
} from "@opengeni/contracts";
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
getRig,
|
|
16
19
|
getScheduledTask,
|
|
17
20
|
getScheduledTaskPersonalConnectionDelegations,
|
|
21
|
+
getSession,
|
|
18
22
|
requireWorkspace,
|
|
19
23
|
updateScheduledTask,
|
|
20
24
|
type Database,
|
|
@@ -22,6 +26,11 @@ import {
|
|
|
22
26
|
} from "@opengeni/db";
|
|
23
27
|
import { HTTPException } from "hono/http-exception";
|
|
24
28
|
import { hasPermission, requirePermission } from "../access";
|
|
29
|
+
import {
|
|
30
|
+
requireSessionAuthorization,
|
|
31
|
+
SessionAuthorizationDeniedError,
|
|
32
|
+
SessionAuthorizationUnavailableError,
|
|
33
|
+
} from "../session-authorization";
|
|
25
34
|
import type { SessionWorkflowClient } from "../dependencies";
|
|
26
35
|
import type { ObjectStorageDependency } from "../dependencies";
|
|
27
36
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
@@ -38,6 +47,7 @@ import {
|
|
|
38
47
|
} from "./sessions";
|
|
39
48
|
import {
|
|
40
49
|
hasReservedOpenGeniSlackBotSessionMetadata,
|
|
50
|
+
scheduledSlackBotConnectionId,
|
|
41
51
|
validateOpenGeniSlackBotConnectionSelection,
|
|
42
52
|
} from "./slack-bot";
|
|
43
53
|
import {
|
|
@@ -81,6 +91,8 @@ export async function createValidatedScheduledTask(input: {
|
|
|
81
91
|
// Set for pack-installation-inherited attachments that were already
|
|
82
92
|
// authorized with variable-sets:use when the pack was enabled.
|
|
83
93
|
variableSetPreauthorized?: boolean;
|
|
94
|
+
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
95
|
+
authorizationSurface?: SessionAuthorizationSurface | undefined;
|
|
84
96
|
}): Promise<ScheduledTask> {
|
|
85
97
|
const agentConfig = await validateScheduledTaskAgentConfig({
|
|
86
98
|
...input,
|
|
@@ -88,6 +100,17 @@ export async function createValidatedScheduledTask(input: {
|
|
|
88
100
|
});
|
|
89
101
|
const id = crypto.randomUUID();
|
|
90
102
|
validateScheduledTaskSchedule(input.payload.schedule);
|
|
103
|
+
const target = await validateScheduledTaskTarget({
|
|
104
|
+
db: input.db,
|
|
105
|
+
sessionAuthorization: input.sessionAuthorization,
|
|
106
|
+
authorizationSurface: input.authorizationSurface,
|
|
107
|
+
grant: input.grant,
|
|
108
|
+
targetSessionId: input.payload.targetSessionId,
|
|
109
|
+
runMode: input.payload.runMode,
|
|
110
|
+
variableSetId: input.payload.variableSetId,
|
|
111
|
+
rigId: input.payload.rigId,
|
|
112
|
+
agentConfig,
|
|
113
|
+
});
|
|
91
114
|
if (input.payload.variableSetId) {
|
|
92
115
|
await validateVariableSetAttachment(
|
|
93
116
|
{ settings: input.settings, db: input.db },
|
|
@@ -132,12 +155,126 @@ export async function createValidatedScheduledTask(input: {
|
|
|
132
155
|
...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
|
|
133
156
|
createdByActor: creationInitiator.actor ?? null,
|
|
134
157
|
personalConnectionDelegations,
|
|
158
|
+
targetSessionId: target?.id ?? null,
|
|
135
159
|
variableSetId: input.payload.variableSetId ?? null,
|
|
136
160
|
rigId: input.payload.rigId ?? null,
|
|
137
161
|
metadata: input.payload.metadata,
|
|
138
162
|
});
|
|
139
163
|
}
|
|
140
164
|
|
|
165
|
+
export async function validateScheduledTaskTarget(input: {
|
|
166
|
+
db: Database;
|
|
167
|
+
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
168
|
+
authorizationSurface?: SessionAuthorizationSurface | undefined;
|
|
169
|
+
grant: AccessGrant;
|
|
170
|
+
targetSessionId: string | null | undefined;
|
|
171
|
+
runMode: ScheduledTask["runMode"];
|
|
172
|
+
variableSetId: string | null | undefined;
|
|
173
|
+
rigId: string | null | undefined;
|
|
174
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
175
|
+
missingTargetStatus?: 404 | 422;
|
|
176
|
+
}): Promise<Session | null> {
|
|
177
|
+
if (input.runMode !== "existing_session") {
|
|
178
|
+
if (input.targetSessionId) {
|
|
179
|
+
throw new HTTPException(422, {
|
|
180
|
+
message: "targetSessionId requires runMode=existing_session",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (!input.targetSessionId) {
|
|
186
|
+
throw new HTTPException(input.missingTargetStatus ?? 422, {
|
|
187
|
+
message:
|
|
188
|
+
input.missingTargetStatus === 404
|
|
189
|
+
? "target session not found"
|
|
190
|
+
: "targetSessionId is required when runMode=existing_session",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
requirePermission(input.grant, "sessions:control");
|
|
194
|
+
if (input.agentConfig.goal) {
|
|
195
|
+
throw new HTTPException(422, {
|
|
196
|
+
message: "agentConfig.goal cannot be used with an existing-session target",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
await requireSessionAuthorization(
|
|
201
|
+
{
|
|
202
|
+
db: input.db,
|
|
203
|
+
...(input.sessionAuthorization !== undefined
|
|
204
|
+
? { sessionAuthorization: input.sessionAuthorization }
|
|
205
|
+
: {}),
|
|
206
|
+
},
|
|
207
|
+
input.grant,
|
|
208
|
+
{
|
|
209
|
+
sessionId: input.targetSessionId,
|
|
210
|
+
operation: "session.control",
|
|
211
|
+
surface: input.authorizationSurface ?? "http",
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof SessionAuthorizationDeniedError) {
|
|
216
|
+
throw new HTTPException(404, { message: "target session not found" });
|
|
217
|
+
}
|
|
218
|
+
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
219
|
+
throw new HTTPException(503, { message: "session authorization is unavailable" });
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const session = await getSession(input.db, input.grant.workspaceId, input.targetSessionId);
|
|
224
|
+
if (!session || session.accountId !== input.grant.accountId) {
|
|
225
|
+
throw new HTTPException(404, { message: "target session not found" });
|
|
226
|
+
}
|
|
227
|
+
if (session.status === "cancelled") {
|
|
228
|
+
throw new HTTPException(409, {
|
|
229
|
+
message: "target session is cancelled; choose a revivable session",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if ((session.variableSetId ?? null) !== (input.variableSetId ?? null)) {
|
|
233
|
+
throw new HTTPException(422, {
|
|
234
|
+
message: "target session variableSet attachment does not match the scheduled task",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (input.rigId && input.rigId !== session.rigId) {
|
|
238
|
+
throw new HTTPException(422, {
|
|
239
|
+
message: "target session rig does not match the scheduled task",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
input.agentConfig.sandboxBackend !== undefined &&
|
|
244
|
+
input.agentConfig.sandboxBackend !== session.sandboxBackend
|
|
245
|
+
) {
|
|
246
|
+
throw new HTTPException(422, {
|
|
247
|
+
message: "target session sandbox backend does not match the scheduled task",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (
|
|
251
|
+
scheduledSlackBotConnectionId(session.metadata) !==
|
|
252
|
+
(input.agentConfig.slackBotConnectionId ?? null)
|
|
253
|
+
) {
|
|
254
|
+
throw new HTTPException(422, {
|
|
255
|
+
message: "target session OpenGeni Slack bot binding does not match the scheduled task",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return session;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function scheduledTaskForGrant(task: ScheduledTask, grant: AccessGrant): ScheduledTask {
|
|
262
|
+
if (hasPermission(grant.permissions, "sessions:control") || task.targetSessionId === null) {
|
|
263
|
+
return task;
|
|
264
|
+
}
|
|
265
|
+
return { ...task, targetSessionId: null };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function scheduledTaskRunForGrant<T extends { sessionId: string | null }>(
|
|
269
|
+
run: T,
|
|
270
|
+
grant: AccessGrant,
|
|
271
|
+
): T {
|
|
272
|
+
if (hasPermission(grant.permissions, "sessions:control") || run.sessionId === null) {
|
|
273
|
+
return run;
|
|
274
|
+
}
|
|
275
|
+
return { ...run, sessionId: null };
|
|
276
|
+
}
|
|
277
|
+
|
|
141
278
|
// Validate a scheduled task's rig reference: it must name a rig in the
|
|
142
279
|
// workspace. A missing/cross-workspace id is a 422 (RLS-invisible == missing).
|
|
143
280
|
async function requireScheduledTaskRig(
|
|
@@ -160,8 +297,28 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
160
297
|
payload: UpdateScheduledTaskPayload;
|
|
161
298
|
/** See createValidatedScheduledTask; only consulted when agentConfig is updated. */
|
|
162
299
|
toolsProvided?: boolean;
|
|
300
|
+
sessionAuthorization?: SessionAuthorizationPort | null | undefined;
|
|
301
|
+
authorizationSurface?: SessionAuthorizationSurface | undefined;
|
|
163
302
|
}): Promise<UpdateScheduledTaskInput> {
|
|
164
303
|
const update: UpdateScheduledTaskInput = {};
|
|
304
|
+
const existingTarget = input.existing.targetSessionId;
|
|
305
|
+
const nextRunMode = input.payload.runMode ?? input.existing.runMode;
|
|
306
|
+
const nextTargetSessionId =
|
|
307
|
+
input.payload.targetSessionId !== undefined
|
|
308
|
+
? input.payload.targetSessionId
|
|
309
|
+
: nextRunMode === "existing_session"
|
|
310
|
+
? existingTarget
|
|
311
|
+
: null;
|
|
312
|
+
if (
|
|
313
|
+
input.existing.runMode === "reusable_session" &&
|
|
314
|
+
input.existing.reusableSessionId &&
|
|
315
|
+
nextRunMode === "existing_session"
|
|
316
|
+
) {
|
|
317
|
+
throw new HTTPException(409, {
|
|
318
|
+
message:
|
|
319
|
+
"cannot target an existing session after this task created its reusable session; create a new task",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
165
322
|
if (input.payload.name !== undefined) {
|
|
166
323
|
update.name = trimmedScheduledTaskName(input.payload.name);
|
|
167
324
|
}
|
|
@@ -278,6 +435,43 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
278
435
|
}
|
|
279
436
|
update.personalConnectionDelegations = personalConnectionDelegations;
|
|
280
437
|
}
|
|
438
|
+
if (
|
|
439
|
+
existingTarget &&
|
|
440
|
+
(nextRunMode !== "existing_session" || nextTargetSessionId !== existingTarget)
|
|
441
|
+
) {
|
|
442
|
+
await validateScheduledTaskTarget({
|
|
443
|
+
db: input.db,
|
|
444
|
+
sessionAuthorization: input.sessionAuthorization,
|
|
445
|
+
authorizationSurface: input.authorizationSurface,
|
|
446
|
+
grant: input.grant,
|
|
447
|
+
targetSessionId: existingTarget,
|
|
448
|
+
runMode: "existing_session",
|
|
449
|
+
variableSetId: input.existing.variableSetId,
|
|
450
|
+
rigId: input.existing.rigId,
|
|
451
|
+
agentConfig: input.existing.agentConfig,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
await validateScheduledTaskTarget({
|
|
455
|
+
db: input.db,
|
|
456
|
+
sessionAuthorization: input.sessionAuthorization,
|
|
457
|
+
authorizationSurface: input.authorizationSurface,
|
|
458
|
+
grant: input.grant,
|
|
459
|
+
targetSessionId: nextTargetSessionId,
|
|
460
|
+
runMode: nextRunMode,
|
|
461
|
+
variableSetId:
|
|
462
|
+
input.payload.variableSetId !== undefined
|
|
463
|
+
? input.payload.variableSetId
|
|
464
|
+
: input.existing.variableSetId,
|
|
465
|
+
rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
|
|
466
|
+
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
467
|
+
});
|
|
468
|
+
if (
|
|
469
|
+
input.payload.targetSessionId !== undefined ||
|
|
470
|
+
input.existing.runMode === "existing_session" ||
|
|
471
|
+
nextRunMode === "existing_session"
|
|
472
|
+
) {
|
|
473
|
+
update.targetSessionId = nextTargetSessionId;
|
|
474
|
+
}
|
|
281
475
|
return update;
|
|
282
476
|
}
|
|
283
477
|
|
|
@@ -325,13 +519,25 @@ export async function restoreScheduledTask(
|
|
|
325
519
|
overlapPolicy: task.overlapPolicy,
|
|
326
520
|
agentConfig: task.agentConfig,
|
|
327
521
|
personalConnectionDelegations: previous.personalConnectionDelegations,
|
|
328
|
-
|
|
522
|
+
...(task.runMode === "existing_session"
|
|
523
|
+
? { targetSessionId: task.targetSessionId }
|
|
524
|
+
: { reusableSessionId: task.reusableSessionId }),
|
|
329
525
|
variableSetId: task.variableSetId,
|
|
330
526
|
rigId: task.rigId,
|
|
331
527
|
metadata: task.metadata,
|
|
332
528
|
});
|
|
333
529
|
}
|
|
334
530
|
|
|
531
|
+
export class ScheduledTaskSyncError extends Error {
|
|
532
|
+
readonly persistenceRestored: boolean;
|
|
533
|
+
|
|
534
|
+
constructor(cause: unknown, persistenceRestored: boolean) {
|
|
535
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
536
|
+
this.name = "ScheduledTaskSyncError";
|
|
537
|
+
this.persistenceRestored = persistenceRestored;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
335
541
|
export async function syncCreatedScheduledTask(input: {
|
|
336
542
|
db: Database;
|
|
337
543
|
workflowClient: SessionWorkflowClient;
|
|
@@ -340,10 +546,13 @@ export async function syncCreatedScheduledTask(input: {
|
|
|
340
546
|
try {
|
|
341
547
|
await input.workflowClient.syncScheduledTask({ task: input.task });
|
|
342
548
|
} catch (error) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
549
|
+
let persistenceRestored = true;
|
|
550
|
+
try {
|
|
551
|
+
await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id);
|
|
552
|
+
} catch {
|
|
553
|
+
persistenceRestored = false;
|
|
554
|
+
}
|
|
555
|
+
throw new ScheduledTaskSyncError(error, persistenceRestored);
|
|
347
556
|
}
|
|
348
557
|
}
|
|
349
558
|
|
|
@@ -356,8 +565,13 @@ export async function syncUpdatedScheduledTask(input: {
|
|
|
356
565
|
try {
|
|
357
566
|
await input.workflowClient.syncScheduledTask({ task: input.task });
|
|
358
567
|
} catch (error) {
|
|
359
|
-
|
|
360
|
-
|
|
568
|
+
let persistenceRestored = true;
|
|
569
|
+
try {
|
|
570
|
+
await restoreScheduledTask(input.db, input.previous);
|
|
571
|
+
} catch {
|
|
572
|
+
persistenceRestored = false;
|
|
573
|
+
}
|
|
574
|
+
throw new ScheduledTaskSyncError(error, persistenceRestored);
|
|
361
575
|
}
|
|
362
576
|
}
|
|
363
577
|
|
package/src/domain/sessions.ts
CHANGED
|
@@ -506,7 +506,22 @@ function validateSessionMcpCredentialUpdates(input: {
|
|
|
506
506
|
return encryptedUpdates;
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
-
export
|
|
509
|
+
export type CreateSessionOutcome = {
|
|
510
|
+
session: CreateSessionResponse;
|
|
511
|
+
/** The committed create/start effect represented by this request. */
|
|
512
|
+
outcome: "created" | "repaired" | "replayed";
|
|
513
|
+
/** Backward-compatible replay flag for existing entity-oriented callers. */
|
|
514
|
+
replay: boolean;
|
|
515
|
+
/** True when the request created/repaired start state or committed a new wake revision. */
|
|
516
|
+
changed: boolean;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export type CreateSessionRequestOutcome = CreateSessionOutcome & {
|
|
520
|
+
/** Billing telemetry is recorded after the committed session start. */
|
|
521
|
+
usageRecording: "recorded" | "failed";
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export async function createAndStartSessionWithOutcome(input: {
|
|
510
525
|
requestedSessionId?: string;
|
|
511
526
|
db: Database;
|
|
512
527
|
bus: EventBus;
|
|
@@ -597,7 +612,7 @@ export async function createAndStartSession(input: {
|
|
|
597
612
|
maxNestedAgentDepthOverride?: number | null;
|
|
598
613
|
allowNestedAgentDepthIncrease?: boolean;
|
|
599
614
|
subjectId?: string | null;
|
|
600
|
-
}): Promise<
|
|
615
|
+
}): Promise<CreateSessionOutcome> {
|
|
601
616
|
const sessionMetadata = {
|
|
602
617
|
...input.metadata,
|
|
603
618
|
model: input.model,
|
|
@@ -647,12 +662,24 @@ export async function createAndStartSession(input: {
|
|
|
647
662
|
}
|
|
648
663
|
const { session: keyed, created } = keyedResult;
|
|
649
664
|
if (!created) {
|
|
650
|
-
|
|
665
|
+
const finished = await finishStartSession(
|
|
651
666
|
keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
652
667
|
keyed,
|
|
653
668
|
);
|
|
669
|
+
return {
|
|
670
|
+
session: finished.session,
|
|
671
|
+
outcome: finished.changed ? "repaired" : "replayed",
|
|
672
|
+
replay: !finished.changed,
|
|
673
|
+
changed: finished.changed,
|
|
674
|
+
};
|
|
654
675
|
}
|
|
655
|
-
|
|
676
|
+
const finished = await finishStartSession(input, keyed);
|
|
677
|
+
return {
|
|
678
|
+
session: finished.session,
|
|
679
|
+
outcome: "created",
|
|
680
|
+
replay: false,
|
|
681
|
+
changed: true,
|
|
682
|
+
};
|
|
656
683
|
}
|
|
657
684
|
let session: Session;
|
|
658
685
|
try {
|
|
@@ -694,7 +721,20 @@ export async function createAndStartSession(input: {
|
|
|
694
721
|
}
|
|
695
722
|
throw error;
|
|
696
723
|
}
|
|
697
|
-
|
|
724
|
+
const finished = await finishStartSession(input, session);
|
|
725
|
+
return {
|
|
726
|
+
session: finished.session,
|
|
727
|
+
outcome: "created",
|
|
728
|
+
replay: false,
|
|
729
|
+
changed: true,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Backward-compatible entity-returning create path used by existing callers. */
|
|
734
|
+
export async function createAndStartSession(
|
|
735
|
+
input: Parameters<typeof createAndStartSessionWithOutcome>[0],
|
|
736
|
+
): Promise<CreateSessionResponse> {
|
|
737
|
+
return (await createAndStartSessionWithOutcome(input)).session;
|
|
698
738
|
}
|
|
699
739
|
|
|
700
740
|
/**
|
|
@@ -730,7 +770,7 @@ async function finishStartSession(
|
|
|
730
770
|
consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
|
|
731
771
|
},
|
|
732
772
|
session: Session,
|
|
733
|
-
): Promise<CreateSessionResponse> {
|
|
773
|
+
): Promise<{ session: CreateSessionResponse; changed: boolean }> {
|
|
734
774
|
// Create-time machine targeting (A-2a): seed the active-sandbox pointer BEFORE
|
|
735
775
|
// the atomic initial turn transaction, so the FIRST turn routes to the chosen
|
|
736
776
|
// machine. swapActiveSandbox does
|
|
@@ -806,7 +846,10 @@ async function finishStartSession(
|
|
|
806
846
|
started.turn?.id ??
|
|
807
847
|
(await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ??
|
|
808
848
|
null;
|
|
809
|
-
return {
|
|
849
|
+
return {
|
|
850
|
+
session: { ...persisted, initialTurnId },
|
|
851
|
+
changed: started.changed,
|
|
852
|
+
};
|
|
810
853
|
}
|
|
811
854
|
|
|
812
855
|
export function workflowIdForSession(sessionId: string): string {
|
|
@@ -997,7 +1040,7 @@ export async function postUserMessageTurn(input: {
|
|
|
997
1040
|
expectedDraftRevision?: number | null;
|
|
998
1041
|
reasoningEffortFallback?: Settings["openaiReasoningEffort"];
|
|
999
1042
|
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
1000
|
-
}): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1043
|
+
}): Promise<{ accepted: SessionEvent; turn: SessionTurn; replay: boolean }> {
|
|
1001
1044
|
const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
|
|
1002
1045
|
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
1003
1046
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
@@ -1103,13 +1146,14 @@ export async function postUserMessageTurn(input: {
|
|
|
1103
1146
|
? { interruptionRequested: true }
|
|
1104
1147
|
: {}),
|
|
1105
1148
|
});
|
|
1106
|
-
} catch
|
|
1107
|
-
console.warn(
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1149
|
+
} catch {
|
|
1150
|
+
console.warn("[sessions] workflow wake failed; durable outbox will retry", {
|
|
1151
|
+
errorClass: "WorkflowWakeOperationError",
|
|
1152
|
+
errorCode: "session_workflow_wake_failed",
|
|
1153
|
+
origin: "core",
|
|
1154
|
+
});
|
|
1111
1155
|
}
|
|
1112
|
-
return { accepted, turn };
|
|
1156
|
+
return { accepted, turn, replay: result.replay };
|
|
1113
1157
|
}
|
|
1114
1158
|
|
|
1115
1159
|
/**
|
|
@@ -1121,12 +1165,12 @@ export async function postUserMessageTurn(input: {
|
|
|
1121
1165
|
* trusted immediate parent, while explicit arrays (including []) win. A
|
|
1122
1166
|
* top-level create with omitted tools applies workspace-default capability MCPs.
|
|
1123
1167
|
*/
|
|
1124
|
-
export async function
|
|
1168
|
+
export async function createSessionForRequestWithOutcome(
|
|
1125
1169
|
deps: ApiRouteDeps,
|
|
1126
1170
|
grant: AccessGrant,
|
|
1127
1171
|
workspaceId: string,
|
|
1128
1172
|
rawPayload: unknown,
|
|
1129
|
-
): Promise<
|
|
1173
|
+
): Promise<CreateSessionRequestOutcome> {
|
|
1130
1174
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1131
1175
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1132
1176
|
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
@@ -1621,9 +1665,9 @@ export async function createSessionForRequest(
|
|
|
1621
1665
|
});
|
|
1622
1666
|
}
|
|
1623
1667
|
const creationInitiator = creationInitiatorForGrant(grant);
|
|
1624
|
-
let
|
|
1668
|
+
let createOutcome: CreateSessionOutcome;
|
|
1625
1669
|
try {
|
|
1626
|
-
|
|
1670
|
+
createOutcome = await createAndStartSessionWithOutcome({
|
|
1627
1671
|
...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
|
|
1628
1672
|
db,
|
|
1629
1673
|
bus,
|
|
@@ -1704,24 +1748,52 @@ export async function createSessionForRequest(
|
|
|
1704
1748
|
}
|
|
1705
1749
|
throw error;
|
|
1706
1750
|
}
|
|
1751
|
+
let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
|
|
1707
1752
|
if (payload.startMode !== "realtime") {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1753
|
+
try {
|
|
1754
|
+
await recordWorkspaceUsage(deps, {
|
|
1755
|
+
accountId: grant.accountId,
|
|
1756
|
+
workspaceId,
|
|
1757
|
+
subjectId: grant.subjectId,
|
|
1758
|
+
eventType: "agent_run.created",
|
|
1759
|
+
quantity: 1,
|
|
1760
|
+
unit: "run",
|
|
1761
|
+
sourceResourceType: "session",
|
|
1762
|
+
sourceResourceId: createOutcome.session.id,
|
|
1763
|
+
sessionId: createOutcome.session.id,
|
|
1764
|
+
initiator: createOutcome.session.createdBy,
|
|
1765
|
+
initiatorContext: createOutcome.session.createdByContext,
|
|
1766
|
+
origin: creationInitiator.actor ? "system" : "user",
|
|
1767
|
+
idempotencyKey: `agent_run.created:${workspaceId}:${createOutcome.session.id}`,
|
|
1768
|
+
});
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
usageRecording = "failed";
|
|
1771
|
+
reportSessionUsageRecordingFailure(error);
|
|
1772
|
+
}
|
|
1723
1773
|
}
|
|
1724
|
-
return
|
|
1774
|
+
return { ...createOutcome, usageRecording };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/** @internal Fixed public projection; the committed session outcome remains authoritative. */
|
|
1778
|
+
export function reportSessionUsageRecordingFailure(_error: unknown): void {
|
|
1779
|
+
console.warn(
|
|
1780
|
+
"[sessions] usage recording failed after committed session create; returning committed outcome",
|
|
1781
|
+
{
|
|
1782
|
+
errorClass: "UsageRecordingError",
|
|
1783
|
+
errorCode: "session_create_usage_recording_failed",
|
|
1784
|
+
origin: "core",
|
|
1785
|
+
},
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/** Backward-compatible entity-returning request path for REST and core callers. */
|
|
1790
|
+
export async function createSessionForRequest(
|
|
1791
|
+
deps: ApiRouteDeps,
|
|
1792
|
+
grant: AccessGrant,
|
|
1793
|
+
workspaceId: string,
|
|
1794
|
+
rawPayload: unknown,
|
|
1795
|
+
): Promise<CreateSessionResponse> {
|
|
1796
|
+
return (await createSessionForRequestWithOutcome(deps, grant, workspaceId, rawPayload)).session;
|
|
1725
1797
|
}
|
|
1726
1798
|
|
|
1727
1799
|
/**
|
|
@@ -1731,7 +1803,7 @@ export async function createSessionForRequest(
|
|
|
1731
1803
|
* enqueue, and usage recording. `toolsProvided: false` durably preserves an
|
|
1732
1804
|
* Tool selection is durable session state and never rides a follow-up prompt.
|
|
1733
1805
|
*/
|
|
1734
|
-
export async function
|
|
1806
|
+
export async function acceptSessionUserMessageWithOutcome(
|
|
1735
1807
|
deps: AcceptSessionUserMessageDependencies,
|
|
1736
1808
|
grant: AccessGrant,
|
|
1737
1809
|
workspaceId: string,
|
|
@@ -1750,7 +1822,7 @@ export async function acceptSessionUserMessage(
|
|
|
1750
1822
|
controlEtag?: string | null;
|
|
1751
1823
|
expectedDraftRevision?: number | null;
|
|
1752
1824
|
},
|
|
1753
|
-
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1825
|
+
): Promise<{ accepted: SessionEvent; turn: SessionTurn; replay: boolean }> {
|
|
1754
1826
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1755
1827
|
await requireSessionAuthorization(deps, grant, {
|
|
1756
1828
|
sessionId,
|
|
@@ -1822,7 +1894,7 @@ export async function acceptSessionUserMessage(
|
|
|
1822
1894
|
source: personalConnectionDelegationSourceForGrant(grant),
|
|
1823
1895
|
});
|
|
1824
1896
|
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
1825
|
-
const { accepted, turn } = await postUserMessageTurn({
|
|
1897
|
+
const { accepted, turn, replay } = await postUserMessageTurn({
|
|
1826
1898
|
db,
|
|
1827
1899
|
bus,
|
|
1828
1900
|
workflowClient,
|
|
@@ -1878,6 +1950,24 @@ export async function acceptSessionUserMessage(
|
|
|
1878
1950
|
origin: turn.source,
|
|
1879
1951
|
idempotencyKey: `agent_run.created:${workspaceId}:${turn.id}`,
|
|
1880
1952
|
});
|
|
1953
|
+
return { accepted, turn, replay };
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** Backward-compatible entity-returning path used by existing REST callers. */
|
|
1957
|
+
export async function acceptSessionUserMessage(
|
|
1958
|
+
deps: Parameters<typeof acceptSessionUserMessageWithOutcome>[0],
|
|
1959
|
+
grant: Parameters<typeof acceptSessionUserMessageWithOutcome>[1],
|
|
1960
|
+
workspaceId: Parameters<typeof acceptSessionUserMessageWithOutcome>[2],
|
|
1961
|
+
sessionId: Parameters<typeof acceptSessionUserMessageWithOutcome>[3],
|
|
1962
|
+
input: Parameters<typeof acceptSessionUserMessageWithOutcome>[4],
|
|
1963
|
+
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1964
|
+
const { accepted, turn } = await acceptSessionUserMessageWithOutcome(
|
|
1965
|
+
deps,
|
|
1966
|
+
grant,
|
|
1967
|
+
workspaceId,
|
|
1968
|
+
sessionId,
|
|
1969
|
+
input,
|
|
1970
|
+
);
|
|
1881
1971
|
return { accepted, turn };
|
|
1882
1972
|
}
|
|
1883
1973
|
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./workflow-wake-contract";
|
|
|
35
35
|
// structural TYPES live here.
|
|
36
36
|
export * from "./sandbox-types";
|
|
37
37
|
export * from "./managed-auth-type";
|
|
38
|
+
export * from "./managed-session";
|
|
38
39
|
export * from "./transcription";
|
|
39
40
|
|
|
40
41
|
// Sandbox fleet/routing service — the closure of `domain/sessions.ts`
|
package/src/managed-auth-type.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// @opengeni/core ManagedAuth TYPE alias.
|
|
2
2
|
//
|
|
3
3
|
// WHY THIS MODULE LIVES IN CORE: `dependencies.ts` carries a `managedAuth?:
|
|
4
|
-
// ManagedAuth | null` passthrough slot and the access layer
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// ManagedAuth | null` passthrough slot and the access layer resolves managed
|
|
5
|
+
// sessions through the cookie-renewing bridge in `managed-session.ts`. The
|
|
6
|
+
// auth type is framework-agnostic, so it belongs in @opengeni/core. The
|
|
7
|
+
// CONSTRUCTION of a real Better Auth
|
|
7
8
|
// instance — `createManagedAuth`, which opens its own `pg.Pool` and wires
|
|
8
9
|
// Resend — stays in `apps/api/src/auth/managed-auth.ts` (it pulls the `pg`
|
|
9
10
|
// driver, which must NEVER enter @opengeni/core).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { ManagedAuth } from "./managed-auth-type";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read a Better Auth session without bypassing its sliding-cookie renewal.
|
|
6
|
+
*
|
|
7
|
+
* Better Auth can refresh the durable session while resolving `getSession`.
|
|
8
|
+
* Programmatic callers must explicitly request and forward the returned cookie
|
|
9
|
+
* headers; the HTTP handler does this automatically, but direct API calls do not.
|
|
10
|
+
*/
|
|
11
|
+
export async function getManagedSession(c: Context, auth: ManagedAuth) {
|
|
12
|
+
const result = await auth.api.getSession({
|
|
13
|
+
headers: c.req.raw.headers,
|
|
14
|
+
returnHeaders: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
for (const cookie of setCookieHeaders(result.headers)) {
|
|
18
|
+
c.header("set-cookie", cookie, { append: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result.response;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setCookieHeaders(headers: Headers): string[] {
|
|
25
|
+
const getSetCookie = (
|
|
26
|
+
headers as Headers & {
|
|
27
|
+
getSetCookie?: () => string[];
|
|
28
|
+
}
|
|
29
|
+
).getSetCookie;
|
|
30
|
+
if (getSetCookie) {
|
|
31
|
+
return getSetCookie.call(headers);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cookie = headers.get("set-cookie");
|
|
35
|
+
return cookie ? [cookie] : [];
|
|
36
|
+
}
|