@opengeni/db 6.0.0 → 6.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 (74) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/capability-reconciliation.js +2 -2
  3. package/dist/child-read-reconciliation.d.ts +35 -0
  4. package/dist/{chunk-CJI6G254.js → chunk-4CCFFLDZ.js} +2 -2
  5. package/dist/{chunk-7JCDSMX2.js → chunk-4RDUZ5UV.js} +2 -2
  6. package/dist/{chunk-SYKHLPTK.js → chunk-6ORJPCYW.js} +2 -2
  7. package/dist/{chunk-GABXCG3N.js → chunk-CSVHCF3R.js} +11 -6
  8. package/dist/chunk-CSVHCF3R.js.map +1 -0
  9. package/dist/{chunk-CDAT6EKW.js → chunk-FULLE5CF.js} +3 -3
  10. package/dist/{chunk-MN5PZMEK.js → chunk-GSB4ISHE.js} +905 -801
  11. package/dist/chunk-GSB4ISHE.js.map +1 -0
  12. package/dist/{chunk-XIXVVBW2.js → chunk-H7BC444C.js} +3 -1
  13. package/dist/chunk-H7BC444C.js.map +1 -0
  14. package/dist/{chunk-LVY33EWL.js → chunk-I7HQL6FK.js} +3 -3
  15. package/dist/{chunk-RIYHP2WU.js → chunk-JILAWE26.js} +2 -2
  16. package/dist/{chunk-ZPJ6V2VG.js → chunk-LO6BVXXH.js} +2 -2
  17. package/dist/{chunk-PJI4MUZ7.js → chunk-LSLNMENV.js} +4 -4
  18. package/dist/{chunk-RL2MUXM3.js → chunk-PGIFHEBA.js} +2 -2
  19. package/dist/{chunk-G32NVMRJ.js → chunk-RXH5CFG3.js} +5 -5
  20. package/dist/{chunk-AJA2ZIUW.js → chunk-XK6HXON2.js} +2 -2
  21. package/dist/connect-attempts.js +3 -3
  22. package/dist/editable-artifact-durable-export.js +2 -2
  23. package/dist/editable-artifacts.js +3 -3
  24. package/dist/index.d.ts +31 -0
  25. package/dist/index.js +6651 -6244
  26. package/dist/index.js.map +1 -1
  27. package/dist/integration-oauth-pending-states.d.ts +14 -0
  28. package/dist/managed-auth-session-sets.js +3 -3
  29. package/dist/mcp-operations.js +2 -2
  30. package/dist/new-session-drafts.d.ts +2 -1
  31. package/dist/organization-integration-policy.js +3 -3
  32. package/dist/provision-roles.js +1 -1
  33. package/dist/retained-provider-commands.js +6 -6
  34. package/dist/runtime-posture.d.ts +2 -2
  35. package/dist/schema.d.ts +151 -8
  36. package/dist/schema.js +3 -1
  37. package/dist/session-background-commands.js +4 -4
  38. package/dist/session-command-output.js +5 -5
  39. package/dist/session-event-slices.js +3 -3
  40. package/dist/session-mcp-credential-rotation.js +4 -4
  41. package/dist/session-meaningful-events.d.ts +15 -0
  42. package/dist/session-tenancy.js +3 -3
  43. package/dist/skills.d.ts +2 -0
  44. package/dist/video-generation.js +3 -3
  45. package/dist/workspace-tool-defaults.js +4 -4
  46. package/drizzle/0503_session_meaningful_attention.sql +81 -0
  47. package/drizzle/0504_skill_folder_limits.sql +26 -0
  48. package/drizzle/0505_workspace_control_revision_frontier.sql +37 -0
  49. package/drizzle/0506_background_command_text.sql +3 -0
  50. package/drizzle/0507_integration_oauth_pending_states.sql +27 -0
  51. package/package.json +5 -5
  52. package/src/child-read-reconciliation.ts +170 -0
  53. package/src/index.ts +439 -60
  54. package/src/integration-oauth-pending-states.ts +52 -0
  55. package/src/new-session-drafts.ts +4 -2
  56. package/src/runtime-posture.ts +2 -0
  57. package/src/schema.ts +35 -3
  58. package/src/session-background-commands.ts +8 -2
  59. package/src/session-meaningful-events.ts +97 -0
  60. package/src/skills.ts +14 -1
  61. package/dist/chunk-GABXCG3N.js.map +0 -1
  62. package/dist/chunk-MN5PZMEK.js.map +0 -1
  63. package/dist/chunk-XIXVVBW2.js.map +0 -1
  64. /package/dist/{chunk-CJI6G254.js.map → chunk-4CCFFLDZ.js.map} +0 -0
  65. /package/dist/{chunk-7JCDSMX2.js.map → chunk-4RDUZ5UV.js.map} +0 -0
  66. /package/dist/{chunk-SYKHLPTK.js.map → chunk-6ORJPCYW.js.map} +0 -0
  67. /package/dist/{chunk-CDAT6EKW.js.map → chunk-FULLE5CF.js.map} +0 -0
  68. /package/dist/{chunk-LVY33EWL.js.map → chunk-I7HQL6FK.js.map} +0 -0
  69. /package/dist/{chunk-RIYHP2WU.js.map → chunk-JILAWE26.js.map} +0 -0
  70. /package/dist/{chunk-ZPJ6V2VG.js.map → chunk-LO6BVXXH.js.map} +0 -0
  71. /package/dist/{chunk-PJI4MUZ7.js.map → chunk-LSLNMENV.js.map} +0 -0
  72. /package/dist/{chunk-RL2MUXM3.js.map → chunk-PGIFHEBA.js.map} +0 -0
  73. /package/dist/{chunk-G32NVMRJ.js.map → chunk-RXH5CFG3.js.map} +0 -0
  74. /package/dist/{chunk-AJA2ZIUW.js.map → chunk-XK6HXON2.js.map} +0 -0
