wave-agent-sdk 1.1.5 → 1.3.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 (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -2,18 +2,20 @@ import * as aiService from "../services/aiService.js";
2
2
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
3
3
  import { supportsVision } from "../utils/modelCapabilities.js";
4
4
  import { persistToolImages } from "../utils/toolImagePersistence.js";
5
- import { parseTaskNotificationXml, taskNotificationToXml, } from "../utils/notificationXml.js";
5
+ import { parseTaskNotificationXml } from "../utils/notificationXml.js";
6
6
  import { estimateContextTokens } from "../utils/tokenCalculation.js";
7
7
  import { estimateTokens } from "../utils/tokenEstimate.js";
8
8
  import { getTaskReminderTurnCounts, maybeInjectTaskReminder, TASK_REMINDER_CONFIG, } from "../utils/taskReminder.js";
9
- import { createWriteStream, existsSync } from "node:fs";
10
- import * as os from "node:os";
9
+ import { getChangedFilesReminder } from "../utils/fileChangeReminder.js";
10
+ import { buildMcpInstructionsAnnouncement, collectAnnouncedServers, } from "../utils/mcpInstructions.js";
11
+ import { buildExecCatalogAnnouncement, collectExecCatalogState, } from "../exec/catalogAnnouncement.js";
12
+ import { existsSync } from "node:fs";
13
+ import { readFile } from "node:fs/promises";
11
14
  import * as path from "node:path";
15
+ import { EDIT_TOOL_NAME, WRITE_TOOL_NAME } from "../constants/tools.js";
12
16
  import { buildSystemPrompt, formatCompactSummary, getCompactPrompt, } from "../prompts/index.js";
13
17
  import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, wrapInSystemReminder, } from "../prompts/planModeReminders.js";
14
18
  import { recoverTruncatedJson } from "../utils/stringUtils.js";
15
- import { AGENT_TOOL_NAME, TASK_CREATE_TOOL_NAME, TASK_GET_TOOL_NAME, TASK_LIST_TOOL_NAME, TASK_UPDATE_TOOL_NAME, } from "../constants/tools.js";
16
- import { addConsolidatedAbortListener } from "../utils/abortUtils.js";
17
19
  import { logger } from "../utils/globalLogger.js";
18
20
  import { startInteractionSpan, endInteractionSpan, startLLMRequestSpan, endLLMRequestSpan, resetTracingState, } from "../telemetry/sessionTracing.js";
19
21
  import { logOTelEvent } from "../telemetry/events.js";
