maka-agent 0.2.0-dev.22.20260905 → 0.2.0-dev.24.20260907

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 (194) hide show
  1. package/dist/acp/maka-acp-agent.js +2 -1
  2. package/dist/acp/session-configuration.js +143 -0
  3. package/dist/acp/session-registry.js +90 -4
  4. package/dist/cli-core.js +2 -0
  5. package/dist/pi-transcript.js +5 -12
  6. package/dist/pi-tui-runner.js +74 -4
  7. package/dist/runtime-host-cli-context.js +167 -78
  8. package/dist/runtime-host-cli-installation.js +8 -2
  9. package/dist/runtime-host-cli.js +31 -1
  10. package/dist/runtime-host-handoff-surface.js +81 -0
  11. package/dist/runtime-host-installed-update-bootstrap.js +13 -0
  12. package/dist/runtime-host-installed-update-coordinator.js +33 -0
  13. package/dist/runtime-host-lifecycle-transaction.js +9 -1
  14. package/dist/runtime-host-local-handoff.js +37 -4
  15. package/dist/runtime-host-local-managed-activation.js +84 -0
  16. package/dist/runtime-host-local-source-retirement.js +18 -2
  17. package/dist/runtime-host-managed-lifecycle-manager.js +1 -0
  18. package/dist/runtime-host-operator-retirement.js +50 -0
  19. package/dist/runtime-host-service-manager.js +3 -0
  20. package/dist/runtime-host-session-driver.js +4 -25
  21. package/dist/runtime-host-session-update.js +62 -0
  22. package/dist/runtime-host-tui-command.js +38 -73
  23. package/dist/runtime-host-tui-context.js +1 -0
  24. package/dist/runtime-host-tui-owner.js +226 -0
  25. package/dist/tui-copy-catalog.js +140 -3
  26. package/dist/tui-host-owner-copy.js +24 -0
  27. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  28. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  29. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  30. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  31. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  32. package/node_modules/@maka/core/dist/bot-events.js +18 -63
  33. package/node_modules/@maka/core/dist/canonical-runtime-event.js +3 -1
  34. package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
  35. package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
  36. package/node_modules/@maka/core/dist/model-failure.js +40 -0
  37. package/node_modules/@maka/core/dist/model-metadata.generated.js +4 -4
  38. package/node_modules/@maka/core/dist/model-metadata.js +3 -3
  39. package/node_modules/@maka/core/dist/model-web-search.js +5 -4
  40. package/node_modules/@maka/core/dist/provider-registry.js +45 -4
  41. package/node_modules/@maka/core/dist/record-schema.js +22 -2
  42. package/node_modules/@maka/core/dist/redaction.js +41 -58
  43. package/node_modules/@maka/core/dist/runtime-boundary.js +31 -11
  44. package/node_modules/@maka/core/dist/runtime-event.js +37 -6
  45. package/node_modules/@maka/core/dist/runtime-handoff.js +98 -0
  46. package/node_modules/@maka/core/dist/runtime-invocation.js +1 -1
  47. package/node_modules/@maka/core/dist/runtime-logical-execution.js +112 -0
  48. package/node_modules/@maka/core/dist/session-trace.js +5 -1
  49. package/node_modules/@maka/core/dist/session.js +54 -52
  50. package/node_modules/@maka/core/dist/slash-command-catalog.js +2 -0
  51. package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +20 -0
  52. package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
  53. package/node_modules/@maka/core/dist/usage-record-schema.js +26 -35
  54. package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
  55. package/node_modules/@maka/core/package.json +4 -0
  56. package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
  57. package/node_modules/@maka/mcp/dist/index.js +131 -1
  58. package/node_modules/@maka/runtime/dist/agent-run.js +272 -105
  59. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +13 -5
  60. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +109 -34
  61. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +155 -134
  62. package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
  63. package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
  64. package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
  65. package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
  66. package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
  67. package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
  68. package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
  69. package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
  70. package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
  71. package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
  72. package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
  73. package/node_modules/@maka/runtime/dist/context-budget.js +14 -9
  74. package/node_modules/@maka/runtime/dist/conversation-copy.js +5 -4
  75. package/node_modules/@maka/runtime/dist/goal-continuation.js +36 -4
  76. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +18 -9
  77. package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +30 -22
  78. package/node_modules/@maka/runtime/dist/history-compaction.js +29 -4
  79. package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +106 -0
  80. package/node_modules/@maka/runtime/dist/message-authority.js +9 -0
  81. package/node_modules/@maka/runtime/dist/model-adapter.js +19 -98
  82. package/node_modules/@maka/runtime/dist/model-factory.js +19 -81
  83. package/node_modules/@maka/runtime/dist/model-fetcher.js +7 -0
  84. package/node_modules/@maka/runtime/dist/model-runtime.js +85 -111
  85. package/node_modules/@maka/runtime/dist/native-web-search-tool.js +2 -1
  86. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +2 -2
  87. package/node_modules/@maka/runtime/dist/opencode-session-header.js +4 -2
  88. package/node_modules/@maka/runtime/dist/provider-error-classification.js +84 -123
  89. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +68 -108
  90. package/node_modules/@maka/runtime/dist/run-handoff-gate.js +97 -0
  91. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +82 -54
  92. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +117 -159
  93. package/node_modules/@maka/runtime/dist/runtime-kernel.js +150 -112
  94. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +160 -79
  95. package/node_modules/@maka/runtime/dist/runtime-read-model.js +30 -89
  96. package/node_modules/@maka/runtime/dist/runtime-resume.js +80 -4
  97. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +5 -0
  98. package/node_modules/@maka/runtime/dist/session-manager.js +171 -288
  99. package/node_modules/@maka/runtime/dist/session-projection-helpers.js +0 -27
  100. package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
  101. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +3 -4
  102. package/node_modules/@maka/runtime/dist/test-connection.js +16 -16
  103. package/node_modules/@maka/runtime/dist/tool-result-archive-encoding.js +38 -0
  104. package/node_modules/@maka/runtime/dist/tool-runtime.js +3 -61
  105. package/node_modules/@maka/runtime/package.json +1 -0
  106. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +35 -0
  107. package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
  108. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +9 -1
  109. package/node_modules/@maka/runtime-host/dist/client/connection.js +3 -0
  110. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +156 -0
  111. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +273 -0
  112. package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +26 -5
  113. package/node_modules/@maka/runtime-host/dist/client/index.js +3 -1
  114. package/node_modules/@maka/runtime-host/dist/client/launcher.js +3 -0
  115. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
  116. package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
  117. package/node_modules/@maka/runtime-host/dist/client/remote-compatibility-error.js +2 -0
  118. package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +27 -5
  119. package/node_modules/@maka/runtime-host/dist/operator/index.js +2 -1
  120. package/node_modules/@maka/runtime-host/dist/operator/retirement-cancellation.js +20 -0
  121. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
  122. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
  123. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +29 -12
  124. package/node_modules/@maka/runtime-host/dist/protocol/index.js +37 -1
  125. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +2 -0
  126. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
  127. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +40 -20
  128. package/node_modules/@maka/runtime-host/dist/protocol/session-transcript.js +0 -5
  129. package/node_modules/@maka/runtime-host/dist/protocol/session-turns.js +29 -58
  130. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +11 -1
  131. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
  132. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
  133. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +6 -1
  134. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +19 -4
  135. package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
  136. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +7 -6
  137. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
  138. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +168 -28
  139. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +5 -5
  140. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +3 -2
  141. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
  142. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +40 -1
  143. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +108 -23
  144. package/node_modules/@maka/runtime-host/dist/server/host-residency-registry.js +13 -7
  145. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-admission-registry.js +3 -1
  146. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +124 -62
  147. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +85 -8
  148. package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
  149. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +344 -71
  150. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +37 -2
  151. package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +45 -3
  152. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +201 -12
  153. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +87 -25
  154. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +8 -55
  155. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +1 -16
  156. package/node_modules/@maka/runtime-host/dist/server/session-transcript-pager.js +0 -1
  157. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +460 -70
  158. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +1 -1
  159. package/node_modules/@maka/runtime-host/dist/server/storage-maintenance.js +109 -0
  160. package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
  161. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
  162. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -2
  163. package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
  164. package/node_modules/@maka/storage/dist/artifact-store.js +21 -10
  165. package/node_modules/@maka/storage/dist/artifact-stores.js +1 -1
  166. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -4
  167. package/node_modules/@maka/storage/dist/codex-session-adapter.js +0 -2
  168. package/node_modules/@maka/storage/dist/context-offload-snapshot.js +325 -0
  169. package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
  170. package/node_modules/@maka/storage/dist/execution-stores.js +11 -7
  171. package/node_modules/@maka/storage/dist/file-session-repository.js +1012 -0
  172. package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
  173. package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
  174. package/node_modules/@maka/storage/dist/opencode-session-adapter.js +0 -4
  175. package/node_modules/@maka/storage/dist/operational-state-backup.js +9 -5
  176. package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
  177. package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
  178. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  179. package/node_modules/@maka/storage/dist/runtime-transcript-query.js +308 -0
  180. package/node_modules/@maka/storage/dist/session-bundle-policy.js +24 -4
  181. package/node_modules/@maka/storage/dist/session-message-projection.js +15 -0
  182. package/node_modules/@maka/storage/dist/session-repository.js +767 -0
  183. package/node_modules/@maka/storage/dist/session-store.js +25 -99
  184. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +10 -3
  185. package/node_modules/@maka/storage/dist/sqlite-context-offload-store.js +24 -8
  186. package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +17 -1
  187. package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +44 -1
  188. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +129 -25
  189. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +109 -892
  190. package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
  191. package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +111 -0
  192. package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
  193. package/node_modules/@maka/storage/package.json +2 -0
  194. package/package.json +1 -1
