wave-agent-sdk 1.2.0 → 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 (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -6,8 +6,13 @@ 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 { getChangedFilesReminder } from "../utils/fileChangeReminder.js";
10
+ import { buildMcpInstructionsAnnouncement, collectAnnouncedServers, } from "../utils/mcpInstructions.js";
11
+ import { buildExecCatalogAnnouncement, collectExecCatalogState, } from "../exec/catalogAnnouncement.js";
9
12
  import { existsSync } from "node:fs";
13
+ import { readFile } from "node:fs/promises";
10
14
  import * as path from "node:path";
15
+ import { EDIT_TOOL_NAME, WRITE_TOOL_NAME } from "../constants/tools.js";
11
16
  import { buildSystemPrompt, formatCompactSummary, getCompactPrompt, } from "../prompts/index.js";
12
17
  import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, wrapInSystemReminder, } from "../prompts/planModeReminders.js";
13
18
  import { recoverTruncatedJson } from "../utils/stringUtils.js";
@@ -112,6 +117,9 @@ export class AIManager {
112
117
  get permissionManager() {
113
118
  return this.container.get("PermissionManager");
114
119
  }
120
+ get mcpManager() {
121
+ return this.container.get("McpManager");
122
+ }
115
123
  get planManager() {
116
124
  return this.container.get("PlanManager");
117
125
  }
@@ -420,6 +428,12 @@ export class AIManager {
420
428
  logger?.warn("Compaction already in progress");
421
429
  return;
422
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();
423
437
  // 1. Run PreCompact hooks
424
438
  let hookInstructions;
425
439
  if (this.hookManager) {
@@ -440,7 +454,7 @@ export class AIManager {
440
454
  this.setIsCompacting(true);
441
455
  try {
442
456
  const modelConfig = this.getModelConfig();
443
- const recentChatMessages = convertMessagesForAPI(messagesToCompact, {
457
+ const recentChatMessages = await convertMessagesForAPI(messagesToCompact, {
444
458
  supportsVision: supportsVision(modelConfig.capabilities),
445
459
  });
446
460
  const compactPrompt = getCompactPrompt(mergedInstructions);
@@ -467,7 +481,15 @@ export class AIManager {
467
481
  const formattedSummary = formatCompactSummary(summaryContent);
468
482
  // 7. Build post-compact context restoration
469
483
  const enhancedSummary = await this.buildPostCompactContext(formattedSummary);
470
- // 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
+ }
471
493
  await this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
472
494
  // Clear readFileState after compaction (aligned with Claude Code's
473
495
  // compact flow): a file_unchanged stub would otherwise reference Read
@@ -534,7 +556,10 @@ export class AIManager {
534
556
  catch (compactError) {
535
557
  this.consecutiveCompactionFailures++;
536
558
  logger?.error(`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive): ${compactError instanceof Error ? compactError.message : String(compactError)}`);
537
- 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
+ }
538
563
  }
539
564
  finally {
540
565
  this.setIsCompacting(false);
@@ -562,6 +587,58 @@ export class AIManager {
562
587
  autoMemory: autoMemoryOptions,
563
588
  });
564
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
+ }
565
642
  resolveFilteredTools() {
566
643
  const toolsConfig = this.getFilteredToolsConfig();
567
644
  const toolNames = new Set(toolsConfig.map((t) => t.function.name));
@@ -596,9 +673,13 @@ export class AIManager {
596
673
  forkMessages.push({ role: "user", content: prompt });
597
674
  const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
598
675
  const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
599
- // Fresh read-state map so Read/Edit state built up inside the fork never
600
- // leaks into the main session's dedup and staleness tracking.
601
- 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 }]));
602
683
  let totalUsage;
603
684
  let content;
604
685
  for (let turn = 0; turn < options.maxTurns; turn++) {
@@ -706,7 +787,7 @@ export class AIManager {
706
787
  */
707
788
  async runAutoMemoryFork(messages, prompt, options, abortSignal) {
708
789
  const modelConfig = this.getModelConfig();
709
- const historyMessages = convertMessagesForAPI(messages, {
790
+ const historyMessages = await convertMessagesForAPI(messages, {
710
791
  supportsVision: supportsVision(modelConfig.capabilities),
711
792
  });
712
793
  // Give the fork a real signal even when the caller has none, so tools
@@ -804,7 +885,7 @@ export class AIManager {
804
885
  const lastMessage = rawMessages[rawMessages.length - 1];
805
886
  const hasInProgressMessage = lastMessage?.role === "assistant" &&
806
887
  lastMessage.blocks.some((b) => "stage" in b && b.stage === "streaming");
807
- 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) });
808
889
  // Mirror the main loop's memory injection so the request prefix matches.
809
890
  const { prependContent } = await this.messageManager.getMemoryForInjection();
810
891
  if (prependContent.trim()) {
@@ -933,29 +1014,32 @@ ${question}`;
933
1014
  // 4. Invoked skills context (with token budget, matching Claude Code)
934
1015
  const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000;
935
1016
  const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5000;
936
- const invokedSkillNames = this.messageManager.getInvokedSkillNames(10);
937
- if (invokedSkillNames.length > 0 && this.skillManager) {
1017
+ const invokedSkills = this.messageManager.getInvokedSkills?.(10) ?? [];
1018
+ if (invokedSkills.length > 0 && this.skillManager) {
938
1019
  const invokedSkillParts = [];
939
1020
  let skillsUsedTokens = 0;
940
- for (const skillName of invokedSkillNames) {
1021
+ for (const invoked of invokedSkills) {
941
1022
  try {
942
- const skill = await this.skillManager.loadSkill(skillName);
943
- 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)
944
1032
  continue;
945
- const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
946
- let skillContent = contentMatch
947
- ? contentMatch[1].trim()
948
- : skill.content;
949
1033
  const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
950
1034
  if (skillContent.length > maxSkillChars) {
951
- skillContent =
952
- skillContent.slice(0, maxSkillChars) + "\n\n...[truncated]...";
1035
+ skillContent = `${skillContent.slice(0, maxSkillChars)}\n\n...[truncated]...`;
953
1036
  }
954
1037
  const skillTokens = estimateTokens(skillContent);
955
1038
  if (skillsUsedTokens + skillTokens > POST_COMPACT_SKILLS_TOKEN_BUDGET)
956
1039
  break;
957
1040
  skillsUsedTokens += skillTokens;
958
- 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\`\`\``);
959
1043
  }
960
1044
  catch {
961
1045
  // Skip skills that can't be loaded
@@ -1030,9 +1114,22 @@ ${question}`;
1030
1114
  get skillManager() {
1031
1115
  return this.container.get("SkillManager");
1032
1116
  }
1117
+ get liveConfigManager() {
1118
+ return this.container.get("LiveConfigManager");
1119
+ }
1033
1120
  async sendAIMessage(options = {}) {
1034
1121
  const { recursionDepth = 0, model, allowedRules, maxTokens } = options;
1035
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
+ }
1036
1133
  // Reserve this turn synchronously before any async work. Bumping the
1037
1134
  // generation invalidates any in-flight turn's end-of-turn cleanup, and
1038
1135
  // recording myGeneration lets us skip cleanup if a newer turn (or abort)
@@ -1132,14 +1229,33 @@ ${question}`;
1132
1229
  isMeta: true,
1133
1230
  });
1134
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
+ }
1135
1245
  // Pre-request auto-compaction: estimate the context about to be sent
1136
1246
  // and compact BEFORE issuing the request, so an over-limit request
1137
1247
  // never goes out. Skipped on fork paths (they use runForkLoop).
1138
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();
1139
1255
  // Get recent message history
1140
1256
  const rawMessages = this.messageManager.getMessages();
1141
1257
  const currentModelConfig = this.getModelConfig();
1142
- const recentMessages = convertMessagesForAPI(rawMessages, {
1258
+ const recentMessages = await convertMessagesForAPI(rawMessages, {
1143
1259
  supportsVision: supportsVision(currentModelConfig.capabilities),
1144
1260
  });
1145
1261
  // Track if assistant message has been created
@@ -1183,6 +1299,19 @@ ${question}`;
1183
1299
  isMeta: true,
1184
1300
  });
1185
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
+ }
1186
1315
  // Add streaming callbacks only if streaming is enabled
1187
1316
  if (this.stream) {
1188
1317
  callAgentOptions.onContentUpdate = (content) => {
@@ -1579,6 +1708,9 @@ ${question}`;
1579
1708
  if (myGeneration === this.turnGeneration) {
1580
1709
  this.setIsLoading(false);
1581
1710
  }
1711
+ if (ownsTurnSnapshot) {
1712
+ this.liveConfigManager?.onTurnEnd();
1713
+ }
1582
1714
  break outer;
1583
1715
  }
1584
1716
  }
@@ -1838,6 +1970,10 @@ ${question}`;
1838
1970
  images: toolImages,
1839
1971
  timestamp: Date.now(),
1840
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);
1841
1977
  // Execute PostToolUse hooks after successful tool completion
1842
1978
  await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
1843
1979
  }
@@ -1856,6 +1992,39 @@ ${question}`;
1856
1992
  });
1857
1993
  }
1858
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
+ }
1859
2028
  /**
1860
2029
  * Execute PreToolUse hooks before tool execution
1861
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 {
@@ -13,6 +13,12 @@ export declare class HookManager {
13
13
  private container;
14
14
  private configuration;
15
15
  private programmaticHooks;
16
+ /**
17
+ * Plugin hooks grouped by plugin root path. A map rather than one merged
18
+ * object because mergeHooksConfiguration only appends — a flat object can
19
+ * never be reversed, so the in-place plugin reload path could not drop
20
+ * exactly one plugin's hooks (and re-registering would double them).
21
+ */
16
22
  private pluginHooks;