@@ -115,6 +117,9 @@ export class AIManager {
115
117
  get permissionManager() {
116
118
  return this.container.get("PermissionManager");
117
119
  }
120
+ get mcpManager() {
121
+ return this.container.get("McpManager");
122
+ }
118
123
  get planManager() {
119
124
  return this.container.get("PlanManager");
120
125
  }
@@ -423,6 +428,12 @@ export class AIManager {
423
428
  logger?.warn("Compaction already in progress");
424
429
  return;
425
430
  }
431
+ // 压缩绑定发起时的会话:LLM fork 期间用户可能已把 agent 原地切到另一会话
432
+ // (IDE「历史对话」restoreSession / CLI 与插件 `/clear` 都会换 sessionId,
433
+ // 且均不被 isCompacting 阻拦)。写入阶段前必须复核,否则摘要块会追加进
434
+ // 切换后会话的内存列表并持久化进其转录文件(spec message-compact「压缩
435
+ // 期间的会话保护」;PM 缺陷 3478352534577408)。
436
+ const sessionIdAtStart = this.messageManager.getSessionId();
426
437
  // 1. Run PreCompact hooks
427
438
  let hookInstructions;
428
439
  if (this.hookManager) {
@@ -443,7 +454,7 @@ export class AIManager {
443
454
  this.setIsCompacting(true);
444
455
  try {
445
456
  const modelConfig = this.getModelConfig();
446
- const recentChatMessages = convertMessagesForAPI(messagesToCompact, {
457
+ const recentChatMessages = await convertMessagesForAPI(messagesToCompact, {
447
458
  supportsVision: supportsVision(modelConfig.capabilities),
448
459
  });
449
460
  const compactPrompt = getCompactPrompt(mergedInstructions);
@@ -470,7 +481,15 @@ export class AIManager {
470
481
  const formattedSummary = formatCompactSummary(summaryContent);
471
482
  // 7. Build post-compact context restoration
472
483
  const enhancedSummary = await this.buildPostCompactContext(formattedSummary);
473
- // 8. Execute message reconstruction
484
+ // 8. Execute message reconstruction — only if the session is still the
485
+ // one this compaction started for. Abandoning here skips the entire
486
+ // write phase (summary append, post-compact context restore, plan-mode
487
+ // reminder, SessionStart/PostCompact hooks); the spent LLM call is not
488
+ // recovered and the original session simply stays uncompacted.
489
+ if (this.messageManager.getSessionId() !== sessionIdAtStart) {
490
+ logger?.warn(`Compaction abandoned: session switched from ${sessionIdAtStart} to ${this.messageManager.getSessionId()} while the summary was being generated`);
491
+ return;
492
+ }
474
493
  await this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
475
494
  // Clear readFileState after compaction (aligned with Claude Code's
476
495
  // compact flow): a file_unchanged stub would otherwise reference Read
@@ -537,7 +556,10 @@ export class AIManager {
537
556
  catch (compactError) {
538
557
  this.consecutiveCompactionFailures++;
539
558
  logger?.error(`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive): ${compactError instanceof Error ? compactError.message : String(compactError)}`);
540
- this.messageManager.addErrorBlock(`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`);
559
+ // 会话已被切走时错误块同样不得写进切换后的会话(与上面的写入放弃同口径)
560
+ if (this.messageManager.getSessionId() === sessionIdAtStart) {
561
+ this.messageManager.addErrorBlock(`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`);
562
+ }
541
563
  }
542
564
  finally {
543
565
  this.setIsCompacting(false);
@@ -565,6 +587,58 @@ export class AIManager {
565
587
  autoMemory: autoMemoryOptions,
566
588
  });
567
589
  }
590
+ /**
591
+ * Announce the connected MCP servers' own usage notes in the conversation,
592
+ * once per server. Not part of the system prompt: this content appears when a
593
+ * connection happens, and rewriting the system prompt mid-session would drop
594
+ * the whole cached prefix (see `utils/mcpInstructions.ts`).
595
+ */
596
+ maybeAnnounceMcpInstructions() {
597
+ const mcpManager = this.mcpManager;
598
+ // Optional for the same reason as `getEffectiveAdditionalDirectories` above:
599
+ // hosts and tests register partial McpManager doubles, and a missing
600
+ // usage-notes channel is not a reason to fail the turn. Bailing out matters
601
+ // more here than elsewhere — reading an absent manager as "no servers left"
602
+ // would announce every previously announced server as disconnected.
603
+ if (!mcpManager || typeof mcpManager.getServerInstructions !== "function") {
604
+ return;
605
+ }
606
+ const available = mcpManager.getServerInstructions((toolName) => this.permissionManager?.isToolDenied?.(toolName) === true);
607
+ const announced = collectAnnouncedServers(this.messageManager.getMessages());
608
+ const availableNames = new Set(available.map((server) => server.name));
609
+ const text = buildMcpInstructionsAnnouncement({
610
+ added: available.filter((server) => !announced.has(server.name)),
611
+ removed: Array.from(announced).filter((name) => !availableNames.has(name)),
612
+ });
613
+ if (!text)
614
+ return;
615
+ this.messageManager.addUserMessage({
616
+ content: wrapInSystemReminder(text),
617
+ isMeta: true,
618
+ });
619
+ }
620
+ /**
621
+ * Announce the MCP catalog in the conversation, whenever the pool renders
622
+ * differently from the last announcement. Keeping it out of `Exec`'s description
623
+ * is what makes that declaration a constant: a server connecting would otherwise
624
+ * rewrite `tools[]`, which sits in the cached prefix (see
625
+ * `exec/catalogAnnouncement.ts`).
626
+ */
627
+ maybeAnnounceExecCatalog() {
628
+ const toolManager = this.toolManager;
629
+ // Optional for the same reason as `maybeAnnounceMcpInstructions` above, and it
630
+ // matters just as much here: reading an absent channel as "the catalog is gone"
631
+ // would tell the model to stop using every tool an earlier catalog listed.
632
+ if (!toolManager || typeof toolManager.getExecCatalog !== "function")
633
+ return;
634
+ const text = buildExecCatalogAnnouncement(collectExecCatalogState(this.messageManager.getMessages()), toolManager.getExecCatalog());
635
+ if (!text)
636
+ return;
637
+ this.messageManager.addUserMessage({
638
+ content: wrapInSystemReminder(text),
639
+ isMeta: true,
640
+ });
641
+ }
568
642
  resolveFilteredTools() {
569
643
  const toolsConfig = this.getFilteredToolsConfig();
570
644
  const toolNames = new Set(toolsConfig.map((t) => t.function.name));
@@ -599,9 +673,13 @@ export class AIManager {
599
673
  forkMessages.push({ role: "user", content: prompt });
600
674
  const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
601
675
  const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
602
- // Fresh read-state map so Read/Edit state built up inside the fork never
603
- // leaks into the main session's dedup and staleness tracking.
604
- const forkReadFileState = new Map();
676
+ // Clone the parent session's read state so the fork inherits the read/dedup
677
+ // baseline (aligned with Claude Code's cloneFileStateCache): the fork can
678
+ // Edit a file the parent already read without re-reading it first. The
679
+ // clone is an independent Map with copied entries, so reads/writes inside
680
+ // the fork never leak back into the main session's dedup and staleness
681
+ // tracking.
682
+ const forkReadFileState = new Map(Array.from(this.readFileState, ([path, entry]) => [path, { ...entry }]));
605
683
  let totalUsage;
606
684
  let content;
607
685
  for (let turn = 0; turn < options.maxTurns; turn++) {
@@ -709,7 +787,7 @@ export class AIManager {
709
787
  */
710
788
  async runAutoMemoryFork(messages, prompt, options, abortSignal) {
711
789
  const modelConfig = this.getModelConfig();
712
- const historyMessages = convertMessagesForAPI(messages, {
790
+ const historyMessages = await convertMessagesForAPI(messages, {
713
791
  supportsVision: supportsVision(modelConfig.capabilities),
714
792
  });
715
793
  // Give the fork a real signal even when the caller has none, so tools
@@ -729,182 +807,6 @@ export class AIManager {
729
807
  }
730
808
  return result;
731
809
  }
732
- /**
733
- * Fork subagent ("/subtask"): spawn a background agent that inherits the
734
- * parent's full conversation context — the same system prompt, tools, model,
735
- * and message prefix as the main loop — so the forked request prefix matches
736
- * exactly and the prompt cache is reused. The fork runs to completion in the
737
- * background via the shared BackgroundTaskManager (visible in /tasks, log at
738
- * `os.tmpdir()/wave-subagent-<taskId>.log`); its final response is enqueued
739
- * as a task notification carrying the `<result>` tag, so the main agent
740
- * picks it up on the next turn and can act on it.
741
- *
742
- * Recursion is prevented by denying the Agent tool and the Task management
743
- * tools (the fork cannot spawn further subagents or mutate the shared task
744
- * list), and by honoring the parent's denied rules for everything else.
745
- * Gate-approved tools execute locally in the stripped fork context (no
746
- * permission prompts), mirroring the other fork paths.
747
- *
748
- * Returns the background task ID (empty string when no BackgroundTaskManager
749
- * is registered, in which case the fork still runs but without task
750
- * bookkeeping or notification).
751
- */
752
- async runForkSubagent(prompt, options, abortSignal) {
753
- const modelConfig = this.getModelConfig();
754
- // Snapshot the conversation synchronously: the /subtask handler records
755
- // the command echo in the transcript after this call, so the fork never
756
- // sees its own trigger message duplicated.
757
- const historyMessages = convertMessagesForAPI(this.messageManager.getMessages(), { supportsVision: supportsVision(modelConfig.capabilities) });
758
- const backgroundTaskManager = this.backgroundTaskManager;
759
- const permissionManager = this.permissionManager;
760
- const messageQueue = this.container.has("MessageQueue")
761
- ? this.container.get("MessageQueue")
762
- : undefined;
763
- // Combine the caller's signal with an internal one so a task kill
764
- // (BackgroundTaskManager.stopTask / cleanup) aborts the in-flight fork.
765
- const abortController = new AbortController();
766
- let abortCleanup;
767
- if (abortSignal) {
768
- abortCleanup = addConsolidatedAbortListener(abortSignal, [
769
- () => abortController.abort(),
770
- ]);
771
- }
772
- // Recursion prevention: deny the Agent tool and Task tools, then honor the
773
- // parent's denied rules for everything else.
774
- const deniedTools = new Set([
775
- AGENT_TOOL_NAME,
776
- TASK_CREATE_TOOL_NAME,
777
- TASK_GET_TOOL_NAME,
778
- TASK_UPDATE_TOOL_NAME,
779
- TASK_LIST_TOOL_NAME,
780
- ]);
781
- const canUseTool = (name) => {
782
- if (deniedTools.has(name))
783
- return false;
784
- if (permissionManager?.isToolDenied(name))
785
- return false;
786
- return true;
787
- };
788
- const deniedToolMessage = "The Agent tool and Task tools are not available in a fork subagent.";
789
- // Background task bookkeeping mirrors subagentManager.executeAgent.
790
- let taskId = "";
791
- let logStream;
792
- if (backgroundTaskManager) {
793
- taskId = backgroundTaskManager.generateId();
794
- const startTime = Date.now();
795
- const logPath = path.join(os.tmpdir(), `wave-subagent-${taskId}.log`);
796
- logStream = createWriteStream(logPath, { flags: "a" });
797
- backgroundTaskManager.addTask({
798
- id: taskId,
799
- type: "subagent",
800
- status: "running",
801
- startTime,
802
- description: truncateWithMarker(options.description, 1000),
803
- stdout: "",
804
- stderr: "",
805
- outputPath: logPath,
806
- onStop: () => {
807
- logStream?.destroy();
808
- logStream = undefined;
809
- abortController.abort();
810
- },
811
- });
812
- logStream.write(`[${new Date().toISOString()}] Fork subagent started: ${options.description}\n`);
813
- }
814
- const finish = async (status, content) => {
815
- if (!backgroundTaskManager || !taskId)
816
- return;
817
- const task = backgroundTaskManager.getTask(taskId);
818
- if (!task)
819
- return;
820
- const wasAlreadyKilled = task.status === "killed";
821
- const endTime = Date.now();
822
- if (status === "completed") {
823
- task.status = "completed";
824
- task.stdout = content;
825
- logStream?.write(`[${new Date().toISOString()}] Final response:\n${content}\n`);
826
- }
827
- else {
828
- task.status = "failed";
829
- task.stderr = content;
830
- logStream?.write(`[${new Date().toISOString()}] Fork subagent failed: ${content}\n`);
831
- }
832
- task.endTime = endTime;
833
- if (task.startTime)
834
- task.runtime = endTime - task.startTime;
835
- logStream?.end();
836
- logStream = undefined;
837
- // Skip the notification when the task was already stopped (e.g. by main
838
- // agent shutdown) — mirrors subagentManager's wasAlreadyKilled guard.
839
- if (!wasAlreadyKilled && messageQueue) {
840
- const description = truncateWithMarker(options.description, 1000);
841
- const summary = status === "completed"
842
- ? `Subtask "${description}" completed`
843
- : `Subtask "${description}" failed: ${content}`;
844
- messageQueue.enqueueNotification(taskNotificationToXml({
845
- type: "task_notification",
846
- taskId,
847
- taskType: "agent",
848
- status,
849
- summary,
850
- ...(status === "completed" && { result: content }),
851
- }));
852
- }
853
- };
854
- // Fire-and-forget: the fork runs to completion in the background; the
855
- // caller gets the task ID immediately. Tracked in the async work registry
856
- // so destroy() drains it before returning (abort via the task's onStop
857
- // makes the loop settle).
858
- const forkPromise = (async () => {
859
- try {
860
- const result = await this.runForkLoop(historyMessages, prompt, {
861
- // /subtask aligns with Claude Code's fork subagent
862
- // (forkSubagent.ts:65): a loose upper bound that practically
863
- // never binds.
864
- maxTurns: options.maxTurns ?? 200,
865
- canUseTool,
866
- deniedToolMessage,
867
- }, abortController.signal);
868
- if (result.usage && this.callbacks?.onUsageAdded) {
869
- this.callbacks.onUsageAdded({
870
- ...result.usage,
871
- model: modelConfig.model,
872
- operation_type: "agent",
873
- });
874
- }
875
- if (result.content?.trim()) {
876
- await finish("completed", result.content);
877
- }
878
- else {
879
- await finish("failed", "Fork subagent produced no text response");
880
- }
881
- }
882
- catch (error) {
883
- // Abort is the expected exit for a killed task; stopTask has already
884
- // marked it "killed", so no notification is sent.
885
- if (abortController.signal.aborted) {
886
- const task = backgroundTaskManager?.getTask(taskId);
887
- if (task && task.status === "running") {
888
- task.status = "killed";
889
- task.endTime = Date.now();
890
- if (task.startTime)
891
- task.runtime = task.endTime - task.startTime;
892
- }
893
- logStream?.end();
894
- logStream = undefined;
895
- }
896
- else {
897
- logger?.error("Fork subagent execution failed:", error);
898
- await finish("failed", error instanceof Error ? error.message : String(error));
899
- }
900
- }
901
- finally {
902
- abortCleanup?.();
903
- }
904
- })();
905
- this.asyncWorkRegistry?.track(forkPromise);
906
- return taskId;
907
- }
908
810
  /**
909
811
  * Parse a fork tool call's JSON arguments, recovering truncated JSON the
910
812
  * same way the main loop does. Unparseable arguments fall back to `{}` and
@@ -983,7 +885,7 @@ export class AIManager {
983
885
  const lastMessage = rawMessages[rawMessages.length - 1];
984
886
  const hasInProgressMessage = lastMessage?.role === "assistant" &&
985
887
  lastMessage.blocks.some((b) => "stage" in b && b.stage === "streaming");
986
- const forkMessages = convertMessagesForAPI(hasInProgressMessage ? rawMessages.slice(0, -1) : rawMessages, { supportsVision: supportsVision(modelConfig.capabilities) });
888
+ const forkMessages = await convertMessagesForAPI(hasInProgressMessage ? rawMessages.slice(0, -1) : rawMessages, { supportsVision: supportsVision(modelConfig.capabilities) });
987
889
  // Mirror the main loop's memory injection so the request prefix matches.
988
890
  const { prependContent } = await this.messageManager.getMemoryForInjection();
989
891
  if (prependContent.trim()) {
@@ -1112,29 +1014,32 @@ ${question}`;
1112
1014
  // 4. Invoked skills context (with token budget, matching Claude Code)
1113
1015
  const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000;
1114
1016
  const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5000;
1115
- const invokedSkillNames = this.messageManager.getInvokedSkillNames(10);
1116
- if (invokedSkillNames.length > 0 && this.skillManager) {
1017
+ const invokedSkills = this.messageManager.getInvokedSkills?.(10) ?? [];
1018
+ if (invokedSkills.length > 0 && this.skillManager) {
1117
1019
  const invokedSkillParts = [];
1118
1020
  let skillsUsedTokens = 0;
1119
- for (const skillName of invokedSkillNames) {
1021
+ for (const invoked of invokedSkills) {
1120
1022
  try {
1121
- const skill = await this.skillManager.loadSkill(skillName);
1122
- if (!skill)
1023
+ const skill = await this.skillManager.loadSkill(invoked.skillName);
1024
+ // Re-inject what the model actually saw (already substituted, with the
1025
+ // base-directory header). Re-render from the file only when that
1026
+ // record is missing — never hand the model a raw SKILL.md, whose
1027
+ // `${WAVE_SKILL_DIR}` would be a literal placeholder
1028
+ // (spec core/message-compact 场景 4).
1029
+ let skillContent = invoked.content?.trim() ??
1030
+ (skill ? this.skillManager.renderSkillContent(skill) : undefined);
1031
+ if (!skillContent)
1123
1032
  continue;
1124
- const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
1125
- let skillContent = contentMatch
1126
- ? contentMatch[1].trim()
1127
- : skill.content;
1128
1033
  const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
1129
1034
  if (skillContent.length > maxSkillChars) {
1130
- skillContent =
1131
- skillContent.slice(0, maxSkillChars) + "\n\n...[truncated]...";
1035
+ skillContent = `${skillContent.slice(0, maxSkillChars)}\n\n...[truncated]...`;
1132
1036
  }
1133
1037
  const skillTokens = estimateTokens(skillContent);
1134
1038
  if (skillsUsedTokens + skillTokens > POST_COMPACT_SKILLS_TOKEN_BUDGET)
1135
1039
  break;
1136
1040
  skillsUsedTokens += skillTokens;
1137
- invokedSkillParts.push(`\n\n## ${skill.name}\n${skill.description ? `*${skill.description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``);
1041
+ const description = skill?.description;
1042
+ invokedSkillParts.push(`\n\n## ${skill?.name ?? invoked.skillName}\n${description ? `*${description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``);
1138
1043
  }
1139
1044
  catch {
1140
1045
  // Skip skills that can't be loaded
@@ -1209,9 +1114,22 @@ ${question}`;
1209
1114
  get skillManager() {
1210
1115
  return this.container.get("SkillManager");
1211
1116
  }
1117
+ get liveConfigManager() {
1118
+ return this.container.get("LiveConfigManager");
1119
+ }
1212
1120
  async sendAIMessage(options = {}) {
1213
1121
  const { recursionDepth = 0, model, allowedRules, maxTokens } = options;
1214
1122
  let turnOffset = recursionDepth;
1123
+ // Pin the settings.json-derived configuration for the whole turn: live
1124
+ // reload can land mid-turn and the turn must not see values shift under it
1125
+ // (core/agent-config.md scenario 5). Nested turns (subagent turns inside
1126
+ // their parent's) share the outer snapshot instead of re-capturing it; the
1127
+ // release at the end of the turn mirrors this capture and lives on the
1128
+ // single exit path of this function.
1129
+ const ownsTurnSnapshot = recursionDepth === 0;
1130
+ if (ownsTurnSnapshot) {
1131
+ this.liveConfigManager?.onTurnStart();
1132
+ }
1215
1133
  // Reserve this turn synchronously before any async work. Bumping the
1216
1134
  // generation invalidates any in-flight turn's end-of-turn cleanup, and
1217
1135
  // recording myGeneration lets us skip cleanup if a newer turn (or abort)
@@ -1271,6 +1189,18 @@ ${question}`;
1271
1189
  this.abortController = abortController;
1272
1190
  toolAbortController = new AbortController();
1273
1191
  this.toolAbortController = toolAbortController;
1192
+ // Every tool call in a turn wires its own abort listener onto the
1193
+ // shared tool signal (see toolAbortController.signal in executeTool),
1194
+ // so a fan-out of >=11 parallel calls trips Node's default 10-listener
1195
+ // cap and spams MaxListenersExceededWarning. The signal dies with the
1196
+ // turn (toolAbortController is cleared in the tool loop), so there is
1197
+ // no real leak — raise the cap to silence the noise. Optional chaining
1198
+ // keeps this a no-op on Node >=26, which removed the EventTarget
1199
+ // warning mechanism (and the setMaxListeners method) entirely.
1200
+ const toolSignal = toolAbortController.signal;
1201
+ const turnSignal = abortController.signal;
1202
+ toolSignal.setMaxListeners?.(0);
1203
+ turnSignal.setMaxListeners?.(0);
1274
1204
  }
1275
1205
  else {
1276
1206
  // Reuse existing controllers
@@ -1299,14 +1229,33 @@ ${question}`;
1299
1229
  isMeta: true,
1300
1230
  });
1301
1231
  }
1232
+ // Nested memory: a read inside a subtree pulls in that subtree's
1233
+ // AGENTS.md (persisted as a meta message, aligned with Claude Code's
1234
+ // nested_memory attachment). Runs with the rules above — before the
1235
+ // message snapshot below, so the file lands in this request rather
1236
+ // than the next one. Optional call: hosts and tests register partial
1237
+ // MessageManager doubles, and absence reads as "nothing triggered".
1238
+ const nestedMemories = (await this.messageManager.collectNestedMemoryFiles?.()) ?? [];
1239
+ for (const memory of nestedMemories) {
1240
+ this.messageManager.addUserMessage({
1241
+ content: wrapInSystemReminder(`Contents of ${memory.path}:\n\n${memory.content}`),
1242
+ isMeta: true,
1243
+ });
1244
+ }
1302
1245
  // Pre-request auto-compaction: estimate the context about to be sent
1303
1246
  // and compact BEFORE issuing the request, so an over-limit request
1304
1247
  // never goes out. Skipped on fork paths (they use runForkLoop).
1305
1248
  await this.maybeAutoCompactBeforeRequest(abortController);
1249
+ // Announce MCP servers' own usage notes (persisted meta message) before
1250
+ // the snapshot below, so a server that connected mid-session explains
1251
+ // itself in the request that can already call it. After compaction, so
1252
+ // the announcement is not the content that just got summarized away.
1253
+ this.maybeAnnounceMcpInstructions();
1254
+ this.maybeAnnounceExecCatalog();
1306
1255
  // Get recent message history
1307
1256
  const rawMessages = this.messageManager.getMessages();
1308
1257
  const currentModelConfig = this.getModelConfig();
1309
- const recentMessages = convertMessagesForAPI(rawMessages, {
1258
+ const recentMessages = await convertMessagesForAPI(rawMessages, {
1310
1259
  supportsVision: supportsVision(currentModelConfig.capabilities),
1311
1260
  });
1312
1261
  // Track if assistant message has been created
@@ -1350,6 +1299,19 @@ ${question}`;
1350
1299
  isMeta: true,
1351
1300
  });
1352
1301
  }
1302
+ // External file-change reminder (aligned with Claude Code's
1303
+ // changed_files attachment): notify the agent about files it read
1304
+ // that were since changed on disk by another process — the
1305
+ // auto-memory extraction fork, an external editor, another session.
1306
+ // Persisted as a meta message like the task reminder, so it stays in
1307
+ // the message prefix the auto-memory fork mirrors for cache reuse.
1308
+ const changedFilesReminder = await getChangedFilesReminder(this.readFileState);
1309
+ if (changedFilesReminder) {
1310
+ this.messageManager.addUserMessage({
1311
+ content: wrapInSystemReminder(changedFilesReminder),
1312
+ isMeta: true,
1313
+ });
1314
+ }
1353
1315
  // Add streaming callbacks only if streaming is enabled
1354
1316
  if (this.stream) {
1355
1317
  callAgentOptions.onContentUpdate = (content) => {
@@ -1746,6 +1708,9 @@ ${question}`;
1746
1708
  if (myGeneration === this.turnGeneration) {
1747
1709
  this.setIsLoading(false);
1748
1710
  }
1711
+ if (ownsTurnSnapshot) {
1712
+ this.liveConfigManager?.onTurnEnd();
1713
+ }
1749
1714
  break outer;
1750
1715
  }
1751
1716
  }
@@ -2005,6 +1970,10 @@ ${question}`;
2005
1970
  images: toolImages,
2006
1971
  timestamp: Date.now(),
2007
1972
  });
1973
+ // Plan panel refresh (spec: 计划文件更新后刷新计划面板): a successful
1974
+ // Write/Edit of the plan file broadcasts the new content so hosts can
1975
+ // update an already-open plan panel without waiting for ExitPlanMode.
1976
+ await this.notifyPlanFileUpdate(toolName, toolArgs, toolResult.success);
2008
1977
  // Execute PostToolUse hooks after successful tool completion
2009
1978
  await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
2010
1979
  }
@@ -2023,6 +1992,39 @@ ${question}`;
2023
1992
  });
2024
1993
  }
2025
1994
  }
1995
+ /**
1996
+ * Plan panel refresh (spec: 计划文件更新后刷新计划面板).
1997
+ *
1998
+ * When a successful `Write`/`Edit` targets the current plan file, read it and
1999
+ * emit the full content so hosts can update an already-open plan panel. The
2000
+ * path comparison mirrors the plan-mode allow branch in PermissionManager so
2001
+ * "the write that was allowed" and "the write that refreshes the panel" stay
2002
+ * the same set. The path is only set while plan mode is active, so post-exit
2003
+ * writes can't reach the panels.
2004
+ */
2005
+ async notifyPlanFileUpdate(toolName, toolArgs, success) {
2006
+ if (!success)
2007
+ return;
2008
+ if (toolName !== WRITE_TOOL_NAME && toolName !== EDIT_TOOL_NAME)
2009
+ return;
2010
+ const targetPath = toolArgs.file_path;
2011
+ if (typeof targetPath !== "string" || targetPath.trim() === "")
2012
+ return;
2013
+ const planFilePath = this.container
2014
+ .get("PermissionManager")
2015
+ ?.getPlanFilePath();
2016
+ if (!planFilePath)
2017
+ return;
2018
+ if (path.resolve(targetPath) !== path.resolve(planFilePath))
2019
+ return;
2020
+ try {
2021
+ const content = await readFile(planFilePath, "utf8");
2022
+ this.callbacks?.onPlanFileUpdated?.(content);
2023
+ }
2024
+ catch (error) {
2025
+ logger?.warn(`Failed to read plan file for panel refresh: ${planFilePath}`, error);
2026
+ }
2027
+ }
2026
2028
  /**
2027
2029
  * Execute PreToolUse hooks before tool execution
2028
2030
  * Returns true if hooks allow tool execution, false if blocked
@@ -3,6 +3,7 @@ import * as os from "os";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { stripAnsiColors } from "../utils/stringUtils.js";
6
+ import { readTailTextSync } from "../utils/fileUtils.js";
6
7
  import { WindowsStreamDecoder } from "../utils/encoding.js";
7
8
  import { logger } from "../utils/globalLogger.js";
8
9
  import { resolveShellPath } from "../utils/shellResolver.js";
@@ -386,6 +387,19 @@ export class BackgroundTaskManager {
386
387
  }
387
388
  let stdout = task.stdout;
388
389
  let stderr = task.stderr;
390
+ // A running agent task keeps its tool-call log in `outputPath` while
391
+ // `stdout` stays empty until the terminal snapshot fills it (see
392
+ // SubagentManager) — so /tasks shows an empty output block for the whole
393
+ // run. Fall back to the log file tail to surface live progress.
394
+ //
395
+ // Only when the in-memory buffer is empty, and only while running: shell
396
+ // tasks accumulate stdout chunk by chunk (that buffer is authoritative and
397
+ // identical to the file), so reading the file on top of it would duplicate
398
+ // the text; once a task reaches a terminal state the snapshot already
399
+ // carries the final output.
400
+ if (!stdout && task.status === "running" && task.outputPath) {
401
+ stdout = stripAnsiColors(readTailTextSync(task.outputPath));
402
+ }
389
403
  // Apply regex filter if provided
390
404
  if (filter) {
391
405
  try {
@@ -0,0 +1,33 @@
1
+ import { Container } from "../utils/container.js";
2
+ export interface BashModeManagerOptions {
3
+ workdir: string;
4
+ }
5
+ export interface CommandExecutionResult {
6
+ exitCode: number;
7
+ output: string;
8
+ }
9
+ /**
10
+ * Bash mode (`!command`) execution. The command is surfaced as a user-message
11
+ * tool block (name: BASH_TOOL_NAME) — the same rendering as fork-skill / `/plan`
12
+ * outputs (⎿ prefix + markdown, result enters the model context via the
13
+ * generic `<local-command-stdout>` rule) — and executed through the shared
14
+ * bash tool engine (shell resolution, CWD tracking, timeout, output handling).
15
+ */
16
+ export declare class BashModeManager {
17
+ private container;
18
+ private workdir;
19
+ isCommandRunning: boolean;
20
+ private abortController;
21
+ onCommandRunningChange?: (running: boolean) => void;
22
+ constructor(container: Container, options: BashModeManagerOptions);
23
+ private get messageManager();
24
+ /**
25
+ * Merged env (OS env overlaid with this session's settings snapshot) for
26
+ * bash-mode subprocesses, so settings `env` vars reach them without
27
+ * polluting other sessions in one `wave --stdio` process.
28
+ */
29
+ private get sessionEnv();
30
+ private setCommandRunning;
31
+ executeCommand(command: string): Promise<number>;
32
+ abortCommand(): void;
33
+ }