@opengeni/db 0.13.4 → 0.14.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.
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,
@@ -94,7 +95,10 @@ import type {
94
95
  RigChangeStatus,
95
96
  RigCheck,
96
97
  } from "@opengeni/contracts";
97
- import { SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS } from "@opengeni/contracts";
98
+ import {
99
+ DEFAULT_FIRST_PARTY_MCP_TOOLS,
100
+ SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS,
101
+ } from "@opengeni/contracts";
98
102
  import {
99
103
  approvalIdentifier,
100
104
  boundWorkspaceControlEvent,
@@ -113,6 +117,7 @@ import {
113
117
  RigChange as RigChangeContract,
114
118
  SessionGoal as SessionGoalContract,
115
119
  SessionSystemUpdatePayload,
120
+ sessionSystemUpdateBatchHistoryItem,
116
121
  HostEventExport as HostEventExportContract,
117
122
  HostEventExportBatch as HostEventExportBatchContract,
118
123
  HostExportConsumerId,
@@ -13877,7 +13882,7 @@ export type SessionCreateInput = {
13877
13882
  resources: ResourceRef[];
13878
13883
  skills?: SessionSkill[];
13879
13884
  tools?: ToolRef[];
13880
- toolPolicy?: SessionToolPolicy | null;
13885
+ toolPolicy?: SessionToolPolicy;
13881
13886
  metadata: Record<string, unknown>;
13882
13887
  createdBy?: TurnInitiator;
13883
13888
  createdByContext?: TurnInitiatorContext;
@@ -13888,6 +13893,7 @@ export type SessionCreateInput = {
13888
13893
  rigId?: string | null;
13889
13894
  rigVersionId?: string | null;
13890
13895
  firstPartyMcpPermissions?: Permission[] | null;
13896
+ firstPartyMcpTools?: FirstPartyMcpToolName[];
13891
13897
  instructions?: string | null;
13892
13898
  parentSessionId?: string | null;
13893
13899
  createIdempotencyKey?: string | null;
@@ -14277,7 +14283,10 @@ async function createSessionInTransaction(
14277
14283
  resources: input.resources,
14278
14284
  skills: input.skills ?? [],
14279
14285
  tools: input.tools ?? [],
14280
- toolPolicy: input.toolPolicy ?? null,
14286
+ toolPolicy: input.toolPolicy ?? {
14287
+ mode: "explicit",
14288
+ inheritedFromSessionId: input.parentSessionId ?? null,
14289
+ },
14281
14290
  metadata: input.metadata,
14282
14291
  ...creatorColumns(frozenCreator),
14283
14292
  model: input.model,
@@ -14288,6 +14297,7 @@ async function createSessionInTransaction(
14288
14297
  rigId: input.rigId ?? null,
14289
14298
  rigVersionId: input.rigVersionId ?? null,
14290
14299
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
14300
+ firstPartyMcpTools: input.firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
14291
14301
  instructions: input.instructions ?? null,
14292
14302
  parentSessionId: input.parentSessionId ?? null,
14293
14303
  createIdempotencyKey,
@@ -25343,6 +25353,89 @@ export async function latestWorkspaceCapture(
25343
25353
  });
25344
25354
  }
25345
25355
 
25356
+ export type SessionWorkspaceCaptureLookup = {
25357
+ sessionExists: boolean;
25358
+ capture: WorkspaceCaptureRow | null;
25359
+ };
25360
+
25361
+ /**
25362
+ * Resolve session existence and its newest capture in one RLS-scoped query.
25363
+ *
25364
+ * The capture metadata endpoint only needs existence for its 404 contract. Using
25365
+ * `getSession` there mapped the complete session, MCP metadata, and control
25366
+ * projection before issuing a second transaction for the capture. A lateral
25367
+ * lookup preserves the exact absent-session / absent-capture distinction without
25368
+ * loading unrelated session state or adding another database round trip.
25369
+ */
25370
+ export async function sessionLatestWorkspaceCapture(
25371
+ db: Database,
25372
+ workspaceId: string,
25373
+ sessionId: string,
25374
+ ): Promise<SessionWorkspaceCaptureLookup> {
25375
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
25376
+ const rows = await scopedDb.execute<{
25377
+ found_session_id: string;
25378
+ capture_id: string | null;
25379
+ capture_session_id: string | null;
25380
+ capture_turn_id: string | null;
25381
+ capture_revision: number | string | null;
25382
+ capture_lease_epoch: number | string | null;
25383
+ capture_state: string | null;
25384
+ capture_manifest_key: string | null;
25385
+ capture_tree_index_key: string | null;
25386
+ capture_blob_keys: unknown;
25387
+ capture_size_bytes: number | string | null;
25388
+ capture_stats: unknown;
25389
+ capture_captured_at: string | Date | null;
25390
+ }>(sql`
25391
+ select
25392
+ sessions.id as found_session_id,
25393
+ capture.id as capture_id,
25394
+ capture.session_id as capture_session_id,
25395
+ capture.turn_id as capture_turn_id,
25396
+ capture.revision as capture_revision,
25397
+ capture.lease_epoch as capture_lease_epoch,
25398
+ capture.state as capture_state,
25399
+ capture.manifest_key as capture_manifest_key,
25400
+ capture.tree_index_key as capture_tree_index_key,
25401
+ capture.blob_keys as capture_blob_keys,
25402
+ capture.size_bytes as capture_size_bytes,
25403
+ capture.stats as capture_stats,
25404
+ capture.captured_at as capture_captured_at
25405
+ from sessions
25406
+ left join lateral (
25407
+ select ${WORKSPACE_CAPTURE_COLUMNS}
25408
+ from workspace_captures
25409
+ where workspace_captures.session_id = sessions.id
25410
+ order by workspace_captures.revision desc
25411
+ limit 1
25412
+ ) capture on true
25413
+ where sessions.workspace_id = ${workspaceId} and sessions.id = ${sessionId}
25414
+ limit 1
25415
+ `);
25416
+ const row = rows[0];
25417
+ if (!row) return { sessionExists: false, capture: null };
25418
+ if (!row.capture_id) return { sessionExists: true, capture: null };
25419
+ return {
25420
+ sessionExists: true,
25421
+ capture: mapWorkspaceCaptureRow({
25422
+ id: row.capture_id,
25423
+ session_id: row.capture_session_id!,
25424
+ turn_id: row.capture_turn_id,
25425
+ revision: row.capture_revision!,
25426
+ lease_epoch: row.capture_lease_epoch!,
25427
+ state: row.capture_state!,
25428
+ manifest_key: row.capture_manifest_key,
25429
+ tree_index_key: row.capture_tree_index_key,
25430
+ blob_keys: row.capture_blob_keys,
25431
+ size_bytes: row.capture_size_bytes,
25432
+ stats: row.capture_stats,
25433
+ captured_at: row.capture_captured_at!,
25434
+ }),
25435
+ };
25436
+ });
25437
+ }
25438
+
25346
25439
  /** A specific capture revision for a session (the M2 file route with an explicit
25347
25440
  * `?revision=`), or null if that revision was never captured / already GC'd. */
25348
25441
  export async function workspaceCaptureAtRevision(
@@ -28985,7 +29078,7 @@ export async function materializeGoalContinuation(
28985
29078
  eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
28986
29079
  eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
28987
29080
  eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
28988
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
29081
+ eq(schema.sessionSystemUpdates.state, "pending"),
28989
29082
  ),
28990
29083
  )
28991
29084
  .limit(1);
@@ -29148,6 +29241,7 @@ export async function materializeGoalContinuation(
29148
29241
  })
29149
29242
  .onConflictDoNothing({ target: schema.usageEvents.idempotencyKey });
29150
29243
 
29244
+ const eventPreview = internalUpdateEventMember(update);
29151
29245
  const insertedEvents = await tx
29152
29246
  .insert(schema.sessionEvents)
29153
29247
  .values([
@@ -29158,11 +29252,13 @@ export async function materializeGoalContinuation(
29158
29252
  sequence: session.lastSequence + 1,
29159
29253
  type: "system.update.pending",
29160
29254
  payload: sanitizeEventPayload({
29161
- updateId: update.id,
29162
- kind: update.kind,
29163
- classification: update.classification,
29164
- sourceId: update.sourceId,
29165
- summary: update.summary,
29255
+ updateId: eventPreview.id,
29256
+ kind: eventPreview.kind,
29257
+ classification: eventPreview.classification,
29258
+ sourceId: eventPreview.sourceId,
29259
+ sourceIdTruncated: eventPreview.sourceIdTruncated,
29260
+ summary: eventPreview.summary,
29261
+ summaryTruncated: eventPreview.summaryTruncated,
29166
29262
  }),
29167
29263
  occurredAt: now,
29168
29264
  },
@@ -29663,6 +29759,79 @@ export type SessionWorkTrigger = { kind: "next" } | { kind: "approval"; triggerE
29663
29759
  export const MAX_INTERNAL_UPDATE_BYTES = 64 * 1024;
29664
29760
  export const MAX_INTERNAL_UPDATE_BATCH_MEMBERS = 100;
29665
29761
  export const MAX_INTERNAL_UPDATE_BATCH_BYTES = 256 * 1024;
29762
+ const MAX_INTERNAL_UPDATE_EVENT_SUMMARY_BYTES = 512;
29763
+ const MAX_INTERNAL_UPDATE_EVENT_SOURCE_BYTES = 256;
29764
+
29765
+ type BoundedSystemUpdate = Pick<
29766
+ typeof schema.sessionSystemUpdates.$inferSelect,
29767
+ "id" | "kind" | "classification" | "sourceId" | "summary" | "payload" | "lineage"
29768
+ >;
29769
+
29770
+ function boundedInternalUpdateEventText(
29771
+ value: string,
29772
+ maxBytes: number,
29773
+ ): {
29774
+ text: string;
29775
+ truncated: boolean;
29776
+ } {
29777
+ if (Buffer.byteLength(value) <= maxBytes) return { text: value, truncated: false };
29778
+ const suffix = "…";
29779
+ const bodyBudget = maxBytes - Buffer.byteLength(suffix);
29780
+ const bytes = Buffer.from(value);
29781
+ let text = bytes.subarray(0, bodyBudget).toString("utf8");
29782
+ if (text.endsWith("\uFFFD")) text = text.slice(0, -1);
29783
+ return { text: `${text}${suffix}`, truncated: true };
29784
+ }
29785
+
29786
+ function internalUpdateEventMember(update: BoundedSystemUpdate) {
29787
+ const summary = boundedInternalUpdateEventText(
29788
+ update.summary,
29789
+ MAX_INTERNAL_UPDATE_EVENT_SUMMARY_BYTES,
29790
+ );
29791
+ const source = boundedInternalUpdateEventText(
29792
+ update.sourceId,
29793
+ MAX_INTERNAL_UPDATE_EVENT_SOURCE_BYTES,
29794
+ );
29795
+ return {
29796
+ id: update.id,
29797
+ kind: update.kind,
29798
+ classification: update.classification,
29799
+ sourceId: source.text,
29800
+ sourceIdTruncated: source.truncated,
29801
+ summary: summary.text,
29802
+ summaryTruncated: summary.truncated,
29803
+ };
29804
+ }
29805
+
29806
+ function selectBoundedSystemUpdateBatch<T extends BoundedSystemUpdate>(updates: readonly T[]): T[] {
29807
+ const selected: T[] = [];
29808
+ let selectedBytes = 0;
29809
+ for (const update of updates) {
29810
+ const updateBytes = Buffer.byteLength(
29811
+ JSON.stringify({
29812
+ id: update.id,
29813
+ kind: update.kind,
29814
+ classification: update.classification,
29815
+ sourceId: update.sourceId,
29816
+ summary: update.summary,
29817
+ payload: update.payload,
29818
+ lineage: update.lineage,
29819
+ }),
29820
+ );
29821
+ if (
29822
+ selected.length >= MAX_INTERNAL_UPDATE_BATCH_MEMBERS ||
29823
+ // One individually large canonical input must still make progress. The
29824
+ // model/context boundary may reject it explicitly, but the queue cannot
29825
+ // wedge forever merely because the coalescing target is smaller.
29826
+ (selected.length > 0 && selectedBytes + updateBytes > MAX_INTERNAL_UPDATE_BATCH_BYTES)
29827
+ ) {
29828
+ break;
29829
+ }
29830
+ selected.push(update);
29831
+ selectedBytes += updateBytes;
29832
+ }
29833
+ return selected;
29834
+ }
29666
29835
 
29667
29836
  export type ClaimSessionWorkForAttemptInput = {
29668
29837
  sessionId: string;
@@ -29708,7 +29877,10 @@ export async function claimSessionWorkForAttempt(
29708
29877
  count: number;
29709
29878
  lastSequence: number;
29710
29879
  triggerEventId: string | null;
29880
+ historyItemId: string | null;
29881
+ historyItem: Record<string, unknown> | null;
29711
29882
  updates: Array<typeof schema.sessionSystemUpdates.$inferSelect>;
29883
+ events: Array<typeof schema.sessionEvents.$inferInsert>;
29712
29884
  event: typeof schema.sessionEvents.$inferInsert | null;
29713
29885
  }> => {
29714
29886
  const [agentSteer] = await tx
@@ -29718,7 +29890,7 @@ export async function claimSessionWorkForAttempt(
29718
29890
  and(
29719
29891
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
29720
29892
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
29721
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
29893
+ eq(schema.sessionSystemUpdates.state, "pending"),
29722
29894
  eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
29723
29895
  ),
29724
29896
  )
@@ -29728,20 +29900,6 @@ export async function claimSessionWorkForAttempt(
29728
29900
  )
29729
29901
  .limit(1)
29730
29902
  .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
29903
  const ordinary = await tx
29746
29904
  .select()
29747
29905
  .from(schema.sessionSystemUpdates)
@@ -29749,7 +29907,7 @@ export async function claimSessionWorkForAttempt(
29749
29907
  and(
29750
29908
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
29751
29909
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
29752
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
29910
+ eq(schema.sessionSystemUpdates.state, "pending"),
29753
29911
  ne(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
29754
29912
  ),
29755
29913
  )
@@ -29765,12 +29923,15 @@ export async function claimSessionWorkForAttempt(
29765
29923
  count: 0,
29766
29924
  lastSequence: nextSequence - 1,
29767
29925
  triggerEventId: null,
29926
+ historyItemId: null,
29927
+ historyItem: null,
29768
29928
  updates: [],
29929
+ events: [],
29769
29930
  event: null,
29770
29931
  };
29771
29932
  }
29772
- const deliverable: typeof updates = [];
29773
- let deliveredBytes = 0;
29933
+ const validUpdates: typeof updates = [];
29934
+ const cancelledUpdateIds: string[] = [];
29774
29935
  for (const update of updates) {
29775
29936
  const payload = update.payload;
29776
29937
  if (payload.type === "goal_continuation") {
@@ -29799,43 +29960,66 @@ export async function claimSessionWorkForAttempt(
29799
29960
  .update(schema.sessionSystemUpdates)
29800
29961
  .set({ state: "cancelled" })
29801
29962
  .where(eq(schema.sessionSystemUpdates.id, update.id));
29963
+ cancelledUpdateIds.push(update.id);
29802
29964
  continue;
29803
29965
  }
29804
29966
  }
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;
29967
+ validUpdates.push(update);
29824
29968
  }
29969
+ const deliverable = selectBoundedSystemUpdateBatch(validUpdates);
29825
29970
  if (deliverable.length === 0) {
29971
+ const cancellationEvent =
29972
+ cancelledUpdateIds.length > 0
29973
+ ? {
29974
+ accountId,
29975
+ workspaceId,
29976
+ sessionId,
29977
+ // No receiving turn exists when every candidate was
29978
+ // cancelled before a model batch could be persisted.
29979
+ turnId: null,
29980
+ turnGeneration: null,
29981
+ turnAttemptId: null,
29982
+ turnAssociation: null,
29983
+ sequence: nextSequence,
29984
+ type: "system.update.cancelled" as const,
29985
+ payload: sanitizeEventPayload({
29986
+ updateIds: cancelledUpdateIds,
29987
+ count: cancelledUpdateIds.length,
29988
+ reason: "stale_goal_continuation",
29989
+ }),
29990
+ occurredAt,
29991
+ }
29992
+ : null;
29826
29993
  return {
29827
29994
  count: 0,
29828
- lastSequence: nextSequence - 1,
29995
+ lastSequence: cancellationEvent ? nextSequence : nextSequence - 1,
29829
29996
  triggerEventId: null,
29997
+ historyItemId: null,
29998
+ historyItem: null,
29830
29999
  updates: [],
30000
+ events: cancellationEvent ? [cancellationEvent] : [],
29831
30001
  event: null,
29832
30002
  };
29833
30003
  }
30004
+ // Inclusion gives the newest Steer first refusal on the bounded
30005
+ // batch. Model ordering is deliberately the opposite: ordinary
30006
+ // updates establish context, then the authoritative replacement
30007
+ // direction is last so it cannot be overridden by an older goal or
30008
+ // lifecycle notice.
30009
+ const modelOrdered = [
30010
+ ...deliverable.filter((update) => update.kind !== "agent_steer_instruction"),
30011
+ ...deliverable.filter((update) => update.kind === "agent_steer_instruction"),
30012
+ ];
30013
+ const historyItemId = crypto.randomUUID();
30014
+ const historyItem = sessionSystemUpdateBatchHistoryItem(
30015
+ modelOrdered.map((update) => mapSessionSystemUpdate(update)),
30016
+ ) as Record<string, unknown>;
29834
30017
  await tx
29835
30018
  .update(schema.sessionSystemUpdates)
29836
30019
  .set({
29837
30020
  state: "delivered",
29838
30021
  deliveredTurnId: turnId,
30022
+ deliveredHistoryItemId: historyItemId,
29839
30023
  deliveredAt: occurredAt,
29840
30024
  })
29841
30025
  .where(
@@ -29849,6 +30033,27 @@ export async function claimSessionWorkForAttempt(
29849
30033
  ),
29850
30034
  );
29851
30035
  const eventId = triggerEventId ?? crypto.randomUUID();
30036
+ let sequence = nextSequence - 1;
30037
+ const events: Array<typeof schema.sessionEvents.$inferInsert> = [];
30038
+ if (cancelledUpdateIds.length > 0) {
30039
+ events.push({
30040
+ accountId,
30041
+ workspaceId,
30042
+ sessionId,
30043
+ turnId,
30044
+ turnGeneration,
30045
+ turnAttemptId: input.attemptId,
30046
+ turnAssociation: "current",
30047
+ sequence: ++sequence,
30048
+ type: "system.update.cancelled",
30049
+ payload: sanitizeEventPayload({
30050
+ updateIds: cancelledUpdateIds,
30051
+ count: cancelledUpdateIds.length,
30052
+ reason: "stale_goal_continuation",
30053
+ }),
30054
+ occurredAt,
30055
+ });
30056
+ }
29852
30057
  const event: typeof schema.sessionEvents.$inferInsert = {
29853
30058
  id: eventId,
29854
30059
  accountId,
@@ -29858,24 +30063,64 @@ export async function claimSessionWorkForAttempt(
29858
30063
  turnGeneration,
29859
30064
  turnAttemptId: input.attemptId,
29860
30065
  turnAssociation: "current",
29861
- sequence: nextSequence,
30066
+ sequence: ++sequence,
29862
30067
  type: "system.update.delivered",
29863
30068
  payload: sanitizeEventPayload({
29864
30069
  updateIds: deliverable.map((update) => update.id),
30070
+ historyItemId,
29865
30071
  count: deliverable.length,
29866
30072
  classifications: [...new Set(deliverable.map((update) => update.classification))],
30073
+ members: modelOrdered.map(internalUpdateEventMember),
29867
30074
  }),
29868
30075
  occurredAt,
29869
30076
  };
30077
+ events.push(event);
29870
30078
  return {
29871
30079
  count: deliverable.length,
29872
- lastSequence: nextSequence,
30080
+ lastSequence: sequence,
29873
30081
  triggerEventId: eventId,
30082
+ historyItemId,
30083
+ historyItem,
29874
30084
  updates: deliverable,
30085
+ events,
29875
30086
  event,
29876
30087
  };
29877
30088
  };
29878
30089
 
30090
+ const persistDeliveredUpdateBatch = async (
30091
+ delivered: Awaited<ReturnType<typeof deliverPendingUpdates>>,
30092
+ accountId: string,
30093
+ turnId: string,
30094
+ ): Promise<void> => {
30095
+ if (!delivered.historyItemId || !delivered.historyItem) {
30096
+ if (delivered.count !== 0) {
30097
+ throw new Error("Delivered machine-input batch has no model-memory item");
30098
+ }
30099
+ return;
30100
+ }
30101
+ const [{ position } = { position: 0 }] = await tx
30102
+ .select({
30103
+ position: sql<number>`coalesce(max(${schema.sessionHistoryItems.position}), -1) + 1`,
30104
+ })
30105
+ .from(schema.sessionHistoryItems)
30106
+ .where(
30107
+ and(
30108
+ eq(schema.sessionHistoryItems.workspaceId, workspaceId),
30109
+ eq(schema.sessionHistoryItems.sessionId, sessionId),
30110
+ ),
30111
+ );
30112
+ await tx.insert(schema.sessionHistoryItems).values({
30113
+ id: delivered.historyItemId,
30114
+ accountId,
30115
+ workspaceId,
30116
+ sessionId,
30117
+ turnId,
30118
+ position: Number(position),
30119
+ item: sanitizeModelPayload(delivered.historyItem),
30120
+ producerCodexCredentialId: null,
30121
+ });
30122
+ };
30123
+
29879
30124
  // Capacity settlement and resume use session -> turn after their
29880
30125
  // workspace rotation lock. Claiming must preserve that shared order:
29881
30126
  // taking a queued turn first can deadlock with a settlement that owns
@@ -30211,7 +30456,7 @@ export async function claimSessionWorkForAttempt(
30211
30456
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
30212
30457
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
30213
30458
  eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
30214
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
30459
+ eq(schema.sessionSystemUpdates.state, "pending"),
30215
30460
  ),
30216
30461
  )
30217
30462
  .orderBy(
@@ -30448,6 +30693,22 @@ export async function claimSessionWorkForAttempt(
30448
30693
  triggerEventId,
30449
30694
  );
30450
30695
  if (delivered.count === 0) {
30696
+ if (delivered.events.length > 0) {
30697
+ await tx.insert(schema.sessionEvents).values(delivered.events);
30698
+ await tx
30699
+ .update(schema.sessions)
30700
+ .set({
30701
+ status: "idle",
30702
+ lastSequence: delivered.lastSequence,
30703
+ updatedAt: now,
30704
+ })
30705
+ .where(
30706
+ and(
30707
+ eq(schema.sessions.workspaceId, workspaceId),
30708
+ eq(schema.sessions.id, sessionId),
30709
+ ),
30710
+ );
30711
+ }
30451
30712
  return { action: "unclaimed", reason: "no-work" };
30452
30713
  }
30453
30714
  const goalUpdate = delivered.updates.find(
@@ -30634,9 +30895,10 @@ export async function claimSessionWorkForAttempt(
30634
30895
  })
30635
30896
  .returning();
30636
30897
  if (!internalTurn) throw new Error("Failed to create internal update inference");
30898
+ await persistDeliveredUpdateBatch(delivered, session.accountId, internalTurn.id);
30637
30899
  await registerAttempt(internalTurn);
30638
30900
  if (!delivered.event) throw new Error("Delivered update batch has no durable event");
30639
- await tx.insert(schema.sessionEvents).values(delivered.event);
30901
+ await tx.insert(schema.sessionEvents).values(delivered.events);
30640
30902
  if (goalUpdate && typeof goalUpdate.payload.goalId === "string") {
30641
30903
  await tx
30642
30904
  .update(schema.sessionGoals)
@@ -30753,8 +31015,9 @@ export async function claimSessionWorkForAttempt(
30753
31015
  session.lastSequence + 1,
30754
31016
  now,
30755
31017
  );
30756
- if (delivered.event) {
30757
- await tx.insert(schema.sessionEvents).values(delivered.event);
31018
+ await persistDeliveredUpdateBatch(delivered, session.accountId, row.id);
31019
+ if (delivered.events.length > 0) {
31020
+ await tx.insert(schema.sessionEvents).values(delivered.events);
30758
31021
  }
30759
31022
  await tx
30760
31023
  .update(schema.sessions)
@@ -30998,6 +31261,196 @@ export async function markSessionAttemptQuiesced(
30998
31261
  });
30999
31262
  }
31000
31263
 
31264
+ export type ReconcileSessionAttemptQuiescenceResult =
31265
+ | { action: "quiesced"; events: SessionEvent[] }
31266
+ | { action: "pending"; events: [] }
31267
+ | { action: "stale"; events: [] };
31268
+
31269
+ export type SessionAttemptActivityRef = {
31270
+ workflowId: string;
31271
+ workflowRunId: string;
31272
+ activityId: string;
31273
+ quiesced: boolean;
31274
+ };
31275
+
31276
+ export async function getSessionAttemptActivityRef(
31277
+ db: Database,
31278
+ input: {
31279
+ accountId: string;
31280
+ workspaceId: string;
31281
+ sessionId: string;
31282
+ attemptId: string;
31283
+ temporalWorkflowId: string;
31284
+ },
31285
+ ): Promise<SessionAttemptActivityRef | null> {
31286
+ return await withRlsContext(
31287
+ db,
31288
+ { accountId: input.accountId, workspaceId: input.workspaceId },
31289
+ async (scopedDb) => {
31290
+ const [attempt] = await scopedDb
31291
+ .select({
31292
+ workflowId: schema.sessionTurnAttempts.temporalWorkflowId,
31293
+ workflowRunId: schema.sessionTurnAttempts.temporalWorkflowRunId,
31294
+ activityId: schema.sessionTurnAttempts.temporalActivityId,
31295
+ quiescedAt: schema.sessionTurnAttempts.quiescedAt,
31296
+ })
31297
+ .from(schema.sessionTurnAttempts)
31298
+ .where(
31299
+ and(
31300
+ eq(schema.sessionTurnAttempts.accountId, input.accountId),
31301
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
31302
+ eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
31303
+ eq(schema.sessionTurnAttempts.id, input.attemptId),
31304
+ eq(schema.sessionTurnAttempts.temporalWorkflowId, input.temporalWorkflowId),
31305
+ ),
31306
+ )
31307
+ .limit(1);
31308
+ return attempt
31309
+ ? {
31310
+ workflowId: attempt.workflowId,
31311
+ workflowRunId: attempt.workflowRunId,
31312
+ activityId: attempt.activityId,
31313
+ quiesced: attempt.quiescedAt !== null,
31314
+ }
31315
+ : null;
31316
+ },
31317
+ );
31318
+ }
31319
+
31320
+ /**
31321
+ * Recover the quiescence receipt when the original activity disappeared after
31322
+ * its attempt was durably interrupted. The caller first proves through
31323
+ * Temporal that the exact activity is absent or its server-owned heartbeat
31324
+ * lease expired. The closed attempt then cannot admit another workspace writer,
31325
+ * and every writer it did admit (including retained-process child writes) must
31326
+ * carry a physical settlement before the ordinary receipt transaction is
31327
+ * allowed to run.
31328
+ */
31329
+ export async function reconcileSessionAttemptQuiescence(
31330
+ db: Database,
31331
+ input: {
31332
+ accountId: string;
31333
+ workspaceId: string;
31334
+ sessionId: string;
31335
+ attemptId: string;
31336
+ temporalWorkflowId: string;
31337
+ temporalWorkflowRunId: string;
31338
+ temporalActivityId: string;
31339
+ activitySettled: boolean;
31340
+ },
31341
+ ): Promise<ReconcileSessionAttemptQuiescenceResult> {
31342
+ const eligibility = await withRlsContext(
31343
+ db,
31344
+ { accountId: input.accountId, workspaceId: input.workspaceId },
31345
+ async (scopedDb) => {
31346
+ const rows = await scopedDb.execute<{
31347
+ account_id: string;
31348
+ state: string;
31349
+ quiesced_at: Date | string | null;
31350
+ temporal_workflow_id: string;
31351
+ temporal_workflow_run_id: string;
31352
+ temporal_activity_id: string;
31353
+ interruption_settled: boolean;
31354
+ interruption_pending: boolean;
31355
+ writer_pending: boolean;
31356
+ }>(sql`
31357
+ select
31358
+ attempt.account_id,
31359
+ attempt.state,
31360
+ attempt.quiesced_at,
31361
+ attempt.temporal_workflow_id,
31362
+ attempt.temporal_workflow_run_id,
31363
+ attempt.temporal_activity_id,
31364
+ exists (
31365
+ select 1
31366
+ from session_attempt_interruptions interruption
31367
+ where interruption.workspace_id = attempt.workspace_id
31368
+ and interruption.session_id = attempt.session_id
31369
+ and interruption.attempt_id = attempt.id
31370
+ and interruption.state in ('settled', 'rejected_stale')
31371
+ ) as interruption_settled,
31372
+ exists (
31373
+ select 1
31374
+ from session_attempt_interruptions interruption
31375
+ where interruption.workspace_id = attempt.workspace_id
31376
+ and interruption.session_id = attempt.session_id
31377
+ and interruption.attempt_id = attempt.id
31378
+ and interruption.state in ('pending', 'delivered', 'acknowledged')
31379
+ ) as interruption_pending,
31380
+ (
31381
+ exists (
31382
+ select 1
31383
+ from sandbox_workspace_mutation_admissions admission
31384
+ where admission.account_id = attempt.account_id
31385
+ and admission.workspace_id = attempt.workspace_id
31386
+ and admission.session_id = attempt.session_id
31387
+ and admission.settled_at is null
31388
+ and (
31389
+ admission.attempt_id = attempt.id
31390
+ or (
31391
+ admission.actor_kind = 'process'
31392
+ and exists (
31393
+ select 1
31394
+ from sandbox_retained_processes process
31395
+ where process.account_id = attempt.account_id
31396
+ and process.workspace_id = attempt.workspace_id
31397
+ and process.session_id = attempt.session_id
31398
+ and process.id = admission.actor_id
31399
+ and process.owner_attempt_id = attempt.id
31400
+ )
31401
+ )
31402
+ )
31403
+ )
31404
+ or exists (
31405
+ select 1
31406
+ from sandbox_retained_processes process
31407
+ where process.account_id = attempt.account_id
31408
+ and process.workspace_id = attempt.workspace_id
31409
+ and process.session_id = attempt.session_id
31410
+ and process.owner_attempt_id = attempt.id
31411
+ and process.state = 'active'
31412
+ )
31413
+ ) as writer_pending
31414
+ from session_turn_attempts attempt
31415
+ where attempt.account_id = ${input.accountId}
31416
+ and attempt.workspace_id = ${input.workspaceId}
31417
+ and attempt.session_id = ${input.sessionId}
31418
+ and attempt.id = ${input.attemptId}
31419
+ limit 1
31420
+ `);
31421
+ return rows[0] ?? null;
31422
+ },
31423
+ );
31424
+ if (
31425
+ !eligibility ||
31426
+ eligibility.account_id !== input.accountId ||
31427
+ eligibility.temporal_workflow_id !== input.temporalWorkflowId ||
31428
+ eligibility.temporal_workflow_run_id !== input.temporalWorkflowRunId ||
31429
+ eligibility.temporal_activity_id !== input.temporalActivityId ||
31430
+ eligibility.state !== "closed" ||
31431
+ !eligibility.interruption_settled ||
31432
+ eligibility.interruption_pending
31433
+ ) {
31434
+ return { action: "stale", events: [] };
31435
+ }
31436
+ if (eligibility.quiesced_at) {
31437
+ return { action: "quiesced", events: [] };
31438
+ }
31439
+ if (!input.activitySettled || eligibility.writer_pending) {
31440
+ return { action: "pending", events: [] };
31441
+ }
31442
+ const events = await markSessionAttemptQuiesced(db, {
31443
+ accountId: input.accountId,
31444
+ workspaceId: input.workspaceId,
31445
+ sessionId: input.sessionId,
31446
+ attemptId: input.attemptId,
31447
+ temporalWorkflowId: input.temporalWorkflowId,
31448
+ temporalWorkflowRunId: input.temporalWorkflowRunId,
31449
+ temporalActivityId: input.temporalActivityId,
31450
+ });
31451
+ return { action: "quiesced", events };
31452
+ }
31453
+
31001
31454
  /**
31002
31455
  * Settle every durable interruption cause for one exact first-class attempt.
31003
31456
  * Steer wins the logical-turn fate when causes coexist; effective control after
@@ -31168,13 +31621,6 @@ export async function settleSessionAttemptInterruptions(
31168
31621
  outcome,
31169
31622
  closedAt: now,
31170
31623
  });
31171
- await requeueInterruptedSessionSystemUpdatesForTurnTx(
31172
- tx as unknown as Database,
31173
- workspaceId,
31174
- sessionId,
31175
- turn.id,
31176
- );
31177
-
31178
31624
  const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = steer
31179
31625
  ? [
31180
31626
  {
@@ -32459,10 +32905,45 @@ export async function applySessionTurnSettlement(
32459
32905
  },
32460
32906
  }),
32461
32907
  );
32908
+ const settledMachineInputs = ["completed", "failed", "cancelled", "superseded"].includes(
32909
+ input.turnStatus,
32910
+ )
32911
+ ? await tx
32912
+ .select({
32913
+ id: schema.sessionSystemUpdates.id,
32914
+ historyItemId: schema.sessionSystemUpdates.deliveredHistoryItemId,
32915
+ })
32916
+ .from(schema.sessionSystemUpdates)
32917
+ .where(
32918
+ and(
32919
+ eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
32920
+ eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
32921
+ eq(schema.sessionSystemUpdates.deliveredTurnId, input.turnId),
32922
+ eq(schema.sessionSystemUpdates.state, "delivered"),
32923
+ ),
32924
+ )
32925
+ .orderBy(
32926
+ asc(schema.sessionSystemUpdates.createdAt),
32927
+ asc(schema.sessionSystemUpdates.id),
32928
+ )
32929
+ : [];
32930
+ const machineInputSettlementEvent: AppendEventInput | null =
32931
+ settledMachineInputs.length > 0
32932
+ ? {
32933
+ type: "system.update.settled",
32934
+ payload: {
32935
+ updateIds: settledMachineInputs.map((update) => update.id),
32936
+ count: settledMachineInputs.length,
32937
+ historyItemId: settledMachineInputs[0]!.historyItemId,
32938
+ outcome: input.turnStatus,
32939
+ },
32940
+ }
32941
+ : null;
32462
32942
  const settlementEvents = [
32463
32943
  ...(recordingEvent ? [recordingEvent] : []),
32464
32944
  ...(compactionRequestEvent ? [compactionRequestEvent] : []),
32465
32945
  ...terminalHumanInputEvents,
32946
+ ...(machineInputSettlementEvent ? [machineInputSettlementEvent] : []),
32466
32947
  ...input.events,
32467
32948
  ];
32468
32949
  const values = settlementEvents.map((event) => {
@@ -32545,21 +33026,6 @@ export async function applySessionTurnSettlement(
32545
33026
  turn,
32546
33027
  );
32547
33028
  }
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
33029
  await tx
32564
33030
  .update(schema.sessions)
32565
33031
  .set({
@@ -32843,14 +33309,6 @@ export async function settleCodexCredentialLeaseLoss(
32843
33309
  eq(schema.sessions.activeTurnId, input.turnId),
32844
33310
  ),
32845
33311
  );
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
33312
  await tx.execute(sql`
32855
33313
  delete from codex_credential_leases
32856
33314
  where account_id = ${input.accountId}
@@ -33794,11 +34252,34 @@ export async function getSessionQueueSnapshot(
33794
34252
  ),
33795
34253
  )
33796
34254
  .orderBy(asc(schema.sessionTurns.position), asc(schema.sessionTurns.createdAt));
34255
+ const pendingInputs = await scopedDb
34256
+ .select()
34257
+ .from(schema.sessionSystemUpdates)
34258
+ .where(
34259
+ and(
34260
+ eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
34261
+ eq(schema.sessionSystemUpdates.sessionId, sessionId),
34262
+ eq(schema.sessionSystemUpdates.state, "pending"),
34263
+ ),
34264
+ )
34265
+ .orderBy(
34266
+ sql`case when ${schema.sessionSystemUpdates.kind} = 'agent_steer_instruction' then 0 else 1 end`,
34267
+ asc(schema.sessionSystemUpdates.createdAt),
34268
+ asc(schema.sessionSystemUpdates.id),
34269
+ );
33797
34270
  const latestInterruption = await latestSessionAttemptInterruption(
33798
34271
  scopedDb,
33799
34272
  workspaceId,
33800
34273
  sessionId,
33801
34274
  );
34275
+ const items = rows.map(mapSessionTurn);
34276
+ const nextInputBatch = selectBoundedSystemUpdateBatch(pendingInputs);
34277
+ const hasPendingAgentSteer = pendingInputs.some(
34278
+ (update) => update.kind === "agent_steer_instruction",
34279
+ );
34280
+ const attachmentTurn = hasPendingAgentSteer
34281
+ ? items.find((turn) => turn.metadata.delivery === "steer")
34282
+ : items[0];
33802
34283
  return {
33803
34284
  version: session.queueVersion,
33804
34285
  effectiveControl: serializeEffectiveSessionControl(effectiveControl),
@@ -33806,7 +34287,26 @@ export async function getSessionQueueSnapshot(
33806
34287
  latestInterruption !== null &&
33807
34288
  latestInterruption.interruptionState !== "rejected_stale" &&
33808
34289
  latestInterruption.quiescedAt === null,
33809
- items: rows.map(mapSessionTurn),
34290
+ items,
34291
+ pendingInputs: pendingInputs.map((update) => {
34292
+ const canonical = mapSessionSystemUpdate(update);
34293
+ return {
34294
+ id: canonical.id,
34295
+ sessionId: canonical.sessionId,
34296
+ kind: canonical.kind,
34297
+ classification: canonical.classification,
34298
+ sourceId: boundedInternalUpdateEventText(canonical.sourceId, 256).text,
34299
+ summary: boundedInternalUpdateEventText(canonical.summary, 512).text,
34300
+ createdAt: canonical.createdAt,
34301
+ };
34302
+ }),
34303
+ pendingInputAttachment:
34304
+ attachmentTurn && nextInputBatch.length > 0
34305
+ ? {
34306
+ turnId: attachmentTurn.id,
34307
+ inputIds: nextInputBatch.map((update) => update.id),
34308
+ }
34309
+ : null,
33810
34310
  };
33811
34311
  });
33812
34312
  }
@@ -34155,12 +34655,13 @@ export async function claimPendingSessionWorkflowWakes(
34155
34655
 
34156
34656
  /**
34157
34657
  * 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.
34658
+ * accepted direction or an interrupted attempt awaiting writer-set proof.
34659
+ * Temporal accepting a signal is transport evidence, not proof that a closing
34660
+ * workflow observed Postgres. While active control still has an actionable
34661
+ * Agent Steer or pending quiescence, retain the revision so the bounded outbox
34662
+ * dispatcher retries signalWithStart. The attempt-fenced claim consumes an
34663
+ * Agent Steer once; a real Pause is the typed blocker and may acknowledge this
34664
+ * revision because Resume commits a new one.
34164
34665
  *
34165
34666
  * An older sender may advance only its own revision; it cannot clear a claim or
34166
34667
  * failure state belonging to a newer revision. The control -> workspace ->
@@ -34169,7 +34670,10 @@ export async function claimPendingSessionWorkflowWakes(
34169
34670
  */
34170
34671
  export type SessionWorkflowWakeDeliveryResult =
34171
34672
  | { action: "acknowledged" }
34172
- | { action: "pending_admission"; blocker: "pending_agent_steer" };
34673
+ | {
34674
+ action: "pending_admission";
34675
+ blocker: "pending_agent_steer" | "pending_quiescence";
34676
+ };
34173
34677
 
34174
34678
  export async function markSessionWorkflowWakeDelivered(
34175
34679
  db: Database,
@@ -34209,6 +34713,35 @@ export async function markSessionWorkflowWakeDelivered(
34209
34713
  if (pendingAgentSteer) {
34210
34714
  return { action: "pending_admission", blocker: "pending_agent_steer" } as const;
34211
34715
  }
34716
+ const [pendingQuiescence] = await tx
34717
+ .select({ id: schema.sessionTurnAttempts.id })
34718
+ .from(schema.sessionTurnAttempts)
34719
+ .innerJoin(
34720
+ schema.sessionAttemptInterruptions,
34721
+ and(
34722
+ eq(
34723
+ schema.sessionAttemptInterruptions.workspaceId,
34724
+ schema.sessionTurnAttempts.workspaceId,
34725
+ ),
34726
+ eq(
34727
+ schema.sessionAttemptInterruptions.sessionId,
34728
+ schema.sessionTurnAttempts.sessionId,
34729
+ ),
34730
+ eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
34731
+ ),
34732
+ )
34733
+ .where(
34734
+ and(
34735
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
34736
+ eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
34737
+ isNull(schema.sessionTurnAttempts.quiescedAt),
34738
+ inArray(schema.sessionAttemptInterruptions.state, ["settled", "rejected_stale"]),
34739
+ ),
34740
+ )
34741
+ .limit(1);
34742
+ if (pendingQuiescence) {
34743
+ return { action: "pending_admission", blocker: "pending_quiescence" } as const;
34744
+ }
34212
34745
  }
34213
34746
  const [row] = await tx
34214
34747
  .update(schema.sessionWorkflowWakeOutbox)
@@ -34443,55 +34976,6 @@ export async function addSessionSystemUpdate(
34443
34976
  return await addSessionSystemUpdateWithSourceMutation(db, input, async () => undefined);
34444
34977
  }
34445
34978
 
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
34979
  /**
34496
34980
  * Persist one internal update without fabricating a user prompt or queue row.
34497
34981
  * Dedupe and any producer/outbox mutation commit in the same transaction.
@@ -34593,6 +35077,7 @@ export async function addSessionSystemUpdateWithSourceMutation(
34593
35077
  }
34594
35078
 
34595
35079
  const now = new Date();
35080
+ const eventPreview = internalUpdateEventMember(inserted);
34596
35081
  const [event] = await tx
34597
35082
  .insert(schema.sessionEvents)
34598
35083
  .values({
@@ -34602,11 +35087,13 @@ export async function addSessionSystemUpdateWithSourceMutation(
34602
35087
  sequence: session.lastSequence + 1,
34603
35088
  type: "system.update.pending",
34604
35089
  payload: sanitizeEventPayload({
34605
- updateId: inserted.id,
34606
- kind: input.kind,
34607
- classification: input.classification,
34608
- sourceId: input.sourceId,
34609
- summary: input.summary,
35090
+ updateId: eventPreview.id,
35091
+ kind: eventPreview.kind,
35092
+ classification: eventPreview.classification,
35093
+ sourceId: eventPreview.sourceId,
35094
+ sourceIdTruncated: eventPreview.sourceIdTruncated,
35095
+ summary: eventPreview.summary,
35096
+ summaryTruncated: eventPreview.summaryTruncated,
34610
35097
  }),
34611
35098
  occurredAt: now,
34612
35099
  })
@@ -34657,7 +35144,7 @@ export async function listOutstandingSessionSystemUpdates(
34657
35144
  and(
34658
35145
  eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
34659
35146
  eq(schema.sessionSystemUpdates.sessionId, sessionId),
34660
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
35147
+ eq(schema.sessionSystemUpdates.state, "pending"),
34661
35148
  ),
34662
35149
  )
34663
35150
  .orderBy(asc(schema.sessionSystemUpdates.createdAt), asc(schema.sessionSystemUpdates.id));
@@ -34713,6 +35200,7 @@ function mapSessionSystemUpdate(
34713
35200
  lineage: row.lineage,
34714
35201
  state: row.state as SessionSystemUpdateState,
34715
35202
  deliveredTurnId: row.deliveredTurnId,
35203
+ deliveredHistoryItemId: row.deliveredHistoryItemId,
34716
35204
  deliveredAt: row.deliveredAt?.toISOString() ?? null,
34717
35205
  createdAt: row.createdAt.toISOString(),
34718
35206
  };
@@ -34730,6 +35218,7 @@ function sessionEventTypesAdvanceActivity(inputs: ReadonlyArray<{ type: string }
34730
35218
  function sessionMutationAdvancesActivity(update: {
34731
35219
  resources?: ResourceRef[];
34732
35220
  tools?: ToolRef[];
35221
+ firstPartyMcpTools?: FirstPartyMcpToolName[];
34733
35222
  toolPolicy?: SessionToolPolicy;
34734
35223
  toolPolicyVersion?: number;
34735
35224
  expectedToolPolicyVersion?: number;
@@ -35329,6 +35818,7 @@ type LockedSessionUpdateResult = {
35329
35818
  update?: {
35330
35819
  resources?: ResourceRef[];
35331
35820
  tools?: ToolRef[];
35821
+ firstPartyMcpTools?: FirstPartyMcpToolName[];
35332
35822
  toolPolicy?: SessionToolPolicy;
35333
35823
  toolPolicyVersion?: number;
35334
35824
  expectedToolPolicyVersion?: number;
@@ -35473,6 +35963,9 @@ export async function appendSessionEventsWithLockedSessionUpdate(
35473
35963
  lastSequence: sequence,
35474
35964
  ...(update.resources !== undefined ? { resources: update.resources } : {}),
35475
35965
  ...(update.tools !== undefined ? { tools: update.tools } : {}),
35966
+ ...(update.firstPartyMcpTools !== undefined
35967
+ ? { firstPartyMcpTools: update.firstPartyMcpTools }
35968
+ : {}),
35476
35969
  ...(update.toolPolicy !== undefined ? { toolPolicy: update.toolPolicy } : {}),
35477
35970
  ...(update.toolPolicyVersion !== undefined
35478
35971
  ? { toolPolicyVersion: update.toolPolicyVersion }
@@ -35562,11 +36055,8 @@ function mapSession(
35562
36055
  resources: row.resources as ResourceRef[],
35563
36056
  skills: (row.skills as SessionSkill[]) ?? [],
35564
36057
  tools: row.tools as ToolRef[],
35565
- toolPolicy: (row.toolPolicy as SessionToolPolicy | null) ?? {
35566
- mode: "legacy",
35567
- inheritedFromSessionId: null,
35568
- },
35569
- toolPolicyVersion: Number(row.toolPolicyVersion ?? 1),
36058
+ toolPolicy: row.toolPolicy as SessionToolPolicy,
36059
+ toolPolicyVersion: Number(row.toolPolicyVersion),
35570
36060
  metadata: row.metadata,
35571
36061
  createdBy: initiatorFromStorage(
35572
36062
  row.createdByKind,
@@ -35590,6 +36080,7 @@ function mapSession(
35590
36080
  rigId: row.rigId ?? null,
35591
36081
  rigVersionId: row.rigVersionId ?? null,
35592
36082
  firstPartyMcpPermissions: (row.firstPartyMcpPermissions as Permission[] | null) ?? null,
36083
+ firstPartyMcpTools: row.firstPartyMcpTools as FirstPartyMcpToolName[],
35593
36084
  mcpServers,
35594
36085
  parentSessionId: row.parentSessionId ?? null,
35595
36086
  rootSessionId: row.rootSessionId,