maka-agent 0.2.0-dev.30.20260912 → 0.2.0-dev.31.20260913

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/THIRD_PARTY_NOTICES.txt +392 -0
  2. package/dist/acp/maka-acp-agent.js +8 -2
  3. package/dist/acp/prompt-content.js +225 -0
  4. package/dist/acp/session-event-mapper.js +103 -0
  5. package/dist/acp/session-registry.js +654 -17
  6. package/dist/acp/stdio-server.js +10 -1
  7. package/dist/cli-core.js +1 -1
  8. package/dist/pi-transcript-tools.js +3 -2
  9. package/dist/pi-transcript.js +12 -2
  10. package/dist/runtime-host-session-channel.js +63 -9
  11. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  12. package/node_modules/@maka/core/dist/artifacts.js +6 -0
  13. package/node_modules/@maka/core/dist/attachments.js +7 -2
  14. package/node_modules/@maka/core/dist/durable-tool-result-projection.js +3 -1
  15. package/node_modules/@maka/core/dist/events.js +21 -0
  16. package/node_modules/@maka/core/dist/goal.js +44 -2
  17. package/node_modules/@maka/core/dist/permission.js +0 -1
  18. package/node_modules/@maka/core/dist/runtime-boundary.js +73 -29
  19. package/node_modules/@maka/core/dist/runtime-event.js +6 -3
  20. package/node_modules/@maka/core/dist/runtime-handoff.js +10 -2
  21. package/node_modules/@maka/core/dist/runtime-logical-execution.js +39 -14
  22. package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +3 -1
  23. package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
  24. package/node_modules/@maka/core/dist/unified-diff.js +33 -26
  25. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +49 -178
  26. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +7 -2
  27. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +15 -53
  28. package/node_modules/@maka/runtime/dist/bash-model-output.js +7 -1
  29. package/node_modules/@maka/runtime/dist/bash-tail-buffer.js +13 -52
  30. package/node_modules/@maka/runtime/dist/builtin-tools.js +59 -111
  31. package/node_modules/@maka/runtime/dist/child-fd-input.js +3 -2
  32. package/node_modules/@maka/runtime/dist/context-budget-policy.js +1 -11
  33. package/node_modules/@maka/runtime/dist/context-budget.js +12 -1
  34. package/node_modules/@maka/runtime/dist/conversation-copy.js +104 -9
  35. package/node_modules/@maka/runtime/dist/filesystem-executor.js +10 -6
  36. package/node_modules/@maka/runtime/dist/filesystem-worker/client.js +77 -19
  37. package/node_modules/@maka/runtime/dist/filesystem-worker/launch-spec.js +66 -55
  38. package/node_modules/@maka/runtime/dist/filesystem-worker/operations.js +34 -97
  39. package/node_modules/@maka/runtime/dist/filesystem-worker/protocol.js +25 -10
  40. package/node_modules/@maka/runtime/dist/filesystem-worker/worker-entry.js +2 -0
  41. package/node_modules/@maka/runtime/dist/glob-search.js +74 -0
  42. package/node_modules/@maka/runtime/dist/goal-continuation.js +78 -8
  43. package/node_modules/@maka/runtime/dist/goal-state.js +7 -0
  44. package/node_modules/@maka/runtime/dist/grep-search.js +155 -0
  45. package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +84 -94
  46. package/node_modules/@maka/runtime/dist/model-history.js +25 -3
  47. package/node_modules/@maka/runtime/dist/pipe-tail-collector.js +6 -9
  48. package/node_modules/@maka/runtime/dist/read-page.js +231 -0
  49. package/node_modules/@maka/runtime/dist/request-projection.js +2 -15
  50. package/node_modules/@maka/runtime/dist/ripgrep-executable.js +62 -0
  51. package/node_modules/@maka/runtime/dist/ripgrep-guidance.js +81 -0
  52. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +187 -20
  53. package/node_modules/@maka/runtime/dist/runtime-read-model.js +2 -1
  54. package/node_modules/@maka/runtime/dist/sandbox/linux-profile-path.js +7 -1
  55. package/node_modules/@maka/runtime/dist/sandbox/linux-sandbox.js +20 -5
  56. package/node_modules/@maka/runtime/dist/session-manager.js +6 -6
  57. package/node_modules/@maka/runtime/dist/session-recap.js +9 -5
  58. package/node_modules/@maka/runtime/dist/shell-exec.js +7 -25
  59. package/node_modules/@maka/runtime/dist/shell-run-manager.js +1 -1
  60. package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -1
  61. package/node_modules/@maka/runtime/dist/shell-tools.js +6 -12
  62. package/node_modules/@maka/runtime/dist/tool-availability.js +0 -1
  63. package/node_modules/@maka/runtime/dist/tool-output.js +1 -1
  64. package/node_modules/@maka/runtime/dist/tool-result-archive-capability.js +24 -36
  65. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +32 -527
  66. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +17 -119
  67. package/node_modules/@maka/runtime/dist/tool-result-archive.js +7 -25
  68. package/node_modules/@maka/runtime/dist/tool-runtime.js +7 -5
  69. package/node_modules/@maka/runtime/dist/workers/filesystem-worker.js +6933 -3681
  70. package/node_modules/@maka/runtime/dist/workspace-executor.js +20 -24
  71. package/node_modules/@maka/runtime/package.json +2 -0
  72. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +185 -191
  73. package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +3 -0
  74. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  75. package/node_modules/@maka/runtime-host/dist/protocol/message.js +2 -2
  76. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  77. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +2 -2
  78. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +10 -2
  79. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +38 -0
  80. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +2 -8
  81. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +2 -0
  82. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +81 -7
  83. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +13 -13
  84. package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +65 -0
  85. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +1 -0
  86. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +5 -2
  87. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +88 -72
  88. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +21 -4
  89. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +145 -1
  90. package/node_modules/@maka/storage/dist/agent-run-store.js +5 -2
  91. package/node_modules/@maka/storage/dist/execution-stores.js +2 -1
  92. package/node_modules/@maka/storage/dist/runtime-transcript-query.js +27 -14
  93. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +69 -3
  94. package/node_modules/balanced-match/LICENSE.md +23 -0
  95. package/node_modules/balanced-match/README.md +57 -0
  96. package/node_modules/balanced-match/dist/commonjs/index.js +59 -0
  97. package/node_modules/balanced-match/dist/commonjs/package.json +3 -0
  98. package/node_modules/balanced-match/dist/esm/index.js +54 -0
  99. package/node_modules/balanced-match/dist/esm/package.json +3 -0
  100. package/node_modules/balanced-match/package.json +68 -0
  101. package/node_modules/brace-expansion/LICENSE +23 -0
  102. package/node_modules/brace-expansion/README.md +105 -0
  103. package/node_modules/brace-expansion/dist/commonjs/index.js +289 -0
  104. package/node_modules/brace-expansion/dist/commonjs/package.json +3 -0
  105. package/node_modules/brace-expansion/dist/esm/index.js +285 -0
  106. package/node_modules/brace-expansion/dist/esm/package.json +3 -0
  107. package/node_modules/brace-expansion/package.json +64 -0
  108. package/node_modules/glob/LICENSE.md +63 -0
  109. package/node_modules/glob/README.md +1203 -0
  110. package/node_modules/glob/dist/commonjs/glob.js +248 -0
  111. package/node_modules/glob/dist/commonjs/has-magic.js +27 -0
  112. package/node_modules/glob/dist/commonjs/ignore.js +119 -0
  113. package/node_modules/glob/dist/commonjs/index.js +68 -0
  114. package/node_modules/glob/dist/commonjs/index.min.js +4 -0
  115. package/node_modules/glob/dist/commonjs/package.json +3 -0
  116. package/node_modules/glob/dist/commonjs/pattern.js +223 -0
  117. package/node_modules/glob/dist/commonjs/processor.js +301 -0
  118. package/node_modules/glob/dist/commonjs/walker.js +387 -0
  119. package/node_modules/glob/dist/esm/glob.js +244 -0
  120. package/node_modules/glob/dist/esm/has-magic.js +23 -0
  121. package/node_modules/glob/dist/esm/ignore.js +115 -0
  122. package/node_modules/glob/dist/esm/index.js +55 -0
  123. package/node_modules/glob/dist/esm/index.min.js +4 -0
  124. package/node_modules/glob/dist/esm/package.json +3 -0
  125. package/node_modules/glob/dist/esm/pattern.js +219 -0
  126. package/node_modules/glob/dist/esm/processor.js +294 -0
  127. package/node_modules/glob/dist/esm/walker.js +381 -0
  128. package/node_modules/glob/package.json +98 -0
  129. package/node_modules/minimatch/LICENSE.md +55 -0
  130. package/node_modules/minimatch/README.md +528 -0
  131. package/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js +14 -0
  132. package/node_modules/minimatch/dist/commonjs/ast.js +845 -0
  133. package/node_modules/minimatch/dist/commonjs/brace-expressions.js +150 -0
  134. package/node_modules/minimatch/dist/commonjs/escape.js +30 -0
  135. package/node_modules/minimatch/dist/commonjs/index.js +1127 -0
  136. package/node_modules/minimatch/dist/commonjs/package.json +3 -0
  137. package/node_modules/minimatch/dist/commonjs/unescape.js +38 -0
  138. package/node_modules/minimatch/dist/esm/assert-valid-pattern.js +10 -0
  139. package/node_modules/minimatch/dist/esm/ast.js +841 -0
  140. package/node_modules/minimatch/dist/esm/brace-expressions.js +146 -0
  141. package/node_modules/minimatch/dist/esm/escape.js +26 -0
  142. package/node_modules/minimatch/dist/esm/index.js +1114 -0
  143. package/node_modules/minimatch/dist/esm/package.json +3 -0
  144. package/node_modules/minimatch/dist/esm/unescape.js +34 -0
  145. package/node_modules/minimatch/package.json +73 -0
  146. package/node_modules/minipass/LICENSE.md +55 -0
  147. package/node_modules/minipass/README.md +825 -0
  148. package/node_modules/minipass/dist/commonjs/index.js +1038 -0
  149. package/node_modules/minipass/dist/commonjs/package.json +3 -0
  150. package/node_modules/minipass/dist/esm/index.js +1020 -0
  151. package/node_modules/minipass/dist/esm/package.json +3 -0
  152. package/node_modules/minipass/package.json +77 -0
  153. package/node_modules/path-scurry/LICENSE.md +55 -0
  154. package/node_modules/path-scurry/README.md +636 -0
  155. package/node_modules/path-scurry/dist/commonjs/index.js +2018 -0
  156. package/node_modules/path-scurry/dist/commonjs/package.json +3 -0
  157. package/node_modules/path-scurry/dist/esm/index.js +1983 -0
  158. package/node_modules/path-scurry/dist/esm/package.json +3 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/LICENSE.md +55 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/README.md +469 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +1726 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +2 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  173. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/package.json +3 -0
  174. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  175. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  176. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  177. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  178. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  179. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  180. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  181. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +1722 -0
  182. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +2 -0
  183. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  184. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  185. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  186. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  187. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/package.json +3 -0
  188. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  189. package/node_modules/path-scurry/node_modules/lru-cache/package.json +154 -0
  190. package/node_modules/path-scurry/package.json +72 -0
  191. package/package.json +1 -1
  192. package/node_modules/@maka/runtime/dist/active-tool-result-prune.js +0 -248
  193. package/node_modules/@maka/runtime/dist/active-tool-result-working-set.js +0 -218
  194. package/node_modules/@maka/runtime/dist/archive-read-tool.js +0 -115
