@opengeni/db 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/{chunk-OGCE6O2X.js → chunk-7LDU7F5P.js} +31 -3
  2. package/dist/chunk-7LDU7F5P.js.map +1 -0
  3. package/dist/chunk-B22X3IEZ.js +2634 -0
  4. package/dist/chunk-B22X3IEZ.js.map +1 -0
  5. package/dist/{chunk-57MLICFR.js → chunk-YFQ7SGE4.js} +18 -6
  6. package/dist/chunk-YFQ7SGE4.js.map +1 -0
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +16571 -5018
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.js +1 -1
  11. package/dist/provision-roles.d.ts +2121 -248
  12. package/dist/provision-roles.js +1 -1
  13. package/dist/{schema-BUbuMteO.d.ts → schema-BN5mB9xZ.d.ts} +8716 -3432
  14. package/dist/schema.d.ts +1 -1
  15. package/dist/schema.js +43 -5
  16. package/drizzle/0044_reap_dead_turn_holders.sql +104 -0
  17. package/drizzle/0045_workspace_captures.sql +83 -0
  18. package/drizzle/0045_workspace_memory_v1.sql +71 -0
  19. package/drizzle/0046_variable_sets_rename.sql +56 -0
  20. package/drizzle/0047_rigs.sql +151 -0
  21. package/drizzle/0048_rig_runtime.sql +9 -0
  22. package/drizzle/0049_enrollment_went_offline.sql +28 -0
  23. package/drizzle/0050_enrollment_op_stream.sql +1 -0
  24. package/drizzle/0051_codex_pin_source.sql +48 -0
  25. package/drizzle/0052_file_upload_cleanup.sql +91 -0
  26. package/drizzle/0053_codex_credential_leases.sql +230 -0
  27. package/drizzle/0054_session_pins.sql +85 -0
  28. package/drizzle/0055_session_list_snapshots.sql +73 -0
  29. package/drizzle/0056_workspace_model_policies.sql +48 -0
  30. package/drizzle/0057_durable_queue_control.sql +536 -0
  31. package/drizzle/0058_turn_admission_usage_enrollment.sql +158 -0
  32. package/drizzle/0059_workspace_pause_control_kind.sql +12 -0
  33. package/drizzle/0060_session_system_update_deferral.sql +10 -0
  34. package/drizzle/0061_session_workflow_wake_outbox.sql +157 -0
  35. package/drizzle/0062_session_list_snapshot_reaper.sql +48 -0
  36. package/drizzle/0063_session_control_mega_foundation.sql +1324 -0
  37. package/package.json +13 -13
  38. package/src/codex-token-resolver.ts +58 -23
  39. package/src/connection-token-resolver.ts +146 -57
  40. package/src/environment-crypto.ts +5 -1
  41. package/src/event-payload-sanitizer.ts +29 -1
  42. package/src/index.ts +20990 -6465
  43. package/src/memory-domain.ts +218 -0
  44. package/src/migrate.ts +58 -3
  45. package/src/provision-roles.ts +46 -17
  46. package/src/schema.ts +2888 -1121
  47. package/src/session-control.ts +1759 -0
  48. package/src/session-queue-commands.ts +1753 -0
  49. package/src/session-tool-call-settlement.ts +269 -0
  50. package/dist/chunk-57MLICFR.js.map +0 -1
  51. package/dist/chunk-OGCE6O2X.js.map +0 -1
  52. package/dist/chunk-ZIUCA2IO.js +0 -1268
  53. package/dist/chunk-ZIUCA2IO.js.map +0 -1
