@namzu/cli 19.0.0 → 21.0.0

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 (164) hide show
  1. package/CHANGELOG.md +308 -0
  2. package/README.md +42 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +24 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/acp.d.ts.map +1 -1
  7. package/dist/commands/acp.js +6 -1
  8. package/dist/commands/acp.js.map +1 -1
  9. package/dist/commands/run-stream.d.ts.map +1 -1
  10. package/dist/commands/run-stream.js +9 -1
  11. package/dist/commands/run-stream.js.map +1 -1
  12. package/dist/commands/run.d.ts.map +1 -1
  13. package/dist/commands/run.js +14 -11
  14. package/dist/commands/run.js.map +1 -1
  15. package/dist/config/load.d.ts.map +1 -1
  16. package/dist/config/load.js +21 -0
  17. package/dist/config/load.js.map +1 -1
  18. package/dist/config/schema.d.ts +11 -4
  19. package/dist/config/schema.d.ts.map +1 -1
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/doctor/checks/chain.d.ts.map +1 -1
  22. package/dist/doctor/checks/chain.js +11 -5
  23. package/dist/doctor/checks/chain.js.map +1 -1
  24. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  25. package/dist/doctor/checks/credentials.js +5 -1
  26. package/dist/doctor/checks/credentials.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/integrations/providers/access.d.ts +7 -0
  31. package/dist/integrations/providers/access.d.ts.map +1 -0
  32. package/dist/integrations/providers/access.js +17 -0
  33. package/dist/integrations/providers/access.js.map +1 -0
  34. package/dist/integrations/providers/discover.d.ts +17 -54
  35. package/dist/integrations/providers/discover.d.ts.map +1 -1
  36. package/dist/integrations/providers/discover.js +46 -58
  37. package/dist/integrations/providers/discover.js.map +1 -1
  38. package/dist/integrations/providers/harness-credentials.d.ts +14 -0
  39. package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
  40. package/dist/integrations/providers/harness-credentials.js +46 -0
  41. package/dist/integrations/providers/harness-credentials.js.map +1 -1
  42. package/dist/integrations/providers/model-reasoning.d.ts +4 -0
  43. package/dist/integrations/providers/model-reasoning.d.ts.map +1 -0
  44. package/dist/integrations/providers/model-reasoning.js +46 -0
  45. package/dist/integrations/providers/model-reasoning.js.map +1 -0
  46. package/dist/integrations/providers/register.d.ts +1 -1
  47. package/dist/integrations/providers/register.d.ts.map +1 -1
  48. package/dist/integrations/providers/register.js +11 -1
  49. package/dist/integrations/providers/register.js.map +1 -1
  50. package/dist/integrations/providers/registry.d.ts +4 -4
  51. package/dist/integrations/providers/registry.d.ts.map +1 -1
  52. package/dist/integrations/providers/registry.js +23 -0
  53. package/dist/integrations/providers/registry.js.map +1 -1
  54. package/dist/integrations/sessions/context-inventory.d.ts +4 -0
  55. package/dist/integrations/sessions/context-inventory.d.ts.map +1 -0
  56. package/dist/integrations/sessions/context-inventory.js +44 -0
  57. package/dist/integrations/sessions/context-inventory.js.map +1 -0
  58. package/dist/integrations/sessions/conversation-search.d.ts +60 -0
  59. package/dist/integrations/sessions/conversation-search.d.ts.map +1 -0
  60. package/dist/integrations/sessions/conversation-search.js +461 -0
  61. package/dist/integrations/sessions/conversation-search.js.map +1 -0
  62. package/dist/integrations/sessions/task-context.d.ts +4 -0
  63. package/dist/integrations/sessions/task-context.d.ts.map +1 -0
  64. package/dist/integrations/sessions/task-context.js +73 -0
  65. package/dist/integrations/sessions/task-context.js.map +1 -0
  66. package/dist/integrations/subagents/activity.d.ts +3 -1
  67. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  68. package/dist/integrations/subagents/activity.js +20 -2
  69. package/dist/integrations/subagents/activity.js.map +1 -1
  70. package/dist/integrations/subagents/runtime.d.ts +15 -6
  71. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  72. package/dist/integrations/subagents/runtime.js +274 -46
  73. package/dist/integrations/subagents/runtime.js.map +1 -1
  74. package/dist/memory/io.d.ts +12 -0
  75. package/dist/memory/io.d.ts.map +1 -0
  76. package/dist/memory/io.js +177 -0
  77. package/dist/memory/io.js.map +1 -0
  78. package/dist/memory/presentation.d.ts +12 -0
  79. package/dist/memory/presentation.d.ts.map +1 -0
  80. package/dist/memory/presentation.js +51 -0
  81. package/dist/memory/presentation.js.map +1 -0
  82. package/dist/memory/store.d.ts +17 -3
  83. package/dist/memory/store.d.ts.map +1 -1
  84. package/dist/memory/store.js +61 -35
  85. package/dist/memory/store.js.map +1 -1
  86. package/dist/output-schema.d.ts +4 -0
  87. package/dist/output-schema.d.ts.map +1 -0
  88. package/dist/output-schema.js +18 -0
  89. package/dist/output-schema.js.map +1 -0
  90. package/dist/permissions/mode.d.ts +3 -0
  91. package/dist/permissions/mode.d.ts.map +1 -1
  92. package/dist/permissions/mode.js +18 -0
  93. package/dist/permissions/mode.js.map +1 -1
  94. package/dist/tui/AgentExplorer.d.ts +13 -2
  95. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  96. package/dist/tui/AgentExplorer.js +93 -23
  97. package/dist/tui/AgentExplorer.js.map +1 -1
  98. package/dist/tui/App.d.ts.map +1 -1
  99. package/dist/tui/App.js +554 -107
  100. package/dist/tui/App.js.map +1 -1
  101. package/dist/tui/BrandHeader.d.ts +2 -5
  102. package/dist/tui/BrandHeader.d.ts.map +1 -1
  103. package/dist/tui/BrandHeader.js +3 -6
  104. package/dist/tui/BrandHeader.js.map +1 -1
  105. package/dist/tui/Composer.d.ts +3 -2
  106. package/dist/tui/Composer.d.ts.map +1 -1
  107. package/dist/tui/Composer.js +11 -9
  108. package/dist/tui/Composer.js.map +1 -1
  109. package/dist/tui/ComposerFrame.d.ts.map +1 -1
  110. package/dist/tui/ComposerFrame.js +1 -1
  111. package/dist/tui/ComposerFrame.js.map +1 -1
  112. package/dist/tui/PermissionOverlay.d.ts +1 -1
  113. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  114. package/dist/tui/PermissionOverlay.js +12 -4
  115. package/dist/tui/PermissionOverlay.js.map +1 -1
  116. package/dist/tui/Picker.d.ts.map +1 -1
  117. package/dist/tui/Picker.js +97 -21
  118. package/dist/tui/Picker.js.map +1 -1
  119. package/dist/tui/TextPrompt.d.ts +3 -1
  120. package/dist/tui/TextPrompt.d.ts.map +1 -1
  121. package/dist/tui/TextPrompt.js +55 -9
  122. package/dist/tui/TextPrompt.js.map +1 -1
  123. package/dist/tui/Transcript.d.ts +1 -9
  124. package/dist/tui/Transcript.d.ts.map +1 -1
  125. package/dist/tui/Transcript.js +32 -44
  126. package/dist/tui/Transcript.js.map +1 -1
  127. package/dist/tui/agent.d.ts +21 -4
  128. package/dist/tui/agent.d.ts.map +1 -1
  129. package/dist/tui/agent.js +207 -78
  130. package/dist/tui/agent.js.map +1 -1
  131. package/dist/tui/model-choices.d.ts +3 -1
  132. package/dist/tui/model-choices.d.ts.map +1 -1
  133. package/dist/tui/model-choices.js +12 -1
  134. package/dist/tui/model-choices.js.map +1 -1
  135. package/dist/tui/model-search.d.ts +4 -0
  136. package/dist/tui/model-search.d.ts.map +1 -0
  137. package/dist/tui/model-search.js +11 -0
  138. package/dist/tui/model-search.js.map +1 -0
  139. package/dist/tui/model-selection-intent.d.ts +8 -0
  140. package/dist/tui/model-selection-intent.d.ts.map +1 -0
  141. package/dist/tui/model-selection-intent.js +96 -0
  142. package/dist/tui/model-selection-intent.js.map +1 -0
  143. package/dist/tui/model-switch-tool.d.ts +7 -0
  144. package/dist/tui/model-switch-tool.d.ts.map +1 -0
  145. package/dist/tui/model-switch-tool.js +74 -0
  146. package/dist/tui/model-switch-tool.js.map +1 -0
  147. package/dist/tui/model-switch.d.ts +35 -0
  148. package/dist/tui/model-switch.d.ts.map +1 -0
  149. package/dist/tui/model-switch.js +106 -0
  150. package/dist/tui/model-switch.js.map +1 -0
  151. package/dist/tui/permission-review.d.ts.map +1 -1
  152. package/dist/tui/permission-review.js +25 -12
  153. package/dist/tui/permission-review.js.map +1 -1
  154. package/dist/tui/run-interruption.d.ts +7 -0
  155. package/dist/tui/run-interruption.d.ts.map +1 -1
  156. package/dist/tui/run-interruption.js +41 -0
  157. package/dist/tui/run-interruption.js.map +1 -1
  158. package/dist/tui/slashCommands.d.ts +5 -0
  159. package/dist/tui/slashCommands.d.ts.map +1 -1
  160. package/dist/tui/slashCommands.js +188 -15
  161. package/dist/tui/slashCommands.js.map +1 -1
  162. package/dist/tui/types.d.ts +6 -13
  163. package/dist/tui/types.d.ts.map +1 -1
  164. package/package.json +8 -7
