@opengeni/db 0.7.3 → 0.10.7

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 (70) hide show
  1. package/dist/{chunk-YFQ7SGE4.js → chunk-BMFDXFPA.js} +23 -1
  2. package/dist/chunk-BMFDXFPA.js.map +1 -0
  3. package/dist/chunk-KW526IJA.js +127 -0
  4. package/dist/chunk-KW526IJA.js.map +1 -0
  5. package/dist/{chunk-B22X3IEZ.js → chunk-P6PKXY5W.js} +604 -32
  6. package/dist/chunk-P6PKXY5W.js.map +1 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +7408 -2276
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.d.ts +29 -4
  11. package/dist/migrate.js +3 -1
  12. package/dist/provision-roles.d.ts +1122 -73
  13. package/dist/provision-roles.js +1 -1
  14. package/dist/{schema-BN5mB9xZ.d.ts → schema-CqkzrBRS.d.ts} +6967 -4457
  15. package/dist/schema.d.ts +1 -1
  16. package/dist/schema.js +19 -1
  17. package/drizzle/0053_codex_credential_leases.sql +2 -2
  18. package/drizzle/0057_durable_queue_control.sql +1 -1
  19. package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
  20. package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
  21. package/drizzle/0063_session_control_mega_foundation.sql +1 -1
  22. package/drizzle/0064_rotation_strategy_sharded_backfill.sql +15 -0
  23. package/drizzle/0065_codex_subscription_overview.sql +168 -0
  24. package/drizzle/0065_session_attempt_quiescence.sql +26 -0
  25. package/drizzle/0065_session_tool_policy.sql +38 -0
  26. package/drizzle/0066_session_interruption_attempt_lookup.sql +4 -0
  27. package/drizzle/0067_session_event_payload_bounds.sql +209 -0
  28. package/drizzle/0068_workspace_control_event_bounds.sql +134 -0
  29. package/drizzle/0069_session_event_history_backfill.sql +32 -0
  30. package/drizzle/0070_session_event_type_sequence_lookup.sql +4 -0
  31. package/drizzle/0071_session_event_monitoring_tail.sql +10 -0
  32. package/drizzle/0072_sessions_workspace_created_id_idx.sql +4 -0
  33. package/drizzle/0073_sessions_workspace_updated_id_idx.sql +4 -0
  34. package/drizzle/0074_session_activity_revisions.sql +72 -0
  35. package/drizzle/0075_sessions_workspace_activity_revision_idx.sql +4 -0
  36. package/drizzle/0076_session_workflow_wake_acl.sql +13 -0
  37. package/drizzle/0077_session_attempt_latest_lookup.sql +4 -0
  38. package/drizzle/0094_quarantine_credential_bearing_catalog_urls.sql +51 -0
  39. package/drizzle/0095_github_existing_installations.sql +84 -0
  40. package/drizzle/0096_session_turn_initiators.sql +97 -0
  41. package/drizzle/0097_host_export_outbox.sql +1220 -0
  42. package/drizzle/0098_usage_events_workspace_session_idx.sql +4 -0
  43. package/drizzle/0099_session_human_input_attempt_owner_index.sql +6 -0
  44. package/drizzle/0100_session_human_input_requests.sql +89 -0
  45. package/drizzle/0101_session_mcp_connection_refs.sql +30 -0
  46. package/drizzle/0102_session_command_receipt_service_actor.sql +16 -0
  47. package/drizzle/0103_host_export_root_session.sql +166 -0
  48. package/drizzle/0104_host_export_root_session_backfill.sql +27 -0
  49. package/drizzle/0105_session_turn_instructions.sql +9 -0
  50. package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
  51. package/drizzle/0107_host_export_lineage_contract.sql +381 -0
  52. package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
  53. package/package.json +5 -4
  54. package/src/codex-token-resolver.ts +175 -14
  55. package/src/connection-token-resolver.ts +428 -119
  56. package/src/event-payload-sanitizer.ts +88 -20
  57. package/src/index.ts +7208 -1273
  58. package/src/memory-domain.ts +1 -1
  59. package/src/migrate.ts +86 -23
  60. package/src/persistence-errors.ts +252 -0
  61. package/src/provision-roles.ts +42 -0
  62. package/src/schema.ts +658 -34
  63. package/src/session-control.ts +520 -38
  64. package/src/session-queue-commands.ts +385 -61
  65. package/src/session-tool-call-settlement.ts +58 -7
  66. package/src/turn-initiator.ts +155 -0
  67. package/dist/chunk-7LDU7F5P.js +0 -80
  68. package/dist/chunk-7LDU7F5P.js.map +0 -1
  69. package/dist/chunk-B22X3IEZ.js.map +0 -1
  70. package/dist/chunk-YFQ7SGE4.js.map +0 -1
@@ -1,18 +1,24 @@
1
1
  import {
2
+ metadataWithTurnExecutionPolicyV1,
2
3
  mergeResourceRefs,
3
4
  mergeToolRefs,
5
+ turnExecutionPolicyAuditMetadata,
4
6
  type ReasoningEffort,
5
7
  type ResourceRef,
8
+ type SessionToolPolicy,
6
9
  type ToolRef,
10
+ type TurnExecutionPolicyV1,
7
11
  } from "@opengeni/contracts";
