@opengeni/db 0.36.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/{chunk-P3RE4D2B.js → chunk-4SR4HKKA.js} +2 -2
  3. package/dist/{chunk-5KTIEDX7.js → chunk-6ZYKCRFO.js} +124 -16
  4. package/dist/chunk-6ZYKCRFO.js.map +1 -0
  5. package/dist/{chunk-CQZ4QSP5.js → chunk-CUPM3FGB.js} +3 -3
  6. package/dist/{chunk-TAUTWOS5.js → chunk-LTG3S6ZY.js} +2 -2
  7. package/dist/{chunk-VKLB2ZGI.js → chunk-NXAFLCQX.js} +81 -2
  8. package/dist/chunk-NXAFLCQX.js.map +1 -0
  9. package/dist/{chunk-F32YT7MP.js → chunk-WD5ROSLL.js} +2 -2
  10. package/dist/{chunk-JC5QPLUM.js → chunk-ZWREYEWA.js} +48 -3
  11. package/dist/chunk-ZWREYEWA.js.map +1 -0
  12. package/dist/editable-artifact-durable-export.d.ts +10 -0
  13. package/dist/editable-artifact-durable-export.js +18 -3
  14. package/dist/editable-artifact-durable-export.js.map +1 -1
  15. package/dist/editable-artifacts.js +3 -3
  16. package/dist/index.d.ts +69 -8
  17. package/dist/index.js +869 -75
  18. package/dist/index.js.map +1 -1
  19. package/dist/knowledge-source-sync-schema.d.ts +730 -0
  20. package/dist/knowledge-source-sync.d.ts +60 -0
  21. package/dist/persistence-errors.d.ts +8 -0
  22. package/dist/provision-roles.js +1 -1
  23. package/dist/runtime-posture.d.ts +3 -3
  24. package/dist/schema.d.ts +21 -4
  25. package/dist/schema.js +5 -1
  26. package/dist/session-queue-commands.d.ts +1 -1
  27. package/dist/session-realtime-ledger.d.ts +1 -0
  28. package/dist/session-tenancy.js +3 -3
  29. package/dist/video-generation.js +3 -3
  30. package/drizzle/0238_recover_unclaimed_session_turns.sql +307 -0
  31. package/drizzle/0240_model_context_user_messages.sql +204 -0
  32. package/drizzle/0243_google_drive_object_acl_authority.sql +554 -0
  33. package/package.json +5 -5
  34. package/src/editable-artifact-durable-export.ts +27 -0
  35. package/src/index.ts +680 -30
  36. package/src/knowledge-source-sync-schema.ts +83 -0
  37. package/src/knowledge-source-sync.ts +530 -0
  38. package/src/persistence-errors.ts +60 -1
  39. package/src/provision-roles.ts +36 -0
  40. package/src/runtime-posture.ts +49 -1
  41. package/src/schema.ts +30 -7
  42. package/src/session-queue-commands.ts +13 -14
  43. package/src/session-realtime-context.ts +16 -0
  44. package/src/session-realtime-ledger.ts +53 -1
  45. package/dist/chunk-5KTIEDX7.js.map +0 -1
  46. package/dist/chunk-JC5QPLUM.js.map +0 -1
  47. package/dist/chunk-VKLB2ZGI.js.map +0 -1
  48. /package/dist/{chunk-P3RE4D2B.js.map → chunk-4SR4HKKA.js.map} +0 -0
  49. /package/dist/{chunk-CQZ4QSP5.js.map → chunk-CUPM3FGB.js.map} +0 -0
  50. /package/dist/{chunk-TAUTWOS5.js.map → chunk-LTG3S6ZY.js.map} +0 -0
  51. /package/dist/{chunk-F32YT7MP.js.map → chunk-WD5ROSLL.js.map} +0 -0
