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

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 (154) hide show
  1. package/README.md +38 -19
  2. package/README.zh-CN.md +34 -19
  3. package/dist/mcp-capability-provider.js +4 -1
  4. package/dist/pi-transcript-tools.js +6 -0
  5. package/dist/pi-tui-layout.js +56 -10
  6. package/dist/pi-tui-mcp-status.js +2 -2
  7. package/dist/pi-tui-pickers.js +121 -30
  8. package/dist/pi-tui-runner.js +156 -42
  9. package/dist/pi-tui-todo.js +221 -0
  10. package/dist/pi-tui-transcript-viewer.js +134 -7
  11. package/dist/runtime-host-cli-context.js +70 -13
  12. package/dist/runtime-host-handoff-surface.js +51 -26
  13. package/dist/runtime-host-local-handoff.js +11 -1
  14. package/dist/runtime-host-service-manager.js +0 -3
  15. package/dist/runtime-host-session-channel.js +13 -0
  16. package/dist/runtime-host-session-driver.js +42 -1
  17. package/dist/tui-autocomplete-layout.js +3 -1
  18. package/dist/tui-copy-catalog.js +85 -6
  19. package/dist/tui-editor-render.js +36 -0
  20. package/dist/tui-mcp-control.js +41 -6
  21. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  22. package/node_modules/@maka/core/dist/agent-run.js +1 -0
  23. package/node_modules/@maka/core/dist/attachments.js +3 -0
  24. package/node_modules/@maka/core/dist/canonical-json.js +27 -0
  25. package/node_modules/@maka/core/dist/capabilities.js +35 -0
  26. package/node_modules/@maka/core/dist/computer-use.js +0 -1
  27. package/node_modules/@maka/core/dist/display-redaction.js +18 -0
  28. package/node_modules/@maka/core/dist/llm-connections.js +13 -0
  29. package/node_modules/@maka/core/dist/model-call-attempt.js +2 -0
  30. package/node_modules/@maka/core/dist/model-metadata.js +4 -0
  31. package/node_modules/@maka/core/dist/redaction.js +35 -0
  32. package/node_modules/@maka/core/dist/run-composition.js +77 -2
  33. package/node_modules/@maka/core/dist/runtime-event.js +3 -0
  34. package/node_modules/@maka/core/dist/session.js +57 -6
  35. package/node_modules/@maka/core/dist/slash-command-catalog.js +1 -0
  36. package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
  37. package/node_modules/@maka/core/dist/workhub-action-result.js +68 -0
  38. package/node_modules/@maka/core/package.json +3 -3
  39. package/node_modules/@maka/eval/dist/experiment-directory.js +4 -13
  40. package/node_modules/@maka/eval/dist/toolchain-verification.js +1 -1
  41. package/node_modules/@maka/mcp/dist/form-elicitation.js +213 -0
  42. package/node_modules/@maka/mcp/dist/index.js +337 -71
  43. package/node_modules/@maka/runtime/dist/agent-run-inspect.js +0 -13
  44. package/node_modules/@maka/runtime/dist/agent-run.js +90 -2
  45. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +16 -8
  46. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +57 -50
  47. package/node_modules/@maka/runtime/dist/archive-read-tool.js +2 -2
  48. package/node_modules/@maka/runtime/dist/context-budget-helpers.js +0 -16
  49. package/node_modules/@maka/runtime/dist/conversation-copy.js +153 -11
  50. package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +3 -3
  51. package/node_modules/@maka/runtime/dist/goal-continuation.js +5 -1
  52. package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +69 -1
  53. package/node_modules/@maka/runtime/dist/mcp-tools.js +7 -2
  54. package/node_modules/@maka/runtime/dist/model-fetcher.js +42 -2
  55. package/node_modules/@maka/runtime/dist/model-history.js +0 -112
  56. package/node_modules/@maka/runtime/dist/plugin-runtime.js +20 -10
  57. package/node_modules/@maka/runtime/dist/plugin-tool-service.js +205 -0
  58. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +8 -1
  59. package/node_modules/@maka/runtime/dist/request-shape.js +23 -0
  60. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -1
  61. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +24 -1
  62. package/node_modules/@maka/runtime/dist/runtime-kernel.js +127 -13
  63. package/node_modules/@maka/runtime/dist/session-manager.js +20 -36
  64. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +21 -0
  65. package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +0 -5
  66. package/node_modules/@maka/runtime/dist/test-connection.js +0 -1
  67. package/node_modules/@maka/runtime/dist/tool-activation-identity.js +41 -0
  68. package/node_modules/@maka/runtime/dist/tool-availability.js +11 -1
  69. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +63 -1
  70. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +42 -12
  71. package/node_modules/@maka/runtime/dist/tool-result-archive.js +27 -4
  72. package/node_modules/@maka/runtime/package.json +1 -0
  73. package/node_modules/@maka/runtime-host/dist/client/connection.js +30 -5
  74. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +52 -13
  75. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +2 -0
  76. package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +4 -2
  77. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +18 -1
  78. package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +19 -0
  79. package/node_modules/@maka/runtime-host/dist/control/endpoint.js +1 -3
  80. package/node_modules/@maka/runtime-host/dist/node-error.js +22 -0
  81. package/node_modules/@maka/runtime-host/dist/operator/managed-deployment.js +1 -3
  82. package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +0 -1
  83. package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +1 -1
  84. package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +1 -3
  85. package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -1
  86. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +3 -2
  87. package/node_modules/@maka/runtime-host/dist/protocol/plan.js +1 -0
  88. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +27 -5
  89. package/node_modules/@maka/runtime-host/dist/protocol/project-catalog.js +2 -2
  90. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -0
  91. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +86 -157
  92. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +4 -0
  93. package/node_modules/@maka/runtime-host/dist/server/agent-graph-coordinator.js +0 -3
  94. package/node_modules/@maka/runtime-host/dist/server/canonical-session-projection.js +2 -1
  95. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +28 -8
  96. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +112 -56
  97. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +32 -32
  98. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +85 -25
  99. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +42 -14
  100. package/node_modules/@maka/runtime-host/dist/server/failure-diagnostic.js +24 -0
  101. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +7 -3
  102. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -3
  103. package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +5 -2
  104. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +3 -2
  105. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +34 -4
  106. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +51 -23
  107. package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +2 -7
  108. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  109. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
  110. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +110 -14
  111. package/node_modules/@maka/runtime-host/dist/server/runtime-resource-coordinator.js +11 -8
  112. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +31 -21
  113. package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +1 -1
  114. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +20 -8
  115. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +5 -2
  116. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
  117. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +15 -104
  118. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +3 -0
  119. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +59 -82
  120. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +132 -139
  121. package/node_modules/@maka/runtime-host/dist/server/workhub-message-attachments.js +46 -0
  122. package/node_modules/@maka/storage/dist/agent-run-store.js +14 -1
  123. package/node_modules/@maka/storage/dist/artifact-store.js +40 -3
  124. package/node_modules/@maka/storage/dist/atomic-file-write.js +74 -0
  125. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -1
  126. package/node_modules/@maka/storage/dist/codex-session-adapter.js +284 -196
  127. package/node_modules/@maka/storage/dist/credential-store.js +12 -63
  128. package/node_modules/@maka/storage/dist/execution-stores.js +4 -1
  129. package/node_modules/@maka/storage/dist/git-exec.js +61 -0
  130. package/node_modules/@maka/storage/dist/git-worktree-child-executor.js +8 -35
  131. package/node_modules/@maka/storage/dist/goal-authority.js +2 -2
  132. package/node_modules/@maka/storage/dist/interaction-store.js +2 -2
  133. package/node_modules/@maka/storage/dist/mcp-config-store.js +10 -25
  134. package/node_modules/@maka/storage/dist/project-catalog.js +2 -23
  135. package/node_modules/@maka/storage/dist/runtime-event-invariants.js +2 -2
  136. package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +8 -3
  137. package/node_modules/@maka/storage/dist/scheduled-task-store.js +249 -283
  138. package/node_modules/@maka/storage/dist/session-bundle-manifest.js +5 -17
  139. package/node_modules/@maka/storage/dist/session-bundle-policy.js +2 -1
  140. package/node_modules/@maka/storage/dist/session-copy-cleanup.js +2 -1
  141. package/node_modules/@maka/storage/dist/session-store.js +14 -2
  142. package/node_modules/@maka/storage/dist/settings-store.js +8 -4
  143. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +11 -0
  144. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +1 -2
  145. package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +12 -1
  146. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +67 -3
  147. package/node_modules/@maka/storage/dist/stable-storage.js +14 -1
  148. package/node_modules/@maka/storage/dist/storage-id.js +28 -0
  149. package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +31 -4
  150. package/node_modules/@maka/storage/dist/workspace-identity.js +2 -15
  151. package/node_modules/@maka/storage/dist/workspace-version-authority-internal.js +1 -8
  152. package/package.json +1 -1
  153. package/node_modules/@maka/core/dist/workhub-creation-intent.js +0 -990
  154. package/node_modules/@maka/core/dist/workhub-session-resolver.js +0 -50