8
12
  import { and, asc, eq, inArray, sql } from "drizzle-orm";
9
13
  import type { Database } from "./index";
14
+ import { sanitizeEventPayload } from "./event-payload-sanitizer";
10
15
  import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
11
16
  import {
12
17
  assertAgentCommandAuthorityInTransaction,
13
18
  autoResumeSessionBranchInTransaction,
14
19
  canonicalSessionCommandHash,
15
20
  evaluateSessionControl,
21
+ lockSessionEventWriteRows,
16
22
  lockWorkspaceInferenceControl,
17
23
  registerInternalUpdateWakeInTransaction,
18
24
  reserveSessionCommandReceipt,
@@ -24,6 +30,12 @@ import {
24
30
  updateSessionCommandReceiptResult,
25
31
  } from "./session-control";
26
32
  import * as schema from "./schema";
33
+ import {
34
+ frozenInitiatorForCommandActor,
35
+ initiatorColumns,
36
+ initiatorFromStorage,
37
+ type FrozenTurnInitiator,
38
+ } from "./turn-initiator";
27
39
 
28
40
  export type QueueCommandConflictCode =
29
41
  | "QUEUE_VERSION_CHANGED"
@@ -31,7 +43,8 @@ export type QueueCommandConflictCode =
31
43
  | "QUEUE_ANCHOR_CHANGED"
32
44
  | "PROMPT_CHANGED"
33
45
  | "DRAFT_CHANGED"
34
- | "DRAFT_NOT_EMPTY";
46
+ | "DRAFT_NOT_EMPTY"
47
+ | "EDIT_SOURCE_CHANGED";
35
48
 
36
49
  export class QueueCommandConflictError extends Error {
37
50
  readonly name = "QueueCommandConflictError";
@@ -99,12 +112,13 @@ async function lockSession(
99
112
  workspaceId: string,
100
113
  sessionId: string,
101
114
  ): 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);
115
+ const locks = await lockSessionEventWriteRows(db, {
116
+ workspaceId,
117
+ controlLock: "already_locked",
118
+ workspaceLock: "already_locked",
119
+ sessionIds: [sessionId],
120
+ });
121
+ const session = locks.sessions[0];
108
122
  if (!session) throw new Error(`Session not found: ${sessionId}`);
109
123
  return session;
110
124
  }
@@ -142,7 +156,7 @@ export async function supersedeSessionCurrentDirectionInTransaction(
142
156
  lastSequence: input.lastSequence,
143
157
  };
144
158
  }
145
- const [current] = await db
159
+ const [preview] = await db
146
160
  .select()
147
161
  .from(schema.sessionTurns)
148
162
  .where(
@@ -152,8 +166,15 @@ export async function supersedeSessionCurrentDirectionInTransaction(
152
166
  eq(schema.sessionTurns.id, input.activeTurnId),
153
167
  ),
154
168
  )
155
- .for("update")
156
169
  .limit(1);
