@opengeni/db 0.7.2 → 0.9.3

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 (59) hide show
  1. package/dist/{chunk-B22X3IEZ.js → chunk-4LG5NBTC.js} +512 -32
  2. package/dist/chunk-4LG5NBTC.js.map +1 -0
  3. package/dist/{chunk-YFQ7SGE4.js → chunk-BMFDXFPA.js} +23 -1
  4. package/dist/chunk-BMFDXFPA.js.map +1 -0
  5. package/dist/chunk-KW526IJA.js +127 -0
  6. package/dist/chunk-KW526IJA.js.map +1 -0
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +6092 -2080
  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 +732 -47
  13. package/dist/provision-roles.js +1 -1
  14. package/dist/{schema-BN5mB9xZ.d.ts → schema-CdPGTHlD.d.ts} +2118 -119
  15. package/dist/schema.d.ts +1 -1
  16. package/dist/schema.js +17 -1
  17. package/drizzle/0064_rotation_strategy_sharded_backfill.sql +15 -0
  18. package/drizzle/0065_session_attempt_quiescence.sql +26 -0
  19. package/drizzle/0066_session_interruption_attempt_lookup.sql +4 -0
  20. package/drizzle/0067_session_event_payload_bounds.sql +209 -0
  21. package/drizzle/0068_workspace_control_event_bounds.sql +134 -0
  22. package/drizzle/0069_session_event_history_backfill.sql +32 -0
  23. package/drizzle/0070_session_event_type_sequence_lookup.sql +4 -0
  24. package/drizzle/0071_session_event_monitoring_tail.sql +10 -0
  25. package/drizzle/0072_sessions_workspace_created_id_idx.sql +4 -0
  26. package/drizzle/0073_sessions_workspace_updated_id_idx.sql +4 -0
  27. package/drizzle/0074_session_activity_revisions.sql +72 -0
  28. package/drizzle/0075_sessions_workspace_activity_revision_idx.sql +4 -0
  29. package/drizzle/0076_session_workflow_wake_acl.sql +13 -0
  30. package/drizzle/0077_session_attempt_latest_lookup.sql +4 -0
  31. package/drizzle/0094_quarantine_credential_bearing_catalog_urls.sql +51 -0
  32. package/drizzle/0095_github_existing_installations.sql +84 -0
  33. package/drizzle/0096_session_turn_initiators.sql +97 -0
  34. package/drizzle/0097_host_export_outbox.sql +1220 -0
  35. package/drizzle/0098_usage_events_workspace_session_idx.sql +4 -0
  36. package/drizzle/0099_session_human_input_attempt_owner_index.sql +6 -0
  37. package/drizzle/0100_session_human_input_requests.sql +89 -0
  38. package/drizzle/0101_session_mcp_connection_refs.sql +30 -0
  39. package/drizzle/0102_session_command_receipt_service_actor.sql +16 -0
  40. package/drizzle/0103_host_export_root_session.sql +166 -0
  41. package/drizzle/0104_host_export_root_session_backfill.sql +27 -0
  42. package/drizzle/0105_session_turn_instructions.sql +9 -0
  43. package/package.json +4 -4
  44. package/src/connection-token-resolver.ts +287 -1
  45. package/src/event-payload-sanitizer.ts +57 -19
  46. package/src/index.ts +5429 -1131
  47. package/src/memory-domain.ts +1 -1
  48. package/src/migrate.ts +86 -23
  49. package/src/persistence-errors.ts +252 -0
  50. package/src/provision-roles.ts +42 -0
  51. package/src/schema.ts +552 -34
  52. package/src/session-control.ts +518 -38
  53. package/src/session-queue-commands.ts +308 -57
  54. package/src/session-tool-call-settlement.ts +58 -7
  55. package/src/turn-initiator.ts +155 -0
  56. package/dist/chunk-7LDU7F5P.js +0 -80
  57. package/dist/chunk-7LDU7F5P.js.map +0 -1
  58. package/dist/chunk-B22X3IEZ.js.map +0 -1
  59. package/dist/chunk-YFQ7SGE4.js.map +0 -1
@@ -7,12 +7,14 @@ import {
7
7
  } from "@opengeni/contracts";
8
8
  import { and, asc, eq, inArray, sql } from "drizzle-orm";
