wave-agent-sdk 1.0.5 → 1.0.7

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 (202) hide show
  1. package/dist/agent.d.ts +34 -1
  2. package/dist/agent.js +49 -4
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +1 -1
  5. package/dist/managers/aiManager.d.ts +36 -0
  6. package/dist/managers/aiManager.js +240 -11
  7. package/dist/managers/messageManager.d.ts +2 -4
  8. package/dist/managers/messageManager.js +14 -7
  9. package/dist/managers/slashCommandManager.js +28 -0
  10. package/dist/managers/subagentManager.d.ts +0 -2
  11. package/dist/services/aiService.d.ts +2 -3
  12. package/dist/services/aiService.js +4 -18
  13. package/dist/services/configurationService.d.ts +7 -0
  14. package/dist/services/configurationService.js +10 -0
  15. package/dist/telemetry/instrumentation.d.ts +1 -1
  16. package/dist/telemetry/instrumentation.js +57 -28
  17. package/dist/tools/bashTool.js +16 -6
  18. package/dist/tools/grepTool.js +11 -3
  19. package/dist/types/core.d.ts +1 -34
  20. package/dist/types/index.d.ts +4 -0
  21. package/dist/types/messaging.d.ts +2 -0
  22. package/dist/utils/cacheControlUtils.d.ts +5 -27
  23. package/dist/utils/cacheControlUtils.js +12 -73
  24. package/dist/utils/containerSetup.js +0 -3
  25. package/dist/utils/messageOperations.d.ts +2 -1
  26. package/dist/utils/messageOperations.js +2 -1
  27. package/dist/utils/notificationXml.js +5 -0
  28. package/dist/utils/ripgrep.d.ts +4 -4
  29. package/dist/utils/ripgrep.js +4 -27
  30. package/package.json +20 -8
  31. package/bin/rg +0 -79
  32. package/dist/managers/forkedAgentManager.d.ts +0 -50
  33. package/dist/managers/forkedAgentManager.js +0 -130
  34. package/scripts/install_ripgrep.js +0 -111
  35. package/scripts/postinstall.js +0 -38
  36. package/src/agent.ts +0 -1271
  37. package/src/constants/subagents.ts +0 -4
  38. package/src/constants/toolLimits.ts +0 -15
  39. package/src/constants/tools.ts +0 -24
  40. package/src/core/plugin.ts +0 -237
  41. package/src/core/session.ts +0 -9
  42. package/src/index.ts +0 -38
  43. package/src/managers/MemoryRuleManager.ts +0 -198
  44. package/src/managers/aiManager.ts +0 -2429
  45. package/src/managers/backgroundTaskManager.ts +0 -480
  46. package/src/managers/bangManager.ts +0 -119
  47. package/src/managers/cronManager.ts +0 -401
  48. package/src/managers/foregroundTaskManager.ts +0 -30
  49. package/src/managers/forkedAgentManager.ts +0 -216
  50. package/src/managers/hookManager.ts +0 -1089
  51. package/src/managers/liveConfigManager.ts +0 -445
  52. package/src/managers/lspManager.ts +0 -470
  53. package/src/managers/mcpManager.ts +0 -1010
  54. package/src/managers/messageManager.ts +0 -1184
  55. package/src/managers/messageQueue.ts +0 -157
  56. package/src/managers/permissionManager.ts +0 -1140
  57. package/src/managers/planManager.ts +0 -112
  58. package/src/managers/pluginManager.ts +0 -330
  59. package/src/managers/pluginScopeManager.ts +0 -124
  60. package/src/managers/reversionManager.ts +0 -178
  61. package/src/managers/skillManager.ts +0 -578
  62. package/src/managers/slashCommandManager.ts +0 -564
  63. package/src/managers/subagentManager.ts +0 -915
  64. package/src/managers/toolManager.ts +0 -489
  65. package/src/managers/workflowManager.ts +0 -493
  66. package/src/prompts/autoMemory.ts +0 -33
  67. package/src/prompts/autoMemoryExtraction.ts +0 -146
  68. package/src/prompts/index.ts +0 -506
  69. package/src/prompts/planModeReminders.ts +0 -126
  70. package/src/services/GitService.ts +0 -131
  71. package/src/services/MarketplaceService.ts +0 -1052
  72. package/src/services/MemoryRuleService.ts +0 -71
  73. package/src/services/aiService.ts +0 -919
  74. package/src/services/authService.ts +0 -565
  75. package/src/services/autoMemoryService.ts +0 -280
  76. package/src/services/configurationService.ts +0 -1520
  77. package/src/services/fileWatcher.ts +0 -319
  78. package/src/services/hook.ts +0 -348
  79. package/src/services/initializationService.ts +0 -357
  80. package/src/services/interactionService.ts +0 -229
  81. package/src/services/jsonlHandler.ts +0 -295
  82. package/src/services/memory.ts +0 -234
  83. package/src/services/pluginLoader.ts +0 -252
  84. package/src/services/remoteSettingsService.ts +0 -366
  85. package/src/services/reversionService.ts +0 -122
  86. package/src/services/session.ts +0 -917
  87. package/src/services/taskManager.ts +0 -321
  88. package/src/telemetry/events.ts +0 -65
  89. package/src/telemetry/instrumentation.ts +0 -499
  90. package/src/telemetry/sessionTracing.ts +0 -348
  91. package/src/tools/agentTool.ts +0 -322
  92. package/src/tools/askUserQuestion.ts +0 -160
  93. package/src/tools/bashTool.ts +0 -621
  94. package/src/tools/buildTool.ts +0 -61
  95. package/src/tools/cronCreateTool.ts +0 -161
  96. package/src/tools/cronDeleteTool.ts +0 -51
  97. package/src/tools/cronListTool.ts +0 -47
  98. package/src/tools/editTool.ts +0 -313
  99. package/src/tools/enterPlanMode.ts +0 -124
  100. package/src/tools/enterWorktreeTool.ts +0 -190
  101. package/src/tools/exitPlanMode.ts +0 -139
  102. package/src/tools/exitWorktreeTool.ts +0 -236
  103. package/src/tools/globTool.ts +0 -167
  104. package/src/tools/grepTool.ts +0 -399
  105. package/src/tools/lspTool.ts +0 -883
  106. package/src/tools/readTool.ts +0 -426
  107. package/src/tools/skillTool.ts +0 -248
  108. package/src/tools/taskManagementTools.ts +0 -574
  109. package/src/tools/taskStopTool.ts +0 -78
  110. package/src/tools/types.ts +0 -137
  111. package/src/tools/webFetchTool.ts +0 -373
  112. package/src/tools/workflowTool.ts +0 -205
  113. package/src/tools/writeTool.ts +0 -267
  114. package/src/types/agent.ts +0 -128
  115. package/src/types/auth.ts +0 -19
  116. package/src/types/commands.ts +0 -30
  117. package/src/types/config.ts +0 -43
  118. package/src/types/configuration.ts +0 -168
  119. package/src/types/core.ts +0 -105
  120. package/src/types/cron.ts +0 -13
  121. package/src/types/environment.ts +0 -106
  122. package/src/types/fileSearch.ts +0 -4
  123. package/src/types/history.ts +0 -7
  124. package/src/types/hooks.ts +0 -256
  125. package/src/types/index.ts +0 -41
  126. package/src/types/lsp.ts +0 -98
  127. package/src/types/marketplace.ts +0 -71
  128. package/src/types/mcp.ts +0 -43
  129. package/src/types/memoryRule.ts +0 -31
  130. package/src/types/messaging.ts +0 -114
  131. package/src/types/permissions.ts +0 -80
  132. package/src/types/plugins.ts +0 -39
  133. package/src/types/processes.ts +0 -99
  134. package/src/types/reversion.ts +0 -29
  135. package/src/types/session.ts +0 -12
  136. package/src/types/skills.ts +0 -108
  137. package/src/types/tasks.ts +0 -13
  138. package/src/types/telemetry.ts +0 -98
  139. package/src/types/tools.ts +0 -44
  140. package/src/types/workflow.ts +0 -6
  141. package/src/utils/abortUtils.ts +0 -118
  142. package/src/utils/atomicWrite.ts +0 -61
  143. package/src/utils/bashParser.ts +0 -899
  144. package/src/utils/cacheControlUtils.ts +0 -426
  145. package/src/utils/commandArgumentParser.ts +0 -161
  146. package/src/utils/commandPathResolver.ts +0 -88
  147. package/src/utils/configPaths.ts +0 -220
  148. package/src/utils/configValidator.ts +0 -100
  149. package/src/utils/constants.ts +0 -39
  150. package/src/utils/container.ts +0 -92
  151. package/src/utils/containerSetup.ts +0 -371
  152. package/src/utils/convertMessagesForAPI.ts +0 -326
  153. package/src/utils/cronTasks.ts +0 -128
  154. package/src/utils/cronTasksLock.ts +0 -182
  155. package/src/utils/cronToHuman.ts +0 -99
  156. package/src/utils/customCommands.ts +0 -95
  157. package/src/utils/editUtils.ts +0 -20
  158. package/src/utils/fileFormat.ts +0 -40
  159. package/src/utils/fileSearch.ts +0 -151
  160. package/src/utils/fileUtils.ts +0 -223
  161. package/src/utils/gitUtils.ts +0 -315
  162. package/src/utils/globalLogger.ts +0 -128
  163. package/src/utils/groupMessagesByApiRound.ts +0 -121
  164. package/src/utils/hookMatcher.ts +0 -176
  165. package/src/utils/markdownParser.ts +0 -317
  166. package/src/utils/mcpUtils.ts +0 -138
  167. package/src/utils/messageOperations.ts +0 -659
  168. package/src/utils/modelCapabilities.ts +0 -30
  169. package/src/utils/nameGenerator.ts +0 -99
  170. package/src/utils/notificationXml.ts +0 -57
  171. package/src/utils/openaiClient.ts +0 -251
  172. package/src/utils/parseCronExpression.ts +0 -78
  173. package/src/utils/path.ts +0 -72
  174. package/src/utils/pathEncoder.ts +0 -400
  175. package/src/utils/pathSafety.ts +0 -40
  176. package/src/utils/promptHistory.ts +0 -168
  177. package/src/utils/ripgrep.ts +0 -35
  178. package/src/utils/shellResolver.ts +0 -190
  179. package/src/utils/skillParser.ts +0 -246
  180. package/src/utils/stringUtils.ts +0 -156
  181. package/src/utils/subagentParser.ts +0 -279
  182. package/src/utils/taskReminder.ts +0 -96
  183. package/src/utils/tokenCalculation.ts +0 -43
  184. package/src/utils/tokenEstimate.ts +0 -34
  185. package/src/utils/toolResultStorage.ts +0 -117
  186. package/src/utils/worktreeSession.ts +0 -26
  187. package/src/utils/worktreeUtils.ts +0 -786
  188. package/src/workflow/budgetTracker.ts +0 -34
  189. package/src/workflow/concurrencyLimiter.ts +0 -47
  190. package/src/workflow/journal.ts +0 -95
  191. package/src/workflow/progressReporter.ts +0 -141
  192. package/src/workflow/runState.ts +0 -65
  193. package/src/workflow/scriptRuntime.ts +0 -274
  194. package/src/workflow/structuredOutput.ts +0 -123
  195. package/src/workflow/types.ts +0 -101
  196. package/src/workflow/workflowApis.ts +0 -410
  197. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  198. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  199. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  200. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  201. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  202. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
