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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/acp/maka-acp-agent.js +2 -1
  2. package/dist/acp/session-configuration.js +143 -0
  3. package/dist/acp/session-registry.js +90 -4
  4. package/dist/cli-core.js +2 -0
  5. package/dist/pi-transcript.js +5 -12
  6. package/dist/pi-tui-runner.js +74 -4
  7. package/dist/runtime-host-cli-context.js +167 -78
  8. package/dist/runtime-host-cli-installation.js +8 -2
  9. package/dist/runtime-host-cli.js +31 -1
  10. package/dist/runtime-host-handoff-surface.js +81 -0
  11. package/dist/runtime-host-installed-update-bootstrap.js +13 -0
  12. package/dist/runtime-host-installed-update-coordinator.js +33 -0
  13. package/dist/runtime-host-lifecycle-transaction.js +9 -1
  14. package/dist/runtime-host-local-handoff.js +37 -4
  15. package/dist/runtime-host-local-managed-activation.js +84 -0
  16. package/dist/runtime-host-local-source-retirement.js +18 -2
  17. package/dist/runtime-host-managed-lifecycle-manager.js +1 -0
  18. package/dist/runtime-host-operator-retirement.js +50 -0
  19. package/dist/runtime-host-service-manager.js +3 -0
  20. package/dist/runtime-host-session-driver.js +4 -25
  21. package/dist/runtime-host-session-update.js +62 -0
  22. package/dist/runtime-host-tui-command.js +38 -73
  23. package/dist/runtime-host-tui-context.js +1 -0
  24. package/dist/runtime-host-tui-owner.js +226 -0
  25. package/dist/tui-copy-catalog.js +140 -3
  26. package/dist/tui-host-owner-copy.js +24 -0
  27. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  28. package/node_modules/@maka/core/dist/canonical-runtime-event.js +3 -1
  29. package/node_modules/@maka/core/dist/model-failure.js +40 -0
  30. package/node_modules/@maka/core/dist/model-metadata.generated.js +4 -4
  31. package/node_modules/@maka/core/dist/model-metadata.js +3 -3
  32. package/node_modules/@maka/core/dist/model-web-search.js +5 -4
  33. package/node_modules/@maka/core/dist/provider-registry.js +45 -4
  34. package/node_modules/@maka/core/dist/record-schema.js +9 -2
  35. package/node_modules/@maka/core/dist/runtime-boundary.js +31 -11
  36. package/node_modules/@maka/core/dist/runtime-event.js +37 -6
  37. package/node_modules/@maka/core/dist/runtime-handoff.js +98 -0
  38. package/node_modules/@maka/core/dist/runtime-invocation.js +1 -1
  39. package/node_modules/@maka/core/dist/runtime-logical-execution.js +112 -0
  40. package/node_modules/@maka/core/dist/session.js +54 -52
  41. package/node_modules/@maka/core/dist/slash-command-catalog.js +2 -0
  42. package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +20 -0
  43. package/node_modules/@maka/core/dist/usage-record-schema.js +26 -35
  44. package/node_modules/@maka/core/package.json +4 -0
  45. package/node_modules/@maka/mcp/dist/index.js +131 -1
  46. package/node_modules/@maka/runtime/dist/agent-run.js +272 -105
  47. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +13 -5
  48. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +109 -34
  49. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +127 -133
  50. package/node_modules/@maka/runtime/dist/context-budget.js +14 -9
  51. package/node_modules/@maka/runtime/dist/conversation-copy.js +5 -4
  52. package/node_modules/@maka/runtime/dist/goal-continuation.js +36 -4
  53. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +18 -9
  54. package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +30 -22
  55. package/node_modules/@maka/runtime/dist/history-compaction.js +29 -4
  56. package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +106 -0
  57. package/node_modules/@maka/runtime/dist/message-authority.js +9 -0
  58. package/node_modules/@maka/runtime/dist/model-adapter.js +19 -98
  59. package/node_modules/@maka/runtime/dist/model-factory.js +19 -81
  60. package/node_modules/@maka/runtime/dist/model-fetcher.js +7 -0
  61. package/node_modules/@maka/runtime/dist/model-runtime.js +85 -111
  62. package/node_modules/@maka/runtime/dist/native-web-search-tool.js +2 -1
  63. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +2 -2
  64. package/node_modules/@maka/runtime/dist/opencode-session-header.js +4 -2
  65. package/node_modules/@maka/runtime/dist/provider-error-classification.js +84 -123
  66. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +68 -108
  67. package/node_modules/@maka/runtime/dist/run-handoff-gate.js +97 -0
  68. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +82 -54
  69. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +117 -159
  70. package/node_modules/@maka/runtime/dist/runtime-kernel.js +150 -112
  71. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +160 -79
  72. package/node_modules/@maka/runtime/dist/runtime-read-model.js +30 -89
  73. package/node_modules/@maka/runtime/dist/runtime-resume.js +80 -4
  74. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +5 -0
  75. package/node_modules/@maka/runtime/dist/session-manager.js +171 -288
  76. package/node_modules/@maka/runtime/dist/session-projection-helpers.js +0 -27
  77. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +3 -4
  78. package/node_modules/@maka/runtime/dist/test-connection.js +16 -16
  79. package/node_modules/@maka/runtime/dist/tool-result-archive-encoding.js +38 -0
  80. package/node_modules/@maka/runtime/dist/tool-runtime.js +3 -61
  81. package/node_modules/@maka/runtime/package.json +1 -0
  82. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +35 -0
  83. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +9 -1
  84. package/node_modules/@maka/runtime-host/dist/client/connection.js +3 -0
  85. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +156 -0
  86. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +273 -0
  87. package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +26 -5
  88. package/node_modules/@maka/runtime-host/dist/client/index.js +2 -0
  89. package/node_modules/@maka/runtime-host/dist/client/launcher.js +3 -0
  90. package/node_modules/@maka/runtime-host/dist/client/remote-compatibility-error.js +2 -0
  91. package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +27 -5
  92. package/node_modules/@maka/runtime-host/dist/operator/index.js +2 -1
  93. package/node_modules/@maka/runtime-host/dist/operator/retirement-cancellation.js +20 -0
  94. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +21 -12
  95. package/node_modules/@maka/runtime-host/dist/protocol/index.js +25 -1
  96. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  97. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +40 -20
  98. package/node_modules/@maka/runtime-host/dist/protocol/session-transcript.js +0 -5
  99. package/node_modules/@maka/runtime-host/dist/protocol/session-turns.js +29 -58
  100. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +11 -1
  101. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +1 -0
  102. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +19 -4
  103. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +7 -6
  104. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +100 -24
  105. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +5 -5
  106. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +3 -2
  107. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +40 -1
  108. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +96 -17
  109. package/node_modules/@maka/runtime-host/dist/server/host-residency-registry.js +13 -7
  110. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-admission-registry.js +3 -1
  111. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +124 -62
  112. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +76 -6
  113. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +325 -73
  114. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +37 -2
  115. package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +45 -3
  116. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +50 -7
  117. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +87 -25
  118. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +8 -55
  119. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +1 -16
  120. package/node_modules/@maka/runtime-host/dist/server/session-transcript-pager.js +0 -1
  121. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +460 -70
  122. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +1 -1
  123. package/node_modules/@maka/runtime-host/dist/server/storage-maintenance.js +109 -0
  124. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +0 -1
  125. package/node_modules/@maka/storage/dist/artifact-store.js +21 -10
  126. package/node_modules/@maka/storage/dist/artifact-stores.js +1 -1
  127. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -4
  128. package/node_modules/@maka/storage/dist/codex-session-adapter.js +0 -2
  129. package/node_modules/@maka/storage/dist/context-offload-snapshot.js +325 -0
  130. package/node_modules/@maka/storage/dist/execution-stores.js +9 -7
  131. package/node_modules/@maka/storage/dist/file-session-repository.js +1012 -0
  132. package/node_modules/@maka/storage/dist/opencode-session-adapter.js +0 -4
  133. package/node_modules/@maka/storage/dist/operational-state-backup.js +9 -5
  134. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  135. package/node_modules/@maka/storage/dist/runtime-transcript-query.js +308 -0
  136. package/node_modules/@maka/storage/dist/session-bundle-policy.js +24 -4
  137. package/node_modules/@maka/storage/dist/session-message-projection.js +15 -0
  138. package/node_modules/@maka/storage/dist/session-repository.js +767 -0
  139. package/node_modules/@maka/storage/dist/session-store.js +25 -99
  140. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +10 -3
  141. package/node_modules/@maka/storage/dist/sqlite-context-offload-store.js +24 -8
  142. package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +7 -1
  143. package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +44 -1
  144. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +129 -25
  145. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +109 -892
  146. package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +111 -0
  147. package/node_modules/@maka/storage/package.json +2 -0
  148. package/package.json +1 -1