package/dist/tui/agent.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * `emptySession()` whose `send()` yields a single error event so the UI
21
21
  * renders an actionable hint rather than crashing.
22
22
  */
23
- import { BOOT_EVENT_NAMES, BackgroundJobRegistry, DefaultPathBuilder, DiskCheckpointStore, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, WebFetchTool, batchNeedsReview, buildAskUserQuestionTool, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createReviewHandler, createToolPresenter, generateProjectId, generateRunId, generateSessionId, generateTenantId, generateTopicId, getBuiltinTools, isReviewExempt, query, resumeRun, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
23
+ import { BOOT_EVENT_NAMES, BackgroundJobRegistry, DefaultPathBuilder, DiskCheckpointStore, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, ToolRegistry, WebFetchTool, batchNeedsReview, buildAskUserQuestionTool, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createMemoryRecallStep, createReviewHandler, createToolPresenter, generateProjectId, generateRunId, generateSessionId, generateTenantId, generateTopicId, getBuiltinTools, isReviewExempt, query, resumeRun, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
24
24
  import { SubprocessComputerUseHost } from '@namzu/computer-use';
25
25
  import { realpath, stat } from 'node:fs/promises';
26
26
  import { join, parse, resolve } from 'node:path';
@@ -34,15 +34,22 @@ import { resolveSandbox, sandboxResolvedSeverity, } from '../context/sandbox.js'
34
34
  import { composeTurnSnapshot, readTurnSnapshot } from '../context/turn-snapshot.js';
35
35
  import { connectMcpServers, } from '../integrations/mcp/servers.js';
36
36
  import { createCliPluginRuntime } from '../integrations/plugins/runtime.js';
37
+ import { hasApiCredential, requiresCredentialForModel } from '../integrations/providers/access.js';
37
38
  import { CredentialRefreshRejectedError, CredentialWithdrawnError, PROVIDER_REGISTRY, chainCapabilityDisagreements, chainPositionName, describeAcceptedMismatch, describeCapabilityRefusal, discoverProviders, ensureFreshAnthropicToken, ensureFreshStoredCodexCredential, ensureRegistered, findDetected, isAnthropicOAuthToken, isRegistered, missingCredentialMessage, primaryProvider, readCodexCredentialFile, readPreferences, readSubscriptionCredential, resolveChainCapabilities, sameOAuthCredential, unresolvedMembers, unsupportedProviderMessage, } from '../integrations/providers/index.js';
39
+ import { modelReasoningView } from '../integrations/providers/model-reasoning.js';
40
+ import { createContextInventoryStep } from '../integrations/sessions/context-inventory.js';
41
+ import { buildConversationReadTool, buildConversationSearchTool, } from '../integrations/sessions/conversation-search.js';
42
+ import { createTaskContextStep } from '../integrations/sessions/task-context.js';
38
43
  import { ensurePrivateStateDirectory } from '../integrations/state/private-directory.js';
39
44
  import { discoverAgentDefinitions } from '../integrations/subagents/definitions.js';
40
45
  import { SubagentPathBuilder, resolveSubagentParent } from '../integrations/subagents/parent.js';
41
46
  import { CLI_INTERACTIVE_RUN_TIMEOUT_MS } from '../integrations/subagents/policy.js';
42
47
  import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
43
48
  import { cliLogger } from '../logging.js';
49
+ import { formatMemoryDiagnostics } from '../memory/presentation.js';
44
50
  import { composeMemoryPrompt, readMemory } from '../memory/store.js';
45
51
  import { projectRunConversation } from './conversation-history.js';
52
+ import { buildSwitchModelTool } from './model-switch-tool.js';
46
53
  /**
47
54
  * Let one caller stop waiting without cutting a shared queue in the middle.
48
55
  *
@@ -303,10 +310,12 @@ function credentialGap(prefs, detected) {
303
310
  // one ever did, it is not a credential problem and must not be reported as
304
311
  // one — a wrong diagnosis sends the operator to paste a key that would not
305
312
  // have helped.
306
- if (!entry || !entry.constructible || !entry.requiresApiKey)
313
+ if (!entry ||
314
+ !entry.constructible ||
315
+ !requiresCredentialForModel(entry, primary.model ?? entry.defaultModel))
307
316
  return null;
308
317
  const det = findDetected(detected, primary.id);
309
- if (det?.apiKey)
318
+ if (hasApiCredential(entry, det?.apiKey))
310
319
  return null;
311
320
  return { providerId: primary.id, reason: missingCredentialMessage(entry) };
312
321
  }
@@ -363,9 +372,12 @@ function foregroundOnlyBash(tool) {
363
372
  };
364
373
  }
365
374
  function builtinTools(backgroundJobs) {
366
- return getBuiltinTools().flatMap((tool) => {
367
- if (EXCLUDED_BUILTINS.has(tool.name))
375
+ return getBuiltinTools().flatMap((source) => {
376
+ if (EXCLUDED_BUILTINS.has(source.name))
368
377
  return [];
378
+ const tool = ['bash', 'write', 'edit'].includes(source.name)
379
+ ? { ...source, executionBarrier: true }
380
+ : source;
369
381
  if (backgroundJobs)
370
382
  return [tool];
371
383
  if (tool.name === 'job')
@@ -399,14 +411,8 @@ function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgrou
399
411
  // handing `getIndex()` to the synchronous overload before the first store
400
412
  // read makes a new process report every persisted memory as absent.
401
413
  registry.register(buildMemoryTools(memoryStore));
402
- // `search_tools` is deliberately NOT registered here. It is only useful
403
- // where deferred tools exist, and that differs between this function's two
404
- // callers: the session below passes a `taskStore`, so query() registers the
405
- // task tools deferred and the search has a roster; a sub-agent is built with
406
- // no task store, so its registry has nothing deferred and the tool could
407
- // only ever answer "no deferred tools matching X" — a capability advertised
408
- // every turn that costs a turn to discover is unusable. Mounting it is the
409
- // caller's decision, made where the roster is known.
414
+ // query() mounts search_tools only if a deferred roster actually exists,
415
+ // after runtime tools are registered. Ordinary CLI task tools are active.
410
416
  return { registry, memoryStore };
411
417
  }
412
418
  /** Refresh plugin skill metadata before each provider operation. */
@@ -464,7 +470,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
464
470
  return emptySession(`Unknown provider "${primary.id}" — pick another.`, 'invocation');
465
471
  }
466
472
  const det = findDetected(detected, primary.id);
467
- if (entry.requiresApiKey && (!det || !det.apiKey)) {
473
+ if (requiresCredentialForModel(entry, primary.model ?? entry.defaultModel) &&
474
+ !hasApiCredential(entry, det?.apiKey)) {
468
475
  // The BACKSTOP, not the operator-facing answer. The TUI never reaches this
469
476
  // line any more: `probeAgentSession` reports the same gap as a
470
477
  // `credentialGap` and the App routes into the picker, where a credential
@@ -473,7 +480,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
473
480
  // pieces of advice below are real: an environment variable, or
474
481
  // `--provider`. Keeping the refusal is what makes those runs fail rather
475
482
  // than quietly start on something else.
476
- return emptySession(`No credential found for ${entry.label}. Set one of: ${entry.envVars.join(', ')} — or pass --provider with one that is configured.`);
483
+ return emptySession(`No credential found for ${entry.label}${entry.id === 'zen' ? ' with the selected model. Choose muse-spark-1.3-contributor-free for public access' : ''}. Set one of: ${entry.envVars.join(', ')} — or pass --provider with one that is configured.`);
477
484
  }
478
485
  try {
479
486
  await ensureRegistered(primary.id);
@@ -523,7 +530,9 @@ export async function createAgentSession(prefs, detected, options = {}) {
523
530
  }
524
531
  let provider;
525
532
  try {
526
- provider = constructProvider(primary.id, det, model);
533
+ provider = constructProvider(primary.id, det, model, {
534
+ sessionId: scope.sessionId,
535
+ });
527
536
  }
528
537
  catch (err) {
529
538
  return emptySession(`Failed to construct ${entry.label}: ${err instanceof Error ? err.message : String(err)}`);
@@ -597,7 +606,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
597
606
  if (fresh === currentToken)
598
607
  return;
599
608
  try {
600
- const refreshedProvider = constructProvider('anthropic', { ...det, apiKey: fresh }, model);
609
+ const refreshedProvider = constructProvider('anthropic', { ...det, apiKey: fresh }, model, { sessionId: scope.sessionId });
601
610
  // Publish the pair together. If construction fails, both old values
602
611
  // remain live and the next operation can retry against the stored token.
603
612
  provider = refreshedProvider;
@@ -634,7 +643,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
634
643
  expiresAt: credential.expiresAt,
635
644
  origin: 'codex-file',
636
645
  },
637
- }, model);
646
+ }, model, { sessionId: scope.sessionId });
638
647
  provider = refreshedProvider;