170
+ const locks = await lockSessionEventWriteRows(db, {
171
+ workspaceId: input.workspaceId,
172
+ controlLock: "already_locked",
173
+ workspaceLock: "already_locked",
174
+ turnIds: [input.activeTurnId],
175
+ attemptIds: preview?.activeAttemptId ? [preview.activeAttemptId] : [],
176
+ });
177
+ const current = locks.turns[0];
157
178
  if (!current) {
158
179
  throw new SessionControlInvariantError(
159
180
  `Session ${input.sessionId} points to missing active turn ${input.activeTurnId}`,
@@ -206,6 +227,44 @@ export async function supersedeSessionCurrentDirectionInTransaction(
206
227
  sequence: input.lastSequence,
207
228
  now,
208
229
  });
230
+ const cancelledHumanInputs = await db
231
+ .update(schema.sessionHumanInputRequests)
232
+ .set({
233
+ status: "cancelled",
234
+ response: { outcome: "cancelled" },
235
+ respondedBy:
236
+ input.actor.type === "agent_attempt"
237
+ ? `attempt:${input.actor.attemptId}`
238
+ : input.actor.subjectId,
239
+ respondedAt: now,
240
+ updatedAt: now,
241
+ })
242
+ .where(
243
+ and(
244
+ eq(schema.sessionHumanInputRequests.workspaceId, input.workspaceId),
245
+ eq(schema.sessionHumanInputRequests.sessionId, input.sessionId),
246
+ eq(schema.sessionHumanInputRequests.turnId, current.id),
247
+ eq(schema.sessionHumanInputRequests.status, "pending"),
248
+ ),
249
+ )
250
+ .returning({ id: schema.sessionHumanInputRequests.id });
251
+ let lastSequence = closedTools.sequence;
252
+ if (cancelledHumanInputs.length > 0) {
253
+ await db.insert(schema.sessionEvents).values(
254
+ cancelledHumanInputs.map((request) => ({
255
+ accountId: input.accountId,
256
+ workspaceId: input.workspaceId,
257
+ sessionId: input.sessionId,
258
+ sequence: ++lastSequence,
259
+ type: "user.humanInputResponse",
260
+ turnId: current.id,
261
+ turnGeneration: current.executionGeneration,
262
+ turnAssociation: "current",
263
+ payload: { requestId: request.id, response: { outcome: "cancelled" } },
264
+ occurredAt: now,
265
+ })),
266
+ );
267
+ }
209
268
  await db
210
269
  .update(schema.sessionTurns)
211
270
  .set({
@@ -248,7 +307,7 @@ export async function supersedeSessionCurrentDirectionInTransaction(
248
307
  interruptionCount: 0,
249
308
  replacedTurn: current,
250
309
  liveCurrentTurnId: null,
251
- lastSequence: closedTools.sequence,
310
+ lastSequence,
252
311
  };
253
312
  }
254
313
 
@@ -274,7 +333,16 @@ async function loadQueuedTurns(
274
333
  asc(schema.sessionTurns.createdAt),
275
334
  asc(schema.sessionTurns.id),
276
335
  );
277
- return lock ? await query.for("update") : await query;
336
+ const rows = await query;
337
+ if (!lock || rows.length === 0) return rows;
338
+ const locks = await lockSessionEventWriteRows(db, {
339
+ workspaceId,
340
+ controlLock: "already_locked",
341
+ workspaceLock: "already_locked",
342
+ turnIds: rows.map((row) => row.id),
343
+ });
344
+ const byId = new Map(locks.turns.map((row) => [row.id, row]));
345
+ return rows.map((row) => byId.get(row.id)).filter((row): row is QueuedTurnRow => Boolean(row));
278
346
  }
279
347
 
280
348
  async function normalizeQueuePositions(
@@ -314,13 +382,19 @@ function draftIsNonEmpty(draft: ComposerDraftRow): boolean {
314
382
  draft.text.length > 0 ||
315
383
  draft.resources.length > 0 ||
316
384
  draft.tools.length > 0 ||
385
+ draft.toolsProvided ||
317
386
  draft.sourceTurnId !== null
318
387
  );
319
388
  }
320
389
 
321
390
  export async function getComposerDraftInTransaction(
322
391
  db: Database,
323
- input: { workspaceId: string; sessionId: string; subjectId: string; lock?: boolean },
392
+ input: {
393
+ workspaceId: string;
394
+ sessionId: string;
395
+ subjectId: string;
396
+ lock?: boolean;
397
+ },
324
398
  ): Promise<ComposerDraftRow | null> {
325
399
  const query = db
326
400
  .select()
@@ -348,13 +422,21 @@ export async function saveComposerDraftInTransaction(
348
422
  text: string;
349
423
  resources: ResourceRef[];
350
424
  tools: ToolRef[];
425
+ toolsProvided: boolean;
351
426
  model: string;
352
427
  reasoningEffort: ReasoningEffort;
353
428
  },
354
429
  ): Promise<ComposerDraftRow> {
355
430
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
431
+ await lockSessionEventWriteRows(db, {
432
+ workspaceId: input.workspaceId,
433
+ controlLock: "already_locked",
434
+ });
356
435
  await lockSession(db, input.workspaceId, input.sessionId);
357
- const current = await getComposerDraftInTransaction(db, { ...input, lock: true });
436
+ const current = await getComposerDraftInTransaction(db, {
437
+ ...input,
438
+ lock: true,
439
+ });
358
440
  const currentRevision = current?.revision ?? 0;
359
441
  if (currentRevision !== input.expectedRevision) {
360
442
  throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
@@ -372,10 +454,14 @@ export async function saveComposerDraftInTransaction(
372
454
  text: input.text,
373
455
  resources: input.resources,
374
456
  tools: input.tools,
457
+ toolsProvided: input.toolsProvided,
375
458
  model: input.model,
376
459
  reasoningEffort: input.reasoningEffort,
377
- sourceTurnId: null,
378
- sourceTurnVersion: null,
460
+ // A queue edit is still the same accepted work item. Preserve its frozen
461
+ // initiator through arbitrary draft saves; only a genuinely new compose or
462
+ // Steer captures the submitting actor.
463
+ sourceTurnId: current?.sourceTurnId ?? null,
464
+ sourceTurnVersion: current?.sourceTurnVersion ?? null,
379
465
  updatedAt: new Date(),
380
466
  };
381
467
  const [saved] = current
@@ -403,6 +489,10 @@ export async function moveQueuedTurnInTransaction(
403
489
  },
404
490
  ): Promise<QueueCommandResult> {
405
491
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
492
+ await lockSessionEventWriteRows(db, {
493
+ workspaceId: input.workspaceId,
494
+ controlLock: "already_locked",
495
+ });
406
496
  const session = await lockSession(db, input.workspaceId, input.sessionId);
407
497
  const requestHash = canonicalSessionCommandHash({
408
498
  beforeTurnId: input.beforeTurnId,
@@ -494,7 +584,11 @@ export async function moveQueuedTurnInTransaction(
494
584
  eventIds.push(event.id);
495
585
  await db
496
586
  .update(schema.sessions)
497
- .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: new Date() })
587
+ .set({
588
+ queueVersion,
589
+ lastSequence: session.lastSequence + 1,
590
+ updatedAt: new Date(),
591
+ })
498
592
  .where(eq(schema.sessions.id, input.sessionId));
499
593
  }
500
594
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
@@ -525,6 +619,10 @@ export async function deleteSessionQueueItemInTransaction(
525
619
  },
526
620
  ): Promise<QueueCommandResult> {
527
621
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
622
+ await lockSessionEventWriteRows(db, {
623
+ workspaceId: input.workspaceId,
624
+ controlLock: "already_locked",
625
+ });
528
626
  const session = await lockSession(db, input.workspaceId, input.sessionId);
529
627
  const requestHash = canonicalSessionCommandHash({
530
628
  expectedTurnVersion: input.expectedTurnVersion,
@@ -549,18 +647,14 @@ export async function deleteSessionQueueItemInTransaction(
549
647
  replay: true,
550
648
  };
551
649
  }
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);
650
+ const turn = (
651
+ await lockSessionEventWriteRows(db, {
652
+ workspaceId: input.workspaceId,
653
+ controlLock: "already_locked",
654
+ workspaceLock: "already_locked",
655
+ turnIds: [input.turnId],
656
+ })
657
+ ).turns[0];
564
658
  if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
565
659
  throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
566
660
  queueVersion: session.queueVersion,
@@ -616,7 +710,11 @@ export async function deleteSessionQueueItemInTransaction(
616
710
  if (!event) throw new Error("Queue delete event was not inserted");
617
711
  await db
618
712
  .update(schema.sessions)
619
- .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
713
+ .set({
714
+ queueVersion,
715
+ lastSequence: session.lastSequence + 1,
716
+ updatedAt: now,
717
+ })
620
718
  .where(eq(schema.sessions.id, input.sessionId));
621
719
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
622
720
  queueVersion,
@@ -648,6 +746,10 @@ export async function editQueuedTurnInTransaction(
648
746
  },
649
747
  ): Promise<EditQueueCommandResult> {
650
748
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
749
+ await lockSessionEventWriteRows(db, {
750
+ workspaceId: input.workspaceId,
751
+ controlLock: "already_locked",
752
+ });
651
753
  const session = await lockSession(db, input.workspaceId, input.sessionId);
652
754
  const requestHash = canonicalSessionCommandHash({
653
755
  expectedTurnVersion: input.expectedTurnVersion,
@@ -664,7 +766,10 @@ export async function editQueuedTurnInTransaction(
664
766
  operationKey: input.operationKey,
665
767
  canonicalRequestHash: requestHash,
666
768
  });
667
- const existingDraft = await getComposerDraftInTransaction(db, { ...input, lock: true });
769
+ const existingDraft = await getComposerDraftInTransaction(db, {
770
+ ...input,
771
+ lock: true,
772
+ });
668
773
  if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
669
774
  if (!existingDraft) throw new Error("Replayed queue Edit has no durable draft");
670
775
  return {
@@ -689,18 +794,14 @@ export async function editQueuedTurnInTransaction(
689
794
  draftRevision,
690
795
  });
691
796
  }
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);
797
+ const turn = (
798
+ await lockSessionEventWriteRows(db, {
799
+ workspaceId: input.workspaceId,
800
+ controlLock: "already_locked",
801
+ workspaceLock: "already_locked",
802
+ turnIds: [input.turnId],
803
+ })
804
+ ).turns[0];
704
805
  if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
705
806
  throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
706
807
  queueVersion: session.queueVersion,
@@ -725,6 +826,7 @@ export async function editQueuedTurnInTransaction(
725
826
  text: turn.prompt,
726
827
  resources: turn.resources,
727
828
  tools: turn.tools,
829
+ toolsProvided: turn.toolsProvided,
728
830
  model: turn.model,
729
831
  reasoningEffort: turn.reasoningEffort,
730
832
  sourceTurnId: turn.id,
@@ -780,7 +882,11 @@ export async function editQueuedTurnInTransaction(
780
882
  if (!event) throw new Error("Queue edit event was not inserted");
781
883
  await db
782
884
  .update(schema.sessions)
783
- .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
885
+ .set({
886
+ queueVersion,
887
+ lastSequence: session.lastSequence + 1,
888
+ updatedAt: now,
889
+ })
784
890
  .where(eq(schema.sessions.id, input.sessionId));
785
891
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
786
892
  queueVersion,
@@ -788,7 +894,14 @@ export async function editQueuedTurnInTransaction(
788
894
  draftRevision: nextDraftRevision,
789
895
  result: { sourceTurnId: turn.id },
790
896
  });
791
- return { receipt, queueVersion, items: remaining, draft, eventIds: [event.id], replay: false };
897
+ return {
898
+ receipt,
899
+ queueVersion,
900
+ items: remaining,
901
+ draft,
902
+ eventIds: [event.id],
903
+ replay: false,
904
+ };
792
905
  }
793
906
 
794
907
  export async function steerQueuedTurnInTransaction(
@@ -804,7 +917,18 @@ export async function steerQueuedTurnInTransaction(
804
917
  operationKey: string;
805
918
  },
806
919
  ): Promise<SteerQueueCommandResult> {
807
- await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
920
+ const workspaceControl = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
921
+ await lockSessionEventWriteRows(db, {
922
+ workspaceId: input.workspaceId,
923
+ controlLock: "already_locked",
924
+ sessionIds:
925
+ input.actor.type === "agent_attempt"
926
+ ? [input.actor.sessionId, input.sessionId]
927
+ : [input.sessionId],
928
+ turnIds:
929
+ input.actor.type === "agent_attempt" ? [input.actor.turnId, input.turnId] : [input.turnId],
930
+ attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : [],
931
+ });
808
932
  const requestHash = canonicalSessionCommandHash({
809
933
  expectedTurnVersion: input.expectedTurnVersion,
810
934
  controlEtag: input.controlEtag ?? null,
@@ -843,7 +967,7 @@ export async function steerQueuedTurnInTransaction(
843
967
  });
844
968
  }
845
969
  const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
846
- lock: "share",
970
+ workspaceControl,
847
971
  });
848
972
  if (input.controlEtag && input.controlEtag !== before.controlEtag) {
849
973
  throw new SessionControlConflictError();
@@ -885,6 +1009,7 @@ export async function steerQueuedTurnInTransaction(
885
1009
  });
886
1010
  const interruptionCount = supersession.interruptionCount;
887
1011
  const supersededTurnId = supersession.replacedTurn?.id ?? null;
1012
+ const supersededAttemptId = supersession.replacedTurn?.activeAttemptId ?? null;
888
1013
  const liveCurrentTurnId = supersession.liveCurrentTurnId;
889
1014
 
890
1015
  const withoutTarget = rows.filter((row) => row.id !== target.id);
@@ -899,7 +1024,17 @@ export async function steerQueuedTurnInTransaction(
899
1024
  const queueVersion = session.queueVersion + 1;
900
1025
  await db
901
1026
  .update(schema.sessionTurns)
902
- .set({ version: target.version + 1, updatedAt: now })
1027
+ .set({
1028
+ version: target.version + 1,
1029
+ metadata: {
1030
+ ...target.metadata,
1031
+ delivery: "steer",
1032
+ replacedTurnId: supersededTurnId,
1033
+ replacedAttemptId: supersededAttemptId,
1034
+ interruptionCount,
1035
+ },
1036
+ updatedAt: now,
1037
+ })
903
1038
  .where(eq(schema.sessionTurns.id, target.id));
904
1039
  let sequence = supersession.lastSequence;
905
1040
  const actor =
@@ -998,33 +1133,62 @@ export async function submitHumanPromptInTransaction(
998
1133
  workspaceId: string;
999
1134
  sessionId: string;
1000
1135
  subjectId: string;
1136
+ subjectLabel?: string;
1001
1137
  actor: SessionCommandActor;
1002
1138
  operationKey: string;
1003
1139
  delivery: "send" | "steer";
1004
1140
  controlEtag?: string | null;
1005
1141
  expectedDraftRevision?: number | null;
1006
1142
  text: string;
1143
+ turnInstructions?: string | null;
1007
1144
  resources: ResourceRef[];
1008
1145
  tools: ToolRef[];
1146
+ toolsProvided?: boolean;
1009
1147
  model?: string | null;
1010
1148
  reasoningEffort?: ReasoningEffort | null;
1011
1149
  reasoningEffortFallback: ReasoningEffort;
1150
+ /** Trusted API/core admission snapshot. Omitted only by legacy low-level callers. */
1151
+ turnExecutionPolicy?: TurnExecutionPolicyV1;
1012
1152
  source: "user" | "api";
1013
- mcpCredentialUpdates?: Array<{ id: string; headersEncrypted: Record<string, string> }>;
1153
+ mcpCredentialUpdates?: Array<{
1154
+ id: string;
1155
+ headersEncrypted: Record<string, string>;
1156
+ }>;
1014
1157
  },
1015
1158
  ): Promise<SubmitHumanPromptResult> {
1016
- await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1159
+ const workspaceControl = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1160
+ await lockSessionEventWriteRows(db, {
1161
+ workspaceId: input.workspaceId,
1162
+ controlLock: "already_locked",
1163
+ sessionIds:
1164
+ input.actor.type === "agent_attempt"
1165
+ ? [input.actor.sessionId, input.sessionId]
1166
+ : [input.sessionId],
1167
+ turnIds: input.actor.type === "agent_attempt" ? [input.actor.turnId] : [],
1168
+ attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : [],
1169
+ });
1017
1170
  const requestHash = canonicalSessionCommandHash({
1018
1171
  delivery: input.delivery,
1019
1172
  controlEtag: input.controlEtag ?? null,
1020
1173
  expectedDraftRevision: input.expectedDraftRevision ?? null,
1021
1174
  text: input.text,
1175
+ turnInstructions: input.turnInstructions ?? null,
1022
1176
  resources: input.resources,
1023
1177
  tools: input.tools,
1178
+ toolsProvided: input.toolsProvided === true,
1024
1179
  model: input.model ?? null,
1025
1180
  reasoningEffort: input.reasoningEffort ?? null,
1026
1181
  source: input.source,
1027
1182
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1183
+ ...(input.actor.type === "service"
1184
+ ? {
1185
+ serviceInitiator: {
1186
+ subjectId: input.actor.subjectId,
1187
+ subjectLabel: input.actor.subjectLabel ?? null,
1188
+ context: input.actor.context ?? {},
1189
+ },
1190
+ }
1191
+ : {}),
1028
1192
  });
1029
1193
  const reserved = await reserveSessionCommandReceipt(db, {
1030
1194
  accountId: input.accountId,
@@ -1063,7 +1227,7 @@ export async function submitHumanPromptInTransaction(
1063
1227
  }
1064
1228
 
1065
1229
  const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1066
- lock: "share",
1230
+ workspaceControl,
1067
1231
  });
1068
1232
  if (input.controlEtag && input.controlEtag !== before.controlEtag) {
1069
1233
  throw new SessionControlConflictError();
@@ -1076,7 +1240,14 @@ export async function submitHumanPromptInTransaction(
1076
1240
  input.actor.type === "agent_attempt"
1077
1241
  ? `attempt:${input.actor.attemptId}`
1078
1242
  : input.actor.subjectId,
1079
- reason: input.delivery === "steer" ? "human_steer" : "human_send",
1243
+ reason:
1244
+ input.actor.type === "service"
1245
+ ? input.delivery === "steer"
1246
+ ? "service_steer"
1247
+ : "service_send"
1248
+ : input.delivery === "steer"
1249
+ ? "human_steer"
1250
+ : "human_send",
1080
1251
  observedControlEtag: input.controlEtag ?? null,
1081
1252
  });
1082
1253
  const session = await lockSession(db, input.workspaceId, input.sessionId);
@@ -1113,6 +1284,7 @@ export async function submitHumanPromptInTransaction(
1113
1284
  text: draft.text,
1114
1285
  resources: draft.resources,
1115
1286
  tools: draft.tools,
1287
+ toolsProvided: draft.toolsProvided,
1116
1288
  model: draft.model,
1117
1289
  reasoningEffort: draft.reasoningEffort,
1118
1290
  }) !==
@@ -1120,6 +1292,7 @@ export async function submitHumanPromptInTransaction(
1120
1292
  text: input.text,
1121
1293
  resources: input.resources,
1122
1294
  tools: input.tools,
1295
+ toolsProvided: input.toolsProvided === true,
1123
1296
  model: input.model ?? session.model,
1124
1297
  reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
1125
1298
  })
@@ -1135,6 +1308,52 @@ export async function submitHumanPromptInTransaction(
1135
1308
  }
1136
1309
  }