@@ -36,15 +36,12 @@ import { messageContentDigest, messageContentsEqual, normalizeMessageContent, }
36
36
  import { AGENT_GRAPH_SUPERVISOR_WAKE_SCHEMA_VERSION, } from '@maka/core/agent-graph-supervisor-wake';
37
37
  import { assertSafeSessionId, decodePersistedSessionHeader, normalizeSessionHeader, SessionNotFoundError, } from './session-store.js';
38
38
  import { isDiscardableConversationCopy, isValidConversationCopyTransition, } from './session-conversation-copy.js';
39
- import { catalogPreviewForUserMessage } from './session-message-projection.js';
39
+ import { projectSessionCatalogMessages } from './session-message-projection.js';
40
40
  import { configureSqliteSessionMetadataDatabase, migrateSqliteSessionMetadataDatabase, readSqliteSessionMetadataSchemaVersion, SQLITE_AGENT_GRAPH_CONTROL_TABLES, SQLITE_SESSION_MESSAGE_CHUNK_BYTES, SQLITE_SESSION_MESSAGE_CHUNK_MARKER, } from './sqlite-session-metadata-schema.js';
41
41
  import { buildSqliteSessionCatalogPageQuery, } from './sqlite-session-catalog-query.js';
42
42
  import { sqliteOrdinarySessionRolePredicate, sqliteRecoverableSessionRolePredicate, } from './sqlite-session-role-scope.js';
43
43
  export { SQLITE_SESSION_METADATA_SCHEMA_VERSION } from './sqlite-session-metadata-schema.js';
44
44
  const SQLITE_TRANSCRIPT_MESSAGE_LOOKUP_BATCH_SIZE = 256;
45
- const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_MESSAGES = 1_024;
46
- const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
47
- const SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES = 32;
48
45
  // Each target Session binds three parameters in the linkage query. Stay well
49
46
  // inside SQLite's bound-parameter limit.
50
47
  const WORKHUB_TARGET_LINKAGE_MAX_SESSIONS = 256;
@@ -351,6 +348,32 @@ export class SqliteSessionMetadataStore {
351
348
  return rows.map(decodeSandboxBoundaryRequestRow);
352
349
  });
353
350
  }
351
+ async hasExplicitSandboxBoundaryDenial(identities) {
352
+ this.assertOpen();
353
+ const query = this.db.prepare(`
354
+ SELECT
355
+ MAX(CASE WHEN outcome_reason = 'client_denied' THEN 1 ELSE 0 END) AS explicit,
356
+ MAX(CASE WHEN outcome_reason IN ('client_denied', 'turn_stopped', 'turn_terminal', 'host_restarted')
357
+ THEN 0 ELSE 1 END) AS ambiguous
358
+ FROM sandbox_boundary_log
359
+ WHERE session_id = ? AND run_id = ? AND turn_id = ?
360
+ AND entry_kind = 'expansion_request' AND status = 'denied'
361
+ `);
362
+ let denied = false;
363
+ for (const identity of identities) {
364
+ assertSafeSessionId(identity.sessionId);
365
+ assertSandboxBoundaryProvenanceId(identity.runId, 'run id');
366
+ assertSandboxBoundaryProvenanceId(identity.turnId, 'turn id');
367
+ const evidence = query.get(identity.sessionId, identity.runId, identity.turnId);
368
+ // Legacy NULL also meant internal cleanup. Do not invent a user decision
369
+ // or erase a possible denial; ambiguous provenance blocks this continuation.
370
+ if (evidence.ambiguous === 1) {
371
+ throw new Error('Historical sandbox denial cannot be attributed safely; start a new user Turn.');
372
+ }
373
+ denied ||= evidence.explicit === 1;
374
+ }
375
+ return denied;
376
+ }
354
377
  async settleSandboxBoundaryRequest(input) {
355
378
  this.assertOpen();
356
379
  assertSafeSessionId(input.sessionId);
@@ -378,7 +401,7 @@ export class SqliteSessionMetadataStore {
378
401
  sessionId: input.sessionId,
379
402
  requestId: input.requestId,
380
403
  status: 'denied',
381
- ...(input.closureReason ? { outcomeReason: input.closureReason } : {}),
404
+ outcomeReason: input.closureReason ?? 'client_denied',
382
405
  settledAt,
383
406
  });
384
407
  return {
@@ -908,6 +931,10 @@ export class SqliteSessionMetadataStore {
908
931
  .run(sessionId);
909
932
  });
910
933
  }
934
+ /**
935
+ * Session records in catalog order, each carrying the projection the Session
936
+ * list shows.
937
+ */
911
938
  async list(filter, roleScope) {
912
939
  this.assertOpen();
913
940
  const { where, parameters } = buildSessionListPredicate(filter ?? {});
@@ -923,17 +950,21 @@ export class SqliteSessionMetadataStore {
923
950
  }
924
951
  const rows = this.db
925
952
  .prepare(`
926
- SELECT session_id, payload_json, metadata_version, committed_at
953
+ SELECT
954
+ metadata.session_id,
955
+ metadata.payload_json,
956
+ metadata.metadata_version,
957
+ metadata.committed_at,
958
+ COALESCE(projection.activity_at, 0) AS activity_at,
959
+ projection.last_message_preview
927
960
  FROM session_metadata metadata
961
+ LEFT JOIN session_catalog_projection projection
962
+ ON projection.session_id = metadata.session_id
928
963
  ${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''}
929
- ORDER BY (
930
- SELECT activity_at
931
- FROM session_catalog_projection projection
932
- WHERE projection.session_id = metadata.session_id
933
- ) DESC, session_id ASC
964
+ ORDER BY activity_at DESC, metadata.session_id ASC
934
965
  `)
935
966
  .all(...parameters);
936
- return rows.map(decodeRecord);
967
+ return rows.map(decodeCatalogRecord);
937
968
  }