639
648
  currentToken = credential.accessToken;
640
649
  currentCodexAccount = credential.accountId;
@@ -652,7 +661,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
652
661
  }
653
662
  if (credential.accessToken === currentToken)
654
663
  return;
655
- provider = constructProvider('anthropic', { ...det, apiKey: credential.accessToken }, model);
664
+ provider = constructProvider('anthropic', { ...det, apiKey: credential.accessToken }, model, { sessionId: scope.sessionId });
656
665
  currentToken = credential.accessToken;
657
666
  };
658
667
  const refreshStoredCodex = async (signal) => {
@@ -668,7 +677,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
668
677
  expiresAt: credential.expiresAt,
669
678
  origin: 'stored',
670
679
  },
671
- }, model);
680
+ }, model, { sessionId: scope.sessionId });
672
681
  provider = refreshedProvider;
673
682
  currentToken = credential.accessToken;
674
683
  currentCodexAccount = credential.accountId;
@@ -695,6 +704,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
695
704
  credentialTail = queued.catch(() => { });
696
705
  return signal ? observeWithSignal(queued, signal) : queued;
697
706
  };
707
+ // The TUI can replace its conversation without replacing this session object.
708
+ // Bind each admitted run to its captured conversation, including durable resumes;
709
+ // a Zen client must never generate a fresh Go session for each model call.
710
+ const providerForSession = (sessionId) => primary.id === 'zen' || primary.id === 'zen-go'
711
+ ? constructProvider(primary.id, det, model, { sessionId })
712
+ : provider;
698
713
  // Session-owned discovery with one drain cursor per run. A child shares the