1137
1310
 
1311
+ let editedSourceTurn: QueuedTurnRow | undefined;
1312
+ let editedSourceTurnInstructions: string | null | undefined;
1313
+ if (draft?.sourceTurnId) {
1314
+ const sourceLocks = await lockSessionEventWriteRows(db, {
1315
+ workspaceId: input.workspaceId,
1316
+ controlLock: "already_locked",
1317
+ workspaceLock: "already_locked",
1318
+ turnIds: [draft.sourceTurnId],
1319
+ });
1320
+ const sourceTurn = sourceLocks.turns[0];
1321
+ const sourceTurnVersion = draft.sourceTurnVersion;
1322
+ const sourceMetadata = sourceTurn?.metadata ?? {};
1323
+ const sourceIsExactWithdrawnRevision =
1324
+ sourceTurn !== undefined &&
1325
+ sourceTurn.accountId === input.accountId &&
1326
+ sourceTurn.workspaceId === input.workspaceId &&
1327
+ sourceTurn.sessionId === input.sessionId &&
1328
+ (sourceTurn.source === "user" || sourceTurn.source === "api") &&
1329
+ sourceTurn.status === "withdrawn_for_edit" &&
1330
+ sourceTurnVersion !== null &&
1331
+ sourceTurn.version === sourceTurnVersion + 1 &&
1332
+ sourceTurn.cancelledBy === input.subjectId &&
1333
+ sourceTurn.cancelReason === "withdrawn_for_edit" &&
1334
+ sourceTurn.activeAttemptId === null &&
1335
+ sourceMetadata.delivery !== "steer";
1336
+ if (!sourceIsExactWithdrawnRevision) {
1337
+ throw new QueueCommandConflictError(
1338
+ "EDIT_SOURCE_CHANGED",
1339
+ "Edited prompt source changed or is no longer withdrawn for edit",
1340
+ {
1341
+ queueVersion: session.queueVersion,
1342
+ draftRevision: draft.revision,
1343
+ ...(sourceTurn ? { turnVersion: sourceTurn.version } : {}),
1344
+ },
1345
+ );
1346
+ }
1347
+ // This is the sole private-instruction source for an edited replacement.
1348
+ // It is deliberately held separately from the public draft and event
1349
+ // projections below. The public draft is expected to differ after editing;
1350
+ // source identity is fenced by its exact withdrawn row version, not by
1351
+ // comparing the replacement content with the old prompt. A client-supplied
1352
+ // instruction value must never override the private source value.
1353
+ editedSourceTurn = sourceTurn;
1354
+ editedSourceTurnInstructions = sourceTurn.turnInstructions ?? null;
1355
+ }
1356
+
1138
1357
  for (const update of input.mcpCredentialUpdates ?? []) {
1139
1358
  const [server] = await db
1140
1359
  .update(schema.sessionMcpServers)
@@ -1155,6 +1374,28 @@ export async function submitHumanPromptInTransaction(
1155
1374
  }
1156
1375
 
1157
1376
  const now = new Date();
1377
+ let frozenInitiator: FrozenTurnInitiator;
1378
+ if (input.delivery === "send" && draft?.sourceTurnId) {
1379
+ const sourceTurn = editedSourceTurn;
1380
+ if (!sourceTurn) {
1381
+ throw new SessionControlInvariantError("Edited prompt source turn is missing");
1382
+ }
1383
+ frozenInitiator = {
1384
+ initiator: initiatorFromStorage(
1385
+ sourceTurn.initiatorKind,
1386
+ sourceTurn.initiatorSubjectId,
1387
+ sourceTurn.initiatorContext ?? {},
1388
+ ),
1389
+ context: sourceTurn.initiatorContext ?? {},
1390
+ };
1391
+ } else {
1392
+ frozenInitiator = await frozenInitiatorForCommandActor(
1393
+ db,
1394
+ input.workspaceId,
1395
+ input.actor,
1396
+ input.subjectLabel,
1397
+ );
1398
+ }
1158
1399
  const acceptedEventId = crypto.randomUUID();
1159
1400
  const turnId = crypto.randomUUID();
1160
1401
  const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
@@ -1168,14 +1409,19 @@ export async function submitHumanPromptInTransaction(
1168
1409
  sequence: ++sequence,
1169
1410
  type: "user.message",
1170
1411
  clientEventId: input.operationKey,
1171
- payload: {
1412
+ payload: sanitizeEventPayload({
1172
1413
  text: input.text,
1173
1414
  ...(input.resources.length ? { resources: input.resources } : {}),
1174
- ...(input.tools.length ? { tools: input.tools } : {}),
1415
+ ...(input.toolsProvided === true
1416
+ ? { tools: input.tools }
1417
+ : input.tools.length
1418
+ ? { tools: input.tools }
1419
+ : {}),
1175
1420
  ...(input.model ? { model: input.model } : {}),
1176
1421
  ...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
1177
1422
  delivery: input.delivery,
1178
- },
1423
+ initiator: frozenInitiator.initiator,
1424
+ }),
1179
1425
  occurredAt: now,
1180
1426
  },
1181
1427
  ];