@@ -17,6 +17,9 @@
17
17
  * under the License.
18
18
  */
19
19
  import { RUN_COMPOSITION_RECORDED_EVENT_TYPE } from '@maka/core/agent-run';
20
+ import { isRuntimeHandoffPause } from '@maka/core/runtime-handoff';
21
+ import { RunHandoffGate } from './run-handoff-gate.js';
22
+ import { preserveHandoffOpening } from './runtime-resume.js';
20
23
  import { decodeRunCompositionSnapshot } from '@maka/core/run-composition';
21
24
  import { DurableStoreWriteError, RunSealedError } from '@maka/core/runtime-event-store';
22
25
  import { buildInvocationOpenedEvent, buildSyntheticTerminalRuntimeEvent, isSessionInlineInvocation, } from '@maka/core/runtime-invocation';
@@ -26,16 +29,18 @@ import { ToolLedgerCorruptionError, ToolLedgerRejectionError, } from '@maka/core
26
29
  import { Buffer } from 'node:buffer';
27
30
  import { isDeepStrictEqual } from 'node:util';
28
31
  import { redactSecrets } from '@maka/core/redaction';
32
+ import { truncateUtf8 } from '@maka/core/diagnostic-log';
33
+ import { MODEL_FAILURE_MESSAGE_MAX_BYTES } from '@maka/core/model-failure';
29
34
  import { MODEL_CALL_ATTEMPT_EVENT_TYPE, } from '@maka/core/model-call-attempt';
30
35
  import { resolveEffectiveOrchestration, } from '@maka/core/orchestration';
31
- import { messageContentsEqual } from '@maka/core/events';
32
36
  import { buildPriorRuntimeContext as buildPriorRuntimeContextProjection, } from './prior-run-context.js';
33
37
  import { buildStatusPatch, normalizeStopSessionSource, statusFromEvent, turnStatusFromEvent, } from './session-projection-helpers.js';