938
969
  async listCatalogPage(filter, cursor, limit) {
939
970
  this.assertOpen();
@@ -1495,11 +1526,12 @@ export class SqliteSessionMetadataStore {
1495
1526
  return [];
1496
1527
  return this.readTransaction(() => {
1497
1528
  const list = targets.map(() => '?').join(', ');
1498
- // One Message moves between these lifecycle tables. Combine every target's
1499
- // identities once, then resolve activity from the canonical Coordination
1500
- // ledger in this same read transaction. That avoids rebuilding the target
1501
- // set once per page or once per candidate, without introducing another
1502
- // durable representation.
1529
+ // One Message moves between these lifecycle tables — pending, admitted
1530
+ // into a Turn, cancelled. Combine every target's identities once, then
1531
+ // resolve activity from the canonical Coordination ledger in this same
1532
+ // read transaction. That avoids rebuilding the target set once per page
1533
+ // or once per candidate, without introducing another durable
1534
+ // representation.
1503
1535
  const rows = this.db
1504
1536
  .prepare(`
1505
1537
  WITH target_messages(session_id, message_id) AS (
@@ -1510,7 +1542,7 @@ export class SqliteSessionMetadataStore {
1510
1542
  AND length(message_id) = 52
1511
1543
  UNION
1512
1544
  SELECT session_id, message_id
1513
- FROM session_messages
1545
+ FROM core_root_source_message_proofs
1514
1546
  WHERE session_id IN (${list})
1515
1547
  AND message_id GLOB 'whm_*'
1516
1548
  AND length(message_id) = 52
@@ -1613,19 +1645,6 @@ export class SqliteSessionMetadataStore {
1613
1645
  provenSteeringMessages.set(normalized.messageId, normalized);
1614
1646
  }
1615
1647
  this.transaction(() => {
1616
- const lastSequenceRow = this.db
1617
- .prepare('SELECT COALESCE(MAX(sequence), -1) AS last_sequence FROM session_messages WHERE session_id = ?')
1618
- .get(input.sessionId);
1619
- if (typeof lastSequenceRow.last_sequence !== 'number' ||
1620
- !Number.isSafeInteger(lastSequenceRow.last_sequence) ||
1621
- lastSequenceRow.last_sequence < -1) {
1622
- throw new SessionMetadataConflictError('Invalid Session message sequence');
1623
- }
1624
- const lastSequence = lastSequenceRow.last_sequence;
1625
- const historicalMissingMessages = new Map();
1626
- const ordinaryMissingMessages = new Map();
1627
- const historicalMessageIdSet = new Set();
1628
- const existingSequences = new Map();
1629
1648
  for (const messageId of unique) {
1630
1649
  const fallback = provenRootMessages.get(messageId);
1631
1650
  const steeringProof = provenSteeringMessages.get(messageId);
@@ -1662,69 +1681,14 @@ export class SqliteSessionMetadataStore {
1662
1681
  !messageContentsEqual(admission.content, fallback.content)) {
1663
1682
  throw new SessionMetadataConflictError('Message admission fallback content conflict');
1664
1683
  }
1665
- if (admission === undefined && fallback !== undefined) {
1666
- historicalMessageIdSet.add(messageId);
1667
- }
1668
1684
  if (!admission &&
1669
1685
  this.db
1670
1686
  .prepare('SELECT 1 AS present FROM cancelled_message_admissions WHERE session_id = ? AND message_id = ?')
1671
1687
  .get(input.sessionId, messageId)) {
1672
1688
  throw new SessionMetadataConflictError('Message admission is already cancelled');
1673
1689
  }
1674
- const rows = this.db
1675
- .prepare(`
1676
- SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
1677
- FROM session_messages AS message
1678
- LEFT JOIN session_message_payloads AS payload
1679
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
1680
- WHERE message.session_id = ? AND message.message_id = ?
1681
- `)
1682
- .all(input.sessionId, messageId);
1683
- if (rows.length > 1) {
1684
- throw new SessionMetadataConflictError('Message admission transcript identity is ambiguous');
1685
- }
1686
- if (rows.length === 0) {
1687
- const source = admission ?? fallback;
1688
- if (!source)
1689
- throw new SessionMetadataConflictError('Message admission does not exist');
1690
- const message = decodeCanonicalMessage({
1691
- type: 'user',
1692
- id: messageId,
1693
- turnId: input.turnId,
1694
- ts: steeringProof?.eventTs ?? source.admittedAt,
1695
- ...source.content,
1696
- steeringEventId: steeringProof?.eventId ?? messageId,
1697
- });
1698
- const json = JSON.stringify(message);
1699
- (historicalMessageIdSet.has(messageId)
1700
- ? historicalMissingMessages
1701
- : ordinaryMissingMessages).set(messageId, { message, json });
1702
- }
1703
- else {
1704
- const sequence = rows[0]?.sequence;
1705
- if (typeof sequence !== 'number' || !Number.isSafeInteger(sequence)) {
1706
- throw new SessionMetadataConflictError('Invalid Message transcript sequence');
1707
- }
1708
- const row = rows[0];
1709
- const recordJson = readStoredMessageRecordJson(this.db, input.sessionId, sequence, row);
1710
- const message = decodeStoredMessage(JSON.parse(recordJson));
1711
- const expectedSource = admission ?? fallback ?? steeringProof;
1712
- if (message.type !== 'user' ||
1713
- message.id !== messageId ||
1714
- (expectedSource !== undefined &&
1715
- !messageContentsEqual(normalizeMessageContent(message), expectedSource.content))) {
1716
- throw new SessionMetadataConflictError('Message admission transcript identity conflict');
1717
- }
1718
- if (message.turnId !== input.turnId) {
1719
- throw new SessionMetadataConflictError('Message admission transcript Turn conflict');
1720
- }
1721
- if (steeringProof !== undefined &&
1722
- (message.ts !== steeringProof.eventTs ||
1723
- message.turnId !== steeringProof.executionTurnId ||
1724
- message.steeringEventId !== steeringProof.eventId)) {
1725
- throw new SessionMetadataConflictError('Proven steering transcript identity conflict');
1726
- }
1727
- existingSequences.set(messageId, sequence);
1690
+ if (admission === undefined && fallback === undefined && steeringProof === undefined) {
1691
+ throw new SessionMetadataConflictError('Message admission does not exist');
1728
1692
  }
1729
1693
  if (admission) {
1730
1694
  const deleted = this.db
@@ -1735,94 +1699,21 @@ export class SqliteSessionMetadataStore {
1735
1699
  }
1736
1700
  }
1737
1701
  }
1738
- let tailLatest;
1739
- if (historicalMessageIdSet.size > 0) {
1740
- const transcript = this.readSessionMessageOrderingSync(input.sessionId);
1741
- let previousExistingSequence = -1;
1742
- const historicalMessageIds = unique.filter((messageId) => historicalMessageIdSet.has(messageId));
1743
- for (const messageId of historicalMessageIds) {
1744
- const sequence = existingSequences.get(messageId);
1745
- if (sequence === undefined)
1746
- continue;
1747
- const admittedAt = provenRootMessages.get(messageId).admittedAt;
1748
- const blockingRow = transcript.find(({ sequence: candidateSequence, message }) => candidateSequence > previousExistingSequence &&
1749
- candidateSequence < sequence &&
1750
- !historicalMessageIdSet.has(message.id) &&
1751
- (message.turnId === input.turnId || message.ts >= admittedAt));
1752
- if (sequence <= previousExistingSequence || blockingRow !== undefined) {
1753
- throw new SessionMetadataConflictError('Message admission transcript source order conflict');
1754
- }
1755
- previousExistingSequence = sequence;
1756
- }
1757
- const insertionGroups = [];
1758
- let pending = [];
1759
- let previousAnchor = -1;
1760
- for (const messageId of historicalMessageIds) {
1761
- const missing = historicalMissingMessages.get(messageId);
1762
- if (missing) {
1763
- pending.push(missing);
1764
- continue;
1765
- }
1766
- const boundary = existingSequences.get(messageId);
1767
- if (pending.length > 0 && boundary !== undefined) {
1768
- const admittedAt = Math.min(...pending.map(({ message }) => message.ts));
1769
- const earlierBoundary = transcript.find(({ sequence, message }) => sequence > previousAnchor &&
1770
- sequence < boundary &&
1771
- !historicalMessageIdSet.has(message.id) &&
1772
- (message.turnId === input.turnId || message.ts >= admittedAt));
1773
- if (earlierBoundary) {
1774
- throw new SessionMetadataConflictError('Message admission transcript source order conflict');
1775
- }
1776
- insertionGroups.push({ boundary, entries: pending });
1777
- pending = [];
1778
- }
1779
- if (boundary !== undefined)
1780
- previousAnchor = boundary;
1781
- }
1782
- if (pending.length > 0) {
1783
- const admittedAt = Math.min(...pending.map(({ message }) => message.ts));
1784
- const repairBoundary = transcript.find(({ sequence, message }) => sequence > previousAnchor &&
1785
- !historicalMessageIdSet.has(message.id) &&
1786
- (message.turnId === input.turnId || message.ts >= admittedAt))?.sequence;
1787
- insertionGroups.push({
1788
- boundary: repairBoundary ?? lastSequence + 1,
1789
- entries: pending,
1790
- });
1791
- }
1792
- for (const group of insertionGroups.reverse()) {
1793
- this.shiftSessionMessageSuffixSync(input.sessionId, group.boundary, group.entries.length);
1794
- this.insertSessionMessagesSync(input.sessionId, group.boundary, group.entries);
1795
- if (group.boundary === lastSequence + 1) {
1796
- tailLatest = group.entries.at(-1)?.message;
1797
- }
1798
- }
1799
- }
1800
- if (ordinaryMissingMessages.size > 0) {
1801
- const ordinaryEntries = unique.flatMap((messageId) => {
1802
- const entry = ordinaryMissingMessages.get(messageId);
1803
- return entry ? [entry] : [];
1804
- });
1805
- const currentLastSequenceRow = this.db
1806
- .prepare('SELECT COALESCE(MAX(sequence), -1) AS last_sequence FROM session_messages WHERE session_id = ?')
1807
- .get(input.sessionId);
1808
- const currentLastSequence = currentLastSequenceRow.last_sequence;
1809
- if (typeof currentLastSequence !== 'number' ||
1810
- !Number.isSafeInteger(currentLastSequence) ||
1811
- currentLastSequence < -1) {
1812
- throw new SessionMetadataConflictError('Invalid Session message sequence');
1813
- }
1814
- this.insertSessionMessagesSync(input.sessionId, currentLastSequence + 1, ordinaryEntries);
1815
- tailLatest = ordinaryEntries.at(-1)?.message;
1816
- }
1817
- if (tailLatest?.type === 'user') {
1818
- this.updateCatalogProjectionSync(input.sessionId, {
1819
- lastMessageAt: tailLatest.ts,
1820
- lastMessagePreview: catalogPreviewForUserMessage(tailLatest),
1821
- }, false, true);
1822
- }
1823
- else if (historicalMissingMessages.size > 0 || ordinaryMissingMessages.size > 0) {
1824
- this.updateCatalogProjectionSync(input.sessionId, {}, false, true);
1825
- }
1702
+ });
1703
+ }
1704
+ /**
1705
+ * The catalog facts a durable message carries, committed without a transcript
1706
+ * row to carry them: the Session list's preview line, its time, and the
1707
+ * connection lock a Session takes on its first user message.
1708
+ */
1709
+ async commitMessageCatalogProjection(sessionId, message) {
1710
+ this.assertOpen();
1711
+ assertSafeSessionId(sessionId);
1712
+ this.transaction(() => {
1713
+ const record = this.readRecordSync(sessionId);
1714
+ if (!record)
1715
+ throw new SessionNotFoundError(sessionId);
1716
+ this.updateCatalogProjectionSync(sessionId, projectSessionCatalogMessages([message]), false, message.type === 'user' && !record.header.connectionLocked);
1826
1717
  });
1827
1718
  }
1828
1719
  async updateMessageAdmission(admission) {
@@ -1937,112 +1828,57 @@ export class SqliteSessionMetadataStore {
1937
1828
  async readMessages(sessionId) {
1938
1829
  return this.readMessagesWith(sessionId, decodeStoredMessage);
1939
1830
  }
1940
- async readTranscriptPage(sessionId, request) {
1831
+ async readMessagesAfter(sessionId, request) {
1941
1832
  this.assertOpen();
1942
1833
  assertSafeSessionId(sessionId);
1943
- assertTranscriptPageRequest(request);
1834
+ if (!Number.isSafeInteger(request.maxMessages) || request.maxMessages < 1) {
1835
+ throw new Error('Invalid Session message count limit');
1836
+ }
1837
+ if (!Number.isSafeInteger(request.maxStoredBytes) || request.maxStoredBytes < 1) {
1838
+ throw new Error('Invalid Session message byte limit');
1839
+ }
1840
+ if (request.afterSequence !== undefined && request.beforeSequence !== undefined) {
1841
+ throw new Error('Invalid Session message scan bounds');
1842
+ }
1843
+ const backward = request.beforeSequence !== undefined;
1944
1844
  return this.readTransaction(() => {
1945
1845
  if (!this.readRecordSync(sessionId))
1946
1846
  throw new SessionNotFoundError(sessionId);
1947
- const highWaterRow = this.db
1948
- .prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
1949
- .get(sessionId);
1950
- const actualHighWater = nullableStoredMessageSequence(highWaterRow.high_water, sessionId);
1951
- const throughSequence = request.throughSequence === undefined ? actualHighWater : request.throughSequence;
1952
- if (throughSequence === null) {
1953
- return {
1954
- throughSequence: null,
1955
- fragments: [],
1956
- rawBytes: 0,
1957
- next: null,
1958
- };
1959
- }
1960
- if (actualHighWater === null || throughSequence > actualHighWater) {
1961
- throw new Error(`Session transcript watermark is ahead of durable storage: ${sessionId}`);
1962
- }
1963
- const position = request.position ?? (request.direction === 'older' ? throughSequence : 0);
1964
- const comparison = request.direction === 'older' ? '<=' : '>=';
1965
- const order = request.direction === 'older' ? 'DESC' : 'ASC';
1966
1847
  const rows = this.db
1967
1848
  .prepare(`
1968
- SELECT message.sequence,
1969
- coalesce(payload.record_bytes, length(CAST(message.record_json AS BLOB))) AS total_bytes,
1970
- payload.record_bytes IS NOT NULL AS chunked,
1971
- payload.sha256 AS payload_sha256
1849
+ SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
1972
1850
  FROM session_messages AS message
1973
1851
  LEFT JOIN session_message_payloads AS payload
1974
1852
  ON payload.session_id = message.session_id AND payload.sequence = message.sequence
1975
- WHERE message.session_id = ?
1976
- AND message.sequence <= ?
1977
- AND message.sequence ${comparison} ?
1978
- ORDER BY message.sequence ${order}
1853
+ WHERE message.session_id = ? AND message.sequence ${backward ? '<' : '>'} ?
1854
+ ORDER BY message.sequence ${backward ? 'DESC' : 'ASC'}
1979
1855
  LIMIT ?
1980
1856
  `)
1981
- .all(sessionId, throughSequence, position, request.maxMessages + 1);
1982
- const slices = [];
1983
- let rawBytes = 0;
1984
- let next = null;
1857
+ .all(sessionId, backward ? request.beforeSequence : (request.afterSequence ?? -1), request.maxMessages);
1858
+ const records = [];
1859
+ let storedBytes = 0;
1985
1860
  for (const row of rows) {
1986
- if (slices.length >= request.maxMessages || rawBytes >= request.maxBytes)
1987
- break;
1988
1861
  const sequence = requireStoredMessageSequence(row.sequence, sessionId);
1989
- const totalBytes = requireTranscriptRecordByteLength(row.total_bytes, sessionId, sequence);
1990
- const chunked = row.chunked === 1;
1991
- const payloadDigest = chunked
1992
- ? requireTranscriptPayloadDigest(row.payload_sha256, sessionId, sequence)
1993
- : null;
1994
- const continued = sequence === position && request.byteOffset !== undefined;
1995
- const edge = continued
1996
- ? request.byteOffset
1997
- : request.direction === 'older'
1998
- ? totalBytes
1999
- : 0;
2000
- if ((request.direction === 'older' && (edge < 1 || edge > totalBytes)) ||
2001
- (request.direction === 'newer' && (edge < 0 || edge >= totalBytes))) {
2002
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
2003
- }
2004
- const available = request.maxBytes - rawBytes;
2005
- const byteOffset = request.direction === 'older' ? Math.max(0, edge - available) : edge;
2006
- const byteLength = request.direction === 'older'
2007
- ? edge - byteOffset
2008
- : Math.min(totalBytes - edge, available);
2009
- const complete = request.direction === 'older' ? byteOffset === 0 : byteOffset + byteLength === totalBytes;
2010
- slices.push({
1862
+ // A record too large for one row is stored in chunks, with only a
1863
+ // marker inline; its size is the chunk total, not the marker's.
1864
+ const recordBytes = typeof row.record_bytes === 'number' ? row.record_bytes : String(row.record_json).length;
1865
+ // The first record of a page is always taken, so a single row larger
1866
+ // than the budget still makes progress instead of stalling the scan.
1867
+ if (records.length > 0 && storedBytes + recordBytes > request.maxStoredBytes)
1868
+ break;
1869
+ storedBytes += recordBytes;
1870
+ records.push({
2011
1871
  sequence,
2012
- byteOffset,
2013
- totalBytes,
2014
- byteLength,
2015
- chunked,
2016
- payloadDigest,
1872
+ message: decodeStoredMessageRecordRow(this.db, sessionId, row),
2017
1873
  });
2018
- rawBytes += byteLength;
2019
- if (!complete) {
2020
- next = {
2021
- position: sequence,
2022
- byteOffset: request.direction === 'older' ? byteOffset : byteOffset + byteLength,
2023
- };
2024
- break;
2025
- }
2026
1874
  }
2027
- if (next === null && slices.length > 0 && slices.length < rows.length) {
2028
- const sequence = slices.at(-1).sequence;
2029
- next = {
2030
- position: sequence + (request.direction === 'older' ? -1 : 1),
2031
- byteOffset: null,
2032
- };
2033
- }
2034
- const dataBySequence = readTranscriptSlices(this.db, sessionId, slices);
2035
- const fragments = slices.map(({ sequence, byteOffset, totalBytes, byteLength, payloadDigest }) => {
2036
- const data = dataBySequence.get(sequence);
2037
- if (!data || data.byteLength !== byteLength) {
2038
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
2039
- }
2040
- if (byteOffset === 0 && byteLength === totalBytes) {
2041
- validateTranscriptRecord(data, sessionId, sequence);
2042
- }
2043
- return { sequence, byteOffset, totalBytes, payloadDigest, data };
2044
- });
2045
- return { throughSequence, fragments, rawBytes, next };
1875
+ const highWater = this.db
1876
+ .prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
1877
+ .get(sessionId);
1878
+ return {
1879
+ records,
1880
+ highWaterSequence: nullableStoredMessageSequence(highWater.high_water, sessionId),
1881
+ };
2046
1882
  });
2047
1883
  }
2048
1884
  async readTranscriptMessages(sessionId, request) {
@@ -2129,80 +1965,6 @@ export class SqliteSessionMetadataStore {
2129
1965
  return messages;
2130
1966
  });
2131
1967
  }
2132
- async readTranscriptRecords(sessionId, request) {
2133
- this.assertOpen();
2134
- assertSafeSessionId(sessionId);
2135
- assertTranscriptRecordScanRequest(request);
2136
- return this.readTransaction(() => {
2137
- if (!this.readRecordSync(sessionId))
2138
- throw new SessionNotFoundError(sessionId);
2139
- const highWaterRow = this.db
2140
- .prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
2141
- .get(sessionId);
2142
- const actualHighWater = nullableStoredMessageSequence(highWaterRow.high_water, sessionId);
2143
- const throughSequence = request.throughSequence === undefined ? actualHighWater : request.throughSequence;
2144
- if (throughSequence === null) {
2145
- return { throughSequence: null, records: [], nextPosition: null };
2146
- }
2147
- if (actualHighWater === null || throughSequence > actualHighWater) {
2148
- throw new Error(`Session transcript watermark is ahead of durable storage: ${sessionId}`);
2149
- }
2150
- const position = request.position ?? (request.direction === 'older' ? throughSequence : 0);
2151
- const comparison = request.direction === 'older' ? '<=' : '>=';
2152
- const order = request.direction === 'older' ? 'DESC' : 'ASC';
2153
- const rows = this.db
2154
- .prepare(`
2155
- SELECT message.sequence,
2156
- coalesce(payload.record_bytes, length(CAST(message.record_json AS BLOB))) AS stored_bytes
2157
- FROM session_messages AS message
2158
- LEFT JOIN session_message_payloads AS payload
2159
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
2160
- WHERE message.session_id = ?
2161
- AND message.sequence <= ?
2162
- AND message.sequence ${comparison} ?
2163
- ORDER BY message.sequence ${order}
2164
- LIMIT ?
2165
- `)
2166
- .all(sessionId, throughSequence, position, request.maxMessages + 1);
2167
- const selected = [];
2168
- let storedBytes = 0;
2169
- for (const row of rows) {
2170
- if (selected.length >= request.maxMessages)
2171
- break;
2172
- const sequence = requireStoredMessageSequence(row.sequence, sessionId);
2173
- const bytes = requireTranscriptRecordByteLength(row.stored_bytes, sessionId, sequence);
2174
- if (selected.length > 0 && storedBytes + bytes > request.maxStoredBytes)
2175
- break;
2176
- selected.push(sequence);
2177
- storedBytes += bytes;
2178
- }
2179
- const decoded = new Map();
2180
- for (const row of readStoredMessageRows(this.db, sessionId, selected)) {
2181
- try {
2182
- decoded.set(row.sequence, decodeStoredMessage(JSON.parse(row.recordJson)));
2183
- }
2184
- catch (error) {
2185
- throw new StoredSessionMessageIncompatibleError(sessionId, row.sequence, {
2186
- cause: error,
2187
- });
2188
- }
2189
- }
2190
- const records = selected.map((sequence) => {
2191
- const message = decoded.get(sequence);
2192
- if (!message)
2193
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
2194
- return { sequence, message };
2195
- });
2196
- const last = selected.at(-1);
2197
- return {
2198
- throughSequence,
2199
- records,
2200
- nextPosition: last !== undefined && rows.length > selected.length
2201
- ? last + (request.direction === 'older' ? -1 : 1)
2202
- : null,
2203
- };
2204
- });
2205
- }
2206
1968
  async readTranscriptHighWater(sessionId) {
2207
1969
  this.assertOpen();
2208
1970
  assertSafeSessionId(sessionId);
@@ -2213,295 +1975,6 @@ export class SqliteSessionMetadataStore {
2213
1975
  .get(sessionId);
2214
1976
  return nullableStoredMessageSequence(row.high_water, sessionId);
2215
1977
  }
2216
- async readTurnContributions(sessionId, throughSequence, position, maxContributions) {
2217
- this.assertOpen();
2218
- assertSafeSessionId(sessionId);
2219
- if ((throughSequence !== null &&
2220
- (!Number.isSafeInteger(throughSequence) || throughSequence < 0)) ||
2221
- !Number.isSafeInteger(position) ||
2222
- position < 0 ||
2223
- !Number.isSafeInteger(maxContributions) ||
2224
- maxContributions < 1 ||
2225
- maxContributions > 128) {
2226
- throw new Error('Invalid Session turn contribution request');
2227
- }
2228
- return this.readTransaction(() => {
2229
- if (!this.readRecordSync(sessionId))
2230
- throw new SessionNotFoundError(sessionId);
2231
- const highWaterRow = this.db
2232
- .prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
2233
- .get(sessionId);
2234
- const actualHighWater = nullableStoredMessageSequence(highWaterRow.high_water, sessionId);
2235
- const fixedThrough = throughSequence ?? actualHighWater;
2236
- if (fixedThrough === null) {
2237
- return { throughSequence: null, contributions: [], nextPosition: null };
2238
- }
2239
- if (actualHighWater === null || fixedThrough > actualHighWater) {
2240
- throw new Error(`Session turn watermark is ahead of durable storage: ${sessionId}`);
2241
- }
2242
- const contributions = new Map();
2243
- let nextPosition = position;
2244
- let sourceMessages = 0;
2245
- let sourceBytes = 0;
2246
- while (nextPosition <= fixedThrough) {
2247
- const rows = this.db
2248
- .prepare(`
2249
- SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
2250
- FROM session_messages AS message
2251
- LEFT JOIN session_message_payloads AS payload
2252
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
2253
- WHERE message.session_id = ?
2254
- AND message.sequence >= ?
2255
- AND message.sequence <= ?
2256
- ORDER BY message.sequence ASC
2257
- LIMIT 128
2258
- `)
2259
- .all(sessionId, nextPosition, fixedThrough);
2260
- if (rows.length === 0) {
2261
- throw new StoredSessionMessageIncompatibleError(sessionId, nextPosition);
2262
- }
2263
- for (const row of rows) {
2264
- const sequence = requireStoredMessageSequence(row.sequence, sessionId);
2265
- const recordBytes = storedMessageRecordBytes(row, sessionId, sequence);
2266
- if (sourceMessages > 0 &&
2267
- (sourceMessages >= SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_MESSAGES ||
2268
- sourceBytes + recordBytes > SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_BYTES)) {
2269
- return {
2270
- throughSequence: fixedThrough,
2271
- contributions: [...contributions.values()],
2272
- nextPosition: sequence,
2273
- };
2274
- }
2275
- const message = decodeStoredMessageRecordRow(this.db, sessionId, row);
2276
- sourceMessages += 1;
2277
- sourceBytes += recordBytes;
2278
- if (!('turnId' in message) || typeof message.turnId !== 'string') {
2279
- nextPosition = sequence + 1;
2280
- continue;
2281
- }
2282
- const turnId = message.turnId;
2283
- if (turnId && !contributions.has(turnId) && contributions.size >= maxContributions) {
2284
- nextPosition = sequence;
2285
- return {
2286
- throughSequence: fixedThrough,
2287
- contributions: [...contributions.values()],
2288
- nextPosition,
2289
- };
2290
- }
2291
- contributions.set(turnId, foldTurnContribution(contributions.get(turnId), turnId, sequence, message));
2292
- nextPosition = sequence + 1;
2293
- }
2294
- }
2295
- return {
2296
- throughSequence: fixedThrough,
2297
- contributions: [...contributions.values()],
2298
- nextPosition: null,
2299
- };
2300
- });
2301
- }
2302
- async readTurnLandmarks(sessionId, maxLandmarks) {
2303
- this.assertOpen();
2304
- assertSafeSessionId(sessionId);
2305
- if (!Number.isSafeInteger(maxLandmarks) || maxLandmarks < 1 || maxLandmarks > 64) {
2306
- throw new Error('Invalid Session turn landmark limit');
2307
- }
2308
- return this.readTransaction(() => {
2309
- if (!this.readRecordSync(sessionId))
2310
- throw new SessionNotFoundError(sessionId);
2311
- const throughRow = this.db
2312
- .prepare(`
2313
- SELECT sequence AS through_sequence
2314
- FROM session_messages
2315
- WHERE session_id = ?
2316
- ORDER BY sequence DESC
2317
- LIMIT 1
2318
- `)
2319
- .get(sessionId);
2320
- const throughSequence = nullableStoredMessageSequence(throughRow?.through_sequence, sessionId);
2321
- if (throughSequence === null) {
2322
- return { throughSequence: null, landmarks: [] };
2323
- }
2324
- const promptRows = this.db
2325
- .prepare(`
2326
- SELECT admission.admitted_at, message.sequence
2327
- FROM core_root_turn_admissions AS admission
2328
- JOIN session_messages AS message
2329
- ON message.session_id = admission.session_id
2330
- AND message.message_id = json_extract(admission.record_json, '$.userMessageId')
2331
- LEFT JOIN session_message_payloads AS payload
2332
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
2333
- WHERE admission.session_id = ? AND payload.sequence IS NULL
2334
- ORDER BY admission.admitted_at ASC, admission.turn_id ASC
2335
- LIMIT ?
2336
- `)
2337
- .all(sessionId, maxLandmarks + 1);
2338
- const selected = new Set();
2339
- if (promptRows.length <= maxLandmarks) {
2340
- for (const row of promptRows) {
2341
- selected.add(requireStoredMessageSequence(row.sequence, sessionId));
2342
- }
2343
- }
2344
- const forward = this.db.prepare(`
2345
- SELECT admission.admitted_at, message.sequence
2346
- FROM core_root_turn_admissions AS admission
2347
- JOIN session_messages AS message
2348
- ON message.session_id = admission.session_id
2349
- AND message.message_id = json_extract(admission.record_json, '$.userMessageId')
2350
- LEFT JOIN session_message_payloads AS payload
2351
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
2352
- WHERE admission.session_id = ? AND admission.admitted_at >= ? AND payload.sequence IS NULL
2353
- ORDER BY admission.admitted_at ASC, admission.turn_id ASC
2354
- LIMIT 1
2355
- `);
2356
- const backward = this.db.prepare(`
2357
- SELECT admission.admitted_at, message.sequence
2358
- FROM core_root_turn_admissions AS admission
2359
- JOIN session_messages AS message
2360
- ON message.session_id = admission.session_id
2361
- AND message.message_id = json_extract(admission.record_json, '$.userMessageId')
2362
- LEFT JOIN session_message_payloads AS payload
2363
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
2364
- WHERE admission.session_id = ? AND admission.admitted_at < ? AND payload.sequence IS NULL
2365
- ORDER BY admission.admitted_at DESC, admission.turn_id DESC
2366
- LIMIT 1
2367
- `);
2368
- if (promptRows.length > maxLandmarks) {
2369
- const firstAdmittedAt = requireTurnLandmarkAdmittedAt(promptRows[0]?.admitted_at);
2370
- const lastRow = this.db
2371
- .prepare(`
2372
- SELECT admitted_at
2373
- FROM core_root_turn_admissions
2374
- WHERE session_id = ?
2375
- ORDER BY admitted_at DESC, turn_id DESC
2376
- LIMIT 1
2377
- `)
2378
- .get(sessionId);
2379
- const lastAdmittedAt = requireTurnLandmarkAdmittedAt(lastRow?.admitted_at);
2380
- for (let index = 0; index < maxLandmarks; index += 1) {
2381
- const target = maxLandmarks === 1
2382
- ? lastAdmittedAt
2383
- : firstAdmittedAt +
2384
- Math.floor(((lastAdmittedAt - firstAdmittedAt) * index) / (maxLandmarks - 1));
2385
- const candidates = [
2386
- ...forward.all(sessionId, target),
2387
- ...backward.all(sessionId, target),
2388
- ];
2389
- let nearest;
2390
- for (const candidate of candidates) {
2391
- const admittedAt = requireTurnLandmarkAdmittedAt(candidate.admitted_at);
2392
- if (nearest === undefined ||
2393
- Math.abs(admittedAt - target) <
2394
- Math.abs(requireTurnLandmarkAdmittedAt(nearest.admitted_at) - target)) {
2395
- nearest = candidate;
2396
- }
2397
- }
2398
- if (nearest)
2399
- selected.add(requireStoredMessageSequence(nearest.sequence, sessionId));
2400
- }
2401
- }
2402
- const firstIndexedSequence = promptRows.length > 0
2403
- ? requireStoredMessageSequence(promptRows[0]?.sequence, sessionId)
2404
- : null;
2405
- const legacyThrough = firstIndexedSequence === null ? throughSequence : firstIndexedSequence - 1;
2406
- if (legacyThrough >= 0) {
2407
- const firstRow = this.db
2408
- .prepare(`
2409
- SELECT sequence AS first_sequence
2410
- FROM session_messages
2411
- WHERE session_id = ?
2412
- ORDER BY sequence ASC
2413
- LIMIT 1
2414
- `)
2415
- .get(sessionId);
2416
- const firstSequence = nullableStoredMessageSequence(firstRow?.first_sequence, sessionId);
2417
- if (firstSequence === null || firstSequence > legacyThrough) {
2418
- throw new StoredSessionMessageIncompatibleError(sessionId, legacyThrough);
2419
- }
2420
- const forwardLegacy = this.db.prepare(`
2421
- SELECT message.sequence, message.message_type, payload.sequence AS payload_sequence
2422
- FROM session_messages AS message
2423
- LEFT JOIN session_message_payloads AS payload
2424
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
2425
- WHERE message.session_id = ? AND message.sequence >= ? AND message.sequence <= ?
2426
- ORDER BY message.sequence ASC
2427
- LIMIT ${SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES}
2428
- `);
2429
- const backwardLegacy = this.db.prepare(`
2430
- SELECT message.sequence, message.message_type, payload.sequence AS payload_sequence
2431
- FROM session_messages AS message
2432
- LEFT JOIN session_message_payloads AS payload
2433
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
2434
- WHERE message.session_id = ? AND message.sequence < ? AND message.sequence >= ?
2435
- ORDER BY message.sequence DESC
2436
- LIMIT ${SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES}
2437
- `);
2438
- const targetCount = Math.min(maxLandmarks, legacyThrough - firstSequence + 1);
2439
- for (let index = 0; index < targetCount; index += 1) {
2440
- const target = targetCount === 1
2441
- ? legacyThrough
2442
- : firstSequence +
2443
- Math.floor(((legacyThrough - firstSequence) * index) / (targetCount - 1));
2444
- const candidates = [
2445
- ...forwardLegacy.all(sessionId, target, legacyThrough),
2446
- ...backwardLegacy.all(sessionId, target, firstSequence),
2447
- ];
2448
- let nearest;
2449
- for (const candidate of candidates) {
2450
- const sequence = requireStoredMessageSequence(candidate.sequence, sessionId);
2451
- if (candidate.message_type !== 'user' || candidate.payload_sequence !== null)
2452
- continue;
2453
- if (nearest === undefined || Math.abs(sequence - target) < Math.abs(nearest - target)) {
2454
- nearest = sequence;
2455
- }
2456
- }
2457
- if (nearest !== undefined)
2458
- selected.add(nearest);
2459
- }
2460
- }
2461
- const selectedSequences = [...selected].sort((left, right) => left - right);
2462
- const sampledSequences = selectedSequences.length <= maxLandmarks
2463
- ? selectedSequences
2464
- : Array.from({ length: maxLandmarks }, (_, index) => selectedSequences[maxLandmarks === 1
2465
- ? selectedSequences.length - 1
2466
- : Math.floor(((selectedSequences.length - 1) * index) / (maxLandmarks - 1))]);
2467
- const landmarks = readStoredMessageRows(this.db, sessionId, sampledSequences).flatMap(({ sequence, recordJson }) => {
2468
- let message;
2469
- try {
2470
- message = decodeStoredMessage(JSON.parse(recordJson));
2471
- }
2472
- catch (error) {
2473
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence, { cause: error });
2474
- }
2475
- if (message.type !== 'user') {
2476
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
2477
- }
2478
- const label = (message.displayText ?? message.text).trim();
2479
- return label ? [{ turnId: message.turnId, sequence, label }] : [];
2480
- });
2481
- return { throughSequence, landmarks };
2482
- });
2483
- }
2484
- async readMessagesForRecovery(sessionId) {
2485
- return this.readMessagesWith(sessionId, decodeStoredMessage);
2486
- }
2487
- async readPreviewMessages(sessionId, limit = 10) {
2488
- this.assertOpen();
2489
- assertSafeSessionId(sessionId);
2490
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 128) {
2491
- throw new Error('Session message preview limit must be between 1 and 128');
2492
- }
2493
- if (!this.readRecordSync(sessionId))
2494
- throw new SessionNotFoundError(sessionId);
2495
- const sequences = this.db
2496
- .prepare(`
2497
- SELECT sequence FROM session_messages
2498
- WHERE session_id = ? ORDER BY sequence DESC LIMIT ?
2499
- `)
2500
- .all(sessionId, limit)
2501
- .map((row) => requireStoredMessageSequence(row.sequence, sessionId))
2502
- .reverse();
2503
- return readStoredMessageRows(this.db, sessionId, sequences, sequences.map(() => '?').join(', ')).map((row) => decodeStoredMessageRow({ sequence: row.sequence, record_json: row.recordJson }, sessionId));
2504
- }
2505
1978
  async beginCatalogProjectionWrite() {
2506
1979
  this.assertOpen();
2507
1980
  this.transaction(() => {
@@ -4022,64 +3495,6 @@ export class SqliteSessionMetadataStore {
4022
3495
  .get(sessionId, messageId);
4023
3496
  return row ? decodeStoredMessageRecordRow(this.db, sessionId, row) : undefined;
4024
3497
  }
4025
- readSessionMessageOrderingSync(sessionId) {
4026
- const rows = this.db
4027
- .prepare(`
4028
- SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
4029
- FROM session_messages AS message
4030
- LEFT JOIN session_message_payloads AS payload
4031
- ON payload.session_id = message.session_id AND payload.sequence = message.sequence
4032
- WHERE message.session_id = ?
4033
- ORDER BY message.sequence
4034
- `)
4035
- .all(sessionId);
4036
- return rows.map((row) => {
4037
- const sequence = requireStoredMessageSequence(row.sequence, sessionId);
4038
- const recordJson = readStoredMessageRecordJson(this.db, sessionId, sequence, row);
4039
- return {
4040
- sequence,
4041
- message: decodeStoredMessage(JSON.parse(recordJson)),
4042
- };
4043
- });
4044
- }
4045
- shiftSessionMessageSuffixSync(sessionId, firstSequence, amount) {
4046
- if (!Number.isSafeInteger(firstSequence) || firstSequence < 0) {
4047
- throw new SessionMetadataConflictError('Invalid transcript insertion sequence');
4048
- }
4049
- if (!Number.isSafeInteger(amount) || amount < 1) {
4050
- throw new SessionMetadataConflictError('Invalid transcript insertion size');
4051
- }
4052
- const sequences = this.db
4053
- .prepare(`
4054
- SELECT sequence
4055
- FROM session_messages
4056
- WHERE session_id = ? AND sequence >= ?
4057
- ORDER BY sequence DESC
4058
- `)
4059
- .all(sessionId, firstSequence).map((row) => requireStoredMessageSequence(row.sequence, sessionId));
4060
- const highest = sequences[0];
4061
- if (highest !== undefined && highest > Number.MAX_SAFE_INTEGER - amount) {
4062
- throw new SessionMetadataConflictError('Session message sequence overflow');
4063
- }
4064
- if (sequences.length === 0)
4065
- return;
4066
- this.db.exec('PRAGMA defer_foreign_keys = ON');
4067
- const moveChunks = this.db.prepare('UPDATE session_message_chunks SET sequence = ? WHERE session_id = ? AND sequence = ?');
4068
- const movePayload = this.db.prepare('UPDATE session_message_payloads SET sequence = ? WHERE session_id = ? AND sequence = ?');
4069
- const moveMessage = this.db.prepare('UPDATE session_messages SET sequence = ? WHERE session_id = ? AND sequence = ?');
4070
- for (const sequence of sequences) {
4071
- const shifted = sequence + amount;
4072
- moveChunks.run(shifted, sessionId, sequence);
4073
- const payload = movePayload.run(shifted, sessionId, sequence);
4074
- if (payload.changes !== 0 && payload.changes !== 1) {
4075
- throw new SessionMetadataConflictError('Message payload sequence is ambiguous');
4076
- }
4077
- const message = moveMessage.run(shifted, sessionId, sequence);
4078
- if (message.changes !== 1) {
4079
- throw new SessionMetadataConflictError('Message transcript sequence changed during repair');
4080
- }
4081
- }
4082
- }
4083
3498
  insertSessionMessagesSync(sessionId, firstSequence, entries) {
4084
3499
  if (!Number.isSafeInteger(firstSequence) ||
4085
3500
  firstSequence < 0 ||
@@ -4180,12 +3595,19 @@ export class SqliteSessionMetadataStore {
4180
3595
  if (!current)
4181
3596
  throw new SessionNotFoundError(sessionId);
4182
3597
  const lastMessageAt = maxTimestamp(current.header.lastMessageAt, projection.lastMessageAt);
3598
+ // The preview refuses to move backwards for the same reason the timestamp
3599
+ // does: a message older than the one on show is a repair of something the
3600
+ // catalog already passed, and recovery replays exactly those.
3601
+ const stale = !replacePreview &&
3602
+ projection.lastMessageAt !== undefined &&
3603
+ current.header.lastMessageAt !== undefined &&
3604
+ projection.lastMessageAt < current.header.lastMessageAt;
4183
3605
  this.updateHeaderSync(sessionId, {
4184
3606
  ...(lockConnection ? { connectionLocked: true } : {}),
4185
3607
  ...(lastMessageAt === undefined ? {} : { lastMessageAt }),
4186
3608
  }, {
4187
3609
  skipNoop: true,
4188
- ...(replacePreview || projection.lastMessagePreview !== undefined
3610
+ ...(!stale && (replacePreview || projection.lastMessagePreview !== undefined)
4189
3611
  ? {
4190
3612
  catalogPreview: {
4191
3613
  kind: 'replace',
@@ -5105,22 +4527,6 @@ function decodeStoredMessageRow(row, sessionId) {
5105
4527
  });
5106
4528
  }
5107
4529
  }
5108
- function requireTurnLandmarkAdmittedAt(value) {
5109
- if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
5110
- throw new Error('Invalid root Turn admission timestamp');
5111
- }
5112
- return value;
5113
- }
5114
- function storedMessageRecordBytes(row, sessionId, sequence) {
5115
- if (row.record_bytes !== null) {
5116
- return requireTranscriptRecordByteLength(row.record_bytes, sessionId, sequence);
5117
- }
5118
- if (typeof row.record_json !== 'string' ||
5119
- row.record_json === SQLITE_SESSION_MESSAGE_CHUNK_MARKER) {
5120
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
5121
- }
5122
- return Buffer.byteLength(row.record_json, 'utf8');
5123
- }
5124
4530
  function decodeStoredMessageRecordRow(db, sessionId, row) {
5125
4531
  const sequence = requireStoredMessageSequence(row.sequence, sessionId);
5126
4532
  return decodeStoredMessageRow({
@@ -5183,31 +4589,6 @@ function sameWorkHubAssignmentRequest(existing, requested) {
5183
4589
  replacesDelegationId: requested.replacesDelegationId,
5184
4590
  });
5185
4591
  }
5186
- function foldTurnContribution(current, turnId, sequence, message) {
5187
- const contribution = current ?? {
5188
- turnId,
5189
- firstSequence: sequence,
5190
- latestState: null,
5191
- userPromptPreview: null,
5192
- hasAssistantMessage: false,
5193
- hasAssistantOutput: false,
5194
- hasToolResult: false,
5195
- hasFailedToolResult: false,
5196
- hasAbortNote: false,
5197
- };
5198
- const userPrompt = message.type === 'user' ? (message.displayText ?? message.text).trim() : '';
5199
- return {
5200
- ...contribution,
5201
- latestState: message.type === 'turn_state' ? { sequence, message } : contribution.latestState,
5202
- userPromptPreview: contribution.userPromptPreview ?? (userPrompt || null),
5203
- hasAssistantMessage: contribution.hasAssistantMessage || message.type === 'assistant',
5204
- hasAssistantOutput: contribution.hasAssistantOutput ||
5205
- (message.type === 'assistant' && message.text.trim().length > 0),
5206
- hasToolResult: contribution.hasToolResult || message.type === 'tool_result',
5207
- hasFailedToolResult: contribution.hasFailedToolResult || (message.type === 'tool_result' && message.isError),
5208
- hasAbortNote: contribution.hasAbortNote || (message.type === 'system_note' && message.kind === 'abort'),
5209
- };
5210
- }
5211
4592
  function readStoredMessageRows(db, sessionId, sequences, placeholders = sequences.map(() => '?').join(', ')) {
5212
4593
  if (sequences.length === 0)
5213
4594
  return [];
@@ -5274,173 +4655,9 @@ function nullableStoredMessageSequence(value, sessionId) {
5274
4655
  return null;
5275
4656
  return requireStoredMessageSequence(value, sessionId);
5276
4657
  }
5277
- function requireTranscriptPayloadDigest(value, sessionId, sequence) {
5278
- if (typeof value !== 'string' || !/^[0-9a-f]{64}$/.test(value)) {
5279
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
5280
- }
5281
- return `sha256:${value}`;
5282
- }
5283
4658
  function requireTranscriptRecordByteLength(value, sessionId, sequence) {
5284
4659
  if (!Number.isSafeInteger(value) || value < 1) {
5285
4660
  throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
5286
4661
  }
5287
4662
  return value;
5288
4663
  }
5289
- function readTranscriptSlices(db, sessionId, slices) {
5290
- if (slices.length === 0)
5291
- return new Map();
5292
- const chunkedSlices = slices.filter((slice) => slice.chunked);
5293
- const values = chunkedSlices.map(() => '(?, ?, ?)').join(', ');
5294
- const parameters = chunkedSlices.flatMap((slice) => [
5295
- slice.sequence,
5296
- Math.floor(slice.byteOffset / SQLITE_SESSION_MESSAGE_CHUNK_BYTES),
5297
- Math.floor((slice.byteOffset + slice.byteLength - 1) / SQLITE_SESSION_MESSAGE_CHUNK_BYTES),
5298
- ]);
5299
- const rows = chunkedSlices.length === 0
5300
- ? []
5301
- : db
5302
- .prepare(`
5303
- WITH requested(sequence, first_chunk, last_chunk) AS (VALUES ${values})
5304
- SELECT requested.sequence, chunk.chunk_index, chunk.data, chunk.sha256
5305
- FROM requested
5306
- INNER JOIN session_message_chunks AS chunk
5307
- ON chunk.session_id = ?
5308
- AND chunk.sequence = requested.sequence
5309
- AND chunk.chunk_index BETWEEN requested.first_chunk AND requested.last_chunk
5310
- ORDER BY requested.sequence, chunk.chunk_index
5311
- `)
5312
- .all(...parameters, sessionId);
5313
- const rowsBySequence = new Map();
5314
- for (const row of rows) {
5315
- const sequence = requireStoredMessageSequence(row.sequence, sessionId);
5316
- const grouped = rowsBySequence.get(sequence);
5317
- if (grouped)
5318
- grouped.push(row);
5319
- else
5320
- rowsBySequence.set(sequence, [row]);
5321
- }
5322
- const result = new Map();
5323
- for (const slice of slices) {
5324
- if (!slice.chunked)
5325
- continue;
5326
- const selected = rowsBySequence.get(slice.sequence) ?? [];
5327
- const firstChunk = Math.floor(slice.byteOffset / SQLITE_SESSION_MESSAGE_CHUNK_BYTES);
5328
- const lastChunk = Math.floor((slice.byteOffset + slice.byteLength - 1) / SQLITE_SESSION_MESSAGE_CHUNK_BYTES);
5329
- if (selected.length !== lastChunk - firstChunk + 1) {
5330
- throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
5331
- }
5332
- const chunks = [];
5333
- for (let index = 0; index < selected.length; index += 1) {
5334
- const row = selected[index];
5335
- if (row.chunk_index !== firstChunk + index ||
5336
- !(row.data instanceof Uint8Array) ||
5337
- typeof row.sha256 !== 'string') {
5338
- throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
5339
- }
5340
- const chunk = Buffer.from(row.data);
5341
- if (createHash('sha256').update(chunk).digest('hex') !== row.sha256) {
5342
- throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
5343
- }
5344
- chunks.push(chunk);
5345
- }
5346
- const joined = Buffer.concat(chunks);
5347
- const start = slice.byteOffset - firstChunk * SQLITE_SESSION_MESSAGE_CHUNK_BYTES;
5348
- const data = joined.subarray(start, start + slice.byteLength);
5349
- if (data.byteLength !== slice.byteLength) {
5350
- throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
5351
- }
5352
- result.set(slice.sequence, data);
5353
- }
5354
- const inlineSlices = slices.filter((slice) => !slice.chunked);
5355
- if (inlineSlices.length > 0) {
5356
- const inlineValues = inlineSlices.map(() => '(?, ?, ?)').join(', ');
5357
- const inlineParameters = inlineSlices.flatMap((slice) => [
5358
- slice.sequence,
5359
- slice.byteOffset + 1,
5360
- slice.byteLength,
5361
- ]);
5362
- const inlineRows = db
5363
- .prepare(`
5364
- WITH requested(sequence, byte_start, byte_length) AS (VALUES ${inlineValues})
5365
- SELECT requested.sequence,
5366
- substr(CAST(message.record_json AS BLOB), requested.byte_start, requested.byte_length)
5367
- AS data
5368
- FROM requested
5369
- INNER JOIN session_messages AS message
5370
- ON message.session_id = ? AND message.sequence = requested.sequence
5371
- `)
5372
- .all(...inlineParameters, sessionId);
5373
- for (const row of inlineRows) {
5374
- const sequence = requireStoredMessageSequence(row.sequence, sessionId);
5375
- if (!(row.data instanceof Uint8Array)) {
5376
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
5377
- }
5378
- result.set(sequence, Buffer.from(row.data));
5379
- }
5380
- }
5381
- return result;
5382
- }
5383
- function validateTranscriptRecord(data, sessionId, sequence) {
5384
- try {
5385
- decodeStoredMessage(markPersisted(JSON.parse(typeof data === 'string' ? data : data.toString('utf8'))));
5386
- }
5387
- catch (error) {
5388
- throw new StoredSessionMessageIncompatibleError(sessionId, sequence, {
5389
- cause: error,
5390
- });
5391
- }
5392
- }
5393
- function assertTranscriptPageRequest(request) {
5394
- if (request.direction !== 'older' && request.direction !== 'newer') {
5395
- throw new Error('Invalid Session transcript page direction');
5396
- }
5397
- if (request.throughSequence !== undefined &&
5398
- request.throughSequence !== null &&
5399
- (!Number.isSafeInteger(request.throughSequence) || request.throughSequence < 0)) {
5400
- throw new Error('Invalid Session transcript watermark');
5401
- }
5402
- if (request.position !== undefined &&
5403
- (!Number.isSafeInteger(request.position) || request.position < 0)) {
5404
- throw new Error('Invalid Session transcript position');
5405
- }
5406
- if (request.byteOffset !== undefined &&
5407
- (request.position === undefined ||
5408
- !Number.isSafeInteger(request.byteOffset) ||
5409
- request.byteOffset < 0)) {
5410
- throw new Error('Invalid Session transcript byte offset');
5411
- }
5412
- if (!Number.isSafeInteger(request.maxBytes) ||
5413
- request.maxBytes < 1 ||
5414
- request.maxBytes > 1024 * 1024) {
5415
- throw new Error('Session transcript page byte limit must be between 1 and 1048576');
5416
- }
5417
- if (!Number.isSafeInteger(request.maxMessages) ||
5418
- request.maxMessages < 1 ||
5419
- request.maxMessages > 256) {
5420
- throw new Error('Session transcript page message limit must be between 1 and 256');
5421
- }
5422
- }
5423
- function assertTranscriptRecordScanRequest(request) {
5424
- if (request.direction !== 'older' && request.direction !== 'newer') {
5425
- throw new Error('Invalid Session transcript record direction');
5426
- }
5427
- if (request.throughSequence !== undefined &&
5428
- request.throughSequence !== null &&
5429
- (!Number.isSafeInteger(request.throughSequence) || request.throughSequence < 0)) {
5430
- throw new Error('Invalid Session transcript watermark');
5431
- }
5432
- if (request.position !== undefined &&
5433
- (!Number.isSafeInteger(request.position) || request.position < 0)) {
5434
- throw new Error('Invalid Session transcript position');
5435
- }
5436
- if (!Number.isSafeInteger(request.maxStoredBytes) ||
5437
- request.maxStoredBytes < 1 ||
5438
- request.maxStoredBytes > 16 * 1024 * 1024) {
5439
- throw new Error('Invalid Session transcript record byte limit');
5440
- }
5441
- if (!Number.isSafeInteger(request.maxMessages) ||
5442
- request.maxMessages < 1 ||
5443
- request.maxMessages > 256) {
5444
- throw new Error('Invalid Session transcript record count limit');
5445
- }
5446
- }