@@ -16,11 +16,12 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ import { DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES } from '@maka/core/durable-tool-result-projection';
19
20
  import { readRunInvocation } from '@maka/core/runtime-event-store';
20
21
  import { runtimeHandoffPause } from '@maka/core/runtime-handoff';
21
22
  import { readLogicalRuntimeExecution } from '@maka/core/runtime-logical-execution';
22
23
  import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
23
- import { activePresentationRuntimeEvents, affectsRuntimeEventStoredMessageProjection, isHardRuntimeEventReadModelDiagnostic, projectRuntimeEventsToStoredMessages, projectRuntimeEventUserMessage, } from '@maka/runtime/runtime-event-read-model';
24
+ import { createRuntimeEventStoredMessageProjector, projectTranscriptToolResult, isHardRuntimeEventReadModelDiagnostic, projectRuntimeEventUserMessage, } from '@maka/runtime/runtime-event-read-model';
24
25
  import { foldTurnContribution } from '@maka/storage/session-message-projection';
25
26
  import { SESSION_TRANSCRIPT_OVERLAY_MAX_MESSAGES } from '../protocol/index.js';
26
27
  const PERMISSION_OUTCOME_READ_CONCURRENCY = 8;
@@ -28,15 +29,13 @@ const PERMISSION_OUTCOME_READ_CONCURRENCY = 8;
28
29
  const EVENT_SEQUENCE_STRIDE = 8;