package/dist/agent.d.ts CHANGED
@@ -14,7 +14,6 @@ export declare class Agent {
14
14
  private permissionManager;
15
15
  private planManager;
16
16
  private subagentManager;
17
- private forkedAgentManager;
18
17
  private slashCommandManager;
19
18
  private pluginManager;
20
19
  private skillManager;
@@ -24,6 +23,7 @@ export declare class Agent {
24
23
  private messageQueue;
25
24
  private dispatchPromise;
26
25
  private isAborting;
26
+ private dispatchAborted;
27
27
  private memoryRuleManager;
28
28
  private liveConfigManager;
29
29
  private taskManager;
@@ -269,6 +269,28 @@ export declare class Agent {
269
269
  * @returns Promise that resolves to the AI's answer
270
270
  */
271
271
  askBtw(question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void, onReasoning?: (content: string) => void): Promise<string>;
272
+ /**
273
+ * Start a fork subagent in the background (the "/subtask" command path).
274
+ *
275
+ * The fork inherits the parent's full conversation context — same system
276
+ * prompt, tools, model, and message prefix — so the prompt cache is reused,
277
+ * then works independently in the background. On completion its final
278
+ * response is delivered back to the main conversation as a task notification
279
+ * carrying the `<result>` tag. Recursion is prevented inside the fork (the
280
+ * Agent tool and Task tools are denied).
281
+ *
282
+ * @param prompt - The task description for the fork subagent
283
+ * @param options - Background task label and optional loose turn bound
284
+ * @param options.description - Display label for the background task
285
+ * @param options.maxTurns - Loose turn bound (defaults to 200, aligned with
286
+ * Claude Code's fork subagent)
287
+ * @param abortSignal - Optional signal to abort the fork
288
+ * @returns Promise that resolves to the background task ID
289
+ */
290
+ forkSubagent(prompt: string, options: {
291
+ description: string;
292
+ maxTurns?: number;
293
+ }, abortSignal?: AbortSignal): Promise<string>;
272
294
  /**
273
295
  * Send a message to the AI agent with optional images
274
296
  *
@@ -391,6 +413,17 @@ export declare class Agent {
391
413
  * @returns The subagent instance or null if not found
392
414
  */
393
415
  getSubagentInstance(subagentId: string): import("./managers/subagentManager.js").SubagentInstance | null;
416
+ /**
417
+ * Get all subagent configurations visible in this session (built-in,
418
+ * user, project, and plugin agents).
419
+ * @returns The list of subagent configurations
420
+ */
421
+ getSubagentConfigurations(): import("./utils/subagentParser.js").SubagentConfiguration[];
422
+ /**
423
+ * Get currently active subagent instances (status active/initializing).
424
+ * @returns The list of active subagent instances
425
+ */
426
+ getActiveSubagentInstances(): import("./managers/subagentManager.js").SubagentInstance[];
394
427
  /**
395
428
  * Get the current task list ID
396
429
  */
package/dist/agent.js CHANGED
@@ -54,6 +54,7 @@ export class Agent {
54
54
  this.bangManager = null;
55
55
  this.dispatchPromise = null; // Track current dispatch for teardown
56
56
  this.isAborting = false; // Guard: prevents tryDispatch from firing during abortMessage
57
+ this.dispatchAborted = false; // Set on abort while a dispatch is running: suppress the .finally re-check so preserved notifications don't get dispatched after abort
57
58
  this.sessionStartTime = Date.now();
58
59
  const { logger, workdir, systemPrompt, stream = true } = options;
59
60
  // Set working directory early as we need it for loading configuration
@@ -106,7 +107,6 @@ export class Agent {
106
107
  this.toolManager = this.container.get("ToolManager");
107
108
  this.liveConfigManager = this.container.get("LiveConfigManager");
108
109
  this.subagentManager = this.container.get("SubagentManager");
109
- this.forkedAgentManager = this.container.get("ForkedAgentManager");
110
110
  this.aiManager = this.container.get("AIManager");
111
111
  this.slashCommandManager = this.container.get("SlashCommandManager");
112
112
  this.pluginManager = this.container.get("PluginManager");
@@ -265,6 +265,8 @@ export class Agent {
265
265
  tryDispatch() {
266
266
  if (this.isAborting)
267
267
  return; // Suppress dispatch during abort to prevent queued notifications from being dispatched as a side-effect
268
+ if (this.dispatchAborted)
269
+ return; // Aborted mid-dispatch: don't start a new dispatch (the user interrupted this turn)
268
270
  if (this.messageQueue.state !== "idle")
269
271
  return;
270
272
  if (!this.messageQueue.hasPending())
@@ -279,7 +281,10 @@ export class Agent {
279
281
  .finally(() => {
280
282
  this.messageQueue.transitionTo("idle");
281
283
  this.dispatchPromise = null;
282
- this.tryDispatch(); // Re-check after processing
284
+ if (!this.dispatchAborted) {
285
+ this.tryDispatch(); // Re-check after processing
286
+ }
287
+ this.dispatchAborted = false;
283
288
  });
284
289
  }
285
290
  /**
@@ -571,6 +576,12 @@ export class Agent {
571
576
  // from dispatching preserved notifications as a new AI turn during the abort.
572
577
  this.isAborting = true;
573
578
  try {
579
+ // If a queued dispatch is in flight (e.g. a notification turn was being
580
+ // processed), remember the abort so the .finally re-check won't start a
581
+ // new dispatch with the preserved notifications right after aborting.
582
+ if (this.dispatchPromise) {
583
+ this.dispatchAborted = true;
584
+ }
574
585
  if (this.aiManager.isLoading || this.isCommandRunning) {
575
586
  // Clear user-facing queue items first to prevent processQueuedMessage
576
587
  // from dequeuing when abortAIMessage triggers onLoadingChange(false).
@@ -684,8 +695,6 @@ export class Agent {
684
695
  }
685
696
  // Cleanup subagent manager
686
697
  this.subagentManager.cleanup();
687
- // Cleanup forked agent manager
688
- await this.forkedAgentManager.cleanup();
689
698
  // Drain an in-flight auto-memory extraction fork so the process doesn't
690
699
  // exit mid-extraction
691
700
  try {
@@ -732,6 +741,27 @@ export class Agent {
732
741
  const result = await this.aiManager.runBtwFork(question, abortSignal, onContent, onReasoning);
733
742
  return result.content ?? result.error ?? "No response received";
734
743
  }
744
+ /**
745
+ * Start a fork subagent in the background (the "/subtask" command path).
746
+ *
747
+ * The fork inherits the parent's full conversation context — same system
748
+ * prompt, tools, model, and message prefix — so the prompt cache is reused,
749
+ * then works independently in the background. On completion its final
750
+ * response is delivered back to the main conversation as a task notification
751
+ * carrying the `<result>` tag. Recursion is prevented inside the fork (the
752
+ * Agent tool and Task tools are denied).
753
+ *
754
+ * @param prompt - The task description for the fork subagent
755
+ * @param options - Background task label and optional loose turn bound
756
+ * @param options.description - Display label for the background task
757
+ * @param options.maxTurns - Loose turn bound (defaults to 200, aligned with
758
+ * Claude Code's fork subagent)
759
+ * @param abortSignal - Optional signal to abort the fork
760
+ * @returns Promise that resolves to the background task ID
761
+ */
762
+ async forkSubagent(prompt, options, abortSignal) {
763
+ return this.aiManager.runForkSubagent(prompt, options, abortSignal);
764
+ }
735
765
  /**
736
766
  * Send a message to the AI agent with optional images
737
767
  *
@@ -943,6 +973,21 @@ export class Agent {
943
973
  getSubagentInstance(subagentId) {
944
974
  return this.subagentManager.getInstance(subagentId);
945
975
  }
976
+ /**
977
+ * Get all subagent configurations visible in this session (built-in,
978
+ * user, project, and plugin agents).
979
+ * @returns The list of subagent configurations
980
+ */
981
+ getSubagentConfigurations() {
982
+ return this.subagentManager.getConfigurations();
983
+ }
984
+ /**
985
+ * Get currently active subagent instances (status active/initializing).
986
+ * @returns The list of active subagent instances
987
+ */
988
+ getActiveSubagentInstances() {
989
+ return this.subagentManager.getActiveInstances();
990
+ }
946
991
  /**
947
992
  * Get the current task list ID
948
993
  */
package/dist/index.d.ts CHANGED
@@ -23,7 +23,9 @@ export * from "./utils/gitUtils.js";
23
23
  export * from "./utils/nameGenerator.js";
24
24
  export * from "./utils/worktreeSession.js";
25
25
  export * from "./utils/worktreeUtils.js";
26
- export { loadMergedWaveConfig } from "./services/configurationService.js";
26
+ export { loadMergedWaveConfig, loadUserConfigEnv, } from "./services/configurationService.js";
27
27
  export * from "./types/index.js";
28
+ export type { SubagentConfiguration } from "./utils/subagentParser.js";
29
+ export type { SubagentInstance } from "./managers/subagentManager.js";
28
30
  export * from "./tools/buildTool.js";
29
31
  export type { ToolPlugin, ToolResult, ToolContext } from "./tools/types.js";
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ export * from "./utils/gitUtils.js";
27
27
  export * from "./utils/nameGenerator.js";
28
28
  export * from "./utils/worktreeSession.js";
29
29
  export * from "./utils/worktreeUtils.js";
30
- export { loadMergedWaveConfig } from "./services/configurationService.js";
30
+ export { loadMergedWaveConfig, loadUserConfigEnv, } from "./services/configurationService.js";
31
31
  export * from "./types/index.js";
32
32
  // Export tool building utilities
33
33
  export * from "./tools/buildTool.js";
@@ -40,6 +40,8 @@ export declare class AIManager {
40
40
  private modelOverride?;
41
41
  private _onCwdChange?;
42
42
  private originalWorkdir;
43
+ /** Pending cwd-recovery notice (set when getWorkdir() auto-recovers), consumed by tools */
44
+ private pendingCwdRecovery;
43
45
  private consecutiveCompactionFailures;
44
46
  private readonly maxTurns?;
45
47
  /** Tracks file mtime/hash at read time for staleness detection on Edit/Write */
@@ -77,6 +79,16 @@ export declare class AIManager {
77
79
  getAutoMemoryEnabled(): boolean;
78
80
  getWorktreeBaseRef(): "fresh" | "head";
79
81
  getWorkdir(): string;
82
+ /**
83
+ * Consume a pending cwd-recovery notice (if any). After the session
84
+ * workdir was auto-recovered because the previous directory disappeared,
85
+ * the first tool that calls this surfaces the notice to the model.
86
+ * Returns null when no recovery is pending.
87
+ */
88
+ consumeCwdRecovery(): {
89
+ from: string;
90
+ to: string;
91
+ } | null;
80
92
  getOriginalWorkdir(): string;
81
93
  /**
82
94
  * Update the working directory mid-session (e.g., when entering/exiting a worktree).
@@ -155,6 +167,30 @@ export declare class AIManager {
155
167
  deniedToolMessage?: string;
156
168
  maxTurns?: number;
157
169
  }, abortSignal?: AbortSignal): Promise<ForkLoopResult>;
170
+ /**
171
+ * Fork subagent ("/subtask"): spawn a background agent that inherits the
172
+ * parent's full conversation context — the same system prompt, tools, model,
173
+ * and message prefix as the main loop — so the forked request prefix matches
174
+ * exactly and the prompt cache is reused. The fork runs to completion in the
175
+ * background via the shared BackgroundTaskManager (visible in /tasks, log at
176
+ * `os.tmpdir()/wave-subagent-<taskId>.log`); its final response is enqueued
177
+ * as a task notification carrying the `<result>` tag, so the main agent
178
+ * picks it up on the next turn and can act on it.
179
+ *
180
+ * Recursion is prevented by denying the Agent tool and the Task management
181
+ * tools (the fork cannot spawn further subagents or mutate the shared task
182
+ * list), and by honoring the parent's denied rules for everything else.
183
+ * Gate-approved tools execute locally in the stripped fork context (no
184
+ * permission prompts), mirroring the other fork paths.
185
+ *
186
+ * Returns the background task ID (empty string when no BackgroundTaskManager
187
+ * is registered, in which case the fork still runs but without task
188
+ * bookkeeping or notification).
189
+ */
190
+ runForkSubagent(prompt: string, options: {
191
+ description: string;
192
+ maxTurns?: number;
193
+ }, abortSignal?: AbortSignal): Promise<string>;
158
194
  /**
159
195
  * Parse a fork tool call's JSON arguments, recovering truncated JSON the
160
196
  * same way the main loop does. Unparseable arguments fall back to `{}` and
@@ -1,14 +1,18 @@
1
1
  import * as aiService from "../services/aiService.js";
2
2
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
3
3
  import { supportsVision } from "../utils/modelCapabilities.js";
4
- import { parseTaskNotificationXml } from "../utils/notificationXml.js";
4
+ import { parseTaskNotificationXml, taskNotificationToXml, } from "../utils/notificationXml.js";
5
5
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
6
6
  import { estimateTokens } from "../utils/tokenEstimate.js";
7
7
  import { getTaskReminderTurnCounts, maybeInjectTaskReminder, TASK_REMINDER_CONFIG, } from "../utils/taskReminder.js";
8
- import { existsSync } from "node:fs";
8
+ import { createWriteStream, existsSync } from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
9
11
  import { buildSystemPrompt, formatCompactSummary, getCompactPrompt, } from "../prompts/index.js";
10
12
  import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, wrapInSystemReminder, } from "../prompts/planModeReminders.js";
11
13
  import { recoverTruncatedJson } from "../utils/stringUtils.js";
14
+ import { AGENT_TOOL_NAME, TASK_CREATE_TOOL_NAME, TASK_GET_TOOL_NAME, TASK_LIST_TOOL_NAME, TASK_UPDATE_TOOL_NAME, } from "../constants/tools.js";
15
+ import { addConsolidatedAbortListener } from "../utils/abortUtils.js";
12
16
  import { logger } from "../utils/globalLogger.js";
13
17
  import { startInteractionSpan, endInteractionSpan, startLLMRequestSpan, endLLMRequestSpan, resetTracingState, } from "../telemetry/sessionTracing.js";
14
18
  import { logOTelEvent } from "../telemetry/events.js";
@@ -60,6 +64,8 @@ export class AIManager {
60
64
  this.turnGeneration = 0; // Guards against concurrent sendAIMessage turns
61
65
  this.abortController = null;
62
66
  this.toolAbortController = null;
67
+ /** Pending cwd-recovery notice (set when getWorkdir() auto-recovers), consumed by tools */
68
+ this.pendingCwdRecovery = null;
63
69
  this.consecutiveCompactionFailures = 0;
64
70
  /** Tracks file mtime/hash at read time for staleness detection on Edit/Write */
65
71
  this.readFileState = new Map();
@@ -151,7 +157,45 @@ export class AIManager {
151
157
  return this.configurationService.resolveWorktreeBaseRef();
152
158
  }
153
159
  getWorkdir() {
154
- return this.container.get("Workdir") ?? process.cwd();
160
+ const workdir = this.container.get("Workdir") ?? process.cwd();
161
+ if (existsSync(workdir)) {
162
+ return workdir;
163
+ }
164
+ // The stored workdir no longer exists — e.g. the current worktree
165
+ // directory was removed manually with `git worktree remove` outside
166
+ // wave's exit flow. Node spawn() chdirs to `cwd` before spawning, so
167
+ // every tool that spawns a subprocess (Bash, Grep, background tasks)
168
+ // would fail with ENOENT and cripple the session. Recover to the
169
+ // original workdir (main repo) so the session keeps working.
170
+ if (!existsSync(this.originalWorkdir)) {
171
+ // No valid fallback — keep the stale value (sessions started in a
172
+ // directory that was deleted since cannot recover to anything real).
173
+ return workdir;
174
+ }
175
+ this.pendingCwdRecovery = { from: workdir, to: this.originalWorkdir };
176
+ // If the removed directory was this session's worktree, drop the stale
177
+ // worktree session state so ExitWorktree no-ops and EnterWorktree can
178
+ // start a fresh session.
179
+ const worktreeSession = this.getWorktreeSession();
180
+ if (worktreeSession &&
181
+ path.resolve(worktreeSession.worktreePath) === path.resolve(workdir)) {
182
+ this.setWorktreeSession(null);
183
+ }
184
+ logger.warn(`Working directory ${workdir} no longer exists; recovered session workdir to ${this.originalWorkdir}`);
185
+ this.container.register("Workdir", this.originalWorkdir);
186
+ this._onCwdChange?.(this.originalWorkdir);
187
+ return this.originalWorkdir;
188
+ }
189
+ /**
190
+ * Consume a pending cwd-recovery notice (if any). After the session
191
+ * workdir was auto-recovered because the previous directory disappeared,
192
+ * the first tool that calls this surfaces the notice to the model.
193
+ * Returns null when no recovery is pending.
194
+ */
195
+ consumeCwdRecovery() {
196
+ const recovery = this.pendingCwdRecovery;
197
+ this.pendingCwdRecovery = null;
198
+ return recovery;
155
199
  }
156
200
  getOriginalWorkdir() {
157
201
  return this.originalWorkdir;
@@ -628,6 +672,179 @@ export class AIManager {
628
672
  }
629
673
  return result;
630
674
  }
675
+ /**
676
+ * Fork subagent ("/subtask"): spawn a background agent that inherits the
677
+ * parent's full conversation context — the same system prompt, tools, model,
678
+ * and message prefix as the main loop — so the forked request prefix matches
679
+ * exactly and the prompt cache is reused. The fork runs to completion in the
680
+ * background via the shared BackgroundTaskManager (visible in /tasks, log at
681
+ * `os.tmpdir()/wave-subagent-<taskId>.log`); its final response is enqueued
682
+ * as a task notification carrying the `<result>` tag, so the main agent
683
+ * picks it up on the next turn and can act on it.
684
+ *
685
+ * Recursion is prevented by denying the Agent tool and the Task management
686
+ * tools (the fork cannot spawn further subagents or mutate the shared task
687
+ * list), and by honoring the parent's denied rules for everything else.
688
+ * Gate-approved tools execute locally in the stripped fork context (no
689
+ * permission prompts), mirroring the other fork paths.
690
+ *
691
+ * Returns the background task ID (empty string when no BackgroundTaskManager
692
+ * is registered, in which case the fork still runs but without task
693
+ * bookkeeping or notification).
694
+ */
695
+ async runForkSubagent(prompt, options, abortSignal) {
696
+ const modelConfig = this.getModelConfig();
697
+ // Snapshot the conversation synchronously: the /subtask handler records
698
+ // the command echo in the transcript after this call, so the fork never
699
+ // sees its own trigger message duplicated.
700
+ const historyMessages = convertMessagesForAPI(this.messageManager.getMessages(), { supportsVision: supportsVision(modelConfig.capabilities) });
701
+ const backgroundTaskManager = this.backgroundTaskManager;
702
+ const permissionManager = this.permissionManager;
703
+ const messageQueue = this.container.has("MessageQueue")
704
+ ? this.container.get("MessageQueue")
705
+ : undefined;
706
+ // Combine the caller's signal with an internal one so a task kill
707
+ // (BackgroundTaskManager.stopTask / cleanup) aborts the in-flight fork.
708
+ const abortController = new AbortController();
709
+ let abortCleanup;
710
+ if (abortSignal) {
711
+ abortCleanup = addConsolidatedAbortListener(abortSignal, [
712
+ () => abortController.abort(),
713
+ ]);
714
+ }
715
+ // Recursion prevention: deny the Agent tool and Task tools, then honor the
716
+ // parent's denied rules for everything else.
717
+ const deniedTools = new Set([
718
+ AGENT_TOOL_NAME,
719
+ TASK_CREATE_TOOL_NAME,
720
+ TASK_GET_TOOL_NAME,
721
+ TASK_UPDATE_TOOL_NAME,
722
+ TASK_LIST_TOOL_NAME,
723
+ ]);
724
+ const canUseTool = (name) => {
725
+ if (deniedTools.has(name))
726
+ return false;
727
+ if (permissionManager?.isToolDenied(name))
728
+ return false;
729
+ return true;
730
+ };
731
+ const deniedToolMessage = "The Agent tool and Task tools are not available in a fork subagent.";
732
+ // Background task bookkeeping mirrors subagentManager.executeAgent.
733
+ let taskId = "";
734
+ let logStream;
735
+ if (backgroundTaskManager) {
736
+ taskId = backgroundTaskManager.generateId();
737
+ const startTime = Date.now();
738
+ const logPath = path.join(os.tmpdir(), `wave-subagent-${taskId}.log`);
739
+ logStream = createWriteStream(logPath, { flags: "a" });
740
+ backgroundTaskManager.addTask({
741
+ id: taskId,
742
+ type: "subagent",
743
+ status: "running",
744
+ startTime,
745
+ description: truncateWithMarker(options.description, 1000),
746
+ stdout: "",
747
+ stderr: "",
748
+ outputPath: logPath,
749
+ onStop: () => {
750
+ logStream?.destroy();
751
+ logStream = undefined;
752
+ abortController.abort();
753
+ },
754
+ });
755
+ logStream.write(`[${new Date().toISOString()}] Fork subagent started: ${options.description}\n`);
756
+ }
757
+ const finish = async (status, content) => {
758
+ if (!backgroundTaskManager || !taskId)
759
+ return;
760
+ const task = backgroundTaskManager.getTask(taskId);
761
+ if (!task)
762
+ return;
763
+ const wasAlreadyKilled = task.status === "killed";
764
+ const endTime = Date.now();
765
+ if (status === "completed") {
766
+ task.status = "completed";
767
+ task.stdout = content;
768
+ logStream?.write(`[${new Date().toISOString()}] Final response:\n${content}\n`);
769
+ }
770
+ else {
771
+ task.status = "failed";
772
+ task.stderr = content;
773
+ logStream?.write(`[${new Date().toISOString()}] Fork subagent failed: ${content}\n`);
774
+ }
775
+ task.endTime = endTime;
776
+ if (task.startTime)
777
+ task.runtime = endTime - task.startTime;
778
+ logStream?.end();
779
+ logStream = undefined;
780
+ // Skip the notification when the task was already stopped (e.g. by main
781
+ // agent shutdown) — mirrors subagentManager's wasAlreadyKilled guard.
782
+ if (!wasAlreadyKilled && messageQueue) {
783
+ const description = truncateWithMarker(options.description, 1000);
784
+ const summary = status === "completed"
785
+ ? `Subtask "${description}" completed`
786
+ : `Subtask "${description}" failed: ${content}`;
787
+ messageQueue.enqueueNotification(taskNotificationToXml({
788
+ type: "task_notification",
789
+ taskId,
790
+ taskType: "agent",
791
+ status,
792
+ summary,
793
+ ...(status === "completed" && { result: content }),
794
+ }));
795
+ }
796
+ };
797
+ // Fire-and-forget: the fork runs to completion in the background; the
798
+ // caller gets the task ID immediately.
799
+ (async () => {
800
+ try {
801
+ const result = await this.runForkLoop(historyMessages, prompt, {
802
+ // /subtask aligns with Claude Code's fork subagent
803
+ // (forkSubagent.ts:65): a loose upper bound that practically
804
+ // never binds.
805
+ maxTurns: options.maxTurns ?? 200,
806
+ canUseTool,
807
+ deniedToolMessage,
808
+ }, abortController.signal);
809
+ if (result.usage && this.callbacks?.onUsageAdded) {
810
+ this.callbacks.onUsageAdded({
811
+ ...result.usage,
812
+ model: modelConfig.model,
813
+ operation_type: "agent",
814
+ });
815
+ }
816
+ if (result.content?.trim()) {
817
+ await finish("completed", result.content);
818
+ }
819
+ else {
820
+ await finish("failed", "Fork subagent produced no text response");
821
+ }
822
+ }
823
+ catch (error) {
824
+ // Abort is the expected exit for a killed task; stopTask has already
825
+ // marked it "killed", so no notification is sent.
826
+ if (abortController.signal.aborted) {
827
+ const task = backgroundTaskManager?.getTask(taskId);
828
+ if (task && task.status === "running") {
829
+ task.status = "killed";
830
+ task.endTime = Date.now();
831
+ if (task.startTime)
832
+ task.runtime = task.endTime - task.startTime;
833
+ }
834
+ logStream?.end();
835
+ logStream = undefined;
836
+ }
837
+ else {
838
+ logger?.error("Fork subagent execution failed:", error);
839
+ await finish("failed", error instanceof Error ? error.message : String(error));
840
+ }
841
+ }
842
+ finally {
843
+ abortCleanup?.();
844
+ }
845
+ })();
846
+ return taskId;
847
+ }
631
848
  /**
632
849
  * Parse a fork tool call's JSON arguments, recovering truncated JSON the
633
850
  * same way the main loop does. Unparseable arguments fall back to `{}` and
@@ -1166,9 +1383,6 @@ ${question}`;
1166
1383
  ...(result.usage.cache_creation_input_tokens !== undefined && {
1167
1384
  cache_creation_input_tokens: result.usage.cache_creation_input_tokens,
1168
1385
  }),
1169
- ...(result.usage.cache_creation && {
1170
- cache_creation: result.usage.cache_creation,
1171
- }),
1172
1386
  };
1173
1387
  }
1174
1388
  // Set usage on the assistant message if available
@@ -1348,9 +1562,15 @@ ${question}`;
1348
1562
  await this.messageManager.saveSession();
1349
1563
  // Clear temporary rules
1350
1564
  this.permissionManager?.clearTemporaryRules();
1351
- // Clear abort controllers
1352
- this.abortController = null;
1353
- this.toolAbortController = null;
1565
+ // Clear abort controllers (only if we still own them — a concurrent
1566
+ // turn may have already registered its own controller, and clearing
1567
+ // it would orphan that turn and make it uninterruptible)
1568
+ if (this.abortController === abortController) {
1569
+ this.abortController = null;
1570
+ }
1571
+ if (this.toolAbortController === toolAbortController) {
1572
+ this.toolAbortController = null;
1573
+ }
1354
1574
  // Execute Stop/SubagentStop hooks only if the operation was not aborted
1355
1575
  const isCurrentlyAborted = abortController.signal.aborted || toolAbortController.signal.aborted;
1356
1576
  if (!isCurrentlyAborted) {
@@ -1373,11 +1593,19 @@ ${question}`;
1373
1593
  }
1374
1594
  }
1375
1595
  // Inject pending notifications from background tasks (after Stop hooks,
1376
- // aligned with Claude Code which fires Stop hooks unconditionally)
1596
+ // aligned with Claude Code which fires Stop hooks unconditionally).
1597
+ // Skip injection when this turn was aborted: folding a pending
1598
+ // notification in would restart the loop and resurrect the aborted
1599
+ // turn — if a concurrent sendMessage is already running (e.g. the
1600
+ // queue's "send now" button), it would spawn a second, orphaned,
1601
+ // uninterruptible agent loop. The notification stays queued for the
1602
+ // next turn.
1377
1603
  const messageQueue = this.container.has("MessageQueue")
1378
1604
  ? this.container.get("MessageQueue")
1379
1605
  : undefined;
1380
- if (messageQueue && messageQueue.hasNotifications()) {
1606
+ if (messageQueue &&
1607
+ messageQueue.hasNotifications() &&
1608
+ !isCurrentlyAborted) {
1381
1609
  const notifications = messageQueue.drainNotifications();
1382
1610
  for (const notification of notifications) {
1383
1611
  const block = parseTaskNotificationXml(notification);
@@ -1388,6 +1616,7 @@ ${question}`;
1388
1616
  status: block.status,
1389
1617
  summary: block.summary,
1390
1618
  outputFile: block.outputFile,
1619
+ result: block.result,
1391
1620
  });
1392
1621
  }
1393
1622
  }
@@ -13,13 +13,11 @@ export interface MessageManagerCallbacks {
13
13
  onAssistantContentUpdated?: (params: {
14
14
  messageId: string;
15
15
  chunk: string;
16
- accumulated: string;
17
16
  stage: "streaming" | "end";
18
17
  }) => void;
19
18
  onAssistantReasoningUpdated?: (params: {
20
19
  messageId: string;
21
20
  chunk: string;
22
- accumulated: string;
23
21
  stage: "streaming" | "end";
24
22
  }) => void;
25
23
  onToolBlockUpdated?: (params: ToolBlockUpdateCallbackParams) => void;
@@ -28,7 +26,7 @@ export interface MessageManagerCallbacks {
28
26
  onCompactionStateChange?: (isCompacting: boolean) => void;
29
27
  onAddBangMessage?: (command: string, messageId: string) => void;
30
28
  onUpdateBangMessage?: (command: string, output: string, messageId: string) => void;
31
- onCompleteBangMessage?: (command: string, exitCode: number, messageId: string) => void;
29
+ onCompleteBangMessage?: (command: string, exitCode: number, messageId: string, output?: string) => void;
32
30
  onInfoBlockAdded?: (content: string) => void;
33
31
  onShowRewind?: () => void;
34
32
  onFileHistoryBlockAdded?: (snapshots: import("../types/reversion.js").FileSnapshot[]) => void;
@@ -180,7 +178,7 @@ export declare class MessageManager {
180
178
  /**
181
179
  * Update the current assistant message content during streaming
182
180
  * This method updates the last assistant message's content without creating a new message
183
- * FR-001: Tracks and provides both chunk (new content) and accumulated (total content)
181
+ * FR-001: Tracks and provides chunk (new content since last update)
184
182
  */
185
183
  updateCurrentMessageContent(newAccumulatedContent: string): void;
186
184
  /**
@@ -366,6 +366,16 @@ export class MessageManager {
366
366
  async compactMessagesAndUpdateSession(compactedContent, usage) {
367
367
  // Get last 2 API rounds to preserve (structurally safe boundary)
368
368
  const lastThreeMessages = getLastApiRounds(this.messages, 2);
369
+ // The turn that triggered compaction has already finished streaming, so
370
+ // finalize any still-'streaming' text/reasoning blocks on the preserved
371
+ // last assistant message. Otherwise hosts that pull a full snapshot after
372
+ // compaction (or the loop's own finalizeStreamingBlocks, which no-ops once
373
+ // a plan-mode reminder is appended) would keep showing the streaming dot.
374
+ const lastPreserved = lastThreeMessages[lastThreeMessages.length - 1];
375
+ if (lastPreserved && lastPreserved.role === "assistant") {
376
+ this.finalizeStreamingBlock(lastPreserved, "text");
377
+ this.finalizeStreamingBlock(lastPreserved, "reasoning");
378
+ }
369
379
  // Create compacted message
370
380
  const compactMessage = {
371
381
  id: generateMessageId(),
@@ -436,7 +446,7 @@ export class MessageManager {
436
446
  });
437
447
  this.setMessages(updatedMessages);
438
448
  const messageId = this.findBangMessageId(command) ?? "";
439
- this.callbacks.onCompleteBangMessage?.(command, exitCode, messageId);
449
+ this.callbacks.onCompleteBangMessage?.(command, exitCode, messageId, output?.trim());
440
450
  }
441
451
  /**
442
452
  * Find the message ID of the most recent message containing a bang block
@@ -526,7 +536,6 @@ export class MessageManager {
526
536
  const callbackParams = {
527
537
  messageId: lastMessage.id,
528
538
  chunk: "",
529
- accumulated: block.content,
530
539
  stage: "end",
531
540
  };
532
541
  if (type === "text") {
@@ -540,7 +549,7 @@ export class MessageManager {
540
549
  /**
541
550
  * Update the current assistant message content during streaming
542
551
  * This method updates the last assistant message's content without creating a new message
543
- * FR-001: Tracks and provides both chunk (new content) and accumulated (total content)
552
+ * FR-001: Tracks and provides chunk (new content since last update)
544
553
  */
545
554
  updateCurrentMessageContent(newAccumulatedContent) {
546
555
  if (this.messages.length === 0)
@@ -573,11 +582,10 @@ export class MessageManager {
573
582
  stage: "streaming",
574
583
  });
575
584
  }
576
- // FR-001: Trigger callbacks with chunk and accumulated content
585
+ // FR-001: Trigger callbacks with chunk delta and stage
577
586
  this.callbacks.onAssistantContentUpdated?.({
578
587
  messageId: lastMessage.id,
579
588
  chunk,
580
- accumulated: newAccumulatedContent,
581
589
  stage: "streaming",
582
590
  });
583
591
  // Note: Subagent-specific callbacks are now handled by SubagentManager
@@ -620,11 +628,10 @@ export class MessageManager {
620
628
  startTime: Date.now(),
621
629
  });
622
630
  }
623
- // Trigger callbacks with chunk and accumulated reasoning content
631
+ // Trigger callbacks with chunk delta and stage
624
632
  this.callbacks.onAssistantReasoningUpdated?.({
625
633
  messageId: lastMessage.id,
626
634
  chunk,
627
- accumulated: newAccumulatedReasoning,
628
635
  stage: "streaming",
629
636
  });
630
637
  }