17
23
  private waveConfigHooks;
18
24
  private readonly matcher;
@@ -120,6 +126,13 @@ export declare class HookManager {
120
126
  * Register hooks provided by a plugin
121
127
  */
122
128
  registerPluginHooks(pluginRoot: string, hooks: PartialHookConfiguration): void;
129
+ /**
130
+ * Drop every hook contributed by a plugin, addressed by its root path.
131
+ * Used by the in-place plugin reload path (before re-registering) and when a
132
+ * plugin is disabled or uninstalled.
133
+ * @returns true if this root had hooks registered
134
+ */
135
+ unregisterPluginHooks(pluginRoot: string): boolean;
123
136
  /**
124
137
  * Get hooks registered by plugins (read-only view for the settings UI).
125
138
  * Plugin hooks carry pluginRoot on each command and are read-only in the UI.
@@ -13,7 +13,13 @@ export class HookManager {
13
13
  constructor(container, workdir, matcher = new HookMatcher()) {
14
14
  this.container = container;
15
15
  this.programmaticHooks = {};
16
- this.pluginHooks = {};
16
+ /**
17
+ * Plugin hooks grouped by plugin root path. A map rather than one merged
18
+ * object because mergeHooksConfiguration only appends — a flat object can
19
+ * never be reversed, so the in-place plugin reload path could not drop
20
+ * exactly one plugin's hooks (and re-registering would double them).
21
+ */
22
+ this.pluginHooks = new Map();
17
23
  this.waveConfigHooks = {};
18
24
  this.workdir = workdir;
19
25
  this.matcher = matcher;
@@ -78,7 +84,9 @@ export class HookManager {
78
84
  rebuildConfiguration() {
79
85
  const rebuilt = {};
80
86
  this.mergeHooksConfiguration(rebuilt, this.programmaticHooks);
81
- this.mergeHooksConfiguration(rebuilt, this.pluginHooks);
87
+ for (const hooks of this.pluginHooks.values()) {
88
+ this.mergeHooksConfiguration(rebuilt, hooks);
89
+ }
82
90
  this.mergeHooksConfiguration(rebuilt, this.waveConfigHooks);
83
91
  this.configuration = Object.keys(rebuilt).length > 0 ? rebuilt : undefined;
84
92
  }
@@ -667,15 +675,34 @@ export class HookManager {
667
675
  hooks: config.hooks.map((cmd) => ({ ...cmd, pluginRoot })),
668
676
  }));
