maka-agent 0.2.0-dev.46.20260922 → 0.2.0-dev.48.20260923

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 (131) hide show
  1. package/DISCLAIMER-WIP +6 -1
  2. package/README.md +6 -1
  3. package/dist/acp/maka-acp-agent.js +1 -1
  4. package/dist/acp/session-mcp.js +212 -0
  5. package/dist/acp/session-registry.js +52 -8
  6. package/dist/acp/stdio-server.js +3 -0
  7. package/dist/cli-core.js +1 -1
  8. package/dist/mcp-capability-provider.js +6 -1
  9. package/dist/mcp-capability-publication.js +139 -0
  10. package/dist/pi-tui-layout.js +46 -17
  11. package/dist/pi-tui-pickers.js +25 -14
  12. package/dist/pi-tui-runner.js +24 -15
  13. package/dist/runtime-host-capability-provider-command.js +25 -6
  14. package/dist/runtime-host-service-manager.js +26 -2
  15. package/dist/tui-context-refresh.js +15 -26
  16. package/dist/tui-mcp-control.js +66 -112
  17. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  18. package/node_modules/@maka/acp-executor-plugin/dist/acp-errors.js +26 -0
  19. package/node_modules/@maka/acp-executor-plugin/dist/acp-filesystem.js +103 -0
  20. package/node_modules/@maka/acp-executor-plugin/dist/acp-process.js +119 -0
  21. package/node_modules/@maka/acp-executor-plugin/dist/acp-projection.js +146 -0
  22. package/node_modules/@maka/acp-executor-plugin/dist/index.js +653 -0
  23. package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +24446 -0
  24. package/node_modules/@maka/acp-executor-plugin/package.json +15 -0
  25. package/node_modules/@maka/antigravity-acp-plugin/dist/index.js +88 -0
  26. package/node_modules/@maka/antigravity-acp-plugin/dist/model-presentation.js +53 -0
  27. package/node_modules/@maka/antigravity-acp-plugin/dist/plugin.mjs +115 -0
  28. package/node_modules/@maka/antigravity-acp-plugin/package.json +15 -0
  29. package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
  30. package/node_modules/@maka/core/dist/executor-catalog.js +106 -0
  31. package/node_modules/@maka/core/dist/mcp.js +4 -1
  32. package/node_modules/@maka/core/dist/model-metadata.js +28 -1
  33. package/node_modules/@maka/core/dist/model-thinking.js +36 -1
  34. package/node_modules/@maka/core/dist/provider-registry.js +0 -1
  35. package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
  36. package/node_modules/@maka/core/dist/runtime-event-store.js +25 -0
  37. package/node_modules/@maka/core/dist/runtime-invocation.js +5 -0
  38. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
  39. package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +6 -0
  40. package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +21 -0
  41. package/node_modules/@maka/core/dist/session.js +19 -1
  42. package/node_modules/@maka/core/dist/settings.js +3 -0
  43. package/node_modules/@maka/core/dist/tool-ledger-scanner.js +365 -104
  44. package/node_modules/@maka/core/dist/turn-origin.js +14 -0
  45. package/node_modules/@maka/core/package.json +3 -0
  46. package/node_modules/@maka/mcp/dist/credential-coordinator.js +6 -1
  47. package/node_modules/@maka/mcp/dist/index.js +31 -5
  48. package/node_modules/@maka/mcp/dist/oauth.js +54 -6
  49. package/node_modules/@maka/runtime/dist/agent-run-inspect.js +1 -1
  50. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
  51. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
  52. package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
  53. package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
  54. package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
  55. package/node_modules/@maka/runtime/dist/conversation-copy.js +56 -1
  56. package/node_modules/@maka/runtime/dist/mcp-tools.js +9 -2
  57. package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
  58. package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
  59. package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
  60. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +122 -3
  61. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +212 -16
  62. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +2 -2
  63. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
  64. package/node_modules/@maka/runtime/dist/session-manager.js +184 -126
  65. package/node_modules/@maka/runtime/dist/shell-run-manager.js +3 -0
  66. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +29 -4
  67. package/node_modules/@maka/runtime-host/dist/candidate-entry.js +8 -5
  68. package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
  69. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
  70. package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
  71. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  72. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +8 -1
  73. package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
  74. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -1
  75. package/node_modules/@maka/runtime-host/dist/composition-identity.js +19 -0
  76. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
  77. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
  78. package/node_modules/@maka/runtime-host/dist/protocol/index.js +20 -2
  79. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +34 -0
  80. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +75 -5
  81. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
  82. package/node_modules/@maka/runtime-host/dist/server/builtin-external-agent-plugins.js +188 -0
  83. package/node_modules/@maka/runtime-host/dist/server/candidate.js +12 -7
  84. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
  85. package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
  86. package/node_modules/@maka/runtime-host/dist/server/client-capability-recovery.js +44 -46
  87. package/node_modules/@maka/runtime-host/dist/server/context-coordinator.js +2 -0
  88. package/node_modules/@maka/runtime-host/dist/server/execution-composition-factory.js +1 -1
  89. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +188 -21
  90. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +6 -0
  91. package/node_modules/@maka/runtime-host/dist/server/extension-bundle.js +4 -0
  92. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
  93. package/node_modules/@maka/runtime-host/dist/server/host-composition-source.js +47 -0
  94. package/node_modules/@maka/runtime-host/dist/server/host-composition.js +1 -29
  95. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +2 -9
  96. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +75 -9
  97. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +5 -1
  98. package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +45 -0
  99. package/node_modules/@maka/runtime-host/dist/server/jev-routing-model.js +157 -0
  100. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -2
  101. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +4 -0
  102. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +11 -1
  103. package/node_modules/@maka/runtime-host/dist/server/process-lifecycle.js +1 -1
  104. package/node_modules/@maka/runtime-host/dist/server/process-termination-error.js +27 -0
  105. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +91 -12
  106. package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +2 -0
  107. package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
  108. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +128 -13
  109. package/node_modules/@maka/runtime-host/dist/server/session-effect-coordinator.js +12 -6
  110. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
  111. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +33 -0
  112. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +4 -0
  113. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +173 -0
  114. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +293 -0
  115. package/node_modules/@maka/runtime-host/package.json +2 -0
  116. package/node_modules/@maka/storage/dist/agent-run-store-contract.js +8 -0
  117. package/node_modules/@maka/storage/dist/execution-stores.js +15 -3
  118. package/node_modules/@maka/storage/dist/mcp-config-store.js +91 -2
  119. package/node_modules/@maka/storage/dist/operational-state-store.js +15 -1
  120. package/node_modules/@maka/storage/dist/plan-authority.js +2 -0
  121. package/node_modules/@maka/storage/dist/plan-store.js +30 -0
  122. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  123. package/node_modules/@maka/storage/dist/runtime-policy/credential-vault-document.js +4 -1
  124. package/node_modules/@maka/storage/dist/runtime-policy/policy-document.js +2 -0
  125. package/node_modules/@maka/storage/dist/session-store-values.js +16 -4
  126. package/node_modules/@maka/storage/dist/shell-run-authority.js +1 -0
  127. package/node_modules/@maka/storage/dist/shell-run-store.js +15 -0
  128. package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +74 -1
  129. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +724 -84
  130. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +16 -0
  131. package/package.json +1 -1
@@ -24,11 +24,12 @@ import { assertHandoffClaimSource } from '@maka/core/runtime-handoff';
24
24
  import { buildWorkspaceBaselineAuthorityEvents, buildWorkspaceSuccessorAuthorityEvent, scanWorkspaceBaselineAuthority, WORKSPACE_AUTHORITY_SESSION_ID, WORKSPACE_VERSION_AUTHORITY_CAPABILITY_V1, } from '@maka/core/workspace-version-authority';
25
25
  import { decodeRuntimeEvent, decodeRuntimeInvocationOpened, isPartialRuntimeEvent, isTerminalRuntimeEvent, runtimeEventInvocationOpening, } from '@maka/core/runtime-event';