@@ -1193,13 +1439,21 @@ export async function submitHumanPromptInTransaction(
1193
1439
  source: input.source,
1194
1440
  position: input.delivery === "steer" ? 0 : existingQueued.length + 1,
1195
1441
  prompt: input.text,
1442
+ turnInstructions:
1443
+ editedSourceTurnInstructions !== undefined
1444
+ ? editedSourceTurnInstructions
1445
+ : (input.turnInstructions ?? null),
1196
1446
  resources: input.resources,
1197
1447
  tools: input.tools,
1448
+ toolsProvided: input.toolsProvided === true,
1198
1449
  model: input.model ?? session.model,
1199
1450
  reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
1200
1451
  sandboxBackend: session.sandboxBackend,
1201
- metadata: {},
1452
+ metadata: input.turnExecutionPolicy
1453
+ ? metadataWithTurnExecutionPolicyV1({}, input.turnExecutionPolicy)
1454
+ : {},
1202
1455
  lineage: { actor: input.actor.type },
1456
+ ...initiatorColumns(frozenInitiator),
1203
1457
  })
1204
1458
  .returning();
1205
1459
  if (!turn) throw new SessionControlInvariantError("Prompt turn was not inserted");
@@ -1210,7 +1464,12 @@ export async function submitHumanPromptInTransaction(
1210
1464
  sequence: ++sequence,
1211
1465
  type: "turn.queued",
1212
1466
  turnId,
1213
- payload: { turnId, triggerEventId: acceptedEventId, source: input.source },
1467
+ payload: {
1468
+ turnId,
1469
+ triggerEventId: acceptedEventId,
1470
+ source: input.source,
1471
+ initiator: frozenInitiator.initiator,
1472
+ },
1214
1473
  occurredAt: now,
1215
1474
  });