669
677
  }
670
- this.mergeHooksConfiguration(this.pluginHooks, stampedHooks);
678
+ // Replace (not append) the previous registration for this root, so
679
+ // re-registering the same plugin never doubles its hooks.
680
+ this.pluginHooks.set(pluginRoot, stampedHooks);
671
681
  this.rebuildConfiguration();
672
682
  }
683
+ /**
684
+ * Drop every hook contributed by a plugin, addressed by its root path.
685
+ * Used by the in-place plugin reload path (before re-registering) and when a
686
+ * plugin is disabled or uninstalled.
687
+ * @returns true if this root had hooks registered
688
+ */
689
+ unregisterPluginHooks(pluginRoot) {
690
+ const removed = this.pluginHooks.delete(pluginRoot);
691
+ if (removed) {
692
+ this.rebuildConfiguration();
693
+ }
694
+ return removed;
695
+ }
673
696
  /**
674
697
  * Get hooks registered by plugins (read-only view for the settings UI).
675
698
  * Plugin hooks carry pluginRoot on each command and are read-only in the UI.
676
699
  */
677
700
  getPluginHooks() {
678
- return this.pluginHooks;
701
+ const merged = {};
702
+ for (const hooks of this.pluginHooks.values()) {
703
+ this.mergeHooksConfiguration(merged, hooks);
704
+ }
705
+ return merged;
679
706
  }