@@ -17,8 +17,10 @@
17
17
  * under the License.
18
18
  */
19
19
  import { randomBytes } from 'node:crypto';
20
- import { auth, Client, extractWWWAuthenticateParams, LATEST_PROTOCOL_VERSION, SdkErrorCode, SdkHttpError, SSEClientTransport, StreamableHTTPClientTransport, } from '@modelcontextprotocol/client';
20
+ import { auth, Client, extractWWWAuthenticateParams, LATEST_PROTOCOL_VERSION, CLIENT_CAPABILITIES_META_KEY, isInputRequiredResult, SdkErrorCode, SdkHttpError, SSEClientTransport, StreamableHTTPClientTransport, } from '@modelcontextprotocol/client';
21
21
  import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
22
+ import { setTimeout as delay } from 'node:timers/promises';
23
+ import { containsMcpFormState, prepareMcpForm } from './form-elicitation.js';
22
24
  import { redactSecrets } from '@maka/core/redaction';
23
25
  import { serializedByteLength } from '@maka/core/serialized-byte-length';
24
26
  import { deepScrubMcpSecrets, EMPTY_MCP_SECRET_INVENTORY, MCP_SECRET_MIN_SUBSTITUTION_LENGTH, mcpSecretInventory, scrubKnownMcpSecrets, } from '@maka/core/mcp-secrets';
@@ -59,6 +61,10 @@ const TOOL_REFRESH_BURST_IDLE_MS = 1_000;
59
61
  // access/refresh/id token, client secret, verifier), small enough that
60
62
  // deepScrub stays O(bound) per payload for the life of the process.
61
63
  const MAX_HARVESTED_SECRETS_PER_SERVER = 40;