699
714
  // discovered scopes without being able to consume the parent's update, and
700
715
  // an edit takes effect in this session rather than only after reconnecting.
@@ -874,9 +889,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
874
889
  computerUseReady: computerUseHost !== undefined,
875
890
  ...(computerUseError ? { computerUseError } : {}),
876
891
  });
877
- // This session passes a `taskStore` to query() below, which registers the
878
- // task tools deferred — so `search_tools` has something to find here.
879
- registry.register([SearchToolsTool]);
880
892
  // Web reach, opted into in the config file and nowhere else. The parent's
881
893
  // registry only: a child's config carries no provider, and a tool whose
882
894
  // provider is missing is a tool that reports itself unwired — truthful,
@@ -890,6 +902,19 @@ export async function createAgentSession(prefs, detected, options = {}) {
890
902
  // delegate a self-contained task to a fresh sub-agent (own context window).
891
903
  // Best-effort — if the runtime can't stand up, the chat still works.
892
904
  const delegationScopes = new Map();
905
+ const delegatedInputWaiters = new Map();
906
+ if (options.conversationSessions) {
907
+ const sessions = options.conversationSessions;
908
+ for (const build of [buildConversationSearchTool, buildConversationReadTool])
909
+ registry.register(build((context) => {
910
+ const owner = delegationScopes.get(context.runId);
911
+ if (!owner ||
912
+ owner.projectId !== sessions.projectId ||
913
+ owner.tenantId !== sessions.tenantId)
914
+ throw new Error('The requesting run does not own this conversation.');
915
+ return { sessions, sessionId: owner.sessionId };
916
+ }));
917
+ }
893
918
  let subagentRuntime;
894
919
  // Stays empty when the runtime below throws, which is the honest answer: the
895
920
  // catch is non-fatal and the session then genuinely has no delegate to
@@ -921,6 +946,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
921
946
  },
922
947
  sandboxWorkspace,
923
948
  resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
949
+ resolveWaitForInbound: (runId) => delegatedInputWaiters.get(runId),
924
950
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
925
951
  ...(options.sandbox?.teardownTimeoutMs !== undefined
926
952
  ? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
@@ -948,7 +974,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
948
974
  // child announcing a swap the parent never made, inside a tool result,
949
975
  // is a worse surface than the child simply failing and the parent
950
976
  // reporting it.
951
- buildProvider: () => constructProvider(primary.id, det ? { ...det, apiKey: currentToken ?? det.apiKey } : det, model),
977
+ buildProvider: (sessionId) => {
978
+ if (!sessionId && (primary.id === 'zen' || primary.id === 'zen-go')) {
979
+ throw new Error('A delegated provider requires its invoking conversation.');
980
+ }
981
+ return constructProvider(primary.id, det ? { ...det, apiKey: currentToken ?? det.apiKey } : det, model, { sessionId });
982
+ },
952
983
  buildTools: () => {
953
984
  // Sub-agents get the parent's working set minus `search_tools`:
954
985
  // they run without a task store, so nothing in their registry is
@@ -964,7 +995,9 @@ export async function createAgentSession(prefs, detected, options = {}) {
964
995
  authorizationGate: gateFor(options.rules),
965
996
  });
966
997
  subagentRuntime = sub;
967
- registry.register([sub.agentTool]);
998
+ registry.register([sub.agentTool, sub.waitForTaskTool]);
999
+ if (sub.sendMessageTool)
1000
+ registry.register(sub.sendMessageTool);
968
1001
  allowedAgentIds = sub.allowedAgentIds;
969
1002
  }