@@ -0,0 +1,1753 @@
1
+ import {
2
+ mergeResourceRefs,
3
+ mergeToolRefs,
4
+ type ReasoningEffort,
5
+ type ResourceRef,
6
+ type ToolRef,
7
+ } from "@opengeni/contracts";
8
+ import { and, asc, eq, inArray, sql } from "drizzle-orm";
9
+ import type { Database } from "./index";
10
+ import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
11
+ import {
12
+ assertAgentCommandAuthorityInTransaction,
13
+ autoResumeSessionBranchInTransaction,
14
+ canonicalSessionCommandHash,
15
+ evaluateSessionControl,
16
+ lockWorkspaceInferenceControl,
17
+ registerInternalUpdateWakeInTransaction,
18
+ reserveSessionCommandReceipt,
19
+ registerSessionWorkflowWakeInTransaction,
20
+ type SessionCommandActor,
21
+ type SessionCommandReceiptRow,
22
+ SessionControlConflictError,
23
+ SessionControlInvariantError,
24
+ updateSessionCommandReceiptResult,
25
+ } from "./session-control";
26
+ import * as schema from "./schema";
27
+
28
+ export type QueueCommandConflictCode =
29
+ | "QUEUE_VERSION_CHANGED"
30
+ | "QUEUE_PROMPT_STARTED"
31
+ | "QUEUE_ANCHOR_CHANGED"
32
+ | "PROMPT_CHANGED"
33
+ | "DRAFT_CHANGED"
34
+ | "DRAFT_NOT_EMPTY";
35
+
36
+ export class QueueCommandConflictError extends Error {
37
+ readonly name = "QueueCommandConflictError";
38
+
39
+ constructor(
40
+ readonly code: QueueCommandConflictCode,
41
+ message: string,
42
+ readonly current: {
43
+ queueVersion: number;
44
+ turnVersion?: number;
45
+ draftRevision?: number;
46
+ },
47
+ ) {
48
+ super(message);
49
+ }
50
+ }
51
+
52
+ export type ComposerDraftRow = typeof schema.composerDrafts.$inferSelect;
53
+ export type QueuedTurnRow = typeof schema.sessionTurns.$inferSelect;
54
+
55
+ export type QueueCommandResult = {
56
+ receipt: SessionCommandReceiptRow;
57
+ queueVersion: number;
58
+ items: QueuedTurnRow[];
59
+ eventIds: string[];
60
+ replay: boolean;
61
+ };
62
+
63
+ export type EditQueueCommandResult = QueueCommandResult & {
64
+ draft: ComposerDraftRow;
65
+ };
66
+
67
+ export type SteerQueueCommandResult = QueueCommandResult & {
68
+ interruptionCount: number;
69
+ workspaceControlEventId: string | null;
70
+ };
71
+
72
+ export type SubmitHumanPromptResult = {
73
+ receipt: SessionCommandReceiptRow;
74
+ queueVersion: number;
75
+ acceptedEventId: string;
76
+ eventIds: string[];
77
+ turnId: string;
78
+ wakeRevision: number;
79
+ interruptionCount: number;
80
+ workspaceControlEventId: string | null;
81
+ replay: boolean;
82
+ };
83
+
84
+ export type AgentInternalUpdateCommandResult = {
85
+ receipt: SessionCommandReceiptRow;
86
+ updateId: string;
87
+ eventIds: string[];
88
+ wakeRevision: number | null;
89
+ shouldSignal: boolean;
90
+ workflowId: string;
91
+ effectiveState: "active" | "paused";
92
+ interruptionCount: number;
93
+ workspaceControlEventId: string | null;
94
+ replay: boolean;
95
+ };
96
+
97
+ async function lockSession(
98
+ db: Database,
99
+ workspaceId: string,
100
+ sessionId: string,
101
+ ): Promise<typeof schema.sessions.$inferSelect> {
102
+ const [session] = await db
103
+ .select()
104
+ .from(schema.sessions)
105
+ .where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
106
+ .for("update")
107
+ .limit(1);
108
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
109
+ return session;
110
+ }
111
+
112
+ type SteerSupersessionResult = {
113
+ interruptionCount: number;
114
+ replacedTurn: typeof schema.sessionTurns.$inferSelect | null;
115
+ liveCurrentTurnId: string | null;
116
+ lastSequence: number;
117
+ };
118
+
119
+ /**
120
+ * One canonical replacement transition shared by human row/new-prompt Steer
121
+ * and Agent Steer. A live owner is interrupted and remains current until exact
122
+ * settlement; an ownerless approval/recovery/capacity turn is superseded now.
123
+ */
124
+ export async function supersedeSessionCurrentDirectionInTransaction(
125
+ db: Database,
126
+ input: {
127
+ accountId: string;
128
+ workspaceId: string;
129
+ sessionId: string;
130
+ activeTurnId: string | null;
131
+ actor: SessionCommandActor;
132
+ operationId: string;
133
+ controlRevision: number;
134
+ lastSequence: number;
135
+ },
136
+ ): Promise<SteerSupersessionResult> {
137
+ if (!input.activeTurnId) {
138
+ return {
139
+ interruptionCount: 0,
140
+ replacedTurn: null,
141
+ liveCurrentTurnId: null,
142
+ lastSequence: input.lastSequence,
143
+ };
144
+ }
145
+ const [current] = await db
146
+ .select()
147
+ .from(schema.sessionTurns)
148
+ .where(
149
+ and(
150
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
151
+ eq(schema.sessionTurns.sessionId, input.sessionId),
152
+ eq(schema.sessionTurns.id, input.activeTurnId),
153
+ ),
154
+ )
155
+ .for("update")
156
+ .limit(1);
157
+ if (!current) {
158
+ throw new SessionControlInvariantError(
159
+ `Session ${input.sessionId} points to missing active turn ${input.activeTurnId}`,
160
+ );
161
+ }
162
+ if (!["running", "requires_action", "recovering", "waiting_capacity"].includes(current.status)) {
163
+ throw new SessionControlInvariantError(
164
+ `Active turn ${current.id} cannot be Steered from ${current.status}`,
165
+ );
166
+ }
167
+ if (current.status === "running" && !current.activeAttemptId) {
168
+ throw new SessionControlInvariantError(
169
+ `Running turn ${current.id} has no first-class attempt owner`,
170
+ );
171
+ }
172
+ if (current.activeAttemptId) {
173
+ if (current.status !== "running") {
174
+ throw new SessionControlInvariantError(
175
+ `Live attempt ${current.activeAttemptId} owns non-running turn ${current.id}`,
176
+ );
177
+ }
178
+ const [interruption] = await db
179
+ .insert(schema.sessionAttemptInterruptions)
180
+ .values({
181
+ accountId: input.accountId,
182
+ workspaceId: input.workspaceId,
183
+ sessionId: input.sessionId,
184
+ operationId: input.operationId,
185
+ attemptId: current.activeAttemptId,
186
+ kind: "steer",
187
+ controlRevision: input.controlRevision,
188
+ })
189
+ .onConflictDoNothing()
190
+ .returning({ id: schema.sessionAttemptInterruptions.id });
191
+ return {
192
+ interruptionCount: interruption ? 1 : 0,
193
+ replacedTurn: current,
194
+ liveCurrentTurnId: current.id,
195
+ lastSequence: input.lastSequence,
196
+ };
197
+ }
198
+
199
+ const now = new Date();
200
+ const closedTools = await closePendingSessionToolCallsInTransaction(db, {
201
+ accountId: input.accountId,
202
+ workspaceId: input.workspaceId,
203
+ sessionId: input.sessionId,
204
+ turnId: current.id,
205
+ reason: "steer",
206
+ sequence: input.lastSequence,
207
+ now,
208
+ });
209
+ await db
210
+ .update(schema.sessionTurns)
211
+ .set({
212
+ status: "superseded",
213
+ cancelledBy:
214
+ input.actor.type === "agent_attempt"
215
+ ? `attempt:${input.actor.attemptId}`
216
+ : input.actor.subjectId,
217
+ cancelReason: "steer",
218
+ version: current.version + 1,
219
+ finishedAt: now,
220
+ updatedAt: now,
221
+ })
222
+ .where(eq(schema.sessionTurns.id, current.id));
223
+ await db
224
+ .update(schema.sessionSystemUpdates)
225
+ .set({ state: "pending", deliveredTurnId: null, deliveredAt: null })
226
+ .where(
227
+ and(
228
+ eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
229
+ eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
230
+ eq(schema.sessionSystemUpdates.deliveredTurnId, current.id),
231
+ eq(schema.sessionSystemUpdates.state, "delivered"),
232
+ ),
233
+ );
234
+ if (current.status === "waiting_capacity") {
235
+ await db
236
+ .update(schema.codexCapacityWaiters)
237
+ .set({ status: "superseded", updatedAt: now })
238
+ .where(
239
+ and(
240
+ eq(schema.codexCapacityWaiters.workspaceId, input.workspaceId),
241
+ eq(schema.codexCapacityWaiters.sessionId, input.sessionId),
242
+ eq(schema.codexCapacityWaiters.blockedTurnId, current.id),
243
+ eq(schema.codexCapacityWaiters.status, "waiting"),
244
+ ),
245
+ );
246
+ }
247
+ return {
248
+ interruptionCount: 0,
249
+ replacedTurn: current,
250
+ liveCurrentTurnId: null,
251
+ lastSequence: closedTools.sequence,
252
+ };
253
+ }
254
+
255
+ async function loadQueuedTurns(
256
+ db: Database,
257
+ workspaceId: string,
258
+ sessionId: string,
259
+ lock = false,
260
+ ): Promise<QueuedTurnRow[]> {
261
+ const query = db
262
+ .select()
263
+ .from(schema.sessionTurns)
264
+ .where(
265
+ and(
266
+ eq(schema.sessionTurns.workspaceId, workspaceId),
267
+ eq(schema.sessionTurns.sessionId, sessionId),
268
+ eq(schema.sessionTurns.status, "queued"),
269
+ inArray(schema.sessionTurns.source, ["user", "api"]),
270
+ ),
271
+ )
272
+ .orderBy(
273
+ asc(schema.sessionTurns.position),
274
+ asc(schema.sessionTurns.createdAt),
275
+ asc(schema.sessionTurns.id),
276
+ );
277
+ return lock ? await query.for("update") : await query;
278
+ }
279
+
280
+ async function normalizeQueuePositions(
281
+ db: Database,
282
+ workspaceId: string,
283
+ sessionId: string,
284
+ orderedIds: string[],
285
+ ): Promise<void> {
286
+ if (orderedIds.length > 0) {
287
+ const orderedValues = sql.join(
288
+ orderedIds.map((id, index) => sql`(${id}::uuid, ${index + 1}::bigint)`),
289
+ sql`, `,
290
+ );
291
+ await db.execute(sql`
292
+ with ordered(id, position) as (values ${orderedValues})
293
+ update ${schema.sessionTurns} turn
294
+ set position = ordered.position, updated_at = now()
295
+ from ordered
296
+ where turn.workspace_id = ${workspaceId}
297
+ and turn.session_id = ${sessionId}
298
+ and turn.id = ordered.id
299
+ and turn.status = 'queued'
300
+ `);
301
+ }
302
+ await db
303
+ .update(schema.sessions)
304
+ .set({
305
+ queueHeadPosition: 0,
306
+ queueTailPosition: orderedIds.length,
307
+ updatedAt: new Date(),
308
+ })
309
+ .where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)));
310
+ }
311
+
312
+ function draftIsNonEmpty(draft: ComposerDraftRow): boolean {
313
+ return (
314
+ draft.text.length > 0 ||
315
+ draft.resources.length > 0 ||
316
+ draft.tools.length > 0 ||
317
+ draft.sourceTurnId !== null
318
+ );
319
+ }
320
+
321
+ export async function getComposerDraftInTransaction(
322
+ db: Database,
323
+ input: { workspaceId: string; sessionId: string; subjectId: string; lock?: boolean },
324
+ ): Promise<ComposerDraftRow | null> {
325
+ const query = db
326
+ .select()
327
+ .from(schema.composerDrafts)
328
+ .where(
329
+ and(
330
+ eq(schema.composerDrafts.workspaceId, input.workspaceId),
331
+ eq(schema.composerDrafts.sessionId, input.sessionId),
332
+ eq(schema.composerDrafts.subjectId, input.subjectId),
333
+ ),
334
+ )
335
+ .limit(1);
336
+ const rows = input.lock ? await query.for("update") : await query;
337
+ return rows[0] ?? null;
338
+ }
339
+
340
+ export async function saveComposerDraftInTransaction(
341
+ db: Database,
342
+ input: {
343
+ accountId: string;
344
+ workspaceId: string;
345
+ sessionId: string;
346
+ subjectId: string;
347
+ expectedRevision: number;
348
+ text: string;
349
+ resources: ResourceRef[];
350
+ tools: ToolRef[];
351
+ model: string;
352
+ reasoningEffort: ReasoningEffort;
353
+ },
354
+ ): Promise<ComposerDraftRow> {
355
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
356
+ await lockSession(db, input.workspaceId, input.sessionId);
357
+ const current = await getComposerDraftInTransaction(db, { ...input, lock: true });
358
+ const currentRevision = current?.revision ?? 0;
359
+ if (currentRevision !== input.expectedRevision) {
360
+ throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
361
+ queueVersion: 0,
362
+ draftRevision: currentRevision,
363
+ });
364
+ }
365
+ const revision = currentRevision + 1;
366
+ const values = {
367
+ accountId: input.accountId,
368
+ workspaceId: input.workspaceId,
369
+ sessionId: input.sessionId,
370
+ subjectId: input.subjectId,
371
+ revision,
372
+ text: input.text,
373
+ resources: input.resources,
374
+ tools: input.tools,
375
+ model: input.model,
376
+ reasoningEffort: input.reasoningEffort,
377
+ sourceTurnId: null,
378
+ sourceTurnVersion: null,
379
+ updatedAt: new Date(),
380
+ };
381
+ const [saved] = current
382
+ ? await db
383
+ .update(schema.composerDrafts)
384
+ .set(values)
385
+ .where(eq(schema.composerDrafts.id, current.id))
386
+ .returning()
387
+ : await db.insert(schema.composerDrafts).values(values).returning();
388
+ if (!saved) throw new Error("Composer draft did not save");
389
+ return saved;
390
+ }
391
+
392
+ export async function moveQueuedTurnInTransaction(
393
+ db: Database,
394
+ input: {
395
+ accountId: string;
396
+ workspaceId: string;
397
+ sessionId: string;
398
+ turnId: string;
399
+ beforeTurnId: string | null;
400
+ expectedQueueVersion: number;
401
+ actor: SessionCommandActor;
402
+ operationKey: string;
403
+ },
404
+ ): Promise<QueueCommandResult> {
405
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
406
+ const session = await lockSession(db, input.workspaceId, input.sessionId);
407
+ const requestHash = canonicalSessionCommandHash({
408
+ beforeTurnId: input.beforeTurnId,
409
+ expectedQueueVersion: input.expectedQueueVersion,
410
+ });
411
+ const reserved = await reserveSessionCommandReceipt(db, {
412
+ accountId: input.accountId,
413
+ workspaceId: input.workspaceId,
414
+ actor: input.actor,
415
+ action: "queue.move",
416
+ targetSessionId: input.sessionId,
417
+ targetTurnId: input.turnId,
418
+ operationKey: input.operationKey,
419
+ canonicalRequestHash: requestHash,
420
+ });
421
+ if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
422
+ return {
423
+ receipt: reserved.receipt,
424
+ queueVersion: session.queueVersion,
425
+ items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
426
+ eventIds: [],
427
+ replay: true,
428
+ };
429
+ }
430
+ if (session.queueVersion !== input.expectedQueueVersion) {
431
+ throw new QueueCommandConflictError("QUEUE_VERSION_CHANGED", "Queue order changed", {
432
+ queueVersion: session.queueVersion,
433
+ });
434
+ }
435
+ const rows = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
436
+ const target = rows.find((row) => row.id === input.turnId);
437
+ if (!target) {
438
+ throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
439
+ queueVersion: session.queueVersion,
440
+ });
441
+ }
442
+ if (input.beforeTurnId === input.turnId) {
443
+ throw new QueueCommandConflictError(
444
+ "QUEUE_ANCHOR_CHANGED",
445
+ "Prompt cannot move before itself",
446
+ {
447
+ queueVersion: session.queueVersion,
448
+ turnVersion: target.version,
449
+ },
450
+ );
451
+ }
452
+ const withoutTarget = rows.filter((row) => row.id !== input.turnId);
453
+ const anchorIndex =
454
+ input.beforeTurnId === null
455
+ ? withoutTarget.length
456
+ : withoutTarget.findIndex((row) => row.id === input.beforeTurnId);
457
+ if (anchorIndex < 0) {
458
+ throw new QueueCommandConflictError("QUEUE_ANCHOR_CHANGED", "Queue anchor changed", {
459
+ queueVersion: session.queueVersion,
460
+ turnVersion: target.version,
461
+ });
462
+ }
463
+ const ordered = [...withoutTarget];
464
+ ordered.splice(anchorIndex, 0, target);
465
+ const changed = ordered.some((row, index) => row.id !== rows[index]?.id);
466
+ const queueVersion = changed ? session.queueVersion + 1 : session.queueVersion;
467
+ const eventIds: string[] = [];
468
+ if (changed) {
469
+ await normalizeQueuePositions(
470
+ db,
471
+ input.workspaceId,
472
+ input.sessionId,
473
+ ordered.map((row) => row.id),
474
+ );
475
+ const [event] = await db
476
+ .insert(schema.sessionEvents)
477
+ .values({
478
+ accountId: input.accountId,
479
+ workspaceId: input.workspaceId,
480
+ sessionId: input.sessionId,
481
+ sequence: session.lastSequence + 1,
482
+ type: "session.queue.changed",
483
+ turnId: target.id,
484
+ payload: {
485
+ operation: "move",
486
+ queueVersion,
487
+ turnId: target.id,
488
+ beforeTurnId: input.beforeTurnId,
489
+ },
490
+ occurredAt: new Date(),
491
+ })
492
+ .returning({ id: schema.sessionEvents.id });
493
+ if (!event) throw new Error("Queue move event was not inserted");
494
+ eventIds.push(event.id);
495
+ await db
496
+ .update(schema.sessions)
497
+ .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: new Date() })
498
+ .where(eq(schema.sessions.id, input.sessionId));
499
+ }
500
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
501
+ queueVersion,
502
+ turnVersion: target.version,
503
+ result: { changed, beforeTurnId: input.beforeTurnId },
504
+ });
505
+ return {
506
+ receipt,
507
+ queueVersion,
508
+ items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
509
+ eventIds,
510
+ replay: false,
511
+ };
512
+ }
513
+
514
+ export async function deleteSessionQueueItemInTransaction(
515
+ db: Database,
516
+ input: {
517
+ accountId: string;
518
+ workspaceId: string;
519
+ sessionId: string;
520
+ turnId: string;
521
+ expectedTurnVersion: number;
522
+ actor: SessionCommandActor;
523
+ operationKey: string;
524
+ reason?: string | null;
525
+ },
526
+ ): Promise<QueueCommandResult> {
527
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
528
+ const session = await lockSession(db, input.workspaceId, input.sessionId);
529
+ const requestHash = canonicalSessionCommandHash({
530
+ expectedTurnVersion: input.expectedTurnVersion,
531
+ reason: input.reason ?? null,
532
+ });
533
+ const reserved = await reserveSessionCommandReceipt(db, {
534
+ accountId: input.accountId,
535
+ workspaceId: input.workspaceId,
536
+ actor: input.actor,
537
+ action: "queue.delete",
538
+ targetSessionId: input.sessionId,
539
+ targetTurnId: input.turnId,
540
+ operationKey: input.operationKey,
541
+ canonicalRequestHash: requestHash,
542
+ });
543
+ if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
544
+ return {
545
+ receipt: reserved.receipt,
546
+ queueVersion: session.queueVersion,
547
+ items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
548
+ eventIds: [],
549
+ replay: true,
550
+ };
551
+ }
552
+ const [turn] = await db
553
+ .select()
554
+ .from(schema.sessionTurns)
555
+ .where(
556
+ and(
557
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
558
+ eq(schema.sessionTurns.sessionId, input.sessionId),
559
+ eq(schema.sessionTurns.id, input.turnId),
560
+ ),
561
+ )
562
+ .for("update")
563
+ .limit(1);
564
+ if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
565
+ throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
566
+ queueVersion: session.queueVersion,
567
+ ...(turn ? { turnVersion: turn.version } : {}),
568
+ });
569
+ }
570
+ if (turn.version !== input.expectedTurnVersion) {
571
+ throw new QueueCommandConflictError("PROMPT_CHANGED", "Prompt changed", {
572
+ queueVersion: session.queueVersion,
573
+ turnVersion: turn.version,
574
+ });
575
+ }
576
+ const now = new Date();
577
+ const queueVersion = session.queueVersion + 1;
578
+ await db
579
+ .update(schema.sessionTurns)
580
+ .set({
581
+ status: "cancelled",
582
+ cancelledBy:
583
+ input.actor.type === "agent_attempt"
584
+ ? `attempt:${input.actor.attemptId}`
585
+ : input.actor.subjectId,
586
+ cancelReason: input.reason ?? "human_delete",
587
+ version: turn.version + 1,
588
+ finishedAt: now,
589
+ updatedAt: now,
590
+ })
591
+ .where(eq(schema.sessionTurns.id, turn.id));
592
+ const remaining = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
593
+ await normalizeQueuePositions(
594
+ db,
595
+ input.workspaceId,
596
+ input.sessionId,
597
+ remaining.map((row) => row.id),
598
+ );
599
+ const [event] = await db
600
+ .insert(schema.sessionEvents)
601
+ .values({
602
+ accountId: input.accountId,
603
+ workspaceId: input.workspaceId,
604
+ sessionId: input.sessionId,
605
+ sequence: session.lastSequence + 1,
606
+ type: "session.queue.changed",
607
+ turnId: turn.id,
608
+ payload: {
609
+ operation: "delete",
610
+ queueVersion,
611
+ turnId: turn.id,
612
+ },
613
+ occurredAt: now,
614
+ })
615
+ .returning({ id: schema.sessionEvents.id });
616
+ if (!event) throw new Error("Queue delete event was not inserted");
617
+ await db
618
+ .update(schema.sessions)
619
+ .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
620
+ .where(eq(schema.sessions.id, input.sessionId));
621
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
622
+ queueVersion,
623
+ turnVersion: turn.version + 1,
624
+ result: { reason: input.reason ?? "human_delete" },
625
+ });
626
+ return {
627
+ receipt,
628
+ queueVersion,
629
+ items: remaining,
630
+ eventIds: [event.id],
631
+ replay: false,
632
+ };
633
+ }
634
+
635
+ export async function editQueuedTurnInTransaction(
636
+ db: Database,
637
+ input: {
638
+ accountId: string;
639
+ workspaceId: string;
640
+ sessionId: string;
641
+ turnId: string;
642
+ subjectId: string;
643
+ expectedTurnVersion: number;
644
+ expectedDraftRevision: number;
645
+ replaceDraft: boolean;
646
+ actor: SessionCommandActor;
647
+ operationKey: string;
648
+ },
649
+ ): Promise<EditQueueCommandResult> {
650
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
651
+ const session = await lockSession(db, input.workspaceId, input.sessionId);
652
+ const requestHash = canonicalSessionCommandHash({
653
+ expectedTurnVersion: input.expectedTurnVersion,
654
+ expectedDraftRevision: input.expectedDraftRevision,
655
+ replaceDraft: input.replaceDraft,
656
+ });
657
+ const reserved = await reserveSessionCommandReceipt(db, {
658
+ accountId: input.accountId,
659
+ workspaceId: input.workspaceId,
660
+ actor: input.actor,
661
+ action: "queue.edit",
662
+ targetSessionId: input.sessionId,
663
+ targetTurnId: input.turnId,
664
+ operationKey: input.operationKey,
665
+ canonicalRequestHash: requestHash,
666
+ });
667
+ const existingDraft = await getComposerDraftInTransaction(db, { ...input, lock: true });
668
+ if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
669
+ if (!existingDraft) throw new Error("Replayed queue Edit has no durable draft");
670
+ return {
671
+ receipt: reserved.receipt,
672
+ queueVersion: session.queueVersion,
673
+ items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
674
+ draft: existingDraft,
675
+ eventIds: [],
676
+ replay: true,
677
+ };
678
+ }
679
+ const draftRevision = existingDraft?.revision ?? 0;
680
+ if (draftRevision !== input.expectedDraftRevision) {
681
+ throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
682
+ queueVersion: session.queueVersion,
683
+ draftRevision,
684
+ });
685
+ }
686
+ if (existingDraft && draftIsNonEmpty(existingDraft) && !input.replaceDraft) {
687
+ throw new QueueCommandConflictError("DRAFT_NOT_EMPTY", "Composer draft is not empty", {
688
+ queueVersion: session.queueVersion,
689
+ draftRevision,
690
+ });
691
+ }
692
+ const [turn] = await db
693
+ .select()
694
+ .from(schema.sessionTurns)
695
+ .where(
696
+ and(
697
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
698
+ eq(schema.sessionTurns.sessionId, input.sessionId),
699
+ eq(schema.sessionTurns.id, input.turnId),
700
+ ),
701
+ )
702
+ .for("update")
703
+ .limit(1);
704
+ if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
705
+ throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
706
+ queueVersion: session.queueVersion,
707
+ draftRevision,
708
+ ...(turn ? { turnVersion: turn.version } : {}),
709
+ });
710
+ }
711
+ if (turn.version !== input.expectedTurnVersion) {
712
+ throw new QueueCommandConflictError("PROMPT_CHANGED", "Prompt changed", {
713
+ queueVersion: session.queueVersion,
714
+ turnVersion: turn.version,
715
+ draftRevision,
716
+ });
717
+ }
718
+ const nextDraftRevision = draftRevision + 1;
719
+ const draftValues = {
720
+ accountId: input.accountId,
721
+ workspaceId: input.workspaceId,
722
+ sessionId: input.sessionId,
723
+ subjectId: input.subjectId,
724
+ revision: nextDraftRevision,
725
+ text: turn.prompt,
726
+ resources: turn.resources,
727
+ tools: turn.tools,
728
+ model: turn.model,
729
+ reasoningEffort: turn.reasoningEffort,
730
+ sourceTurnId: turn.id,
731
+ sourceTurnVersion: turn.version,
732
+ updatedAt: new Date(),
733
+ };
734
+ const [draft] = existingDraft
735
+ ? await db
736
+ .update(schema.composerDrafts)
737
+ .set(draftValues)
738
+ .where(eq(schema.composerDrafts.id, existingDraft.id))
739
+ .returning()
740
+ : await db.insert(schema.composerDrafts).values(draftValues).returning();
741
+ if (!draft) throw new Error("Queue Edit did not persist its draft");
742
+ const now = new Date();
743
+ const queueVersion = session.queueVersion + 1;
744
+ await db
745
+ .update(schema.sessionTurns)
746
+ .set({
747
+ status: "withdrawn_for_edit",
748
+ cancelledBy: input.subjectId,
749
+ cancelReason: "withdrawn_for_edit",
750
+ version: turn.version + 1,
751
+ finishedAt: now,
752
+ updatedAt: now,
753
+ })
754
+ .where(eq(schema.sessionTurns.id, turn.id));
755
+ const remaining = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
756
+ await normalizeQueuePositions(
757
+ db,
758
+ input.workspaceId,
759
+ input.sessionId,
760
+ remaining.map((row) => row.id),
761
+ );
762
+ const [event] = await db
763
+ .insert(schema.sessionEvents)
764
+ .values({
765
+ accountId: input.accountId,
766
+ workspaceId: input.workspaceId,
767
+ sessionId: input.sessionId,
768
+ sequence: session.lastSequence + 1,
769
+ type: "session.queue.changed",
770
+ turnId: turn.id,
771
+ payload: {
772
+ operation: "edit",
773
+ queueVersion,
774
+ turnId: turn.id,
775
+ draftRevision: nextDraftRevision,
776
+ },
777
+ occurredAt: now,
778
+ })
779
+ .returning({ id: schema.sessionEvents.id });
780
+ if (!event) throw new Error("Queue edit event was not inserted");
781
+ await db
782
+ .update(schema.sessions)
783
+ .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
784
+ .where(eq(schema.sessions.id, input.sessionId));
785
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
786
+ queueVersion,
787
+ turnVersion: turn.version + 1,
788
+ draftRevision: nextDraftRevision,
789
+ result: { sourceTurnId: turn.id },
790
+ });
791
+ return { receipt, queueVersion, items: remaining, draft, eventIds: [event.id], replay: false };
792
+ }
793
+
794
+ export async function steerQueuedTurnInTransaction(
795
+ db: Database,
796
+ input: {
797
+ accountId: string;
798
+ workspaceId: string;
799
+ sessionId: string;
800
+ turnId: string;
801
+ expectedTurnVersion: number;
802
+ controlEtag?: string | null;
803
+ actor: SessionCommandActor;
804
+ operationKey: string;
805
+ },
806
+ ): Promise<SteerQueueCommandResult> {
807
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
808
+ const requestHash = canonicalSessionCommandHash({
809
+ expectedTurnVersion: input.expectedTurnVersion,
810
+ controlEtag: input.controlEtag ?? null,
811
+ });
812
+ const reserved = await reserveSessionCommandReceipt(db, {
813
+ accountId: input.accountId,
814
+ workspaceId: input.workspaceId,
815
+ actor: input.actor,
816
+ action: "queue.steer",
817
+ targetSessionId: input.sessionId,
818
+ targetTurnId: input.turnId,
819
+ operationKey: input.operationKey,
820
+ canonicalRequestHash: requestHash,
821
+ });
822
+ if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
823
+ const replaySession = await lockSession(db, input.workspaceId, input.sessionId);
824
+ return {
825
+ receipt: reserved.receipt,
826
+ queueVersion: replaySession.queueVersion,
827
+ items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
828
+ eventIds: [],
829
+ interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
830
+ workspaceControlEventId:
831
+ typeof reserved.receipt.result.workspaceControlEventId === "string"
832
+ ? reserved.receipt.result.workspaceControlEventId
833
+ : null,
834
+ replay: true,
835
+ };
836
+ }
837
+ if (input.actor.type === "agent_attempt") {
838
+ await assertAgentCommandAuthorityInTransaction(db, {
839
+ workspaceId: input.workspaceId,
840
+ actor: input.actor,
841
+ targetSessionId: input.sessionId,
842
+ action: "steer",
843
+ });
844
+ }
845
+ const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
846
+ lock: "share",
847
+ });
848
+ if (input.controlEtag && input.controlEtag !== before.controlEtag) {
849
+ throw new SessionControlConflictError();
850
+ }
851
+ const resumed = await autoResumeSessionBranchInTransaction(db, {
852
+ workspaceId: input.workspaceId,
853
+ sessionId: input.sessionId,
854
+ actor:
855
+ input.actor.type === "agent_attempt"
856
+ ? `attempt:${input.actor.attemptId}`
857
+ : input.actor.subjectId,
858
+ reason: "human_steer",
859
+ observedControlEtag: input.controlEtag ?? null,
860
+ });
861
+ const session = await lockSession(db, input.workspaceId, input.sessionId);
862
+ const rows = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
863
+ const target = rows.find((row) => row.id === input.turnId);
864
+ if (!target) {
865
+ throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
866
+ queueVersion: session.queueVersion,
867
+ });
868
+ }
869
+ if (target.version !== input.expectedTurnVersion) {
870
+ throw new QueueCommandConflictError("PROMPT_CHANGED", "Prompt changed", {
871
+ queueVersion: session.queueVersion,
872
+ turnVersion: target.version,
873
+ });
874
+ }
875
+
876
+ const supersession = await supersedeSessionCurrentDirectionInTransaction(db, {
877
+ accountId: input.accountId,
878
+ workspaceId: input.workspaceId,
879
+ sessionId: input.sessionId,
880
+ activeTurnId: session.activeTurnId,
881
+ actor: input.actor,
882
+ operationId: reserved.receipt.id,
883
+ controlRevision: resumed.revision,
884
+ lastSequence: session.lastSequence,
885
+ });
886
+ const interruptionCount = supersession.interruptionCount;
887
+ const supersededTurnId = supersession.replacedTurn?.id ?? null;
888
+ const liveCurrentTurnId = supersession.liveCurrentTurnId;
889
+
890
+ const withoutTarget = rows.filter((row) => row.id !== target.id);
891
+ const ordered = [target, ...withoutTarget];
892
+ await normalizeQueuePositions(
893
+ db,
894
+ input.workspaceId,
895
+ input.sessionId,
896
+ ordered.map((row) => row.id),
897
+ );
898
+ const now = new Date();
899
+ const queueVersion = session.queueVersion + 1;
900
+ await db
901
+ .update(schema.sessionTurns)
902
+ .set({ version: target.version + 1, updatedAt: now })
903
+ .where(eq(schema.sessionTurns.id, target.id));
904
+ let sequence = supersession.lastSequence;
905
+ const actor =
906
+ input.actor.type === "agent_attempt"
907
+ ? `attempt:${input.actor.attemptId}`
908
+ : input.actor.subjectId;
909
+ const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = [];
910
+ if (supersededTurnId && !liveCurrentTurnId) {
911
+ eventValues.push({
912
+ accountId: input.accountId,
913
+ workspaceId: input.workspaceId,
914
+ sessionId: input.sessionId,
915
+ sequence: ++sequence,
916
+ type: "turn.superseded",
917
+ turnId: supersededTurnId,
918
+ payload: { reason: "steer", targetTurnId: target.id },
919
+ occurredAt: now,
920
+ });
921
+ }
922
+ eventValues.push({
923
+ accountId: input.accountId,
924
+ workspaceId: input.workspaceId,
925
+ sessionId: input.sessionId,
926
+ sequence: ++sequence,
927
+ type: "session.control.steer_requested",
928
+ turnId: supersededTurnId ?? target.id,
929
+ payload: {
930
+ operationId: reserved.receipt.id,
931
+ targetTurnId: target.id,
932
+ replacedTurnId: supersededTurnId,
933
+ stopping: liveCurrentTurnId !== null,
934
+ },
935
+ occurredAt: now,
936
+ });
937
+ const eventRows = await db.insert(schema.sessionEvents).values(eventValues).returning({
938
+ id: schema.sessionEvents.id,
939
+ });
940
+ await db
941
+ .update(schema.sessions)
942
+ .set({
943
+ activeTurnId: liveCurrentTurnId,
944
+ status: liveCurrentTurnId ? session.status : "queued",
945
+ queueVersion,
946
+ queueHeadPosition: 0,
947
+ queueTailPosition: ordered.length,
948
+ lastSequence: sequence,
949
+ updatedAt: now,
950
+ })
951
+ .where(eq(schema.sessions.id, input.sessionId));
952
+ await db.insert(schema.auditEvents).values({
953
+ accountId: input.accountId,
954
+ workspaceId: input.workspaceId,
955
+ subjectId: actor,
956
+ action: "session.queue.steer",
957
+ targetType: "session_turn",
958
+ targetId: target.id,
959
+ metadata: {
960
+ operationId: reserved.receipt.id,
961
+ replacedTurnId: supersededTurnId,
962
+ interruptionCount,
963
+ },
964
+ });
965
+ const wakeRevision = await registerSessionWorkflowWakeInTransaction(db, {
966
+ accountId: input.accountId,
967
+ workspaceId: input.workspaceId,
968
+ sessionId: input.sessionId,
969
+ temporalWorkflowId: session.temporalWorkflowId ?? `session-${input.sessionId}`,
970
+ reason: "queue_steer",
971
+ });
972
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
973
+ controlRevision: resumed.revision,
974
+ queueVersion,
975
+ turnVersion: target.version + 1,
976
+ result: {
977
+ interruptionCount,
978
+ supersededTurnId,
979
+ wakeRevision,
980
+ workspaceControlEventId: resumed.workspaceControlEventId,
981
+ },
982
+ });
983
+ return {
984
+ receipt,
985
+ queueVersion,
986
+ items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
987
+ eventIds: eventRows.map((event) => event.id),
988
+ interruptionCount,
989
+ workspaceControlEventId: resumed.workspaceControlEventId,
990
+ replay: false,
991
+ };
992
+ }
993
+
994
+ export async function submitHumanPromptInTransaction(
995
+ db: Database,
996
+ input: {
997
+ accountId: string;
998
+ workspaceId: string;
999
+ sessionId: string;
1000
+ subjectId: string;
1001
+ actor: SessionCommandActor;
1002
+ operationKey: string;
1003
+ delivery: "send" | "steer";
1004
+ controlEtag?: string | null;
1005
+ expectedDraftRevision?: number | null;
1006
+ text: string;
1007
+ resources: ResourceRef[];
1008
+ tools: ToolRef[];
1009
+ model?: string | null;
1010
+ reasoningEffort?: ReasoningEffort | null;
1011
+ reasoningEffortFallback: ReasoningEffort;
1012
+ source: "user" | "api";
1013
+ mcpCredentialUpdates?: Array<{ id: string; headersEncrypted: Record<string, string> }>;
1014
+ },
1015
+ ): Promise<SubmitHumanPromptResult> {
1016
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1017
+ const requestHash = canonicalSessionCommandHash({
1018
+ delivery: input.delivery,
1019
+ controlEtag: input.controlEtag ?? null,
1020
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
1021
+ text: input.text,
1022
+ resources: input.resources,
1023
+ tools: input.tools,
1024
+ model: input.model ?? null,
1025
+ reasoningEffort: input.reasoningEffort ?? null,
1026
+ source: input.source,
1027
+ mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1028
+ });
1029
+ const reserved = await reserveSessionCommandReceipt(db, {
1030
+ accountId: input.accountId,
1031
+ workspaceId: input.workspaceId,
1032
+ actor: input.actor,
1033
+ action: input.delivery === "steer" ? "prompt.steer" : "prompt.send",
1034
+ targetSessionId: input.sessionId,
1035
+ targetTurnId: null,
1036
+ operationKey: input.operationKey,
1037
+ canonicalRequestHash: requestHash,
1038
+ });
1039
+ if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
1040
+ const turnId = String(reserved.receipt.result.turnId ?? "");
1041
+ const acceptedEventId = String(reserved.receipt.result.acceptedEventId ?? "");
1042
+ const eventIds = Array.isArray(reserved.receipt.result.eventIds)
1043
+ ? reserved.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
1044
+ : [];
1045
+ const wakeRevision = Number(reserved.receipt.result.wakeRevision ?? 0);
1046
+ if (!turnId || !acceptedEventId || wakeRevision < 1) {
1047
+ throw new SessionControlInvariantError("Replayed prompt receipt is incomplete");
1048
+ }
1049
+ return {
1050
+ receipt: reserved.receipt,
1051
+ queueVersion: Number(reserved.receipt.appliedQueueVersion),
1052
+ acceptedEventId,
1053
+ eventIds,
1054
+ turnId,
1055
+ wakeRevision,
1056
+ interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
1057
+ workspaceControlEventId:
1058
+ typeof reserved.receipt.result.workspaceControlEventId === "string"
1059
+ ? reserved.receipt.result.workspaceControlEventId
1060
+ : null,
1061
+ replay: true,
1062
+ };
1063
+ }
1064
+
1065
+ const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1066
+ lock: "share",
1067
+ });
1068
+ if (input.controlEtag && input.controlEtag !== before.controlEtag) {
1069
+ throw new SessionControlConflictError();
1070
+ }
1071
+
1072
+ const resumed = await autoResumeSessionBranchInTransaction(db, {
1073
+ workspaceId: input.workspaceId,
1074
+ sessionId: input.sessionId,
1075
+ actor:
1076
+ input.actor.type === "agent_attempt"
1077
+ ? `attempt:${input.actor.attemptId}`
1078
+ : input.actor.subjectId,
1079
+ reason: input.delivery === "steer" ? "human_steer" : "human_send",
1080
+ observedControlEtag: input.controlEtag ?? null,
1081
+ });
1082
+ const session = await lockSession(db, input.workspaceId, input.sessionId);
1083
+ if (session.status === "cancelled") {
1084
+ throw new QueueCommandConflictError(
1085
+ "QUEUE_PROMPT_STARTED",
1086
+ "Cancelled session cannot accept work",
1087
+ {
1088
+ queueVersion: session.queueVersion,
1089
+ },
1090
+ );
1091
+ }
1092
+
1093
+ const draft =
1094
+ input.expectedDraftRevision === null || input.expectedDraftRevision === undefined
1095
+ ? null
1096
+ : await getComposerDraftInTransaction(db, {
1097
+ workspaceId: input.workspaceId,
1098
+ sessionId: input.sessionId,
1099
+ subjectId: input.subjectId,
1100
+ lock: true,
1101
+ });
1102
+ if (input.expectedDraftRevision !== null && input.expectedDraftRevision !== undefined) {
1103
+ const actualRevision = draft?.revision ?? 0;
1104
+ if (actualRevision !== input.expectedDraftRevision) {
1105
+ throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
1106
+ queueVersion: session.queueVersion,
1107
+ draftRevision: actualRevision,
1108
+ });
1109
+ }
1110
+ if (
1111
+ draft &&
1112
+ canonicalSessionCommandHash({
1113
+ text: draft.text,
1114
+ resources: draft.resources,
1115
+ tools: draft.tools,
1116
+ model: draft.model,
1117
+ reasoningEffort: draft.reasoningEffort,
1118
+ }) !==
1119
+ canonicalSessionCommandHash({
1120
+ text: input.text,
1121
+ resources: input.resources,
1122
+ tools: input.tools,
1123
+ model: input.model ?? session.model,
1124
+ reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
1125
+ })
1126
+ ) {
1127
+ throw new QueueCommandConflictError(
1128
+ "DRAFT_CHANGED",
1129
+ "Submitted content is not the saved draft",
1130
+ {
1131
+ queueVersion: session.queueVersion,
1132
+ draftRevision: draft.revision,
1133
+ },
1134
+ );
1135
+ }
1136
+ }
1137
+
1138
+ for (const update of input.mcpCredentialUpdates ?? []) {
1139
+ const [server] = await db
1140
+ .update(schema.sessionMcpServers)
1141
+ .set({
1142
+ headersEncrypted: update.headersEncrypted,
1143
+ credentialVersion: sql`${schema.sessionMcpServers.credentialVersion} + 1`,
1144
+ updatedAt: new Date(),
1145
+ })
1146
+ .where(
1147
+ and(
1148
+ eq(schema.sessionMcpServers.workspaceId, input.workspaceId),
1149
+ eq(schema.sessionMcpServers.sessionId, input.sessionId),
1150
+ eq(schema.sessionMcpServers.serverId, update.id),
1151
+ ),
1152
+ )
1153
+ .returning({ id: schema.sessionMcpServers.serverId });
1154
+ if (!server) throw new Error(`Unknown session MCP server: ${update.id}`);
1155
+ }
1156
+
1157
+ const now = new Date();
1158
+ const acceptedEventId = crypto.randomUUID();
1159
+ const turnId = crypto.randomUUID();
1160
+ const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
1161
+ let sequence = session.lastSequence;
1162
+ const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = [
1163
+ {
1164
+ id: acceptedEventId,
1165
+ accountId: input.accountId,
1166
+ workspaceId: input.workspaceId,
1167
+ sessionId: input.sessionId,
1168
+ sequence: ++sequence,
1169
+ type: "user.message",
1170
+ clientEventId: input.operationKey,
1171
+ payload: {
1172
+ text: input.text,
1173
+ ...(input.resources.length ? { resources: input.resources } : {}),
1174
+ ...(input.tools.length ? { tools: input.tools } : {}),
1175
+ ...(input.model ? { model: input.model } : {}),
1176
+ ...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
1177
+ delivery: input.delivery,
1178
+ },
1179
+ occurredAt: now,
1180
+ },
1181
+ ];
1182
+ const existingQueued = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
1183
+ const [turn] = await db
1184
+ .insert(schema.sessionTurns)
1185
+ .values({
1186
+ id: turnId,
1187
+ accountId: input.accountId,
1188
+ workspaceId: input.workspaceId,
1189
+ sessionId: input.sessionId,
1190
+ triggerEventId: acceptedEventId,
1191
+ temporalWorkflowId: workflowId,
1192
+ status: "queued",
1193
+ source: input.source,
1194
+ position: input.delivery === "steer" ? 0 : existingQueued.length + 1,
1195
+ prompt: input.text,
1196
+ resources: input.resources,
1197
+ tools: input.tools,
1198
+ model: input.model ?? session.model,
1199
+ reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
1200
+ sandboxBackend: session.sandboxBackend,
1201
+ metadata: {},
1202
+ lineage: { actor: input.actor.type },
1203
+ })
1204
+ .returning();
1205
+ if (!turn) throw new SessionControlInvariantError("Prompt turn was not inserted");
1206
+ eventValues.push({
1207
+ accountId: input.accountId,
1208
+ workspaceId: input.workspaceId,
1209
+ sessionId: input.sessionId,
1210
+ sequence: ++sequence,
1211
+ type: "turn.queued",
1212
+ turnId,
1213
+ payload: { turnId, triggerEventId: acceptedEventId, source: input.source },
1214
+ occurredAt: now,
1215
+ });
1216
+
1217
+ const supersession =
1218
+ input.delivery === "steer"
1219
+ ? await supersedeSessionCurrentDirectionInTransaction(db, {
1220
+ accountId: input.accountId,
1221
+ workspaceId: input.workspaceId,
1222
+ sessionId: input.sessionId,
1223
+ activeTurnId: session.activeTurnId,
1224
+ actor: input.actor,
1225
+ operationId: reserved.receipt.id,
1226
+ controlRevision: resumed.revision,
1227
+ lastSequence: session.lastSequence,
1228
+ })
1229
+ : {
1230
+ interruptionCount: 0,
1231
+ replacedTurn: null,
1232
+ liveCurrentTurnId: null,
1233
+ lastSequence: session.lastSequence,
1234
+ };
1235
+ // Ownerless Steer settlement may have appended interrupted tool results.
1236
+ // Rebase the not-yet-inserted foreground events after those canonical rows.
1237
+ sequence = supersession.lastSequence;
1238
+ for (const event of eventValues) event.sequence = ++sequence;
1239
+ const interruptionCount = supersession.interruptionCount;
1240
+ const replacedTurnId = supersession.replacedTurn?.id ?? null;
1241
+ const liveCurrentTurnId = supersession.liveCurrentTurnId;
1242
+ if (supersession.replacedTurn) {
1243
+ const current = supersession.replacedTurn;
1244
+ if (!liveCurrentTurnId) {
1245
+ eventValues.push({
1246
+ accountId: input.accountId,
1247
+ workspaceId: input.workspaceId,
1248
+ sessionId: input.sessionId,
1249
+ sequence: ++sequence,
1250
+ type: "turn.superseded",
1251
+ turnId: current.id,
1252
+ payload: { reason: "steer", targetTurnId: turnId },
1253
+ occurredAt: now,
1254
+ });
1255
+ }
1256
+ eventValues.push({
1257
+ accountId: input.accountId,
1258
+ workspaceId: input.workspaceId,
1259
+ sessionId: input.sessionId,
1260
+ sequence: ++sequence,
1261
+ type: "session.control.steer_requested",
1262
+ turnId: current.id,
1263
+ turnGeneration: current.executionGeneration,
1264
+ turnAttemptId: current.activeAttemptId,
1265
+ turnAssociation: "current",
1266
+ payload: {
1267
+ operationId: reserved.receipt.id,
1268
+ targetTurnId: turnId,
1269
+ replacedTurnId: current.id,
1270
+ stopping: liveCurrentTurnId !== null,
1271
+ },
1272
+ occurredAt: now,
1273
+ });
1274
+ }
1275
+
1276
+ const ordered =
1277
+ input.delivery === "steer" ? [turn, ...existingQueued] : [...existingQueued, turn];
1278
+ await normalizeQueuePositions(
1279
+ db,
1280
+ input.workspaceId,
1281
+ input.sessionId,
1282
+ ordered.map((row) => row.id),
1283
+ );
1284
+ const noCurrentAfter =
1285
+ input.delivery === "steer" ? liveCurrentTurnId === null : !session.activeTurnId;
1286
+ const nextStatus = noCurrentAfter ? "queued" : session.status;
1287
+ if (nextStatus !== session.status) {
1288
+ eventValues.push({
1289
+ accountId: input.accountId,
1290
+ workspaceId: input.workspaceId,
1291
+ sessionId: input.sessionId,
1292
+ sequence: ++sequence,
1293
+ type: "session.status.changed",
1294
+ payload: { status: nextStatus },
1295
+ occurredAt: now,
1296
+ });
1297
+ }
1298
+ const eventRows = await db.insert(schema.sessionEvents).values(eventValues).returning();
1299
+ const queueVersion = session.queueVersion + 1;
1300
+ await db
1301
+ .update(schema.sessions)
1302
+ .set({
1303
+ resources: mergeResourceRefs(session.resources as ResourceRef[], input.resources),
1304
+ tools: mergeToolRefs(session.tools as ToolRef[], input.tools),
1305
+ activeTurnId: input.delivery === "steer" ? liveCurrentTurnId : session.activeTurnId,
1306
+ status: nextStatus,
1307
+ queueVersion,
1308
+ queueHeadPosition: 0,
1309
+ queueTailPosition: ordered.length,
1310
+ lastSequence: sequence,
1311
+ updatedAt: now,
1312
+ })
1313
+ .where(eq(schema.sessions.id, input.sessionId));
1314
+ if (draft) {
1315
+ await db.delete(schema.composerDrafts).where(eq(schema.composerDrafts.id, draft.id));
1316
+ }
1317
+ const wakeRevision = await registerSessionWorkflowWakeInTransaction(db, {
1318
+ accountId: input.accountId,
1319
+ workspaceId: input.workspaceId,
1320
+ sessionId: input.sessionId,
1321
+ temporalWorkflowId: workflowId,
1322
+ reason: input.delivery === "steer" ? "prompt_steer" : "prompt_send",
1323
+ });
1324
+ await db.insert(schema.auditEvents).values({
1325
+ accountId: input.accountId,
1326
+ workspaceId: input.workspaceId,
1327
+ subjectId:
1328
+ input.actor.type === "agent_attempt"
1329
+ ? `attempt:${input.actor.attemptId}`
1330
+ : input.actor.subjectId,
1331
+ action: input.delivery === "steer" ? "session.prompt.steer" : "session.prompt.send",
1332
+ targetType: "session_turn",
1333
+ targetId: turnId,
1334
+ metadata: { operationId: reserved.receipt.id, replacedTurnId, interruptionCount },
1335
+ });
1336
+ const eventIds = eventRows.map((event) => event.id);
1337
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
1338
+ controlRevision: resumed.revision,
1339
+ queueVersion,
1340
+ turnVersion: turn.version,
1341
+ ...(draft ? { draftRevision: draft.revision } : {}),
1342
+ result: {
1343
+ turnId,
1344
+ acceptedEventId,
1345
+ eventIds,
1346
+ wakeRevision,
1347
+ interruptionCount,
1348
+ replacedTurnId,
1349
+ workspaceControlEventId: resumed.workspaceControlEventId,
1350
+ },
1351
+ });
1352
+ return {
1353
+ receipt,
1354
+ queueVersion,
1355
+ acceptedEventId,
1356
+ eventIds,
1357
+ turnId,
1358
+ wakeRevision,
1359
+ interruptionCount,
1360
+ workspaceControlEventId: resumed.workspaceControlEventId,
1361
+ replay: false,
1362
+ };
1363
+ }
1364
+
1365
+ export async function sendAgentMessageInTransaction(
1366
+ db: Database,
1367
+ input: {
1368
+ accountId: string;
1369
+ workspaceId: string;
1370
+ targetSessionId: string;
1371
+ actor: Extract<SessionCommandActor, { type: "agent_attempt" }>;
1372
+ operationKey: string;
1373
+ text: string;
1374
+ },
1375
+ ): Promise<AgentInternalUpdateCommandResult> {
1376
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
1377
+ const requestHash = canonicalSessionCommandHash({ text: input.text });
1378
+ const reserved = await reserveSessionCommandReceipt(db, {
1379
+ accountId: input.accountId,
1380
+ workspaceId: input.workspaceId,
1381
+ actor: input.actor,
1382
+ action: "agent.message",
1383
+ targetSessionId: input.targetSessionId,
1384
+ targetTurnId: null,
1385
+ operationKey: input.operationKey,
1386
+ canonicalRequestHash: requestHash,
1387
+ });
1388
+ if (reserved.replay) {
1389
+ const updateId = String(reserved.receipt.result.updateId ?? "");
1390
+ const workflowId = String(reserved.receipt.result.workflowId ?? "");
1391
+ if (!updateId || !workflowId) {
1392
+ throw new SessionControlInvariantError("Replayed Agent message receipt is incomplete");
1393
+ }
1394
+ return {
1395
+ receipt: reserved.receipt,
1396
+ updateId,
1397
+ eventIds: Array.isArray(reserved.receipt.result.eventIds)
1398
+ ? reserved.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
1399
+ : [],
1400
+ wakeRevision:
1401
+ typeof reserved.receipt.result.wakeRevision === "number"
1402
+ ? reserved.receipt.result.wakeRevision
1403
+ : null,
1404
+ shouldSignal: false,
1405
+ workflowId,
1406
+ effectiveState: reserved.receipt.result.effectiveState === "paused" ? "paused" : "active",
1407
+ interruptionCount: 0,
1408
+ workspaceControlEventId: null,
1409
+ replay: true,
1410
+ };
1411
+ }
1412
+ await assertAgentCommandAuthorityInTransaction(db, {
1413
+ workspaceId: input.workspaceId,
1414
+ actor: input.actor,
1415
+ targetSessionId: input.targetSessionId,
1416
+ action: "message",
1417
+ });
1418
+ const session = await lockSession(db, input.workspaceId, input.targetSessionId);
1419
+ if (session.status === "cancelled") {
1420
+ throw new QueueCommandConflictError(
1421
+ "QUEUE_PROMPT_STARTED",
1422
+ "Cancelled session cannot accept an Agent message",
1423
+ { queueVersion: session.queueVersion },
1424
+ );
1425
+ }
1426
+ const effective = await evaluateSessionControl(db, input.workspaceId, input.targetSessionId, {
1427
+ lock: "share",
1428
+ });
1429
+ const now = new Date();
1430
+ const [update] = await db
1431
+ .insert(schema.sessionSystemUpdates)
1432
+ .values({
1433
+ accountId: input.accountId,
1434
+ workspaceId: input.workspaceId,
1435
+ sessionId: input.targetSessionId,
1436
+ kind: "agent_message",
1437
+ classification: "info",
1438
+ sourceId: input.actor.sessionId,
1439
+ dedupeKey: `agent-message:${reserved.receipt.id}`,
1440
+ summary: input.text,
1441
+ payload: {
1442
+ type: "agent_message",
1443
+ text: input.text,
1444
+ operationId: reserved.receipt.id,
1445
+ },
1446
+ lineage: {
1447
+ callerSessionId: input.actor.sessionId,
1448
+ callerTurnId: input.actor.turnId,
1449
+ callerAttemptId: input.actor.attemptId,
1450
+ callerExecutionGeneration: input.actor.executionGeneration,
1451
+ },
1452
+ state: "pending",
1453
+ })
1454
+ .returning({ id: schema.sessionSystemUpdates.id });
1455
+ if (!update) throw new SessionControlInvariantError("Agent message was not inserted");
1456
+ const [event] = await db
1457
+ .insert(schema.sessionEvents)
1458
+ .values({
1459
+ accountId: input.accountId,
1460
+ workspaceId: input.workspaceId,
1461
+ sessionId: input.targetSessionId,
1462
+ sequence: session.lastSequence + 1,
1463
+ type: "system.update.pending",
1464
+ payload: {
1465
+ updateId: update.id,
1466
+ kind: "agent_message",
1467
+ sourceSessionId: input.actor.sessionId,
1468
+ },
1469
+ occurredAt: now,
1470
+ })
1471
+ .returning({ id: schema.sessionEvents.id });
1472
+ if (!event) throw new SessionControlInvariantError("Agent message event was not inserted");
1473
+ const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
1474
+ const runnable = session.activeTurnId === null && effective.state === "active";
1475
+ const wake = runnable
1476
+ ? await registerInternalUpdateWakeInTransaction(db, {
1477
+ accountId: input.accountId,
1478
+ workspaceId: input.workspaceId,
1479
+ sessionId: input.targetSessionId,
1480
+ temporalWorkflowId: workflowId,
1481
+ })
1482
+ : null;
1483
+ await db
1484
+ .update(schema.sessions)
1485
+ .set({
1486
+ lastSequence: session.lastSequence + 1,
1487
+ ...(runnable ? { status: "queued" as const } : {}),
1488
+ updatedAt: now,
1489
+ })
1490
+ .where(eq(schema.sessions.id, input.targetSessionId));
1491
+ await db.insert(schema.auditEvents).values({
1492
+ accountId: input.accountId,
1493
+ workspaceId: input.workspaceId,
1494
+ subjectId: `attempt:${input.actor.attemptId}`,
1495
+ action: "session.agent_message",
1496
+ targetType: "session",
1497
+ targetId: input.targetSessionId,
1498
+ metadata: {
1499
+ operationId: reserved.receipt.id,
1500
+ callerSessionId: input.actor.sessionId,
1501
+ callerTurnId: input.actor.turnId,
1502
+ callerAttemptId: input.actor.attemptId,
1503
+ callerExecutionGeneration: input.actor.executionGeneration,
1504
+ },
1505
+ });
1506
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
1507
+ result: {
1508
+ updateId: update.id,
1509
+ eventIds: [event.id],
1510
+ wakeRevision: wake?.wakeRevision ?? null,
1511
+ workflowId,
1512
+ effectiveState: effective.state,
1513
+ },
1514
+ });
1515
+ return {
1516
+ receipt,
1517
+ updateId: update.id,
1518
+ eventIds: [event.id],
1519
+ wakeRevision: wake?.wakeRevision ?? null,
1520
+ shouldSignal: wake?.shouldSignal ?? false,
1521
+ workflowId,
1522
+ effectiveState: effective.state,
1523
+ interruptionCount: 0,
1524
+ workspaceControlEventId: null,
1525
+ replay: false,
1526
+ };
1527
+ }
1528
+
1529
+ export async function steerAgentSessionInTransaction(
1530
+ db: Database,
1531
+ input: {
1532
+ accountId: string;
1533
+ workspaceId: string;
1534
+ targetSessionId: string;
1535
+ actor: Extract<SessionCommandActor, { type: "agent_attempt" }>;
1536
+ operationKey: string;
1537
+ instruction: string;
1538
+ },
1539
+ ): Promise<AgentInternalUpdateCommandResult> {
1540
+ await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1541
+ const reserved = await reserveSessionCommandReceipt(db, {
1542
+ accountId: input.accountId,
1543
+ workspaceId: input.workspaceId,
1544
+ actor: input.actor,
1545
+ action: "agent.steer",
1546
+ targetSessionId: input.targetSessionId,
1547
+ targetTurnId: null,
1548
+ operationKey: input.operationKey,
1549
+ canonicalRequestHash: canonicalSessionCommandHash({ instruction: input.instruction }),
1550
+ });
1551
+ if (reserved.replay) {
1552
+ const updateId = String(reserved.receipt.result.updateId ?? "");
1553
+ const workflowId = String(reserved.receipt.result.workflowId ?? "");
1554
+ const wakeRevision = Number(reserved.receipt.result.wakeRevision ?? 0);
1555
+ if (!updateId || !workflowId || wakeRevision < 1) {
1556
+ throw new SessionControlInvariantError("Replayed Agent Steer receipt is incomplete");
1557
+ }
1558
+ return {
1559
+ receipt: reserved.receipt,
1560
+ updateId,
1561
+ eventIds: Array.isArray(reserved.receipt.result.eventIds)
1562
+ ? reserved.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
1563
+ : [],
1564
+ wakeRevision,
1565
+ shouldSignal: false,
1566
+ workflowId,
1567
+ effectiveState: "active",
1568
+ interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
1569
+ workspaceControlEventId:
1570
+ typeof reserved.receipt.result.workspaceControlEventId === "string"
1571
+ ? reserved.receipt.result.workspaceControlEventId
1572
+ : null,
1573
+ replay: true,
1574
+ };
1575
+ }
1576
+ await assertAgentCommandAuthorityInTransaction(db, {
1577
+ workspaceId: input.workspaceId,
1578
+ actor: input.actor,
1579
+ targetSessionId: input.targetSessionId,
1580
+ action: "steer",
1581
+ });
1582
+ const resumed = await autoResumeSessionBranchInTransaction(db, {
1583
+ workspaceId: input.workspaceId,
1584
+ sessionId: input.targetSessionId,
1585
+ actor: `attempt:${input.actor.attemptId}`,
1586
+ reason: "agent_steer",
1587
+ });
1588
+ const session = await lockSession(db, input.workspaceId, input.targetSessionId);
1589
+ if (session.status === "cancelled") {
1590
+ throw new QueueCommandConflictError(
1591
+ "QUEUE_PROMPT_STARTED",
1592
+ "Cancelled session cannot be Steered",
1593
+ { queueVersion: session.queueVersion },
1594
+ );
1595
+ }
1596
+ const supersession = await supersedeSessionCurrentDirectionInTransaction(db, {
1597
+ accountId: input.accountId,
1598
+ workspaceId: input.workspaceId,
1599
+ sessionId: input.targetSessionId,
1600
+ activeTurnId: session.activeTurnId,
1601
+ actor: input.actor,
1602
+ operationId: reserved.receipt.id,
1603
+ controlRevision: resumed.revision,
1604
+ lastSequence: session.lastSequence,
1605
+ });
1606
+ await db
1607
+ .update(schema.sessionSystemUpdates)
1608
+ .set({ state: "superseded" })
1609
+ .where(
1610
+ and(
1611
+ eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
1612
+ eq(schema.sessionSystemUpdates.sessionId, input.targetSessionId),
1613
+ eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
1614
+ inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
1615
+ ),
1616
+ );
1617
+ const now = new Date();
1618
+ const [update] = await db
1619
+ .insert(schema.sessionSystemUpdates)
1620
+ .values({
1621
+ accountId: input.accountId,
1622
+ workspaceId: input.workspaceId,
1623
+ sessionId: input.targetSessionId,
1624
+ kind: "agent_steer_instruction",
1625
+ classification: "action_required",
1626
+ sourceId: input.actor.sessionId,
1627
+ dedupeKey: `agent-steer:${reserved.receipt.id}`,
1628
+ summary: input.instruction,
1629
+ payload: {
1630
+ type: "agent_steer_instruction",
1631
+ instruction: input.instruction,
1632
+ operationId: reserved.receipt.id,
1633
+ },
1634
+ lineage: {
1635
+ callerSessionId: input.actor.sessionId,
1636
+ callerTurnId: input.actor.turnId,
1637
+ callerAttemptId: input.actor.attemptId,
1638
+ callerExecutionGeneration: input.actor.executionGeneration,
1639
+ },
1640
+ state: "pending",
1641
+ })
1642
+ .returning({ id: schema.sessionSystemUpdates.id });
1643
+ if (!update) throw new SessionControlInvariantError("Agent Steer instruction was not inserted");
1644
+ let sequence = supersession.lastSequence;
1645
+ const events: Array<typeof schema.sessionEvents.$inferInsert> = [];
1646
+ if (supersession.replacedTurn && !supersession.liveCurrentTurnId) {
1647
+ events.push({
1648
+ accountId: input.accountId,
1649
+ workspaceId: input.workspaceId,
1650
+ sessionId: input.targetSessionId,
1651
+ sequence: ++sequence,
1652
+ type: "turn.superseded",
1653
+ turnId: supersession.replacedTurn.id,
1654
+ payload: { reason: "agent_steer", targetUpdateId: update.id },
1655
+ occurredAt: now,
1656
+ });
1657
+ }
1658
+ events.push(
1659
+ {
1660
+ accountId: input.accountId,
1661
+ workspaceId: input.workspaceId,
1662
+ sessionId: input.targetSessionId,
1663
+ sequence: ++sequence,
1664
+ type: "session.control.steer_requested",
1665
+ turnId: supersession.replacedTurn?.id ?? null,
1666
+ turnGeneration: supersession.replacedTurn?.executionGeneration ?? null,
1667
+ turnAttemptId: supersession.replacedTurn?.activeAttemptId ?? null,
1668
+ turnAssociation: supersession.replacedTurn ? "current" : null,
1669
+ payload: {
1670
+ operationId: reserved.receipt.id,
1671
+ targetUpdateId: update.id,
1672
+ replacedTurnId: supersession.replacedTurn?.id ?? null,
1673
+ actorSessionId: input.actor.sessionId,
1674
+ stopping: supersession.liveCurrentTurnId !== null,
1675
+ },
1676
+ occurredAt: now,
1677
+ },
1678
+ {
1679
+ accountId: input.accountId,
1680
+ workspaceId: input.workspaceId,
1681
+ sessionId: input.targetSessionId,
1682
+ sequence: ++sequence,
1683
+ type: "system.update.pending",
1684
+ payload: {
1685
+ updateId: update.id,
1686
+ kind: "agent_steer_instruction",
1687
+ sourceSessionId: input.actor.sessionId,
1688
+ },
1689
+ occurredAt: now,
1690
+ },
1691
+ );
1692
+ const insertedEvents = await db.insert(schema.sessionEvents).values(events).returning({
1693
+ id: schema.sessionEvents.id,
1694
+ });
1695
+ const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
1696
+ const wakeRevision = await registerSessionWorkflowWakeInTransaction(db, {
1697
+ accountId: input.accountId,
1698
+ workspaceId: input.workspaceId,
1699
+ sessionId: input.targetSessionId,
1700
+ temporalWorkflowId: workflowId,
1701
+ reason: "agent_steer",
1702
+ });
1703
+ await db
1704
+ .update(schema.sessions)
1705
+ .set({
1706
+ activeTurnId: supersession.liveCurrentTurnId,
1707
+ status: supersession.liveCurrentTurnId ? session.status : "queued",
1708
+ lastSequence: sequence,
1709
+ updatedAt: now,
1710
+ })
1711
+ .where(eq(schema.sessions.id, input.targetSessionId));
1712
+ await db.insert(schema.auditEvents).values({
1713
+ accountId: input.accountId,
1714
+ workspaceId: input.workspaceId,
1715
+ subjectId: `attempt:${input.actor.attemptId}`,
1716
+ action: "session.agent_steer",
1717
+ targetType: "session",
1718
+ targetId: input.targetSessionId,
1719
+ metadata: {
1720
+ operationId: reserved.receipt.id,
1721
+ callerSessionId: input.actor.sessionId,
1722
+ callerTurnId: input.actor.turnId,
1723
+ callerAttemptId: input.actor.attemptId,
1724
+ callerExecutionGeneration: input.actor.executionGeneration,
1725
+ controlRevision: resumed.revision,
1726
+ interruptionCount: supersession.interruptionCount,
1727
+ workspaceControlEventId: resumed.workspaceControlEventId,
1728
+ },
1729
+ });
1730
+ const eventIds = insertedEvents.map((event) => event.id);
1731
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
1732
+ controlRevision: resumed.revision,
1733
+ result: {
1734
+ updateId: update.id,
1735
+ eventIds,
1736
+ wakeRevision,
1737
+ workflowId,
1738
+ interruptionCount: supersession.interruptionCount,
1739
+ },
1740
+ });
1741
+ return {
1742
+ receipt,
1743
+ updateId: update.id,
1744
+ eventIds,
1745
+ wakeRevision,
1746
+ shouldSignal: true,
1747
+ workflowId,
1748
+ effectiveState: "active",
1749
+ interruptionCount: supersession.interruptionCount,
1750
+ workspaceControlEventId: resumed.workspaceControlEventId,
1751
+ replay: false,
1752
+ };
1753
+ }