26
26
  import { RunSealedError, RUNTIME_CONTINUATION_AUTHORITY_V1, TOOL_RECOVERY_BUNDLE_CAPABILITY_V1, } from '@maka/core/runtime-event-store';
27
+ import { mayBeTranscriptLedgerInvocationId, } from '@maka/core/runtime-invocation';
27
28
  import { stableJsonStringify } from '@maka/core/tool-args-identity';
28
29
  import { mergeRuntimePartialSnapshots, groupRuntimePartialSnapshots, compareRuntimePartialSnapshots, partialRuntimeStream, completedPartialRuntimeStreamKey, } from './runtime-partial-values.js';
29
30
  import { assertPreparedInput, assertOutcomeInput, assertPreparedIdentity, assertOutcomeIdentity, } from './tool-commit-validation.js';
30
31
  import { encodeCanonicalRuntimeEvent } from '@maka/core/canonical-runtime-event';
31
- import { scanToolLedger, ToolLedgerCorruptionError, ToolLedgerRejectionError, validateGenericToolLedgerAppend, validateToolLedgerTransition, } from '@maka/core/tool-ledger-scanner';
32
+ import { scanToolLedger, referencedToolOperationIds, ToolLedgerReducer, ToolLedgerCorruptionError, ToolLedgerRejectionError, validateGenericToolLedgerAppend, validateIncrementalToolLedgerTransition, } from '@maka/core/tool-ledger-scanner';
32
33
  import { buildImmutableRuntimePrefix, buildImmutableRuntimePrefixProof, continuationStartEventMatchesClaim, decodeContinuationClaim, } from '@maka/core/runtime-boundary';
33
34
  import { assertToolRecoveryEventBundle, interpretScannedToolRecovery, } from '@maka/core/tool-recovery-bundle';
34
35
  import { configureSqliteRuntimeDatabase, migrateSqliteRuntimeDatabase, readUserVersion, RUNTIME_RECOVERY_AUTHORITY_CAPABILITY, RUNTIME_RECOVERY_AUTHORITY_CAPABILITY_VERSION, RUNTIME_CONTINUATION_AUTHORITY_CAPABILITY, RUNTIME_CONTINUATION_AUTHORITY_CAPABILITY_VERSION, RUNTIME_WORKSPACE_VERSION_AUTHORITY_CAPABILITY, RUNTIME_WORKSPACE_VERSION_AUTHORITY_CAPABILITY_VERSION, runtimeEventKind, SQLITE_RUNTIME_SCHEMA_VERSION, } from './sqlite-runtime-schema.js';