64
+ // No eviction: a live transport may reflect any previous continuation later.
65
+ const MAX_CONNECTION_FORM_STATES = 64;
66
+ const MAX_CONNECTION_FORM_STATE_BYTES = 256 * 1024;
67
+ const FORM_PRIVACY_EXHAUSTED = 'form privacy retention exhausted (state limit); reconnect required';
62
68
  export class McpClientManager {
63
69
  connections = new Map();
64
70
  /** Servers with an interactive OAuth round in flight (startAuthorization
@@ -67,6 +73,7 @@ export class McpClientManager {
67
73
  * pass — and racing one against the round trips the round's version
68
74
  * fence, failing the user's login over nothing they did. */
69
75
  interactiveRounds = new Set();
76
+ formCalls = new Map();
70
77
  bindingIndex = new Map();
71
78
  listeners = new Set();
72
79
  syncQueue = Promise.resolve();
@@ -174,6 +181,22 @@ export class McpClientManager {
174
181
  for (const value of this.storedSecrets.get(serverId) ?? []) {
175
182
  (value.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(value);
176
183
  }
184
+ const privacy = this.connections.get(serverId)?.diagnosticPrivacy;
185
+ if (privacy?.exhausted) {
186
+ // An empty withheld token intentionally matches every diagnostic. Once
187
+ // retention is full, fail closed until a fresh physical connection.
188
+ inventory.withhold.push('');
189
+ }
190
+ for (const state of privacy?.states ?? []) {
191
+ (state.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(state);
192
+ }
193
+ for (const call of this.formCalls.values()) {
194
+ if (call.serverId !== serverId)
195
+ continue;
196
+ for (const state of call.states) {
197
+ (state.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(state);
198
+ }
199
+ }
177
200
  return inventory;
178
201
  }
179
202
  onChange(listener) {
@@ -520,93 +543,231 @@ export class McpClientManager {
520
543
  throw new McpToolCallError(serverId, toolName, 'tool binding is stale');
521
544
  }
522
545
  const client = entry.client;
546
+ let inventory = this.secretsFor(serverId, entry.config);
523
547
  const headerArguments = validateMcpHeaderArguments(snapshot.headerDeclarations, args);
524
548
  if (!headerArguments.valid) {
525
- throw new McpToolCallError(serverId, toolName, `unsafe integer argument at ${formatMcpDiagnosticText(headerArguments.path.join('.'))}`);
549
+ throw new McpToolCallError(serverId, toolName, `unsafe integer argument at ${formatMcpDiagnosticText(scrubKnownSecrets(headerArguments.path.join('.'), inventory))}`);
526
550
  }
527
- const inventory = this.secretsFor(serverId, entry.config);
528
551
  const preparation = snapshot.callPreparation ??
529
552
  (snapshot.callPreparation = this.toolCallPreparer.prepare(snapshot.definition));
530
553
  if (!preparation.ok) {
531
554
  throw new McpToolCallError(serverId, toolName, 'server advertised an invalid output schema', {
532
- cause: preparation.cause,
555
+ cause: sanitizedCause(preparation.cause, inventory),
533
556
  });
534
557
  }
535
- let result;
558
+ const requestInteraction = entry.status.negotiatedProtocol?.era === 'modern' ? options.requestInteraction : undefined;
559
+ const privacy = entry.diagnosticPrivacy;
560
+ if (privacy?.exhausted) {
561
+ throw new McpToolCallError(serverId, toolName, FORM_PRIVACY_EXHAUSTED);
562
+ }
563
+ const controller = requestInteraction ? new AbortController() : undefined;
564
+ const signal = controller
565
+ ? AbortSignal.any([
566
+ controller.signal,
567
+ ...(privacy ? [privacy.failure.signal] : []),
568
+ ...(options.signal ? [options.signal] : []),
569
+ ])
570
+ : privacy
571
+ ? AbortSignal.any([privacy.failure.signal, ...(options.signal ? [options.signal] : [])])
572
+ : options.signal;
573
+ const refreshInventory = () => {
574
+ inventory = this.secretsFor(serverId, entry.config);
575
+ if (privacy?.exhausted)
576
+ inventory.withhold.push('');
577
+ for (const state of privacy?.states ?? []) {
578
+ (state.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(state);
579
+ }
580
+ };
581
+ const states = [];
582
+ if (controller) {
583
+ this.formCalls.set(controller, { binding, serverId, states });
584
+ if (privacy) {
585
+ privacy.pendingCalls += 1;
586
+ privacy.discardPendingStderr?.();
587
+ }
588
+ }
589
+ const assertCurrent = () => {
590
+ signal?.throwIfAborted();
591
+ if (this.closed ||
592
+ entry.closing ||
593
+ this.connections.get(serverId) !== entry ||
594
+ entry.client !== client ||
595
+ entry.status.state !== 'connected' ||
596
+ entry.connectionGeneration !== identity.connectionGeneration ||
597
+ entry.toolSnapshot.get(toolName)?.binding !== binding)
598
+ throw new McpToolCallError(serverId, toolName, 'tool binding is stale');
599
+ };
536
600
  try {
537
- result = await client.callTool({ name: toolName, arguments: args }, {
538
- signal: options.signal,
539
- timeout: options.timeoutMs ?? this.timeouts.callToolMs,
540
- toolDefinition: structuredClone(preparation.value.definitionForSdk),
541
- });
601
+ const originalArguments = requestInteraction ? structuredClone(args) : args;
602
+ let continuation = {};
603
+ let rounds = 0;
604
+ let result;
605
+ while (true) {
606
+ if (requestInteraction)
607
+ assertCurrent();
608
+ result = await client.callTool({
609
+ name: toolName,
610
+ arguments: originalArguments,
611
+ ...continuation,
612
+ ...(requestInteraction
613
+ ? {
614
+ _meta: { [CLIENT_CAPABILITIES_META_KEY]: { elicitation: { form: {} } } },
615
+ }
616
+ : {}),
617
+ }, {
618
+ signal,
619
+ timeout: options.timeoutMs ?? this.timeouts.callToolMs,
620
+ toolDefinition: structuredClone(preparation.value.definitionForSdk),
621
+ ...(requestInteraction ? { allowInputRequired: true } : {}),
622
+ });
623
+ refreshInventory();
624
+ if (privacy?.exhausted)
625
+ throw new McpToolCallError(serverId, toolName, FORM_PRIVACY_EXHAUSTED);
626
+ if (requestInteraction)
627
+ assertCurrent();
628
+ if (!requestInteraction || !isInputRequiredResult(result))
629
+ break;
630
+ const state = result.requestState;
631
+ if (state !== undefined &&
632
+ (typeof state !== 'string' || Buffer.byteLength(state) > 16 * 1024)) {
633
+ if (privacy) {
634
+ exhaustConnectionDiagnosticPrivacy(entry, privacy);
635
+ this.emit(entry.status);
636
+ }
637
+ inventory.withhold.push('');
638
+ throw new McpToolCallError(serverId, toolName, 'form continuation state exceeds the limit');
639
+ }
640
+ const learnedState = Boolean(state && privacy && !privacy.states.has(state));
641
+ if (state) {
642
+ if (privacy && !retainConnectionFormState(entry, privacy, state)) {
643
+ this.emit(entry.status);
644
+ inventory.withhold.push('');
645
+ throw new McpToolCallError(serverId, toolName, FORM_PRIVACY_EXHAUSTED);
646
+ }
647
+ states.push(state);
648
+ (state.length >= MIN_SUBSTITUTION_LENGTH
649
+ ? inventory.substitute
650
+ : inventory.withhold).push(state);
651
+ }
652
+ if (learnedState && entry.status.stderrTail !== undefined) {
653
+ // Existing lines have already been formatted/truncated, so an exact
654
+ // replacement cannot reliably remove fragments of newly learned
655
+ // state. Clear the cache and notify consumers. Earlier published
656
+ // diagnostics from before the state was known cannot be retracted.
657
+ entry.status = { ...entry.status, stderrTail: undefined };
658
+ this.emit(entry.status);
659
+ }
660
+ if (++rounds > 8)
661
+ throw new McpToolCallError(serverId, toolName, 'form round limit exceeded');
662
+ const requests = result.inputRequests ?? {};
663
+ const keys = Object.keys(requests).sort(compareText);
664
+ if (keys.length > 8 || serializedByteLength(requests, 64 * 1024) > 64 * 1024) {
665
+ throw new McpToolCallError(serverId, toolName, 'form input requests exceed the limit');
666
+ }
667
+ if (containsMcpFormState(requests, privacy ? [...privacy.states] : states)) {
668
+ throw new McpToolCallError(serverId, toolName, 'form contains private continuation material');
669
+ }
670
+ // Validate every sibling before publishing any user-facing Interaction.
671
+ const forms = keys.map((key) => ({
672
+ key,
673
+ prepared: prepareMcpForm(requests[key], { name: toolName, source: serverId }),
674
+ }));
675
+ for (const { prepared } of forms) {
676
+ if (JSON.stringify(deepScrub(prepared.form, inventory)) !== JSON.stringify(prepared.form)) {
677
+ throw new McpToolCallError(serverId, toolName, 'form contains private continuation material');
678
+ }
679
+ }
680
+ const responses = {};
681
+ for (const { key, prepared } of forms) {
682
+ assertCurrent();
683
+ // Providers may ignore cancellationSignal. Racing the callback still
684
+ // settles their invocation, which closes the canonical Host form.
685
+ const answer = await waitForMcpForm(() => requestInteraction(prepared.form, { cancellationSignal: signal }), signal);
686
+ assertCurrent();
687
+ Object.defineProperty(responses, key, {
688
+ value: prepared.respond(answer),
689
+ enumerable: true,
690
+ configurable: true,
691
+ writable: true,
692
+ });
693
+ }
694
+ continuation = {
695
+ ...(keys.length ? { inputResponses: responses } : {}),
696
+ ...(state === undefined ? {} : { requestState: state }),
697
+ };
698
+ if (!keys.length)
699
+ await delay(25, undefined, { signal });
700
+ }
701
+ // The SDK's legacy compatibility schema defaults a missing content array
702
+ // before returning, but retains deferred-result compatibility fields.
703
+ // Reject every decoded marker and any future content-less result.
704
+ if (!Object.hasOwn(result, 'content') ||
705
+ Object.hasOwn(result, 'toolResult') ||
706
+ Object.hasOwn(result, 'task') ||
707
+ Object.hasOwn(result, 'inputRequests') ||
708
+ Object.hasOwn(result, 'requestState')) {
709
+ throw new McpToolCallError(serverId, toolName, 'server returned an unsupported deferred tool result');
710
+ }
711
+ if (!Array.isArray(result.content)) {
712
+ throw new McpToolCallError(serverId, toolName, 'server returned invalid content');
713
+ }
714
+ if (result.isError) {
715
+ // The server writes this text; it can echo a secret it was sent.
716
+ throw new McpToolCallError(serverId, toolName, scrubKnownSecrets(redactSecrets(summarizeErrorContent(result.content)), inventory));
717
+ }
718
+ assertSuccessfulToolResultBudget(serverId, toolName, result, { raw: true });
719
+ const validateOutput = preparation.value.validateOutput;
720
+ if (validateOutput) {
721
+ if (result.structuredContent === undefined) {
722
+ throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result');
723
+ }
724
+ let validation;
725
+ try {
726
+ validation = validateOutput(result.structuredContent);
727
+ }
728
+ catch (cause) {
729
+ throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
730
+ cause,
731
+ });
732
+ }
733
+ if (!validation.valid) {
734
+ throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
735
+ cause: new Error(validation.errorMessage),
736
+ });
737
+ }
738
+ }
739
+ // Success payloads cross toward the renderer and the transcript too; a
740
+ // server can embed the credential it was just sent into a result.
741
+ const published = {
742
+ content: deepScrub(result.content.map(normalizeContent), inventory),
743
+ structuredContent: deepScrub(result.structuredContent, inventory),
744
+ };
745
+ assertSuccessfulToolResultBudget(serverId, toolName, published);
746
+ return published;
542
747
  }
543
748
  catch (error) {
544
- // A 401 after connect means the server revoked the session, not that
545
- // one call hiccuped: leave `connected` and the UI never offers the
546
- // login it now needs.
749
+ refreshInventory();
750
+ const normalized = normalizeToolCallError(serverId, toolName, error, signal);
547
751
  if (isAuthRequiredError(error) &&
548
752
  this.connections.get(serverId) === entry &&
549
753
  entry.client === client) {
550
- this.markError(entry, error);
754
+ this.markError(entry, scrubbedError(error, inventory));
551
755
  }
552
- // The transport error can carry reflected request material (the body
553
- // of a failed POST); scrub it like every other outbound message. The
554
- // cause chain still holds the RAW transport error — the rejection
555
- // leaves the manager (IPC, logs, telemetry), and any cause-aware
556
- // serializer downstream would expose it — so the retained cause is an
557
- // allowlisted copy: typed identity (name, code, status, SDK brands)
558
- // with a scrubbed message and no deeper chain or payload fields.
559
- const normalized = normalizeToolCallError(serverId, toolName, error, options.signal);
560
- normalized.message = scrubKnownSecrets(normalized.message, inventory);
756
+ normalized.message = privacy?.exhausted
757
+ ? FORM_PRIVACY_EXHAUSTED
758
+ : scrubKnownSecrets(normalized.message, inventory);
561
759
  normalized.cause = sanitizedCause(normalized.cause, inventory);
562
760
  throw normalized;
563
761
  }
564
- // The SDK's legacy compatibility schema defaults a missing content array
565
- // before returning, but retains deferred-result compatibility fields.
566
- // Reject every decoded marker and any future content-less result.
567
- if (!Object.hasOwn(result, 'content') ||
568
- Object.hasOwn(result, 'toolResult') ||
569
- Object.hasOwn(result, 'task') ||
570
- Object.hasOwn(result, 'inputRequests') ||
571
- Object.hasOwn(result, 'requestState')) {
572
- throw new McpToolCallError(serverId, toolName, 'server returned an unsupported deferred tool result');
573
- }
574
- if (!Array.isArray(result.content)) {
575
- throw new McpToolCallError(serverId, toolName, 'server returned invalid content');
576
- }
577
- if (result.isError) {
578
- // The server writes this text; it can echo a secret it was sent.
579
- throw new McpToolCallError(serverId, toolName, scrubKnownSecrets(redactSecrets(summarizeErrorContent(result.content)), inventory));
580
- }
581
- assertSuccessfulToolResultBudget(serverId, toolName, result, { raw: true });
582
- const validateOutput = preparation.value.validateOutput;
583
- if (validateOutput) {
584
- if (result.structuredContent === undefined) {
585
- throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result');
586
- }
587
- let validation;
588
- try {
589
- validation = validateOutput(result.structuredContent);
590
- }
591
- catch (cause) {
592
- throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
593
- cause,
594
- });
595
- }
596
- if (!validation.valid) {
597
- throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
598
- cause: new Error(validation.errorMessage),
599
- });
762
+ finally {
763
+ if (controller) {
764
+ this.formCalls.delete(controller);
765
+ if (privacy) {
766
+ privacy.discardPendingStderr?.();
767
+ privacy.pendingCalls -= 1;
768
+ }
600
769
  }
601
770
  }