29
30
  export const ACTIVE_TRANSCRIPT_OVERLAY_MAX_MESSAGES = SESSION_TRANSCRIPT_OVERLAY_MAX_MESSAGES;
30
31
  export const ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES = 16 * 1024 * 1024;
31
- const ACTIVE_TRANSCRIPT_SOURCE_MAX_EVENTS = ACTIVE_TRANSCRIPT_OVERLAY_MAX_MESSAGES * 2;
32
+ const TRANSCRIPT_SOURCE_MAX_EVENTS = ACTIVE_TRANSCRIPT_OVERLAY_MAX_MESSAGES * 2;
33
+ // One RuntimeEvent can carry both the raw Tool Result and its durable model projection.
34
+ const TRANSCRIPT_SOURCE_MAX_RECORD_BYTES = DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES * 2 + 256 * 1024;
35
+ // The scan releases each decoded record after projection. Its memory budget is
36
+ // separate from the bounded amount of immutable input it may visit.
37
+ const TRANSCRIPT_SOURCE_MAX_BYTES = TRANSCRIPT_SOURCE_MAX_EVENTS * TRANSCRIPT_SOURCE_MAX_RECORD_BYTES;
32
38
  const ACTIVE_TRANSCRIPT_SCAN_BATCH_MAX_BYTES = 256 * 1024;
33
- /**
34
- * What one durable page may read of a Turn, matching the bound the live
35
- * overlay already holds for a run. A Turn past it is refused rather than
36
- * half-projected: a prefix of a Turn is not a smaller transcript of it.
37
- */
38
- const DURABLE_TRANSCRIPT_TURN_MAX_EVENTS = ACTIVE_TRANSCRIPT_SOURCE_MAX_EVENTS;
39
- const DURABLE_TRANSCRIPT_TURN_MAX_BYTES = ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES;
40
39
  /** Turns per storage round trip: one, so a page loads no Turn it cannot use. */
41
40
  const TRANSCRIPT_TURN_SCAN_LIMIT = 1;
42
41
  /**
@@ -66,17 +65,7 @@ export function createSessionTranscriptReader(input) {
66
65
  const root = await readRunInvocation(store, sessionId, rootTurn.runId);
67
66
  if (!root)
68
67
  return [];
69
- const budget = { events: 0, bytes: 0 };
70
- const scanned = new Map();
71
68
  const invocations = new Map([[root.runId, root]]);
72
- const readEvents = async (runId) => {
73
- const cached = scanned.get(runId);
74
- if (cached)
75
- return cached;
76
- const events = await readActiveRuntimeEvents(input.stores, sessionId, runId, budget);
77
- scanned.set(runId, events);
78
- return events;
79
- };
80
69
  let runIds = [root.runId];
81
70
  if (root.terminalEvent && runtimeHandoffPause(root.terminalEvent)) {
82
71
  const logical = await readLogicalRuntimeExecution({
@@ -87,26 +76,20 @@ export function createSessionTranscriptReader(input) {
87
76
  invocations.set(runId, run);
88
77
  return run;
89
78
  },
90
- // Authenticate only after the same bounded scan used for presentation.
91
- // Sealed prefixes cannot grow between this check and digest verification.
92
- readImmutableRuntimeEvents: async (_id, runId) => (await readEvents(runId)).filter((event) => !event.partial),
93
- readImmutableRuntimePrefix: async (prefix) => {
94
- await readEvents(prefix.runId);
95
- return store.readImmutableRuntimePrefix(prefix);
96
- },
97
- }, { sessionId, turnId: rootTurn.turnId, runId: rootTurn.runId }, root);
79
+ readImmutableRuntimePrefixProof: (prefix) => store.readImmutableRuntimePrefixProof(prefix, {
80
+ maxEvents: TRANSCRIPT_SOURCE_MAX_EVENTS,
81
+ maxBytes: TRANSCRIPT_SOURCE_MAX_BYTES,
82
+ maxRecordBytes: TRANSCRIPT_SOURCE_MAX_RECORD_BYTES,
83
+ }),
84
+ }, { sessionId, turnId: rootTurn.turnId, runId: rootTurn.runId }, root, { mode: 'membership' });
98
85
  if (!logical)
99
86
  return [];
100
87
  runIds = logical.runIds;
101
88
  }
102
- const events = [];
89
+ const pending = createTranscriptProjection(runIds.map((runId) => invocations.get(runId)), true);
103
90
  for (const runId of runIds)
104
- events.push(...(await readEvents(runId)).filter(affectsRuntimeEventStoredMessageProjection));
105
- const canonicalPermissionOutcomes = await readCanonicalPermissionOutcomes(events, input.canonicalPermissionOutcomes);
106
- const projected = projectRuntimeEventsToStoredMessages(activePresentationRuntimeEvents(events), {
107
- invocations: runIds.map((runId) => invocations.get(runId)),
108
- canonicalPermissionOutcomes,
109
- });
91
+ await scanActiveRuntimeEvents(input.stores, sessionId, runId, pending.push);
92
+ const projected = await pending.finish(input.canonicalPermissionOutcomes);
110
93
  if (projected.diagnostics.some(isHardRuntimeEventReadModelDiagnostic)) {
111
94
  throw new Error('Active RuntimeEvent transcript projection is incomplete');
112
95
  }
@@ -131,11 +114,7 @@ function createDurableLedgerTranscriptReader(input) {
131
114
  };
132
115
  /** One Turn's rows, each at the sequence its own event sits at. */