9
9
  import type { Database } from "./index";
10
+ import { sanitizeEventPayload } from "./event-payload-sanitizer";
10
11
  import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
11
12
  import {
12
13
  assertAgentCommandAuthorityInTransaction,
13
14
  autoResumeSessionBranchInTransaction,
14
15
  canonicalSessionCommandHash,
15
16
  evaluateSessionControl,
17
+ lockSessionEventWriteRows,
16
18
  lockWorkspaceInferenceControl,
17
19
  registerInternalUpdateWakeInTransaction,
18
20
  reserveSessionCommandReceipt,
@@ -24,6 +26,12 @@ import {
24
26
  updateSessionCommandReceiptResult,
25
27
  } from "./session-control";
26
28
  import * as schema from "./schema";
29
+ import {
30
+ frozenInitiatorForCommandActor,
31
+ initiatorColumns,
32
+ initiatorFromStorage,
33
+ type FrozenTurnInitiator,
34
+ } from "./turn-initiator";
27
35
 
28
36
  export type QueueCommandConflictCode =
29
37
  | "QUEUE_VERSION_CHANGED"
@@ -99,12 +107,13 @@ async function lockSession(
99
107
  workspaceId: string,
100
108
  sessionId: string,
101
109
  ): 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);
110
+ const locks = await lockSessionEventWriteRows(db, {
111
+ workspaceId,
112
+ controlLock: "already_locked",
113
+ workspaceLock: "already_locked",
114
+ sessionIds: [sessionId],
115
+ });
116
+ const session = locks.sessions[0];
108
117
  if (!session) throw new Error(`Session not found: ${sessionId}`);
109
118
  return session;
110
119
  }
@@ -142,7 +151,7 @@ export async function supersedeSessionCurrentDirectionInTransaction(
142
151
  lastSequence: input.lastSequence,
143
152
  };
144
153
  }
145
- const [current] = await db
154
+ const [preview] = await db
146
155
  .select()
147
156
  .from(schema.sessionTurns)
148
157
  .where(
@@ -152,8 +161,15 @@ export async function supersedeSessionCurrentDirectionInTransaction(
152
161
  eq(schema.sessionTurns.id, input.activeTurnId),
153
162
  ),
154
163
  )
155
- .for("update")
156
164
  .limit(1);