38
+ import { admittedPromptEventId } from './message-authority.js';
34
39
  import { commitOrCreateTerminalRunFact } from './terminal-run-commit.js';
35
40
  import { createRuntimeContinuationStartAdmissionProof, } from './runtime-continuation-admission.js';
36
41
  import { DEFAULT_TOOL_MODE, isToolMode } from '@maka/core/tool-mode';
37
- import { materializeRuntimeEventTranscriptProjection } from './runtime-ledger-repair.js';
38
42
  import { cloneAndFreezeRuntimeSnapshot } from './runtime-snapshot.js';
43
+ import { projectRuntimeEventUserMessage } from './runtime-event-read-model.js';
39
44
  export class ContinuationStartCommitError extends Error {
40
45
  storeCause;
41
46
  name = 'ContinuationStartCommitError';
@@ -65,6 +70,7 @@ export class AgentRun {
65
70
  runStoreAvailable = true;
66
71
  runtimeEventStoreAvailable = true;
67
72
  runtimeEventStoreFailure;
73
+ lastAssistantPreview;
68
74
  runtimePartialStreamKey;
69
75
  runtimePartialBuffer = [];
70
76
  runtimePartialBufferBytes = 0;
@@ -72,6 +78,7 @@ export class AgentRun {
72
78
  traceWriteError;
73
79
  runComposition;
74
80
  runCompositionWrite;
81
+ runCompositionCommitted = false;
75
82
  failureClass;
76
83
  failureMessage;
77
84
  lastTs = 0;
@@ -79,11 +86,16 @@ export class AgentRun {
79
86
  finalStatus;
80
87
  turnFailed = false;
81
88
  finalized = false;
89
+ handoffGate = new RunHandoffGate();
90
+ handoffRequest;
91
+ handoffPaused = false;
82
92
  terminalRunFactCommitted = false;
83
93
  continuationActive = false;
84
94
  providerStateIdentity;
85
95
  invocationOpening;
86
96
  invocationOpeningCommitted = false;
97
+ /** Set once `begin()` owes this run's prompt, cleared once the ledger has it. */
98
+ initialRuntimeEventPending = false;
87
99
  terminalClaim;
88
100
  constructor(input) {
89
101
  const acceptedInput = {
@@ -92,6 +104,9 @@ export class AgentRun {
92
104
  ...(input.effectiveOrchestration
93
105
  ? { effectiveOrchestration: cloneAndFreezeRuntimeSnapshot(input.effectiveOrchestration) }
94
106
  : {}),
107
+ ...(input.handoffSourceComposition
108
+ ? { handoffSourceComposition: decodeRunCompositionSnapshot(input.handoffSourceComposition) }
109
+ : {}),
95
110
  };
96
111
  this.input = acceptedInput;
97
112
  if (acceptedInput.runStore && !acceptedInput.runtimeEventStore) {
@@ -140,12 +155,121 @@ export class AgentRun {
140
155
  return false;
141
156
  this.terminalClaim = { owner: 'stop' };
142
157
  this.stopped = true;
158
+ this.handoffGate.close();
143
159
  this.abortSource = abortSource;
144
160
  return true;
145
161
  }
146
162
  isStopped() {
147
163
  return this.stopped;
148
164
  }
165
+ requestHandoff(pause, signal) {
166
+ if (this.handoffRequest ||
167
+ this.finalized ||
168
+ this.terminalClaim ||
169
+ this.input.runtimeEventStore?.durability !== 'canonical' ||
170
+ !this.toolBoundaryProtocol ||
171
+ Object.hasOwn(pause, 'remainingSteps') ||
172
+ !isRuntimeHandoffPause({ ...pause, remainingSteps: null }) ||
173
+ !this.invocationOpening ||
174
+ pause.rootRunId !==
175
+ (this.invocationOpening.source.kind === 'handoff'
176
+ ? this.invocationOpening.source.rootRunId
177
+ : this.runId)) {
178
+ throw new Error('Run cannot reserve a cooperative handoff');
179
+ }
180
+ let settle;
181
+ let fail;
182
+ const sealed = new Promise((resolve, reject) => {
183
+ settle = resolve;
184
+ fail = reject;
185
+ });
186
+ void sealed.catch(() => { });
187
+ const pending = {
188
+ pause: cloneAndFreezeRuntimeSnapshot(pause),
189
+ preview: undefined,
190
+ committed: false,
191
+ settle: (value) => {
192
+ signal.removeEventListener('abort', onAbort);
193
+ settle(value);
194
+ },
195
+ fail: (error) => {
196
+ signal.removeEventListener('abort', onAbort);
197
+ fail(error);
198
+ },
199
+ };
200
+ this.handoffRequest = pending;
201
+ const gate = this.handoffGate.request(signal);
202
+ const cancel = () => {
203
+ if (pending.committed || this.handoffRequest !== pending)
204
+ return;
205
+ gate.cancel();
206
+ this.handoffRequest = undefined;
207
+ pending.settle(false);
208
+ };
209
+ const onAbort = () => cancel();
210
+ signal.addEventListener('abort', onAbort, { once: true });
211
+ if (signal.aborted)
212
+ cancel();
213
+ void gate.ready.then((ready) => {
214
+ if (!ready)
215
+ cancel();
216
+ });
217
+ return {
218
+ ready: gate.ready,
219
+ sealed,
220
+ cancel,
221
+ preview: () => {
222
+ if (this.handoffRequest !== pending ||
223
+ this.stopped ||
224
+ pending.committed ||
225
+ !pending.preview) {
226
+ throw new Error('Handoff preview requires the currently held Runtime boundary');
227
+ }
228
+ this.assertRunCompositionCommitted();
229
+ return pending.preview;
230
+ },
231
+ commit: () => {
232
+ this.assertRunCompositionCommitted();
233
+ if (this.handoffRequest !== pending || !gate.commit())
234
+ return false;
235
+ pending.committed = true;
236
+ return true;
237
+ },
238
+ };
239
+ }
240
+ async reachHandoffBoundary(signal, remainingSteps) {
241
+ if (remainingSteps !== null && (!Number.isSafeInteger(remainingSteps) || remainingSteps <= 0)) {
242
+ throw new Error('Invalid handoff step budget');
243
+ }
244
+ const pending = this.handoffRequest;
245
+ if (pending) {
246
+ pending.preview = cloneAndFreezeRuntimeSnapshot({
247
+ id: this.input.newId(),
248
+ sessionId: this.sessionId,
249
+ runId: this.runId,
250
+ invocationId: this.invocationId,
251
+ turnId: this.turnId,
252
+ ts: this.input.now(),
253
+ partial: false,
254
+ role: 'system',
255
+ author: 'host',
256
+ modelVisibility: 'hidden',
257
+ actions: { endInvocation: true, handoffPause: { ...pending.pause, remainingSteps } },
258
+ });
259
+ }
260
+ const decision = await this.handoffGate.reachBoundary(signal);
261
+ if (decision === 'pause') {
262
+ this.handoffPaused = true;
263
+ }
264
+ return decision;
265
+ }
266
+ hasCommittedHandoff() {
267
+ return (this.handoffPaused &&
268
+ this.handoffRequest?.committed === true &&
269
+ !this.stopped &&
270
+ !this.failureClass &&
271
+ !this.terminalClaim);
272
+ }
149
273
  headerSnapshot() {
150
274
  return this.header;
151
275
  }
@@ -251,6 +375,15 @@ export class AgentRun {
251
375
  return Promise.reject(new Error('AgentRun store is not configured'));
252
376
  }
253
377
  const normalized = decodeRunCompositionSnapshot(snapshot);
378
+ const expected = this.input.handoffSourceComposition;
379
+ if (expected &&
380
+ (normalized.baseSystemPromptHash !== expected.baseSystemPromptHash ||
381
+ normalized.toolCatalogHash !== expected.toolCatalogHash ||
382
+ normalized.toolAvailabilityHash !== expected.toolAvailabilityHash ||
383
+ normalized.baseProviderOptionsHash !== expected.baseProviderOptionsHash ||
384
+ normalized.contextWindow !== expected.contextWindow)) {
385
+ return Promise.reject(new Error('Handoff Run Composition execution semantics changed'));
386
+ }
254
387
  if (this.runComposition && !isDeepStrictEqual(this.runComposition, normalized)) {
255
388
  return Promise.reject(new Error('AgentRun Run Composition changed after resolution'));
256
389
  }
@@ -266,7 +399,12 @@ export class AgentRun {
266
399
  turnId: this.turnId,
267
400
  ts: this.input.now(),
268
401
  data: { runComposition: normalized },
269
- }, { durable: this.requiresDurablePersistence() });
402
+ }, {
403
+ durable: this.requiresDurablePersistence() ||
404
+ this.input.runtimeEventStore?.durability === 'canonical',
405
+ });
406
+ }).then(() => {
407
+ this.runCompositionCommitted = true;
270
408
  });
271
409
  this.runCompositionWrite = write;
272
410
  return write.catch((error) => {
@@ -275,6 +413,11 @@ export class AgentRun {
275
413
  throw error;
276
414
  });
277
415
  }
416
+ assertRunCompositionCommitted() {
417
+ if (!this.runCompositionCommitted) {
418
+ throw new Error('Cooperative handoff requires a durably committed Run Composition');
419
+ }
420
+ }
278
421
  /**
279
422
  * Canonical accounting record for one physical provider request (#1679).
280
423
  *
@@ -406,8 +549,10 @@ export class AgentRun {
406
549
  requireTerminalWrite: options.requireTerminalWrite ?? Boolean(this.input.runtimeEventStore),
407
550
  });
408
551
  await this.recordSessionEvent(sessionEvent, options);
552
+ await this.commitMessageProjection(this.lastAssistantPreview);
409
553
  return;
410
554
  }
555
+ this.rememberAssistantPreview(runtimeEvent);
411
556
  if (this.requiresDurablePersistence() && isInteractionResumeAck(sessionEvent)) {
412
557
  // A hosted continuation may resume execution only after its identity-only
413
558
  // settlement fact is durable. Session status advances next, and the queue
@@ -435,48 +580,54 @@ export class AgentRun {
435
580
  // stays fail-open (a trace gap, not a correctness gap).
436
581
  const steering = runtimeEvent.content?.kind === 'text' && runtimeEvent.content.steering === true;
437
582
  await this.recordRuntimeEvents([runtimeEvent], steering ? { requireDurableWrite: true } : {});
438
- await materializeRuntimeEventTranscriptProjection(this.input.store, this.sessionId, runtimeEvent);
583
+ if (steering) {
584
+ await this.commitMessageProjection(projectRuntimeEventUserMessage(runtimeEvent, runtimeEvent.id));
585
+ }
439
586
  }
440
587
  }
588
+ /**
589
+ * A user message is fail-CLOSED: it also takes the Session's connection lock,
590
+ * and no other path re-derives that latch now that the transcript is not a
591
+ * second authority. An assistant preview is fail-open — losing it costs a
592
+ * stale sidebar entry, never the turn.
593
+ */
594
+ async commitMessageProjection(message) {
595
+ const commit = this.input.hooks.commitMessageProjection;
596
+ if (!commit || !message)
597
+ return;
598
+ const committed = commit.call(this.input.hooks, this.sessionId, message);
599
+ if (message.type === 'user')
600
+ return committed;
601
+ await committed.catch(() => { });
602
+ }
603
+ /**
604
+ * The assistant text the Session list shows once the Turn ends.
605
+ *
606
+ * Kept as the run goes so the catalog costs one write per Turn rather than
607
+ * one per streamed step, and read only after the terminal fact is durable —
608
+ * a Turn that never spoke leaves the previous preview standing.
609
+ */
610
+ rememberAssistantPreview(event) {
611
+ if (event.role !== 'model' || event.content?.kind !== 'text')
612
+ return;
613
+ if (!event.content.text?.trim())
614
+ return;
615
+ this.lastAssistantPreview = {
616
+ type: 'assistant',
617
+ id: event.id,
618
+ turnId: event.turnId,
619
+ ts: event.ts,
620
+ text: event.content.text,
621
+ modelId: this.header.model,
622
+ };
623
+ }
441
624
  async begin() {
625
+ // Owed from here, not from after the opening: `openInvocation` can leave the
626
+ // invocation open and still throw, and `finalize` reopens what it can.
627
+ this.initialRuntimeEventPending = true;
442
628
  await this.openInvocation();
443
- let initialRuntimeEventId;
444
- const userMessageTs = this.input.now();
445
- if (this.input.userMessageId === null) {
446
- initialRuntimeEventId = this.input.newId();
447
- }
448
- else {
449
- const userMessageId = this.input.userMessageId ?? this.input.newId();
450
- initialRuntimeEventId = userMessageId;
451
- const userMsg = cloneAndFreezeRuntimeSnapshot({
452
- type: 'user',
453
- id: userMessageId,
454
- turnId: this.turnId,
455
- ts: userMessageTs,
456
- text: this.input.userInput.text,
457
- ...(this.input.userInput.displayText !== undefined
458
- ? { displayText: this.input.userInput.displayText }
459
- : {}),
460
- ...(this.input.userInput.attachments
461
- ? { attachments: this.input.userInput.attachments }
462
- : {}),
463
- ...(this.input.userInput.directoryReferences
464
- ? { directoryReferences: this.input.userInput.directoryReferences }
465
- : {}),
466
- ...(this.input.userInput.quotes ? { quotes: this.input.userInput.quotes } : {}),
467
- ...(this.input.userInput.inlineReferences
468
- ? { inlineReferences: this.input.userInput.inlineReferences }
469
- : {}),
470
- ...(this.input.userInput.origin ? { origin: this.input.userInput.origin } : {}),
471
- });
472
- await appendUserMessageOnce(this.input.store, this.sessionId, userMsg);
473
- }
474
- await this.input.hooks.appendTurnState(this.sessionId, this.turnId, 'running', this.lineage);
475
- this.lastTs = userMessageTs;
476
- const initialRuntimeEvent = cloneAndFreezeRuntimeSnapshot(this.buildInitialRuntimeEvent(initialRuntimeEventId, this.lastTs));
477
- await this.recordRuntimeEvents([initialRuntimeEvent], {
478
- requireDurableWrite: this.requiresDurablePersistence(),
479
- });
629
+ this.lastTs = this.input.now();
630
+ const initialRuntimeEvent = await this.recordInitialRuntimeEvent(this.lastTs);
480
631
  this.active = await this.input.hooks.reserveRun(this.sessionId, this.header, this);
481
632
  await this.input.hooks.updateStatus(this.sessionId, 'running', undefined, this.lastTs);
482
633
  const priorRuntimeContext = await this.buildPriorRuntimeContext();
@@ -507,13 +658,23 @@ export class AgentRun {
507
658
  initialRuntimeEvent,
508
659
  };
509
660
  }
661
+ /** Say what this run was asked to do. */
662
+ async recordInitialRuntimeEvent(ts) {
663
+ const event = cloneAndFreezeRuntimeSnapshot(this.buildInitialRuntimeEvent(admittedPromptEventId(this.runId, this.input.userMessageId), ts));
664
+ await this.recordRuntimeEvents([event], {
665
+ requireDurableWrite: this.requiresDurablePersistence(),
666
+ });
667
+ // Owed until the catalog carries it too, not just until the ledger does:
668
+ // the projection is where the connection lock latches, and re-recording the
669
+ // event is free because its id is derived and the store dedupes it.
670
+ await this.commitMessageProjection(projectRuntimeEventUserMessage(event, event.id));
671
+ this.initialRuntimeEventPending = false;
672
+ return event;
673
+ }
510
674
  async beginOperation() {
511
675
  await this.openInvocation();
512
676
  const startedAt = this.input.now();
513
677
  this.lastTs = startedAt;
514
- await this.input.hooks.appendTurnState(this.sessionId, this.turnId, 'running', this.lineage, {
515
- ts: startedAt,
516
- });
517
678
  this.active = await this.input.hooks.reserveRun(this.sessionId, this.header, this);
518
679
  await this.input.hooks.updateStatus(this.sessionId, 'running', undefined, startedAt);
519
680
  const priorRuntimeContext = await this.buildPriorRuntimeContext();
@@ -545,9 +706,6 @@ export class AgentRun {
545
706
  throw new ContinuationStartCommitError(error);
546
707
  }
547
708
  await this.input.continuationFailpoint?.('after_continuation_start_committed');
548
- await this.input.hooks.appendTurnState(this.sessionId, this.turnId, 'running', this.lineage, {
549
- ts: startedAt,
550
- });
551
709
  this.active = await this.input.hooks.reserveRun(this.sessionId, this.header, this);
552
710
  await this.input.hooks.updateStatus(this.sessionId, 'running', undefined, startedAt);
553
711
  return {
@@ -603,10 +761,29 @@ export class AgentRun {
603
761
  : {}),
604
762
  };
605
763
  }
606
- async recordStoredSessionEvent(ev) {
607
- if (ev.type === 'token_usage') {
608
- await this.input.store.appendMessage(this.sessionId, { ...ev });
609
- }
764
+ /**
765
+ * Record something the runtime needs to tell the reader about this turn.
766
+ *
767
+ * It is a fact of the invocation, so it goes where the invocation's facts go.
768
+ * Never model-visible: the note describes what happened to the conversation,
769
+ * it is not part of it.
770
+ */
771
+ async recordSystemNote(kind, data) {
772
+ await this.recordRuntimeEvents([
773
+ {
774
+ id: this.input.newId(),
775
+ invocationId: this.invocationId,
776
+ runId: this.runId,
777
+ sessionId: this.sessionId,
778
+ turnId: this.turnId,
779
+ ts: this.input.now(),
780
+ partial: false,
781
+ role: 'system',
782
+ author: 'system',
783
+ modelVisibility: 'hidden',
784
+ content: { kind: 'system_note', note: kind, ...(data !== undefined ? { data } : {}) },
785
+ },
786
+ ]);
610
787
  }
611
788
  async recordSessionEvent(ev, options = {}) {
612
789
  this.lastTs = ev.ts;
@@ -631,9 +808,9 @@ export class AgentRun {
631
808
  this.markRunFailed(turnStatus.errorClass, `turn ended with stopReason=${ev.type === 'complete' ? ev.stopReason : 'unknown'}`);
632
809
  }
633
810
  }
634
- if (transition && !this.stopped) {
811
+ if (transition && !this.stopped && ev.type !== 'error') {
635
812
  const updateSessionStatus = async () => {
636
- if (terminalSessionEvent || ev.type === 'error') {
813
+ if (terminalSessionEvent) {
637
814
  await this.input.hooks
638
815
  .updateStatus(this.sessionId, transition.status, transition.blockedReason, ev.ts)
639
816
  .catch((error) => this.enqueueTraceWriteFailure(error, 'terminal session projection'));
@@ -643,21 +820,6 @@ export class AgentRun {
643
820
  };
644
821
  await updateSessionStatus();
645
822
  }
646
- if (turnStatus && !this.stopped) {
647
- const appendTurnState = this.input.hooks.appendTurnState(this.sessionId, this.turnId, turnStatus.status, this.lineage, {
648
- ts: ev.ts,
649
- errorClass: turnStatus.errorClass,
650
- ...(turnStatus.status === 'aborted' && this.abortSource
651
- ? { abortSource: this.abortSource }
652
- : {}),
653
- });
654
- if (terminalSessionEvent || ev.type === 'error') {
655
- await appendTurnState.catch((error) => this.enqueueTraceWriteFailure(error, 'terminal session projection'));
656
- }
657
- else {
658
- await appendTurnState;
659
- }
660
- }
661
823
  if (ev.type === 'error') {
662
824
  if (this.stopped) {
663
825
  this.finalStatus = { status: 'aborted' };
@@ -665,12 +827,6 @@ export class AgentRun {
665
827
  else {
666
828
  this.turnFailed = true;
667
829
  this.finalStatus = transition ?? { status: 'blocked', blockedReason: 'unknown' };
668
- await this.input.hooks
669
- .appendTurnState(this.sessionId, this.turnId, 'failed', this.lineage, {
670
- ts: ev.ts,
671
- errorClass: ev.reason ?? ev.code ?? 'unknown',
672
- })
673
- .catch((error) => this.enqueueTraceWriteFailure(error, 'terminal session projection'));
674
830
  this.markRunFailed(ev.reason ?? ev.code ?? 'unknown', ev.message);
675
831
  }
676
832
  }
@@ -766,17 +922,40 @@ export class AgentRun {
766
922
  return;
767
923
  }
768
924
  this.finalStatus = { status: 'blocked', blockedReason: 'unknown' };
769
- await this.input.hooks
770
- .appendTurnState(this.sessionId, this.turnId, 'failed', this.lineage, {
771
- errorClass: error instanceof Error ? error.name : 'unknown',
772
- })
773
- .catch(() => { });
774
- this.markRunFailed(error instanceof Error ? error.name : 'unknown', errorMessage(error));
925
+ this.markRunFailed(error instanceof Error ? error.name : 'unknown', error instanceof Error ? error.message : String(error));
775
926
  }
776
927
  async finalize() {
777
928
  if (this.finalized)
778
929
  return;
779
930
  this.finalized = true;
931
+ this.handoffGate.close();
932
+ const handoff = this.handoffRequest;
933
+ if (this.handoffPaused &&
934
+ handoff?.committed &&
935
+ !this.stopped &&
936
+ !this.failureClass &&
937
+ !this.terminalClaim) {
938
+ try {
939
+ await this.flushRuntimePartialBuffer(true);
940
+ // Stop may have claimed the logical outcome during the flush. Reserving
941
+ // the pause below is synchronous up to its first write, so only one wins.
942
+ if (!this.stopped && !this.failureClass && !this.terminalClaim) {
943
+ this.assertRunCompositionCommitted();
944
+ await this.recordRuntimeEvents([handoff.preview], { requireTerminalWrite: true });
945
+ this.terminalRunFactCommitted = true;
946
+ if (this.active)
947
+ await this.input.hooks.unregisterRun(this.active, this);
948
+ await this.traceQueue;
949
+ handoff.settle(true);
950
+ return;
951
+ }
952
+ }
953
+ catch (error) {
954
+ handoff.fail(error);
955
+ throw error;
956
+ }
957
+ }
958
+ handoff?.settle(false);
780
959
  // A run cannot end without having begun. Finalizing one that never reached
781
960
  // its start would otherwise leave a terminal event on an invocation the
782
961
  // inventory cannot see, because nothing opened it. A continuation is the
@@ -784,6 +963,13 @@ export class AgentRun {
784
963
  // and a continuation that never committed one has no invocation to end.
785
964
  if (!this.input.commitContinuationStart)
786
965
  await this.openInvocation().catch(() => { });
966
+ // A run also cannot end without saying what it was asked to do. `begin()`
967
+ // can fail between opening the invocation and recording its prompt, and
968
+ // the terminal event below seals the run against every later append —
969
+ // including the one crash recovery would use to repair the same shape.
970
+ if (this.initialRuntimeEventPending) {
971
+ await this.recordInitialRuntimeEvent(this.lastTs || this.input.now()).catch(() => { });
972
+ }
787
973
  await this.flushRuntimePartialBuffer(true);
788
974
  const lastTs = this.lastTs || this.input.now();
789
975
  if (this.stopped)
@@ -809,17 +995,6 @@ export class AgentRun {
809
995
  catch {
810
996
  // The user-visible turn already completed; preserve existing behavior.
811
997
  }
812
- if (this.sawCompletion) {
813
- await this.input.store
814
- .appendMessage(this.sessionId, {
815
- type: 'system_note',
816
- id: this.input.newId(),
817
- turnId: this.turnId,
818
- ts: lastTs,
819
- kind: 'session_resume',
820
- })
821
- .catch(() => { });
822
- }
823
998
  await this.finishRun(this.finalStatus, lastTs);
824
999
  }
825
1000
  async openInvocation(continuation) {
@@ -864,7 +1039,7 @@ export class AgentRun {
864
1039
  ...(this.input.userInput.agentName ? { agentName: this.input.userInput.agentName } : {}),
865
1040
  ...(continuation ? { parentRunId: continuation.sourceRunId } : {}),
866
1041
  };
867
- return {
1042
+ const opening = {
868
1043
  kind: 'invocation_opened',
869
1044
  protocol: 'invocation_opened_v1',
870
1045
  route: this.header.llmConnectionId === undefined
@@ -912,6 +1087,9 @@ export class AgentRun {
912
1087
  : { kind: 'fresh' },
913
1088
  ...(Object.keys(lineage).length > 0 ? { lineage } : {}),
914
1089
  };
1090
+ return continuation
1091
+ ? preserveHandoffOpening(continuation, opening, this.input.handoffSourceOpening)
1092
+ : opening;
915
1093
  }
916
1094
  invocationRootAuthority() {
917
1095
  const origin = this.input.userInput.origin;
@@ -976,12 +1154,13 @@ export class AgentRun {
976
1154
  /**
977
1155
  * Remember why this run is going to fail.
978
1156
  *
1157
+ * Preserve diagnostic text without trace redaction, within the byte budget.
979
1158
  * Nothing is written here: the terminal RuntimeEvent carries the failure, and
980
1159
  * it is committed once, at the end, by `commitTerminalRun`.
981
1160
  */
982
1161
  markRunFailed(failureClass, message) {
983
1162
  this.failureClass = failureClass;
984
- this.failureMessage = redactTraceString(message);
1163
+ this.failureMessage = truncateUtf8(message, MODEL_FAILURE_MESSAGE_MAX_BYTES, '…');
985
1164
  }
986
1165
  /**
987
1166
  * End the run by committing its terminal RuntimeEvent, and nothing else.
@@ -1394,18 +1573,6 @@ function redactTraceString(value) {
1394
1573
  function errorMessage(error) {
1395
1574
  return redactTraceString(error instanceof Error ? error.message : String(error));
1396
1575
  }
1397
- async function appendUserMessageOnce(store, sessionId, message) {
1398
- const existing = (await store.readMessages(sessionId)).find((candidate) => candidate.id === message.id);
1399
- if (!existing) {
1400
- await store.appendMessage(sessionId, message);
1401
- return;
1402
- }
1403
- if (existing.type !== 'user' ||
1404
- existing.turnId !== message.turnId ||
1405
- !messageContentsEqual(existing, message)) {
1406
- throw new Error(`Durable UserMessage identity ${message.id} has conflicting content`);
1407
- }
1408
- }
1409
1576
  function isInteractionResumeAck(event) {
1410
1577
  return (event.type === 'sandbox_boundary_decision_ack' ||
1411
1578
  event.type === 'user_question_answer_ack' ||
@@ -29,7 +29,7 @@ import { AiSdkTurn } from './ai-sdk-turn.js';
29
29
  import { buildInvalidMakaTool } from './ai-sdk-tool-repair.js';
30
30
  import { ToolAvailabilityRuntime } from './tool-availability.js';
31
31
  import { MEMORY_EXTRACT_TOOL_NAME, MEMORY_REMEMBER_TOOL_NAME, buildMemoryExtractionTriggerTools, } from './memory-extraction.js';
32
- import { modelUsesNativeOpenAiResponses, resolveModelRuntime } from './model-runtime.js';
32
+ import { resolveModelRuntime } from './model-runtime.js';
33
33
  import { routeApplyPatchTools } from './apply-patch-profile.js';
34
34
  import { bindToolResultArchiveDecoder } from './tool-result-archive-capability.js';
35
35
  import { resolveSelectedModelContextWindow } from './context-budget-policy.js';
@@ -111,15 +111,17 @@ export class AiSdkBackend {
111
111
  // One resolved options value for every reader: the main call, the
112
112
  // auxiliary memory-extraction call, and the provider request all use the
113
113
  // same options value, so they cannot disagree on what was sent.
114
+ const runtime = resolveModelRuntime(input.connection, input.modelId);
114
115
  this.resolvedProviderOptions =
115
116
  input.providerOptions ??
116
- buildProviderOptions(input.connection, input.modelId, input.header.thinkingLevel);
117
+ buildProviderOptions(input.connection, input.modelId, input.header.thinkingLevel, runtime);
117
118
  this.modelAdapter = new ModelAdapter({
118
119
  sessionId: input.sessionId,
119
120
  connection: input.connection,
120
121
  apiKey: input.apiKey,
121
122
  modelId: input.modelId,
122
123
  modelFactory: input.modelFactory,
124
+ resolvedRuntime: runtime,
123
125
  // `input.providerOptions` is an override escape hatch: when set it owns
124
126
  // the whole provider-options namespace (including reasoning effort), and
125
127
  // the computed defaults are dropped entirely. Keep providerOptions the
@@ -144,7 +146,6 @@ export class AiSdkBackend {
144
146
  assertModelCallAccountingReady: input.assertModelCallAccountingReady,
145
147
  beforeRunProviderDispatch: input.beforeRunProviderDispatch,
146
148
  });
147
- const runtime = resolveModelRuntime(input.connection, input.modelId);
148
149
  const applyPatchProfile = runtime.applyPatchProfile;
149
150
  this.messageProjection = new AiSdkMessageProjection({
150
151
  modelAdapter: this.modelAdapter,
@@ -176,7 +177,7 @@ export class AiSdkBackend {
176
177
  if (turn)
177
178
  turn.memoryExtractRequested = true;
178
179
  },
179
- ...(modelUsesNativeOpenAiResponses(input.connection, input.modelId)
180
+ ...(input.connection.providerType === 'openai' && runtime.wire === 'openai-responses'
180
181
  ? { unsupportedReason: 'provider_unsupported' }
181
182
  : {}),
182
183
  })
@@ -206,11 +207,11 @@ export class AiSdkBackend {
206
207
  createToolRuntime(identity) {
207
208
  const input = this.input;
208
209
  return new ToolRuntime({
210
+ inheritedSandboxBoundaryDenied: identity.inheritedSandboxBoundaryDenied,
209
211
  sessionId: input.sessionId,
210
212
  header: input.header,
211
213
  connection: input.connection,
212
214
  modelId: input.modelId,
213
- appendMessage: input.appendMessage,
214
215
  readExecutionBoundary: input.readExecutionBoundary,
215
216
  createSandboxBoundaryRequest: input.createSandboxBoundaryRequest,
216
217
  settleSandboxBoundaryRequest: input.settleSandboxBoundaryRequest,
@@ -241,6 +242,12 @@ export class AiSdkBackend {
241
242
  // --------------------------------------------------------------------------
242
243
  // send()
243
244
  // --------------------------------------------------------------------------
245
+ async prepareRunComposition(input) {
246
+ if (!this.input.beforeRunProviderDispatch) {
247
+ throw new Error('Backend has no durable Run Composition preparation authority');
248
+ }
249
+ await this.input.beforeRunProviderDispatch({ sessionId: this.sessionId, ...input });
250
+ }
244
251
  openTurnScope(input) {
245
252
  const turn = new AiSdkTurn({
246
253
  backend: this.input,
@@ -257,6 +264,7 @@ export class AiSdkBackend {
257
264
  maxSteps: this.maxSteps,
258
265
  providerRetrySleep: this.providerRetrySleep,
259
266
  createToolRuntime: (owner) => this.createToolRuntime({
267
+ inheritedSandboxBoundaryDenied: input.continuation?.sandboxBoundaryDenied === true,
260
268
  turnId: owner.turnId,
261
269
  runId: owner.runId,
262
270
  invocationId: input.invocationId ?? input.runId,