package/src/index.ts CHANGED
@@ -156,7 +156,7 @@ import {
156
156
  MODEL_ATTACHMENT_REFS_FIELD,
157
157
  MODEL_TIMELINE_ANNOTATIONS_FIELD,
158
158
  TimelineAnnotations,
159
- renderTimelineAnnotationsForModel,
159
+ renderUserMessageContentForModel,
160
160
  SessionMcpApprovalPolicy as SessionMcpApprovalPolicySchema,
161
161
  RequestHumanInteractionToolInput,
162
162
  WORKSPACE_XAI_PROVIDER_ACCOUNT_AUTHORITY_SNAPSHOT_V1,
@@ -4331,7 +4331,7 @@ export type EnqueueSessionTurnInput = {
4331
4331
  temporalWorkflowId: string;
4332
4332
  source: SessionTurnSource;
4333
4333
  prompt: string;
4334
- turnInstructions?: string | null;
4334
+ modelContext?: string | null;
4335
4335
  resources: ResourceRef[];
4336
4336
  tools: ToolRef[];
4337
4337
  toolsProvided?: boolean;
@@ -4350,12 +4350,8 @@ export type EnqueueSessionTurnInput = {
4350
4350
  placement?: "head" | "tail";
4351
4351
  };
4352
4352
 
4353
- /**
4354
- * Worker-only turn projection. Host instructions are durable execution input,
4355
- * never part of the public SessionTurn/queue/HTTP contract.
4356
- */
4353
+ /** Worker-only turn projection with causal authority omitted from public APIs. */
4357
4354
  export type SessionTurnForExecution = SessionTurn & {
4358
- turnInstructions: string | null;
4359
4355
  personalConnectionDelegations: McpPersonalConnectionDelegation[];
4360
4356
  /** Worker-only causal authority; never inferred from the current worker. */
4361
4357
  initiatingHumanSubjectId: string | null;
@@ -4581,6 +4577,89 @@ export async function requireFile(
4581
4577
  return file;
4582
4578
  }
4583
4579
 
4580
+ /**
4581
+ * Subject-bound file read for user-visible and model-injection surfaces. The
4582
+ * database predicate returns true for ordinary workspace files, but Drive-
4583
+ * derived bytes require current per-object ACL evidence for every protecting
4584
+ * mapping. Missing/stale/mixed denied mappings therefore look not found.
4585
+ */
4586
+ export async function requireFileForSubject(
4587
+ db: Database,
4588
+ input: {
4589
+ accountId: string;
4590
+ workspaceId: string;
4591
+ subjectId: string | null;
4592
+ fileId: string;
4593
+ },
4594
+ ): Promise<FileAsset> {
4595
+ return await withRlsContext(
4596
+ db,
4597
+ { accountId: input.accountId, workspaceId: input.workspaceId },
4598
+ async (scopedDb) => {
4599
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
4600
+ const subjectIdSql = input.subjectId === null ? sql`NULL::text` : sql`${input.subjectId}`;
4601
+ const [row] = await scopedDb
4602
+ .select()
4603
+ .from(schema.files)
4604
+ .where(
4605
+ and(
4606
+ eq(schema.files.accountId, input.accountId),
4607
+ eq(schema.files.workspaceId, input.workspaceId),
4608
+ eq(schema.files.id, input.fileId),
4609
+ sql`google_drive_file_authorized(
4610
+ ${input.accountId}::uuid,
4611
+ ${input.workspaceId}::uuid,
4612
+ ${subjectIdSql},
4613
+ ${input.fileId}::uuid
4614
+ )`,
4615
+ ),
4616
+ )
4617
+ .limit(1);
4618
+ if (!row) throw new Error(`File not found: ${input.fileId}`);
4619
+ return mapFile(row);
4620
+ },
4621
+ );
4622
+ }
4623
+
4624
+ /** Batch form of requireFileForSubject for durable model-history projection. */
4625
+ export async function getFilesForSubject(
4626
+ db: Database,
4627
+ input: {
4628
+ accountId: string;
4629
+ workspaceId: string;
4630
+ subjectId: string | null;
4631
+ fileIds: readonly string[];
4632
+ },
4633
+ ): Promise<FileAsset[]> {
4634
+ const ids = [...new Set(input.fileIds)];
4635
+ if (ids.length === 0) return [];
4636
+ return await withRlsContext(
4637
+ db,
4638
+ { accountId: input.accountId, workspaceId: input.workspaceId },
4639
+ async (scopedDb) => {
4640
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
4641
+ const subjectIdSql = input.subjectId === null ? sql`NULL::text` : sql`${input.subjectId}`;
4642
+ const rows = await scopedDb
4643
+ .select()
4644
+ .from(schema.files)
4645
+ .where(
4646
+ and(
4647
+ eq(schema.files.accountId, input.accountId),
4648
+ eq(schema.files.workspaceId, input.workspaceId),
4649
+ inArray(schema.files.id, ids),
4650
+ sql`google_drive_file_authorized(
4651
+ ${input.accountId}::uuid,
4652
+ ${input.workspaceId}::uuid,
4653
+ ${subjectIdSql},
4654
+ ${schema.files.id}
4655
+ )`,
4656
+ ),
4657
+ );
4658
+ return rows.map(mapFile);
4659
+ },
4660
+ );
4661
+ }
4662
+
4584
4663
  /** One RLS-scoped query for all attachment metadata needed by a model turn. */
4585
4664
  export async function getFiles(
4586
4665
  db: Database,
@@ -4603,6 +4682,7 @@ export function durableUserHistoryItem(
4603
4682
  prompt: string,
4604
4683
  resources: readonly ResourceRef[],
4605
4684
  annotations: readonly TimelineAnnotation[] = [],
4685
+ modelContext?: string | null,
4606
4686
  ): Record<string, unknown> {
4607
4687
  const attachmentRefs = resources.filter(
4608
4688
  (resource): resource is Extract<ResourceRef, { kind: "file" }> => resource.kind === "file",
@@ -4610,7 +4690,7 @@ export function durableUserHistoryItem(
4610
4690
  return {
4611
4691
  type: "message",
4612
4692
  role: "user",
4613
- content: renderTimelineAnnotationsForModel(prompt, annotations),
4693
+ content: renderUserMessageContentForModel(prompt, annotations, modelContext),
4614
4694
  ...(attachmentRefs.length > 0 ? { [MODEL_ATTACHMENT_REFS_FIELD]: attachmentRefs } : {}),
4615
4695
  ...(annotations.length > 0
4616
4696
  ? {
@@ -22578,6 +22658,122 @@ export async function upsertConnectorActionPolicy(
22578
22658
  );
22579
22659
  }
22580
22660
 
22661
+ /**
22662
+ * Install a risk-appropriate connector default without overwriting an explicit
22663
+ * user choice that already exists for the same immutable connector action.
22664
+ */
22665
+ export async function ensureConnectorActionPolicyDefault(
22666
+ db: Database,
22667
+ input: {
22668
+ accountId: string;
22669
+ workspaceId: string;
22670
+ subjectId: string;
22671
+ connectionId: string;
22672
+ serverId: string;
22673
+ toolName: string;
22674
+ actionName: string;
22675
+ policy: ConnectorActionPolicyDecision;
22676
+ },
22677
+ ): Promise<{
22678
+ policy: typeof schema.connectorActionPolicies.$inferSelect;
22679
+ changed: boolean;
22680
+ }> {
22681
+ const scope = {
22682
+ connectionId: boundedConnectorActionText(
22683
+ input.connectionId,
22684
+ "connector connection id",
22685
+ CONNECTOR_ACTION_CONNECTION_ID_MAX,
22686
+ ),
22687
+ serverId: boundedConnectorActionText(
22688
+ input.serverId,
22689
+ "connector server id",
22690
+ CONNECTOR_ACTION_SERVER_ID_MAX,
22691
+ ),
22692
+ toolName: boundedConnectorActionText(
22693
+ input.toolName,
22694
+ "connector tool name",
22695
+ CONNECTOR_ACTION_NAME_MAX,
22696
+ ),
22697
+ actionName: boundedConnectorActionText(
22698
+ input.actionName,
22699
+ "connector action name",
22700
+ CONNECTOR_ACTION_NAME_MAX,
22701
+ ),
22702
+ };
22703
+ const subjectId = boundedConnectorActionText(input.subjectId, "policy actor", 1024);
22704
+ return await withRlsContext(
22705
+ db,
22706
+ { accountId: input.accountId, workspaceId: input.workspaceId },
22707
+ async (scopedDb) =>
22708
+ await scopedDb.transaction(async (tx) => {
22709
+ await assertWorkspaceAccountPairInScope(tx, input.accountId, input.workspaceId);
22710
+ const [inserted] = await tx
22711
+ .insert(schema.connectorActionPolicies)
22712
+ .values({
22713
+ accountId: input.accountId,
22714
+ workspaceId: input.workspaceId,
22715
+ ...scope,
22716
+ policy: input.policy,
22717
+ createdBySubjectId: subjectId,
22718
+ updatedBySubjectId: subjectId,
22719
+ })
22720
+ .onConflictDoNothing({
22721
+ target: [
22722
+ schema.connectorActionPolicies.workspaceId,
22723
+ schema.connectorActionPolicies.connectionId,
22724
+ schema.connectorActionPolicies.serverId,
22725
+ schema.connectorActionPolicies.toolName,
22726
+ schema.connectorActionPolicies.actionName,
22727
+ ],
22728
+ })
22729
+ .returning();
22730
+ if (!inserted) {
22731
+ const [existing] = await tx
22732
+ .select()
22733
+ .from(schema.connectorActionPolicies)
22734
+ .where(
22735
+ and(
22736
+ eq(schema.connectorActionPolicies.workspaceId, input.workspaceId),
22737
+ eq(schema.connectorActionPolicies.connectionId, scope.connectionId),
22738
+ eq(schema.connectorActionPolicies.serverId, scope.serverId),
22739
+ eq(schema.connectorActionPolicies.toolName, scope.toolName),
22740
+ eq(schema.connectorActionPolicies.actionName, scope.actionName),
22741
+ ),
22742
+ )
22743
+ .limit(1);
22744
+ if (!existing) throw new Error("Failed to reconcile connector action policy default");
22745
+ return { policy: existing, changed: false };
22746
+ }
22747
+ await tx.insert(schema.auditEvents).values(
22748
+ withLosslessContentWriteVersion(
22749
+ {
22750
+ accountId: input.accountId,
22751
+ workspaceId: input.workspaceId,
22752
+ subjectId,
22753
+ action: "connector.action.policy_changed",
22754
+ targetType: "connector_action_policy",
22755
+ targetId: inserted.id,
22756
+ metadata: {
22757
+ connectionId: inserted.connectionId,
22758
+ serverId: inserted.serverId,
22759
+ toolName: inserted.toolName,
22760
+ actionName: inserted.actionName,
22761
+ policy: inserted.policy,
22762
+ version: inserted.version,
22763
+ previousPolicy: null,
22764
+ previousVersion: null,
22765
+ source: "connector_default",
22766
+ },
22767
+ },
22768
+ "metadata",
22769
+ "metadataCodecVersion",
22770
+ ),
22771
+ );
22772
+ return { policy: inserted, changed: true };
22773
+ }),
22774
+ );
22775
+ }
22776
+
22581
22777
  export async function prepareConnectorActionApproval(
22582
22778
  db: Database,
22583
22779
  identity: ConnectorActionAttemptIdentity,
@@ -22916,7 +23112,7 @@ export type SessionCreateInput = {
22916
23112
  accountId: string;
22917
23113
  workspaceId: string;
22918
23114
  initialMessage: string;
22919
- initialTurnInstructions?: string | null;
23115
+ initialModelContext?: string | null;
22920
23116
  resources: ResourceRef[];
22921
23117
  skills?: SessionSkill[];
22922
23118
  tools?: ToolRef[];
@@ -23366,7 +23562,7 @@ async function createSessionInTransaction(
23366
23562
  accountId: input.accountId,
23367
23563
  workspaceId: input.workspaceId,
23368
23564
  initialMessage: input.initialMessage,
23369
- initialTurnInstructions: input.initialTurnInstructions ?? null,
23565
+ initialModelContext: input.initialModelContext ?? null,
23370
23566
  resources: input.resources,
23371
23567
  skills: input.skills ?? [],
23372
23568
  tools: input.tools ?? [],
@@ -45617,6 +45813,7 @@ export async function materializeGoalContinuation(
45617
45813
  if (
45618
45814
  !goalRead ||
45619
45815
  goalRead.status !== "active" ||
45816
+ session.status === "failed" ||
45620
45817
  session.status === "cancelled" ||
45621
45818
  effectiveControl.state !== "active"
45622
45819
  ) {
@@ -46370,6 +46567,9 @@ export async function initializeSessionStartAtomically(
46370
46567
  type: "user.message",
46371
46568
  payload: {
46372
46569
  ...initialPayload,
46570
+ ...(session.initialModelContext
46571
+ ? { modelContext: session.initialModelContext }
46572
+ : {}),
46373
46573
  initiator: creator.initiator,
46374
46574
  },
46375
46575
  clientEventId: input.clientEventId ?? `session-initial:${session.id}`,
@@ -46425,7 +46625,7 @@ export async function initializeSessionStartAtomically(
46425
46625
  source: "user",
46426
46626
  position: queueTailPosition,
46427
46627
  prompt: canonicalInitialMessage,
46428
- turnInstructions: session.initialTurnInstructions ?? null,
46628
+ modelContext: session.initialModelContext ?? null,
46429
46629
  resources: session.resources,
46430
46630
  tools: session.tools,
46431
46631
  toolsProvided: session.toolPolicy?.mode === "explicit",
@@ -46626,7 +46826,7 @@ export async function enqueueSessionTurn(
46626
46826
  source: input.source,
46627
46827
  position,
46628
46828
  prompt: input.prompt,
46629
- turnInstructions: input.turnInstructions ?? null,
46829
+ modelContext: input.modelContext ?? null,
46630
46830
  resources: input.resources,
46631
46831
  tools: input.tools,
46632
46832
  toolsProvided: input.toolsProvided ?? false,
@@ -46974,9 +47174,14 @@ export async function claimSessionWorkForAttempt(
46974
47174
  input: ClaimSessionWorkForAttemptInput,
46975
47175
  ): Promise<ClaimSessionWorkForAttemptResult> {
46976
47176
  const { sessionId, workflowId } = input;
46977
- return await withWorkspaceSessionActivityRls(
47177
+ return await retrySessionActivityRls(
46978
47178
  db,
46979
47179
  workspaceId,
47180
+ {
47181
+ stage: "session_attempts.claim",
47182
+ eventTypes: ["session.turn.attempt_claimed"],
47183
+ maxAttempts: 3,
47184
+ },
46980
47185
  async (scopedDb) =>
46981
47186
  await withSessionActivitySavepoint(scopedDb, async (tx) => {
46982
47187
  const deliverPendingUpdates = async (
@@ -48372,6 +48577,7 @@ export async function claimSessionWorkForAttempt(
48372
48577
  fromPostgresLosslessText(row.prompt, row.promptCodecVersion),
48373
48578
  Array.isArray(row.resources) ? (row.resources as ResourceRef[]) : [],
48374
48579
  TimelineAnnotations.parse(row.annotations),
48580
+ row.modelContext,
48375
48581
  ),
48376
48582
  },
48377
48583
  "item",
@@ -50003,6 +50209,405 @@ export async function peekSessionWork(
50003
50209
  });
50004
50210
  }
50005
50211
 
50212
+ export type FailSessionWorkBeforeAttemptClaimInput = {
50213
+ accountId: string;
50214
+ sessionId: string;
50215
+ workflowId: string;
50216
+ trigger: SessionWorkTrigger;
50217
+ error: string;
50218
+ };
50219
+
50220
+ export type FailSessionWorkBeforeAttemptClaimResult =
50221
+ | { action: "failed"; turnId: string | null; events: SessionEvent[] }
50222
+ | { action: "terminal"; turnId: null; events: [] }
50223
+ | { action: "stale"; turnId: null; events: [] };
50224
+
50225
+ /**
50226
+ * Terminally settle a permanent admission failure that happened before an
50227
+ * attempt row existed. The workflow supplies the exact trigger recorded in
50228
+ * Temporal history; this transaction re-evaluates it under the canonical
50229
+ * control -> workspace -> session -> turn lock order and refuses stale,
50230
+ * paused, capacity-blocked, or concurrently claimed work. No attempt row is
50231
+ * fabricated because provider/tool execution never started.
50232
+ */
50233
+ export async function failSessionWorkBeforeAttemptClaim(
50234
+ db: Database,
50235
+ workspaceId: string,
50236
+ input: FailSessionWorkBeforeAttemptClaimInput,
50237
+ ): Promise<FailSessionWorkBeforeAttemptClaimResult> {
50238
+ return await retrySessionActivityRls(
50239
+ db,
50240
+ workspaceId,
50241
+ {
50242
+ stage: "session_lifecycle_outbox.fail_before_attempt_claim",
50243
+ eventTypes: ["child_terminal_result", "session.status.changed", "turn.failed"],
50244
+ maxAttempts: 3,
50245
+ },
50246
+ async (scopedDb) =>
50247
+ await scopedDb.transaction(async (tx) => {
50248
+ const baseLocks = await lockChildLifecycleOutboxWriteRowsTx(
50249
+ tx as unknown as Database,
50250
+ workspaceId,
50251
+ { sessionId: input.sessionId },
50252
+ );
50253
+ const session = baseLocks.session;
50254
+ if (session.accountId !== input.accountId) {
50255
+ throw new SessionControlInvariantError(
50256
+ `Session ${input.sessionId} does not belong to account ${input.accountId}`,
50257
+ );
50258
+ }
50259
+ if (session.status === "failed" || session.status === "cancelled") {
50260
+ return { action: "terminal", turnId: null, events: [] } as const;
50261
+ }
50262
+ if (
50263
+ session.temporalWorkflowId !== null &&
50264
+ session.temporalWorkflowId !== input.workflowId
50265
+ ) {
50266
+ return { action: "stale", turnId: null, events: [] } as const;
50267
+ }
50268
+ const effectiveControl = await evaluateSessionControl(
50269
+ tx as unknown as Database,
50270
+ workspaceId,
50271
+ input.sessionId,
50272
+ { workspaceControl: baseLocks.control ?? undefined },
50273
+ );
50274
+ if (!baseLocks.workspace || effectiveControl.state !== "active") {
50275
+ return { action: "stale", turnId: null, events: [] } as const;
50276
+ }
50277
+
50278
+ // The session lock serializes this check with claim. If another worker
50279
+ // committed ownership while the activity response was ambiguous, its
50280
+ // attempt is authoritative and this terminal path must no-op.
50281
+ const [liveAttempt] = await tx
50282
+ .select({ id: schema.sessionTurnAttempts.id })
50283
+ .from(schema.sessionTurnAttempts)
50284
+ .where(
50285
+ and(
50286
+ eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
50287
+ eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
50288
+ inArray(schema.sessionTurnAttempts.state, ["claimed", "running"]),
50289
+ ),
50290
+ )
50291
+ .limit(1);
50292
+ if (liveAttempt) {
50293
+ return { action: "stale", turnId: null, events: [] } as const;
50294
+ }
50295
+
50296
+ let turn: typeof schema.sessionTurns.$inferSelect | null = null;
50297
+ if (session.activeTurnId) {
50298
+ const exactLocks = await lockSessionEventWriteRows(tx as unknown as Database, {
50299
+ workspaceId,
50300
+ controlLock: "already_locked",
50301
+ workspaceLock: "already_locked",
50302
+ turnIds: [session.activeTurnId],
50303
+ });
50304
+ turn = exactLocks.turns[0] ?? null;
50305
+ if (!turn || turn.accountId !== session.accountId || turn.sessionId !== input.sessionId) {
50306
+ // The claim failed on this same locked corrupt pointer. There is no
50307
+ // safe turn identity to settle, but retrying can never repair it;
50308
+ // fail the owning session and its session-level obligations below.
50309
+ turn = null;
50310
+ }
50311
+ if (turn && isTerminalSessionTurnStatus(turn.status as SessionTurnStatus)) {
50312
+ // Repair the corrupt session pointer without writing a second
50313
+ // terminal event onto an already-terminal logical turn.
50314
+ turn = null;
50315
+ } else if (turn && input.trigger.kind === "approval") {
50316
+ if (
50317
+ turn.status !== "requires_action" ||
50318
+ !(await isNewerApprovalTrigger(
50319
+ tx as unknown as Database,
50320
+ workspaceId,
50321
+ input.sessionId,
50322
+ turn.triggerEventId,
50323
+ input.trigger.triggerEventId,
50324
+ ))
50325
+ ) {
50326
+ return { action: "stale", turnId: null, events: [] } as const;
50327
+ }
50328
+ } else if (turn) {
50329
+ if (turn.status === "requires_action") {
50330
+ return { action: "stale", turnId: null, events: [] } as const;
50331
+ }
50332
+ if (turn.status === "waiting_capacity") {
50333
+ const [codexWaiter] = await tx
50334
+ .select({ id: schema.codexCapacityWaiters.id })
50335
+ .from(schema.codexCapacityWaiters)
50336
+ .where(
50337
+ and(
50338
+ eq(schema.codexCapacityWaiters.workspaceId, workspaceId),
50339
+ eq(schema.codexCapacityWaiters.sessionId, input.sessionId),
50340
+ eq(schema.codexCapacityWaiters.status, "waiting"),
50341
+ ),
50342
+ )
50343
+ .limit(1);
50344
+ const xaiWaiter = await getXaiCapacityWaitForSessionInTransaction(
50345
+ tx as unknown as Database,
50346
+ workspaceId,
50347
+ input.sessionId,
50348
+ );
50349
+ if (codexWaiter || xaiWaiter) {
50350
+ return { action: "stale", turnId: null, events: [] } as const;
50351
+ }
50352
+ }
50353
+ }
50354
+ } else {
50355
+ if (input.trigger.kind === "approval") {
50356
+ return { action: "stale", turnId: null, events: [] } as const;
50357
+ }
50358
+ const [pendingAgentSteer] = await tx
50359
+ .select({ id: schema.sessionSystemUpdates.id })
50360
+ .from(schema.sessionSystemUpdates)
50361
+ .where(
50362
+ and(
50363
+ eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
50364
+ eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
50365
+ eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
50366
+ eq(schema.sessionSystemUpdates.state, "pending"),
50367
+ ),
50368
+ )
50369
+ .orderBy(
50370
+ desc(schema.sessionSystemUpdates.createdAt),
50371
+ desc(schema.sessionSystemUpdates.id),
50372
+ )
50373
+ .limit(1);
50374
+ const queuedRows = await rawRows<{ id: string }>(
50375
+ tx as unknown as Database,
50376
+ sql`select id from session_turns
50377
+ where workspace_id = ${workspaceId} and session_id = ${input.sessionId}
50378
+ and status = 'queued' and source in ('user', 'api')
50379
+ and (
50380
+ ${Boolean(pendingAgentSteer)} = false
50381
+ or metadata->>'delivery' = 'steer'
50382
+ )
50383
+ order by position asc, created_at asc, id asc
50384
+ limit 1`,
50385
+ );
50386
+ const queuedPreview = queuedRows[0];
50387
+ if (queuedPreview) {
50388
+ const exactLocks = await lockSessionEventWriteRows(tx as unknown as Database, {
50389
+ workspaceId,
50390
+ controlLock: "already_locked",
50391
+ workspaceLock: "already_locked",
50392
+ turnIds: [queuedPreview.id],
50393
+ });
50394
+ turn = exactLocks.turns[0] ?? null;
50395
+ if (
50396
+ !turn ||
50397
+ turn.accountId !== session.accountId ||
50398
+ turn.sessionId !== input.sessionId ||
50399
+ turn.status !== "queued" ||
50400
+ turn.activeAttemptId !== null
50401
+ ) {
50402
+ return { action: "stale", turnId: null, events: [] } as const;
50403
+ }
50404
+ } else if (!session.compactRequested) {
50405
+ if (
50406
+ !pendingAgentSteer &&
50407
+ (await latestFinishedTurnHasFailureCodeTx(
50408
+ tx as unknown as Database,
50409
+ workspaceId,
50410
+ input.sessionId,
50411
+ "context_compaction_failed",
50412
+ ))
50413
+ ) {
50414
+ return { action: "stale", turnId: null, events: [] } as const;
50415
+ }
50416
+ const [pendingUpdate] = await tx
50417
+ .select({ id: schema.sessionSystemUpdates.id })
50418
+ .from(schema.sessionSystemUpdates)
50419
+ .where(
50420
+ and(
50421
+ eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
50422
+ eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
50423
+ eq(schema.sessionSystemUpdates.state, "pending"),
50424
+ ),
50425
+ )
50426
+ .limit(1)
50427
+ .for("update");
50428
+ if (!pendingUpdate) {
50429
+ return { action: "stale", turnId: null, events: [] } as const;
50430
+ }
50431
+ }
50432
+ }
50433
+
50434
+ const now = new Date();
50435
+ let sequence = session.lastSequence;
50436
+ let closedToolEvents: SessionEvent[] = [];
50437
+ if (turn) {
50438
+ await cancelTurnInteractionInterventionsInTransaction(tx as unknown as Database, {
50439
+ accountId: session.accountId,
50440
+ workspaceId,
50441
+ sessionId: input.sessionId,
50442
+ turnId: turn.id,
50443
+ });
50444
+ const closedTools = await closePendingSessionToolCallsInTransaction(
50445
+ tx as unknown as Database,
50446
+ {
50447
+ accountId: session.accountId,
50448
+ workspaceId,
50449
+ sessionId: input.sessionId,
50450
+ turnId: turn.id,
50451
+ reason: "turn_failed",
50452
+ sequence,
50453
+ now,
50454
+ },
50455
+ );
50456
+ sequence = closedTools.sequence;
50457
+ closedToolEvents = closedTools.events;
50458
+ await tx
50459
+ .update(schema.sessionHumanInputRequests)
50460
+ .set({
50461
+ status: "cancelled",
50462
+ response: { outcome: "cancelled" },
50463
+ respondedBy: "system:turn_failed",
50464
+ respondedAt: now,
50465
+ updatedAt: now,
50466
+ })
50467
+ .where(
50468
+ and(
50469
+ eq(schema.sessionHumanInputRequests.workspaceId, workspaceId),
50470
+ eq(schema.sessionHumanInputRequests.sessionId, input.sessionId),
50471
+ eq(schema.sessionHumanInputRequests.turnId, turn.id),
50472
+ eq(schema.sessionHumanInputRequests.status, "pending"),
50473
+ ),
50474
+ );
50475
+ }
50476
+ await settleSessionMaintenanceInTransaction(tx as unknown as Database, {
50477
+ accountId: session.accountId,
50478
+ workspaceId,
50479
+ sessionId: input.sessionId,
50480
+ });
50481
+ if (!turn) {
50482
+ await tx
50483
+ .update(schema.sessionSystemUpdates)
50484
+ .set({ state: "failed" })
50485
+ .where(
50486
+ and(
50487
+ eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
50488
+ eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
50489
+ eq(schema.sessionSystemUpdates.state, "pending"),
50490
+ ),
50491
+ );
50492
+ }
50493
+ const values = [
50494
+ ...(turn
50495
+ ? [
50496
+ {
50497
+ accountId: session.accountId,
50498
+ workspaceId,
50499
+ sessionId: input.sessionId,
50500
+ sequence: ++sequence,
50501
+ type: "turn.failed" as const,
50502
+ payload: {
50503
+ triggerEventId: turn.triggerEventId,
50504
+ code: "pre_claim_failure",
50505
+ error: input.error,
50506
+ },
50507
+ turnId: turn.id,
50508
+ turnGeneration: turn.executionGeneration,
50509
+ turnAttemptId: null,
50510
+ turnAssociation: "current" as const,
50511
+ occurredAt: now,
50512
+ },
50513
+ ]
50514
+ : []),
50515
+ {
50516
+ accountId: session.accountId,
50517
+ workspaceId,
50518
+ sessionId: input.sessionId,
50519
+ sequence: ++sequence,
50520
+ type: "session.status.changed" as const,
50521
+ payload: { status: "failed", code: "pre_claim_failure" },
50522
+ turnId: turn?.id ?? null,
50523
+ turnGeneration: turn?.executionGeneration ?? null,
50524
+ turnAttemptId: null,
50525
+ turnAssociation: turn ? ("current" as const) : null,
50526
+ occurredAt: now,
50527
+ },
50528
+ ];
50529
+ const inserted = await tx
50530
+ .insert(schema.sessionEvents)
50531
+ .values(withLosslessContentWriteVersion(values, "payload", "payloadCodecVersion"))
50532
+ .returning();
50533
+ if (turn) {
50534
+ const failedEvent = inserted.find((event) => event.type === "turn.failed");
50535
+ if (!failedEvent) {
50536
+ throw new SessionControlInvariantError(
50537
+ `Pre-claim failure for turn ${turn.id} has no terminal event`,
50538
+ );
50539
+ }
50540
+ await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
50541
+ accountId: session.accountId,
50542
+ workspaceId,
50543
+ sessionId: input.sessionId,
50544
+ turnId: turn.id,
50545
+ turnStatus: "failed",
50546
+ terminalEvent: {
50547
+ id: failedEvent.id,
50548
+ type: "turn.failed",
50549
+ payload: sessionEventPayloadRecord(
50550
+ failedEvent.payload,
50551
+ failedEvent.payloadCodecVersion,
50552
+ ),
50553
+ },
50554
+ now,
50555
+ });
50556
+ await tx
50557
+ .update(schema.sessionTurns)
50558
+ .set({
50559
+ status: "failed",
50560
+ activeAttemptId: null,
50561
+ version: turn.version + 1,
50562
+ finishedAt: now,
50563
+ updatedAt: now,
50564
+ })
50565
+ .where(
50566
+ and(
50567
+ eq(schema.sessionTurns.workspaceId, workspaceId),
50568
+ eq(schema.sessionTurns.sessionId, input.sessionId),
50569
+ eq(schema.sessionTurns.id, turn.id),
50570
+ ),
50571
+ );
50572
+ await enqueueFailedChildOutboxForTurnTx(
50573
+ tx as unknown as Database,
50574
+ workspaceId,
50575
+ session,
50576
+ turn,
50577
+ );
50578
+ } else {
50579
+ await enqueueFailedChildOutboxWithoutTurnTx(
50580
+ tx as unknown as Database,
50581
+ workspaceId,
50582
+ session,
50583
+ sequence,
50584
+ );
50585
+ }
50586
+ await tx
50587
+ .update(schema.sessions)
50588
+ .set({
50589
+ status: "failed",
50590
+ activeTurnId: null,
50591
+ compactRequested: false,
50592
+ lastSequence: sequence,
50593
+ queueVersion: session.queueVersion + 1,
50594
+ updatedAt: now,
50595
+ })
50596
+ .where(
50597
+ and(
50598
+ eq(schema.sessions.workspaceId, workspaceId),
50599
+ eq(schema.sessions.id, input.sessionId),
50600
+ ),
50601
+ );
50602
+ return {
50603
+ action: "failed",
50604
+ turnId: turn?.id ?? null,
50605
+ events: [...closedToolEvents, ...inserted.map(mapEvent)],
50606
+ } as const;
50607
+ }),
50608
+ );
50609
+ }
50610
+
50006
50611
  /**
50007
50612
  * Child lifecycle transactions discover immutable parentage only after taking
50008
50613
  * the control/workspace prefix. They must then lock child and parent together,
@@ -52804,14 +53409,16 @@ function queuedSteerReplacementAttemptId(metadata: Record<string, unknown>): str
52804
53409
  return null;
52805
53410
  }
52806
53411
 
52807
- async function enqueueFailedChildOutboxForTurnTx(
53412
+ async function enqueueFailedChildOutboxTx(
52808
53413
  tx: Database,
52809
53414
  workspaceId: string,
52810
- session: Pick<typeof schema.sessions.$inferSelect, "id" | "parentSessionId" | "parentTurnId">,
52811
- turn: Pick<typeof schema.sessionTurns.$inferSelect, "id" | "accountId" | "sessionId">,
53415
+ session: Pick<
53416
+ typeof schema.sessions.$inferSelect,
53417
+ "id" | "accountId" | "parentSessionId" | "parentTurnId"
53418
+ >,
53419
+ input: { turnId: string | null; dedupeKey: string },
52812
53420
  ): Promise<void> {
52813
53421
  if (!session.parentSessionId) return;
52814
- const dedupeKey = `child-completion:${turn.sessionId}:turn:${turn.id}`;
52815
53422
  const personalConnectionDelegations = session.parentTurnId
52816
53423
  ? await personalConnectionDelegationsForTurnInTransaction(
52817
53424
  tx,
@@ -52859,26 +53466,26 @@ async function enqueueFailedChildOutboxForTurnTx(
52859
53466
  withLosslessContentWriteVersion(
52860
53467
  withLosslessContentWriteVersion(
52861
53468
  {
52862
- accountId: turn.accountId,
53469
+ accountId: session.accountId,
52863
53470
  workspaceId,
52864
- sourceSessionId: turn.sessionId,
53471
+ sourceSessionId: session.id,
52865
53472
  targetSessionId: session.parentSessionId,
52866
- dedupeKey,
53473
+ dedupeKey: input.dedupeKey,
52867
53474
  kind: "child_terminal_result",
52868
53475
  classification: "failure",
52869
- sourceId: turn.sessionId,
53476
+ sourceId: session.id,
52870
53477
  summary: "Child session failed; inspect the durable child timeline.",
52871
53478
  payload: {
52872
53479
  type: "child_terminal_result",
52873
- childSessionId: turn.sessionId,
53480
+ childSessionId: session.id,
52874
53481
  status: "failed",
52875
- turnId: turn.id,
53482
+ ...(input.turnId ? { turnId: input.turnId } : {}),
52876
53483
  },
52877
53484
  lineage: {
52878
- childSessionId: turn.sessionId,
53485
+ childSessionId: session.id,
52879
53486
  parentSessionId: session.parentSessionId,
52880
53487
  ...(session.parentTurnId ? { parentTurnId: session.parentTurnId } : {}),
52881
- turnId: turn.id,
53488
+ ...(input.turnId ? { turnId: input.turnId } : {}),
52882
53489
  ...(xaiAuthoritySubjectId ? { xaiAuthoritySubjectId } : {}),
52883
53490
  },
52884
53491
  personalConnectionDelegations,
@@ -52899,6 +53506,41 @@ async function enqueueFailedChildOutboxForTurnTx(
52899
53506
  });
52900
53507
  }
52901
53508
 
53509
+ async function enqueueFailedChildOutboxForTurnTx(
53510
+ tx: Database,
53511
+ workspaceId: string,
53512
+ session: Pick<
53513
+ typeof schema.sessions.$inferSelect,
53514
+ "id" | "accountId" | "parentSessionId" | "parentTurnId"
53515
+ >,
53516
+ turn: Pick<typeof schema.sessionTurns.$inferSelect, "id" | "accountId" | "sessionId">,
53517
+ ): Promise<void> {
53518
+ if (turn.accountId !== session.accountId || turn.sessionId !== session.id) {
53519
+ throw new SessionControlInvariantError(
53520
+ `Failed child turn ${turn.id} lost session ${session.id} ownership`,
53521
+ );
53522
+ }
53523
+ await enqueueFailedChildOutboxTx(tx, workspaceId, session, {
53524
+ turnId: turn.id,
53525
+ dedupeKey: `child-completion:${turn.sessionId}:turn:${turn.id}`,
53526
+ });
53527
+ }
53528
+
53529
+ async function enqueueFailedChildOutboxWithoutTurnTx(
53530
+ tx: Database,
53531
+ workspaceId: string,
53532
+ session: Pick<
53533
+ typeof schema.sessions.$inferSelect,
53534
+ "id" | "accountId" | "parentSessionId" | "parentTurnId"
53535
+ >,
53536
+ terminalSequence: number,
53537
+ ): Promise<void> {
53538
+ await enqueueFailedChildOutboxTx(tx, workspaceId, session, {
53539
+ turnId: null,
53540
+ dedupeKey: `child-completion:${session.id}:pre-claim:${terminalSequence}`,
53541
+ });
53542
+ }
53543
+
52902
53544
  type ChildTerminalResultPayload = Extract<
52903
53545
  SessionSystemUpdatePayload,
52904
53546
  { type: "child_terminal_result" }
@@ -53106,10 +53748,19 @@ export async function enqueueSessionWorkflowWakeInTransaction(
53106
53748
  : sql`${schema.sessionWorkflowWakeOutbox.controlRevision}`,
53107
53749
  reason: input.reason,
53108
53750
  attempts: 0,
53109
- // Coalescing a delayed retry must never postpone an already-due wake
53110
- // owned by another producer. A later revision makes the batch richer;
53111
- // it does not revoke the earlier delivery obligation.
53112
- nextAttemptAt: sql`least(${schema.sessionWorkflowWakeOutbox.nextAttemptAt}, ${nextAttemptAt.toISOString()}::timestamptz)`,
53751
+ // Coalescing a delayed retry must never postpone an undelivered wake
53752
+ // owned by another producer. Once every prior revision is delivered,
53753
+ // however, this is a new obligation and must honor its own deadline;
53754
+ // retaining the old (usually past) deadline would defeat backoff.
53755
+ nextAttemptAt: sql`case
53756
+ when ${schema.sessionWorkflowWakeOutbox.wakeRevision}
53757
+ > ${schema.sessionWorkflowWakeOutbox.deliveredRevision}
53758
+ then least(
53759
+ ${schema.sessionWorkflowWakeOutbox.nextAttemptAt},
53760
+ ${nextAttemptAt.toISOString()}::timestamptz
53761
+ )
53762
+ else ${nextAttemptAt.toISOString()}::timestamptz
53763
+ end`,
53113
53764
  lastError: null,
53114
53765
  updatedAt: now,
53115
53766
  },
@@ -55307,7 +55958,6 @@ function mapSessionTurnForExecution(
55307
55958
  ): SessionTurnForExecution {
55308
55959
  return {
55309
55960
  ...mapSessionTurn(row),
55310
- turnInstructions: row.turnInstructions ?? null,
55311
55961
  initiatingHumanSubjectId: row.initiatingHumanSubjectId ?? null,
55312
55962
  personalConnectionDelegations: parsedPersonalConnectionDelegations(
55313
55963
  row.personalConnectionDelegations,