@@ -41,6 +42,24 @@ import { rebuildTranscriptTurnExtents, recordTranscriptTurnExtent, RuntimeTransc
41
42
  export { SQLITE_RUNTIME_SCHEMA_VERSION } from './sqlite-runtime-schema.js';
42
43
  const RUNTIME_EVENT_SCAN_BATCH_SIZE = 128;
43
44
  const RUNTIME_PARTIAL_SEGMENT_TARGET_BYTES = 64 * 1024;
45
+ const RECOVERY_MESSAGE_QUERY_BATCH_SIZE = 400;
46
+ const TOOL_LEDGER_CACHE_EVENT_OVERHEAD_BYTES = 1024;
47
+ const DEFAULT_TOOL_LEDGER_CACHE_BUDGET = {
48
+ maxEntries: 32,
49
+ maxEstimatedBytes: 16 * 1024 * 1024,
50
+ maxSingleEntryEstimatedBytes: 8 * 1024 * 1024,
51
+ };
52
+ const IMMUTABLE_STEERING_SQL_PREDICATE = `
53
+ event_kind = 'text'
54
+ AND CASE
55
+ WHEN json_valid(payload_json)
56
+ THEN json_extract(payload_json, '$.partial')
57
+ END = 0
58
+ AND CASE
59
+ WHEN json_valid(payload_json)
60
+ THEN json_extract(payload_json, '$.content.steering')
61
+ END = 1
62
+ `;
44
63
  function assertRuntimeEventScanBudget(budget) {
45
64
  for (const [name, value] of Object.entries(budget)) {
46
65
  if (!Number.isSafeInteger(value) || value < 1) {
@@ -61,6 +80,19 @@ function requireRuntimeEventScanCount(value) {
61
80
  }
62
81
  return value;
63
82
  }
83
+ function normalizeToolLedgerCacheBudget(override) {
84
+ const budget = { ...DEFAULT_TOOL_LEDGER_CACHE_BUDGET, ...override };
85
+ for (const [name, value] of Object.entries(budget)) {
86
+ if (!Number.isSafeInteger(value) || value < 1) {
87
+ throw new Error(`Invalid tool ledger cache ${name}`);
88
+ }
89
+ }
90
+ return budget;
91
+ }
92
+ function estimateToolLedgerCacheBytes(storedPayloadBytes, eventCount) {
93
+ const estimate = storedPayloadBytes * 2 + eventCount * TOOL_LEDGER_CACHE_EVENT_OVERHEAD_BYTES;
94
+ return Number.isSafeInteger(estimate) ? estimate : Number.MAX_SAFE_INTEGER;
95
+ }
64
96
  const require = createRequire(import.meta.url);
65
97
  function loadDatabaseSync() {
66
98
  return require('node:sqlite').DatabaseSync;
@@ -82,12 +114,26 @@ export class SqliteRuntimeStore {
82
114
  workspaceVersionAuthorityCapability = WORKSPACE_VERSION_AUTHORITY_CAPABILITY_V1;
83
115
  db;
84
116
  databaseLease;
85
- toolLedgerHealth;
117
+ toolLedgerCacheBudget;
118
+ toolLedgerCache = new Map();
119
+ toolLedgerCacheEstimatedBytes = 0;
120
+ toolLedgerCacheEventCount = 0;
121
+ toolLedgerCacheMetrics = {
122
+ hits: 0,
123
+ misses: 0,
124
+ budgetEvictions: 0,
125
+ terminalEvictions: 0,
126
+ transientEntries: 0,
127
+ };
128
+ uncommittedToolLedgerStates = new Map();
129
+ toolLedgerCacheVersion;
130
+ localCommittedWriteRevision = 0;
86
131
  closed = false;
87
132
  uncommittedEventSessions = new Set();
88
133
  eventCommitListeners = new Set();
89
134
  constructor(path, options = {}) {
90
135
  this.options = options;
136
+ this.toolLedgerCacheBudget = normalizeToolLedgerCacheBudget(options.toolLedgerCacheBudget);
91
137
  if (options.readOnly && options.databaseLease) {
92
138
  throw new Error('Operational state database leases cannot be opened read-only');
93
139
  }
@@ -102,7 +148,6 @@ export class SqliteRuntimeStore {
102
148
  registerRecallFoldFunction(this.db);
103
149
  if (!options.readOnly) {
104
150
  this.registerWorkspaceBaselineAuthorityWriter();
105
- this.refreshToolLedgerHealth();
106
151
  }
107
152
  return;
108
153
  }
@@ -128,7 +173,6 @@ export class SqliteRuntimeStore {
128
173
  registerRecallFoldFunction(this.db);
129
174
  if (!options.readOnly) {
130
175
  this.registerWorkspaceBaselineAuthorityWriter();
131
- this.refreshToolLedgerHealth();
132
176
  }
133
177
  }
134
178
  catch (error) {
@@ -152,6 +196,8 @@ export class SqliteRuntimeStore {
152
196
  if (this.closed)
153
197
  return;
154
198
  this.closed = true;
199
+ this.clearToolLedgerCache();
200
+ this.uncommittedToolLedgerStates.clear();
155
201
  if (this.databaseLease)
156
202
  this.databaseLease.close();
157
203
  else
@@ -195,6 +241,7 @@ export class SqliteRuntimeStore {
195
241
  existing.at(-1)?.id !== canonicalEvent.id) {
196
242
  throw new Error('Terminal RuntimeEvent must be the immutable ledger tail');
197
243
  }
244
+ this.invalidateToolLedgerCacheForInvocation(canonicalEvent.invocationId);
198
245
  return;
199
246
  }
200
247
  const existingTerminal = existing.find(isTerminalRuntimeEvent);
@@ -334,6 +381,202 @@ export class SqliteRuntimeStore {
334
381
  assertRuntimeStorageSafeId(sessionId, 'Invalid session id');
335
382
  return this.readTransaction(() => this.readInvocationOpeningsSync(sessionId, { direction: 'asc' }).map((row) => this.completeInvocationRecordSync(row)));
336
383
  }
384
+ async listInvocationRecoveryInventory(sessionIds) {
385
+ const selectedSessionIds = [...new Set(sessionIds)];
386
+ for (const sessionId of selectedSessionIds) {
387
+ assertRuntimeStorageSafeId(sessionId, 'Invalid session id');
388
+ }
389
+ if (selectedSessionIds.length === 0)
390
+ return [];
391
+ const selectedJson = JSON.stringify(selectedSessionIds);
392
+ return this.readTransaction(() => {
393
+ const eventRows = this.db
394
+ .prepare(`
395
+ WITH selected(session_id) AS (SELECT value FROM json_each(?))
396
+ SELECT
397
+ opening.rowid AS opening_rowid,
398
+ opening.session_id AS session_id,
399
+ opening.invocation_id AS invocation_id,
400
+ 1 AS from_events,
401
+ NULL AS run_id,
402
+ NULL AS turn_id,
403
+ NULL AS opened_at,
404
+ (
405
+ SELECT terminal.rowid
406
+ FROM runtime_events AS terminal INDEXED BY runtime_events_terminal
407
+ WHERE terminal.invocation_id = opening.invocation_id
408
+ AND ${TERMINAL_RUNTIME_EVENT_SQL.replaceAll('payload_json', 'terminal.payload_json')}
409
+ ORDER BY terminal.event_seq
410
+ LIMIT 1
411
+ ) AS terminal_rowid
412
+ FROM selected
413
+ JOIN runtime_events AS opening INDEXED BY runtime_events_by_session_kind
414
+ ON opening.session_id = selected.session_id
415
+ AND opening.event_kind = 'invocation_opened'
416
+ `)
417
+ .all(selectedJson);
418
+ const legacyRows = this.db
419
+ .prepare(`
420
+ WITH selected(session_id) AS (SELECT value FROM json_each(?))
421
+ SELECT
422
+ NULL AS opening_rowid,
423
+ legacy.session_id AS session_id,
424
+ legacy.invocation_id AS invocation_id,
425
+ legacy.run_id AS run_id,
426
+ legacy.turn_id AS turn_id,
427
+ legacy.opened_at AS opened_at,
428
+ 0 AS from_events,
429
+ (
430
+ SELECT terminal.rowid
431
+ FROM runtime_events AS terminal INDEXED BY runtime_events_terminal
432
+ WHERE terminal.invocation_id = legacy.invocation_id
433
+ AND ${TERMINAL_RUNTIME_EVENT_SQL.replaceAll('payload_json', 'terminal.payload_json')}
434
+ ORDER BY terminal.event_seq
435
+ LIMIT 1
436
+ ) AS terminal_rowid
437
+ FROM selected
438
+ JOIN runtime_legacy_invocation_openings AS legacy
439
+ ON legacy.session_id = selected.session_id
440
+ WHERE NOT EXISTS (
441
+ SELECT 1 FROM runtime_events AS opening
442
+ WHERE opening.invocation_id = legacy.invocation_id
443
+ AND opening.event_kind = 'invocation_opened'
444
+ )
445
+ `)
446
+ .all(selectedJson);
447
+ const handoffRows = this.db
448
+ .prepare(`
449
+ WITH selected(session_id) AS (SELECT value FROM json_each(?))
450
+ SELECT
451
+ terminal.rowid AS terminal_rowid,
452
+ terminal.event_id AS event_id,
453
+ terminal.session_id AS session_id,
454
+ terminal.invocation_id AS invocation_id,
455
+ terminal.run_id AS run_id,
456
+ terminal.turn_id AS turn_id,
457
+ terminal.payload_json AS payload_json
458
+ FROM selected
459
+ JOIN runtime_events AS terminal INDEXED BY runtime_events_by_session_kind
460
+ ON terminal.session_id = selected.session_id
461
+ AND terminal.event_kind = 'invocation_end'
462
+ WHERE CASE
463
+ WHEN json_valid(terminal.payload_json)
464
+ THEN json_type(terminal.payload_json, '$.actions.handoffPause')
465
+ END IS NOT NULL
466
+ `)
467
+ .all(selectedJson);
468
+ const handoffByRowId = new Map(handoffRows.map((row) => [row.terminal_rowid, row]));
469
+ const eventIdentity = this.db.prepare(`
470
+ SELECT event_id, session_id, invocation_id, run_id, turn_id, committed_at
471
+ FROM runtime_events
472
+ WHERE rowid = ?
473
+ `);
474
+ const eventCandidate = this.db.prepare(`
475
+ SELECT event_id, session_id, invocation_id, run_id, turn_id, committed_at, payload_json
476
+ FROM runtime_events
477
+ WHERE rowid = ?
478
+ `);
479
+ const legacyCandidate = this.db.prepare(`
480
+ SELECT opening_json
481
+ FROM runtime_legacy_invocation_openings
482
+ WHERE invocation_id = ?
483
+ `);
484
+ return [...eventRows, ...legacyRows]
485
+ .sort((a, b) => a.session_id.localeCompare(b.session_id) ||
486
+ a.invocation_id.localeCompare(b.invocation_id))
487
+ .map((row) => {
488
+ const base = { sessionId: row.session_id, invocationId: row.invocation_id };
489
+ const handoff = row.terminal_rowid === null ? undefined : handoffByRowId.get(row.terminal_rowid);
490
+ const needsCandidate = row.terminal_rowid === null || handoff !== undefined;
491
+ const needsIdentity = needsCandidate || mayBeTranscriptLedgerInvocationId(row.invocation_id);
492
+ if (!needsIdentity)
493
+ return base;
494
+ let identity;
495
+ let openingJson;
496
+ let openingEventId;
497
+ if (row.from_events === 1) {
498
+ if (row.opening_rowid === null) {
499
+ throw new Error(`Runtime invocation opening row is missing: ${row.invocation_id}`);
500
+ }
501
+ const detail = (needsCandidate ? eventCandidate : eventIdentity).get(row.opening_rowid);
502
+ if (!detail ||
503
+ detail.session_id !== row.session_id ||
504
+ detail.invocation_id !== row.invocation_id) {
505
+ throw new Error(`Runtime invocation opening identity changed: ${row.invocation_id}`);
506
+ }
507
+ identity = {
508
+ sessionId: detail.session_id,
509
+ invocationId: detail.invocation_id,
510
+ runId: detail.run_id,
511
+ turnId: detail.turn_id,
512
+ openedAt: detail.committed_at,
513
+ };
514
+ openingEventId = detail.event_id;
515
+ openingJson = detail.payload_json;
516
+ }
517
+ else {
518
+ if (row.run_id === null || row.turn_id === null || row.opened_at === null) {
519
+ throw new Error(`Legacy invocation opening identity is incomplete: ${row.invocation_id}`);
520
+ }
521
+ identity = {
522
+ sessionId: row.session_id,
523
+ invocationId: row.invocation_id,
524
+ runId: row.run_id,
525
+ turnId: row.turn_id,
526
+ openedAt: row.opened_at,
527
+ };
528
+ if (needsCandidate) {
529
+ openingJson = legacyCandidate.get(row.invocation_id)?.opening_json;
530
+ }
531
+ }
532
+ if (!needsCandidate)
533
+ return { ...base, identity };
534
+ if (typeof openingJson !== 'string') {
535
+ throw new Error(`Runtime invocation opening payload is missing: ${row.invocation_id}`);
536
+ }
537
+ let opening;
538
+ if (row.from_events === 1) {
539
+ const event = decodeRuntimeEventStorageRow({
540
+ event_id: openingEventId ?? '',
541
+ session_id: identity.sessionId,
542
+ invocation_id: identity.invocationId,
543
+ run_id: identity.runId,
544
+ turn_id: identity.turnId,
545
+ payload_json: openingJson,
546
+ });
547
+ const decoded = runtimeEventInvocationOpening(event);
548
+ if (!decoded) {
549
+ throw new Error(`RuntimeEvent ${event.id} is indexed as an opening fact but is not one`);
550
+ }
551
+ opening = decoded;
552
+ }
553
+ else {
554
+ opening = decodeRuntimeInvocationOpened(JSON.parse(openingJson));
555
+ }
556
+ let terminalEvent;
557
+ if (handoff) {
558
+ if (handoff.session_id !== identity.sessionId ||
559
+ handoff.invocation_id !== identity.invocationId ||
560
+ handoff.run_id !== identity.runId ||
561
+ handoff.turn_id !== identity.turnId) {
562
+ throw new Error(`Runtime handoff terminal identity changed: ${row.invocation_id}`);
563
+ }
564
+ terminalEvent = decodeRuntimeEventStorageRow(handoff);
565
+ if (!isTerminalRuntimeEvent(terminalEvent)) {
566
+ throw new Error(`Runtime handoff fact is not terminal: ${terminalEvent.id}`);
567
+ }
568
+ }
569
+ return {
570
+ ...base,
571
+ candidate: {
572
+ ...identity,
573
+ opening,
574
+ ...(terminalEvent ? { terminalEvent } : {}),
575
+ },
576
+ };
577
+ });
578
+ });
579
+ }
337
580
  async readRunInvocation(sessionId, runId) {
338
581
  assertRuntimeStorageSafeId(sessionId, 'Invalid session id');
339
582
  assertRuntimeStorageSafeId(runId, 'Invalid run id');
@@ -964,6 +1207,51 @@ export class SqliteRuntimeStore {
964
1207
  const row = this.readContinuationClaimRow('boundary_digest = ?', boundaryDigest);
965
1208
  return row ? this.decodeContinuationClaimStateRow(row) : undefined;
966
1209
  }
1210
+ async listUnsettledContinuationClaimsForRecovery(sessionIds) {
1211
+ const selectedSessionIds = [...new Set(sessionIds)];
1212
+ for (const sessionId of selectedSessionIds) {
1213
+ assertRuntimeStorageSafeId(sessionId, 'Invalid session id');
1214
+ }
1215
+ if (selectedSessionIds.length === 0)
1216
+ return [];
1217
+ const rows = this.db
1218
+ .prepare(`
1219
+ WITH selected(session_id) AS (SELECT value FROM json_each(?))
1220
+ SELECT
1221
+ claim.claim_id,
1222
+ claim.source_session_id,
1223
+ claim.source_invocation_id,
1224
+ claim.source_run_id,
1225
+ claim.source_turn_id,
1226
+ claim.source_event_high_water,
1227
+ claim.source_prefix_digest,
1228
+ claim.boundary_digest,
1229
+ claim.boundary_json,
1230
+ claim.provider_projection_version,
1231
+ claim.provider_replay_digest,
1232
+ claim.target_session_id,
1233
+ claim.target_invocation_id,
1234
+ claim.target_run_id,
1235
+ claim.target_turn_id,
1236
+ claim.target_opening_json,
1237
+ claim.claimed_at,
1238
+ claim.start_event_id,
1239
+ claim.start_kind,
1240
+ claim.protocol_version
1241
+ FROM selected
1242
+ JOIN runtime_continuation_claims AS claim
1243
+ ON claim.target_session_id = selected.session_id
1244
+ WHERE NOT EXISTS (
1245
+ SELECT 1
1246
+ FROM runtime_events AS terminal INDEXED BY runtime_events_terminal
1247
+ WHERE terminal.invocation_id = claim.target_invocation_id
1248
+ AND ${TERMINAL_RUNTIME_EVENT_SQL.replaceAll('payload_json', 'terminal.payload_json')}
1249
+ )
1250
+ ORDER BY claim.claimed_at ASC, claim.claim_id ASC
1251
+ `)
1252
+ .all(JSON.stringify(selectedSessionIds));
1253
+ return rows.map((row) => this.decodeContinuationClaimStateRow(row));
1254
+ }
967
1255
  async listContinuationClaimsForRecovery(sessionId) {
968
1256
  const rows = this.db
969
1257
  .prepare(`
@@ -1041,25 +1329,119 @@ export class SqliteRuntimeStore {
1041
1329
  async readImmutableSteeringMessageProof(sessionId, messageId) {
1042
1330
  assertRuntimeStorageSafeId(sessionId, 'Invalid session id');
1043
1331
  assertRuntimeStorageSafeId(messageId, 'Invalid message id');
1044
- const matches = this.readImmutableSessionRuntimeEvents(sessionId).filter((event) => immutableSteeringMessageId(event) === messageId);
1332
+ const matches = this.readImmutableSteeringMessageEvents(sessionId, messageId);
1045
1333
  if (matches.length > 1) {
1046
1334
  throw new Error(`Immutable steering message identity conflict: ${messageId}`);
1047
1335
  }
1048
1336
  return matches[0] ? Object.freeze({ event: matches[0] }) : undefined;
1049
1337
  }
1050
- async repairImmutableSteeringMessageProofsForRecovery(sessionId) {
1051
- assertRuntimeStorageSafeId(sessionId, 'Invalid session id');
1052
- const messages = new Map();
1053
- for (const event of this.readImmutableSessionRuntimeEvents(sessionId)) {
1054
- const messageId = immutableSteeringMessageId(event);
1055
- if (!messageId)
1056
- continue;
1057
- const existing = messages.get(messageId);
1058
- if (existing && !isDeepStrictEqual(existing, event)) {
1059
- throw new Error(`Immutable steering message identity conflict: ${messageId}`);
1338
+ async readRecoveryMessageEvents(input) {
1339
+ assertRuntimeStorageSafeId(input.sessionId, 'Invalid session id');
1340
+ assertEvidenceReadBudget(input.budget);
1341
+ const turnIds = distinctSafeRuntimeIds(input.turnIds, 'Invalid recovery Turn id');
1342
+ const eventIds = distinctSafeRuntimeIds(input.eventIds, 'Invalid recovery event id');
1343
+ const selectedEventIds = new Set();
1344
+ const measuredRows = [];
1345
+ const measureEventsById = (ids) => {
1346
+ for (const batch of batches(ids, RECOVERY_MESSAGE_QUERY_BATCH_SIZE)) {
1347
+ const placeholders = batch.map(() => '?').join(', ');
1348
+ const rows = this.db
1349
+ .prepare(`
1350
+ SELECT event_id, session_id,
1351
+ length(CAST(payload_json AS BLOB)) AS stored_bytes
1352
+ FROM runtime_events
1353
+ WHERE event_id IN (${placeholders})
1354
+ `)
1355
+ .all(...batch);
1356
+ for (const row of rows) {
1357
+ if (typeof row.event_id !== 'string' || typeof row.session_id !== 'string') {
1358
+ throw new Error('Invalid recovery RuntimeEvent measurement row');
1359
+ }
1360
+ if (row.session_id !== input.sessionId || selectedEventIds.has(row.event_id))
1361
+ continue;
1362
+ selectedEventIds.add(row.event_id);
1363
+ measuredRows.push({ event_id: row.event_id, stored_bytes: row.stored_bytes });
1364
+ if (measuredRows.length > input.budget.maxRecords)
1365
+ return false;
1366
+ }
1367
+ }
1368
+ return true;
1369
+ };
1370
+ if (!measureEventsById(eventIds))
1371
+ return { status: 'limit_exceeded' };
1372
+ const additionalTurnEventIds = new Set();
1373
+ for (const batch of batches(turnIds, RECOVERY_MESSAGE_QUERY_BATCH_SIZE)) {
1374
+ const placeholders = batch.map(() => '?').join(', ');
1375
+ const rows = this.db
1376
+ .prepare(`
1377
+ SELECT event_id
1378
+ FROM runtime_events
1379
+ WHERE session_id = ?
1380
+ AND turn_id IN (${placeholders})
1381
+ AND event_kind = 'text'
1382
+ AND CASE
1383
+ WHEN json_valid(payload_json)
1384
+ THEN json_extract(payload_json, '$.partial')
1385
+ END = 0
1386
+ AND CASE
1387
+ WHEN json_valid(payload_json)
1388
+ THEN json_extract(payload_json, '$.role')
1389
+ END = 'user'
1390
+ AND coalesce(CASE
1391
+ WHEN json_valid(payload_json)
1392
+ THEN json_extract(payload_json, '$.content.steering')
1393
+ END, 0) = 0
1394
+ LIMIT ?
1395
+ `)
1396
+ .all(input.sessionId, ...batch, input.budget.maxRecords + 1);
1397
+ for (const row of rows) {
1398
+ if (typeof row.event_id !== 'string') {
1399
+ throw new Error('Invalid recovery RuntimeEvent identity row');
1400
+ }
1401
+ if (!selectedEventIds.has(row.event_id))
1402
+ additionalTurnEventIds.add(row.event_id);
1403
+ if (selectedEventIds.size + additionalTurnEventIds.size > input.budget.maxRecords) {
1404
+ return { status: 'limit_exceeded' };
1405
+ }
1406
+ }
1407
+ }
1408
+ if (!measureEventsById([...additionalTurnEventIds]))
1409
+ return { status: 'limit_exceeded' };
1410
+ const measurement = measureEvidenceRows(measuredRows, input.budget, 'Invalid recovery RuntimeEvent evidence measurement row');
1411
+ if (!measurement)
1412
+ return { status: 'limit_exceeded' };
1413
+ const storedBytesById = new Map(measuredRows.map((row) => [row.event_id, requireRuntimeEventScanCount(row.stored_bytes)]));
1414
+ const events = new Map();
1415
+ for (const batch of batches([...selectedEventIds], RECOVERY_MESSAGE_QUERY_BATCH_SIZE)) {
1416
+ const placeholders = batch.map(() => '?').join(', ');
1417
+ const rows = this.db
1418
+ .prepare(`
1419
+ SELECT event_id, session_id, invocation_id, run_id, turn_id, payload_json,
1420
+ length(CAST(payload_json AS BLOB)) AS stored_bytes
1421
+ FROM runtime_events
1422
+ WHERE event_id IN (${placeholders})
1423
+ `)
1424
+ .all(...batch);
1425
+ for (const row of rows) {
1426
+ const event = decodeRuntimeEventStorageRow(row);
1427
+ if (event.sessionId !== input.sessionId)
1428
+ continue;
1429
+ if (storedBytesById.get(event.id) !== requireRuntimeEventScanCount(row.stored_bytes)) {
1430
+ throw new Error('Recovery RuntimeEvent evidence changed while reading');
1431
+ }
1432
+ events.set(event.id, event);
1060
1433
  }
1061
- messages.set(messageId, event);
1062
1434
  }
1435
+ if (events.size !== selectedEventIds.size) {
1436
+ throw new Error('Recovery RuntimeEvent evidence changed while reading');
1437
+ }
1438
+ return {
1439
+ status: 'complete',
1440
+ records: [...events.values()].sort((left, right) => left.ts - right.ts ||
1441
+ left.runId.localeCompare(right.runId) ||
1442
+ left.id.localeCompare(right.id)),
1443
+ ...measurement,
1444
+ };
1063
1445
  }
1064
1446
  /**
1065
1447
  * Narrows recall to the Sessions whose ledger could project a message
@@ -2235,25 +2617,37 @@ export class SqliteRuntimeStore {
2235
2617
  async readToolOperation(operationId) {
2236
2618
  return this.readToolOperationSync(operationId);
2237
2619
  }
2238
- async listUnsettledToolOperations(sessionId) {
2620
+ async listUnsettledToolOperations(sessionIds) {
2621
+ const selectedSessionIds = sessionIds === undefined
2622
+ ? undefined
2623
+ : [...new Set(typeof sessionIds === 'string' ? [sessionIds] : sessionIds)];
2624
+ if (selectedSessionIds?.length === 0)
2625
+ return [];
2239
2626
  const query = `
2240
- SELECT operation_id, invocation_id, run_id, turn_id, provider_tool_call_id,
2241
- tool_name, canonical_args_hash, recovery_mode, current_state,
2242
- call_event_id, dispatch_event_id, result_event_id, version
2627
+ SELECT call_event.session_id,
2628
+ tool_operations.operation_id, tool_operations.invocation_id,
2629
+ tool_operations.run_id, tool_operations.turn_id,
2630
+ tool_operations.provider_tool_call_id, tool_operations.tool_name,
2631
+ tool_operations.canonical_args_hash, tool_operations.recovery_mode,
2632
+ tool_operations.current_state, tool_operations.call_event_id,
2633
+ tool_operations.dispatch_event_id, tool_operations.result_event_id,
2634
+ tool_operations.version
2243
2635
  FROM tool_operations
2244
- WHERE current_state = 'prepared'
2245
- AND result_event_id IS NULL
2246
- AND dispatch_event_id IS NOT NULL
2247
- ${sessionId === undefined
2636
+ JOIN runtime_events AS call_event
2637
+ ON call_event.event_id = tool_operations.call_event_id
2638
+ WHERE tool_operations.current_state = 'prepared'
2639
+ AND tool_operations.result_event_id IS NULL
2640
+ AND tool_operations.dispatch_event_id IS NOT NULL
2641
+ ${selectedSessionIds === undefined
2248
2642
  ? ''
2249
- : 'AND call_event_id IN (SELECT event_id FROM runtime_events WHERE session_id = ?)'}
2250
- ORDER BY invocation_id ASC, operation_id ASC
2643
+ : 'AND call_event.session_id IN (SELECT value FROM json_each(?))'}
2644
+ ORDER BY tool_operations.invocation_id ASC, tool_operations.operation_id ASC
2251
2645
  `;
2252
2646
  const statement = this.db.prepare(query);
2253
- const rows = (sessionId === undefined
2647
+ const rows = (selectedSessionIds === undefined
2254
2648
  ? statement.all()
2255
- : statement.all(sessionId));
2256
- return rows.map(toolOperationFromRow);
2649
+ : statement.all(JSON.stringify(selectedSessionIds)));
2650
+ return rows.map((row) => ({ sessionId: row.session_id, ...toolOperationFromRow(row) }));
2257
2651
  }
2258
2652
  async readToolJournal(operationId) {
2259
2653
  const rows = this.db
@@ -2268,6 +2662,7 @@ export class SqliteRuntimeStore {
2268
2662
  .all(operationId);
2269
2663
  return rows.map(toolJournalRecordFromRow);
2270
2664
  }
2665
+ /** Explicit full-workspace integrity audit and disposable projection rebuild. */
2271
2666
  async rebuildToolProjectionsFromRuntimeEvents() {
2272
2667
  return this.transaction(() => this.rebuildToolProjectionsFromRuntimeEventsSync());
2273
2668
  }
@@ -2529,8 +2924,16 @@ export class SqliteRuntimeStore {
2529
2924
  }
2530
2925
  }
2531
2926
  transaction(operation) {
2532
- if (this.databaseLease)
2533
- return this.databaseLease.transaction('write', operation);
2927
+ if (this.databaseLease) {
2928
+ const nested = this.db.isTransaction;
2929
+ return this.databaseLease.transaction('write', () => {
2930
+ // A sibling lease may have changed the shared transaction view since
2931
+ // this store last used its reconstructible cache.
2932
+ if (nested)
2933
+ this.clearToolLedgerCache();
2934
+ return operation();
2935
+ });
2936
+ }
2534
2937
  this.db.exec('BEGIN IMMEDIATE');
2535
2938
  let result;
2536
2939
  try {
@@ -2544,10 +2947,17 @@ export class SqliteRuntimeStore {
2544
2947
  catch {
2545
2948
  // Preserve the protocol failure that caused rollback.
2546
2949
  }
2950
+ // Reducers may have loaded rows written earlier in this transaction as
2951
+ // their immutable base. Bump the local view even on rollback so the next
2952
+ // admission discards that phantom base before retrying.
2953
+ this.localCommittedWriteRevision += 1;
2547
2954
  this.settleEventCommits(false);
2955
+ this.settleToolLedgerStates(false);
2548
2956
  throw error;
2549
2957
  }
2958
+ this.localCommittedWriteRevision += 1;
2550
2959
  this.settleEventCommits(true);
2960
+ this.settleToolLedgerStates(true);
2551
2961
  return result;
2552
2962
  }
2553
2963
  noteEventCommit(sessionId) {
@@ -2765,64 +3175,268 @@ export class SqliteRuntimeStore {
2765
3175
  }
2766
3176
  }
2767
3177
  assertToolLedgerTransition(candidateEvents, expectedTransition) {
2768
- this.assertWorkspaceToolLedgerHealthy();
2769
- // Tool-call identity is scoped by invocation. Reading unrelated invocations here turns
2770
- // concurrent subagents into repeated whole-workspace scans without strengthening the
2771
- // transition check; event and operation uniqueness remain enforced by SQLite keys.
2772
- const rows = [];
2773
- const invocationIds = [...new Set(candidateEvents.map((event) => event.invocationId))].sort();
3178
+ const canonicalEncodings = candidateEvents.map(encodeCanonicalRuntimeEvent);
3179
+ const canonicalEvents = canonicalEncodings.map(({ event }) => event);
3180
+ const entry = this.toolLedgerCacheEntry(canonicalEvents);
3181
+ const appendedEncodings = canonicalEncodings.filter(({ event }) => entry.reducer.event(event.id) === undefined);
3182
+ const validation = validateIncrementalToolLedgerTransition({
3183
+ reducer: entry.reducer,
3184
+ candidateEvents: canonicalEvents,
3185
+ expectedTransition,
3186
+ });
3187
+ if (!validation.ok) {
3188
+ if (validation.source === 'existing') {
3189
+ throw new ToolLedgerCorruptionError(validation.code, validation.eventId);
3190
+ }
3191
+ throw new ToolLedgerRejectionError(validation.code, validation.eventId);
3192
+ }
3193
+ if (validation.appendedEvents === 0)
3194
+ return;
3195
+ this.noteToolLedgerState(entry, validation.checkpoint, canonicalEvents.map((event) => event.invocationId), {
3196
+ eventCount: validation.appendedEvents,
3197
+ estimatedBytes: estimateToolLedgerCacheBytes(appendedEncodings.reduce((bytes, encoding) => bytes + Buffer.byteLength(encoding.json, 'utf8'), 0), validation.appendedEvents),
3198
+ });
3199
+ }
3200
+ toolLedgerCacheEntry(candidateEvents) {
3201
+ const currentVersion = this.currentToolLedgerCacheVersion();
3202
+ if (this.toolLedgerCacheVersion !== currentVersion)
3203
+ this.refreshToolLedgerCacheVersion();
3204
+ const directInvocationIds = new Set(candidateEvents.map((event) => event.invocationId));
3205
+ const directOperationIds = referencedToolOperationIds(candidateEvents);
3206
+ for (const invocationId of this.toolLedgerOperationInvocations(directOperationIds)) {
3207
+ directInvocationIds.add(invocationId);
3208
+ }
3209
+ for (const [key, entry] of this.toolLedgerCache) {
3210
+ if (entry.seedInvocationIds.size === directInvocationIds.size &&
3211
+ [...directInvocationIds].every((invocationId) => entry.seedInvocationIds.has(invocationId))) {
3212
+ this.toolLedgerCacheMetrics.hits += 1;
3213
+ this.touchToolLedgerCacheEntry(key, entry);
3214
+ return entry;
3215
+ }
3216
+ }
3217
+ this.toolLedgerCacheMetrics.misses += 1;
3218
+ const invocationIds = new Set(directInvocationIds);
3219
+ const operationIds = new Set(directOperationIds);
3220
+ const resolvedOperationIds = new Set();
3221
+ const loadedInvocationIds = new Set();
3222
+ const scopedEvents = [];
3223
+ let storedPayloadBytes = 0;
2774
3224
  const readInvocation = this.db.prepare(`
2775
- SELECT event_id, session_id, invocation_id, run_id, turn_id, payload_json
3225
+ SELECT event_id, session_id, invocation_id, run_id, turn_id, event_seq, payload_json,
3226
+ length(CAST(payload_json AS BLOB)) AS stored_bytes
2776
3227
  FROM runtime_events
2777
3228
  WHERE invocation_id = ?
2778
3229
  ORDER BY event_seq ASC, event_id ASC
2779
3230
  `);
2780
- for (const invocationId of invocationIds) {
2781
- rows.push(...readInvocation.all(invocationId));
3231
+ while (true) {
3232
+ const pendingOperationIds = [...operationIds].filter((operationId) => !resolvedOperationIds.has(operationId));
3233
+ if (pendingOperationIds.length > 0) {
3234
+ for (const operationId of pendingOperationIds)
3235
+ resolvedOperationIds.add(operationId);
3236
+ for (const invocationId of this.toolLedgerOperationInvocations(pendingOperationIds)) {
3237
+ invocationIds.add(invocationId);
3238
+ }
3239
+ }
3240
+ const pendingInvocationIds = [...invocationIds]
3241
+ .filter((invocationId) => !loadedInvocationIds.has(invocationId))
3242
+ .sort();
3243
+ if (pendingInvocationIds.length === 0 && pendingOperationIds.length === 0)
3244
+ break;
3245
+ for (const invocationId of pendingInvocationIds) {
3246
+ loadedInvocationIds.add(invocationId);
3247
+ const rows = readInvocation.all(invocationId);
3248
+ for (const row of rows) {
3249
+ storedPayloadBytes += requireRuntimeEventScanCount(row.stored_bytes);
3250
+ const event = decodeRuntimeEventStorageRow(row);
3251
+ scopedEvents.push({ event, eventSeq: row.event_seq });
3252
+ if (event.refs?.parentOperationId)
3253
+ operationIds.add(event.refs.parentOperationId);
3254
+ }
3255
+ }
2782
3256
  }
2783
- const validation = validateToolLedgerTransition({
2784
- existingEvents: rows.map(decodeRuntimeEventStorageRow),
2785
- candidateEvents: candidateEvents.map(canonicalizeRuntimeEventForStorage),
2786
- expectedTransition,
2787
- });
2788
- if (!validation.ok) {
2789
- throw new ToolLedgerRejectionError(validation.code, validation.eventId);
3257
+ const sortedSeedInvocationIds = [...directInvocationIds].sort();
3258
+ const sortedInvocationIds = [...invocationIds].sort();
3259
+ const key = JSON.stringify([sortedSeedInvocationIds, sortedInvocationIds]);
3260
+ const existing = this.toolLedgerCache.get(key);
3261
+ if (existing) {
3262
+ this.touchToolLedgerCacheEntry(key, existing);
3263
+ return existing;
3264
+ }
3265
+ scopedEvents.sort((left, right) => left.event.invocationId.localeCompare(right.event.invocationId) ||
3266
+ left.eventSeq - right.eventSeq ||
3267
+ left.event.id.localeCompare(right.event.id));
3268
+ const entry = {
3269
+ reducer: new ToolLedgerReducer(scopedEvents.map(({ event }) => event)),
3270
+ seedInvocationIds: new Set(sortedSeedInvocationIds),
3271
+ invocationIds: new Set(sortedInvocationIds),
3272
+ eventCount: scopedEvents.length,
3273
+ estimatedBytes: estimateToolLedgerCacheBytes(storedPayloadBytes, scopedEvents.length),
3274
+ };
3275
+ if (this.toolLedgerEntryExceedsAdmissionBudget(entry)) {
3276
+ this.toolLedgerCacheMetrics.transientEntries += 1;
3277
+ return entry;
3278
+ }
3279
+ this.retainToolLedgerCacheEntry(key, entry);
3280
+ this.trimToolLedgerCache();
3281
+ return entry;
3282
+ }
3283
+ toolLedgerOperationInvocations(operationIds) {
3284
+ const ids = [...new Set(operationIds)].sort();
3285
+ if (ids.length === 0)
3286
+ return new Set();
3287
+ const placeholders = ids.map(() => '?').join(', ');
3288
+ const rows = this.db
3289
+ .prepare(`
3290
+ SELECT DISTINCT invocation_id
3291
+ FROM runtime_events
3292
+ WHERE event_kind = 'tool_dispatch'
3293
+ AND CASE
3294
+ WHEN json_valid(payload_json)
3295
+ THEN json_extract(payload_json, '$.actions.toolDispatch.operationId')
3296
+ END IN (${placeholders})
3297
+ `)
3298
+ .all(...ids);
3299
+ return new Set(rows.map((row) => row.invocation_id));
3300
+ }
3301
+ noteToolLedgerState(entry, checkpoint, invocationIds, delta) {
3302
+ const reducer = entry.reducer;
3303
+ const existing = this.uncommittedToolLedgerStates.get(reducer);
3304
+ if (existing) {
3305
+ for (const invocationId of invocationIds)
3306
+ existing.changedInvocationIds.add(invocationId);
3307
+ this.adjustToolLedgerCacheEntry(entry, delta);
3308
+ return;
2790
3309
  }
3310
+ if (this.uncommittedToolLedgerStates.size === 0 && this.databaseLease) {
3311
+ this.databaseLease.onTransactionSettled((committed) => this.settleToolLedgerStates(committed));
3312
+ }
3313
+ this.uncommittedToolLedgerStates.set(reducer, {
3314
+ checkpoint,
3315
+ changedInvocationIds: new Set(invocationIds),
3316
+ entry,
3317
+ estimatedBytesBefore: entry.estimatedBytes,
3318
+ eventCountBefore: entry.eventCount,
3319
+ });
3320
+ this.adjustToolLedgerCacheEntry(entry, delta);
2791
3321
  }
2792
- assertWorkspaceToolLedgerHealthy() {
2793
- const dataVersion = this.runtimeDataVersion();
2794
- if (!this.toolLedgerHealth || this.toolLedgerHealth.dataVersion !== dataVersion) {
2795
- this.refreshToolLedgerHealth();
3322
+ settleToolLedgerStates(committed) {
3323
+ const states = [...this.uncommittedToolLedgerStates.entries()];
3324
+ this.uncommittedToolLedgerStates.clear();
3325
+ if (states.length === 0)
3326
+ return;
3327
+ if (!committed) {
3328
+ for (const [reducer, state] of states.reverse()) {
3329
+ reducer.rollback(state.checkpoint);
3330
+ this.restoreToolLedgerCacheEntry(state);
3331
+ }
3332
+ this.trimToolLedgerCache();
3333
+ return;
2796
3334
  }
2797
- const health = this.toolLedgerHealth;
2798
- if (health.decodeFailure)
2799
- throw health.decodeFailure.error;
2800
- if (health.issue) {
2801
- // Pre-existing damage, not a bad candidate. Note the reach of "refused":
2802
- // this gate is only ever consulted for tool-bearing events, so a damaged
2803
- // ledger refuses tool facts and takes everything else. Callers that treat
2804
- // this as "the store is gone" are overreading it — see the note on the
2805
- // latch in `AgentRun.enqueueRuntimeEventStore`.
2806
- throw new ToolLedgerCorruptionError(health.issue.code, health.issue.eventId);
3335
+ const retained = new Set(states.map(([reducer]) => reducer));
3336
+ const changedInvocationIds = new Set(states.flatMap(([, state]) => [...state.changedInvocationIds]));
3337
+ for (const [key, entry] of this.toolLedgerCache) {
3338
+ if (retained.has(entry.reducer))
3339
+ continue;
3340
+ if ([...entry.invocationIds].some((id) => changedInvocationIds.has(id))) {
3341
+ this.deleteToolLedgerCacheEntry(key);
3342
+ }
3343
+ }
3344
+ for (const [reducer, state] of states)
3345
+ reducer.commit(state.checkpoint);
3346
+ this.toolLedgerCacheVersion = this.currentToolLedgerCacheVersion();
3347
+ this.trimToolLedgerCache();
3348
+ }
3349
+ refreshToolLedgerCacheVersion() {
3350
+ if (this.uncommittedToolLedgerStates.size > 0) {
3351
+ throw new Error('Cannot invalidate tool ledger state during an open write transaction');
3352
+ }
3353
+ this.clearToolLedgerCache();
3354
+ this.toolLedgerCacheVersion = this.currentToolLedgerCacheVersion();
3355
+ }
3356
+ retainToolLedgerCacheEntry(key, entry) {
3357
+ entry.cacheKey = key;
3358
+ this.toolLedgerCache.set(key, entry);
3359
+ this.toolLedgerCacheEstimatedBytes += entry.estimatedBytes;
3360
+ this.toolLedgerCacheEventCount += entry.eventCount;
3361
+ }
3362
+ touchToolLedgerCacheEntry(key, entry) {
3363
+ this.toolLedgerCache.delete(key);
3364
+ this.toolLedgerCache.set(key, entry);
3365
+ }
3366
+ adjustToolLedgerCacheEntry(entry, delta) {
3367
+ entry.estimatedBytes += delta.estimatedBytes;
3368
+ entry.eventCount += delta.eventCount;
3369
+ if (entry.cacheKey && this.toolLedgerCache.get(entry.cacheKey) === entry) {
3370
+ this.toolLedgerCacheEstimatedBytes += delta.estimatedBytes;
3371
+ this.toolLedgerCacheEventCount += delta.eventCount;
2807
3372
  }
2808
3373
  }
2809
- refreshToolLedgerHealth() {
2810
- const dataVersion = this.runtimeDataVersion();
2811
- try {
2812
- const rows = this.db
2813
- .prepare(`
2814
- SELECT event_id, session_id, invocation_id, run_id, turn_id, payload_json
2815
- FROM runtime_events
2816
- ORDER BY invocation_id ASC, event_seq ASC, event_id ASC
2817
- `)
2818
- .all();
2819
- const scan = scanToolLedger(rows.map(decodeRuntimeEventStorageRow));
2820
- this.toolLedgerHealth = { dataVersion, issue: scan.issues[0] };
3374
+ restoreToolLedgerCacheEntry(state) {
3375
+ const entry = state.entry;
3376
+ if (entry.cacheKey && this.toolLedgerCache.get(entry.cacheKey) === entry) {
3377
+ this.toolLedgerCacheEstimatedBytes += state.estimatedBytesBefore - entry.estimatedBytes;
3378
+ this.toolLedgerCacheEventCount += state.eventCountBefore - entry.eventCount;
2821
3379
  }
2822
- catch (error) {
2823
- this.toolLedgerHealth = { dataVersion, decodeFailure: { error } };
3380
+ entry.estimatedBytes = state.estimatedBytesBefore;
3381
+ entry.eventCount = state.eventCountBefore;
3382
+ }
3383
+ deleteToolLedgerCacheEntry(key, reason) {
3384
+ const entry = this.toolLedgerCache.get(key);
3385
+ if (!entry)
3386
+ return;
3387
+ this.toolLedgerCache.delete(key);
3388
+ if (entry.cacheKey === key)
3389
+ entry.cacheKey = undefined;
3390
+ this.toolLedgerCacheEstimatedBytes -= entry.estimatedBytes;
3391
+ this.toolLedgerCacheEventCount -= entry.eventCount;
3392
+ if (reason === 'budget')
3393
+ this.toolLedgerCacheMetrics.budgetEvictions += 1;
3394
+ if (reason === 'terminal')
3395
+ this.toolLedgerCacheMetrics.terminalEvictions += 1;
3396
+ }
3397
+ clearToolLedgerCache() {
3398
+ for (const entry of this.toolLedgerCache.values())
3399
+ entry.cacheKey = undefined;
3400
+ this.toolLedgerCache.clear();
3401
+ this.toolLedgerCacheEstimatedBytes = 0;
3402
+ this.toolLedgerCacheEventCount = 0;
3403
+ }
3404
+ invalidateToolLedgerCacheForInvocation(invocationId) {
3405
+ for (const [key, entry] of this.toolLedgerCache) {
3406
+ if (entry.invocationIds.has(invocationId)) {
3407
+ this.deleteToolLedgerCacheEntry(key, 'terminal');
3408
+ }
2824
3409
  }
2825
3410
  }
3411
+ toolLedgerEntryExceedsAdmissionBudget(entry) {
3412
+ return entry.estimatedBytes > this.toolLedgerCacheBudget.maxSingleEntryEstimatedBytes;
3413
+ }
3414
+ toolLedgerCacheExceedsBudget() {
3415
+ return (this.toolLedgerCache.size > this.toolLedgerCacheBudget.maxEntries ||
3416
+ this.toolLedgerCacheEstimatedBytes > this.toolLedgerCacheBudget.maxEstimatedBytes);
3417
+ }
3418
+ trimToolLedgerCache() {
3419
+ for (;;) {
3420
+ let victim;
3421
+ for (const [key, entry] of this.toolLedgerCache) {
3422
+ if (this.uncommittedToolLedgerStates.has(entry.reducer))
3423
+ continue;
3424
+ if (this.toolLedgerEntryExceedsAdmissionBudget(entry)) {
3425
+ victim = key;
3426
+ break;
3427
+ }
3428
+ }
3429
+ if (!victim && this.toolLedgerCacheExceedsBudget()) {
3430
+ victim = [...this.toolLedgerCache].find(([, entry]) => !this.uncommittedToolLedgerStates.has(entry.reducer))?.[0];
3431
+ }
3432
+ if (!victim)
3433
+ return;
3434
+ this.deleteToolLedgerCacheEntry(victim, 'budget');
3435
+ }
3436
+ }
3437
+ currentToolLedgerCacheVersion() {
3438
+ return `${this.runtimeDataVersion()}:${this.databaseLease?.writeViewRevision() ?? this.localCommittedWriteRevision}`;
3439
+ }
2826
3440
  runtimeDataVersion() {
2827
3441
  const row = this.db.prepare('PRAGMA data_version').get();
2828
3442
  return row.data_version;
@@ -3005,21 +3619,28 @@ export class SqliteRuntimeStore {
3005
3619
  const messageId = immutableSteeringMessageId(event);
3006
3620
  if (!messageId)
3007
3621
  return;
3008
- const matches = this.readImmutableSessionRuntimeEvents(event.sessionId).filter((candidate) => immutableSteeringMessageId(candidate) === messageId);
3622
+ const matches = this.readImmutableSteeringMessageEvents(event.sessionId, messageId);
3009
3623
  if (matches.some((candidate) => !isDeepStrictEqual(candidate, event))) {
3010
3624
  throw new Error(`Immutable steering message identity conflict: ${messageId}`);
3011
3625
  }
3012
3626
  }
3013
- readImmutableSessionRuntimeEvents(sessionId) {
3627
+ readImmutableSteeringMessageEvents(sessionId, messageId) {
3014
3628
  const rows = this.db
3015
3629
  .prepare(`
3016
- SELECT event_id, session_id, invocation_id, run_id, turn_id, payload_json
3017
- FROM runtime_events
3018
- WHERE session_id = ?
3019
- ORDER BY committed_at ASC, event_id ASC
3020
- `)
3021
- .all(sessionId);
3022
- return rows.map(decodeRuntimeEventStorageRow);
3630
+ SELECT event_id, session_id, invocation_id, run_id, turn_id, payload_json
3631
+ FROM runtime_events
3632
+ WHERE session_id = ?
3633
+ AND ${IMMUTABLE_STEERING_SQL_PREDICATE}
3634
+ AND CASE
3635
+ WHEN json_valid(payload_json)
3636
+ THEN json_extract(payload_json, '$.refs.providerEventId')
3637
+ END = ?
3638
+ ORDER BY committed_at ASC, event_id ASC
3639
+ `)
3640
+ .all(sessionId, messageId);
3641
+ return rows
3642
+ .map(decodeRuntimeEventStorageRow)
3643
+ .filter((event) => immutableSteeringMessageId(event) === messageId);
3023
3644
  }
3024
3645
  insertRuntimeEvent(event, committedAt, allowExactDuplicate, authorizedPendingContinuationClaimId) {
3025
3646
  const encoding = encodeCanonicalRuntimeEvent(event);
@@ -3035,6 +3656,9 @@ export class SqliteRuntimeStore {
3035
3656
  if (!allowExactDuplicate) {
3036
3657
  throw new Error(`RuntimeEvent ${canonicalEvent.id} already exists outside this tool transaction`);
3037
3658
  }
3659
+ if (isTerminalRuntimeEvent(canonicalEvent)) {
3660
+ this.invalidateToolLedgerCacheForInvocation(canonicalEvent.invocationId);
3661
+ }
3038
3662
  return this.runtimeEventSeq(canonicalEvent.id);
3039
3663
  }
3040
3664
  this.assertContinuationAuthorityAllowsEvent(canonicalEvent, authorizedPendingContinuationClaimId);
@@ -3068,6 +3692,9 @@ export class SqliteRuntimeStore {
3068
3692
  recordTranscriptTurnExtent(this.db, { ...canonicalEvent, kind: runtimeEventKind(canonicalEvent) }, ordinal);
3069
3693
  this.noteEventCommit(canonicalEvent.sessionId);
3070
3694
  this.deleteCompletedPartialSnapshot(canonicalEvent);
3695
+ if (isTerminalRuntimeEvent(canonicalEvent)) {
3696
+ this.invalidateToolLedgerCacheForInvocation(canonicalEvent.invocationId);
3697
+ }
3071
3698
  return next;
3072
3699
  }
3073
3700
  deleteCompletedPartialSnapshot(event) {
@@ -3348,6 +3975,19 @@ function assertRuntimeStorageSafeId(value, message) {
3348
3975
  if (!isRuntimeStorageSafeId(value))
3349
3976
  throw new Error(message);
3350
3977
  }
3978
+ function distinctSafeRuntimeIds(values, message) {
3979
+ const result = new Set();
3980
+ for (const value of values) {
3981
+ assertRuntimeStorageSafeId(value, message);
3982
+ result.add(value);
3983
+ }
3984
+ return [...result];
3985
+ }
3986
+ function* batches(values, size) {
3987
+ for (let offset = 0; offset < values.length; offset += size) {
3988
+ yield values.slice(offset, offset + size);
3989
+ }
3990
+ }
3351
3991
  function assertInvocationSearchLimit(limit) {
3352
3992
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) {
3353
3993
  throw new RangeError('Runtime invocation search limit must be an integer between 1 and 256');