package/src/index.ts CHANGED
@@ -24,6 +24,18 @@ import {
24
24
  } from "@opengeni/codex";
25
25
  export { getSessionAttemptMcpApprovalPolicies } from "./session-mcp-approval";
26
26
  import { sessionAttemptPendingWritersSql } from "./session-attempt-writers";
27
+ import {
28
+ childLifecycleEvidenceCandidatesSql,
29
+ completeMeaningfulSessionEventSql,
30
+ meaningfulSessionEventSql,
31
+ meaningfulSessionSequenceSql,
32
+ } from "./session-meaningful-events";
33
+ import {
34
+ boundedChildLifecycleEvidence,
35
+ logicalChildReadEvent,
36
+ historicalChildReadItems,
37
+ historicalChildReadMatches,
38
+ } from "./child-read-reconciliation";
27
39
  import { createRetryFailedSessionInTransaction } from "./session-retry";
28
40
  export { SessionRetryConflictError, getSessionRetryReceiptInTransaction } from "./session-retry";
29
41
  import { retainedProviderCommandPersistence as retainedProviderCommandState } from "./retained-provider-commands";
@@ -636,6 +648,10 @@ export * from "./xai-subscription";
636
648
  export * from "./organization-xai-subscriptions";
637
649
  export { interruptedToolCallResult } from "./session-tool-call-settlement";
638
650
  export { decryptEnvironmentValue, encryptEnvironmentValue } from "./environment-crypto";
651
+ export {
652
+ loadIntegrationOAuthPendingState,
653
+ storeIntegrationOAuthPendingState,
654
+ } from "./integration-oauth-pending-states";
639
655
  export {
640
656
  decryptEnvironmentValue as decryptVariableSetValue,
641
657
  encryptEnvironmentValue as encryptVariableSetValue,
@@ -7278,7 +7294,34 @@ export async function getRetainedScreenshotArtifact(
7278
7294
  .where(
7279
7295
  and(
7280
7296
  eq(schema.retainedScreenshotArtifacts.workspaceId, workspaceId),
7281
- eq(schema.retainedScreenshotArtifacts.sessionId, sessionId),
7297
+ or(
7298
+ eq(schema.retainedScreenshotArtifacts.sessionId, sessionId),
7299
+ // A fork copies canonical receipts, not the original artifact row.
7300
+ // Require both recorded ancestry and an actually copied image receipt;
7301
+ // knowing an ancestor artifact UUID is not enough. All reads retain RLS.
7302
+ sql`exists (
7303
+ with recursive lineage(id, parent_id) as (
7304
+ select s.id, s.forked_from_session_id from sessions s
7305
+ where s.workspace_id = ${workspaceId} and s.id = ${sessionId}
7306
+ union
7307
+ select s.id, s.forked_from_session_id from sessions s
7308
+ join lineage l on s.id = l.parent_id
7309
+ where s.workspace_id = ${workspaceId}
7310
+ )
7311
+ select 1 from lineage l
7312
+ where l.id = ${schema.retainedScreenshotArtifacts.sessionId}
7313
+ and exists (
7314
+ select 1 from session_history_items h
7315
+ where h.workspace_id = ${workspaceId} and h.session_id = ${sessionId}
7316
+ and h.turn_id is null
7317
+ and (
7318
+ h.item @> ${JSON.stringify({ output: { type: "retained_artifact", artifact: { artifactId } } })}::jsonb
7319
+ or h.item @> ${JSON.stringify({ output: [{ image: { type: "retained_artifact", artifact: { artifactId } } }] })}::jsonb
7320
+ or h.item @> ${JSON.stringify({ output: { content: [{ image: { type: "retained_artifact", artifact: { artifactId } } }] } })}::jsonb
7321
+ )
7322
+ )
7323
+ )`,
7324
+ ),
7282
7325
  eq(schema.retainedScreenshotArtifacts.artifactId, artifactId),
7283
7326
  ),
7284
7327
  )
@@ -34634,6 +34677,7 @@ type SessionPinRow = Pick<
34634
34677
  | "pinnedAt"
34635
34678
  | "version"
34636
34679
  | "acknowledgedSequence"
34680
+ | "manuallyUnreadThrough"
34637
34681
  | "activelyWorking"
34638
34682
  | "attentionVersion"
34639
34683
  | "archived"
@@ -34653,7 +34697,10 @@ function mapSessionPin(
34653
34697
  : { pinned: false, pinnedAt: null, pinVersion: 0 };
34654
34698
  }
34655
34699
 
34656
- type SessionRow = typeof schema.sessions.$inferSelect & { currentInputWait?: Session["inputWait"] };
34700
+ type SessionRow = typeof schema.sessions.$inferSelect & {
34701
+ currentInputWait?: Session["inputWait"];
34702
+ meaningfulSequence?: number;
34703
+ };
34657
34704
 
34658
34705
  /** One bounded query in the caller's RLS scope; never infer waits from history text. */
34659
34706
  async function withCurrentSessionInputWait(
@@ -34712,9 +34759,9 @@ async function withCurrentSessionInputWait(
34712
34759
  }
34713
34760
 
34714
34761
  /**
34715
- * Public sequence and unread projections are cursor-authoritative after the
34716
- * raw-lane activation. The compatibility column may intentionally lag raw
34717
- * deltas, but it may never lead the durable cursor.
34762
+ * Public sequence remains cursor-authoritative. Attention has its own derived
34763
+ * meaningful frontier; raw deltas and housekeeping must not create unread dots.
34764
+ * The compatibility column may lag raw deltas but cannot lead the durable cursor.
34718
34765
  */
34719
34766
  async function canonicalSessionRowsFromEventCursors(
34720
34767
  db: Database,
@@ -34729,6 +34776,10 @@ async function canonicalSessionRowsFromEventCursors(
34729
34776
  workspaceId: schema.sessionEventCursors.workspaceId,
34730
34777
  sessionId: schema.sessionEventCursors.sessionId,
34731
34778
  lastSequence: schema.sessionEventCursors.lastSequence,
34779
+ meaningfulSequence: meaningfulSessionSequenceSql(
34780
+ schema.sessionEventCursors.workspaceId,
34781
+ schema.sessionEventCursors.sessionId,
34782
+ ),
34732
34783
  })
34733
34784
  .from(schema.sessionEventCursors)
34734
34785
  .where(
@@ -34751,20 +34802,24 @@ async function canonicalSessionRowsFromEventCursors(
34751
34802
  ) {
34752
34803
  throw new Error(`Session event cursor invariant failed for session ${row.id}`);
34753
34804
  }
34754
- return cursor.lastSequence === row.lastSequence
34755
- ? row
34756
- : { ...row, lastSequence: cursor.lastSequence };
34805
+ return {
34806
+ ...row,
34807
+ lastSequence: cursor.lastSequence,
34808
+ meaningfulSequence: cursor.meaningfulSequence,
34809
+ };
34757
34810
  }),
34758
34811
  );
34759
34812
  }