165
+ const locks = await lockSessionEventWriteRows(db, {
166
+ workspaceId: input.workspaceId,
167
+ controlLock: "already_locked",
168
+ workspaceLock: "already_locked",
169
+ turnIds: [input.activeTurnId],
170
+ attemptIds: preview?.activeAttemptId ? [preview.activeAttemptId] : [],
171
+ });
172
+ const current = locks.turns[0];
157
173
  if (!current) {
158
174
  throw new SessionControlInvariantError(
159
175
  `Session ${input.sessionId} points to missing active turn ${input.activeTurnId}`,
@@ -206,6 +222,44 @@ export async function supersedeSessionCurrentDirectionInTransaction(
206
222
  sequence: input.lastSequence,
207
223
  now,
208
224
  });
225
+ const cancelledHumanInputs = await db
226
+ .update(schema.sessionHumanInputRequests)
227
+ .set({
228
+ status: "cancelled",
229
+ response: { outcome: "cancelled" },
230
+ respondedBy:
231
+ input.actor.type === "agent_attempt"
232
+ ? `attempt:${input.actor.attemptId}`
233
+ : input.actor.subjectId,
234
+ respondedAt: now,
235
+ updatedAt: now,
236
+ })
237
+ .where(
238
+ and(
239
+ eq(schema.sessionHumanInputRequests.workspaceId, input.workspaceId),
240
+ eq(schema.sessionHumanInputRequests.sessionId, input.sessionId),
241
+ eq(schema.sessionHumanInputRequests.turnId, current.id),
242
+ eq(schema.sessionHumanInputRequests.status, "pending"),
243
+ ),
244
+ )
245
+ .returning({ id: schema.sessionHumanInputRequests.id });
246
+ let lastSequence = closedTools.sequence;
247
+ if (cancelledHumanInputs.length > 0) {
248
+ await db.insert(schema.sessionEvents).values(
249
+ cancelledHumanInputs.map((request) => ({
250
+ accountId: input.accountId,
251
+ workspaceId: input.workspaceId,
252
+ sessionId: input.sessionId,
253
+ sequence: ++lastSequence,
254
+ type: "user.humanInputResponse",
255
+ turnId: current.id,
256
+ turnGeneration: current.executionGeneration,
257
+ turnAssociation: "current",
258
+ payload: { requestId: request.id, response: { outcome: "cancelled" } },
259
+ occurredAt: now,
260
+ })),
261
+ );
262
+ }
209
263
  await db
210
264
  .update(schema.sessionTurns)
211
265
  .set({
@@ -248,7 +302,7 @@ export async function supersedeSessionCurrentDirectionInTransaction(
248
302
  interruptionCount: 0,
249
303
  replacedTurn: current,
250
304
  liveCurrentTurnId: null,
251
- lastSequence: closedTools.sequence,
305
+ lastSequence,
252
306
  };
253
307
  }
254
308
 
@@ -274,7 +328,16 @@ async function loadQueuedTurns(
274
328
  asc(schema.sessionTurns.createdAt),
275
329
  asc(schema.sessionTurns.id),
276
330
  );
277
- return lock ? await query.for("update") : await query;
331
+ const rows = await query;
332
+ if (!lock || rows.length === 0) return rows;
333
+ const locks = await lockSessionEventWriteRows(db, {
334
+ workspaceId,
335
+ controlLock: "already_locked",
336
+ workspaceLock: "already_locked",
337
+ turnIds: rows.map((row) => row.id),
338
+ });
339
+ const byId = new Map(locks.turns.map((row) => [row.id, row]));
340
+ return rows.map((row) => byId.get(row.id)).filter((row): row is QueuedTurnRow => Boolean(row));
278
341
  }
279
342
 
280
343
  async function normalizeQueuePositions(
@@ -320,7 +383,12 @@ function draftIsNonEmpty(draft: ComposerDraftRow): boolean {
320
383
 
321
384
  export async function getComposerDraftInTransaction(
322
385
  db: Database,
323
- input: { workspaceId: string; sessionId: string; subjectId: string; lock?: boolean },
386
+ input: {
387
+ workspaceId: string;
388
+ sessionId: string;
389
+ subjectId: string;
390
+ lock?: boolean;
391
+ },
324
392
  ): Promise<ComposerDraftRow | null> {
325
393
  const query = db
326
394
  .select()
@@ -353,8 +421,15 @@ export async function saveComposerDraftInTransaction(
353
421
  },
354
422
  ): Promise<ComposerDraftRow> {
355
423
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
424
+ await lockSessionEventWriteRows(db, {
425
+ workspaceId: input.workspaceId,
426
+ controlLock: "already_locked",
427
+ });
356
428
  await lockSession(db, input.workspaceId, input.sessionId);
357
- const current = await getComposerDraftInTransaction(db, { ...input, lock: true });
429
+ const current = await getComposerDraftInTransaction(db, {
430
+ ...input,
431
+ lock: true,
432
+ });
358
433
  const currentRevision = current?.revision ?? 0;
359
434
  if (currentRevision !== input.expectedRevision) {
360
435
  throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
@@ -374,8 +449,11 @@ export async function saveComposerDraftInTransaction(
374
449
  tools: input.tools,
375
450
  model: input.model,
376
451
  reasoningEffort: input.reasoningEffort,
377
- sourceTurnId: null,
378
- sourceTurnVersion: null,
452
+ // A queue edit is still the same accepted work item. Preserve its frozen
453
+ // initiator through arbitrary draft saves; only a genuinely new compose or
454
+ // Steer captures the submitting actor.
455
+ sourceTurnId: current?.sourceTurnId ?? null,
456
+ sourceTurnVersion: current?.sourceTurnVersion ?? null,
379
457
  updatedAt: new Date(),
380
458
  };
381
459
  const [saved] = current
@@ -403,6 +481,10 @@ export async function moveQueuedTurnInTransaction(
403
481
  },
404
482
  ): Promise<QueueCommandResult> {
405
483
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
484
+ await lockSessionEventWriteRows(db, {
485
+ workspaceId: input.workspaceId,
486
+ controlLock: "already_locked",
487
+ });
406
488
  const session = await lockSession(db, input.workspaceId, input.sessionId);
407
489
  const requestHash = canonicalSessionCommandHash({
408
490
  beforeTurnId: input.beforeTurnId,
@@ -494,7 +576,11 @@ export async function moveQueuedTurnInTransaction(
494
576
  eventIds.push(event.id);
495
577
  await db
496
578
  .update(schema.sessions)
497
- .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: new Date() })
579
+ .set({
580
+ queueVersion,
581
+ lastSequence: session.lastSequence + 1,
582
+ updatedAt: new Date(),
583
+ })
498
584
  .where(eq(schema.sessions.id, input.sessionId));
499
585
  }
500
586
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
@@ -525,6 +611,10 @@ export async function deleteSessionQueueItemInTransaction(
525
611
  },
526
612
  ): Promise<QueueCommandResult> {
527
613
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
614
+ await lockSessionEventWriteRows(db, {
615
+ workspaceId: input.workspaceId,
616
+ controlLock: "already_locked",
617
+ });
528
618
  const session = await lockSession(db, input.workspaceId, input.sessionId);
529
619
  const requestHash = canonicalSessionCommandHash({
530
620
  expectedTurnVersion: input.expectedTurnVersion,
@@ -549,18 +639,14 @@ export async function deleteSessionQueueItemInTransaction(
549
639
  replay: true,
550
640
  };
551
641
  }
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);
642
+ const turn = (
643
+ await lockSessionEventWriteRows(db, {
644
+ workspaceId: input.workspaceId,
645
+ controlLock: "already_locked",
646
+ workspaceLock: "already_locked",
647
+ turnIds: [input.turnId],
648
+ })
649
+ ).turns[0];
564
650
  if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
565
651
  throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
566
652
  queueVersion: session.queueVersion,
@@ -616,7 +702,11 @@ export async function deleteSessionQueueItemInTransaction(
616
702
  if (!event) throw new Error("Queue delete event was not inserted");
617
703
  await db
618
704
  .update(schema.sessions)
619
- .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
705
+ .set({
706
+ queueVersion,
707
+ lastSequence: session.lastSequence + 1,
708
+ updatedAt: now,
709
+ })
620
710
  .where(eq(schema.sessions.id, input.sessionId));
621
711
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
622
712
  queueVersion,
@@ -648,6 +738,10 @@ export async function editQueuedTurnInTransaction(
648
738
  },
649
739
  ): Promise<EditQueueCommandResult> {
650
740
  await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
741
+ await lockSessionEventWriteRows(db, {
742
+ workspaceId: input.workspaceId,
743
+ controlLock: "already_locked",
744
+ });
651
745
  const session = await lockSession(db, input.workspaceId, input.sessionId);
652
746
  const requestHash = canonicalSessionCommandHash({
653
747
  expectedTurnVersion: input.expectedTurnVersion,
@@ -664,7 +758,10 @@ export async function editQueuedTurnInTransaction(
664
758
  operationKey: input.operationKey,
665
759
  canonicalRequestHash: requestHash,
666
760
  });
667
- const existingDraft = await getComposerDraftInTransaction(db, { ...input, lock: true });
761
+ const existingDraft = await getComposerDraftInTransaction(db, {
762
+ ...input,
763
+ lock: true,
764
+ });
668
765
  if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
669
766
  if (!existingDraft) throw new Error("Replayed queue Edit has no durable draft");
670
767
  return {
@@ -689,18 +786,14 @@ export async function editQueuedTurnInTransaction(
689
786
  draftRevision,
690
787
  });
691
788
  }
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);
789
+ const turn = (
790
+ await lockSessionEventWriteRows(db, {
791
+ workspaceId: input.workspaceId,
792
+ controlLock: "already_locked",
793
+ workspaceLock: "already_locked",
794
+ turnIds: [input.turnId],
795
+ })
796
+ ).turns[0];
704
797
  if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
705
798
  throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
706
799
  queueVersion: session.queueVersion,
@@ -780,7 +873,11 @@ export async function editQueuedTurnInTransaction(
780
873
  if (!event) throw new Error("Queue edit event was not inserted");
781
874
  await db
782
875
  .update(schema.sessions)
783
- .set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
876
+ .set({
877
+ queueVersion,
878
+ lastSequence: session.lastSequence + 1,
879
+ updatedAt: now,
880
+ })
784
881
  .where(eq(schema.sessions.id, input.sessionId));
785
882
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
786
883
  queueVersion,
@@ -788,7 +885,14 @@ export async function editQueuedTurnInTransaction(
788
885
  draftRevision: nextDraftRevision,
789
886
  result: { sourceTurnId: turn.id },
790
887
  });
791
- return { receipt, queueVersion, items: remaining, draft, eventIds: [event.id], replay: false };
888
+ return {
889
+ receipt,
890
+ queueVersion,
891
+ items: remaining,
892
+ draft,
893
+ eventIds: [event.id],
894
+ replay: false,
895
+ };
792
896
  }
793
897
 
794
898
  export async function steerQueuedTurnInTransaction(
@@ -804,7 +908,18 @@ export async function steerQueuedTurnInTransaction(
804
908
  operationKey: string;
805
909
  },
806
910
  ): Promise<SteerQueueCommandResult> {
807
- await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
911
+ const workspaceControl = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
912
+ await lockSessionEventWriteRows(db, {
913
+ workspaceId: input.workspaceId,
914
+ controlLock: "already_locked",
915
+ sessionIds:
916
+ input.actor.type === "agent_attempt"
917
+ ? [input.actor.sessionId, input.sessionId]
918
+ : [input.sessionId],
919
+ turnIds:
920
+ input.actor.type === "agent_attempt" ? [input.actor.turnId, input.turnId] : [input.turnId],
921
+ attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : [],
922
+ });
808
923
  const requestHash = canonicalSessionCommandHash({
809
924
  expectedTurnVersion: input.expectedTurnVersion,
810
925
  controlEtag: input.controlEtag ?? null,
@@ -843,7 +958,7 @@ export async function steerQueuedTurnInTransaction(
843
958
  });
844
959
  }
845
960
  const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
846
- lock: "share",
961
+ workspaceControl,
847
962
  });
848
963
  if (input.controlEtag && input.controlEtag !== before.controlEtag) {
849
964
  throw new SessionControlConflictError();
@@ -885,6 +1000,7 @@ export async function steerQueuedTurnInTransaction(
885
1000
  });
886
1001
  const interruptionCount = supersession.interruptionCount;
887
1002
  const supersededTurnId = supersession.replacedTurn?.id ?? null;
1003
+ const supersededAttemptId = supersession.replacedTurn?.activeAttemptId ?? null;
888
1004
  const liveCurrentTurnId = supersession.liveCurrentTurnId;
889
1005
 
890
1006
  const withoutTarget = rows.filter((row) => row.id !== target.id);
@@ -899,7 +1015,17 @@ export async function steerQueuedTurnInTransaction(
899
1015
  const queueVersion = session.queueVersion + 1;
900
1016
  await db
901
1017
  .update(schema.sessionTurns)
902
- .set({ version: target.version + 1, updatedAt: now })
1018
+ .set({
1019
+ version: target.version + 1,
1020
+ metadata: {
1021
+ ...target.metadata,
1022
+ delivery: "steer",
1023
+ replacedTurnId: supersededTurnId,
1024
+ replacedAttemptId: supersededAttemptId,
1025
+ interruptionCount,
1026
+ },
1027
+ updatedAt: now,
1028
+ })
903
1029
  .where(eq(schema.sessionTurns.id, target.id));
904
1030
  let sequence = supersession.lastSequence;
905
1031
  const actor =
@@ -998,33 +1124,58 @@ export async function submitHumanPromptInTransaction(
998
1124
  workspaceId: string;
999
1125
  sessionId: string;
1000
1126
  subjectId: string;
1127
+ subjectLabel?: string;
1001
1128
  actor: SessionCommandActor;
1002
1129
  operationKey: string;
1003
1130
  delivery: "send" | "steer";
1004
1131
  controlEtag?: string | null;
1005
1132
  expectedDraftRevision?: number | null;
1006
1133
  text: string;
1134
+ turnInstructions?: string | null;
1007
1135
  resources: ResourceRef[];
1008
1136
  tools: ToolRef[];
1009
1137
  model?: string | null;
1010
1138
  reasoningEffort?: ReasoningEffort | null;
1011
1139
  reasoningEffortFallback: ReasoningEffort;
1012
1140
  source: "user" | "api";
1013
- mcpCredentialUpdates?: Array<{ id: string; headersEncrypted: Record<string, string> }>;
1141
+ mcpCredentialUpdates?: Array<{
1142
+ id: string;
1143
+ headersEncrypted: Record<string, string>;
1144
+ }>;
1014
1145
  },
1015
1146
  ): Promise<SubmitHumanPromptResult> {
1016
- await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1147
+ const workspaceControl = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1148
+ await lockSessionEventWriteRows(db, {
1149
+ workspaceId: input.workspaceId,
1150
+ controlLock: "already_locked",
1151
+ sessionIds:
1152
+ input.actor.type === "agent_attempt"
1153
+ ? [input.actor.sessionId, input.sessionId]
1154
+ : [input.sessionId],
1155
+ turnIds: input.actor.type === "agent_attempt" ? [input.actor.turnId] : [],
1156
+ attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : [],
1157
+ });
1017
1158
  const requestHash = canonicalSessionCommandHash({
1018
1159
  delivery: input.delivery,
1019
1160
  controlEtag: input.controlEtag ?? null,
1020
1161
  expectedDraftRevision: input.expectedDraftRevision ?? null,
1021
1162
  text: input.text,
1163
+ turnInstructions: input.turnInstructions ?? null,
1022
1164
  resources: input.resources,
1023
1165
  tools: input.tools,
1024
1166
  model: input.model ?? null,
1025
1167
  reasoningEffort: input.reasoningEffort ?? null,
1026
1168
  source: input.source,
1027
1169
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1170
+ ...(input.actor.type === "service"
1171
+ ? {
1172
+ serviceInitiator: {
1173
+ subjectId: input.actor.subjectId,
1174
+ subjectLabel: input.actor.subjectLabel ?? null,
1175
+ context: input.actor.context ?? {},
1176
+ },
1177
+ }
1178
+ : {}),
1028
1179
  });
1029
1180
  const reserved = await reserveSessionCommandReceipt(db, {
1030
1181
  accountId: input.accountId,
@@ -1063,7 +1214,7 @@ export async function submitHumanPromptInTransaction(
1063
1214
  }
1064
1215
 
1065
1216
  const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1066
- lock: "share",
1217
+ workspaceControl,
1067
1218
  });
1068
1219
  if (input.controlEtag && input.controlEtag !== before.controlEtag) {
1069
1220
  throw new SessionControlConflictError();
@@ -1076,7 +1227,14 @@ export async function submitHumanPromptInTransaction(
1076
1227
  input.actor.type === "agent_attempt"
1077
1228
  ? `attempt:${input.actor.attemptId}`
1078
1229
  : input.actor.subjectId,
1079
- reason: input.delivery === "steer" ? "human_steer" : "human_send",
1230
+ reason:
1231
+ input.actor.type === "service"
1232
+ ? input.delivery === "steer"
1233
+ ? "service_steer"
1234
+ : "service_send"
1235
+ : input.delivery === "steer"
1236
+ ? "human_steer"
1237
+ : "human_send",
1080
1238
  observedControlEtag: input.controlEtag ?? null,
1081
1239
  });
1082
1240
  const session = await lockSession(db, input.workspaceId, input.sessionId);
@@ -1155,6 +1313,43 @@ export async function submitHumanPromptInTransaction(
1155
1313
  }
1156
1314
 
1157
1315
  const now = new Date();
1316
+ let frozenInitiator: FrozenTurnInitiator;
1317
+ if (input.delivery === "send" && draft?.sourceTurnId) {
1318
+ const [sourceTurn] = await db
1319
+ .select({
1320
+ sessionId: schema.sessionTurns.sessionId,
1321
+ initiatorKind: schema.sessionTurns.initiatorKind,
1322
+ initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
1323
+ initiatorContext: schema.sessionTurns.initiatorContext,
1324
+ })
1325
+ .from(schema.sessionTurns)
1326
+ .where(
1327
+ and(
1328
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
1329
+ eq(schema.sessionTurns.sessionId, input.sessionId),
1330
+ eq(schema.sessionTurns.id, draft.sourceTurnId),
1331
+ ),
1332
+ )
1333
+ .limit(1);
1334
+ if (!sourceTurn) {
1335
+ throw new SessionControlInvariantError("Edited prompt source turn is missing");
1336
+ }
1337
+ frozenInitiator = {
1338
+ initiator: initiatorFromStorage(
1339
+ sourceTurn.initiatorKind,
1340
+ sourceTurn.initiatorSubjectId,
1341
+ sourceTurn.initiatorContext ?? {},
1342
+ ),
1343
+ context: sourceTurn.initiatorContext ?? {},
1344
+ };
1345
+ } else {
1346
+ frozenInitiator = await frozenInitiatorForCommandActor(
1347
+ db,
1348
+ input.workspaceId,
1349
+ input.actor,
1350
+ input.subjectLabel,
1351
+ );
1352
+ }
1158
1353
  const acceptedEventId = crypto.randomUUID();
1159
1354
  const turnId = crypto.randomUUID();
1160
1355
  const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
@@ -1168,14 +1363,15 @@ export async function submitHumanPromptInTransaction(
1168
1363
  sequence: ++sequence,
1169
1364
  type: "user.message",
1170
1365
  clientEventId: input.operationKey,
1171
- payload: {
1366
+ payload: sanitizeEventPayload({
1172
1367
  text: input.text,
1173
1368
  ...(input.resources.length ? { resources: input.resources } : {}),
1174
1369
  ...(input.tools.length ? { tools: input.tools } : {}),
1175
1370
  ...(input.model ? { model: input.model } : {}),
1176
1371
  ...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
1177
1372
  delivery: input.delivery,
1178
- },
1373
+ initiator: frozenInitiator.initiator,
1374
+ }),
1179
1375
  occurredAt: now,
1180
1376
  },
1181
1377
  ];
@@ -1193,6 +1389,7 @@ export async function submitHumanPromptInTransaction(
1193
1389
  source: input.source,
1194
1390
  position: input.delivery === "steer" ? 0 : existingQueued.length + 1,
1195
1391
  prompt: input.text,
1392
+ turnInstructions: input.turnInstructions ?? null,
1196
1393
  resources: input.resources,
1197
1394
  tools: input.tools,
1198
1395
  model: input.model ?? session.model,
@@ -1200,6 +1397,7 @@ export async function submitHumanPromptInTransaction(
1200
1397
  sandboxBackend: session.sandboxBackend,
1201
1398
  metadata: {},
1202
1399
  lineage: { actor: input.actor.type },
1400
+ ...initiatorColumns(frozenInitiator),
1203
1401
  })
1204
1402
  .returning();
1205
1403
  if (!turn) throw new SessionControlInvariantError("Prompt turn was not inserted");
@@ -1210,7 +1408,12 @@ export async function submitHumanPromptInTransaction(
1210
1408
  sequence: ++sequence,
1211
1409
  type: "turn.queued",
1212
1410
  turnId,
1213
- payload: { turnId, triggerEventId: acceptedEventId, source: input.source },
1411
+ payload: {
1412
+ turnId,
1413
+ triggerEventId: acceptedEventId,
1414
+ source: input.source,
1415
+ initiator: frozenInitiator.initiator,
1416
+ },
1214
1417
  occurredAt: now,
1215
1418
  });
1216
1419
 
@@ -1238,7 +1441,35 @@ export async function submitHumanPromptInTransaction(
1238
1441
  for (const event of eventValues) event.sequence = ++sequence;
1239
1442
  const interruptionCount = supersession.interruptionCount;
1240
1443
  const replacedTurnId = supersession.replacedTurn?.id ?? null;
1444
+ const replacedAttemptId = supersession.replacedTurn?.activeAttemptId ?? null;
1241
1445
  const liveCurrentTurnId = supersession.liveCurrentTurnId;
1446
+ // Durable provenance for the queue projection. Until the superseded attempt
1447
+ // loses inference, user-visible output, and workspace-persistence authority,
1448
+ // its exact first-class attempt has no quiescence receipt. Pairing that
1449
+ // attempt with the replacement lets every client render "Stopping previous
1450
+ // attempt…" truthfully across refresh/reconnect, even though logical
1451
+ // settlement has already cleared sessions.active_turn_id. Do not infer this
1452
+ // state from a local request spinner or queue position.
1453
+ if (input.delivery === "steer") {
1454
+ await db
1455
+ .update(schema.sessionTurns)
1456
+ .set({
1457
+ metadata: {
1458
+ delivery: "steer",
1459
+ replacedTurnId,
1460
+ replacedAttemptId,
1461
+ interruptionCount,
1462
+ },
1463
+ updatedAt: now,
1464
+ })
1465
+ .where(
1466
+ and(
1467
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
1468
+ eq(schema.sessionTurns.sessionId, input.sessionId),
1469
+ eq(schema.sessionTurns.id, turnId),
1470
+ ),
1471
+ );
1472
+ }
1242
1473
  if (supersession.replacedTurn) {
1243
1474
  const current = supersession.replacedTurn;
1244
1475
  if (!liveCurrentTurnId) {
@@ -1331,7 +1562,11 @@ export async function submitHumanPromptInTransaction(
1331
1562
  action: input.delivery === "steer" ? "session.prompt.steer" : "session.prompt.send",
1332
1563
  targetType: "session_turn",
1333
1564
  targetId: turnId,
1334
- metadata: { operationId: reserved.receipt.id, replacedTurnId, interruptionCount },
1565
+ metadata: {
1566
+ operationId: reserved.receipt.id,
1567
+ replacedTurnId,
1568
+ interruptionCount,
1569
+ },
1335
1570
  });
1336
1571
  const eventIds = eventRows.map((event) => event.id);
1337
1572
  const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
@@ -1373,7 +1608,14 @@ export async function sendAgentMessageInTransaction(
1373
1608
  text: string;
1374
1609
  },
1375
1610
  ): Promise<AgentInternalUpdateCommandResult> {
1376
- await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
1611
+ const workspaceControl = await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
1612
+ await lockSessionEventWriteRows(db, {
1613
+ workspaceId: input.workspaceId,
1614
+ controlLock: "already_locked",
1615
+ sessionIds: [input.actor.sessionId, input.targetSessionId],
1616
+ turnIds: [input.actor.turnId],
1617
+ attemptIds: [input.actor.attemptId],
1618
+ });
1377
1619
  const requestHash = canonicalSessionCommandHash({ text: input.text });
1378
1620
  const reserved = await reserveSessionCommandReceipt(db, {
1379
1621
  accountId: input.accountId,
@@ -1424,7 +1666,7 @@ export async function sendAgentMessageInTransaction(
1424
1666
  );
1425
1667
  }
1426
1668
  const effective = await evaluateSessionControl(db, input.workspaceId, input.targetSessionId, {
1427
- lock: "share",
1669
+ workspaceControl,
1428
1670
  });
1429
1671
  const now = new Date();
1430
1672
  const [update] = await db
@@ -1538,6 +1780,13 @@ export async function steerAgentSessionInTransaction(
1538
1780
  },
1539
1781
  ): Promise<AgentInternalUpdateCommandResult> {
1540
1782
  await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
1783
+ await lockSessionEventWriteRows(db, {
1784
+ workspaceId: input.workspaceId,
1785
+ controlLock: "already_locked",
1786
+ sessionIds: [input.actor.sessionId, input.targetSessionId],
1787
+ turnIds: [input.actor.turnId],
1788
+ attemptIds: [input.actor.attemptId],
1789
+ });
1541
1790
  const reserved = await reserveSessionCommandReceipt(db, {
1542
1791
  accountId: input.accountId,
1543
1792
  workspaceId: input.workspaceId,
@@ -1546,7 +1795,9 @@ export async function steerAgentSessionInTransaction(
1546
1795
  targetSessionId: input.targetSessionId,
1547
1796
  targetTurnId: null,
1548
1797
  operationKey: input.operationKey,
1549
- canonicalRequestHash: canonicalSessionCommandHash({ instruction: input.instruction }),
1798
+ canonicalRequestHash: canonicalSessionCommandHash({
1799
+ instruction: input.instruction,
1800
+ }),
1550
1801
  });
1551
1802
  if (reserved.replay) {
1552
1803
  const updateId = String(reserved.receipt.result.updateId ?? "");