970
1003
  catch (err) {
@@ -978,6 +1011,20 @@ export async function createAgentSession(prefs, detected, options = {}) {
978
1011
  // and got none had nothing on stderr to say why.
979
1012
  cliLogger().warn('sub-agent runtime unavailable this session', exceptionAttributes(err));
980
1013
  }
1014
+ // This capability belongs to the active main turn, never the child roster.
1015
+ const modelSwitchHandlers = new Map();
1016
+ if (options.allowModelSwitch) {
1017
+ registry.register(buildSwitchModelTool(async (request, context) => {
1018
+ const handler = modelSwitchHandlers.get(context.runId);
1019
+ if (!handler || context.abortSignal?.aborted) {
1020
+ return {
1021
+ kind: 'rejected',
1022
+ reason: 'This turn no longer owns model selection.',
1023
+ };
1024
+ }
1025
+ return handler(request, context.abortSignal);
1026
+ }));
1027
+ }
981
1028
  // `ask_user_question`, where somebody can answer. The SDK tool parks the
982
1029
  // run through the handler it was BUILT with, so that handler reads the
983
1030
  // turn's answerer through a holder the prelude fills: the tool is per
@@ -1019,8 +1066,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
1019
1066
  // query call, because the doctrine tells the model to plan with them and a
1020
1067
  // tool it must search for first is a tool it skips.
1021
1068
  //
1022
- // `search_tools` stays mounted above even so: a tool server or plugin can
1023
- // still register a deferred roster, and that is what the search is for. The
1069
+ // A tool server or plugin can still register a deferred roster; query()
1070
+ // mounts search_tools when one exists. Do not advertise an empty search. The
1024
1071
  // task tools are registered inside query(), after this function returns,
1025
1072
  // which is why the connect line reports no count of them — counting here
1026
1073
  // would mean restating query's registration order in the CLI.
@@ -1065,16 +1112,9 @@ export async function createAgentSession(prefs, detected, options = {}) {
1065
1112
  // Persists across turns: once the user picks "approve all", later tool
1066
1113
  // batches in this session run without prompting.
1067
1114
  const approval = { all: false };
1068
- // What a settled run leaves behind, over the SAME store `search_memory`
1069
- // reads on the next run. Built once per session rather than per turn: it
1070
- // holds no per-run state, and a per-turn construction would re-open the
1071
- // index for every message.
1072
- //
1073
- // Unconditional, unlike the answer gate. A gate changes what a run may do
1074
- // and so must be asked for; promotion changes only what survives it, and
1075
- // the alternative — the run's own extracted knowledge being discarded at
1076
- // settle — is what this repository has been doing all along by accident.
1077
- // A run that learned nothing still writes nothing.
1115
+ // Share the project store with tools and recall. Each run selects either
1116
+ // this extracted-claim promoter or explicit consolidation, never both.
1117
+ // Candidates without useful claims write nothing.
1078
1118
  const promoteMemory = createMemoryPromoter({ store: memoryStore });
1079
1119
  // Plugins are the last fallible startup resource. The ordering is ownership:
1080
1120
  // a malformed MCP entry cannot strand imported plugin hooks, and a plugin
@@ -1153,10 +1193,25 @@ export async function createAgentSession(prefs, detected, options = {}) {
1153
1193
  let reasoningEffortDefault;
1154
1194
  let effortNotice;
1155
1195
  try {
1156
- const capabilityView = withProviderFallback([
1196
+ const capabilityMembers = [
1157
1197
  { provider, model },
1158
- ...fallbackPlan.build(currentToken),
1159
- ]);
1198
+ ...fallbackPlan.build(currentToken, scope.sessionId),
1199
+ ];
1200
+ const capabilityView = withProviderFallback(await Promise.all(capabilityMembers.map(async (member) => {
1201
+ const memberModel = member.model ?? model;
1202
+ const known = member.provider.reasoningEffortLevelsFor
1203
+ ? member.provider.reasoningEffortLevelsFor(memberModel)
1204
+ : member.provider.effortLevelsFor?.(memberModel);
1205
+ // A model-owned answer (including []) needs no extra catalogue
1206
+ // request. Discover only missing capability information.
1207
+ const catalogue = known === undefined && member.provider.listModels
1208
+ ? await runPickerProviderOperation(undefined, (signal) => member.provider.listModels?.(signal) ?? Promise.resolve([])).catch(() => [])
1209
+ : [];
1210
+ return {
1211
+ ...member,
1212
+ provider: modelReasoningView(member.provider, memberModel, catalogue),
1213
+ };
1214
+ })));
1160
1215
  const offered = capabilityView.reasoningEffortLevelsFor
1161
1216
  ? capabilityView.reasoningEffortLevelsFor(model)
1162
1217
  : capabilityView.effortLevelsFor?.(model);
@@ -1197,7 +1252,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
1197
1252
  const pluginSkills = pluginRuntime
1198
1253
  ? await currentPluginSkills(pluginRuntime.skills)
1199
1254
  : undefined;
1200
- const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
1255
+ const curatedMemory = readMemory(undefined, cwd);
1256
+ for (const notice of formatMemoryDiagnostics(curatedMemory))
1257
+ cliLogger().warn(notice);
1258
+ const memoryPrompt = composeMemoryPrompt(curatedMemory);
1201
1259
  const environmentPrompt = composeEnvironmentPrompt({
1202
1260
  ...(await readEnvironmentFacts(cwd)),
1203
1261
  additionalDirectories: [...directories],
@@ -1217,15 +1275,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
1217
1275
  }
1218
1276
  delegatedResumeHandlers.set(entry.runId, resumeHandler);
1219
1277
  delegationScopes.set(entry.runId, { ...entry, topicId: scope.topicId });
1278
+ const runTaskStore = selectTaskStore(entry.runId, { ...entry, topicId: scope.topicId });
1220
1279
  try {
1221
1280
  return await resumeRun({
1222
- provider,
1223
- fallbackProviders: fallbackPlan.build(currentToken),
1281
+ provider: providerForSession(entry.sessionId),
1282
+ fallbackProviders: fallbackPlan.build(currentToken, entry.sessionId),
1224
1283
  tools: registry,
1225
1284
  pluginManager: pluginRuntime?.manager,
1226
1285
  skillRegistry: pluginRuntime?.skills,
1227
1286
  skills: pluginSkills,
1228
- taskStore: selectTaskStore(entry.runId, { ...entry, topicId: scope.topicId }),
1287
+ taskStore: runTaskStore,
1229
1288
  // The same availability the original run registered under.
1230
1289
  // A resumed run re-registers the task tools; leaving them at
1231
1290
  // the kernel's `deferred` default would hand the model a plan
@@ -1242,6 +1301,21 @@ export async function createAgentSession(prefs, detected, options = {}) {
1242
1301
  : {}),
1243
1302
  authorizationGate: gateFor(options.rules),
1244
1303
  compactionConfig: compactionConfigFor(options.compaction),
1304
+ prepareStep: [
1305
+ createTaskContextStep(runTaskStore, entry.tenantId),
1306
+ ...(options.memory?.recall === false
1307
+ ? []
1308
+ : [
1309
+ createMemoryRecallStep({
1310
+ store: memoryStore,
1311
+ identifierGrounding: options.memory?.identifierGrounding,
1312
+ }),
1313
+ ]),
1314
+ ...(options.conversationSessions ? [createContextInventoryStep()] : []),
1315
+ ],
1316
+ ...(options.compaction?.consolidate
1317
+ ? { consolidateInto: memoryStore }
1318
+ : { promoteMemory }),
1245
1319
  projectInstructionContext: projectInstructions.createRunContext(),
1246
1320
  pathBuilder,
1247
1321
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
@@ -1389,11 +1463,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
1389
1463
  reasoningEffortLevels,
1390
1464
  reasoningEffortDefault,
1391
1465
  compact: (messages) => operations.promise(undefined, async (signal) => {
1466
+ const sessionId = scope.sessionId;
1392
1467
  await prepareProviderCredential(signal);
1393
1468
  return compactNow({
1394
1469
  messages,
1395
1470
  config: compactionConfigFor(options.compaction),
1396
- provider,
1471
+ provider: providerForSession(sessionId),
1397
1472
  model,
1398
1473
  signal,
1399
1474
  });
@@ -1457,6 +1532,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
1457
1532
  throw new Error(`Run ${runId} already owns a delegated review channel.`);
1458
1533
  }
1459
1534
  delegatedResumeHandlers.set(runId, resumeHandler);
1535
+ if (opts?.onModelSwitch)
1536
+ modelSwitchHandlers.set(runId, opts.onModelSwitch);
1537
+ if (opts?.waitForInbound)
1538
+ delegatedInputWaiters.set(runId, opts.waitForInbound);
1460
1539
  const turnScope = { ...scope };
1461
1540
  delegationScopes.set(runId, turnScope);
1462
1541
  try {
@@ -1477,7 +1556,11 @@ export async function createAgentSession(prefs, detected, options = {}) {
1477
1556
  const pluginSkills = pluginRuntime
1478
1557
  ? await currentPluginSkills(pluginRuntime.skills)
1479
1558
  : undefined;
1480
- const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
1559
+ const curatedMemory = readMemory(undefined, cwd);
1560
+ for (const notice of formatMemoryDiagnostics(curatedMemory)) {
1561
+ yield { kind: 'context', text: notice, shed: false };
1562
+ }
1563
+ const memoryPrompt = composeMemoryPrompt(curatedMemory);
1481
1564
  currentOnQuestion = opts?.onQuestion;
1482
1565
  const [environmentFacts, turnSnapshot] = await Promise.all([
1483
1566
  readEnvironmentFacts(cwd),
@@ -1549,9 +1632,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
1549
1632
  capturedAuthority = Object.freeze({ ...opts.goalRound });
1550
1633
  goalAuthorities.set(opts.runId, capturedAuthority);
1551
1634
  }
1635
+ const runTaskStore = selectTaskStore(runId, turnScope);
1552
1636
  try {
1553
1637
  yield* runTurn({
1554
- provider,
1638
+ provider: providerForSession(turnScope.sessionId),
1555
1639
  compactionConfig: compactionConfigFor(options.compaction),
1556
1640
  ...(options.compaction?.consolidate ? { consolidateInto: memoryStore } : {}),
1557
1641
  ...(jobRegistry
@@ -1567,7 +1651,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1567
1651
  // that expired hours ago — and a chain whose own members are stale
1568
1652
  // is a fallback that fails for the reason the fallback exists to
1569
1653
  // survive. Building a driver is a client object, not a request.
1570
- fallbackProviders: fallbackPlan.build(currentToken),
1654
+ fallbackProviders: fallbackPlan.build(currentToken, turnScope.sessionId),
1571
1655
  model,
1572
1656
  tools: registry,
1573
1657
  pluginManager: pluginRuntime?.manager,
@@ -1580,16 +1664,31 @@ export async function createAgentSession(prefs, detected, options = {}) {
1580
1664
  ...(options.limits ? { limits: options.limits } : {}),
1581
1665
  sandboxWorkspace,
1582
1666
  rules: options.rules,
1667
+ structuredOutput: options.structuredOutput,
1583
1668
  reviewAnswer: options.reviewAnswer,
1584
1669
  maxAnswerReviews: options.maxAnswerReviews,
1585
- promoteMemory,
1586
- taskStore: selectTaskStore(runId, turnScope),
1670
+ promoteMemory: options.compaction?.consolidate ? undefined : promoteMemory,
1671
+ prepareStep: [
1672
+ createTaskContextStep(runTaskStore, turnScope.tenantId),
1673
+ ...(options.memory?.recall === false
1674
+ ? []
1675
+ : [
1676
+ createMemoryRecallStep({
1677
+ store: memoryStore,
1678
+ query: lastUserText(messages),
1679
+ identifierGrounding: options.memory?.identifierGrounding,
1680
+ }),
1681
+ ]),
1682
+ ...(options.conversationSessions ? [createContextInventoryStep()] : []),
1683
+ ],
1684
+ taskStore: runTaskStore,
1587
1685
  systemPrompt,
1588
1686
  messages,
1589
1687
  projectInstructionContext: projectInstructions.createRunContext(),
1590
1688
  opts: turnOpts,
1591
1689
  resumeHandler,
1592
1690
  taskGateway: await subagentRuntime?.gatewayForRun(runId),
1691
+ completionInbox: await subagentRuntime?.completionInboxForRun(runId),
1593
1692
  promptContributions,
1594
1693
  ...(webCapability ? { web: webCapability } : {}),
1595
1694
  // Active, not deferred: the doctrine tells the model to open a
@@ -1620,6 +1719,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
1620
1719
  finally {
1621
1720
  if (delegatedResumeHandlers.get(runId) === resumeHandler) {
1622
1721
  delegatedResumeHandlers.delete(runId);
1722
+ modelSwitchHandlers.delete(runId);
1723
+ delegatedInputWaiters.delete(runId);
1623
1724
  delegationScopes.delete(runId);
1624
1725
  await subagentRuntime?.releaseRun(runId);
1625
1726
  }
@@ -1650,7 +1751,8 @@ function planFallbacks(members, detected) {
1650
1751
  continue;
1651
1752
  }
1652
1753
  const det = findDetected(detected, member.id);
1653
- if (entry.requiresApiKey && !det?.apiKey) {
1754
+ if (requiresCredentialForModel(entry, member.model ?? entry.defaultModel) &&
1755
+ !hasApiCredential(entry, det?.apiKey)) {
1654
1756
  notices.push(`Provider chain: ${position} (${entry.label}) has no credential, so nothing will fall over to it. ` +
1655
1757
  `Set one of: ${entry.envVars.join(', ')}.`);
1656
1758
  continue;
@@ -1665,7 +1767,7 @@ function planFallbacks(members, detected) {
1665
1767
  }
1666
1768
  return {
1667
1769
  notices,
1668
- build(headToken) {
1770
+ build(headToken, sessionId) {
1669
1771
  const out = [];
1670
1772
  for (const { choice, det } of usable) {
1671
1773
  const entry = PROVIDER_REGISTRY[choice.id];
@@ -1675,7 +1777,9 @@ function planFallbacks(members, detected) {
1675
1777
  try {
1676
1778
  const credential = headToken !== undefined && det?.oauth ? { ...det, apiKey: headToken } : det;
1677
1779
  out.push({
1678
- provider: constructProvider(choice.id, credential, memberModel),
1780
+ provider: constructProvider(choice.id, credential, memberModel, {
1781
+ sessionId,
1782
+ }),
1679
1783
  model: memberModel,
1680
1784
  });
1681
1785
  }
@@ -1691,7 +1795,7 @@ function planFallbacks(members, detected) {
1691
1795
  },
1692
1796
  };
1693
1797
  }
1694
- export function constructProvider(id, det, model) {
1798
+ export function constructProvider(id, det, model, context = {}) {
1695
1799
  switch (id) {
1696
1800
  case 'anthropic': {
1697
1801
  const token = det?.apiKey ?? '';
@@ -1743,6 +1847,24 @@ export function constructProvider(id, det, model) {
1743
1847
  });
1744
1848
  return provider;
1745
1849
  }
1850
+ case 'zen':
1851
+ case 'zen-go': {
1852
+ const apiKey = hasApiCredential(PROVIDER_REGISTRY[id], det?.apiKey) ? det?.apiKey : undefined;
1853
+ if (id === 'zen-go' && apiKey === undefined)
1854
+ throw new Error('Zen Go requires an API key.');
1855
+ const { provider } = ProviderRegistry.create({
1856
+ ...(id === 'zen-go'
1857
+ ? { type: 'zen-go', apiKey: apiKey }
1858
+ : {
1859
+ type: 'zen',
1860
+ ...(apiKey === undefined ? {} : { apiKey }),
1861
+ }),
1862
+ baseURL: det?.baseUrl,
1863
+ model,
1864
+ ...(context.sessionId ? { sessionId: context.sessionId } : {}),
1865
+ });
1866
+ return provider;
1867
+ }
1746
1868
  case 'ollama': {
1747
1869
  const { provider } = ProviderRegistry.create({
1748
1870
  type: 'ollama',
@@ -2007,7 +2129,7 @@ function compactionConfigFor(compaction) {
2007
2129
  : {}),
2008
2130
  };
2009
2131
  }
2010
- async function* runTurn({ provider, compactionConfig, consolidateInto, backgroundJobs, backgroundJobOwner, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, limits, additionalDirectories, sandboxWorkspace, rules, reviewAnswer, maxAnswerReviews, promoteMemory, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, promptContributions, runtimeToolOverrides, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
2132
+ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroundJobs, backgroundJobOwner, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, limits, additionalDirectories, sandboxWorkspace, rules, structuredOutput, reviewAnswer, maxAnswerReviews, promoteMemory, prepareStep, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, completionInbox, promptContributions, runtimeToolOverrides, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
2011
2133
  const signal = opts?.signal;
2012
2134
  // One presenter for the whole stream, built from the registry this scope
2013
2135
  // already holds. Its absence HERE is what forced presentation to be name
@@ -2016,6 +2138,7 @@ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroun
2016
2138
  const presenter = createToolPresenter(tools);
2017
2139
  try {
2018
2140
  const events = query({
2141
+ ...(structuredOutput ? { structuredOutput } : {}),
2019
2142
  provider,
2020
2143
  pathBuilder,
2021
2144
  ...(opts?.runId ? { runId: opts.runId } : {}),
@@ -2062,17 +2185,17 @@ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroun
2062
2185
  // that shipped before gates existed.
2063
2186
  ...(reviewAnswer ? { reviewAnswer } : {}),
2064
2187
  ...(maxAnswerReviews !== undefined ? { maxAnswerReviews } : {}),
2065
- // Always present, unlike the gate: a gate changes what a run may
2066
- // do and so must be asked for; promotion changes only what
2067
- // survives it, and a run that learned nothing still writes
2068
- // nothing.
2188
+ // Undefined when the host selected consolidation as its writer.
2069
2189
  promoteMemory,
2190
+ ...(prepareStep ? { prepareStep } : {}),
2070
2191
  agentId: 'namzu',
2071
2192
  agentName: 'namzu',
2072
2193
  ...(systemPrompt ? { systemPrompt } : {}),
2073
2194
  projectInstructionContext,
2074
2195
  messages: [...messages],
2075
2196
  ...(opts?.inboundMessages ? { inboundMessages: opts.inboundMessages } : {}),
2197
+ ...(opts?.waitForInbound ? { waitForInbound: opts.waitForInbound } : {}),
2198
+ ...(completionInbox ? { completionInbox } : {}),
2076
2199
  workingDirectory,
2077
2200
  ...(additionalDirectories?.length ? { additionalDirectories } : {}),
2078
2201
  // The exemption reads `tools` at decision time, so it sees the task
@@ -2086,6 +2209,7 @@ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroun
2086
2209
  ...scope,
2087
2210
  });
2088
2211
  let settled = false;
2212
+ let abortReported = false;
2089
2213
  try {
2090
2214
  while (true) {
2091
2215
  const next = await events.next();
@@ -2106,8 +2230,14 @@ async function* runTurn({ provider, compactionConfig, consolidateInto, backgroun
2106
2230
  // be a recording of the interface rather than of the session.
2107
2231
  onRunEvent?.(event);
2108
2232
  if (signal?.aborted) {
2109
- yield { kind: 'error', message: 'aborted' };
2110
- return;
2233
+ if (!abortReported) {
2234
+ abortReported = true;
2235
+ yield { kind: 'error', message: 'aborted' };
2236
+ }
2237
+ // Let cancellation settle in the kernel. Calling return() here
2238
+ // discarded its Run and forced App to save only visible prose,
2239
+ // losing tool receipts and reasoning before the next user turn.
2240
+ continue;
2111
2241
  }
2112
2242
  const mapped = toAgentEvent(event, presenter);
2113
2243
  if (!mapped)
@@ -2215,14 +2345,12 @@ export function toAgentEvent(event, presenter) {
2215
2345
  output: event.result,
2216
2346
  });
2217
2347
  const detail = viewToLines(view);
2218
- // For output shown line by line, the summary IS the first rendered
2219
- // line, so the body can drop it without a second, differently
2220
- // whitespaced copy of the same text — a `read` used to show its
2221
- // first line twice, once collapsed and once numbered.
2348
+ // Drop only an exact duplicate. A shortened summary cannot replace
2349
+ // the first line's evidence in expanded or raw output.
2222
2350
  const summary = view.kind === 'terminal' && detail && detail.length > 0
2223
2351
  ? truncate(detail[0], 120)
2224
2352
  : firstLine(event.result);
2225
- const withoutRepeatedSummary = view.kind === 'terminal' && detail && detail.length > 0 ? detail.slice(1) : detail;
2353
+ const withoutRepeatedSummary = view.kind === 'terminal' && detail?.[0] === summary ? detail.slice(1) : detail;
2226
2354
  return {
2227
2355
  kind: 'tool-end',
2228
2356
  runId: event.runId,
@@ -2334,6 +2462,7 @@ export function toAgentEvent(event, presenter) {
2334
2462
  // answer was refused.
2335
2463
  return {
2336
2464
  kind: 'done',
2465
+ text: event.result,
2337
2466
  ...(event.budget ? { budget: event.budget } : {}),
2338
2467
  ...(event.stopReason ? { stopReason: event.stopReason } : {}),
2339
2468
  };
@@ -2483,7 +2612,7 @@ function describeFallback(event) {
2483
2612
  * server's, a plugin's — could not get a diff no matter what it did.
2484
2613
  *
2485
2614
  * The tool now says which admitted shape it wants, and this decides what
2486
- * that looks like in a terminal. Clamping and the `STDOUT:`/`STDERR:`
2615
+ * that looks like in a terminal. Collapsing and the `STDOUT:`/`STDERR:`
2487
2616
  * cleanup stay here on purpose: how many rows fit and how a shell labels
2488
2617
  * its streams are properties of this surface, not of the tool.
2489
2618
  */
@@ -2499,13 +2628,13 @@ export function viewToLines(view) {
2499
2628
  // never produces this — it returns no view at all for an insert,
2500
2629
  // rather than claim the file was empty.
2501
2630
  if (view.before === '') {
2502
- const lines = clampLines(view.after);
2631
+ const lines = outputLines(view.after);
2503
2632
  return lines.length > 0 ? lines : undefined;
2504
2633
  }
2505
2634
  const lines = [];
2506
- for (const line of clampLines(view.before))
2635
+ for (const line of outputLines(view.before))
2507
2636
  lines.push(`- ${line}`);
2508
- for (const line of clampLines(view.after))
2637
+ for (const line of outputLines(view.after))
2509
2638
  lines.push(`+ ${line}`);
2510
2639
  return lines.length > 0 ? lines : undefined;
2511
2640
  }
@@ -2514,7 +2643,7 @@ export function viewToLines(view) {
2514
2643
  return undefined;
2515
2644
  const lines = resultToLines(view.output);
2516
2645
  // A single short line is already the summary — no need to repeat it.
2517
- return lines.length <= 1 ? undefined : lines;
2646
+ return lines.length === 1 && lines[0] === truncate(lines[0] ?? '', 120) ? undefined : lines;
2518
2647
  }
2519
2648
  }
2520
2649
  }
@@ -2533,10 +2662,10 @@ function truncate(value, max) {
2533
2662
  const oneLine = value.replace(/\s+/g, ' ');
2534
2663
  return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
2535
2664
  }
2536
- const MAX_DETAIL_LINES = 200;
2537
- function clampLines(value) {
2538
- const lines = value.replace(/\s+$/, '').split('\n');
2539
- return lines.length > MAX_DETAIL_LINES ? lines.slice(0, MAX_DETAIL_LINES) : lines;
2665
+ function outputLines(value) {
2666
+ // The renderer bounds the preview. Retain admitted output so expanding
2667
+ // or selecting raw text never loses a diagnostic after an arbitrary line.
2668
+ return value.replace(/\s+$/, '').split('\n');
2540
2669
  }
2541
2670
  /** Parse a string as a JSON object, or null. Connector tools return JSON. */
2542
2671
  function parseJsonObject(s) {
@@ -2578,11 +2707,11 @@ function payloadString(result) {
2578
2707
  function resultToLines(result) {
2579
2708
  const payload = payloadString(result);
2580
2709
  if (payload !== null)
2581
- return clampLines(payload);
2710
+ return outputLines(payload);
2582
2711
  const obj = parseJsonObject(result);
2583
2712
  if (obj)
2584
- return clampLines(JSON.stringify(obj, null, 2));
2585
- return clampLines(cleanToolText(result.trim()));
2713
+ return outputLines(JSON.stringify(obj, null, 2));
2714
+ return outputLines(cleanToolText(result.trim()));
2586
2715
  }
2587
2716
  /** Concise one-line summary of a tool result for the `⎿` line. */
2588
2717
  function firstLine(result) {