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
@@ -18,6 +18,8 @@
18
18
  */
19
19
  import { createHash, randomUUID } from 'node:crypto';
20
20
  import { isDeepStrictEqual } from 'node:util';
21
+ import { readLogicalRuntimeExecution, readLogicalRuntimeExecutionForRun, } from '@maka/core/runtime-logical-execution';
22
+ import { runtimeHandoffPause } from '@maka/core/runtime-handoff';
21
23
  import { INLINE_REFERENCE_MAX_COUNT, messageContentDigest, messageContentsEqual, normalizeMessageContent, } from '@maka/core/events';
22
24
  import { isWorkHubCoordinationSessionId } from '@maka/core/session';
23
25
  import { resolveEffectiveOrchestration } from '@maka/core/orchestration';
@@ -58,6 +60,8 @@ export class RootTurnCoordinator {
58
60
  };
59
61
  #executions = new HostedExecutionRegistry();
60
62
  #admissions;
63
+ #handoffHold;
64
+ #cancelHandoff;
61
65
  #recoveryPlansBySession = new Map();
62
66
  stores;
63
67
  executionProjection;
@@ -117,6 +121,8 @@ export class RootTurnCoordinator {
117
121
  }
118
122
  }
119
123
  else {
124
+ if (plan.rootReplayAdmission === admission)
125
+ continue;
120
126
  if (admission.execution.kind !== 'safe_boundary_continuation') {
121
127
  throw new Error(`Startup recovery left Turn ${admission.turnId} non-terminal`);
122
128
  }
@@ -127,7 +133,11 @@ export class RootTurnCoordinator {
127
133
  if (!admission)
128
134
  continue;
129
135
  // Session recovery may have materialized this Run after the replay plan was prepared.
130
- if (await this.readRunIfPresent(sessionId, admission.runId))
136
+ const existingRun = await this.readRunIfPresent(sessionId, admission.runId);
137
+ const pendingHandoff = existingRun &&
138
+ (await readLogicalRuntimeExecution(this.stores.runtimeEventStore, admission, existingRun))
139
+ ?.pendingHandoff;
140
+ if (existingRun && !pendingHandoff)
131
141
  continue;
132
142
  const input = activationInputForAdmission(admission);
133
143
  const disposition = await this.sessionAdmission.run(sessionId, async (lease) => {
@@ -138,7 +148,7 @@ export class RootTurnCoordinator {
138
148
  return undefined;
139
149
  }
140
150
  }
141
- const continuation = admission.execution.kind === 'safe_boundary_continuation'
151
+ const continuation = admission.execution.kind === 'safe_boundary_continuation' && !pendingHandoff
142
152
  ? await this.reconstructAdmittedContinuation(admission)
143
153
  : undefined;
144
154
  if (continuation?.disposition === 'parked') {
@@ -169,6 +179,7 @@ export class RootTurnCoordinator {
169
179
  this.#recoveryPlansBySession.clear();
170
180
  }
171
181
  async close() {
182
+ this.#cancelHandoff?.();
172
183
  this.beginDrain();
173
184
  await this.#admissions.waitForSettledAdmissions();
174
185
  const errors = [];
@@ -221,7 +232,9 @@ export class RootTurnCoordinator {
221
232
  runId: active.runId,
222
233
  };
223
234
  }
224
- return this.#admissions.has(sessionId) ? { kind: 'reserved' } : { kind: 'idle' };
235
+ return this.#handoffHold || this.#admissions.has(sessionId)
236
+ ? { kind: 'reserved' }
237
+ : { kind: 'idle' };
225
238
  }
226
239
  /** Returns the newest Host-admitted continuation descended from one root execution. */
227
240
  async readLatestRootTurnLineage(identity) {
@@ -231,7 +244,11 @@ export class RootTurnCoordinator {
231
244
  }
232
245
  let latest = origin;
233
246
  while (true) {
234
- const continuation = await this.stores.agentRunStore.readRootTurnContinuationAdmission(identity.sessionId, latest.turnId, latest.runId);
247
+ const run = await this.readRunIfPresent(latest.sessionId, latest.runId);
248
+ const physical = run?.terminalEvent && runtimeHandoffPause(run.terminalEvent)
249
+ ? (await readLogicalRuntimeExecution(this.stores.runtimeEventStore, latest, run))?.tip
250
+ : run;
251
+ const continuation = await this.stores.agentRunStore.readRootTurnContinuationAdmission(identity.sessionId, latest.turnId, physical?.runId ?? latest.runId);
235
252
  if (!continuation)
236
253
  break;
237
254
  latest = continuation;
@@ -249,12 +266,16 @@ export class RootTurnCoordinator {
249
266
  }, context);
250
267
  }
251
268
  reserveRootTurn(sessionId) {
269
+ if (this.#handoffHold)
270
+ return undefined;
252
271
  return this.#admissions.reserve(sessionId);
253
272
  }
254
273
  parkContinuationAdmission(admission) {
255
274
  this.#admissions.park(admission);
256
275
  }
257
276
  takeParkedContinuationReservation(admission) {
277
+ if (this.#handoffHold)
278
+ return undefined;
258
279
  return this.#admissions.takeParked(admission);
259
280
  }
260
281
  clearParkedContinuationAdmission(admission) {
@@ -272,6 +293,163 @@ export class RootTurnCoordinator {
272
293
  beginDrain() {
273
294
  this.#admissions.beginDrain();
274
295
  }
296
+ async prepareHandoff(hostEpoch, signal) {
297
+ if (this.#handoffHold || this.#admissions.isDraining || signal.aborted)
298
+ return undefined;
299
+ const hold = deferred();
300
+ this.#handoffHold = hold;
301
+ const held = new Map();
302
+ let sealed = false;
303
+ let detached = false;
304
+ const releaseHold = () => {
305
+ if (this.#handoffHold === hold) {
306
+ this.#handoffHold = undefined;
307
+ this.#cancelHandoff = undefined;
308
+ }
309
+ hold.resolve();
310
+ signal.removeEventListener('abort', cancel);
311
+ };
312
+ const cancel = () => {
313
+ if (detached)
314
+ return;
315
+ sealed = false;
316
+ for (const [active, { request, attachment }] of held) {
317
+ request.cancel();
318
+ attachment.arrived.resolve();
319
+ attachment.decision.resolve('resume');
320
+ if (active.handoff === attachment)
321
+ active.handoff = undefined;
322
+ }
323
+ releaseHold();
324
+ };
325
+ this.#cancelHandoff = cancel;
326
+ signal.addEventListener('abort', cancel, { once: true });
327
+ try {
328
+ await waitForHostedExecutionIdleOrAbort(this.#admissions.waitForSettledAdmissions(), signal);
329
+ // Natural completion can hand a queued follow-up to a fresh root while
330
+ // gates converge. Bound that convergence; never chase an endless queue.
331
+ for (let pass = 0; pass < 3; pass += 1) {
332
+ for (const [, active] of this.#executions.entries()) {
333
+ if (held.has(active))
334
+ continue;
335
+ await waitForHostedExecutionIdleOrAbort(active.startSettled.promise, signal);
336
+ if (this.#executions.get(active.sessionId) !== active)
337
+ continue;
338
+ const request = this.manager.requestRunHandoff(active.sessionId, active.continuation?.runId ?? active.runId, {
339
+ protocol: 'runtime_handoff_pause_v1',
340
+ hostEpoch,
341
+ handoffId: randomUUID(),
342
+ rootRunId: active.runId,
343
+ successorRunId: randomUUID(),
344
+ successorInvocationId: randomUUID(),
345
+ claimId: randomUUID(),
346
+ }, signal);
347
+ if (!request) {
348
+ cancel();
349
+ return undefined;
350
+ }
351
+ const attachment = {
352
+ decision: valueDeferred(),
353
+ arrived: deferred(),
354
+ };
355
+ active.handoff = attachment;
356
+ held.set(active, { request, attachment });
357
+ }
358
+ for (const [active, item] of held) {
359
+ if (await waitForAbortableValue(item.request.ready, signal))
360
+ continue;
361
+ const snapshot = await this.readCanonicalSnapshot(active.sessionId, active.turnId, active.runId);
362
+ if (!isTerminalSnapshot(snapshot)) {
363
+ cancel();
364
+ return undefined;
365
+ }
366
+ item.attachment.decision.resolve('resume');
367
+ if (active.handoff === item.attachment)
368
+ active.handoff = undefined;
369
+ await waitForHostedExecutionIdleOrAbort(active.done, signal);
370
+ held.delete(active);
371
+ }
372
+ if ([...this.#executions.entries()].every(([, active]) => held.has(active)))
373
+ break;
374
+ }
375
+ if (signal.aborted ||
376
+ [...this.#executions.entries()].some(([, active]) => !held.has(active))) {
377
+ cancel();
378
+ return undefined;
379
+ }
380
+ return {
381
+ executions: [...held.keys()].map(({ sessionId, turnId, runId }) => ({
382
+ sessionId,
383
+ turnId,
384
+ runId,
385
+ })),
386
+ seal: async () => {
387
+ if (signal.aborted || this.#handoffHold !== hold || detached)
388
+ return false;
389
+ if (sealed)
390
+ return true;
391
+ for (const [active, { request }] of held) {
392
+ if (this.#executions.get(active.sessionId) !== active || !request.commit()) {
393
+ cancel();
394
+ return false;
395
+ }
396
+ }
397
+ try {
398
+ const results = await Promise.all([...held.values()].map(({ request }) => request.sealed));
399
+ sealed = !signal.aborted && this.#handoffHold === hold && results.every(Boolean);
400
+ if (sealed) {
401
+ await Promise.all([...held].map(([active, { attachment }]) => Promise.race([
402
+ attachment.arrived.promise,
403
+ active.done.then(() => {
404
+ throw new Error('Sealed execution ended before its attachment handoff');
405
+ }),
406
+ ])));
407
+ }
408
+ }
409
+ catch (error) {
410
+ cancel();
411
+ throw error;
412
+ }
413
+ if (!sealed)
414
+ cancel();
415
+ return sealed;
416
+ },
417
+ residencies: async () => {
418
+ if (!sealed || signal.aborted || this.#handoffHold !== hold || detached)
419
+ return undefined;
420
+ const residencies = [];
421
+ for (const [active, { attachment }] of held) {
422
+ if (this.#executions.get(active.sessionId) !== active ||
423
+ active.handoff !== attachment ||
424
+ active.stopRequested)
425
+ return undefined;
426
+ const messages = await this.sessionAdmission.run(active.sessionId, (lease) => this.messages.handoffResidencies(active, lease));
427
+ if (!messages)
428
+ return undefined;
429
+ residencies.push(active.residency, ...messages);
430
+ }
431
+ return residencies;
432
+ },
433
+ detach: async () => {
434
+ if (!sealed || signal.aborted || this.#handoffHold !== hold || detached) {
435
+ throw new Error('Cannot detach a handoff without all durable seals');
436
+ }
437
+ detached = true;
438
+ for (const { attachment } of held.values())
439
+ attachment.decision.resolve('detach');
440
+ await Promise.all([...held.keys()].map((active) => active.done));
441
+ releaseHold();
442
+ },
443
+ cancel,
444
+ };
445
+ }
446
+ catch (error) {
447
+ cancel();
448
+ if (signal.aborted)
449
+ return undefined;
450
+ throw error;
451
+ }
452
+ }
275
453
  async runExclusiveSessionOperation(input, operation) {
276
454
  const { sessionId, abortSignal } = input;
277
455
  let reservation;
@@ -287,7 +465,7 @@ export class RootTurnCoordinator {
287
465
  }
288
466
  const active = this.#executions.get(sessionId);
289
467
  const pending = this.#admissions.get(sessionId);
290
- const whenIdle = active?.done ?? pending?.whenIdle.promise;
468
+ const whenIdle = active?.done ?? pending?.whenIdle.promise ?? this.#handoffHold?.promise;
291
469
  if (whenIdle) {
292
470
  await waitForHostedExecutionIdleOrAbort(whenIdle, abortSignal);
293
471
  }
@@ -336,6 +514,8 @@ export class RootTurnCoordinator {
336
514
  reason: 'Runtime Host execution authority is draining.',
337
515
  };
338
516
  }
517
+ if (this.#handoffHold)
518
+ return { kind: 'busy', whenIdle: this.#handoffHold.promise };
339
519
  const active = this.#executions.get(sessionId);
340
520
  if (active) {
341
521
  return {
@@ -528,7 +708,9 @@ export class RootTurnCoordinator {
528
708
  return this.#executions.subscribe(listener);
529
709
  }
530
710
  whenIdle(sessionId) {
531
- return (this.#executions.get(sessionId)?.done ?? this.#admissions.get(sessionId)?.whenIdle.promise);
711
+ return (this.#executions.get(sessionId)?.done ??
712
+ this.#admissions.get(sessionId)?.whenIdle.promise ??
713
+ this.#handoffHold?.promise);
532
714
  }
533
715
  stopRoot(identity, input = {}) {
534
716
  normalizeStopSessionSource(input.source, input.workHubActionId);
@@ -1330,9 +1512,19 @@ export class RootTurnCoordinator {
1330
1512
  });
1331
1513
  }
1332
1514
  async planTurnResume(input) {
1333
- const plan = input.sourceRunId
1515
+ let sourceRunId = input.sourceRunId;
1516
+ if (sourceRunId) {
1517
+ const run = await this.readRunIfPresent(input.sessionId, sourceRunId);
1518
+ if (run?.terminalEvent && runtimeHandoffPause(run.terminalEvent)) {
1519
+ const logical = await readLogicalRuntimeExecutionForRun(this.stores.runtimeEventStore, run);
1520
+ // Queries may name the stable public root. The returned plan and the
1521
+ // subsequent start must still bind the exact physical source/high-water.
1522
+ sourceRunId = logical?.tip.runId ?? sourceRunId;
1523
+ }
1524
+ }
1525
+ const plan = sourceRunId
1334
1526
  ? await this.manager.planAuthoritativeSafeBoundaryContinuation(input.sessionId, {
1335
- sourceRunId: input.sourceRunId,
1527
+ sourceRunId,
1336
1528
  ...(input.expectedRuntimeEventHighWater !== undefined
1337
1529
  ? {
1338
1530
  expectedRuntimeEventHighWater: input.expectedRuntimeEventHighWater,
@@ -1391,9 +1583,9 @@ export class RootTurnCoordinator {
1391
1583
  }
1392
1584
  const fence = commitQueueFence();
1393
1585
  await this.messages.cancelMessages(input.sessionId, fence.retracted.map((message) => message.messageId));
1394
- await this.interactions.claimRunClosure(input, 'turn_stopped', admission);
1586
+ await this.interactions.claimRunClosure({ ...input, runId: active.continuation?.runId ?? active.runId }, 'turn_stopped', admission);
1395
1587
  const shouldDeliverStop = !active.stopRequested;
1396
- active.stopRequested = true;
1588
+ active.stopRequested = stopInput;
1397
1589
  return {
1398
1590
  active,
1399
1591
  deliverStop: () => shouldDeliverStop
@@ -1418,7 +1610,7 @@ export class RootTurnCoordinator {
1418
1610
  if (active?.turnId === input.turnId && active.runId === input.runId) {
1419
1611
  const fence = commitQueueFence();
1420
1612
  await this.messages.cancelMessages(input.sessionId, fence.retracted.map((message) => message.messageId));
1421
- active.stopRequested = true;
1613
+ active.stopRequested ??= { source: 'stop_button' };
1422
1614
  return { kind: 'await_terminal', active };
1423
1615
  }
1424
1616
  return { kind: 'complete', outcome: { ok: true, result: snapshot } };
@@ -1441,9 +1633,9 @@ export class RootTurnCoordinator {
1441
1633
  }
1442
1634
  const fence = commitQueueFence();
1443
1635
  await this.messages.cancelMessages(input.sessionId, fence.retracted.map((message) => message.messageId));
1444
- await this.interactions.claimRunClosure(input, 'turn_stopped', admissionLease);
1636
+ await this.interactions.claimRunClosure({ ...input, runId: active.continuation?.runId ?? active.runId }, 'turn_stopped', admissionLease);
1445
1637
  const shouldRequestStop = !active.stopRequested;
1446
- active.stopRequested = true;
1638
+ active.stopRequested ??= { source: 'stop_button' };
1447
1639
  return shouldRequestStop
1448
1640
  ? { kind: 'request_stop', active }
1449
1641
  : { kind: 'await_terminal', active };
@@ -1484,7 +1676,19 @@ export class RootTurnCoordinator {
1484
1676
  }
1485
1677
  if (active)
1486
1678
  return completedStart(sessionBusy('Session already has an active root Turn'));
1487
- throw new Error('Admitted non-terminal Turn has no active Runtime Host execution');
1679
+ const logical = await readLogicalRuntimeExecution(this.stores.runtimeEventStore, admission, existingRun);
1680
+ if (!logical?.pendingHandoff) {
1681
+ throw new Error('Admitted non-terminal Turn has no active Runtime Host execution');
1682
+ }
1683
+ const plan = await this.manager.planAuthoritativeSafeBoundaryContinuation(input.sessionId, {
1684
+ sourceRunId: logical.tip.runId,
1685
+ purpose: 'handoff',
1686
+ });
1687
+ if (!plan.continuation) {
1688
+ this.parkContinuationAdmission(admission);
1689
+ return completedStart(operationUnavailable(`Sealed handoff cannot resume: ${plan.rejectionReasons.join(', ')}`));
1690
+ }
1691
+ continuation = plan.continuation;
1488
1692
  }
1489
1693
  const active = this.#executions.get(input.sessionId);
1490
1694
  const currentReservation = this.#admissions.get(input.sessionId);
@@ -1517,6 +1721,9 @@ export class RootTurnCoordinator {
1517
1721
  try {
1518
1722
  this.messages.reserveRootTurn(messageIdentity);
1519
1723
  messageReserved = true;
1724
+ if (continuation?.handoffRootRunId !== undefined) {
1725
+ await this.messages.advanceHandoffRun(messageIdentity, continuation.runId, admissionLease);
1726
+ }
1520
1727
  await this.continuity.holdTerminalPublication(input.sessionId, input.turnId, runId, admissionLease);
1521
1728
  }
1522
1729
  catch (error) {
@@ -1547,7 +1754,7 @@ export class RootTurnCoordinator {
1547
1754
  startSettled,
1548
1755
  done: Promise.resolve(),
1549
1756
  residency,
1550
- stopRequested: false,
1757
+ stopRequested: undefined,
1551
1758
  messageTransitionCommitted: false,
1552
1759
  };
1553
1760
  if (replacing && this.#executions.get(input.sessionId) !== replacing) {
@@ -1613,6 +1820,7 @@ export class RootTurnCoordinator {
1613
1820
  }
1614
1821
  async drainTurn(input, active, startSettled) {
1615
1822
  let terminalTransitionStarted = false;
1823
+ let detached = false;
1616
1824
  try {
1617
1825
  const messageOrigin = hostedExecutionMessageOrigin(active.descriptor);
1618
1826
  const onRunStarted = async () => {
@@ -1620,46 +1828,80 @@ export class RootTurnCoordinator {
1620
1828
  await this.continuity.refreshCanonical(input.sessionId);
1621
1829
  startSettled.resolve();
1622
1830
  };
1623
- const stream = active.execution
1624
- ? active.execution.start({
1625
- runId: active.runId,
1626
- userMessageId: active.userMessageId,
1627
- onRunStarted: async () => {
1628
- await onRunStarted();
1629
- await active.execution?.onReady?.();
1630
- },
1831
+ let stream = active.continuation
1832
+ ? this.manager.resumeSafeBoundaryContinuation(active.continuation, {
1833
+ onRunStarted,
1834
+ stopBeforeDispatch: () => active.stopRequested,
1631
1835
  })
1632
- : active.descriptor.kind === 'context_compact'
1633
- ? this.manager.compactSession(input.sessionId, {
1634
- turnId: input.turnId,
1635
- hostedRoot: {
1636
- runId: active.runId,
1637
- onRunStarted,
1836
+ : active.execution
1837
+ ? active.execution.start({
1838
+ runId: active.runId,
1839
+ userMessageId: active.userMessageId,
1840
+ onRunStarted: async () => {
1841
+ await onRunStarted();
1842
+ await active.execution?.onReady?.();
1638
1843
  },
1639
1844
  })
1640
- : active.continuation
1641
- ? this.manager.resumeSafeBoundaryContinuation(active.continuation, {
1642
- onRunStarted,
1845
+ : active.descriptor.kind === 'context_compact'
1846
+ ? this.manager.compactSession(input.sessionId, {
1847
+ turnId: input.turnId,
1848
+ hostedRoot: {
1849
+ runId: active.runId,
1850
+ onRunStarted,
1851
+ },
1643
1852
  })
1644
- : this.startRootMessageTurn(input, active, normalizeMessageContent(requireRootMessageContent(input)), messageOrigin, onRunStarted);
1645
- for await (const event of stream) {
1646
- if (active.execution?.onEvent) {
1647
- try {
1648
- active.execution.onEvent(event);
1853
+ : active.continuation
1854
+ ? this.manager.resumeSafeBoundaryContinuation(active.continuation, {
1855
+ onRunStarted,
1856
+ })
1857
+ : this.startRootMessageTurn(input, active, normalizeMessageContent(requireRootMessageContent(input)), messageOrigin, onRunStarted);
1858
+ for (;;) {
1859
+ for await (const event of stream) {
1860
+ if (active.execution?.onEvent) {
1861
+ try {
1862
+ active.execution.onEvent(event);
1863
+ }
1864
+ catch {
1865
+ // Presentation observers do not participate in execution authority.
1866
+ }
1649
1867
  }
1650
- catch {
1651
- // Presentation observers do not participate in execution authority.
1868
+ if (isRuntimeSessionForwardedEvent(event)) {
1869
+ await this.continuity.acceptRuntimeEvent(input.sessionId, active.runId, event);
1870
+ }
1871
+ else if (isInteractionAnswerAck(event)) {
1872
+ await this.continuity.refreshCanonical(input.sessionId);
1873
+ }
1874
+ else if (event.type === 'user_question_request' || event.type === 'form_request') {
1875
+ this.continuity.enqueueCanonicalRefresh(input.sessionId);
1652
1876
  }
1653
1877
  }
1654
- if (isRuntimeSessionForwardedEvent(event)) {
1655
- await this.continuity.acceptRuntimeEvent(input.sessionId, active.runId, event);
1656
- }
1657
- else if (isInteractionAnswerAck(event)) {
1658
- await this.continuity.refreshCanonical(input.sessionId);
1659
- }
1660
- else if (event.type === 'user_question_request' || event.type === 'form_request') {
1661
- this.continuity.enqueueCanonicalRefresh(input.sessionId);
1662
- }
1878
+ const logical = await readLogicalRuntimeExecution(this.stores.runtimeEventStore, active);
1879
+ if (!logical?.pendingHandoff)
1880
+ break;
1881
+ const handoff = active.handoff;
1882
+ handoff?.arrived.resolve();
1883
+ if (handoff && (await handoff.decision.promise) === 'detach') {
1884
+ await this.sessionAdmission.run(input.sessionId, (lease) => this.messages.detachHandoffRoot(active, lease));
1885
+ detached = true;
1886
+ return;
1887
+ }
1888
+ await this.sessionAdmission.run(input.sessionId, async (lease) => {
1889
+ if (this.#executions.get(input.sessionId) !== active) {
1890
+ throw new RuntimeMessageAuthorityInvariantError('Sealed handoff lost its original root owner');
1891
+ }
1892
+ const plan = await this.manager.planAuthoritativeSafeBoundaryContinuation(input.sessionId, {
1893
+ sourceRunId: logical.tip.runId,
1894
+ purpose: 'handoff',
1895
+ });
1896
+ if (!plan.continuation)
1897
+ throw new Error(`Sealed handoff cannot resume: ${plan.rejectionReasons.join(', ')}`);
1898
+ await this.messages.advanceHandoffRun(active, plan.continuation.runId, lease);
1899
+ active.continuation = plan.continuation;
1900
+ });
1901
+ stream = this.manager.resumeSafeBoundaryContinuation(active.continuation, {
1902
+ onRunStarted,
1903
+ stopBeforeDispatch: () => active.stopRequested,
1904
+ });
1663
1905
  }
1664
1906
  const snapshot = await this.readCanonicalSnapshot(input.sessionId, input.turnId, active.runId);
1665
1907
  await this.assertCompletedExecutionIdentity(input, active);
@@ -1702,7 +1944,7 @@ export class RootTurnCoordinator {
1702
1944
  ((!active.stopRequested &&
1703
1945
  snapshot.status === 'failed' &&
1704
1946
  isContainableRunFailure(error)) ||
1705
- (active.stopRequested &&
1947
+ (Boolean(active.stopRequested) &&
1706
1948
  snapshot.status === 'cancelled' &&
1707
1949
  isStoppedInteractionAdmission(error)));
1708
1950
  }
@@ -1725,32 +1967,38 @@ export class RootTurnCoordinator {
1725
1967
  throw commandFailure;
1726
1968
  }
1727
1969
  finally {
1728
- this.observeExecutionCompletion(active, {
1729
- kind: 'authority_error',
1730
- execution: active,
1731
- reason: 'Runtime root Turn ended without a canonical completion.',
1732
- });
1733
- await active.observationSettled?.catch(() => this.requestHostDrain());
1734
- let releaseRootOwnership = active.messageTransitionCommitted;
1735
- if (!active.messageTransitionCommitted) {
1736
- try {
1737
- this.messages.abandonRootReservation({
1738
- sessionId: input.sessionId,
1739
- turnId: active.turnId,
1740
- runId: active.runId,
1741
- });
1742
- releaseRootOwnership = true;
1743
- }
1744
- catch {
1745
- this.requestHostDrain();
1746
- }
1747
- }
1748
- if (releaseRootOwnership) {
1970
+ if (detached) {
1749
1971
  this.#executions.release(active);
1750
1972
  active.residency.release();
1751
1973
  }
1752
- active.completion.resolve(active.observedCompletion);
1753
- this.#executions.publish(active);
1974
+ else {
1975
+ this.observeExecutionCompletion(active, {
1976
+ kind: 'authority_error',
1977
+ execution: active,
1978
+ reason: 'Runtime root Turn ended without a canonical completion.',
1979
+ });
1980
+ await active.observationSettled?.catch(() => this.requestHostDrain());
1981
+ let releaseRootOwnership = active.messageTransitionCommitted;
1982
+ if (!active.messageTransitionCommitted) {
1983
+ try {
1984
+ this.messages.abandonRootReservation({
1985
+ sessionId: input.sessionId,
1986
+ turnId: active.turnId,
1987
+ runId: active.runId,
1988
+ });
1989
+ releaseRootOwnership = true;
1990
+ }
1991
+ catch {
1992
+ this.requestHostDrain();
1993
+ }
1994
+ }
1995
+ if (releaseRootOwnership) {
1996
+ this.#executions.release(active);
1997
+ active.residency.release();
1998
+ }
1999
+ active.completion.resolve(active.observedCompletion);
2000
+ this.#executions.publish(active);
2001
+ }
1754
2002
  }
1755
2003
  }
1756
2004
  async materializeAdmittedMessageSources(active) {
@@ -1789,7 +2037,7 @@ export class RootTurnCoordinator {
1789
2037
  turnId: active.turnId,
1790
2038
  runId: active.runId,
1791
2039
  };
1792
- await this.interactions.assertTerminalFence(identity, lease);
2040
+ await this.interactions.assertTerminalFence({ ...identity, runId: active.continuation?.runId ?? active.runId }, lease);
1793
2041
  const batch = this.messages.beginTerminalTransition(identity);
1794
2042
  await this.continuity.publishTerminalProjection(sessionId, active.turnId, active.runId, lease);
1795
2043
  if (batch.sources.length === 0) {
@@ -1914,6 +2162,10 @@ export class RootTurnCoordinator {
1914
2162
  }
1915
2163
  }
1916
2164
  }
2165
+ async function waitForAbortableValue(value, signal) {
2166
+ await waitForHostedExecutionIdleOrAbort(value.then(() => undefined), signal);
2167
+ return value;
2168
+ }
1917
2169
  class HostedRootAdmissionGateError extends Error {
1918
2170
  cause;
1919
2171
  name = 'HostedRootAdmissionGateError';
@@ -71,6 +71,7 @@ export class HostScheduledTaskCoordinator {
71
71
  #prepared = false;
72
72
  #started = false;
73
73
  #draining = false;
74
+ #handoffHeld = false;
74
75
  #closed = false;
75
76
  #retiringSessions = new Set();
76
77
  constructor(input) {
@@ -156,6 +157,34 @@ export class HostScheduledTaskCoordinator {
156
157
  this.#started = true;
157
158
  void this.#refresh().catch((error) => this.#fatal(error));
158
159
  }
160
+ holdForHandoff() {
161
+ if (this.#draining || this.#handoffHeld)
162
+ return undefined;
163
+ this.#handoffHeld = true;
164
+ this.#stopTimer();
165
+ let released = false;
166
+ return {
167
+ settled: async () => {
168
+ // An admitted native effect or Agent fire finishes normally. No new
169
+ // fire may start while the handoff waits for this lane.
170
+ for (;;) {
171
+ const lane = this.#lane;
172
+ await lane;
173
+ if (lane === this.#lane)
174
+ return;
175
+ }
176
+ },
177
+ residencies: () => (this.#residency ? [this.#residency] : []),
178
+ release: () => {
179
+ if (released)
180
+ return;
181
+ released = true;
182
+ this.#handoffHeld = false;
183
+ if (!this.#draining)
184
+ void this.#refresh().catch((error) => this.#fatal(error));
185
+ },
186
+ };
187
+ }
159
188
  beginDrain() {
160
189
  if (this.#draining)
161
190
  return;
@@ -390,12 +419,16 @@ export class HostScheduledTaskCoordinator {
390
419
  }
391
420
  async #refresh() {
392
421
  await this.#exclusive(async () => {
422
+ if (this.#handoffHeld || this.#draining)
423
+ return;
393
424
  for (const claim of await this.#store.listPendingFires()) {
425
+ if (this.#handoffHeld || this.#draining)
426
+ break;
394
427
  if (claim.nativeState === 'waiting_for_provider') {
395
428
  await this.#fulfill(claim, true);
396
429
  }
397
430
  }
398
- while (!this.#draining) {
431
+ while (!this.#draining && !this.#handoffHeld) {
399
432
  const scan = await this.#store.claimNextDue(this.#now());
400
433
  for (const expired of scan.expired)
401
434
  this.#publish('updated', expired.id);
@@ -616,7 +649,7 @@ export class HostScheduledTaskCoordinator {
616
649
  async #refreshSchedule() {
617
650
  this.#stopTimer();
618
651
  await this.#refreshResidency();
619
- if (!this.#started || this.#draining)
652
+ if (!this.#started || this.#draining || this.#handoffHeld)
620
653
  return;
621
654
  const [tasks, claims] = await Promise.all([this.#store.list(), this.#store.listPendingFires()]);
622
655
  const next = tasks
@@ -626,6 +659,8 @@ export class HostScheduledTaskCoordinator {
626
659
  return earliest === null || deadline < earliest ? deadline : earliest;
627
660
  }, null);
628
661
  const waitingForProvider = claims.some((claim) => claim.nativeState === 'waiting_for_provider');
662
+ if (this.#draining || this.#handoffHeld)
663
+ return;
629
664
  if (next === null && !waitingForProvider)
630
665
  return;
631
666
  const nextTaskDelay = next === null