@opengeni/db 0.13.4 → 0.14.0

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.
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  FileAsset,
17
17
  FileStatus,
18
18
  FileUploadStatus,
19
+ FirstPartyMcpToolName,
19
20
  HumanInputAnswer,
20
21
  HumanInputQuestion,
21
22
  HumanInputResponse,
@@ -113,6 +114,7 @@ import {
113
114
  RigChange as RigChangeContract,
114
115
  SessionGoal as SessionGoalContract,
115
116
  SessionSystemUpdatePayload,
117
+ sessionSystemUpdateBatchHistoryItem,
116
118
  HostEventExport as HostEventExportContract,
117
119
  HostEventExportBatch as HostEventExportBatchContract,
118
120
  HostExportConsumerId,
@@ -13888,6 +13890,7 @@ export type SessionCreateInput = {
13888
13890
  rigId?: string | null;
13889
13891
  rigVersionId?: string | null;
13890
13892
  firstPartyMcpPermissions?: Permission[] | null;
13893
+ firstPartyMcpTools?: FirstPartyMcpToolName[] | null;
13891
13894
  instructions?: string | null;
13892
13895
  parentSessionId?: string | null;
13893
13896
  createIdempotencyKey?: string | null;
@@ -14288,6 +14291,7 @@ async function createSessionInTransaction(
14288
14291
  rigId: input.rigId ?? null,
14289
14292
  rigVersionId: input.rigVersionId ?? null,
14290
14293
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
14294
+ firstPartyMcpTools: input.firstPartyMcpTools ?? null,
14291
14295
  instructions: input.instructions ?? null,
14292
14296
  parentSessionId: input.parentSessionId ?? null,
14293
14297
  createIdempotencyKey,
@@ -28985,7 +28989,7 @@ export async function materializeGoalContinuation(
28985
28989
  eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
28986
28990
  eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
28987
28991
  eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
28988
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
28992
+ eq(schema.sessionSystemUpdates.state, "pending"),
28989
28993
  ),
28990
28994
  )
28991
28995
  .limit(1);
@@ -29148,6 +29152,7 @@ export async function materializeGoalContinuation(
29148
29152
  })
29149
29153
  .onConflictDoNothing({ target: schema.usageEvents.idempotencyKey });
29150
29154
 
29155
+ const eventPreview = internalUpdateEventMember(update);
29151
29156
  const insertedEvents = await tx
29152
29157
  .insert(schema.sessionEvents)
29153
29158
  .values([
@@ -29158,11 +29163,13 @@ export async function materializeGoalContinuation(
29158
29163
  sequence: session.lastSequence + 1,
29159
29164
  type: "system.update.pending",
29160
29165
  payload: sanitizeEventPayload({
29161
- updateId: update.id,
29162
- kind: update.kind,
29163
- classification: update.classification,
29164
- sourceId: update.sourceId,
29165
- summary: update.summary,
29166
+ updateId: eventPreview.id,
29167
+ kind: eventPreview.kind,
29168
+ classification: eventPreview.classification,
29169
+ sourceId: eventPreview.sourceId,
29170
+ sourceIdTruncated: eventPreview.sourceIdTruncated,
29171
+ summary: eventPreview.summary,
29172
+ summaryTruncated: eventPreview.summaryTruncated,
29166
29173
  }),
29167
29174
  occurredAt: now,
29168
29175
  },
@@ -29663,6 +29670,79 @@ export type SessionWorkTrigger = { kind: "next" } | { kind: "approval"; triggerE
29663
29670
  export const MAX_INTERNAL_UPDATE_BYTES = 64 * 1024;
29664
29671
  export const MAX_INTERNAL_UPDATE_BATCH_MEMBERS = 100;
29665
29672
  export const MAX_INTERNAL_UPDATE_BATCH_BYTES = 256 * 1024;
29673
+ const MAX_INTERNAL_UPDATE_EVENT_SUMMARY_BYTES = 512;
29674
+ const MAX_INTERNAL_UPDATE_EVENT_SOURCE_BYTES = 256;
29675
+
29676
+ type BoundedSystemUpdate = Pick<
29677
+ typeof schema.sessionSystemUpdates.$inferSelect,
29678
+ "id" | "kind" | "classification" | "sourceId" | "summary" | "payload" | "lineage"
29679
+ >;
29680
+
29681
+ function boundedInternalUpdateEventText(
29682
+ value: string,
29683
+ maxBytes: number,
29684
+ ): {
29685
+ text: string;
29686
+ truncated: boolean;
29687
+ } {
29688
+ if (Buffer.byteLength(value) <= maxBytes) return { text: value, truncated: false };
29689
+ const suffix = "…";
29690
+ const bodyBudget = maxBytes - Buffer.byteLength(suffix);
29691
+ const bytes = Buffer.from(value);
29692
+ let text = bytes.subarray(0, bodyBudget).toString("utf8");
29693
+ if (text.endsWith("\uFFFD")) text = text.slice(0, -1);
29694
+ return { text: `${text}${suffix}`, truncated: true };
29695
+ }
29696
+
29697
+ function internalUpdateEventMember(update: BoundedSystemUpdate) {
29698
+ const summary = boundedInternalUpdateEventText(
29699
+ update.summary,
29700
+ MAX_INTERNAL_UPDATE_EVENT_SUMMARY_BYTES,
29701
+ );
29702
+ const source = boundedInternalUpdateEventText(
29703
+ update.sourceId,
29704
+ MAX_INTERNAL_UPDATE_EVENT_SOURCE_BYTES,
29705
+ );
29706
+ return {
29707
+ id: update.id,
29708
+ kind: update.kind,
29709
+ classification: update.classification,
29710
+ sourceId: source.text,
29711
+ sourceIdTruncated: source.truncated,
29712
+ summary: summary.text,
29713
+ summaryTruncated: summary.truncated,
29714
+ };
29715
+ }
29716
+
29717
+ function selectBoundedSystemUpdateBatch<T extends BoundedSystemUpdate>(updates: readonly T[]): T[] {
29718
+ const selected: T[] = [];
29719
+ let selectedBytes = 0;
29720
+ for (const update of updates) {
29721
+ const updateBytes = Buffer.byteLength(
29722
+ JSON.stringify({
29723
+ id: update.id,
29724
+ kind: update.kind,
29725
+ classification: update.classification,
29726
+ sourceId: update.sourceId,
29727
+ summary: update.summary,
29728
+ payload: update.payload,
29729
+ lineage: update.lineage,
29730
+ }),
29731
+ );
29732
+ if (
29733
+ selected.length >= MAX_INTERNAL_UPDATE_BATCH_MEMBERS ||
29734
+ // One individually large canonical input must still make progress. The
29735
+ // model/context boundary may reject it explicitly, but the queue cannot
29736
+ // wedge forever merely because the coalescing target is smaller.
29737
+ (selected.length > 0 && selectedBytes + updateBytes > MAX_INTERNAL_UPDATE_BATCH_BYTES)
29738
+ ) {
29739
+ break;
29740
+ }
29741
+ selected.push(update);
29742
+ selectedBytes += updateBytes;
29743
+ }
29744
+ return selected;
29745
+ }
29666
29746
 
29667
29747
  export type ClaimSessionWorkForAttemptInput = {
29668
29748
  sessionId: string;
@@ -29708,7 +29788,10 @@ export async function claimSessionWorkForAttempt(
29708
29788
  count: number;
29709
29789
  lastSequence: number;
29710
29790
  triggerEventId: string | null;
29791
+ historyItemId: string | null;
29792
+ historyItem: Record<string, unknown> | null;
29711
29793
  updates: Array<typeof schema.sessionSystemUpdates.$inferSelect>;
29794
+ events: Array<typeof schema.sessionEvents.$inferInsert>;
29712
29795
  event: typeof schema.sessionEvents.$inferInsert | null;
29713
29796
  }> => {
29714
29797
  const [agentSteer] = await tx
@@ -29718,7 +29801,7 @@ export async function claimSessionWorkForAttempt(
29718
29801
  and(
29719
29802
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
29720
29803
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
29721
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
29804
+ eq(schema.sessionSystemUpdates.state, "pending"),
29722
29805
  eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
29723
29806
  ),
29724
29807
  )
@@ -29728,20 +29811,6 @@ export async function claimSessionWorkForAttempt(
29728
29811
  )
29729
29812
  .limit(1)
29730
29813
  .for("update");
29731
- if (agentSteer) {
29732
- await tx
29733
- .update(schema.sessionSystemUpdates)
29734
- .set({ state: "superseded" })
29735
- .where(
29736
- and(
29737
- eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
29738
- eq(schema.sessionSystemUpdates.sessionId, sessionId),
29739
- eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
29740
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
29741
- ne(schema.sessionSystemUpdates.id, agentSteer.id),
29742
- ),
29743
- );
29744
- }
29745
29814
  const ordinary = await tx
29746
29815
  .select()
29747
29816
  .from(schema.sessionSystemUpdates)
@@ -29749,7 +29818,7 @@ export async function claimSessionWorkForAttempt(
29749
29818
  and(
29750
29819
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
29751
29820
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
29752
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
29821
+ eq(schema.sessionSystemUpdates.state, "pending"),
29753
29822
  ne(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
29754
29823
  ),
29755
29824
  )
@@ -29765,12 +29834,15 @@ export async function claimSessionWorkForAttempt(
29765
29834
  count: 0,
29766
29835
  lastSequence: nextSequence - 1,
29767
29836
  triggerEventId: null,
29837
+ historyItemId: null,
29838
+ historyItem: null,
29768
29839
  updates: [],
29840
+ events: [],
29769
29841
  event: null,
29770
29842
  };
29771
29843
  }
29772
- const deliverable: typeof updates = [];
29773
- let deliveredBytes = 0;
29844
+ const validUpdates: typeof updates = [];
29845
+ const cancelledUpdateIds: string[] = [];
29774
29846
  for (const update of updates) {
29775
29847
  const payload = update.payload;
29776
29848
  if (payload.type === "goal_continuation") {
@@ -29799,43 +29871,66 @@ export async function claimSessionWorkForAttempt(
29799
29871
  .update(schema.sessionSystemUpdates)
29800
29872
  .set({ state: "cancelled" })
29801
29873
  .where(eq(schema.sessionSystemUpdates.id, update.id));
29874
+ cancelledUpdateIds.push(update.id);
29802
29875
  continue;
29803
29876
  }
29804
29877
  }
29805
- const updateBytes = Buffer.byteLength(
29806
- JSON.stringify({
29807
- id: update.id,
29808
- kind: update.kind,
29809
- classification: update.classification,
29810
- sourceId: update.sourceId,
29811
- summary: update.summary,
29812
- payload: update.payload,
29813
- lineage: update.lineage,
29814
- }),
29815
- );
29816
- if (
29817
- deliverable.length >= MAX_INTERNAL_UPDATE_BATCH_MEMBERS ||
29818
- deliveredBytes + updateBytes > MAX_INTERNAL_UPDATE_BATCH_BYTES
29819
- ) {
29820
- break;
29821
- }
29822
- deliverable.push(update);
29823
- deliveredBytes += updateBytes;
29878
+ validUpdates.push(update);
29824
29879
  }
29880
+ const deliverable = selectBoundedSystemUpdateBatch(validUpdates);
29825
29881
  if (deliverable.length === 0) {
29882
+ const cancellationEvent =
29883
+ cancelledUpdateIds.length > 0
29884
+ ? {
29885
+ accountId,
29886
+ workspaceId,
29887
+ sessionId,
29888
+ // No receiving turn exists when every candidate was
29889
+ // cancelled before a model batch could be persisted.
29890
+ turnId: null,
29891
+ turnGeneration: null,
29892
+ turnAttemptId: null,
29893
+ turnAssociation: null,
29894
+ sequence: nextSequence,
29895
+ type: "system.update.cancelled" as const,
29896
+ payload: sanitizeEventPayload({
29897
+ updateIds: cancelledUpdateIds,
29898
+ count: cancelledUpdateIds.length,
29899
+ reason: "stale_goal_continuation",
29900
+ }),
29901
+ occurredAt,
29902
+ }
29903
+ : null;
29826
29904
  return {
29827
29905
  count: 0,
29828
- lastSequence: nextSequence - 1,
29906
+ lastSequence: cancellationEvent ? nextSequence : nextSequence - 1,
29829
29907
  triggerEventId: null,
29908
+ historyItemId: null,
29909
+ historyItem: null,
29830
29910
  updates: [],
29911
+ events: cancellationEvent ? [cancellationEvent] : [],
29831
29912
  event: null,
29832
29913
  };
29833
29914
  }
29915
+ // Inclusion gives the newest Steer first refusal on the bounded
29916
+ // batch. Model ordering is deliberately the opposite: ordinary
29917
+ // updates establish context, then the authoritative replacement
29918
+ // direction is last so it cannot be overridden by an older goal or
29919
+ // lifecycle notice.
29920
+ const modelOrdered = [
29921
+ ...deliverable.filter((update) => update.kind !== "agent_steer_instruction"),
29922
+ ...deliverable.filter((update) => update.kind === "agent_steer_instruction"),
29923
+ ];
29924
+ const historyItemId = crypto.randomUUID();
29925
+ const historyItem = sessionSystemUpdateBatchHistoryItem(
29926
+ modelOrdered.map((update) => mapSessionSystemUpdate(update)),
29927
+ ) as Record<string, unknown>;
29834
29928
  await tx
29835
29929
  .update(schema.sessionSystemUpdates)
29836
29930
  .set({
29837
29931
  state: "delivered",
29838
29932
  deliveredTurnId: turnId,
29933
+ deliveredHistoryItemId: historyItemId,
29839
29934
  deliveredAt: occurredAt,
29840
29935
  })
29841
29936
  .where(
@@ -29849,6 +29944,27 @@ export async function claimSessionWorkForAttempt(
29849
29944
  ),
29850
29945
  );
29851
29946
  const eventId = triggerEventId ?? crypto.randomUUID();
29947
+ let sequence = nextSequence - 1;
29948
+ const events: Array<typeof schema.sessionEvents.$inferInsert> = [];
29949
+ if (cancelledUpdateIds.length > 0) {
29950
+ events.push({
29951
+ accountId,
29952
+ workspaceId,
29953
+ sessionId,
29954
+ turnId,
29955
+ turnGeneration,
29956
+ turnAttemptId: input.attemptId,
29957
+ turnAssociation: "current",
29958
+ sequence: ++sequence,
29959
+ type: "system.update.cancelled",
29960
+ payload: sanitizeEventPayload({
29961
+ updateIds: cancelledUpdateIds,
29962
+ count: cancelledUpdateIds.length,
29963
+ reason: "stale_goal_continuation",
29964
+ }),
29965
+ occurredAt,
29966
+ });
29967
+ }
29852
29968
  const event: typeof schema.sessionEvents.$inferInsert = {
29853
29969
  id: eventId,
29854
29970
  accountId,
@@ -29858,24 +29974,64 @@ export async function claimSessionWorkForAttempt(
29858
29974
  turnGeneration,
29859
29975
  turnAttemptId: input.attemptId,
29860
29976
  turnAssociation: "current",
29861
- sequence: nextSequence,
29977
+ sequence: ++sequence,
29862
29978
  type: "system.update.delivered",
29863
29979
  payload: sanitizeEventPayload({
29864
29980
  updateIds: deliverable.map((update) => update.id),
29981
+ historyItemId,
29865
29982
  count: deliverable.length,
29866
29983
  classifications: [...new Set(deliverable.map((update) => update.classification))],
29984
+ members: modelOrdered.map(internalUpdateEventMember),
29867
29985
  }),
29868
29986
  occurredAt,
29869
29987
  };
29988
+ events.push(event);
29870
29989
  return {
29871
29990
  count: deliverable.length,
29872
- lastSequence: nextSequence,
29991
+ lastSequence: sequence,
29873
29992
  triggerEventId: eventId,
29993
+ historyItemId,
29994
+ historyItem,
29874
29995
  updates: deliverable,
29996
+ events,
29875
29997
  event,
29876
29998
  };
29877
29999
  };
29878
30000
 
30001
+ const persistDeliveredUpdateBatch = async (
30002
+ delivered: Awaited<ReturnType<typeof deliverPendingUpdates>>,
30003
+ accountId: string,
30004
+ turnId: string,
30005
+ ): Promise<void> => {
30006
+ if (!delivered.historyItemId || !delivered.historyItem) {
30007
+ if (delivered.count !== 0) {
30008
+ throw new Error("Delivered machine-input batch has no model-memory item");
30009
+ }
30010
+ return;
30011
+ }
30012
+ const [{ position } = { position: 0 }] = await tx
30013
+ .select({
30014
+ position: sql<number>`coalesce(max(${schema.sessionHistoryItems.position}), -1) + 1`,
30015
+ })
30016
+ .from(schema.sessionHistoryItems)
30017
+ .where(
30018
+ and(
30019
+ eq(schema.sessionHistoryItems.workspaceId, workspaceId),
30020
+ eq(schema.sessionHistoryItems.sessionId, sessionId),
30021
+ ),
30022
+ );
30023
+ await tx.insert(schema.sessionHistoryItems).values({
30024
+ id: delivered.historyItemId,
30025
+ accountId,
30026
+ workspaceId,
30027
+ sessionId,
30028
+ turnId,
30029
+ position: Number(position),
30030
+ item: sanitizeModelPayload(delivered.historyItem),
30031
+ producerCodexCredentialId: null,
30032
+ });
30033
+ };
30034
+
29879
30035
  // Capacity settlement and resume use session -> turn after their
29880
30036
  // workspace rotation lock. Claiming must preserve that shared order:
29881
30037
  // taking a queued turn first can deadlock with a settlement that owns
@@ -30211,7 +30367,7 @@ export async function claimSessionWorkForAttempt(
30211
30367
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
30212
30368
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
30213
30369
  eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
30214
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
30370
+ eq(schema.sessionSystemUpdates.state, "pending"),
30215
30371
  ),
30216
30372
  )
30217
30373
  .orderBy(
@@ -30448,6 +30604,22 @@ export async function claimSessionWorkForAttempt(
30448
30604
  triggerEventId,
30449
30605
  );
30450
30606
  if (delivered.count === 0) {
30607
+ if (delivered.events.length > 0) {
30608
+ await tx.insert(schema.sessionEvents).values(delivered.events);
30609
+ await tx
30610
+ .update(schema.sessions)
30611
+ .set({
30612
+ status: "idle",
30613
+ lastSequence: delivered.lastSequence,
30614
+ updatedAt: now,
30615
+ })
30616
+ .where(
30617
+ and(
30618
+ eq(schema.sessions.workspaceId, workspaceId),
30619
+ eq(schema.sessions.id, sessionId),
30620
+ ),
30621
+ );
30622
+ }
30451
30623
  return { action: "unclaimed", reason: "no-work" };
30452
30624
  }
30453
30625
  const goalUpdate = delivered.updates.find(
@@ -30634,9 +30806,10 @@ export async function claimSessionWorkForAttempt(
30634
30806
  })
30635
30807
  .returning();
30636
30808
  if (!internalTurn) throw new Error("Failed to create internal update inference");
30809
+ await persistDeliveredUpdateBatch(delivered, session.accountId, internalTurn.id);
30637
30810
  await registerAttempt(internalTurn);
30638
30811
  if (!delivered.event) throw new Error("Delivered update batch has no durable event");
30639
- await tx.insert(schema.sessionEvents).values(delivered.event);
30812
+ await tx.insert(schema.sessionEvents).values(delivered.events);
30640
30813
  if (goalUpdate && typeof goalUpdate.payload.goalId === "string") {
30641
30814
  await tx
30642
30815
  .update(schema.sessionGoals)
@@ -30753,8 +30926,9 @@ export async function claimSessionWorkForAttempt(
30753
30926
  session.lastSequence + 1,
30754
30927
  now,
30755
30928
  );
30756
- if (delivered.event) {
30757
- await tx.insert(schema.sessionEvents).values(delivered.event);
30929
+ await persistDeliveredUpdateBatch(delivered, session.accountId, row.id);
30930
+ if (delivered.events.length > 0) {
30931
+ await tx.insert(schema.sessionEvents).values(delivered.events);
30758
30932
  }
30759
30933
  await tx
30760
30934
  .update(schema.sessions)
@@ -30998,6 +31172,196 @@ export async function markSessionAttemptQuiesced(
30998
31172
  });
30999
31173
  }
31000
31174
 
31175
+ export type ReconcileSessionAttemptQuiescenceResult =
31176
+ | { action: "quiesced"; events: SessionEvent[] }
31177
+ | { action: "pending"; events: [] }
31178
+ | { action: "stale"; events: [] };
31179
+
31180
+ export type SessionAttemptActivityRef = {
31181
+ workflowId: string;
31182
+ workflowRunId: string;
31183
+ activityId: string;
31184
+ quiesced: boolean;
31185
+ };
31186
+
31187
+ export async function getSessionAttemptActivityRef(
31188
+ db: Database,
31189
+ input: {
31190
+ accountId: string;
31191
+ workspaceId: string;
31192
+ sessionId: string;
31193
+ attemptId: string;
31194
+ temporalWorkflowId: string;
31195
+ },
31196
+ ): Promise<SessionAttemptActivityRef | null> {
31197
+ return await withRlsContext(
31198
+ db,
31199
+ { accountId: input.accountId, workspaceId: input.workspaceId },
31200
+ async (scopedDb) => {
31201
+ const [attempt] = await scopedDb
31202
+ .select({
31203
+ workflowId: schema.sessionTurnAttempts.temporalWorkflowId,
31204
+ workflowRunId: schema.sessionTurnAttempts.temporalWorkflowRunId,
31205
+ activityId: schema.sessionTurnAttempts.temporalActivityId,
31206
+ quiescedAt: schema.sessionTurnAttempts.quiescedAt,
31207
+ })
31208
+ .from(schema.sessionTurnAttempts)
31209
+ .where(
31210
+ and(
31211
+ eq(schema.sessionTurnAttempts.accountId, input.accountId),
31212
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
31213
+ eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
31214
+ eq(schema.sessionTurnAttempts.id, input.attemptId),
31215
+ eq(schema.sessionTurnAttempts.temporalWorkflowId, input.temporalWorkflowId),
31216
+ ),
31217
+ )
31218
+ .limit(1);
31219
+ return attempt
31220
+ ? {
31221
+ workflowId: attempt.workflowId,
31222
+ workflowRunId: attempt.workflowRunId,
31223
+ activityId: attempt.activityId,
31224
+ quiesced: attempt.quiescedAt !== null,
31225
+ }
31226
+ : null;
31227
+ },
31228
+ );
31229
+ }
31230
+
31231
+ /**
31232
+ * Recover the quiescence receipt when the original activity disappeared after
31233
+ * its attempt was durably interrupted. The caller first proves through
31234
+ * Temporal that the exact activity is absent or its server-owned heartbeat
31235
+ * lease expired. The closed attempt then cannot admit another workspace writer,
31236
+ * and every writer it did admit (including retained-process child writes) must
31237
+ * carry a physical settlement before the ordinary receipt transaction is
31238
+ * allowed to run.
31239
+ */
31240
+ export async function reconcileSessionAttemptQuiescence(
31241
+ db: Database,
31242
+ input: {
31243
+ accountId: string;
31244
+ workspaceId: string;
31245
+ sessionId: string;
31246
+ attemptId: string;
31247
+ temporalWorkflowId: string;
31248
+ temporalWorkflowRunId: string;
31249
+ temporalActivityId: string;
31250
+ activitySettled: boolean;
31251
+ },
31252
+ ): Promise<ReconcileSessionAttemptQuiescenceResult> {
31253
+ const eligibility = await withRlsContext(
31254
+ db,
31255
+ { accountId: input.accountId, workspaceId: input.workspaceId },
31256
+ async (scopedDb) => {
31257
+ const rows = await scopedDb.execute<{
31258
+ account_id: string;
31259
+ state: string;
31260
+ quiesced_at: Date | string | null;
31261
+ temporal_workflow_id: string;
31262
+ temporal_workflow_run_id: string;
31263
+ temporal_activity_id: string;
31264
+ interruption_settled: boolean;
31265
+ interruption_pending: boolean;
31266
+ writer_pending: boolean;
31267
+ }>(sql`
31268
+ select
31269
+ attempt.account_id,
31270
+ attempt.state,
31271
+ attempt.quiesced_at,
31272
+ attempt.temporal_workflow_id,
31273
+ attempt.temporal_workflow_run_id,
31274
+ attempt.temporal_activity_id,
31275
+ exists (
31276
+ select 1
31277
+ from session_attempt_interruptions interruption
31278
+ where interruption.workspace_id = attempt.workspace_id
31279
+ and interruption.session_id = attempt.session_id
31280
+ and interruption.attempt_id = attempt.id
31281
+ and interruption.state in ('settled', 'rejected_stale')
31282
+ ) as interruption_settled,
31283
+ exists (
31284
+ select 1
31285
+ from session_attempt_interruptions interruption
31286
+ where interruption.workspace_id = attempt.workspace_id
31287
+ and interruption.session_id = attempt.session_id
31288
+ and interruption.attempt_id = attempt.id
31289
+ and interruption.state in ('pending', 'delivered', 'acknowledged')
31290
+ ) as interruption_pending,
31291
+ (
31292
+ exists (
31293
+ select 1
31294
+ from sandbox_workspace_mutation_admissions admission
31295
+ where admission.account_id = attempt.account_id
31296
+ and admission.workspace_id = attempt.workspace_id
31297
+ and admission.session_id = attempt.session_id
31298
+ and admission.settled_at is null
31299
+ and (
31300
+ admission.attempt_id = attempt.id
31301
+ or (
31302
+ admission.actor_kind = 'process'
31303
+ and exists (
31304
+ select 1
31305
+ from sandbox_retained_processes process
31306
+ where process.account_id = attempt.account_id
31307
+ and process.workspace_id = attempt.workspace_id
31308
+ and process.session_id = attempt.session_id
31309
+ and process.id = admission.actor_id
31310
+ and process.owner_attempt_id = attempt.id
31311
+ )
31312
+ )
31313
+ )
31314
+ )
31315
+ or exists (
31316
+ select 1
31317
+ from sandbox_retained_processes process
31318
+ where process.account_id = attempt.account_id
31319
+ and process.workspace_id = attempt.workspace_id
31320
+ and process.session_id = attempt.session_id
31321
+ and process.owner_attempt_id = attempt.id
31322
+ and process.state = 'active'
31323
+ )
31324
+ ) as writer_pending
31325
+ from session_turn_attempts attempt
31326
+ where attempt.account_id = ${input.accountId}
31327
+ and attempt.workspace_id = ${input.workspaceId}
31328
+ and attempt.session_id = ${input.sessionId}
31329
+ and attempt.id = ${input.attemptId}
31330
+ limit 1
31331
+ `);
31332
+ return rows[0] ?? null;
31333
+ },
31334
+ );
31335
+ if (
31336
+ !eligibility ||
31337
+ eligibility.account_id !== input.accountId ||
31338
+ eligibility.temporal_workflow_id !== input.temporalWorkflowId ||
31339
+ eligibility.temporal_workflow_run_id !== input.temporalWorkflowRunId ||
31340
+ eligibility.temporal_activity_id !== input.temporalActivityId ||
31341
+ eligibility.state !== "closed" ||
31342
+ !eligibility.interruption_settled ||
31343
+ eligibility.interruption_pending
31344
+ ) {
31345
+ return { action: "stale", events: [] };
31346
+ }
31347
+ if (eligibility.quiesced_at) {
31348
+ return { action: "quiesced", events: [] };
31349
+ }
31350
+ if (!input.activitySettled || eligibility.writer_pending) {
31351
+ return { action: "pending", events: [] };
31352
+ }
31353
+ const events = await markSessionAttemptQuiesced(db, {
31354
+ accountId: input.accountId,
31355
+ workspaceId: input.workspaceId,
31356
+ sessionId: input.sessionId,
31357
+ attemptId: input.attemptId,
31358
+ temporalWorkflowId: input.temporalWorkflowId,
31359
+ temporalWorkflowRunId: input.temporalWorkflowRunId,
31360
+ temporalActivityId: input.temporalActivityId,
31361
+ });
31362
+ return { action: "quiesced", events };
31363
+ }
31364
+
31001
31365
  /**
31002
31366
  * Settle every durable interruption cause for one exact first-class attempt.
31003
31367
  * Steer wins the logical-turn fate when causes coexist; effective control after
@@ -31168,13 +31532,6 @@ export async function settleSessionAttemptInterruptions(
31168
31532
  outcome,
31169
31533
  closedAt: now,
31170
31534
  });
31171
- await requeueInterruptedSessionSystemUpdatesForTurnTx(
31172
- tx as unknown as Database,
31173
- workspaceId,
31174
- sessionId,
31175
- turn.id,
31176
- );
31177
-
31178
31535
  const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = steer
31179
31536
  ? [
31180
31537
  {
@@ -32459,10 +32816,45 @@ export async function applySessionTurnSettlement(
32459
32816
  },
32460
32817
  }),
32461
32818
  );
32819
+ const settledMachineInputs = ["completed", "failed", "cancelled", "superseded"].includes(
32820
+ input.turnStatus,
32821
+ )
32822
+ ? await tx
32823
+ .select({
32824
+ id: schema.sessionSystemUpdates.id,
32825
+ historyItemId: schema.sessionSystemUpdates.deliveredHistoryItemId,
32826
+ })
32827
+ .from(schema.sessionSystemUpdates)
32828
+ .where(
32829
+ and(
32830
+ eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
32831
+ eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
32832
+ eq(schema.sessionSystemUpdates.deliveredTurnId, input.turnId),
32833
+ eq(schema.sessionSystemUpdates.state, "delivered"),
32834
+ ),
32835
+ )
32836
+ .orderBy(
32837
+ asc(schema.sessionSystemUpdates.createdAt),
32838
+ asc(schema.sessionSystemUpdates.id),
32839
+ )
32840
+ : [];
32841
+ const machineInputSettlementEvent: AppendEventInput | null =
32842
+ settledMachineInputs.length > 0
32843
+ ? {
32844
+ type: "system.update.settled",
32845
+ payload: {
32846
+ updateIds: settledMachineInputs.map((update) => update.id),
32847
+ count: settledMachineInputs.length,
32848
+ historyItemId: settledMachineInputs[0]!.historyItemId,
32849
+ outcome: input.turnStatus,
32850
+ },
32851
+ }
32852
+ : null;
32462
32853
  const settlementEvents = [
32463
32854
  ...(recordingEvent ? [recordingEvent] : []),
32464
32855
  ...(compactionRequestEvent ? [compactionRequestEvent] : []),
32465
32856
  ...terminalHumanInputEvents,
32857
+ ...(machineInputSettlementEvent ? [machineInputSettlementEvent] : []),
32466
32858
  ...input.events,
32467
32859
  ];
32468
32860
  const values = settlementEvents.map((event) => {
@@ -32545,21 +32937,6 @@ export async function applySessionTurnSettlement(
32545
32937
  turn,
32546
32938
  );
32547
32939
  }
32548
- if (input.turnStatus === "failed") {
32549
- await deferFailedSessionSystemUpdatesForTurnTx(
32550
- tx as unknown as Database,
32551
- workspaceId,
32552
- input.sessionId,
32553
- input.turnId,
32554
- );
32555
- } else if (["cancelled", "superseded"].includes(input.turnStatus)) {
32556
- await requeueInterruptedSessionSystemUpdatesForTurnTx(
32557
- tx as unknown as Database,
32558
- workspaceId,
32559
- input.sessionId,
32560
- input.turnId,
32561
- );
32562
- }
32563
32940
  await tx
32564
32941
  .update(schema.sessions)
32565
32942
  .set({
@@ -32843,14 +33220,6 @@ export async function settleCodexCredentialLeaseLoss(
32843
33220
  eq(schema.sessions.activeTurnId, input.turnId),
32844
33221
  ),
32845
33222
  );
32846
- if (!input.checkpointDurable) {
32847
- await deferFailedSessionSystemUpdatesForTurnTx(
32848
- tx as unknown as Database,
32849
- input.workspaceId,
32850
- input.sessionId,
32851
- input.turnId,
32852
- );
32853
- }
32854
33223
  await tx.execute(sql`
32855
33224
  delete from codex_credential_leases
32856
33225
  where account_id = ${input.accountId}
@@ -33794,11 +34163,34 @@ export async function getSessionQueueSnapshot(
33794
34163
  ),
33795
34164
  )
33796
34165
  .orderBy(asc(schema.sessionTurns.position), asc(schema.sessionTurns.createdAt));
34166
+ const pendingInputs = await scopedDb
34167
+ .select()
34168
+ .from(schema.sessionSystemUpdates)
34169
+ .where(
34170
+ and(
34171
+ eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
34172
+ eq(schema.sessionSystemUpdates.sessionId, sessionId),
34173
+ eq(schema.sessionSystemUpdates.state, "pending"),
34174
+ ),
34175
+ )
34176
+ .orderBy(
34177
+ sql`case when ${schema.sessionSystemUpdates.kind} = 'agent_steer_instruction' then 0 else 1 end`,
34178
+ asc(schema.sessionSystemUpdates.createdAt),
34179
+ asc(schema.sessionSystemUpdates.id),
34180
+ );
33797
34181
  const latestInterruption = await latestSessionAttemptInterruption(
33798
34182
  scopedDb,
33799
34183
  workspaceId,
33800
34184
  sessionId,
33801
34185
  );
34186
+ const items = rows.map(mapSessionTurn);
34187
+ const nextInputBatch = selectBoundedSystemUpdateBatch(pendingInputs);
34188
+ const hasPendingAgentSteer = pendingInputs.some(
34189
+ (update) => update.kind === "agent_steer_instruction",
34190
+ );
34191
+ const attachmentTurn = hasPendingAgentSteer
34192
+ ? items.find((turn) => turn.metadata.delivery === "steer")
34193
+ : items[0];
33802
34194
  return {
33803
34195
  version: session.queueVersion,
33804
34196
  effectiveControl: serializeEffectiveSessionControl(effectiveControl),
@@ -33806,7 +34198,26 @@ export async function getSessionQueueSnapshot(
33806
34198
  latestInterruption !== null &&
33807
34199
  latestInterruption.interruptionState !== "rejected_stale" &&
33808
34200
  latestInterruption.quiescedAt === null,
33809
- items: rows.map(mapSessionTurn),
34201
+ items,
34202
+ pendingInputs: pendingInputs.map((update) => {
34203
+ const canonical = mapSessionSystemUpdate(update);
34204
+ return {
34205
+ id: canonical.id,
34206
+ sessionId: canonical.sessionId,
34207
+ kind: canonical.kind,
34208
+ classification: canonical.classification,
34209
+ sourceId: boundedInternalUpdateEventText(canonical.sourceId, 256).text,
34210
+ summary: boundedInternalUpdateEventText(canonical.summary, 512).text,
34211
+ createdAt: canonical.createdAt,
34212
+ };
34213
+ }),
34214
+ pendingInputAttachment:
34215
+ attachmentTurn && nextInputBatch.length > 0
34216
+ ? {
34217
+ turnId: attachmentTurn.id,
34218
+ inputIds: nextInputBatch.map((update) => update.id),
34219
+ }
34220
+ : null,
33810
34221
  };
33811
34222
  });
33812
34223
  }
@@ -34155,12 +34566,13 @@ export async function claimPendingSessionWorkflowWakes(
34155
34566
 
34156
34567
  /**
34157
34568
  * Acknowledge an immediate post-commit signal only after it cannot strand an
34158
- * accepted Agent Steer. Temporal accepting a signal is transport evidence, not
34159
- * proof that a closing workflow observed Postgres or admitted its pending
34160
- * direction. While active control still has an actionable Agent Steer, retain
34161
- * the revision so the bounded outbox dispatcher retries signalWithStart. The
34162
- * attempt-fenced claim consumes the update once; a real Pause is the typed
34163
- * blocker and may acknowledge this revision because Resume commits a new one.
34569
+ * accepted direction or an interrupted attempt awaiting writer-set proof.
34570
+ * Temporal accepting a signal is transport evidence, not proof that a closing
34571
+ * workflow observed Postgres. While active control still has an actionable
34572
+ * Agent Steer or pending quiescence, retain the revision so the bounded outbox
34573
+ * dispatcher retries signalWithStart. The attempt-fenced claim consumes an
34574
+ * Agent Steer once; a real Pause is the typed blocker and may acknowledge this
34575
+ * revision because Resume commits a new one.
34164
34576
  *
34165
34577
  * An older sender may advance only its own revision; it cannot clear a claim or
34166
34578
  * failure state belonging to a newer revision. The control -> workspace ->
@@ -34169,7 +34581,10 @@ export async function claimPendingSessionWorkflowWakes(
34169
34581
  */
34170
34582
  export type SessionWorkflowWakeDeliveryResult =
34171
34583
  | { action: "acknowledged" }
34172
- | { action: "pending_admission"; blocker: "pending_agent_steer" };
34584
+ | {
34585
+ action: "pending_admission";
34586
+ blocker: "pending_agent_steer" | "pending_quiescence";
34587
+ };
34173
34588
 
34174
34589
  export async function markSessionWorkflowWakeDelivered(
34175
34590
  db: Database,
@@ -34209,6 +34624,35 @@ export async function markSessionWorkflowWakeDelivered(
34209
34624
  if (pendingAgentSteer) {
34210
34625
  return { action: "pending_admission", blocker: "pending_agent_steer" } as const;
34211
34626
  }
34627
+ const [pendingQuiescence] = await tx
34628
+ .select({ id: schema.sessionTurnAttempts.id })
34629
+ .from(schema.sessionTurnAttempts)
34630
+ .innerJoin(
34631
+ schema.sessionAttemptInterruptions,
34632
+ and(
34633
+ eq(
34634
+ schema.sessionAttemptInterruptions.workspaceId,
34635
+ schema.sessionTurnAttempts.workspaceId,
34636
+ ),
34637
+ eq(
34638
+ schema.sessionAttemptInterruptions.sessionId,
34639
+ schema.sessionTurnAttempts.sessionId,
34640
+ ),
34641
+ eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
34642
+ ),
34643
+ )
34644
+ .where(
34645
+ and(
34646
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
34647
+ eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
34648
+ isNull(schema.sessionTurnAttempts.quiescedAt),
34649
+ inArray(schema.sessionAttemptInterruptions.state, ["settled", "rejected_stale"]),
34650
+ ),
34651
+ )
34652
+ .limit(1);
34653
+ if (pendingQuiescence) {
34654
+ return { action: "pending_admission", blocker: "pending_quiescence" } as const;
34655
+ }
34212
34656
  }
34213
34657
  const [row] = await tx
34214
34658
  .update(schema.sessionWorkflowWakeOutbox)
@@ -34443,55 +34887,6 @@ export async function addSessionSystemUpdate(
34443
34887
  return await addSessionSystemUpdateWithSourceMutation(db, input, async () => undefined);
34444
34888
  }
34445
34889
 
34446
- async function requeueInterruptedSessionSystemUpdatesForTurnTx(
34447
- tx: Database,
34448
- workspaceId: string,
34449
- sessionId: string,
34450
- turnId: string,
34451
- ): Promise<void> {
34452
- await tx
34453
- .update(schema.sessionSystemUpdates)
34454
- .set({ state: "pending", deliveredTurnId: null, deliveredAt: null })
34455
- .where(
34456
- and(
34457
- eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
34458
- eq(schema.sessionSystemUpdates.sessionId, sessionId),
34459
- eq(schema.sessionSystemUpdates.deliveredTurnId, turnId),
34460
- eq(schema.sessionSystemUpdates.state, "delivered"),
34461
- ),
34462
- );
34463
- }
34464
-
34465
- /**
34466
- * A failed internal-only inference must not manufacture another inference by
34467
- * making its inputs immediately runnable again. Preserve ordinary internal
34468
- * updates as deferred input for the next real prompt/new update. Goal
34469
- * continuation notices are derivable from the durable goal and become terminal
34470
- * so the goal evaluator can pause or synthesize the next valid continuation.
34471
- */
34472
- async function deferFailedSessionSystemUpdatesForTurnTx(
34473
- tx: Database,
34474
- workspaceId: string,
34475
- sessionId: string,
34476
- turnId: string,
34477
- ): Promise<void> {
34478
- await tx
34479
- .update(schema.sessionSystemUpdates)
34480
- .set({
34481
- state: sql`case when ${schema.sessionSystemUpdates.payload} ->> 'type' = 'goal_continuation' then 'failed' else 'deferred' end`,
34482
- deliveredTurnId: null,
34483
- deliveredAt: null,
34484
- })
34485
- .where(
34486
- and(
34487
- eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
34488
- eq(schema.sessionSystemUpdates.sessionId, sessionId),
34489
- eq(schema.sessionSystemUpdates.deliveredTurnId, turnId),
34490
- eq(schema.sessionSystemUpdates.state, "delivered"),
34491
- ),
34492
- );
34493
- }
34494
-
34495
34890
  /**
34496
34891
  * Persist one internal update without fabricating a user prompt or queue row.
34497
34892
  * Dedupe and any producer/outbox mutation commit in the same transaction.
@@ -34593,6 +34988,7 @@ export async function addSessionSystemUpdateWithSourceMutation(
34593
34988
  }
34594
34989
 
34595
34990
  const now = new Date();
34991
+ const eventPreview = internalUpdateEventMember(inserted);
34596
34992
  const [event] = await tx
34597
34993
  .insert(schema.sessionEvents)
34598
34994
  .values({
@@ -34602,11 +34998,13 @@ export async function addSessionSystemUpdateWithSourceMutation(
34602
34998
  sequence: session.lastSequence + 1,
34603
34999
  type: "system.update.pending",
34604
35000
  payload: sanitizeEventPayload({
34605
- updateId: inserted.id,
34606
- kind: input.kind,
34607
- classification: input.classification,
34608
- sourceId: input.sourceId,
34609
- summary: input.summary,
35001
+ updateId: eventPreview.id,
35002
+ kind: eventPreview.kind,
35003
+ classification: eventPreview.classification,
35004
+ sourceId: eventPreview.sourceId,
35005
+ sourceIdTruncated: eventPreview.sourceIdTruncated,
35006
+ summary: eventPreview.summary,
35007
+ summaryTruncated: eventPreview.summaryTruncated,
34610
35008
  }),
34611
35009
  occurredAt: now,
34612
35010
  })
@@ -34657,7 +35055,7 @@ export async function listOutstandingSessionSystemUpdates(
34657
35055
  and(
34658
35056
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
34659
35057
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
34660
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
35058
+ eq(schema.sessionSystemUpdates.state, "pending"),
34661
35059
  ),
34662
35060
  )
34663
35061
  .orderBy(asc(schema.sessionSystemUpdates.createdAt), asc(schema.sessionSystemUpdates.id));
@@ -34713,6 +35111,7 @@ function mapSessionSystemUpdate(
34713
35111
  lineage: row.lineage,
34714
35112
  state: row.state as SessionSystemUpdateState,
34715
35113
  deliveredTurnId: row.deliveredTurnId,
35114
+ deliveredHistoryItemId: row.deliveredHistoryItemId,
34716
35115
  deliveredAt: row.deliveredAt?.toISOString() ?? null,
34717
35116
  createdAt: row.createdAt.toISOString(),
34718
35117
  };
@@ -35590,6 +35989,7 @@ function mapSession(
35590
35989
  rigId: row.rigId ?? null,
35591
35990
  rigVersionId: row.rigVersionId ?? null,
35592
35991
  firstPartyMcpPermissions: (row.firstPartyMcpPermissions as Permission[] | null) ?? null,
35992
+ firstPartyMcpTools: (row.firstPartyMcpTools as FirstPartyMcpToolName[] | null) ?? null,
35593
35993
  mcpServers,
35594
35994
  parentSessionId: row.parentSessionId ?? null,
35595
35995
  rootSessionId: row.rootSessionId,