34760
34813
 
34761
34814
  function mapSessionAttention(
34762
- session: Pick<typeof schema.sessions.$inferSelect, "lastSequence">,
34815
+ session: Pick<SessionRow, "lastSequence" | "meaningfulSequence">,
34763
34816
  row: SessionPinRow | null | undefined,
34764
34817
  ): Pick<Session, "unread" | "activelyWorking" | "attentionVersion"> {
34765
34818
  return row
34766
34819
  ? {
34767
- unread: session.lastSequence > row.acknowledgedSequence,
34820
+ unread:
34821
+ row.manuallyUnreadThrough != null ||
34822
+ (session.meaningfulSequence ?? session.lastSequence) > row.acknowledgedSequence,
34768
34823
  activelyWorking: row.activelyWorking,
34769
34824
  attentionVersion: Number(row.attentionVersion),
34770
34825
  }
@@ -34772,7 +34827,7 @@ function mapSessionAttention(
34772
34827
  // An absent personal row means this subject has never acknowledged a
34773
34828
  // completion. Existing and newly discovered finished work therefore
34774
34829
  // remains visible until the member explicitly marks it read.
34775
- unread: session.lastSequence > 0,
34830
+ unread: (session.meaningfulSequence ?? session.lastSequence) > 0,
34776
34831
  activelyWorking: false,
34777
34832
  attentionVersion: 0,
34778
34833
  };
@@ -34978,7 +35033,7 @@ export async function sessionTreeStatsForSessions(
34978
35033
  select
34979
35034
  root.id,
34980
35035
  root.status,
34981
- root_cursor.last_sequence,
35036
+ ${meaningfulSessionSequenceSql(sql`root.workspace_id`, sql`root.id`)},
34982
35037
  greatest(
34983
35038
  case
34984
35039
  when workspace_control.workspace_state = 'paused'
@@ -35003,7 +35058,7 @@ export async function sessionTreeStatsForSessions(
35003
35058
  select
35004
35059
  child.id,
35005
35060
  child.status,
35006
- child_cursor.last_sequence,
35061
+ ${meaningfulSessionSequenceSql(sql`child.workspace_id`, sql`child.id`)},
35007
35062
  greatest(
35008
35063
  case
35009
35064
  -- A subtree resume defeats every inherited pause older than it.
@@ -35106,7 +35161,7 @@ export async function sessionTreeStatsForSessions(
35106
35161
  and status = 'failed'
35107
35162
  and ${subjectId ?? null}::text is not null
35108
35163
  and last_sequence > coalesce((
35109
- select personal.acknowledged_sequence
35164
+ select case when personal.manually_unread_through is not null then -1 else personal.acknowledged_sequence end
35110
35165
  from ${schema.sessionPins} personal
35111
35166
  where personal.workspace_id = ${workspaceId}
35112
35167
  and personal.subject_id = ${subjectId ?? null}
@@ -35118,7 +35173,7 @@ export async function sessionTreeStatsForSessions(
35118
35173
  and depth > 0
35119
35174
  and ${subjectId ?? null}::text is not null
35120
35175
  and last_sequence > coalesce((
35121
- select personal.acknowledged_sequence
35176
+ select case when personal.manually_unread_through is not null then -1 else personal.acknowledged_sequence end
35122
35177
  from ${schema.sessionPins} personal
35123
35178
  where personal.workspace_id = ${workspaceId}
35124
35179
  and personal.subject_id = ${subjectId ?? null}
@@ -36368,6 +36423,10 @@ export async function getSessionForSubject(
36368
36423
  session: schema.sessions,
36369
36424
  pin: schema.sessionPins,
36370
36425
  cursor: schema.sessionEventCursors,
36426
+ meaningfulSequence: meaningfulSessionSequenceSql(
36427
+ schema.sessions.workspaceId,
36428
+ schema.sessions.id,
36429
+ ),
36371
36430
  })
36372
36431
  .from(schema.sessions)
36373
36432
  // Status and replay cursor must share one statement snapshot. Otherwise a
@@ -36415,7 +36474,11 @@ export async function getSessionForSubject(
36415
36474
  scopedDb,
36416
36475
  workspaceId,
36417
36476
  await withCurrentSessionInputWait(scopedDb, workspaceId, [
36418
- { ...row.session, lastSequence: row.cursor.lastSequence },
36477
+ {
36478
+ ...row.session,
36479
+ lastSequence: row.cursor.lastSequence,
36480
+ meaningfulSequence: row.meaningfulSequence,
36481
+ },
36419
36482
  ]),
36420
36483
  );
36421
36484
  if (!session) throw new Error(`Session event cursor missing for session ${sessionId}`);
@@ -36652,9 +36715,7 @@ export async function setSessionAttention(
36652
36715
  input.unread === undefined
36653
36716
  ? currentAcknowledgedSequence
36654
36717
  : input.unread
36655
- ? current.unread
36656
- ? currentAcknowledgedSequence
36657
- : Math.max(-1, session.lastSequence - 1)
36718
+ ? -1
36658
36719
  : Math.max(
36659
36720
  currentAcknowledgedSequence,
36660
36721
  Math.min(
@@ -36663,8 +36724,15 @@ export async function setSessionAttention(
36663
36724
  ),
36664
36725
  );
36665
36726
  const desiredActivelyWorking = input.activelyWorking ?? current.activelyWorking;
36727
+ const manuallyUnreadThrough =
36728
+ input.unread === undefined
36729
+ ? (existing?.manuallyUnreadThrough ?? null)
36730
+ : input.unread
36731
+ ? session.lastSequence
36732
+ : null;
36666
36733
  if (
36667
36734
  acknowledgedSequence === currentAcknowledgedSequence &&
36735
+ manuallyUnreadThrough === (existing?.manuallyUnreadThrough ?? null) &&
36668
36736
  desiredActivelyWorking === current.activelyWorking
36669
36737
  ) {
36670
36738
  const mcpServers = await sessionMcpServerMetadataForSessions(tx, input.workspaceId, [
@@ -36709,6 +36777,7 @@ export async function setSessionAttention(
36709
36777
  pinnedAt: null,
36710
36778
  version: 0,
36711
36779
  acknowledgedSequence,
36780
+ manuallyUnreadThrough,
36712
36781
  activelyWorking: desiredActivelyWorking,
36713
36782
  attentionVersion: 1,
36714
36783
  archiveVersion: 0,
@@ -36721,6 +36790,7 @@ export async function setSessionAttention(
36721
36790
  ],
36722
36791
  set: {
36723
36792
  acknowledgedSequence: acknowledgedSequenceWrite,
36793
+ manuallyUnreadThrough,
36724
36794
  activelyWorking: desiredActivelyWorking,
36725
36795
  attentionVersion: sql`${schema.sessionPins.attentionVersion} + 1`,
36726
36796
  },
@@ -36732,6 +36802,7 @@ export async function setSessionAttention(
36732
36802
  .update(schema.sessionPins)
36733
36803
  .set({
36734
36804
  acknowledgedSequence: acknowledgedSequenceWrite,
36805
+ manuallyUnreadThrough,
36735
36806
  activelyWorking: desiredActivelyWorking,
36736
36807
  attentionVersion: sql`${schema.sessionPins.attentionVersion} + 1`,
36737
36808
  })
@@ -66167,38 +66238,56 @@ function isChildLifecycleSystemUpdatePayload(
66167
66238
  function consumedChildLifecycleSessionIds(
66168
66239
  updates: ReadonlyArray<typeof schema.sessionSystemUpdates.$inferSelect>,
66169
66240
  payloadsById: ReadonlyMap<string, SessionSystemUpdatePayload>,
66170
- ): string[] {
66171
- const childSessionIds = new Set<string>();
66241
+ ): ConsumedChildEvidence[] {
66242
+ const children: ConsumedChildEvidence[] = [];
66172
66243
  for (const update of updates) {
66173
66244
  if (!isChildLifecycleSystemUpdateKind(update.kind)) continue;
66174
66245
  const payload = payloadsById.get(update.id);
66175
66246
  // The row's kind and its payload discriminator are written together; narrow
66176
66247
  // on the discriminator so the shared field access is typed rather than cast.
66177
66248
  if (!payload || !isChildLifecycleSystemUpdatePayload(payload)) continue;
66178
- childSessionIds.add(payload.childSessionId);
66249
+ const evidence = payload.childEventEvidence;
66250
+ if (!Array.isArray(evidence) || evidence.length > 32) continue;
66251
+ for (const event of evidence) {
66252
+ if (
66253
+ event &&
66254
+ typeof event === "object" &&
66255
+ Number.isSafeInteger(event.sequence) &&
66256
+ event.sequence > 0 &&
66257
+ event.sequence <= POSTGRES_INT_MAX &&
66258
+ typeof event.type === "string" &&
66259
+ "payload" in event
66260
+ ) {
66261
+ children.push({
66262
+ sessionId: payload.childSessionId,
66263
+ sequence: event.sequence,
66264
+ type: event.type,
66265
+ payload: event.payload,
66266
+ });
66267
+ }
66268
+ }
66179
66269
  }
66180
- return [...childSessionIds];
66270
+ return children;
66181
66271
  }
66182
66272
 
66273
+ type ConsumedChildEvidence = {
66274
+ sessionId: string;
66275
+ sequence: number;
66276
+ type: string;
66277
+ payload: unknown;
66278
+ };
66279
+
66183
66280
  /**
66184
66281
  * Acknowledge every child whose lifecycle notice this claim just turned into
66185
66282
  * durable model input, for the receiving turn's frozen initiating human only.
66186
66283
  *
66187
- * A parent agent that consumed a child's result, pause, capacity wait, progress
66188
- * note, or human-input boundary has already carried that fact to the human who
66189
- * started the turn, so the child's blue unread dot is pure noise: an
66190
- * orchestrator with dozens of children otherwise leaves dozens of permanently
66191
- * unread rows until a human opens each one by hand. This advances exactly the
66192
- * per-viewer fence `setSessionAttention` writes, as if that human had viewed the
66193
- * child. A pure service turn has no initiating human and writes nothing.
66194
- *
66195
- * `failed` and `requires_action` indicators are derived from `sessions.status`
66196
- * and rank above unread, so an acknowledged child that still needs a human keeps
66197
- * saying so, and a child that emits a further event goes unread again on its own
66198
- * because unread is only the sequence comparison in {@link mapSessionAttention}.
66199
- * The fence is monotone, so it also advances past a human's earlier explicit
66200
- * mark-unread once the parent consumes a newer notice: consumption is the
66201
- * signal, and OpenGeni keeps no durable "keep this unread" intent.
66284
+ * Only complete immutable event evidence carried in the claimed payload counts.
66285
+ * A terminal status alone does not prove that the parent saw the child's answer.
66286
+ * Pure service turns and legacy notices without content evidence write nothing.
66287
+ * A complete final answer is a cumulative read watermark. Other evidence advances
66288
+ * only a contiguous meaningful prefix. The shared writer preserves newer
66289
+ * personal attention mutations, and never reads the child's current raw cursor
66290
+ * as consumption evidence. Failed/requires_action lifecycle indicators remain.
66202
66291
  *
66203
66292
  * This deliberately does NOT touch `attention_version`. That revision exists
66204
66293
  * only to order explicit human attention mutations against each other, and this
@@ -66234,13 +66323,12 @@ function consumedChildLifecycleSessionIds(
66234
66323
  * The upsert is monotone: the conflict path re-reads the committed row under its
66235
66324
  * own row lock and refuses to move a fence backward.
66236
66325
  *
66237
- * There is no membership probe. The write grants nothing and discloses nothing
66238
- * (a fence is visible only to its own subject), the subject comes from durable
66239
- * frozen turn provenance rather than a request, membership removal deletes these
66240
- * rows and is serialized against this writer by the fence above, and the
66241
- * personal-workspace-owner exception a bare membership probe would need is
66242
- * authorized by an API-layer canonical-managed-cookie stamp that this worker path
66243
- * does not have. Both RLS policies on `session_pins` still apply through the
66326
+ * Current authority is rechecked inside the advisory fence, so historical replay
66327
+ * after membership removal cannot recreate deleted personal state. Frozen turn
66328
+ * provenance identifies the human; the canonical live-authority resolver only
66329
+ * checks that human's still-active shared membership or exact Personal pointer,
66330
+ * and is not used to authorize a caller or grant access. No creator fallback or
66331
+ * request-derived human is accepted. Both RLS policies on `session_pins` still apply through the
66244
66332
  * temporary subject scope, and the insert is additionally fenced on
66245
66333
  * `parent_session_id`, so a payload field can never decide whose personal state
66246
66334
  * is mutated on an unrelated session.
@@ -66251,11 +66339,239 @@ async function acknowledgeConsumedChildLifecycleNotices(
66251
66339
  workspaceId: string;
66252
66340
  sessionId: string;
66253
66341
  subjectId: string | null;
66254
- childSessionIds: readonly string[];
66342
+ childSessionIds: readonly ConsumedChildEvidence[];
66255
66343
  },
66256
66344
  ): Promise<void> {
66257
66345
  const subjectId = input.subjectId?.trim();
66258
66346
  if (!subjectId || input.childSessionIds.length === 0) return;
66347
+ // Payloads are evidence, not authority. Verify every complete snapshot against
66348
+ // its immutable child event before accepting the claimed model-input receipt.
66349
+ const proven = new Map<string, number[]>();
66350
+ const grouped = new Map<string, ConsumedChildEvidence[]>();
66351
+ for (const evidence of input.childSessionIds) {
66352
+ const group = grouped.get(evidence.sessionId) ?? [];
66353
+ group.push(evidence);
66354
+ grouped.set(evidence.sessionId, group);
66355
+ }
66356
+ for (const [childId, evidence] of grouped) {
66357
+ const events = await tx
66358
+ .select({
66359
+ sequence: schema.sessionEvents.sequence,
66360
+ type: schema.sessionEvents.type,
66361
+ payload: schema.sessionEvents.payload,
66362
+ payloadCodecVersion: schema.sessionEvents.payloadCodecVersion,
66363
+ })
66364
+ .from(schema.sessionEvents)
66365
+ .innerJoin(
66366
+ schema.sessions,
66367
+ and(
66368
+ eq(schema.sessions.workspaceId, schema.sessionEvents.workspaceId),
66369
+ eq(schema.sessions.id, schema.sessionEvents.sessionId),
66370
+ eq(schema.sessions.parentSessionId, input.sessionId),
66371
+ ),
66372
+ )
66373
+ .where(
66374
+ and(
66375
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
66376
+ eq(schema.sessionEvents.sessionId, childId),
66377
+ inArray(schema.sessionEvents.sequence, [
66378
+ ...new Set(evidence.map((event) => event.sequence)),
66379
+ ]),
66380
+ ),
66381
+ );
66382
+ const bySequence = new Map(
66383
+ events.map((event) => [event.sequence, logicalChildReadEvent(event)]),
66384
+ );
66385
+ const sequences = evidence
66386
+ .filter((proof) => {
66387
+ const event = bySequence.get(proof.sequence);
66388
+ return event?.type === proof.type && isDeepStrictEqual(event.payload, proof.payload);
66389
+ })
66390
+ .map((event) => event.sequence);
66391
+ if (sequences.length > 0) proven.set(childId, sequences);
66392
+ }
66393
+ await acknowledgeConsumedChildSequencesInTransaction(tx, {
66394
+ ...input,
66395
+ children: [...proven].map(([sessionId, sequences]) => ({ sessionId, sequences })),
66396
+ });
66397
+ }
66398
+
66399
+ /** Internal read receipt. Call only after the complete bounded response has been
66400
+ * constructed and the exact caller attempt's frozen human reauthorized. */
66401
+ export async function acknowledgeConsumedChildEvents(
66402
+ db: Database,
66403
+ input: {
66404
+ workspaceId: string;
66405
+ sessionId: string;
66406
+ subjectId: string | null;
66407
+ children: readonly { sessionId: string; sequences: readonly number[] }[];
66408
+ },
66409
+ ): Promise<void> {
66410
+ await withWorkspaceRls(db, input.workspaceId, async (tx) => {
66411
+ await acknowledgeConsumedChildSequencesInTransaction(tx, input);
66412
+ });
66413
+ }
66414
+
66415
+ /** Bounded operator reconciliation, dry-run by default. No timestamps, session
66416
+ * creator or latest human are evidence of consumption. Unrecognized historical
66417
+ * providers/Codemode layouts remain untouched rather than guessed. */
66418
+ export async function reconcileHistoricalChildReadAcknowledgments(
66419
+ db: Database,
66420
+ input: {
66421
+ workspaceId: string;
66422
+ parentSessionId: string;
66423
+ afterSequence?: number;
66424
+ limit?: number;
66425
+ apply?: boolean;
66426
+ },
66427
+ ): Promise<{
66428
+ scanned: number;
66429
+ provenEvents: number;
66430
+ unsupportedReceipts: number;
66431
+ nextAfter: number;
66432
+ hasMore: boolean;
66433
+ applied: boolean;
66434
+ }> {
66435
+ const limit = Math.max(1, Math.min(100, input.limit ?? 50));
66436
+ return await withWorkspaceRls(db, input.workspaceId, async (tx) => {
66437
+ const outputs = await tx
66438
+ .select()
66439
+ .from(schema.sessionEvents)
66440
+ .where(
66441
+ and(
66442
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
66443
+ eq(schema.sessionEvents.sessionId, input.parentSessionId),
66444
+ eq(schema.sessionEvents.type, "agent.toolCall.output"),
66445
+ gt(schema.sessionEvents.sequence, input.afterSequence ?? 0),
66446
+ isNull(schema.sessionEvents.duplicateOfEventId),
66447
+ eq(schema.sessionEvents.turnAssociation, "current"),
66448
+ sql`octet_length(${schema.sessionEvents.payload}::text) <= 262144`,
66449
+ ),
66450
+ )
66451
+ .orderBy(asc(schema.sessionEvents.sequence))
66452
+ .limit(limit + 1);
66453
+ let provenEvents = 0;
66454
+ let unsupportedReceipts = 0;
66455
+ for (const output of outputs.slice(0, limit)) {
66456
+ const payload = logicalChildReadEvent(output).payload as Record<string, unknown> | null;
66457
+ if (!output.turnId || !payload || typeof payload.id !== "string" || payload.truncation) {
66458
+ unsupportedReceipts += 1;
66459
+ continue;
66460
+ }
66461
+ const [turn] = await tx
66462
+ .select()
66463
+ .from(schema.sessionTurns)
66464
+ .where(
66465
+ and(
66466
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
66467
+ eq(schema.sessionTurns.sessionId, input.parentSessionId),
66468
+ eq(schema.sessionTurns.id, output.turnId),
66469
+ ),
66470
+ )
66471
+ .limit(1);
66472
+ const subjectId =
66473
+ turn?.initiatingHumanSubjectId ??
66474
+ (turn?.initiatorKind === "subject" ? turn.initiatorSubjectId : null);
66475
+ if (!subjectId) {
66476
+ unsupportedReceipts += 1;
66477
+ continue;
66478
+ }
66479
+ const calls = await tx
66480
+ .select()
66481
+ .from(schema.sessionEvents)
66482
+ .where(
66483
+ and(
66484
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
66485
+ eq(schema.sessionEvents.sessionId, input.parentSessionId),
66486
+ eq(schema.sessionEvents.turnId, output.turnId),
66487
+ eq(schema.sessionEvents.type, "agent.toolCall.created"),
66488
+ eq(schema.sessionEvents.turnAssociation, "current"),
66489
+ isNull(schema.sessionEvents.duplicateOfEventId),
66490
+ lt(schema.sessionEvents.sequence, output.sequence),
66491
+ sql`${schema.sessionEvents.payload} ->> 'id' = ${payload.id}`,
66492
+ sql`octet_length(${schema.sessionEvents.payload}::text) <= 16384`,
66493
+ ),
66494
+ )
66495
+ .limit(2);
66496
+ if (calls.length !== 1) {
66497
+ unsupportedReceipts += 1;
66498
+ continue;
66499
+ }
66500
+ const call = logicalChildReadEvent(calls[0]!).payload as Record<string, unknown>;
66501
+ const candidates = historicalChildReadItems(call.name, call.arguments, payload.output);
66502
+ const proven: ConsumedChildEvidence[] = [];
66503
+ for (const candidate of candidates.slice(0, 100)) {
66504
+ if (
66505
+ !UUID_PATTERN.test(candidate.sessionId) ||
66506
+ !Number.isSafeInteger(candidate.item.sequence) ||
66507
+ (candidate.item.sequence as number) < 1 ||
66508
+ (candidate.item.sequence as number) > POSTGRES_INT_MAX
66509
+ )
66510
+ continue;
66511
+ const [source] = await tx
66512
+ .select({ event: schema.sessionEvents })
66513
+ .from(schema.sessionEvents)
66514
+ .innerJoin(
66515
+ schema.sessions,
66516
+ and(
66517
+ eq(schema.sessions.workspaceId, schema.sessionEvents.workspaceId),
66518
+ eq(schema.sessions.id, schema.sessionEvents.sessionId),
66519
+ eq(schema.sessions.parentSessionId, input.parentSessionId),
66520
+ ),
66521
+ )
66522
+ .where(
66523
+ and(
66524
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
66525
+ eq(schema.sessionEvents.sessionId, candidate.sessionId),
66526
+ eq(schema.sessionEvents.sequence, candidate.item.sequence as number),
66527
+ completeMeaningfulSessionEventSql("session_events"),
66528
+ sql`octet_length(${schema.sessionEvents.payload}::text) <= 262144`,
66529
+ ),
66530
+ )
66531
+ .limit(1);
66532
+ if (!source) continue;
66533
+ const logicalEvent = logicalChildReadEvent(source.event);
66534
+ if (!historicalChildReadMatches(candidate, logicalEvent)) continue;
66535
+ proven.push({
66536
+ sessionId: candidate.sessionId,
66537
+ sequence: source.event.sequence,
66538
+ type: source.event.type,
66539
+ payload: logicalEvent.payload,
66540
+ });
66541
+ }
66542
+ if (proven.length === 0) unsupportedReceipts += 1;
66543
+ provenEvents += proven.length;
66544
+ if (input.apply && proven.length > 0) {
66545
+ await acknowledgeConsumedChildLifecycleNotices(tx, {
66546
+ workspaceId: input.workspaceId,
66547
+ sessionId: input.parentSessionId,
66548
+ subjectId,
66549
+ childSessionIds: proven,
66550
+ });
66551
+ }
66552
+ }
66553
+ return {
66554
+ scanned: Math.min(limit, outputs.length),
66555
+ provenEvents,
66556
+ unsupportedReceipts,
66557
+ nextAfter: outputs[Math.min(limit, outputs.length) - 1]?.sequence ?? input.afterSequence ?? 0,
66558
+ hasMore: outputs.length > limit,
66559
+ applied: input.apply === true,
66560
+ };
66561
+ });
66562
+ }
66563
+
66564
+ async function acknowledgeConsumedChildSequencesInTransaction(
66565
+ tx: Database,
66566
+ input: {
66567
+ workspaceId: string;
66568
+ sessionId: string;
66569
+ subjectId: string | null;
66570
+ children: readonly { sessionId: string; sequences: readonly number[] }[];
66571
+ },
66572
+ ): Promise<void> {
66573
+ const subjectId = input.subjectId?.trim();
66574
+ if (!subjectId || input.children.length === 0) return;
66259
66575
  // SHARED, not exclusive. `listSessionsForSubject` holds the shared counterpart
66260
66576
  // of this fence for its whole rail-list transaction, and that list refreshes on
66261
66577
  // focus, online, and visibilitychange. An exclusive probe fails against a held
@@ -66275,9 +66591,47 @@ async function acknowledgeConsumedChildLifecycleNotices(
66275
66591
  if (!fence?.acquired) return;
66276
66592
  const pins = schema.sessionPins;
66277
66593
  const sessions = schema.sessions;
66278
- const cursors = schema.sessionEventCursors;
66279
66594
  await withTemporarySubjectRls(tx, subjectId, async () => {
66280
- await tx.execute(sql`
66595
+ if (
66596
+ !(await subjectHasLiveWorkspaceAuthorityInScope(tx, {
66597
+ accountId: await accountIdInRlsScope(tx),
66598
+ workspaceId: input.workspaceId,
66599
+ subjectId,
66600
+ }))
66601
+ )
66602
+ return;
66603
+ // Stable UUID order preserves the personal-row lock order. A complete final
66604
+ // answer summarizes earlier work; nonfinal filtered reads cannot skip holes.
66605
+ for (const child of [...input.children].sort((a, b) =>
66606
+ a.sessionId.localeCompare(b.sessionId),
66607
+ )) {
66608
+ const requested = [
66609
+ ...new Set(
66610
+ child.sequences.filter(
66611
+ (value) => Number.isSafeInteger(value) && value > 0 && value <= POSTGRES_INT_MAX,
66612
+ ),
66613
+ ),
66614
+ ];
66615
+ if (requested.length === 0) continue;
66616
+ const events = await tx
66617
+ .select({ sequence: schema.sessionEvents.sequence, type: schema.sessionEvents.type })
66618
+ .from(schema.sessionEvents)
66619
+ .where(
66620
+ and(
66621
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
66622
+ eq(schema.sessionEvents.sessionId, child.sessionId),
66623
+ inArray(schema.sessionEvents.sequence, requested),
66624
+ completeMeaningfulSessionEventSql("session_events"),
66625
+ ),
66626
+ );
66627
+ const sequences = events.map((event) => event.sequence);
66628
+ if (sequences.length === 0) continue; // status-only reads do not acknowledge
66629
+ const through = Math.max(...sequences);
66630
+ const finalThrough = Math.max(
66631
+ 0,
66632
+ ...events.filter((event) => event.type === "turn.completed").map((event) => event.sequence),
66633
+ );
66634
+ await tx.execute(sql`
66281
66635
  insert into ${pins} (
66282
66636
  account_id, workspace_id, subject_id, session_id,
66283
66637
  pinned, pinned_at, version,
@@ -66290,26 +66644,37 @@ async function acknowledgeConsumedChildLifecycleNotices(
66290
66644
  -- at the absent-row projection so a concurrent pin/archive writer
66291
66645
  -- holding that projection stays valid.
66292
66646
  false, null, 0,
66293
- ${cursors.lastSequence}, false, 0,
66647
+ least(${through}, coalesce((
66648
+ select min(unseen.sequence) - 1 from session_events unseen
66649
+ where unseen.workspace_id = ${sessions.workspaceId} and unseen.session_id = ${sessions.id}
66650
+ and unseen.sequence > greatest(
66651
+ coalesce(personal.acknowledged_sequence, 0),
66652
+ coalesce(personal.manually_unread_through, 0), ${finalThrough})
66653
+ and unseen.sequence <= ${through}
66654
+ and ${meaningfulSessionEventSql("unseen")}
66655
+ and unseen.sequence not in (${sql.join(
66656
+ sequences.map((sequence) => sql`${sequence}`),
66657
+ sql`, `,
66658
+ )})
66659
+ ), ${through})), false, 0,
66294
66660
  false, null, 0
66295
66661
  from ${sessions}
66296
- join ${cursors}
66297
- on ${cursors.accountId} = ${sessions.accountId}
66298
- and ${cursors.workspaceId} = ${sessions.workspaceId}
66299
- and ${cursors.sessionId} = ${sessions.id}
66662
+ left join ${pins} personal on personal.workspace_id = ${sessions.workspaceId}
66663
+ and personal.session_id = ${sessions.id} and personal.subject_id = ${subjectId}
66300
66664
  where ${and(
66301
66665
  eq(sessions.workspaceId, input.workspaceId),
66302
66666
  eq(sessions.parentSessionId, input.sessionId),
66303
- inArray(sessions.id, [...input.childSessionIds]),
66304
- // Nothing to acknowledge on a child with no durable events: the
66305
- // absent-row projection already reports it read.
66306
- gt(cursors.lastSequence, 0),
66667
+ eq(sessions.id, child.sessionId),
66307
66668
  )}
66669
+ and (personal.manually_unread_through is null or ${through} > personal.manually_unread_through)
66308
66670
  order by ${sessions.id}
66309
66671
  on conflict (subject_id, workspace_id, session_id) do update
66310
- set acknowledged_sequence = excluded.acknowledged_sequence
66672
+ set acknowledged_sequence = excluded.acknowledged_sequence,
66673
+ manually_unread_through = null
66311
66674
  where ${pins.acknowledgedSequence} < excluded.acknowledged_sequence
66675
+ and (${pins.manuallyUnreadThrough} is null or excluded.acknowledged_sequence > ${pins.manuallyUnreadThrough})
66312
66676
  `);
66677
+ }
66313
66678
  });
66314
66679
  }
66315
66680
 
@@ -66385,7 +66750,7 @@ export async function claimSessionWorkForAttempt(
66385
66750
  historyItem: Record<string, unknown> | null;
66386
66751
  updates: Array<typeof schema.sessionSystemUpdates.$inferSelect>;
66387
66752
  /** Deduped children the delivered batch reports on, in delivery order. */
66388
- childSessionIds: string[];
66753
+ childSessionIds: ConsumedChildEvidence[];
66389
66754
  events: SessionEventInsertWithPayload[];
66390
66755
  event: SessionEventInsertWithPayload | null;
66391
66756
  }> => {
@@ -75751,6 +76116,20 @@ async function enqueueChildLifecycleNoticeOutboxTx(
75751
76116
  ...session,
75752
76117
  parentSessionId: session.parentSessionId,
75753
76118
  });
76119
+ // Freeze complete content at notice creation, never at parent claim time.
76120
+ // Oversized evidence is omitted, not truncated and then called consumed.
76121
+ // Bound index candidates before payload filters, and prefer the newest complete
76122
+ // result within the total budget. Explicit reads handle omitted evidence.
76123
+ const candidates = await rawRows<{
76124
+ sequence: number;
76125
+ type: string;
76126
+ payload: unknown;
76127
+ payloadCodecVersion: number | null;
76128
+ }>(tx, childLifecycleEvidenceCandidatesSql(sql`${workspaceId}::uuid`, sql`${session.id}::uuid`));
76129
+ const noticePayload = {
76130
+ ...input.payload,
76131
+ childEventEvidence: boundedChildLifecycleEvidence(candidates),
76132
+ };
75754
76133
  const inserted = await tx
75755
76134
  .insert(schema.sessionSystemUpdateOutbox)
75756
76135
  .values(
@@ -75766,7 +76145,7 @@ async function enqueueChildLifecycleNoticeOutboxTx(
75766
76145
  classification: input.classification,
75767
76146
  sourceId: session.id,
75768
76147
  summary: input.summary,
75769
- payload: input.payload,
76148
+ payload: noticePayload,
75770
76149
  lineage: { ...authority.lineage, ...(input.lineage ?? {}) },
75771
76150
  personalConnectionDelegations: authority.personalConnectionDelegations,
75772
76151
  mcpAccountBindings: authority.mcpAccountBindings,