1216
1475
 
@@ -1238,7 +1497,36 @@ export async function submitHumanPromptInTransaction(
1238
1497
  for (const event of eventValues) event.sequence = ++sequence;
1239
1498
  const interruptionCount = supersession.interruptionCount;
1240
1499
  const replacedTurnId = supersession.replacedTurn?.id ?? null;
1500
+ const replacedAttemptId = supersession.replacedTurn?.activeAttemptId ?? null;
1241
1501
  const liveCurrentTurnId = supersession.liveCurrentTurnId;
1502
+ // Durable provenance for the queue projection. Until the superseded attempt
1503
+ // loses inference, user-visible output, and workspace-persistence authority,
1504
+ // its exact first-class attempt has no quiescence receipt. Pairing that
1505
+ // attempt with the replacement lets every client render "Stopping previous
1506
+ // attempt…" truthfully across refresh/reconnect, even though logical
1507
+ // settlement has already cleared sessions.active_turn_id. Do not infer this
1508
+ // state from a local request spinner or queue position.
1509
+ if (input.delivery === "steer") {
1510
+ await db
1511
+ .update(schema.sessionTurns)
1512
+ .set({
1513
+ metadata: {
1514
+ ...turn.metadata,
1515
+ delivery: "steer",
1516
+ replacedTurnId,
1517
+ replacedAttemptId,
1518
+ interruptionCount,
1519
+ },
1520
+ updatedAt: now,
1521
+ })
1522
+ .where(
1523
+ and(
1524
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
1525
+ eq(schema.sessionTurns.sessionId, input.sessionId),
1526
+ eq(schema.sessionTurns.id, turnId),
1527
+ ),
1528
+ );
1529
+ }
1242
1530
  if (supersession.replacedTurn) {
1243
1531
  const current = supersession.replacedTurn;
1244
1532
  if (!liveCurrentTurnId) {
@@ -1301,7 +1589,9 @@ export async function submitHumanPromptInTransaction(
1301
1589
  .update(schema.sessions)
1302
1590
  .set({
1303
1591
  resources: mergeResourceRefs(session.resources as ResourceRef[], input.resources),
1304
- tools: mergeToolRefs(session.tools as ToolRef[], input.tools),
1592
+ tools: sessionToolPolicyIsFixed(session.toolPolicy)
1593
+ ? session.tools
1594
+ : mergeToolRefs(session.tools as ToolRef[], input.tools),
1305
1595
  activeTurnId: input.delivery === "steer" ? liveCurrentTurnId : session.activeTurnId,
1306
1596
  status: nextStatus,
1307
1597
  queueVersion,
@@ -1331,7 +1621,14 @@ export async function submitHumanPromptInTransaction(
1331
1621
  action: input.delivery === "steer" ? "session.prompt.steer" : "session.prompt.send",
1332
1622
  targetType: "session_turn",
1333
1623
  targetId: turnId,
1334
- metadata: { operationId: reserved.receipt.id, replacedTurnId, interruptionCount },
1624
+ metadata: {
1625
+ operationId: reserved.receipt.id,
1626
+ replacedTurnId,
1627
+ interruptionCount,
1628
+ ...(input.turnExecutionPolicy
1629
+ ? turnExecutionPolicyAuditMetadata(input.turnExecutionPolicy, turnId)
1630
+ : {}),
1631
+ },
1335
1632
  });
1336
1633
  const eventIds = eventRows.map((event) => event.id);
1337
1634
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
@@ -1347,6 +1644,11 @@ export async function submitHumanPromptInTransaction(
1347
1644
  interruptionCount,
1348
1645
  replacedTurnId,
1349
1646
  workspaceControlEventId: resumed.workspaceControlEventId,
1647
+ ...(input.turnExecutionPolicy
1648
+ ? {
1649
+ executionPolicy: turnExecutionPolicyAuditMetadata(input.turnExecutionPolicy, turnId),
1650
+ }
1651
+ : {}),
1350
1652
  },
1351
1653
  });
1352
1654
  return {
@@ -1362,6 +1664,12 @@ export async function submitHumanPromptInTransaction(
1362
1664
  };
1363
1665
  }
1364
1666
 
1667
+ function sessionToolPolicyIsFixed(value: unknown): boolean {
1668
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1669
+ const mode = (value as Partial<SessionToolPolicy>).mode;
1670
+ return mode === "workspace_default" || mode === "explicit" || mode === "inherited";
1671
+ }
1672
+
1365
1673
  export async function sendAgentMessageInTransaction(
1366
1674
  db: Database,
1367
1675
  input: {
@@ -1373,7 +1681,14 @@ export async function sendAgentMessageInTransaction(
1373
1681
  text: string;
1374
1682
  },
1375
1683
  ): Promise<AgentInternalUpdateCommandResult> {
1376
- await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
1684
+ const workspaceControl = await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
1685
+ await lockSessionEventWriteRows(db, {
1686
+ workspaceId: input.workspaceId,
1687
+ controlLock: "already_locked",
1688
+ sessionIds: [input.actor.sessionId, input.targetSessionId],
1689
+ turnIds: [input.actor.turnId],
1690
+ attemptIds: [input.actor.attemptId],
1691
+ });
1377
1692
  const requestHash = canonicalSessionCommandHash({ text: input.text });
1378
1693
  const reserved = await reserveSessionCommandReceipt(db, {
1379
1694
  accountId: input.accountId,
@@ -1424,7 +1739,7 @@ export async function sendAgentMessageInTransaction(
1424
1739
  );
1425
1740
  }
1426
1741
  const effective = await evaluateSessionControl(db, input.workspaceId, input.targetSessionId, {
1427
- lock: "share",
1742
+ workspaceControl,
1428
1743
  });
1429
1744
  const now = new Date();
1430
1745
  const [update] = await db
@@ -1538,6 +1853,13 @@ export async function steerAgentSessionInTransaction(
1538
1853
  },
1539
1854
  ): Promise<AgentInternalUpdateCommandResult> {
1540
1855
  await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1856
+ await lockSessionEventWriteRows(db, {
1857
+ workspaceId: input.workspaceId,
1858
+ controlLock: "already_locked",
1859
+ sessionIds: [input.actor.sessionId, input.targetSessionId],
1860
+ turnIds: [input.actor.turnId],
1861
+ attemptIds: [input.actor.attemptId],
1862
+ });
1541
1863
  const reserved = await reserveSessionCommandReceipt(db, {
1542
1864
  accountId: input.accountId,
1543
1865
  workspaceId: input.workspaceId,
@@ -1546,7 +1868,9 @@ export async function steerAgentSessionInTransaction(
1546
1868
  targetSessionId: input.targetSessionId,
1547
1869
  targetTurnId: null,
1548
1870
  operationKey: input.operationKey,
1549
- canonicalRequestHash: canonicalSessionCommandHash({ instruction: input.instruction }),
1871
+ canonicalRequestHash: canonicalSessionCommandHash({
1872
+ instruction: input.instruction,
1873
+ }),
1550
1874
  });
1551
1875
  if (reserved.replay) {
1552
1876
  const updateId = String(reserved.receipt.result.updateId ?? "");