602
- // Success payloads cross toward the renderer and the transcript too; a
603
- // server can embed the credential it was just sent into a result.
604
- const published = {
605
- content: deepScrub(result.content.map(normalizeContent), inventory),
606
- structuredContent: deepScrub(result.structuredContent, inventory),
607
- };
608
- assertSuccessfulToolResultBudget(serverId, toolName, published);
609
- return published;
610
771
  }
611
772
  async test(serverId) {
612
773
  const started = this.now();
@@ -633,6 +794,14 @@ export class McpClientManager {
633
794
  async connectEntry(serverId, entry, signal) {
634
795
  let connected;
635
796
  entry.closing = false;
797
+ entry.diagnosticPrivacy = {
798
+ states: new Set(),
799
+ failure: new AbortController(),
800
+ bytes: 0,
801
+ pendingCalls: 0,
802
+ stderrSuppressed: false,
803
+ exhausted: false,
804
+ };
636
805
  entry.refreshDiagnostic = undefined;
637
806
  entry.subscription = undefined;
638
807
  entry.subscriptionDiagnostic = undefined;
@@ -793,7 +962,7 @@ export class McpClientManager {
793
962
  env: buildStdioEnvironment(entry.config.env, process.env, this.excludedStdioEnvironmentKeys),
794
963
  stderr: 'pipe',
795
964
  });
796
- attachStderrTail(transport, entry, collectConfigSecrets(entry.config), () => {
965
+ attachStderrTail(transport, entry, entry.diagnosticPrivacy, () => this.secretsFor(serverId, entry.config), () => {
797
966
  if (this.connections.get(serverId) === entry)
798
967
  this.emit(entry.status);
799
968
  });
@@ -1587,6 +1756,13 @@ export class McpClientManager {
1587
1756
  entry.toolSnapshot = snapshot;
1588
1757
  this.bindingIndex = nextIndex;
1589
1758
  this.callableSnapshot = nextCallableSnapshot;
1759
+ // Notify after publishing the replacement so every resumed callback sees
1760
+ // the same retired binding. Unchanged refreshes preserve binding tokens.
1761
+ for (const [controller, call] of this.formCalls) {
1762
+ if (call.serverId === entry.status.serverId && !nextIndex.has(call.binding)) {
1763
+ controller.abort(new Error('MCP tool binding is stale'));
1764
+ }
1765
+ }
1590
1766
  }
1591
1767
  buildCallableSnapshot(replacedEntry, replacement) {
1592
1768
  const entries = this.closed
@@ -2030,14 +2206,73 @@ export function buildStdioEnvironment(explicit = {}, source = process.env, exclu
2030
2206
  }
2031
2207
  return environment;
2032
2208
  }
2033
- function attachStderrTail(transport, entry, secrets, onUpdate) {
2209
+ function exhaustConnectionDiagnosticPrivacy(entry, privacy) {
2210
+ privacy.exhausted = true;
2211
+ privacy.failure.abort(new Error(FORM_PRIVACY_EXHAUSTED));
2212
+ privacy.stderrSuppressed = true;
2213
+ privacy.discardPendingStderr?.();
2214
+ entry.status = { ...entry.status, stderrTail: undefined };
2215
+ }
2216
+ function retainConnectionFormState(entry, privacy, state) {
2217
+ if (/[^\x20-\x7e]/u.test(state)) {
2218
+ // Line splitting/formatting can remove controls, and chunk-wise UTF-8
2219
+ // decoding can split non-ASCII state bytes. Never publish this generation's
2220
+ // stderr when exact raw-string replacement cannot cover those transforms.
2221
+ privacy.stderrSuppressed = true;
2222
+ privacy.discardPendingStderr?.();
2223
+ }
2224
+ if (privacy.states.has(state))
2225
+ return true;
2226
+ const bytes = Buffer.byteLength(state);
2227
+ if (privacy.states.size >= MAX_CONNECTION_FORM_STATES ||
2228
+ privacy.bytes + bytes > MAX_CONNECTION_FORM_STATE_BYTES) {
2229
+ exhaustConnectionDiagnosticPrivacy(entry, privacy);
2230
+ return false;
2231
+ }
2232
+ privacy.states.add(state);
2233
+ privacy.bytes += bytes;
2234
+ return true;
2235
+ }
2236
+ function attachStderrTail(transport, entry, privacy, secrets, onUpdate) {
2034
2237
  let pending = '';
2035
2238
  let oversized = false;
2036
2239
  let discardingContinuation = false;
2037
2240
  let physicalSuffix = '';
2241
+ let sensitivePartialLine = false;
2242
+ let sensitiveSuffix = '';
2243
+ const discardSensitive = (value, all) => {
2244
+ let offset = 0;
2245
+ while (offset < value.length && (all || sensitivePartialLine)) {
2246
+ const newline = value.indexOf('\n', offset);
2247
+ const part = value.slice(offset, newline < 0 ? undefined : newline);
2248
+ sensitiveSuffix = part.length >= 2 ? part.slice(-2) : `${sensitiveSuffix}${part}`.slice(-2);
2249
+ if (newline < 0) {
2250
+ sensitivePartialLine = true;
2251
+ return value.length;
2252
+ }
2253
+ sensitivePartialLine = sensitiveSuffix.endsWith('\\') || sensitiveSuffix.endsWith('\\\r');
2254
+ sensitiveSuffix = '';
2255
+ offset = newline + 1;
2256
+ }
2257
+ return offset;
2258
+ };
2259
+ const current = () => entry.diagnosticPrivacy === privacy && !entry.closing;
2260
+ const suppressed = () => privacy.stderrSuppressed || privacy.pendingCalls > 0;
2261
+ privacy.discardPendingStderr = () => {
2262
+ if (pending.length > 0 || oversized || discardingContinuation) {
2263
+ sensitivePartialLine = true;
2264
+ sensitiveSuffix = physicalSuffix;
2265
+ }
2266
+ pending = '';
2267
+ oversized = false;
2268
+ discardingContinuation = false;
2269
+ physicalSuffix = '';
2270
+ };
2038
2271
  const append = (lines) => {
2272
+ if (!current() || suppressed())
2273
+ return;
2039
2274
  const rendered = lines
2040
- .map((line) => formatMcpDiagnosticText(scrubKnownSecrets(line, secrets), STDERR_LINE_CHARS))
2275
+ .map((line) => formatMcpDiagnosticText(scrubKnownSecrets(line, secrets()), STDERR_LINE_CHARS))
2041
2276
  .filter(Boolean);
2042
2277
  if (rendered.length === 0)
2043
2278
  return;
@@ -2094,8 +2329,15 @@ function attachStderrTail(transport, entry, secrets, onUpdate) {
2094
2329
  const stream = transport.stderr;
2095
2330
  stream?.on('data', (chunk) => {
2096
2331
  const batch = [];
2332
+ if (!current())
2333
+ return;
2097
2334
  const value = String(chunk);
2098
- let offset = 0;
2335
+ if (suppressed()) {
2336
+ privacy.discardPendingStderr?.();
2337
+ discardSensitive(value, true);
2338
+ return;
2339
+ }
2340
+ let offset = discardSensitive(value, false);
2099
2341
  for (let newline = value.indexOf('\n', offset); newline >= 0;) {
2100
2342
  consume(value.slice(offset, newline), true, batch);
2101
2343
  offset = newline + 1;
@@ -2105,6 +2347,8 @@ function attachStderrTail(transport, entry, secrets, onUpdate) {
2105
2347
  append(batch);
2106
2348
  });
2107
2349
  const flush = () => {
2350
+ if (!current() || suppressed() || sensitivePartialLine)
2351
+ return;
2108
2352
  if (!pending && !oversized && !discardingContinuation)
2109
2353
  return;
2110
2354
  const batch = [];
@@ -2467,3 +2711,25 @@ class McpAbandonSupersededError extends Error {
2467
2711
  function isRecord(value) {
2468
2712
  return typeof value === 'object' && value !== null && !Array.isArray(value);
2469
2713
  }
2714
+ /** Race even handlers that do not accept a cancellation signal, and always
2715
+ * consume a late callback rejection after the invocation has been released. */
2716
+ async function waitForMcpForm(request, signal) {
2717
+ signal.throwIfAborted();
2718
+ let abort;
2719
+ const cancelled = new Promise((_, reject) => {
2720
+ abort = () => reject(signal.reason ?? new Error('MCP form cancelled'));
2721
+ signal.addEventListener('abort', abort, { once: true });
2722
+ });
2723
+ try {
2724
+ return await Promise.race([
2725
+ Promise.resolve().then(() => {
2726
+ signal.throwIfAborted();
2727
+ return request();
2728
+ }),
2729
+ cancelled,
2730
+ ]);
2731
+ }
2732
+ finally {
2733
+ signal.removeEventListener('abort', abort);
2734
+ }
2735
+ }
@@ -61,19 +61,6 @@ export async function inspectAgentRunReadModel(runStore, runtimeEventStore, opti
61
61
  diagnostics,
62
62
  };
63
63
  }
64
- export async function inspectSessionRunReadModels(runStore, runtimeEventStore, sessionId, options = {}) {
65
- const invocations = await runtimeEventStore.listSessionInvocations(sessionId);
66
- const models = [];
67
- for (const invocation of invocations) {
68
- models.push(await inspectAgentRunReadModel(runStore, runtimeEventStore, {
69
- sessionId,
70
- runId: invocation.runId,
71
- invocation,
72
- ...(options.isFatalReadError ? { isFatalReadError: options.isFatalReadError } : {}),
73
- }));
74
- }
75
- return models;
76
- }
77
64
  // A run is not its invocation: a continuation is a new run on the invocation it
78
65
  // resumes. This reader is addressed by run, so it looks the invocation up by the
79
66
  // id it was actually given.
@@ -20,12 +20,13 @@ import { RUN_COMPOSITION_RECORDED_EVENT_TYPE } from '@maka/core/agent-run';
20
20
  import { isRuntimeHandoffPause } from '@maka/core/runtime-handoff';
21
21
  import { RunHandoffGate } from './run-handoff-gate.js';
22
22
  import { preserveHandoffOpening } from './runtime-resume.js';
23
- import { decodeRunCompositionSnapshot } from '@maka/core/run-composition';
23
+ import { createRequestCompositionSnapshot, decodeRequestCompositionSnapshot, decodeRunCompositionSnapshot, } from '@maka/core/run-composition';
24
24
  import { DurableStoreWriteError, RunSealedError } from '@maka/core/runtime-event-store';
25
25
  import { buildInvocationOpenedEvent, buildSyntheticTerminalRuntimeEvent, isSessionInlineInvocation, } from '@maka/core/runtime-invocation';
26
26
  import { MODEL_PROJECTION_TRANSITION_EVENT_TYPE, } from '@maka/core/model-projection-transition';
27
27
  import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
28
28
  import { ToolLedgerCorruptionError, ToolLedgerRejectionError, } from '@maka/core/tool-ledger-scanner';
29
+ import { stableHash } from './request-shape.js';
29
30
  import { Buffer } from 'node:buffer';
30
31
  import { isDeepStrictEqual } from 'node:util';
31
32
  import { redactSecrets } from '@maka/core/redaction';
@@ -79,6 +80,9 @@ export class AgentRun {
79
80
  runComposition;
80
81
  runCompositionWrite;
81
82
  runCompositionCommitted = false;
83
+ requestComposition;
84
+ requestCompositionIndex;
85
+ requestCompositionIndexRead;
82
86
  failureClass;
83
87
  failureMessage;
84
88
  lastTs = 0;
@@ -418,6 +422,72 @@ export class AgentRun {
418
422
  throw new Error('Cooperative handoff requires a durably committed Run Composition');
419
423
  }
420
424
  }
425
+ /**
426
+ * Durably binds one logical model step to its effective request surface.
427
+ * Unchanged steps reuse the latest snapshot; a changed surface appends a full
428
+ * replacement before provider dispatch, matching DSH request/header epochs.
429
+ */
430
+ async recordRequestComposition(input) {
431
+ if (!this.input.runStore) {
432
+ throw new Error('AgentRun store is not configured');
433
+ }
434
+ await this.loadRequestCompositionIndex();
435
+ const snapshot = createRequestCompositionSnapshot(input, this.requestCompositionIndex?.size ? 'change' : 'initial');
436
+ const surfaceHash = requestCompositionSurfaceHash(snapshot);
437
+ const existing = this.requestCompositionIndex?.get(surfaceHash);
438
+ if (existing) {
439
+ if (!sameRequestCompositionSurface(existing, snapshot)) {
440
+ throw new Error(`Request Composition surface hash collision: ${surfaceHash}`);
441
+ }
442
+ this.requestComposition = existing;
443
+ return existing.compositionId;
444
+ }
445
+ await this.enqueueRequiredRunStoreWrite('append request composition', async () => {
446
+ await this.input.runStore?.appendEvent(this.sessionId, this.runId, {
447
+ type: 'request_composition_resolved',
448
+ id: snapshot.compositionId,
449
+ runId: this.runId,
450
+ sessionId: this.sessionId,
451
+ turnId: this.turnId,
452
+ ts: this.input.now(),
453
+ data: { snapshot },
454
+ }, { durable: true });
455
+ });
456
+ this.requestComposition = snapshot;
457
+ this.requestCompositionIndex?.set(surfaceHash, snapshot);
458
+ return snapshot.compositionId;
459
+ }
460
+ async loadRequestCompositionIndex() {
461
+ if (this.requestCompositionIndex)
462
+ return;
463
+ if (this.requestCompositionIndexRead)
464
+ return await this.requestCompositionIndexRead;
465
+ const read = (async () => {
466
+ const index = new Map();
467
+ const events = await this.input.runStore?.readEvents(this.sessionId, this.runId);
468
+ for (const event of events ?? []) {
469
+ if (event.type !== 'request_composition_resolved')
470
+ continue;
471
+ const snapshot = decodeRequestCompositionSnapshot(event.data?.snapshot);
472
+ const surfaceHash = requestCompositionSurfaceHash(snapshot);
473
+ const existing = index.get(surfaceHash);
474
+ if (existing && !sameRequestCompositionSurface(existing, snapshot)) {
475
+ throw new Error(`Request Composition surface hash collision: ${surfaceHash}`);
476
+ }
477
+ index.set(surfaceHash, existing ?? snapshot);
478
+ this.requestComposition = snapshot;
479
+ }
480
+ this.requestCompositionIndex = index;
481
+ })();
482
+ this.requestCompositionIndexRead = read;
483
+ try {
484
+ await read;
485
+ }
486
+ finally {
487
+ if (this.requestCompositionIndexRead === read)
488
+ this.requestCompositionIndexRead = undefined;
489
+ }
490
+ }
421
491
  /**
422
492
  * Canonical accounting record for one physical provider request (#1679).
423
493
  *
@@ -621,13 +691,22 @@ export class AgentRun {
621
691
  modelId: this.header.model,
622
692
  };
623
693
  }
624
- async begin() {
694
+ async beginUserTurn() {
625
695
  // Owed from here, not from after the opening: `openInvocation` can leave the
626
696
  // invocation open and still throw, and `finalize` reopens what it can.
627
697
  this.initialRuntimeEventPending = true;
628
698
  await this.openInvocation();
629
699
  this.lastTs = this.input.now();
630
700
  const initialRuntimeEvent = await this.recordInitialRuntimeEvent(this.lastTs);
701
+ return initialRuntimeEvent;
702
+ }
703
+ /** Host actions share Turn facts and finalization without activating a provider. */
704
+ async beginCoordination() {
705
+ await this.beginUserTurn();
706
+ await this.input.hooks.updateStatus(this.sessionId, 'running', undefined, this.lastTs);
707
+ }
708
+ async begin() {
709
+ const initialRuntimeEvent = await this.beginUserTurn();
631
710
  this.active = await this.input.hooks.reserveRun(this.sessionId, this.header, this);
632
711
  await this.input.hooks.updateStatus(this.sessionId, 'running', undefined, this.lastTs);
633
712
  const priorRuntimeContext = await this.buildPriorRuntimeContext();
@@ -1573,6 +1652,15 @@ function redactTraceString(value) {
1573
1652
  function errorMessage(error) {
1574
1653
  return redactTraceString(error instanceof Error ? error.message : String(error));
1575
1654
  }
1655
+ function sameRequestCompositionSurface(current, candidate) {
1656
+ const { schemaVersion: _schemaVersion, compositionId: _compositionId, step: _step, reason: _reason, ...currentSurface } = current;
1657
+ const { schemaVersion: _candidateSchemaVersion, compositionId: _candidateCompositionId, step: _candidateStep, reason: _candidateReason, ...candidateSurface } = candidate;
1658
+ return isDeepStrictEqual(currentSurface, candidateSurface);
1659
+ }
1660
+ function requestCompositionSurfaceHash(snapshot) {
1661
+ const { schemaVersion: _schemaVersion, compositionId: _compositionId, step: _step, reason: _reason, ...surface } = snapshot;
1662
+ return stableHash(surface);
1663
+ }
1576
1664
  function isInteractionResumeAck(event) {
1577
1665
  return (event.type === 'sandbox_boundary_decision_ack' ||
1578
1666
  event.type === 'user_question_answer_ack' ||