133
116
  const projectTurn = async (turn) => {
134
- const events = turn.events.map((entry) => entry.event);
135
- const projected = projectRuntimeEventsToStoredMessages(events, {
136
- invocations: [turn.invocation],
137
- canonicalPermissionOutcomes: await readCanonicalPermissionOutcomes(events, input.canonicalPermissionOutcomes),
138
- });
117
+ const projected = await turn.projection.finish(input.canonicalPermissionOutcomes);
139
118
  if (projected.diagnostics.some(isHardRuntimeEventReadModelDiagnostic)) {
140
119
  throw new Error('Durable RuntimeEvent transcript projection is incomplete');
141
120
  }
@@ -145,7 +124,7 @@ function createDurableLedgerTranscriptReader(input) {
145
124
  const actionId = admission?.execution.kind === 'workhub_coordination'
146
125
  ? admission.execution.actionId
147
126
  : undefined;
148
- const ordinals = new Map(turn.events.map((entry) => [entry.event.id, entry.ordinal]));
127
+ const ordinals = turn.ordinals;
149
128
  const emitted = new Map();
150
129
  return projected.messages.map((message, index) => {
151
130
  const ordinal = ordinals.get(projected.sourceEventIds[index]);
@@ -165,11 +144,20 @@ function createDurableLedgerTranscriptReader(input) {
165
144
  };
166
145
  });
167
146
  };
168
- const readTurns = async (sessionId, request) => store.readTranscriptInvocations(sessionId, {
147
+ const readTurns = async (sessionId, request, limit = TRANSCRIPT_TURN_SCAN_LIMIT) => store.readTranscriptInvocations(sessionId, {
169
148
  ...request,
170
- limit: TRANSCRIPT_TURN_SCAN_LIMIT,
171
- maxEvents: DURABLE_TRANSCRIPT_TURN_MAX_EVENTS,
172
- maxBytes: DURABLE_TRANSCRIPT_TURN_MAX_BYTES,
149
+ limit,
150
+ maxEvents: TRANSCRIPT_SOURCE_MAX_EVENTS,
151
+ maxBytes: TRANSCRIPT_SOURCE_MAX_BYTES,
152
+ maxRecordBytes: TRANSCRIPT_SOURCE_MAX_RECORD_BYTES,
153
+ }, (turn, events) => {
154
+ const projection = createTranscriptProjection([turn.invocation]);
155
+ const ordinals = new Map();
156
+ for (const { event, ordinal } of events) {
157
+ ordinals.set(event.id, ordinal);
158
+ projection.push(event);
159
+ }
160
+ return { ...turn, projection, ordinals };
173
161
  });
174
162
  const scan = async function* (sessionId, request) {
175
163
  const throughSequence = request.throughSequence === undefined ? await highWater(sessionId) : request.throughSequence;
@@ -234,14 +222,11 @@ function createDurableLedgerTranscriptReader(input) {
234
222
  if (watermark === null) {
235
223
  return { throughSequence: null, contributions: [], nextPosition: null };
236
224
  }
237
- const turns = await store.readTranscriptInvocations(sessionId, {
225
+ const turns = await readTurns(sessionId, {
238
226
  direction: 'newer',
239
227
  throughOrdinal: ordinalOf(watermark),
240
228
  position: ordinalOf(position),
241
- limit: maxContributions + 1,
242
- maxEvents: DURABLE_TRANSCRIPT_TURN_MAX_EVENTS,
243
- maxBytes: DURABLE_TRANSCRIPT_TURN_MAX_BYTES,
244
- });
229
+ }, maxContributions + 1);
245
230
  const contributions = [];
246
231
  for (const turn of turns.slice(0, maxContributions)) {
247
232
  // Folded from the Turn's own rows, so `firstSequence` lands on its first
@@ -414,11 +399,7 @@ function assertActiveOverlayBounded(messages) {
414
399
  }
415
400
  }
416
401
  }
417
- async function readCanonicalPermissionOutcomes(events, reader) {
418
- const requestIds = new Set(events.flatMap((event) => {
419
- const requestId = event.actions?.permissionAnswerAccepted?.requestId;
420
- return requestId ? [requestId] : [];
421
- }));
402
+ async function readCanonicalPermissionOutcomes(requestIds, reader) {
422
403
  const outcomes = new Map();
423
404
  const ids = [...requestIds];
424
405
  let encodedBytes = 0;
@@ -439,32 +420,67 @@ async function readCanonicalPermissionOutcomes(events, reader) {
439
420
  }
440
421
  return outcomes;
441
422
  }
442
- async function readActiveRuntimeEvents(stores, sessionId, runId, budget) {
443
- const events = [];
423
+ async function scanActiveRuntimeEvents(stores, sessionId, runId, visit) {
444
424
  const result = await stores.runtimeEventStore.scanRuntimeEvents(sessionId, runId, {
445
425
  maxBatchBytes: ACTIVE_TRANSCRIPT_SCAN_BATCH_MAX_BYTES,
446
- maxRecordBytes: ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES,
447
- maxImmutableRecords: ACTIVE_TRANSCRIPT_SOURCE_MAX_EVENTS,
448
- maxImmutableBytes: ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES,
449
- maxPartialRecords: ACTIVE_TRANSCRIPT_SOURCE_MAX_EVENTS,
426
+ maxRecordBytes: TRANSCRIPT_SOURCE_MAX_RECORD_BYTES,
427
+ maxImmutableRecords: TRANSCRIPT_SOURCE_MAX_EVENTS,
428
+ maxImmutableBytes: TRANSCRIPT_SOURCE_MAX_BYTES,
429
+ maxPartialRecords: TRANSCRIPT_SOURCE_MAX_EVENTS,
450
430
  maxPartialBytes: ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES,
451
431
  }, (batch) => {
452
- for (const event of batch) {
453
- budget.events += 1;
454
- budget.bytes += Buffer.byteLength(JSON.stringify(event), 'utf8');
455
- if (budget.events > ACTIVE_TRANSCRIPT_SOURCE_MAX_EVENTS) {
456
- throw new Error('Active RuntimeEvent transcript exceeds its event limit');
457
- }
458
- if (budget.bytes > ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES) {
459
- throw new Error('Active RuntimeEvent transcript exceeds its byte limit');
460
- }
461
- }
462
- events.push(...batch);
432
+ for (const event of batch)
433
+ visit(event);
463
434
  });
464
435
  if (result.status === 'limit_exceeded') {
465
436
  throw new Error('Active RuntimeEvent transcript exceeds its storage scan limit');
466
437
  }
467
- return events;
438
+ }
439
+ /** Keep only presentation state while the storage snapshot visits complete facts. */
440
+ function createTranscriptProjection(invocations, active = false) {
441
+ const canonicalPermissionOutcomes = new Map();
442
+ let messageCount = 0;
443
+ let messageBytes = 0;
444
+ let eventCount = 0;
445
+ let sourceBytes = 0;
446
+ const projector = createRuntimeEventStoredMessageProjector({
447
+ invocations,
448
+ active,
449
+ canonicalPermissionOutcomes,
450
+ projectToolResult: projectTranscriptToolResult,
451
+ onMessage: (message) => {
452
+ messageCount += 1;
453
+ messageBytes += Buffer.byteLength(JSON.stringify(message));
454
+ if (messageCount > ACTIVE_TRANSCRIPT_OVERLAY_MAX_MESSAGES ||
455
+ messageBytes > ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES)
456
+ throw new Error('Session transcript projection exceeds its presentation limit');
457
+ },
458
+ });
459
+ return {
460
+ push(event) {
461
+ eventCount += 1;
462
+ const content = event.content;
463
+ // The durable model projection is never a transcript input. Large Bash
464
+ // streams are consumed into a bounded terminal preview by the read model.
465
+ const measured = content?.kind === 'function_response'
466
+ ? { ...event, content: { ...content, modelProjection: undefined, result: undefined } }
467
+ : event;
468
+ sourceBytes += Buffer.byteLength(JSON.stringify(measured));
469
+ if (eventCount > TRANSCRIPT_SOURCE_MAX_EVENTS)
470
+ throw new Error('Active RuntimeEvent transcript exceeds its event limit');
471
+ if (sourceBytes > ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES)
472
+ throw new Error('Active RuntimeEvent transcript exceeds its byte limit');
473
+ projector.push(event);
474
+ },
475
+ async finish(reader) {
476
+ const outcomes = await readCanonicalPermissionOutcomes(projector.permissionRequestIds, reader);
477
+ for (const [id, outcome] of outcomes)
478
+ canonicalPermissionOutcomes.set(id, outcome);
479
+ const projected = projector.finish();
480
+ assertActiveOverlayBounded(projected.messages);
481
+ return projected;
482
+ },
483
+ };
468
484
  }
469
485
  function isTerminalTurn(turn) {
470
486
  return turn.status === 'completed' || turn.status === 'failed' || turn.status === 'cancelled';
@@ -213,15 +213,31 @@ export class WorkHubCoordinationActionGate {
213
213
  return this.#replace(intent, context);
214
214
  }
215
215
  const candidates = await this.candidates();
216
- if (candidates.candidateSetId !== input.candidateSetId) {
216
+ if (!input.selectedTarget && candidates.candidateSetId !== input.candidateSetId) {
217
217
  throw new WorkHubActionGateFailure('candidate_set_stale', 'WorkHub Session candidates changed; refresh before delegating');
218
218
  }
219
- const target = candidates.candidates.find((candidate) => candidate.candidateRef === proposal.candidateRef);
219
+ const target = candidates.candidates.find((candidate) => input.selectedTarget
220
+ ? candidate.sessionId === input.selectedTarget.sessionId &&
221
+ digest(candidate.workspace) === input.selectedTarget.workspaceDigest
222
+ : candidate.candidateRef === proposal.candidateRef);
220
223
  if (!target) {
221
- throw new WorkHubActionGateFailure('candidate_unavailable', 'WorkHub target is not in the admitted candidate set');
224
+ throw new WorkHubActionGateFailure(input.selectedTarget ? 'candidate_set_stale' : 'candidate_unavailable', 'WorkHub target is no longer in the offered workspace');
222
225
  }
223
226
  this.#assertTarget(target);
224
- return this.#assign(delegationAssignment(input, fingerprint, target.sessionId, target.sessionName), context);
227
+ return this.#assign({
228
+ ...delegationAssignment(input, fingerprint, target.sessionId, target.sessionName),
229
+ ...(input.selectedTarget
230
+ ? {
231
+ validateFreshTarget: async () => {
232
+ const fresh = (await this.candidates()).candidates.find((candidate) => candidate.sessionId === input.selectedTarget.sessionId &&
233
+ digest(candidate.workspace) === input.selectedTarget.workspaceDigest);
234
+ if (!fresh)
235
+ throw new WorkHubActionGateFailure('candidate_set_stale', 'The selected work changed before admission');
236
+ this.#assertTarget(fresh);
237
+ },
238
+ }
239
+ : {}),
240
+ }, context);
225
241
  }
226
242
  /**
227
243
  * The delegation a stop names, by the only two keys that can name it.
@@ -639,6 +655,7 @@ function digest(value) {
639
655
  }
640
656
  function actionFingerprint(input) {
641
657
  const common = {
658
+ ...(input.selectedTarget ? { selectedTarget: input.selectedTarget } : {}),
642
659
  userText: input.userText,
643
660
  ...(input.attachments ? { attachments: input.attachments } : {}),
644
661
  ...(input.newWorkDefaults ? { newWorkDefaults: input.newWorkDefaults } : {}),
@@ -24,6 +24,7 @@ import { normalizeMessageContent } from '@maka/core/events';
24
24
  import { WORKHUB_COORDINATION_SESSION_ID, WORKHUB_COORDINATION_SESSION_ROLE, WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION, WORKHUB_COORDINATION_STOP_SCHEMA_VERSION, isWorkHubCoordinationSession, isWorkHubCoordinationSessionId, } from '@maka/core/session';
25
25
  import { WORKHUB_COORDINATION_TEXT_MAX_BYTES } from '../protocol/index.js';
26
26
  import { SessionOperationFailure, projectSessionCatalogRecord, } from './session-catalog-coordinator.js';
27
+ import { RuntimeInteractionAdmissionRejectedError, RuntimeInteractionFailStopError, } from '@maka/runtime/interaction-authority';
27
28
  import { WorkHubActionEffectFailure, WorkHubActionGateFailure, WorkHubCoordinationActionGate, } from './workhub-coordination-action-gate.js';
28
29
  const CREATE_FINGERPRINT = `sha256:${createHash('sha256')
29
30
  .update('maka:workhub-coordination-session:v1', 'utf8')
@@ -44,7 +45,9 @@ export const WORKHUB_ROUTING_HISTORY_MAX_MESSAGES = 32;
44
45
  export const WORKHUB_ROUTING_HISTORY_MAX_STORED_BYTES = 64 * 1024;
45
46
  /** Resolves the one durable Coordination Session owned by this Runtime Host. */
46
47
  export class HostWorkHubCoordinationCoordinator {
48
+ #requestForm;
47
49
  handlers = {
50
+ 'workhub.coordination.selectAndDelegate': (input, context) => this.#selectAndDelegate(input, context),
48
51
  'workhub.coordination.resolve': () => this.#resolve(),
49
52
  'workhub.coordination.query': () => this.#query(),
50
53
  'workhub.coordination.configureModel': (input) => this.#configureModel(input),
@@ -65,6 +68,7 @@ export class HostWorkHubCoordinationCoordinator {
65
68
  #routingModel;
66
69
  #readDelegationRetirement;
67
70
  constructor(options) {
71
+ this.#requestForm = options.requestForm;
68
72
  this.#configureModel = options.configureModel;
69
73
  this.#routingModel = options.routingModel;
70
74
  this.#transitionConfiguration = options.transitionConfiguration;
@@ -332,7 +336,146 @@ export class HostWorkHubCoordinationCoordinator {
332
336
  };
333
337
  }
334
338
  }
335
- async #actFromTurn(input, context) {
339
+ async #selectAndDelegate(input, context) {
340
+ try {
341
+ const authority = await this.#executions.readActiveWorkHubRoutingRequest(input.turnId);
342
+ if (!authority || authority.decision || !this.#requestForm) {
343
+ return {
344
+ ok: false,
345
+ error: {
346
+ code: 'operation_conflict',
347
+ message: 'Target selection requires an active, unbound Coordination Turn',
348
+ },
349
+ };
350
+ }
351
+ // The interaction identity binds the complete operation and original user authority.
352
+ // Its durable answer can be replayed without rebuilding (or interpreting) the offer.
353
+ const requestId = createHash('sha256')
354
+ .update(JSON.stringify({
355
+ input,
356
+ runId: authority.runId,
357
+ content: authority.content,
358
+ }))
359
+ .digest('hex');
360
+ const choice = await this.#requestForm({
361
+ sessionId: WORKHUB_COORDINATION_SESSION_ID,
362
+ turnId: input.turnId,
363
+ runId: authority.runId,
364
+ requestId,
365
+ create: async () => {
366
+ if (await this.#stores.readWorkHubAssignment(input.actionId)) {
367
+ throw new WorkHubActionGateFailure('action_conflict', 'This action identity already belongs to another selection');
368
+ }
369
+ const page = await this.#actionGate.candidates();
370
+ if (page.candidateSetId !== input.candidateSetId) {
371
+ throw new WorkHubActionGateFailure('candidate_set_stale', 'Discover fresh candidates before requesting a target choice');
372
+ }
373
+ const options = input.candidateRefs.map((ref) => {
374
+ const candidate = page.candidates.find((item) => item.candidateRef === ref);
375
+ if (!candidate)
376
+ throw new WorkHubActionGateFailure('candidate_unavailable', 'Target choice contains an unavailable candidate');
377
+ // Keep labels inside the shared form wire budget; identity is the opaque value.
378
+ const label = `${candidate.sessionName} — ${candidate.workspace.hostCwd}`;
379
+ let bounded = '';
380
+ for (const character of label) {
381
+ if (Buffer.byteLength(bounded + character, 'utf8') > 190)
382
+ break;
383
+ bounded += character;
384
+ }
385
+ return {
386
+ // An opaque, durable binding: only an exact offered value is accepted by
387
+ // the form authority. Names and model-written answers never resolve identity.
388
+ value: JSON.stringify([ref, candidate.sessionId, digest(candidate.workspace)]),
389
+ label: page.candidates.some((other) => other.sessionId !== candidate.sessionId &&
390
+ other.sessionName === candidate.sessionName &&
391
+ other.workspace.hostCwd === candidate.workspace.hostCwd)
392
+ ? `${bounded} [${candidate.sessionId.slice(0, 12)}]`
393
+ : bounded,
394
+ };
395
+ });
396
+ return {
397
+ kind: 'form',
398
+ toolUseId: input.actionId,
399
+ message: 'Choose the work to continue',
400
+ requester: { name: 'WorkHub' },
401
+ fields: [
402
+ {
403
+ kind: 'single_select',
404
+ name: 'target',
405
+ label: 'Work / Workspace',
406
+ required: true,
407
+ options,
408
+ },
409
+ ],
410
+ };
411
+ },
412
+ });
413
+ if (choice.answer.action !== 'accept')
414
+ return { ok: true, result: { kind: 'cancelled' } };
415
+ const value = choice.answer.values.target;
416
+ const binding = typeof value === 'string' ? JSON.parse(value) : undefined;
417
+ if (!Array.isArray(binding) ||
418
+ binding.length !== 3 ||
419
+ !binding.every((part) => typeof part === 'string') ||
420
+ !input.candidateRefs.includes(binding[0])) {
421
+ return {
422
+ ok: false,
423
+ error: {
424
+ code: 'operation_conflict',
425
+ message: 'Target choice does not belong to this operation',
426
+ },
427
+ };
428
+ }
429
+ if (Date.now() - choice.createdAt > 10 * 60_000 &&
430
+ !(await this.#stores.readWorkHubAssignment(input.actionId))) {
431
+ return {
432
+ ok: false,
433
+ error: {
434
+ code: 'candidate_set_stale',
435
+ message: 'Target choice expired; discover candidates and ask again',
436
+ },
437
+ };
438
+ }
439
+ const [offeredRef, sessionId, workspaceDigest] = binding;
440
+ const freshAuthority = await this.#executions.readActiveWorkHubRoutingRequest(input.turnId);
441
+ if (freshAuthority?.runId !== authority.runId) {
442
+ return {
443
+ ok: false,
444
+ error: { code: 'operation_conflict', message: 'The selecting Run is no longer active' },
445
+ };
446
+ }
447
+ // Re-read active authority after waiting. The Gate performs fresh candidate validation
448
+ // and durable action replay before it admits any target execution.
449
+ const outcome = await this.#actFromTurn({
450
+ turnId: input.turnId,
451
+ actionId: input.actionId,
452
+ proposal: { disposition: 'delegate_existing', candidateRef: offeredRef },
453
+ candidateSetId: input.candidateSetId,
454
+ delegationText: input.delegationText,
455
+ }, context, { sessionId, workspaceDigest });
456
+ return outcome.ok
457
+ ? { ok: true, result: { kind: 'delegated', result: outcome.result } }
458
+ : outcome;
459
+ }
460
+ catch (error) {
461
+ if (error instanceof RuntimeInteractionFailStopError)
462
+ throw error;
463
+ return {
464
+ ok: false,
465
+ error: {
466
+ code: error instanceof WorkHubActionGateFailure
467
+ ? error.code === 'candidate_set_stale'
468
+ ? 'candidate_set_stale'
469
+ : 'operation_conflict'
470
+ : error instanceof RuntimeInteractionAdmissionRejectedError
471
+ ? 'operation_conflict'
472
+ : 'persistence_failed',
473
+ message: error instanceof Error ? error.message : 'Target selection is unavailable',
474
+ },
475
+ };
476
+ }
477
+ }
478
+ async #actFromTurn(input, context, selectedTarget) {
336
479
  let request;
337
480
  try {
338
481
  request = await this.#executions.readActiveWorkHubRoutingRequest(input.turnId);
@@ -372,6 +515,7 @@ export class HostWorkHubCoordinationCoordinator {
372
515
  ok: true,
373
516
  result: await this.#actionGate.act({
374
517
  ...action,
518
+ ...(selectedTarget ? { selectedTarget } : {}),
375
519
  userText: request.content.text,
376
520
  ...(carriesAttachments && request.content.attachments
377
521
  ? { attachments: request.content.attachments }
@@ -24,7 +24,7 @@ import { assertNoReservedWorkspaceAuthorityAppend } from './runtime-event-author
24
24
  import { acquireOperationalStateDatabase, } from './operational-state-store.js';
25
25
  import { assertEvidenceReadBudget, measureEvidenceRows, } from './bounded-evidence.js';
26
26
  import { decodeSkillInvocationResult, } from '@maka/core/skill-invocation';
27
- import { aggregateMessageContents, decodeMessageContent, isCanonicalAttachmentRef, messageContentsEqual, } from '@maka/core/events';
27
+ import { aggregateMessageContents, decodeMessageContent, hasMeaningfulMessageContent, isCanonicalAttachmentRef, messageContentsEqual, } from '@maka/core/events';
28
28
  import { decodeAgentGraphIntentClaim } from '@maka/core/agent-graph-control';
29
29
  import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
30
30
  import { MODEL_CALL_ATTEMPT_EVENT_TYPE } from '@maka/core/model-call-attempt';
@@ -1052,7 +1052,10 @@ function normalizeRootTurnMessageContent(value, description, maxAttachments) {
1052
1052
  }
1053
1053
  throw new Error(`Invalid ${description}`);
1054
1054
  }
1055
- if (normalized.text.length === 0 || (normalized.attachments?.length ?? 0) > maxAttachments) {
1055
+ // Quote- or attachment-only input is meaningful (#4804): the text carrier
1056
+ // alone no longer decides durability admission.
1057
+ if (!hasMeaningfulMessageContent(normalized) ||
1058
+ (normalized.attachments?.length ?? 0) > maxAttachments) {
1056
1059
  throw new Error(`Invalid ${description}`);
1057
1060
  }
1058
1061
  for (const [index, attachment] of (normalized.attachments ?? []).entries()) {
@@ -229,6 +229,7 @@ async function createExecutionStoresForWrite(lease, kind, extension) {
229
229
  readRuntimeEventsBounded: (sessionId, runId, budget) => run(() => runtimeEventStore.readRuntimeEventsBounded(sessionId, runId, budget)),
230
230
  readImmutableRuntimeEvents: (sessionId, runId) => run(() => runtimeEventStore.readImmutableRuntimeEvents(sessionId, runId)),
231
231
  readImmutableRuntimePrefix: (input) => run(() => runtimeEventStore.readImmutableRuntimePrefix(input)),
232
+ readImmutableRuntimePrefixProof: (input, budget) => run(() => runtimeEventStore.readImmutableRuntimePrefixProof(input, budget)),
232
233
  listSessionInvocations: (sessionId) => run(() => runtimeEventStore.listSessionInvocations(sessionId)),
233
234
  readRunInvocation: (sessionId, runId) => run(() => runtimeEventStore.readRunInvocation(sessionId, runId)),
234
235
  listSessionInvocationsBounded: (sessionId, limit) => run(() => runtimeEventStore.listSessionInvocationsBounded(sessionId, limit)),
@@ -238,7 +239,7 @@ async function createExecutionStoresForWrite(lease, kind, extension) {
238
239
  readSessionRuntimeEventEntries: (sessionId) => run(() => runtimeEventStore.readSessionRuntimeEventEntries(sessionId)),
239
240
  resequenceSessionEventOrdinals: (sessionId) => run(() => runtimeEventStore.resequenceSessionEventOrdinals(sessionId)),
240
241
  readTranscriptHighWater: (sessionId) => run(() => runtimeEventStore.readTranscriptHighWater(sessionId)),
241
- readTranscriptInvocations: (sessionId, request) => run(() => runtimeEventStore.readTranscriptInvocations(sessionId, request)),
242
+ readTranscriptInvocations: (sessionId, request, project) => run(() => runtimeEventStore.readTranscriptInvocations(sessionId, request, project)),
242
243
  readTranscriptLandmarks: (sessionId, throughOrdinal, limit) => run(() => runtimeEventStore.readTranscriptLandmarks(sessionId, throughOrdinal, limit)),
243
244
  claimContinuation: (input) => run(() => runtimeEventStore.claimContinuation(input)),
244
245
  readContinuationClaimByBoundary: (boundaryDigest) => run(() => runtimeEventStore.readContinuationClaimByBoundary(boundaryDigest)),
@@ -107,9 +107,12 @@ export class RuntimeTranscriptQuery {
107
107
  });
108
108
  return row?.last ?? null;
109
109
  }
110
- invocations(sessionId, request) {
110
+ invocations(sessionId, request, project) {
111
111
  assertOrdinal(request.throughOrdinal);
112
112
  assertOrdinal(request.position);
113
+ assertReadLimit(request.maxEvents, 'event count');
114
+ assertReadLimit(request.maxBytes, 'byte');
115
+ assertReadLimit(request.maxRecordBytes, 'record byte');
113
116
  if (request.direction !== 'older' && request.direction !== 'newer') {
114
117
  throw new Error('Invalid transcript direction');
115
118
  }
@@ -130,12 +133,11 @@ export class RuntimeTranscriptQuery {
130
133
  throughOrdinal: request.throughOrdinal,
131
134
  limit: request.limit,
132
135
  }).sort((a, b) => a.first - b.first);
133
- return rows.map((row) => ({
136
+ return rows.map((row) => project({
134
137
  invocation: this.invocation(sessionId, row.invocation_id),
135
138
  firstOrdinal: row.first,
136
139
  lastOrdinal: row.last,
137
- events: this.events(row.invocation_id, request),
138
- }));
140
+ }, this.events(row.invocation_id, request)));
139
141
  }
140
142
  landmarks(sessionId, throughOrdinal, limit) {
141
143
  assertOrdinal(throughOrdinal);
@@ -258,29 +260,35 @@ export class RuntimeTranscriptQuery {
258
260
  limit: bounds.limit,
259
261
  });
260
262
  }
261
- events(invocationId, limits) {
262
- // Walked row by row: the limits cap what one Turn may pull into memory, so
263
- // a check after `.all()` has already paid the cost it was meant to refuse.
263
+ *events(invocationId, limits) {
264
+ // Walked row by row so cumulative limits apply to raw IO without retaining
265
+ // the Turn. SQLite withholds an oversized payload before it crosses into JS.
264
266
  const cursor = this.db
265
267
  .prepare(`
266
- SELECT o.ordinal, e.event_id, e.session_id, e.invocation_id, e.run_id, e.turn_id, e.payload_json
268
+ SELECT o.ordinal, e.event_id, e.session_id, e.invocation_id, e.run_id, e.turn_id,
269
+ length(CAST(e.payload_json AS BLOB)) AS stored_bytes,
270
+ CASE WHEN length(CAST(e.payload_json AS BLOB)) <= ? THEN e.payload_json END AS payload_json
267
271
  FROM runtime_events e JOIN runtime_session_event_ordinals o ON o.event_id = e.event_id
268
272
  WHERE e.invocation_id = ? ORDER BY e.event_seq
269
273
  `)
270
- .iterate(invocationId);
271
- const events = [];
274
+ .iterate(limits.maxRecordBytes, invocationId);
275
+ let count = 0;
272
276
  let bytes = 0;
273
277
  for (const row of cursor) {
274
- if (events.length === limits.maxEvents) {
278
+ if (count === limits.maxEvents) {
275
279
  throw new RuntimeTranscriptOversizedTurnError(`Turn ${invocationId} holds more RuntimeEvents than a transcript page may read`);
276
280
  }
277
- bytes += Buffer.byteLength(row.payload_json);
281
+ const payload = row.payload_json;
282
+ if (payload === null) {
283
+ throw new RuntimeTranscriptOversizedTurnError(`Turn ${invocationId} holds a RuntimeEvent larger than a transcript page may read`);
284
+ }
285
+ bytes += row.stored_bytes;
278
286
  if (bytes > limits.maxBytes) {
279
287
  throw new RuntimeTranscriptOversizedTurnError(`Turn ${invocationId} holds more RuntimeEvent bytes than a transcript page may read`);
280
288
  }
281
- events.push({ ordinal: row.ordinal, event: decodeStoredEvent(row) });
289
+ count += 1;
290
+ yield { ordinal: row.ordinal, event: decodeStoredEvent({ ...row, payload_json: payload }) };
282
291
  }
283
- return events;
284
292
  }
285
293
  event(id) {
286
294
  const row = this.db
@@ -306,3 +314,8 @@ function assertOrdinal(value) {
306
314
  if (!Number.isSafeInteger(value) || value < 0)
307
315
  throw new Error('Invalid transcript event ordinal');
308
316
  }
317
+ function assertReadLimit(value, name) {
318
+ if (!Number.isSafeInteger(value) || value < 1) {
319
+ throw new RangeError(`Invalid transcript ${name} limit`);
320
+ }
321
+ }