680
707
  /**
681
708
  * Execute SessionStart hooks during initialization.
@@ -6,6 +6,7 @@
6
6
  * - Configuration file watching for settings.json files
7
7
  * - Coordination between file watchers and configuration updates
8
8
  */
9
+ import type { TurnConfigurationSnapshot } from "../services/configurationService.js";
9
10
  import { Container } from "../utils/container.js";
10
11
  import type { WaveConfiguration } from "../types/configuration.js";
11
12
  export interface LiveConfigManagerOptions {
@@ -19,6 +20,8 @@ export declare class LiveConfigManager {
19
20
  private isInitialized;
20
21
  private currentConfiguration;
21
22
  private lastValidConfiguration;
23
+ private turnSnapshot;
24
+ private turnDepth;
22
25
  private fileWatcher;
23
26
  private userConfigPaths?;
24
27
  private projectConfigPaths?;
@@ -28,6 +31,15 @@ export declare class LiveConfigManager {
28
31
  private get hookManager();
29
32
  private get permissionManager();
30
33
  private get configurationService();
34
+ private get memoryService();
35
+ /**
36
+ * Keep the auto-memory system safe zone in sync with the live auto-memory
37
+ * toggle (core/agent-config.md scenario 6). Turning auto-memory off must
38
+ * revoke the privilege its directory got at construction time, and turning it
39
+ * back on must restore it — both without rebuilding the session. Idempotent:
40
+ * the underlying add/remove are dedup'ed by path.
41
+ */
42
+ private syncAutoMemorySafeZone;
31
43
  /**
32
44
  * Initialize configuration watching
33
45
  * Maps to FR-004: System MUST watch settings.json files
@@ -46,6 +58,19 @@ export declare class LiveConfigManager {
46
58
  * Shutdown configuration management and cleanup resources
47
59
  */
48
60
  shutdown(): Promise<void>;
61
+ /**
62
+ * Turn boundary: pin the settings.json-derived configuration for the duration
63
+ * of a turn (called by AIManager). A live reload may land mid-turn and the
64
+ * turn must not see it (core/agent-config.md scenario 5) — the change is
65
+ * picked up by the snapshot taken when the next turn starts. Permission rules,
66
+ * hooks and env stay live: those are enforcement state and follow the file
67
+ * immediately.
68
+ */
69
+ onTurnStart(): void;
70
+ /** Turn boundary: drop the snapshot so the next turn reads live values. */
71
+ onTurnEnd(): void;
72
+ /** Configuration snapshot of the running turn, or null outside a turn. */
73
+ getTurnSnapshot(): TurnConfigurationSnapshot | null;
49
74
  /**
50
75
  * Reload configuration from files
51
76
  * Maps to FR-008: Continue with previous valid configuration on errors
@@ -77,6 +102,14 @@ export declare class LiveConfigManager {
77
102
  private setupFileWatcherEvents;
78
103
  private handleFileChange;
79
104
  private detectChanges;
105
+ /**
106
+ * Chokidar only starts watching a non-existent path when its parent directory
107
+ * exists at watch time (a path whose parents are all missing is never
108
+ * picked up, not even after they appear). Walk up to the deepest node whose
109
+ * parent exists and watch that instead — events for its descendants still
110
+ * reach this path's callback (FileWatcherService matches by path prefix).
111
+ */
112
+ private resolveWatchTarget;
80
113
  /**
81
114
  * Get configuration file paths for user and project settings
82
115
  * Returns paths in priority order (local.json first, then .json)