@opengeni/db 0.7.0 → 0.7.2

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.
@@ -0,0 +1,1759 @@
1
+ import { createHash } from "node:crypto";
2
+ import { and, eq, sql } from "drizzle-orm";
3
+ import type { Database } from "./index";
4
+ import * as schema from "./schema";
5
+
6
+ export const SESSION_ANCESTRY_LIMIT = 10_000;
7
+
8
+ export type WorkspaceControlLockMode = "share" | "update";
9
+ export type EffectiveControlState = "active" | "paused";
10
+ export type SessionCommandActor =
11
+ | { type: "human" | "operator"; subjectId: string }
12
+ | {
13
+ type: "agent_attempt";
14
+ attemptId: string;
15
+ sessionId: string;
16
+ turnId: string;
17
+ executionGeneration: number;
18
+ };
19
+ export type SessionTurnAttemptOutcome =
20
+ | "completed"
21
+ | "failed"
22
+ | "cancelled"
23
+ | "superseded"
24
+ | "requires_action"
25
+ | "interrupted_recoverable"
26
+ | "lease_lost_recoverable"
27
+ | "pre_cutover_closed";
28
+
29
+ export type EffectiveControlBlocker = {
30
+ kind: "session" | "workspace";
31
+ sessionId?: string;
32
+ displayName: string;
33
+ actor: string | null;
34
+ reason: string | null;
35
+ changedAt: Date | null;
36
+ revision: number;
37
+ };
38
+
39
+ export type EffectiveControlResumeOption = {
40
+ scope: "selected" | "session" | "workspace";
41
+ targetId?: string;
42
+ selectedStateAfter: EffectiveControlState;
43
+ remainingPrimaryBlocker?: EffectiveControlBlocker;
44
+ impactCopy: string;
45
+ };
46
+
47
+ export type EffectiveSessionControl = {
48
+ state: EffectiveControlState;
49
+ controlVersion: number;
50
+ controlEtag: string;
51
+ directState: EffectiveControlState;
52
+ primaryBlocker: EffectiveControlBlocker | null;
53
+ additionalBlockerCount: number;
54
+ blockers: EffectiveControlBlocker[];
55
+ resumeOptions: EffectiveControlResumeOption[];
56
+ override: { rootSessionId: string; revision: number } | null;
57
+ settlement: { state: "stopping"; attemptCount: number } | null;
58
+ };
59
+
60
+ export function serializeEffectiveSessionControl(control: EffectiveSessionControl) {
61
+ const blocker = (
62
+ value: EffectiveControlBlocker,
63
+ ): Omit<EffectiveControlBlocker, "changedAt"> & {
64
+ changedAt: string | null;
65
+ } => ({
66
+ ...value,
67
+ changedAt: value.changedAt?.toISOString() ?? null,
68
+ });
69
+ return {
70
+ ...control,
71
+ primaryBlocker: control.primaryBlocker ? blocker(control.primaryBlocker) : null,
72
+ blockers: control.blockers.map(blocker),
73
+ resumeOptions: control.resumeOptions.map(({ remainingPrimaryBlocker, ...option }) => ({
74
+ ...option,
75
+ ...(remainingPrimaryBlocker
76
+ ? { remainingPrimaryBlocker: blocker(remainingPrimaryBlocker) }
77
+ : {}),
78
+ })),
79
+ };
80
+ }
81
+
82
+ type WorkspaceControlRow = {
83
+ workspaceId: string;
84
+ accountId: string;
85
+ revision: number | string;
86
+ workspaceState: string;
87
+ workspacePauseRevision: number | string | null;
88
+ reason: string | null;
89
+ changedBy: string | null;
90
+ changedAt: Date | string | null;
91
+ };
92
+
93
+ type AncestryRow = {
94
+ targetId: string;
95
+ sessionId: string;
96
+ parentSessionId: string | null;
97
+ title: string | null;
98
+ directState: string;
99
+ directPauseRevision: number | string | null;
100
+ subtreeRunOverrideRevision: number | string | null;
101
+ controlVersion: number | string;
102
+ directControlChangedBy: string | null;
103
+ directControlReason: string | null;
104
+ directControlChangedAt: Date | string | null;
105
+ depth: number | string;
106
+ cycle: boolean;
107
+ };
108
+
109
+ type AncestryNode = Omit<AncestryRow, "targetId" | "depth" | "cycle">;
110
+
111
+ export type SessionCommandReceiptRow = typeof schema.sessionCommandReceipts.$inferSelect;
112
+
113
+ export class SessionControlInvariantError extends Error {
114
+ readonly code = "SESSION_CONTROL_INVARIANT";
115
+
116
+ constructor(message: string) {
117
+ super(message);
118
+ this.name = "SessionControlInvariantError";
119
+ }
120
+ }
121
+
122
+ export class SessionControlConflictError extends Error {
123
+ readonly code = "CONTROL_CHANGED";
124
+
125
+ constructor(message = "The workstream control changed") {
126
+ super(message);
127
+ this.name = "SessionControlConflictError";
128
+ }
129
+ }
130
+
131
+ export class SessionCommandIdempotencyError extends Error {
132
+ readonly code = "IDEMPOTENCY_KEY_REUSED";
133
+
134
+ constructor() {
135
+ super("The operation key was already used with different input");
136
+ this.name = "SessionCommandIdempotencyError";
137
+ }
138
+ }
139
+
140
+ export class AgentCommandAuthorityError extends Error {
141
+ constructor(
142
+ readonly code: "CALLER_STALE" | "CALLER_INTERRUPTED" | "SELF_OR_ANCESTOR_PAUSE" | "SELF_STEER",
143
+ message: string,
144
+ ) {
145
+ super(message);
146
+ this.name = "AgentCommandAuthorityError";
147
+ }
148
+ }
149
+
150
+ export async function assertAgentCommandAuthorityInTransaction(
151
+ db: Database,
152
+ input: {
153
+ workspaceId: string;
154
+ actor: Extract<SessionCommandActor, { type: "agent_attempt" }>;
155
+ targetSessionId: string;
156
+ action: "pause" | "resume" | "steer" | "message";
157
+ },
158
+ ): Promise<void> {
159
+ const lockedSessions = await db
160
+ .select({ id: schema.sessions.id })
161
+ .from(schema.sessions)
162
+ .where(
163
+ and(
164
+ eq(schema.sessions.workspaceId, input.workspaceId),
165
+ sql`${schema.sessions.id} in (${input.actor.sessionId}::uuid, ${input.targetSessionId}::uuid)`,
166
+ ),
167
+ )
168
+ .orderBy(schema.sessions.id)
169
+ .for("update");
170
+ if (!lockedSessions.some((row) => row.id === input.actor.sessionId)) {
171
+ throw new AgentCommandAuthorityError("CALLER_STALE", "The calling session no longer exists");
172
+ }
173
+ if (!lockedSessions.some((row) => row.id === input.targetSessionId)) {
174
+ throw new SessionControlInvariantError(`Target session not found: ${input.targetSessionId}`);
175
+ }
176
+ const rows = await db.execute<{
177
+ attemptId: string;
178
+ attemptState: string;
179
+ attemptSessionId: string;
180
+ attemptTurnId: string;
181
+ executionGeneration: number;
182
+ activeAttemptId: string | null;
183
+ turnStatus: string;
184
+ activeTurnId: string | null;
185
+ interrupted: boolean;
186
+ }>(sql`
187
+ select
188
+ attempt.id as "attemptId",
189
+ attempt.state as "attemptState",
190
+ attempt.session_id as "attemptSessionId",
191
+ attempt.turn_id as "attemptTurnId",
192
+ attempt.execution_generation as "executionGeneration",
193
+ turn.active_attempt_id as "activeAttemptId",
194
+ turn.status as "turnStatus",
195
+ session.active_turn_id as "activeTurnId",
196
+ exists (
197
+ select 1
198
+ from ${schema.sessionAttemptInterruptions} interruption
199
+ where interruption.workspace_id = ${input.workspaceId}
200
+ and interruption.attempt_id = attempt.id
201
+ and interruption.state in ('pending', 'delivered', 'acknowledged')
202
+ ) as interrupted
203
+ from ${schema.sessionTurnAttempts} attempt
204
+ join ${schema.sessionTurns} turn
205
+ on turn.workspace_id = attempt.workspace_id and turn.id = attempt.turn_id
206
+ join ${schema.sessions} session
207
+ on session.workspace_id = attempt.workspace_id and session.id = attempt.session_id
208
+ where attempt.workspace_id = ${input.workspaceId}
209
+ and attempt.id = ${input.actor.attemptId}
210
+ for update of attempt, turn
211
+ `);
212
+ const caller = rows[0];
213
+ if (
214
+ !caller ||
215
+ !["claimed", "running"].includes(caller.attemptState) ||
216
+ caller.attemptSessionId !== input.actor.sessionId ||
217
+ caller.attemptTurnId !== input.actor.turnId ||
218
+ Number(caller.executionGeneration) !== input.actor.executionGeneration ||
219
+ caller.activeAttemptId !== input.actor.attemptId ||
220
+ caller.activeTurnId !== input.actor.turnId ||
221
+ !["running", "requires_action", "recovering", "waiting_capacity"].includes(caller.turnStatus)
222
+ ) {
223
+ throw new AgentCommandAuthorityError(
224
+ "CALLER_STALE",
225
+ "The calling agent attempt no longer owns its turn",
226
+ );
227
+ }
228
+ if (caller.interrupted) {
229
+ throw new AgentCommandAuthorityError(
230
+ "CALLER_INTERRUPTED",
231
+ "The calling agent attempt is being interrupted",
232
+ );
233
+ }
234
+ if (input.action === "steer" && input.targetSessionId === input.actor.sessionId) {
235
+ throw new AgentCommandAuthorityError("SELF_STEER", "An agent cannot steer its own session");
236
+ }
237
+ if (input.action !== "pause") return;
238
+ const ancestry = await db.execute<{
239
+ containsCaller: boolean;
240
+ invalid: boolean;
241
+ }>(sql`
242
+ with recursive caller_ancestry(id, parent_id, depth, path, cycle) as (
243
+ select session.id, session.parent_session_id, 0, array[session.id], false
244
+ from ${schema.sessions} session
245
+ where session.workspace_id = ${input.workspaceId}
246
+ and session.id = ${input.actor.sessionId}
247
+ union all
248
+ select parent.id, parent.parent_session_id, child.depth + 1,
249
+ child.path || parent.id, parent.id = any(child.path)
250
+ from caller_ancestry child
251
+ join ${schema.sessions} parent
252
+ on parent.workspace_id = ${input.workspaceId} and parent.id = child.parent_id
253
+ where child.depth < ${SESSION_ANCESTRY_LIMIT} and not child.cycle
254
+ )
255
+ select
256
+ coalesce(bool_or(id = ${input.targetSessionId}), false) as "containsCaller",
257
+ coalesce(bool_or(cycle), false) or coalesce(max(depth), 0) >= ${SESSION_ANCESTRY_LIMIT}
258
+ as invalid
259
+ from caller_ancestry
260
+ `);
261
+ if (ancestry[0]?.invalid) {
262
+ throw new SessionControlInvariantError("Caller ancestry is invalid");
263
+ }
264
+ if (ancestry[0]?.containsCaller) {
265
+ throw new AgentCommandAuthorityError(
266
+ "SELF_OR_ANCESTOR_PAUSE",
267
+ "An agent cannot pause its own session or an ancestor workstream",
268
+ );
269
+ }
270
+ }
271
+
272
+ function asSafeRevision(value: number | string | null, label: string): number | null {
273
+ if (value === null) return null;
274
+ const revision = Number(value);
275
+ if (!Number.isSafeInteger(revision) || revision < 0) {
276
+ throw new SessionControlInvariantError(`${label} is not a safe non-negative revision`);
277
+ }
278
+ return revision;
279
+ }
280
+
281
+ function asDate(value: Date | string | null): Date | null {
282
+ if (value === null) return null;
283
+ return value instanceof Date ? value : new Date(value);
284
+ }
285
+
286
+ function canonicalize(value: unknown): unknown {
287
+ if (Array.isArray(value)) return value.map(canonicalize);
288
+ if (value && typeof value === "object") {
289
+ return Object.fromEntries(
290
+ Object.entries(value as Record<string, unknown>)
291
+ .sort(([left], [right]) => left.localeCompare(right))
292
+ .map(([key, item]) => [key, canonicalize(item)]),
293
+ );
294
+ }
295
+ return value;
296
+ }
297
+
298
+ export function canonicalSessionCommandHash(value: unknown): string {
299
+ return createHash("sha256")
300
+ .update(JSON.stringify(canonicalize(value)))
301
+ .digest("hex");
302
+ }
303
+
304
+ function controlEtag(value: unknown): string {
305
+ return `sc1:${canonicalSessionCommandHash(value)}`;
306
+ }
307
+
308
+ function lockClause(mode: WorkspaceControlLockMode) {
309
+ return mode === "update" ? sql.raw("for update") : sql.raw("for share");
310
+ }
311
+
312
+ export async function lockWorkspaceInferenceControl(
313
+ db: Database,
314
+ workspaceId: string,
315
+ mode: WorkspaceControlLockMode,
316
+ ): Promise<WorkspaceControlRow> {
317
+ const rows = await db.execute<WorkspaceControlRow>(sql`
318
+ select
319
+ workspace_id as "workspaceId",
320
+ account_id as "accountId",
321
+ revision,
322
+ workspace_state as "workspaceState",
323
+ workspace_pause_revision as "workspacePauseRevision",
324
+ reason,
325
+ changed_by as "changedBy",
326
+ changed_at as "changedAt"
327
+ from ${schema.workspaceInferenceControls}
328
+ where workspace_id = ${workspaceId}
329
+ ${lockClause(mode)}
330
+ `);
331
+ const row = rows[0];
332
+ if (!row) {
333
+ throw new SessionControlInvariantError(
334
+ `Workspace ${workspaceId} has no mandatory inference-control row`,
335
+ );
336
+ }
337
+ return row;
338
+ }
339
+
340
+ export async function registerSessionTurnAttemptClaim(
341
+ db: Database,
342
+ input: {
343
+ id: string;
344
+ accountId: string;
345
+ workspaceId: string;
346
+ sessionId: string;
347
+ turnId: string;
348
+ executionGeneration: number;
349
+ temporalWorkflowId: string;
350
+ temporalWorkflowRunId: string;
351
+ temporalActivityId: string;
352
+ verifiedControlRevision: number;
353
+ },
354
+ ): Promise<typeof schema.sessionTurnAttempts.$inferSelect> {
355
+ const [inserted] = await db
356
+ .insert(schema.sessionTurnAttempts)
357
+ .values({
358
+ ...input,
359
+ state: "claimed",
360
+ })
361
+ // Only an idempotent replay of this exact preallocated attempt ID may
362
+ // enter the comparison path below. A collision on live-session, live-turn,
363
+ // or Temporal dispatch ownership is a distinct invariant violation and
364
+ // must not be disguised as an attempt-ID conflict.
365
+ .onConflictDoNothing({ target: schema.sessionTurnAttempts.id })
366
+ .returning();
367
+ if (inserted) return inserted;
368
+ const [existing] = await db
369
+ .select()
370
+ .from(schema.sessionTurnAttempts)
371
+ .where(
372
+ and(
373
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
374
+ eq(schema.sessionTurnAttempts.id, input.id),
375
+ ),
376
+ )
377
+ .for("update")
378
+ .limit(1);
379
+ if (
380
+ !existing ||
381
+ existing.accountId !== input.accountId ||
382
+ existing.sessionId !== input.sessionId ||
383
+ existing.turnId !== input.turnId ||
384
+ existing.executionGeneration !== input.executionGeneration ||
385
+ existing.temporalWorkflowId !== input.temporalWorkflowId ||
386
+ existing.temporalWorkflowRunId !== input.temporalWorkflowRunId ||
387
+ existing.temporalActivityId !== input.temporalActivityId ||
388
+ existing.state === "closed"
389
+ ) {
390
+ throw new SessionControlInvariantError(
391
+ `Attempt ${input.id} conflicts with a different or closed ownership chain`,
392
+ );
393
+ }
394
+ return existing;
395
+ }
396
+
397
+ /**
398
+ * Close the exact first-class owner while the caller still holds the owning
399
+ * session/turn locks. Every path that clears `session_turns.active_attempt_id`
400
+ * must call this in the same transaction; otherwise a later claim would either
401
+ * collide with a zombie live owner or have to weaken the ownership fence.
402
+ */
403
+ export async function closeSessionTurnAttemptInTransaction(
404
+ db: Database,
405
+ input: {
406
+ id: string;
407
+ accountId: string;
408
+ workspaceId: string;
409
+ sessionId: string;
410
+ turnId: string;
411
+ executionGeneration: number;
412
+ outcome: SessionTurnAttemptOutcome;
413
+ closedAt?: Date;
414
+ },
415
+ ): Promise<{ action: "closed" | "already_closed" }> {
416
+ const [attempt] = await db
417
+ .select()
418
+ .from(schema.sessionTurnAttempts)
419
+ .where(
420
+ and(
421
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
422
+ eq(schema.sessionTurnAttempts.id, input.id),
423
+ ),
424
+ )
425
+ .for("update")
426
+ .limit(1);
427
+ if (
428
+ !attempt ||
429
+ attempt.accountId !== input.accountId ||
430
+ attempt.sessionId !== input.sessionId ||
431
+ attempt.turnId !== input.turnId ||
432
+ attempt.executionGeneration !== input.executionGeneration
433
+ ) {
434
+ throw new SessionControlInvariantError(
435
+ `Attempt ${input.id} does not own the expected session turn generation`,
436
+ );
437
+ }
438
+ if (attempt.state === "closed") {
439
+ if (attempt.outcome !== input.outcome) {
440
+ throw new SessionControlInvariantError(
441
+ `Attempt ${input.id} is already closed as ${attempt.outcome ?? "unknown"}`,
442
+ );
443
+ }
444
+ return { action: "already_closed" };
445
+ }
446
+ if (attempt.state !== "claimed" && attempt.state !== "running") {
447
+ throw new SessionControlInvariantError(
448
+ `Attempt ${input.id} has invalid live state ${attempt.state}`,
449
+ );
450
+ }
451
+ const now = input.closedAt ?? new Date();
452
+ const [closed] = await db
453
+ .update(schema.sessionTurnAttempts)
454
+ .set({
455
+ state: "closed",
456
+ outcome: input.outcome,
457
+ workerId: null,
458
+ leaseId: null,
459
+ leaseExpiresAt: null,
460
+ closedAt: now,
461
+ updatedAt: now,
462
+ })
463
+ .where(
464
+ and(
465
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
466
+ eq(schema.sessionTurnAttempts.id, input.id),
467
+ sql`${schema.sessionTurnAttempts.state} in ('claimed', 'running')`,
468
+ ),
469
+ )
470
+ .returning({ id: schema.sessionTurnAttempts.id });
471
+ if (!closed) {
472
+ throw new SessionControlInvariantError(`Attempt ${input.id} changed while locked`);
473
+ }
474
+ return { action: "closed" };
475
+ }
476
+
477
+ function targetValues(sessionIds: string[]) {
478
+ if (sessionIds.length === 0) {
479
+ throw new SessionControlInvariantError("At least one target session is required");
480
+ }
481
+ return sql.join(
482
+ sessionIds.map((id) => sql`(${id}::uuid)`),
483
+ sql`, `,
484
+ );
485
+ }
486
+
487
+ const TARGET_PATH_PROJECTION_LIMIT = 128;
488
+
489
+ async function loadTargetAncestryRows(
490
+ db: Database,
491
+ workspaceId: string,
492
+ sessionIds: string[],
493
+ ): Promise<AncestryRow[]> {
494
+ return await db.execute<AncestryRow>(sql`
495
+ with recursive targets(id) as (values ${targetValues(sessionIds)}),
496
+ ancestry as (
497
+ select
498
+ target.id as target_id,
499
+ session.id as session_id,
500
+ session.parent_session_id,
501
+ session.title,
502
+ session.direct_control_state,
503
+ session.direct_pause_revision,
504
+ session.subtree_run_override_revision,
505
+ session.control_version,
506
+ session.direct_control_changed_by,
507
+ session.direct_control_reason,
508
+ session.direct_control_changed_at,
509
+ 0::integer as depth,
510
+ array[session.id]::uuid[] as path,
511
+ false as cycle
512
+ from targets target
513
+ join ${schema.sessions} session
514
+ on session.workspace_id = ${workspaceId} and session.id = target.id
515
+ union all
516
+ select
517
+ child.target_id,
518
+ parent.id,
519
+ parent.parent_session_id,
520
+ parent.title,
521
+ parent.direct_control_state,
522
+ parent.direct_pause_revision,
523
+ parent.subtree_run_override_revision,
524
+ parent.control_version,
525
+ parent.direct_control_changed_by,
526
+ parent.direct_control_reason,
527
+ parent.direct_control_changed_at,
528
+ child.depth + 1,
529
+ child.path || parent.id,
530
+ parent.id = any(child.path)
531
+ from ancestry child
532
+ join ${schema.sessions} parent
533
+ on parent.workspace_id = ${workspaceId} and parent.id = child.parent_session_id
534
+ where child.parent_session_id is not null
535
+ and not child.cycle
536
+ and child.depth < ${SESSION_ANCESTRY_LIMIT}
537
+ )
538
+ select
539
+ target_id as "targetId",
540
+ session_id as "sessionId",
541
+ parent_session_id as "parentSessionId",
542
+ title,
543
+ direct_control_state as "directState",
544
+ direct_pause_revision as "directPauseRevision",
545
+ subtree_run_override_revision as "subtreeRunOverrideRevision",
546
+ control_version as "controlVersion",
547
+ direct_control_changed_by as "directControlChangedBy",
548
+ direct_control_reason as "directControlReason",
549
+ direct_control_changed_at as "directControlChangedAt",
550
+ depth,
551
+ cycle
552
+ from ancestry
553
+ order by target_id, depth
554
+ `);
555
+ }
556
+
557
+ async function loadAncestryNodes(
558
+ db: Database,
559
+ workspaceId: string,
560
+ sessionIds: string[],
561
+ ): Promise<Map<string, AncestryNode>> {
562
+ const rows = await db.execute<AncestryNode>(sql`
563
+ with recursive targets(id) as (values ${targetValues(sessionIds)}),
564
+ ancestry_ids(id) as (
565
+ select session.id
566
+ from targets target
567
+ join ${schema.sessions} session
568
+ on session.workspace_id = ${workspaceId} and session.id = target.id
569
+ union
570
+ select parent.id
571
+ from ancestry_ids child
572
+ join ${schema.sessions} current
573
+ on current.workspace_id = ${workspaceId} and current.id = child.id
574
+ join ${schema.sessions} parent
575
+ on parent.workspace_id = current.workspace_id
576
+ and parent.id = current.parent_session_id
577
+ )
578
+ select
579
+ session.id as "sessionId",
580
+ session.parent_session_id as "parentSessionId",
581
+ session.title,
582
+ session.direct_control_state as "directState",
583
+ session.direct_pause_revision as "directPauseRevision",
584
+ session.subtree_run_override_revision as "subtreeRunOverrideRevision",
585
+ session.control_version as "controlVersion",
586
+ session.direct_control_changed_by as "directControlChangedBy",
587
+ session.direct_control_reason as "directControlReason",
588
+ session.direct_control_changed_at as "directControlChangedAt"
589
+ from ancestry_ids ancestry
590
+ join ${schema.sessions} session
591
+ on session.workspace_id = ${workspaceId} and session.id = ancestry.id
592
+ `);
593
+ return new Map(rows.map((row: AncestryNode) => [row.sessionId, row]));
594
+ }
595
+
596
+ function ancestryRowsForTarget(targetId: string, nodes: Map<string, AncestryNode>): AncestryRow[] {
597
+ const rows: AncestryRow[] = [];
598
+ const path = new Set<string>();
599
+ let sessionId: string | null = targetId;
600
+ for (let depth = 0; sessionId !== null; depth += 1) {
601
+ const node = nodes.get(sessionId);
602
+ if (!node) break;
603
+ const cycle = path.has(sessionId);
604
+ rows.push({ ...node, targetId, depth, cycle });
605
+ if (cycle || depth >= SESSION_ANCESTRY_LIMIT) break;
606
+ path.add(sessionId);
607
+ sessionId = node.parentSessionId;
608
+ }
609
+ return rows;
610
+ }
611
+
612
+ function assertCompleteAncestry(sessionId: string, rows: AncestryRow[]): void {
613
+ if (rows.length === 0) {
614
+ throw new SessionControlInvariantError(`Session ${sessionId} does not exist in its workspace`);
615
+ }
616
+ if (rows.some((row) => row.cycle)) {
617
+ throw new SessionControlInvariantError(`Session ${sessionId} has cyclic ancestry`);
618
+ }
619
+ const last = rows.at(-1)!;
620
+ const depth = Number(last.depth);
621
+ if (last.parentSessionId !== null) {
622
+ throw new SessionControlInvariantError(
623
+ depth >= SESSION_ANCESTRY_LIMIT
624
+ ? `Session ${sessionId} ancestry exceeds ${SESSION_ANCESTRY_LIMIT}`
625
+ : `Session ${sessionId} has a missing ancestor ${last.parentSessionId}`,
626
+ );
627
+ }
628
+ }
629
+
630
+ function blockerForSession(row: AncestryRow, revision: number): EffectiveControlBlocker {
631
+ return {
632
+ kind: "session",
633
+ sessionId: row.sessionId,
634
+ displayName: row.title?.trim() || "Untitled session",
635
+ actor: row.directControlChangedBy,
636
+ reason: row.directControlReason,
637
+ changedAt: asDate(row.directControlChangedAt),
638
+ revision,
639
+ };
640
+ }
641
+
642
+ function projectEffectiveControl(
643
+ workspace: WorkspaceControlRow,
644
+ targetId: string,
645
+ rows: AncestryRow[],
646
+ stoppingAttempts: number,
647
+ ): EffectiveSessionControl {
648
+ assertCompleteAncestry(targetId, rows);
649
+ const workspaceRevision = asSafeRevision(workspace.revision, "workspace control revision")!;
650
+ const path = rows.map((row) => ({
651
+ row,
652
+ depth: Number(row.depth),
653
+ pauseRevision: asSafeRevision(row.directPauseRevision, "direct pause revision"),
654
+ overrideRevision: asSafeRevision(
655
+ row.subtreeRunOverrideRevision,
656
+ "subtree run override revision",
657
+ ),
658
+ controlVersion: asSafeRevision(row.controlVersion, "session control version")!,
659
+ }));
660
+
661
+ const undefeated: Array<{ blocker: EffectiveControlBlocker; depth: number }> = [];
662
+ for (const candidate of path) {
663
+ if (candidate.row.directState !== "paused" || candidate.pauseRevision === null) continue;
664
+ const defeated = path.some(
665
+ (possibleOverride) =>
666
+ possibleOverride.depth < candidate.depth &&
667
+ possibleOverride.overrideRevision !== null &&
668
+ possibleOverride.overrideRevision > candidate.pauseRevision!,
669
+ );
670
+ if (!defeated) {
671
+ undefeated.push({
672
+ blocker: blockerForSession(candidate.row, candidate.pauseRevision),
673
+ depth: candidate.depth,
674
+ });
675
+ }
676
+ }
677
+
678
+ const workspacePauseRevision = asSafeRevision(
679
+ workspace.workspacePauseRevision,
680
+ "workspace pause revision",
681
+ );
682
+ if (workspace.workspaceState === "paused") {
683
+ if (workspacePauseRevision === null) {
684
+ throw new SessionControlInvariantError("Paused workspace is missing its pause revision");
685
+ }
686
+ const defeated = path.some(
687
+ (candidate) =>
688
+ candidate.overrideRevision !== null && candidate.overrideRevision > workspacePauseRevision,
689
+ );
690
+ if (!defeated) {
691
+ undefeated.push({
692
+ blocker: {
693
+ kind: "workspace",
694
+ displayName: "Workspace",
695
+ actor: workspace.changedBy,
696
+ reason: workspace.reason,
697
+ changedAt: asDate(workspace.changedAt),
698
+ revision: workspacePauseRevision,
699
+ },
700
+ depth: Number.POSITIVE_INFINITY,
701
+ });
702
+ }
703
+ }
704
+
705
+ undefeated.sort((left, right) => left.depth - right.depth);
706
+ const blockers = undefeated.map(({ blocker }) => blocker);
707
+ const primaryBlocker = blockers[0] ?? null;
708
+ const target = path[0]!;
709
+ const override = path
710
+ .filter((candidate) => candidate.overrideRevision !== null)
711
+ .sort((left, right) => right.overrideRevision! - left.overrideRevision!)[0];
712
+
713
+ const options: EffectiveControlResumeOption[] = [];
714
+ if (blockers.length > 0) {
715
+ options.push({
716
+ scope: "selected",
717
+ targetId,
718
+ selectedStateAfter: "active",
719
+ impactCopy: "Resume this session and its descendants without changing sibling workstreams.",
720
+ });
721
+ for (const entry of undefeated) {
722
+ if (entry.blocker.kind !== "session" || entry.blocker.sessionId === targetId) continue;
723
+ const remaining = undefeated.find(
724
+ (candidate) =>
725
+ candidate.depth < entry.depth && candidate.blocker.sessionId !== entry.blocker.sessionId,
726
+ )?.blocker;
727
+ options.push({
728
+ scope: "session",
729
+ targetId: entry.blocker.sessionId!,
730
+ selectedStateAfter: remaining ? "paused" : "active",
731
+ ...(remaining ? { remainingPrimaryBlocker: remaining } : {}),
732
+ impactCopy: `Resume the workstream rooted at ${entry.blocker.displayName}.`,
733
+ });
734
+ }
735
+ if (workspace.workspaceState === "paused") {
736
+ const remaining = undefeated.find((entry) => entry.blocker.kind === "session")?.blocker;
737
+ options.push({
738
+ scope: "workspace",
739
+ selectedStateAfter: remaining ? "paused" : "active",
740
+ ...(remaining ? { remainingPrimaryBlocker: remaining } : {}),
741
+ impactCopy: "Resume the entire workspace; narrower session pauses remain in force.",
742
+ });
743
+ }
744
+ }
745
+
746
+ const etagFacts = {
747
+ workspace: {
748
+ state: workspace.workspaceState,
749
+ pauseRevision: workspacePauseRevision,
750
+ },
751
+ path: path.map((candidate) => ({
752
+ sessionId: candidate.row.sessionId,
753
+ directState: candidate.row.directState,
754
+ pauseRevision: candidate.pauseRevision,
755
+ overrideRevision: candidate.overrideRevision,
756
+ })),
757
+ };
758
+
759
+ return {
760
+ state: blockers.length > 0 ? "paused" : "active",
761
+ controlVersion: Math.max(
762
+ workspaceRevision,
763
+ ...path.map((candidate) => candidate.controlVersion),
764
+ ),
765
+ controlEtag: controlEtag(etagFacts),
766
+ directState: target.row.directState === "paused" ? "paused" : "active",
767
+ primaryBlocker,
768
+ additionalBlockerCount: Math.max(0, blockers.length - 1),
769
+ blockers,
770
+ resumeOptions: options,
771
+ override:
772
+ override?.overrideRevision === null || !override
773
+ ? null
774
+ : {
775
+ rootSessionId: override.row.sessionId,
776
+ revision: override.overrideRevision,
777
+ },
778
+ settlement: stoppingAttempts > 0 ? { state: "stopping", attemptCount: stoppingAttempts } : null,
779
+ };
780
+ }
781
+
782
+ async function unsettledAttemptCounts(
783
+ db: Database,
784
+ workspaceId: string,
785
+ sessionIds: string[],
786
+ ): Promise<Map<string, number>> {
787
+ const rows = await db.execute<{
788
+ sessionId: string;
789
+ count: number | string;
790
+ }>(sql`
791
+ with recursive targets(id) as (values ${targetValues(sessionIds)}),
792
+ interruptions as (
793
+ select interruption.session_id, interruption.attempt_id
794
+ from ${schema.sessionAttemptInterruptions} interruption
795
+ where interruption.workspace_id = ${workspaceId}
796
+ and interruption.state in ('pending', 'delivered', 'acknowledged')
797
+ ), interruption_ancestry(session_id, ancestor_id, attempt_id, depth, path) as (
798
+ select
799
+ interruption.session_id,
800
+ interruption.session_id,
801
+ interruption.attempt_id,
802
+ 0::integer,
803
+ array[interruption.session_id]::uuid[]
804
+ from interruptions interruption
805
+ union all
806
+ select
807
+ ancestry.session_id,
808
+ current.parent_session_id,
809
+ ancestry.attempt_id,
810
+ ancestry.depth + 1,
811
+ ancestry.path || current.parent_session_id
812
+ from interruption_ancestry ancestry
813
+ join ${schema.sessions} current
814
+ on current.workspace_id = ${workspaceId} and current.id = ancestry.ancestor_id
815
+ where current.parent_session_id is not null
816
+ and not current.parent_session_id = any(ancestry.path)
817
+ and ancestry.depth < ${SESSION_ANCESTRY_LIMIT}
818
+ )
819
+ select target.id as "sessionId", count(distinct ancestry.attempt_id)::integer as count
820
+ from targets target
821
+ join interruption_ancestry ancestry on ancestry.ancestor_id = target.id
822
+ group by target.id
823
+ `);
824
+ return new Map(
825
+ rows.map((row: { sessionId: string; count: number | string }) => [
826
+ row.sessionId,
827
+ Number(row.count),
828
+ ]),
829
+ );
830
+ }
831
+
832
+ export async function evaluateSessionControls(
833
+ db: Database,
834
+ workspaceId: string,
835
+ sessionIds: string[],
836
+ options: { lock?: WorkspaceControlLockMode } = {},
837
+ ): Promise<Map<string, EffectiveSessionControl>> {
838
+ const uniqueIds = [...new Set(sessionIds)];
839
+ if (uniqueIds.length === 0) {
840
+ return new Map();
841
+ }
842
+ const workspace = await lockWorkspaceInferenceControl(db, workspaceId, options.lock ?? "share");
843
+ const stopping = await unsettledAttemptCounts(db, workspaceId, uniqueIds);
844
+ const result = new Map<string, EffectiveSessionControl>();
845
+ if (uniqueIds.length <= TARGET_PATH_PROJECTION_LIMIT) {
846
+ // PostgreSQL's direct target-path plan is substantially faster for the
847
+ // ordinary one-session and paged-list cases. Keep its bounded row shape.
848
+ const ancestry = await loadTargetAncestryRows(db, workspaceId, uniqueIds);
849
+ const ancestryByTarget = new Map<string, AncestryRow[]>();
850
+ for (const row of ancestry) {
851
+ const rows = ancestryByTarget.get(row.targetId);
852
+ if (rows) rows.push(row);
853
+ else ancestryByTarget.set(row.targetId, [row]);
854
+ }
855
+ for (const sessionId of uniqueIds) {
856
+ result.set(
857
+ sessionId,
858
+ projectEffectiveControl(
859
+ workspace,
860
+ sessionId,
861
+ ancestryByTarget.get(sessionId) ?? [],
862
+ stopping.get(sessionId) ?? 0,
863
+ ),
864
+ );
865
+ }
866
+ return result;
867
+ }
868
+
869
+ // Large sets fetch each shared ancestor exactly once. Returning one row per
870
+ // target/ancestor pair makes a 10k-session tree explode into millions of
871
+ // protocol objects even though the tree itself contains only 10k nodes.
872
+ const ancestry = await loadAncestryNodes(db, workspaceId, uniqueIds);
873
+ for (const sessionId of uniqueIds) {
874
+ result.set(
875
+ sessionId,
876
+ projectEffectiveControl(
877
+ workspace,
878
+ sessionId,
879
+ ancestryRowsForTarget(sessionId, ancestry),
880
+ stopping.get(sessionId) ?? 0,
881
+ ),
882
+ );
883
+ }
884
+ return result;
885
+ }
886
+
887
+ export async function evaluateSessionControl(
888
+ db: Database,
889
+ workspaceId: string,
890
+ sessionId: string,
891
+ options: { lock?: WorkspaceControlLockMode } = {},
892
+ ): Promise<EffectiveSessionControl> {
893
+ return (await evaluateSessionControls(db, workspaceId, [sessionId], options)).get(sessionId)!;
894
+ }
895
+
896
+ async function findCommandReceipt(
897
+ db: Database,
898
+ input: {
899
+ workspaceId: string;
900
+ actor: SessionCommandActor;
901
+ action: string;
902
+ targetSessionId: string | null;
903
+ targetTurnId: string | null;
904
+ operationKey: string;
905
+ },
906
+ ): Promise<SessionCommandReceiptRow | null> {
907
+ const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
908
+ const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
909
+ const rows = await db
910
+ .select()
911
+ .from(schema.sessionCommandReceipts)
912
+ .where(
913
+ and(
914
+ eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
915
+ eq(schema.sessionCommandReceipts.actorType, input.actor.type),
916
+ sql`${schema.sessionCommandReceipts.actorSubjectId} is not distinct from ${actorSubjectId}`,
917
+ sql`${schema.sessionCommandReceipts.actorAttemptId} is not distinct from ${actorAttemptId}::uuid`,
918
+ eq(schema.sessionCommandReceipts.action, input.action),
919
+ sql`${schema.sessionCommandReceipts.targetSessionId} is not distinct from ${input.targetSessionId}::uuid`,
920
+ sql`${schema.sessionCommandReceipts.targetTurnId} is not distinct from ${input.targetTurnId}::uuid`,
921
+ eq(schema.sessionCommandReceipts.operationKey, input.operationKey),
922
+ ),
923
+ )
924
+ .for("update");
925
+ return rows[0] ?? null;
926
+ }
927
+
928
+ export async function reserveSessionCommandReceipt(
929
+ db: Database,
930
+ input: {
931
+ accountId: string;
932
+ workspaceId: string;
933
+ actor: SessionCommandActor;
934
+ action: string;
935
+ targetSessionId: string | null;
936
+ targetTurnId: string | null;
937
+ operationKey: string;
938
+ canonicalRequestHash: string;
939
+ },
940
+ ): Promise<{ receipt: SessionCommandReceiptRow; replay: boolean }> {
941
+ if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
942
+ const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
943
+ const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
944
+ const [inserted] = await db
945
+ .insert(schema.sessionCommandReceipts)
946
+ .values({
947
+ accountId: input.accountId,
948
+ workspaceId: input.workspaceId,
949
+ actorType: input.actor.type,
950
+ actorSubjectId,
951
+ actorAttemptId,
952
+ action: input.action,
953
+ targetSessionId: input.targetSessionId,
954
+ targetTurnId: input.targetTurnId,
955
+ operationKey: input.operationKey,
956
+ canonicalRequestHash: input.canonicalRequestHash,
957
+ })
958
+ .onConflictDoNothing()
959
+ .returning();
960
+ const receipt =
961
+ inserted ??
962
+ (await findCommandReceipt(db, {
963
+ workspaceId: input.workspaceId,
964
+ actor: input.actor,
965
+ action: input.action,
966
+ targetSessionId: input.targetSessionId,
967
+ targetTurnId: input.targetTurnId,
968
+ operationKey: input.operationKey,
969
+ }));
970
+ if (!receipt) throw new SessionControlInvariantError("Command receipt conflict was not readable");
971
+ if (receipt.canonicalRequestHash !== input.canonicalRequestHash) {
972
+ throw new SessionCommandIdempotencyError();
973
+ }
974
+ return { receipt, replay: !inserted };
975
+ }
976
+
977
+ function nextRevision(workspace: WorkspaceControlRow): number {
978
+ const current = asSafeRevision(workspace.revision, "workspace control revision")!;
979
+ if (current >= Number.MAX_SAFE_INTEGER) {
980
+ throw new SessionControlInvariantError("Workspace control revision is exhausted");
981
+ }
982
+ return current + 1;
983
+ }
984
+
985
+ async function advanceWorkspaceRevision(
986
+ db: Database,
987
+ workspaceId: string,
988
+ revision: number,
989
+ ): Promise<void> {
990
+ const rows = await db
991
+ .update(schema.workspaceInferenceControls)
992
+ .set({ revision, updatedAt: new Date() })
993
+ .where(
994
+ and(
995
+ eq(schema.workspaceInferenceControls.workspaceId, workspaceId),
996
+ eq(schema.workspaceInferenceControls.revision, revision - 1),
997
+ ),
998
+ )
999
+ .returning({ workspaceId: schema.workspaceInferenceControls.workspaceId });
1000
+ if (rows.length !== 1) {
1001
+ throw new SessionControlInvariantError(
1002
+ "Workspace control revision did not advance exactly once",
1003
+ );
1004
+ }
1005
+ }
1006
+
1007
+ async function registerContinuableWakes(
1008
+ db: Database,
1009
+ input: { workspaceId: string; rootSessionId: string | null; reason: string },
1010
+ ): Promise<number> {
1011
+ const rows = await db.execute<{ wakeCount: number | string }>(sql`
1012
+ with eligible as (
1013
+ select *
1014
+ from opengeni_private.list_continuable_sessions(
1015
+ ${input.workspaceId}::uuid,
1016
+ ${input.rootSessionId}::uuid
1017
+ )
1018
+ ), upserted as (
1019
+ insert into ${schema.sessionWorkflowWakeOutbox} (
1020
+ session_id, account_id, workspace_id, temporal_workflow_id, reason
1021
+ )
1022
+ select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
1023
+ from eligible
1024
+ on conflict (session_id) do update set
1025
+ wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
1026
+ temporal_workflow_id = excluded.temporal_workflow_id,
1027
+ reason = excluded.reason,
1028
+ attempts = 0,
1029
+ next_attempt_at = now(),
1030
+ last_error = null,
1031
+ updated_at = now()
1032
+ returning session_id
1033
+ )
1034
+ select count(*)::bigint as "wakeCount" from upserted
1035
+ `);
1036
+ return Number(rows[0]?.wakeCount ?? 0);
1037
+ }
1038
+
1039
+ async function registerDescendantWakes(
1040
+ db: Database,
1041
+ input: { workspaceId: string; sessionId: string; reason: string },
1042
+ ): Promise<number> {
1043
+ return await registerContinuableWakes(db, {
1044
+ workspaceId: input.workspaceId,
1045
+ rootSessionId: input.sessionId,
1046
+ reason: input.reason,
1047
+ });
1048
+ }
1049
+
1050
+ async function registerWorkspaceWakes(
1051
+ db: Database,
1052
+ input: { workspaceId: string; reason: string },
1053
+ ): Promise<number> {
1054
+ return await registerContinuableWakes(db, {
1055
+ workspaceId: input.workspaceId,
1056
+ rootSessionId: null,
1057
+ reason: input.reason,
1058
+ });
1059
+ }
1060
+
1061
+ /**
1062
+ * Pause never needs to rediscover ordinary continuable work: it closes
1063
+ * admission. Only workflows owning an interruption created by this exact
1064
+ * command must wake so they can settle their in-flight attempt. The operation
1065
+ * receipt is indexed and bounds this to the number of affected attempts rather
1066
+ * than the size or depth of the paused tree.
1067
+ */
1068
+ async function registerInterruptionWakes(
1069
+ db: Database,
1070
+ input: { operationId: string; reason: string },
1071
+ ): Promise<number> {
1072
+ const rows = await db.execute<{ wakeCount: number | string }>(sql`
1073
+ with eligible as (
1074
+ select distinct
1075
+ session.id as session_id,
1076
+ session.account_id,
1077
+ session.workspace_id,
1078
+ coalesce(session.temporal_workflow_id, 'session-' || session.id::text)
1079
+ as temporal_workflow_id
1080
+ from ${schema.sessionAttemptInterruptions} interruption
1081
+ join ${schema.sessions} session
1082
+ on session.workspace_id = interruption.workspace_id
1083
+ and session.id = interruption.session_id
1084
+ where interruption.operation_id = ${input.operationId}::uuid
1085
+ and interruption.state in ('pending', 'delivered', 'acknowledged')
1086
+ ), upserted as (
1087
+ insert into ${schema.sessionWorkflowWakeOutbox} (
1088
+ session_id, account_id, workspace_id, temporal_workflow_id, reason
1089
+ )
1090
+ select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
1091
+ from eligible
1092
+ on conflict (session_id) do update set
1093
+ wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
1094
+ temporal_workflow_id = excluded.temporal_workflow_id,
1095
+ reason = excluded.reason,
1096
+ attempts = 0,
1097
+ next_attempt_at = now(),
1098
+ last_error = null,
1099
+ updated_at = now()
1100
+ returning session_id
1101
+ )
1102
+ select count(*)::bigint as "wakeCount" from upserted
1103
+ `);
1104
+ return Number(rows[0]?.wakeCount ?? 0);
1105
+ }
1106
+
1107
+ /** Register one exact post-commit Temporal nudge without encoding eligibility in
1108
+ * the transport. The workflow re-reads canonical Postgres state; coalescing is
1109
+ * revisioned so a lost or stale delivery cannot hide a later mutation. */
1110
+ export async function registerSessionWorkflowWakeInTransaction(
1111
+ db: Database,
1112
+ input: {
1113
+ accountId: string;
1114
+ workspaceId: string;
1115
+ sessionId: string;
1116
+ temporalWorkflowId: string;
1117
+ reason: string;
1118
+ },
1119
+ ): Promise<number> {
1120
+ const [row] = await db
1121
+ .insert(schema.sessionWorkflowWakeOutbox)
1122
+ .values({
1123
+ accountId: input.accountId,
1124
+ workspaceId: input.workspaceId,
1125
+ sessionId: input.sessionId,
1126
+ temporalWorkflowId: input.temporalWorkflowId,
1127
+ reason: input.reason,
1128
+ })
1129
+ .onConflictDoUpdate({
1130
+ target: schema.sessionWorkflowWakeOutbox.sessionId,
1131
+ set: {
1132
+ temporalWorkflowId: input.temporalWorkflowId,
1133
+ wakeRevision: sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`,
1134
+ reason: input.reason,
1135
+ attempts: 0,
1136
+ nextAttemptAt: new Date(),
1137
+ lastError: null,
1138
+ updatedAt: new Date(),
1139
+ },
1140
+ })
1141
+ .returning({ wakeRevision: schema.sessionWorkflowWakeOutbox.wakeRevision });
1142
+ if (!row) {
1143
+ throw new SessionControlInvariantError(`Failed to register wake for ${input.sessionId}`);
1144
+ }
1145
+ return Number(row.wakeRevision);
1146
+ }
1147
+
1148
+ /**
1149
+ * Internal producers share one outstanding session-level receipt. While a wake
1150
+ * remains undelivered, another update makes that same batch richer instead of
1151
+ * manufacturing another transport revision or sequential model turn.
1152
+ */
1153
+ export async function registerInternalUpdateWakeInTransaction(
1154
+ db: Database,
1155
+ input: {
1156
+ accountId: string;
1157
+ workspaceId: string;
1158
+ sessionId: string;
1159
+ temporalWorkflowId: string;
1160
+ },
1161
+ ): Promise<{ wakeRevision: number; shouldSignal: boolean }> {
1162
+ const [existing] = await db
1163
+ .select()
1164
+ .from(schema.sessionWorkflowWakeOutbox)
1165
+ .where(
1166
+ and(
1167
+ eq(schema.sessionWorkflowWakeOutbox.workspaceId, input.workspaceId),
1168
+ eq(schema.sessionWorkflowWakeOutbox.sessionId, input.sessionId),
1169
+ ),
1170
+ )
1171
+ .for("update")
1172
+ .limit(1);
1173
+ if (existing && existing.wakeRevision > existing.deliveredRevision) {
1174
+ await db
1175
+ .update(schema.sessionWorkflowWakeOutbox)
1176
+ .set({
1177
+ temporalWorkflowId: input.temporalWorkflowId,
1178
+ reason: "internal_update_batch",
1179
+ nextAttemptAt: new Date(),
1180
+ lastError: null,
1181
+ updatedAt: new Date(),
1182
+ })
1183
+ .where(eq(schema.sessionWorkflowWakeOutbox.sessionId, input.sessionId));
1184
+ return { wakeRevision: existing.wakeRevision, shouldSignal: false };
1185
+ }
1186
+ return {
1187
+ wakeRevision: await registerSessionWorkflowWakeInTransaction(db, {
1188
+ ...input,
1189
+ reason: "internal_update_batch",
1190
+ }),
1191
+ shouldSignal: true,
1192
+ };
1193
+ }
1194
+
1195
+ async function interruptDescendantAttempts(
1196
+ db: Database,
1197
+ input: {
1198
+ accountId: string;
1199
+ workspaceId: string;
1200
+ sessionId: string;
1201
+ operationId: string;
1202
+ kind: "session_pause" | "steer" | "maintenance";
1203
+ controlRevision: number;
1204
+ },
1205
+ ): Promise<number> {
1206
+ const rows = await db.execute<{ count: number | string }>(sql`
1207
+ with recursive live_attempts as (
1208
+ select attempt.id, attempt.account_id, attempt.workspace_id, attempt.session_id
1209
+ from ${schema.sessionTurnAttempts} attempt
1210
+ where attempt.workspace_id = ${input.workspaceId}
1211
+ and attempt.state in ('claimed', 'running')
1212
+ ), attempt_ancestry(attempt_id, account_id, workspace_id, session_id, ancestor_id, depth, path) as (
1213
+ select
1214
+ attempt.id,
1215
+ attempt.account_id,
1216
+ attempt.workspace_id,
1217
+ attempt.session_id,
1218
+ attempt.session_id,
1219
+ 0::integer,
1220
+ array[attempt.session_id]::uuid[]
1221
+ from live_attempts attempt
1222
+ union all
1223
+ select
1224
+ ancestry.attempt_id,
1225
+ ancestry.account_id,
1226
+ ancestry.workspace_id,
1227
+ ancestry.session_id,
1228
+ current.parent_session_id,
1229
+ ancestry.depth + 1,
1230
+ ancestry.path || current.parent_session_id
1231
+ from attempt_ancestry ancestry
1232
+ join ${schema.sessions} current
1233
+ on current.workspace_id = ${input.workspaceId} and current.id = ancestry.ancestor_id
1234
+ where current.parent_session_id is not null
1235
+ and not current.parent_session_id = any(ancestry.path)
1236
+ and ancestry.depth < ${SESSION_ANCESTRY_LIMIT}
1237
+ ), inserted as (
1238
+ insert into ${schema.sessionAttemptInterruptions} (
1239
+ account_id, workspace_id, session_id, operation_id, attempt_id,
1240
+ kind, control_revision
1241
+ )
1242
+ select ancestry.account_id, ancestry.workspace_id, ancestry.session_id,
1243
+ ${input.operationId}::uuid, ancestry.attempt_id, ${input.kind}, ${input.controlRevision}
1244
+ from attempt_ancestry ancestry
1245
+ where ancestry.ancestor_id = ${input.sessionId}
1246
+ on conflict (operation_id, attempt_id) do nothing
1247
+ returning id
1248
+ )
1249
+ select count(*)::integer as count from inserted
1250
+ `);
1251
+ return Number(rows[0]?.count ?? 0);
1252
+ }
1253
+
1254
+ async function interruptWorkspaceAttempts(
1255
+ db: Database,
1256
+ input: {
1257
+ workspaceId: string;
1258
+ operationId: string;
1259
+ controlRevision: number;
1260
+ },
1261
+ ): Promise<number> {
1262
+ const rows = await db.execute<{ count: number | string }>(sql`
1263
+ with inserted as (
1264
+ insert into ${schema.sessionAttemptInterruptions} (
1265
+ account_id, workspace_id, session_id, operation_id, attempt_id,
1266
+ kind, control_revision
1267
+ )
1268
+ select attempt.account_id, attempt.workspace_id, attempt.session_id,
1269
+ ${input.operationId}::uuid, attempt.id, 'workspace_pause',
1270
+ ${input.controlRevision}
1271
+ from ${schema.sessionTurnAttempts} attempt
1272
+ where attempt.workspace_id = ${input.workspaceId}
1273
+ and attempt.state in ('claimed', 'running')
1274
+ on conflict (operation_id, attempt_id) do nothing
1275
+ returning id
1276
+ )
1277
+ select count(*)::integer as count from inserted
1278
+ `);
1279
+ return Number(rows[0]?.count ?? 0);
1280
+ }
1281
+
1282
+ export async function updateSessionCommandReceiptResult(
1283
+ db: Database,
1284
+ receiptId: string,
1285
+ input: {
1286
+ controlRevision?: number | null;
1287
+ queueVersion?: number | null;
1288
+ turnVersion?: number | null;
1289
+ draftRevision?: number | null;
1290
+ result: Record<string, unknown>;
1291
+ },
1292
+ ): Promise<SessionCommandReceiptRow> {
1293
+ const [receipt] = await db
1294
+ .update(schema.sessionCommandReceipts)
1295
+ .set({
1296
+ ...(input.controlRevision !== undefined
1297
+ ? { appliedControlRevision: input.controlRevision }
1298
+ : {}),
1299
+ ...(input.queueVersion !== undefined ? { appliedQueueVersion: input.queueVersion } : {}),
1300
+ ...(input.turnVersion !== undefined ? { appliedTurnVersion: input.turnVersion } : {}),
1301
+ ...(input.draftRevision !== undefined ? { appliedDraftRevision: input.draftRevision } : {}),
1302
+ result: input.result,
1303
+ updatedAt: new Date(),
1304
+ })
1305
+ .where(eq(schema.sessionCommandReceipts.id, receiptId))
1306
+ .returning();
1307
+ if (!receipt) throw new SessionControlInvariantError("Command receipt disappeared");
1308
+ return receipt;
1309
+ }
1310
+
1311
+ export type SessionControlMutationResult = {
1312
+ receipt: SessionCommandReceiptRow;
1313
+ control: EffectiveSessionControl;
1314
+ sessionControlEventId: string;
1315
+ workspaceControlEventId: string;
1316
+ interruptionCount: number;
1317
+ wakeCount: number;
1318
+ replay: boolean;
1319
+ };
1320
+
1321
+ export async function mutateSessionControlInTransaction(
1322
+ db: Database,
1323
+ input: {
1324
+ accountId: string;
1325
+ workspaceId: string;
1326
+ sessionId: string;
1327
+ actor: SessionCommandActor;
1328
+ operationKey: string;
1329
+ action: "pause" | "resume";
1330
+ reason?: string | null;
1331
+ expectedControlEtag?: string | null;
1332
+ },
1333
+ ): Promise<SessionControlMutationResult> {
1334
+ const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1335
+ const hash = canonicalSessionCommandHash({
1336
+ action: input.action,
1337
+ reason: input.reason ?? null,
1338
+ expectedControlEtag: input.expectedControlEtag ?? null,
1339
+ });
1340
+ const reserved = await reserveSessionCommandReceipt(db, {
1341
+ accountId: input.accountId,
1342
+ workspaceId: input.workspaceId,
1343
+ actor: input.actor,
1344
+ action: `session.${input.action}`,
1345
+ targetSessionId: input.sessionId,
1346
+ targetTurnId: null,
1347
+ operationKey: input.operationKey,
1348
+ canonicalRequestHash: hash,
1349
+ });
1350
+ if (reserved.replay && reserved.receipt.appliedControlRevision !== null) {
1351
+ const workspaceControlEventId = String(reserved.receipt.result.workspaceControlEventId ?? "");
1352
+ if (!workspaceControlEventId) {
1353
+ throw new SessionControlInvariantError("Replayed session control receipt has no event");
1354
+ }
1355
+ const sessionControlEventId = String(reserved.receipt.result.eventId ?? "");
1356
+ if (!sessionControlEventId) {
1357
+ throw new SessionControlInvariantError(
1358
+ "Replayed session control receipt has no session event",
1359
+ );
1360
+ }
1361
+ return {
1362
+ receipt: reserved.receipt,
1363
+ control: await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1364
+ lock: "share",
1365
+ }),
1366
+ sessionControlEventId,
1367
+ workspaceControlEventId,
1368
+ interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
1369
+ wakeCount: Number(reserved.receipt.result.wakeCount ?? 0),
1370
+ replay: true,
1371
+ };
1372
+ }
1373
+ if (input.actor.type === "agent_attempt") {
1374
+ await assertAgentCommandAuthorityInTransaction(db, {
1375
+ workspaceId: input.workspaceId,
1376
+ actor: input.actor,
1377
+ targetSessionId: input.sessionId,
1378
+ action: input.action,
1379
+ });
1380
+ }
1381
+ const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1382
+ lock: "share",
1383
+ });
1384
+ if (input.expectedControlEtag && input.expectedControlEtag !== before.controlEtag) {
1385
+ throw new SessionControlConflictError();
1386
+ }
1387
+
1388
+ const revision = nextRevision(workspace);
1389
+ await advanceWorkspaceRevision(db, input.workspaceId, revision);
1390
+ const [updated] = await db
1391
+ .update(schema.sessions)
1392
+ .set(
1393
+ input.action === "pause"
1394
+ ? {
1395
+ directControlState: "paused",
1396
+ directPauseRevision: revision,
1397
+ controlVersion: revision,
1398
+ directControlReason: input.reason ?? null,
1399
+ directControlChangedBy:
1400
+ input.actor.type === "agent_attempt"
1401
+ ? `attempt:${input.actor.attemptId}`
1402
+ : input.actor.subjectId,
1403
+ directControlChangedAt: new Date(),
1404
+ updatedAt: new Date(),
1405
+ }
1406
+ : {
1407
+ directControlState: "active",
1408
+ directPauseRevision: null,
1409
+ subtreeRunOverrideRevision: revision,
1410
+ controlVersion: revision,
1411
+ directControlReason: input.reason ?? null,
1412
+ directControlChangedBy:
1413
+ input.actor.type === "agent_attempt"
1414
+ ? `attempt:${input.actor.attemptId}`
1415
+ : input.actor.subjectId,
1416
+ directControlChangedAt: new Date(),
1417
+ updatedAt: new Date(),
1418
+ },
1419
+ )
1420
+ .where(
1421
+ and(
1422
+ eq(schema.sessions.workspaceId, input.workspaceId),
1423
+ eq(schema.sessions.id, input.sessionId),
1424
+ ),
1425
+ )
1426
+ .returning({
1427
+ id: schema.sessions.id,
1428
+ lastSequence: schema.sessions.lastSequence,
1429
+ });
1430
+ if (!updated) throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
1431
+
1432
+ const actor =
1433
+ input.actor.type === "agent_attempt"
1434
+ ? `attempt:${input.actor.attemptId}`
1435
+ : input.actor.subjectId;
1436
+ const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
1437
+ accountId: input.accountId,
1438
+ workspaceId: input.workspaceId,
1439
+ revision,
1440
+ scope: "session",
1441
+ rootSessionId: input.sessionId,
1442
+ action: input.action,
1443
+ automatic: false,
1444
+ reason: input.reason ?? null,
1445
+ actor,
1446
+ });
1447
+
1448
+ const interruptionCount =
1449
+ input.action === "pause"
1450
+ ? await interruptDescendantAttempts(db, {
1451
+ accountId: input.accountId,
1452
+ workspaceId: input.workspaceId,
1453
+ sessionId: input.sessionId,
1454
+ operationId: reserved.receipt.id,
1455
+ kind: "session_pause",
1456
+ controlRevision: revision,
1457
+ })
1458
+ : 0;
1459
+ const wakeCount =
1460
+ input.action === "pause"
1461
+ ? await registerInterruptionWakes(db, {
1462
+ operationId: reserved.receipt.id,
1463
+ reason: "session_pause_interruption",
1464
+ })
1465
+ : await registerDescendantWakes(db, {
1466
+ workspaceId: input.workspaceId,
1467
+ sessionId: input.sessionId,
1468
+ reason: "session_resume",
1469
+ });
1470
+ const [controlEvent] = await db
1471
+ .insert(schema.sessionEvents)
1472
+ .values({
1473
+ accountId: input.accountId,
1474
+ workspaceId: input.workspaceId,
1475
+ sessionId: input.sessionId,
1476
+ sequence: updated.lastSequence + 1,
1477
+ type: input.action === "pause" ? "session.control.paused" : "session.control.resumed",
1478
+ payload: {
1479
+ operationId: reserved.receipt.id,
1480
+ revision,
1481
+ actor,
1482
+ ...(input.reason ? { reason: input.reason } : {}),
1483
+ interruptionCount,
1484
+ },
1485
+ occurredAt: new Date(),
1486
+ })
1487
+ .returning({ id: schema.sessionEvents.id });
1488
+ if (!controlEvent)
1489
+ throw new SessionControlInvariantError("Session control event was not inserted");
1490
+ await db
1491
+ .update(schema.sessions)
1492
+ .set({ lastSequence: updated.lastSequence + 1, updatedAt: new Date() })
1493
+ .where(eq(schema.sessions.id, input.sessionId));
1494
+ await db.insert(schema.auditEvents).values({
1495
+ accountId: input.accountId,
1496
+ workspaceId: input.workspaceId,
1497
+ subjectId: actor,
1498
+ action: `session.control.${input.action}`,
1499
+ targetType: "session",
1500
+ targetId: input.sessionId,
1501
+ metadata: {
1502
+ operationId: reserved.receipt.id,
1503
+ revision,
1504
+ interruptionCount,
1505
+ ...(input.actor.type === "agent_attempt"
1506
+ ? {
1507
+ callerSessionId: input.actor.sessionId,
1508
+ callerTurnId: input.actor.turnId,
1509
+ callerAttemptId: input.actor.attemptId,
1510
+ callerExecutionGeneration: input.actor.executionGeneration,
1511
+ }
1512
+ : {}),
1513
+ },
1514
+ });
1515
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
1516
+ controlRevision: revision,
1517
+ result: {
1518
+ interruptionCount,
1519
+ wakeCount,
1520
+ eventId: controlEvent.id,
1521
+ workspaceControlEventId,
1522
+ },
1523
+ });
1524
+ const control = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1525
+ lock: "share",
1526
+ });
1527
+ return {
1528
+ receipt,
1529
+ control,
1530
+ sessionControlEventId: controlEvent.id,
1531
+ workspaceControlEventId,
1532
+ interruptionCount,
1533
+ wakeCount,
1534
+ replay: false,
1535
+ };
1536
+ }
1537
+
1538
+ export async function autoResumeSessionBranchInTransaction(
1539
+ db: Database,
1540
+ input: {
1541
+ workspaceId: string;
1542
+ sessionId: string;
1543
+ actor: string;
1544
+ reason: "human_send" | "human_steer" | "agent_steer";
1545
+ observedControlEtag?: string | null;
1546
+ },
1547
+ ): Promise<{
1548
+ revision: number;
1549
+ control: EffectiveSessionControl;
1550
+ changed: boolean;
1551
+ workspaceControlEventId: string | null;
1552
+ }> {
1553
+ const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1554
+ const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1555
+ lock: "share",
1556
+ });
1557
+ if (input.observedControlEtag && input.observedControlEtag !== before.controlEtag) {
1558
+ throw new SessionControlConflictError();
1559
+ }
1560
+ if (before.state === "active") {
1561
+ return {
1562
+ revision: asSafeRevision(workspace.revision, "workspace control revision")!,
1563
+ control: before,
1564
+ changed: false,
1565
+ workspaceControlEventId: null,
1566
+ };
1567
+ }
1568
+ const revision = nextRevision(workspace);
1569
+ await advanceWorkspaceRevision(db, input.workspaceId, revision);
1570
+ await db
1571
+ .update(schema.sessions)
1572
+ .set({
1573
+ directControlState: "active",
1574
+ directPauseRevision: null,
1575
+ subtreeRunOverrideRevision: revision,
1576
+ controlVersion: revision,
1577
+ directControlReason: input.reason,
1578
+ directControlChangedBy: input.actor,
1579
+ directControlChangedAt: new Date(),
1580
+ updatedAt: new Date(),
1581
+ })
1582
+ .where(
1583
+ and(
1584
+ eq(schema.sessions.workspaceId, input.workspaceId),
1585
+ eq(schema.sessions.id, input.sessionId),
1586
+ ),
1587
+ );
1588
+ const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
1589
+ accountId: workspace.accountId,
1590
+ workspaceId: input.workspaceId,
1591
+ revision,
1592
+ scope: "session",
1593
+ rootSessionId: input.sessionId,
1594
+ action: "resume",
1595
+ automatic: true,
1596
+ reason: input.reason,
1597
+ actor: input.actor,
1598
+ });
1599
+ return {
1600
+ revision,
1601
+ control: await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1602
+ lock: "share",
1603
+ }),
1604
+ changed: true,
1605
+ workspaceControlEventId,
1606
+ };
1607
+ }
1608
+
1609
+ export type WorkspaceControlMutationResult = {
1610
+ receipt: SessionCommandReceiptRow;
1611
+ revision: number;
1612
+ workspaceControlEventId: string;
1613
+ workspaceState: EffectiveControlState;
1614
+ interruptionCount: number;
1615
+ wakeCount: number;
1616
+ replay: boolean;
1617
+ };
1618
+
1619
+ export async function mutateWorkspaceControlInTransaction(
1620
+ db: Database,
1621
+ input: {
1622
+ accountId: string;
1623
+ workspaceId: string;
1624
+ actor: SessionCommandActor;
1625
+ operationKey: string;
1626
+ action: "pause" | "resume";
1627
+ reason?: string | null;
1628
+ expectedRevision?: number | null;
1629
+ },
1630
+ ): Promise<WorkspaceControlMutationResult> {
1631
+ const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1632
+ const currentRevision = asSafeRevision(workspace.revision, "workspace control revision")!;
1633
+ const hash = canonicalSessionCommandHash({
1634
+ action: input.action,
1635
+ reason: input.reason ?? null,
1636
+ expectedRevision: input.expectedRevision ?? null,
1637
+ });
1638
+ const reserved = await reserveSessionCommandReceipt(db, {
1639
+ accountId: input.accountId,
1640
+ workspaceId: input.workspaceId,
1641
+ actor: input.actor,
1642
+ action: `workspace.${input.action}`,
1643
+ targetSessionId: null,
1644
+ targetTurnId: null,
1645
+ operationKey: input.operationKey,
1646
+ canonicalRequestHash: hash,
1647
+ });
1648
+ if (reserved.replay && reserved.receipt.appliedControlRevision !== null) {
1649
+ const workspaceControlEventId = String(reserved.receipt.result.workspaceControlEventId ?? "");
1650
+ if (!workspaceControlEventId) {
1651
+ throw new SessionControlInvariantError("Replayed workspace control receipt has no event");
1652
+ }
1653
+ return {
1654
+ receipt: reserved.receipt,
1655
+ revision: Number(reserved.receipt.appliedControlRevision),
1656
+ workspaceControlEventId,
1657
+ workspaceState: input.action === "pause" ? "paused" : "active",
1658
+ interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
1659
+ wakeCount: Number(reserved.receipt.result.wakeCount ?? 0),
1660
+ replay: true,
1661
+ };
1662
+ }
1663
+ if (input.expectedRevision !== null && input.expectedRevision !== undefined) {
1664
+ if (input.expectedRevision !== currentRevision) throw new SessionControlConflictError();
1665
+ }
1666
+
1667
+ const revision = nextRevision(workspace);
1668
+ const actor =
1669
+ input.actor.type === "agent_attempt"
1670
+ ? `attempt:${input.actor.attemptId}`
1671
+ : input.actor.subjectId;
1672
+ const [updated] = await db
1673
+ .update(schema.workspaceInferenceControls)
1674
+ .set({
1675
+ revision,
1676
+ workspaceState: input.action === "pause" ? "paused" : "active",
1677
+ workspacePauseRevision: input.action === "pause" ? revision : null,
1678
+ reason: input.reason ?? null,
1679
+ changedBy: actor,
1680
+ changedAt: new Date(),
1681
+ updatedAt: new Date(),
1682
+ })
1683
+ .where(
1684
+ and(
1685
+ eq(schema.workspaceInferenceControls.workspaceId, input.workspaceId),
1686
+ eq(schema.workspaceInferenceControls.revision, currentRevision),
1687
+ ),
1688
+ )
1689
+ .returning({ workspaceId: schema.workspaceInferenceControls.workspaceId });
1690
+ if (!updated) {
1691
+ throw new SessionControlInvariantError("Workspace control did not mutate exactly once");
1692
+ }
1693
+ const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
1694
+ accountId: input.accountId,
1695
+ workspaceId: input.workspaceId,
1696
+ revision,
1697
+ scope: "workspace",
1698
+ rootSessionId: null,
1699
+ action: input.action,
1700
+ automatic: false,
1701
+ reason: input.reason ?? null,
1702
+ actor,
1703
+ });
1704
+ const interruptionCount =
1705
+ input.action === "pause"
1706
+ ? await interruptWorkspaceAttempts(db, {
1707
+ workspaceId: input.workspaceId,
1708
+ operationId: reserved.receipt.id,
1709
+ controlRevision: revision,
1710
+ })
1711
+ : 0;
1712
+ const wakeCount =
1713
+ input.action === "pause"
1714
+ ? await registerInterruptionWakes(db, {
1715
+ operationId: reserved.receipt.id,
1716
+ reason: "workspace_pause_interruption",
1717
+ })
1718
+ : await registerWorkspaceWakes(db, {
1719
+ workspaceId: input.workspaceId,
1720
+ reason: "workspace_resume",
1721
+ });
1722
+ const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
1723
+ controlRevision: revision,
1724
+ result: { interruptionCount, wakeCount, workspaceControlEventId },
1725
+ });
1726
+ return {
1727
+ receipt,
1728
+ revision,
1729
+ workspaceControlEventId,
1730
+ workspaceState: input.action === "pause" ? "paused" : "active",
1731
+ interruptionCount,
1732
+ wakeCount,
1733
+ replay: false,
1734
+ };
1735
+ }
1736
+
1737
+ async function insertWorkspaceControlEventInTransaction(
1738
+ db: Database,
1739
+ input: {
1740
+ accountId: string;
1741
+ workspaceId: string;
1742
+ revision: number;
1743
+ scope: "workspace" | "session";
1744
+ rootSessionId: string | null;
1745
+ action: "pause" | "resume";
1746
+ automatic: boolean;
1747
+ reason: string | null;
1748
+ actor: string;
1749
+ },
1750
+ ): Promise<string> {
1751
+ const [event] = await db
1752
+ .insert(schema.workspaceControlEvents)
1753
+ .values(input)
1754
+ .returning({ id: schema.workspaceControlEvents.id });
1755
+ if (!event) {
1756
+ throw new SessionControlInvariantError("Workspace control event was not inserted");
1757
+ }
1758
+ return event.id;
1759
+ }