wave-agent-sdk 0.5.0 → 0.6.1

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 (152) hide show
  1. package/dist/agent.d.ts +14 -6
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +65 -88
  4. package/dist/constants/prompts.d.ts +18 -14
  5. package/dist/constants/prompts.d.ts.map +1 -1
  6. package/dist/constants/prompts.js +130 -54
  7. package/dist/constants/tools.d.ts +4 -1
  8. package/dist/constants/tools.d.ts.map +1 -1
  9. package/dist/constants/tools.js +4 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/managers/aiManager.d.ts +2 -5
  14. package/dist/managers/aiManager.d.ts.map +1 -1
  15. package/dist/managers/aiManager.js +59 -48
  16. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  17. package/dist/managers/backgroundTaskManager.js +59 -53
  18. package/dist/managers/foregroundTaskManager.d.ts.map +1 -1
  19. package/dist/managers/foregroundTaskManager.js +3 -2
  20. package/dist/managers/mcpManager.d.ts.map +1 -1
  21. package/dist/managers/messageManager.d.ts +14 -10
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +102 -62
  24. package/dist/managers/permissionManager.d.ts.map +1 -1
  25. package/dist/managers/permissionManager.js +25 -15
  26. package/dist/managers/planManager.d.ts +1 -1
  27. package/dist/managers/planManager.d.ts.map +1 -1
  28. package/dist/managers/planManager.js +2 -2
  29. package/dist/managers/slashCommandManager.d.ts +3 -0
  30. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  31. package/dist/managers/slashCommandManager.js +7 -2
  32. package/dist/managers/subagentManager.d.ts +4 -0
  33. package/dist/managers/subagentManager.d.ts.map +1 -1
  34. package/dist/managers/subagentManager.js +22 -14
  35. package/dist/managers/toolManager.d.ts +11 -0
  36. package/dist/managers/toolManager.d.ts.map +1 -1
  37. package/dist/managers/toolManager.js +20 -2
  38. package/dist/services/aiService.d.ts +0 -1
  39. package/dist/services/aiService.d.ts.map +1 -1
  40. package/dist/services/aiService.js +4 -140
  41. package/dist/services/memory.d.ts +0 -3
  42. package/dist/services/memory.d.ts.map +1 -1
  43. package/dist/services/memory.js +0 -59
  44. package/dist/services/session.d.ts +15 -1
  45. package/dist/services/session.d.ts.map +1 -1
  46. package/dist/services/session.js +57 -1
  47. package/dist/services/taskManager.d.ts +21 -0
  48. package/dist/services/taskManager.d.ts.map +1 -0
  49. package/dist/services/taskManager.js +158 -0
  50. package/dist/tools/askUserQuestion.d.ts.map +1 -1
  51. package/dist/tools/askUserQuestion.js +39 -25
  52. package/dist/tools/bashTool.d.ts.map +1 -1
  53. package/dist/tools/bashTool.js +7 -9
  54. package/dist/tools/editTool.d.ts.map +1 -1
  55. package/dist/tools/editTool.js +2 -1
  56. package/dist/tools/exitPlanMode.d.ts.map +1 -1
  57. package/dist/tools/exitPlanMode.js +25 -1
  58. package/dist/tools/globTool.d.ts.map +1 -1
  59. package/dist/tools/globTool.js +8 -2
  60. package/dist/tools/grepTool.d.ts.map +1 -1
  61. package/dist/tools/grepTool.js +17 -6
  62. package/dist/tools/lsTool.d.ts.map +1 -1
  63. package/dist/tools/lsTool.js +3 -1
  64. package/dist/tools/readTool.d.ts.map +1 -1
  65. package/dist/tools/readTool.js +16 -1
  66. package/dist/tools/taskManagementTools.d.ts +6 -0
  67. package/dist/tools/taskManagementTools.d.ts.map +1 -0
  68. package/dist/tools/taskManagementTools.js +453 -0
  69. package/dist/tools/taskOutputTool.d.ts.map +1 -1
  70. package/dist/tools/taskOutputTool.js +32 -8
  71. package/dist/tools/taskStopTool.d.ts.map +1 -1
  72. package/dist/tools/taskStopTool.js +7 -1
  73. package/dist/tools/taskTool.d.ts.map +1 -1
  74. package/dist/tools/taskTool.js +6 -1
  75. package/dist/tools/types.d.ts +9 -0
  76. package/dist/tools/types.d.ts.map +1 -1
  77. package/dist/tools/writeTool.d.ts.map +1 -1
  78. package/dist/tools/writeTool.js +9 -1
  79. package/dist/types/index.d.ts +1 -0
  80. package/dist/types/index.d.ts.map +1 -1
  81. package/dist/types/index.js +1 -0
  82. package/dist/types/messaging.d.ts +2 -8
  83. package/dist/types/messaging.d.ts.map +1 -1
  84. package/dist/types/processes.d.ts +11 -6
  85. package/dist/types/processes.d.ts.map +1 -1
  86. package/dist/types/tasks.d.ts +13 -0
  87. package/dist/types/tasks.d.ts.map +1 -0
  88. package/dist/types/tasks.js +1 -0
  89. package/dist/types/tools.d.ts +4 -1
  90. package/dist/types/tools.d.ts.map +1 -1
  91. package/dist/utils/builtinSubagents.d.ts.map +1 -1
  92. package/dist/utils/builtinSubagents.js +38 -1
  93. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  94. package/dist/utils/cacheControlUtils.js +18 -12
  95. package/dist/utils/constants.d.ts +0 -4
  96. package/dist/utils/constants.d.ts.map +1 -1
  97. package/dist/utils/constants.js +0 -4
  98. package/dist/utils/convertMessagesForAPI.js +2 -2
  99. package/dist/utils/messageOperations.d.ts +2 -35
  100. package/dist/utils/messageOperations.d.ts.map +1 -1
  101. package/dist/utils/messageOperations.js +4 -97
  102. package/dist/utils/nameGenerator.d.ts +1 -1
  103. package/dist/utils/nameGenerator.d.ts.map +1 -1
  104. package/dist/utils/nameGenerator.js +19 -3
  105. package/package.json +1 -1
  106. package/src/agent.ts +90 -101
  107. package/src/constants/prompts.ts +156 -65
  108. package/src/constants/tools.ts +4 -1
  109. package/src/index.ts +1 -0
  110. package/src/managers/aiManager.ts +79 -70
  111. package/src/managers/backgroundTaskManager.ts +53 -54
  112. package/src/managers/foregroundTaskManager.ts +3 -2
  113. package/src/managers/mcpManager.ts +6 -3
  114. package/src/managers/messageManager.ts +137 -73
  115. package/src/managers/permissionManager.ts +32 -21
  116. package/src/managers/planManager.ts +2 -2
  117. package/src/managers/slashCommandManager.ts +11 -2
  118. package/src/managers/subagentManager.ts +33 -14
  119. package/src/managers/toolManager.ts +32 -2
  120. package/src/services/aiService.ts +3 -145
  121. package/src/services/memory.ts +0 -72
  122. package/src/services/session.ts +73 -0
  123. package/src/services/taskManager.ts +188 -0
  124. package/src/tools/askUserQuestion.ts +51 -29
  125. package/src/tools/bashTool.ts +9 -15
  126. package/src/tools/editTool.ts +3 -1
  127. package/src/tools/exitPlanMode.ts +26 -2
  128. package/src/tools/globTool.ts +10 -2
  129. package/src/tools/grepTool.ts +17 -6
  130. package/src/tools/lsTool.ts +3 -1
  131. package/src/tools/readTool.ts +17 -1
  132. package/src/tools/taskManagementTools.ts +498 -0
  133. package/src/tools/taskOutputTool.ts +34 -12
  134. package/src/tools/taskStopTool.ts +7 -1
  135. package/src/tools/taskTool.ts +7 -1
  136. package/src/tools/types.ts +10 -0
  137. package/src/tools/writeTool.ts +9 -2
  138. package/src/types/index.ts +1 -0
  139. package/src/types/messaging.ts +1 -9
  140. package/src/types/processes.ts +13 -7
  141. package/src/types/tasks.ts +13 -0
  142. package/src/types/tools.ts +4 -1
  143. package/src/utils/builtinSubagents.ts +47 -1
  144. package/src/utils/cacheControlUtils.ts +26 -18
  145. package/src/utils/constants.ts +0 -5
  146. package/src/utils/convertMessagesForAPI.ts +2 -2
  147. package/src/utils/messageOperations.ts +5 -136
  148. package/src/utils/nameGenerator.ts +20 -3
  149. package/dist/tools/todoWriteTool.d.ts +0 -6
  150. package/dist/tools/todoWriteTool.d.ts.map +0 -1
  151. package/dist/tools/todoWriteTool.js +0 -220
  152. package/src/tools/todoWriteTool.ts +0 -257
@@ -69,6 +69,26 @@ export class BackgroundTaskManager {
69
69
  status: "running",
70
70
  stdout: "",
71
71
  stderr: "",
72
+ onStop: () => {
73
+ try {
74
+ if (child.pid) {
75
+ process.kill(-child.pid, "SIGTERM");
76
+ setTimeout(() => {
77
+ if (child.pid && !child.killed) {
78
+ try {
79
+ process.kill(-child.pid, "SIGKILL");
80
+ } catch (error) {
81
+ logger.error("Failed to force kill process:", error);
82
+ }
83
+ }
84
+ }, 1000);
85
+ } else {
86
+ child.kill("SIGTERM");
87
+ }
88
+ } catch {
89
+ child.kill("SIGTERM");
90
+ }
91
+ },
72
92
  };
73
93
 
74
94
  this.tasks.set(id, shell);
@@ -155,6 +175,26 @@ export class BackgroundTaskManager {
155
175
  status: "running",
156
176
  stdout: initialStdout,
157
177
  stderr: initialStderr,
178
+ onStop: () => {
179
+ try {
180
+ if (child.pid) {
181
+ process.kill(-child.pid, "SIGTERM");
182
+ setTimeout(() => {
183
+ if (child.pid && !child.killed) {
184
+ try {
185
+ process.kill(-child.pid, "SIGKILL");
186
+ } catch (error) {
187
+ logger.error("Failed to force kill process:", error);
188
+ }
189
+ }
190
+ }, 1000);
191
+ } else {
192
+ child.kill("SIGTERM");
193
+ }
194
+ } catch {
195
+ child.kill("SIGTERM");
196
+ }
197
+ },
158
198
  };
159
199
 
160
200
  this.tasks.set(id, shell);
@@ -232,65 +272,24 @@ export class BackgroundTaskManager {
232
272
  return false;
233
273
  }
234
274
 
235
- if (task.type === "shell") {
236
- const shell = task as BackgroundShell;
275
+ if (task.onStop) {
237
276
  try {
238
- // Try to kill process group first
239
- if (shell.process.pid) {
240
- process.kill(-shell.process.pid, "SIGTERM");
241
-
242
- // Force kill after timeout
243
- setTimeout(() => {
244
- if (
245
- shell.status === "running" &&
246
- shell.process.pid &&
247
- !shell.process.killed
248
- ) {
249
- try {
250
- process.kill(-shell.process.pid, "SIGKILL");
251
- } catch (error) {
252
- logger.error("Failed to force kill process:", error);
253
- }
254
- }
255
- }, 1000);
256
- }
257
-
258
- shell.status = "killed";
259
- shell.endTime = Date.now();
260
- shell.runtime = shell.endTime - shell.startTime;
261
- this.notifyTasksChange();
262
- return true;
263
- } catch {
264
- // Fallback to direct process kill
265
- try {
266
- shell.process.kill("SIGTERM");
267
- setTimeout(() => {
268
- if (!shell.process.killed) {
269
- shell.process.kill("SIGKILL");
270
- }
271
- }, 1000);
272
- shell.status = "killed";
273
- shell.endTime = Date.now();
274
- shell.runtime = shell.endTime - shell.startTime;
275
- this.notifyTasksChange();
276
- return true;
277
- } catch (directKillError) {
278
- logger.error("Failed to kill child process:", directKillError);
279
- return false;
277
+ const result = task.onStop();
278
+ if (result instanceof Promise) {
279
+ result.catch((error) => {
280
+ logger.error("Error in background task onStop callback:", error);
281
+ });
280
282
  }
283
+ } catch (error) {
284
+ logger.error("Error in background task onStop callback:", error);
281
285
  }
282
- } else if (task.type === "subagent") {
283
- // Subagent termination logic will be handled by aborting the AI loop
284
- // which is already managed by the SubagentManager and AIManager.
285
- // Here we just update the status.
286
- task.status = "killed";
287
- task.endTime = Date.now();
288
- task.runtime = task.endTime - task.startTime;
289
- this.notifyTasksChange();
290
- return true;
291
286
  }
292
287
 
293
- return false;
288
+ task.status = "killed";
289
+ task.endTime = Date.now();
290
+ task.runtime = task.endTime - task.startTime;
291
+ this.notifyTasksChange();
292
+ return true;
294
293
  }
295
294
 
296
295
  public cleanup(): void {
@@ -14,8 +14,9 @@ export class ForegroundTaskManager implements IForegroundTaskManager {
14
14
  }
15
15
 
16
16
  public async backgroundCurrentTask(): Promise<void> {
17
- const task = this.activeForegroundTasks.pop();
18
- if (task) {
17
+ const tasks = [...this.activeForegroundTasks].reverse();
18
+ this.activeForegroundTasks = [];
19
+ for (const task of tasks) {
19
20
  await task.backgroundHandler();
20
21
  }
21
22
  }
@@ -3,7 +3,7 @@ import { join } from "path";
3
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
5
  import { ChatCompletionFunctionTool } from "openai/resources.js";
6
- import { createMcpToolPlugin, findToolServer } from "@/utils/mcpUtils.js";
6
+ import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
7
7
  import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
8
8
  import type {
9
9
  Logger,
@@ -476,8 +476,11 @@ export class McpManager {
476
476
  const server = findToolServer(tool.name, servers);
477
477
 
478
478
  if (server) {
479
- const plugin = createMcpToolPlugin(tool, server.name, (name, args) =>
480
- this.executeMcpTool(name, args),
479
+ const plugin = createMcpToolPlugin(
480
+ tool,
481
+ server.name,
482
+ (name: string, args: Record<string, unknown>) =>
483
+ this.executeMcpTool(name, args),
481
484
  );
482
485
  mcpTools.set(plugin.name, plugin);
483
486
  }
@@ -3,7 +3,6 @@ import {
3
3
  updateToolBlockInMessage,
4
4
  addErrorBlockToMessage,
5
5
  addUserMessageToMessages,
6
- extractUserInputHistory,
7
6
  addCommandOutputMessage,
8
7
  updateCommandOutputInMessage,
9
8
  completeCommandInMessage,
@@ -33,7 +32,6 @@ export interface MessageManagerCallbacks {
33
32
  onMessagesChange?: (messages: Message[]) => void;
34
33
  onSessionIdChange?: (sessionId: string) => void;
35
34
  onLatestTotalTokensChange?: (latestTotalTokens: number) => void;
36
- onUserInputHistoryChange?: (history: string[]) => void;
37
35
  onUsagesChange?: (usages: Usage[]) => void;
38
36
  // Incremental callback
39
37
  onUserMessageAdded?: (params: UserMessageParams) => void;
@@ -45,7 +43,7 @@ export interface MessageManagerCallbacks {
45
43
  onAssistantReasoningUpdated?: (chunk: string, accumulated: string) => void;
46
44
  onToolBlockUpdated?: (params: AgentToolBlockUpdateParams) => void;
47
45
  onErrorBlockAdded?: (error: string) => void;
48
- onCompressBlockAdded?: (insertIndex: number, content: string) => void;
46
+ onCompressBlockAdded?: (content: string) => void;
49
47
  onCompressionStateChange?: (isCompressing: boolean) => void;
50
48
  onMemoryBlockAdded?: (
51
49
  content: string,
@@ -74,6 +72,10 @@ export interface MessageManagerCallbacks {
74
72
  subagentId: string,
75
73
  status: "active" | "completed" | "error" | "aborted",
76
74
  ) => void;
75
+ onFileHistoryBlockAdded?: (
76
+ snapshots: import("../types/reversion.js").FileSnapshot[],
77
+ ) => void;
78
+ onSubagentTaskStopRequested?: (subagentId: string) => void;
77
79
  }
78
80
 
79
81
  export interface MessageManagerOptions {
@@ -88,9 +90,10 @@ export interface MessageManagerOptions {
88
90
  export class MessageManager {
89
91
  // Private state properties
90
92
  private sessionId: string;
93
+ private rootSessionId: string;
94
+ private parentSessionId?: string;
91
95
  private messages: Message[];
92
96
  private latestTotalTokens: number;
93
- private userInputHistory: string[];
94
97
  private workdir: string;
95
98
  private encodedWorkdir: string; // Cached encoded workdir
96
99
  private logger?: Logger; // Add optional logger property
@@ -104,9 +107,9 @@ export class MessageManager {
104
107
 
105
108
  constructor(options: MessageManagerOptions) {
106
109
  this.sessionId = generateSessionId();
110
+ this.rootSessionId = this.sessionId;
107
111
  this.messages = [];
108
112
  this.latestTotalTokens = 0;
109
- this.userInputHistory = [];
110
113
  this.workdir = options.workdir;
111
114
  this.encodedWorkdir = pathEncoder.encodeSync(this.workdir); // Cache encoded workdir
112
115
  this.callbacks = options.callbacks;
@@ -125,6 +128,14 @@ export class MessageManager {
125
128
  return this.sessionId;
126
129
  }
127
130
 
131
+ public getRootSessionId(): string {
132
+ return this.rootSessionId;
133
+ }
134
+
135
+ public getParentSessionId(): string | undefined {
136
+ return this.parentSessionId;
137
+ }
138
+
128
139
  public getMessages(): Message[] {
129
140
  return [...this.messages];
130
141
  }
@@ -133,10 +144,6 @@ export class MessageManager {
133
144
  return this.latestTotalTokens;
134
145
  }
135
146
 
136
- public getUserInputHistory(): string[] {
137
- return [...this.userInputHistory];
138
- }
139
-
140
147
  public getWorkdir(): string {
141
148
  return this.workdir;
142
149
  }
@@ -247,6 +254,8 @@ export class MessageManager {
247
254
  unsavedMessages, // Only append new messages
248
255
  this.workdir,
249
256
  this.sessionType,
257
+ this.rootSessionId,
258
+ this.parentSessionId,
250
259
  );
251
260
 
252
261
  // Update the saved message count
@@ -264,18 +273,13 @@ export class MessageManager {
264
273
  }
265
274
  }
266
275
 
267
- public setUserInputHistory(userInputHistory: string[]): void {
268
- this.userInputHistory = [...userInputHistory];
269
- this.callbacks.onUserInputHistoryChange?.(this.userInputHistory);
270
- }
271
-
272
276
  /**
273
- * Clear messages and input history
277
+ * Clear messages
274
278
  */
275
279
  public clearMessages(): void {
276
280
  this.setMessages([]);
277
- this.setUserInputHistory([]);
278
281
  this.setSessionId(generateSessionId());
282
+ this.rootSessionId = this.sessionId;
279
283
  this.setlatestTotalTokens(0);
280
284
  this.savedMessageCount = 0; // Reset saved message count
281
285
  }
@@ -297,36 +301,17 @@ export class MessageManager {
297
301
  // Initialize state from session data
298
302
  public initializeFromSession(sessionData: SessionData): void {
299
303
  this.setSessionId(sessionData.id);
304
+ this.rootSessionId = sessionData.rootSessionId || sessionData.id;
305
+ this.parentSessionId = sessionData.parentSessionId;
300
306
  this.setMessages([...sessionData.messages]);
301
307
  this.updateFilesInContext(sessionData.messages);
302
308
  this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
303
309
 
304
- // Extract user input history from session messages
305
- this.setUserInputHistory(extractUserInputHistory(sessionData.messages));
306
-
307
310
  // Set saved message count to the number of loaded messages since they're already saved
308
311
  // This must be done after setSessionId which resets it to 0
309
312
  this.savedMessageCount = sessionData.messages.length;
310
313
  }
311
314
 
312
- // Add to input history
313
- public addToInputHistory(input: string): void {
314
- // Avoid adding duplicate inputs
315
- if (
316
- this.userInputHistory.length > 0 &&
317
- this.userInputHistory[this.userInputHistory.length - 1] === input
318
- ) {
319
- return;
320
- }
321
- // Limit history records, keep the latest 100
322
- this.setUserInputHistory([...this.userInputHistory, input].slice(-100));
323
- }
324
-
325
- // Clear input history
326
- public clearInputHistory(): void {
327
- this.setUserInputHistory([]);
328
- }
329
-
330
315
  // Encapsulated message operation functions
331
316
  public addUserMessage(params: UserMessageParams): void {
332
317
  const newMessages = addUserMessageToMessages({
@@ -402,17 +387,7 @@ export class MessageManager {
402
387
  public updateToolBlock(params: AgentToolBlockUpdateParams): void {
403
388
  const newMessages = updateToolBlockInMessage({
404
389
  messages: this.messages,
405
- id: params.id,
406
- parameters: params.parameters,
407
- result: params.result,
408
- success: params.success,
409
- error: params.error,
410
- stage: params.stage,
411
- name: params.name,
412
- shortResult: params.shortResult,
413
- images: params.images,
414
- compactParams: params.compactParams,
415
- parametersChunk: params.parametersChunk,
390
+ ...params,
416
391
  });
417
392
  this.setMessages(newMessages);
418
393
  this.callbacks.onToolBlockUpdated?.(params);
@@ -442,14 +417,14 @@ export class MessageManager {
442
417
  }
443
418
 
444
419
  /**
445
- * Compress messages and update session, delete compressed messages, only keep compressed messages and subsequent messages
420
+ * Compress messages and update session, delete compressed messages, only keep compressed messages and last 3 messages
446
421
  */
447
422
  public compressMessagesAndUpdateSession(
448
- insertIndex: number,
449
423
  compressedContent: string,
450
424
  usage?: Usage,
451
425
  ): void {
452
- const currentMessages = this.messages;
426
+ // Get last 3 messages to preserve
427
+ const lastThreeMessages = this.messages.slice(-3);
453
428
 
454
429
  // Create compressed message
455
430
  const compressMessage: Message = {
@@ -464,24 +439,24 @@ export class MessageManager {
464
439
  ...(usage && { usage }),
465
440
  };
466
441
 
467
- // Convert negative index to positive index
468
- const actualIndex =
469
- insertIndex < 0 ? currentMessages.length + insertIndex : insertIndex;
442
+ // Build new message array: keep the compressed message and last 3 messages
443
+ const newMessages: Message[] = [compressMessage, ...lastThreeMessages];
470
444
 
471
- // Build new message array: keep compressed message and all messages from actualIndex onwards
472
- const newMessages: Message[] = [
473
- compressMessage,
474
- ...currentMessages.slice(actualIndex),
475
- ];
476
-
477
- // Update sessionId
445
+ // Update sessionId and parentSessionId
446
+ const oldSessionId = this.sessionId;
478
447
  this.setSessionId(generateSessionId());
448
+ this.parentSessionId = oldSessionId;
449
+
450
+ // Trigger task list update if this is the main session to ensure continuity
451
+ if (this.sessionType === "main") {
452
+ this.callbacks.onSessionIdChange?.(this.sessionId);
453
+ }
479
454
 
480
455
  // Set new message list
481
456
  this.setMessages(newMessages);
482
457
 
483
- // Trigger compression callback, insertIndex remains unchanged
484
- this.callbacks.onCompressBlockAdded?.(insertIndex, compressedContent);
458
+ // Trigger compression callback
459
+ this.callbacks.onCompressBlockAdded?.(compressedContent);
485
460
  }
486
461
 
487
462
  public addFileHistoryBlock(
@@ -496,6 +471,7 @@ export class MessageManager {
496
471
  snapshots,
497
472
  } as unknown as import("../types/index.js").MessageBlock);
498
473
  this.setMessages([...this.messages]);
474
+ this.callbacks.onFileHistoryBlockAdded?.(snapshots);
499
475
  }
500
476
  }
501
477
 
@@ -703,6 +679,14 @@ export class MessageManager {
703
679
  this.setMessages(newMessages);
704
680
  }
705
681
 
682
+ public async getFullMessageThread(): Promise<{
683
+ messages: Message[];
684
+ sessionIds: string[];
685
+ }> {
686
+ const { loadFullMessageThread } = await import("../services/session.js");
687
+ return loadFullMessageThread(this.sessionId, this.workdir);
688
+ }
689
+
706
690
  /**
707
691
  * Truncate history to a specific index and revert file changes.
708
692
  * @param index - The index of the user message to truncate to.
@@ -712,30 +696,110 @@ export class MessageManager {
712
696
  index: number,
713
697
  reversionManager?: import("./reversionManager.js").ReversionManager,
714
698
  ): Promise<void> {
715
- if (index < 0 || index >= this.messages.length) {
699
+ const { messages, sessionIds } = await this.getFullMessageThread();
700
+
701
+ if (index < 0 || index >= messages.length) {
716
702
  throw new Error(`Invalid message index: ${index}`);
717
703
  }
718
704
 
719
- // Identify messages to be removed
720
- const messagesToRemove = this.messages.slice(index);
705
+ // Find which session the index belongs to
706
+ let targetSessionId = this.sessionId;
707
+ let targetIndexInSession = index;
708
+
709
+ // We need to be careful here because loadFullMessageThread might have removed "compress" blocks
710
+ // Let's re-calculate based on the actual messages returned.
711
+ // Actually, it's easier to just load sessions one by one again or keep track of counts.
712
+
713
+ // For simplicity, let's assume we want to truncate the WHOLE thread.
714
+ // If the index is in a previous session, we need to:
715
+ // 1. Load that session.
716
+ // 2. Truncate it.
717
+ // 3. Make it the current session.
718
+ // 4. Delete/Invalidate subsequent sessions.
719
+
720
+ // To correctly map 'index' to a session, we need to know the message count of each session
721
+ // as they appear in the concatenated 'messages' array.
722
+
723
+ let remainingIndex = index;
724
+ const { loadSessionFromJsonl } = await import("../services/session.js");
725
+
726
+ for (const sid of sessionIds) {
727
+ const sessionData = await loadSessionFromJsonl(sid, this.workdir);
728
+ if (!sessionData) continue;
729
+
730
+ const sessionMessages = sessionData.messages;
731
+ // If this is not the first session in the thread, it might have a compress block at the start
732
+ // that was removed in getFullMessageThread.
733
+ const hasCompressBlock = sessionMessages[0]?.blocks.some(
734
+ (b) => b.type === "compress",
735
+ );
736
+ const effectiveMessages =
737
+ hasCompressBlock && sid !== sessionIds[0]
738
+ ? sessionMessages.slice(1)
739
+ : sessionMessages;
740
+
741
+ if (remainingIndex < effectiveMessages.length) {
742
+ targetSessionId = sid;
743
+ targetIndexInSession = hasCompressBlock
744
+ ? remainingIndex + 1
745
+ : remainingIndex;
746
+ break;
747
+ }
748
+ remainingIndex -= effectiveMessages.length;
749
+ }
750
+
751
+ // Load the target session to perform truncation
752
+ const targetSessionData = await loadSessionFromJsonl(
753
+ targetSessionId,
754
+ this.workdir,
755
+ );
756
+ if (!targetSessionData)
757
+ throw new Error(`Target session ${targetSessionId} not found`);
758
+
759
+ // Identify messages to be removed (from the whole thread)
760
+ const messagesToRemove = messages.slice(index);
721
761
  const messageIdsToRemove = messagesToRemove
722
762
  .map((m) => m.id as string)
723
763
  .filter((id) => !!id);
724
764
 
725
765
  // Revert file changes if manager is provided
726
766
  if (reversionManager && messageIdsToRemove.length > 0) {
727
- await reversionManager.revertTo(messageIdsToRemove, this.messages);
767
+ await reversionManager.revertTo(messageIdsToRemove, messages);
728
768
  }
729
769
 
730
- // Truncate messages in memory
731
- const newMessages = this.messages.slice(0, index);
732
- this.setMessages(newMessages);
770
+ // Truncate messages in the target session
771
+ const newMessagesInSession = targetSessionData.messages.slice(
772
+ 0,
773
+ targetIndexInSession,
774
+ );
775
+
776
+ // Identify subagent tasks to stop
777
+ for (const message of messagesToRemove) {
778
+ for (const block of message.blocks) {
779
+ if (block.type === "subagent" && block.subagentId) {
780
+ this.callbacks.onSubagentTaskStopRequested?.(block.subagentId);
781
+ }
782
+ }
783
+ }
784
+
785
+ // Update target session file
786
+ this.sessionId = targetSessionId;
787
+ this.rootSessionId = targetSessionData.rootSessionId || targetSessionId;
788
+ this.parentSessionId = targetSessionData.parentSessionId;
789
+ this.transcriptPath = this.computeTranscriptPath();
790
+
791
+ await this.rewriteSessionFile(newMessagesInSession);
733
792
 
734
- // Update persistence: rewrite the session file
735
- await this.rewriteSessionFile(newMessages);
793
+ // Update in-memory messages to the truncated session messages
794
+ // We do NOT include ancestor messages here to avoid exceeding context limits.
795
+ // The 'compress' block at the start of the session (if any) already summarizes them.
796
+ this.setMessages(newMessagesInSession);
736
797
 
737
798
  // Update saved message count
738
- this.savedMessageCount = newMessages.length;
799
+ this.savedMessageCount = newMessagesInSession.length;
800
+
801
+ // Notify session ID change if it changed
802
+ this.callbacks.onSessionIdChange?.(this.sessionId);
739
803
  }
740
804
 
741
805
  /**
@@ -323,40 +323,30 @@ export class PermissionManager {
323
323
  workdir,
324
324
  );
325
325
  if (!isInside) {
326
- this.logger?.warn(
327
- "File operation outside the Safe Zone in acceptEdits mode",
326
+ this.logger?.info(
327
+ "File operation outside the Safe Zone in acceptEdits mode, falling back to manual confirmation",
328
328
  {
329
329
  toolName: context.toolName,
330
330
  targetPath,
331
331
  resolvedPath,
332
332
  },
333
333
  );
334
- return {
335
- behavior: "deny",
336
- message: `Tool '${context.toolName}' attempted to modify a file outside the Safe Zone: ${targetPath}. Operations outside the Safe Zone always require manual confirmation.`,
337
- };
334
+ // Fall through to normal permission check flow to trigger confirmation prompt
335
+ } else {
336
+ this.logger?.debug(
337
+ "Permission automatically accepted for tool in acceptEdits mode",
338
+ {
339
+ toolName: context.toolName,
340
+ },
341
+ );
342
+ return { behavior: "allow" };
338
343
  }
339
344
  }
340
-
341
- this.logger?.debug(
342
- "Permission automatically accepted for tool in acceptEdits mode",
343
- {
344
- toolName: context.toolName,
345
- },
346
- );
347
- return { behavior: "allow" };
348
345
  }
349
346
  }
350
347
 
351
348
  // 1.3 If plan mode, allow Read-only tools and Edit/Write for plan file
352
349
  if (context.permissionMode === "plan") {
353
- if (context.toolName === BASH_TOOL_NAME) {
354
- return {
355
- behavior: "deny",
356
- message: "Bash commands are not allowed in plan mode.",
357
- };
358
- }
359
-
360
350
  const writeTools = [
361
351
  EDIT_TOOL_NAME,
362
352
  MULTI_EDIT_TOOL_NAME,
@@ -492,6 +482,27 @@ export class PermissionManager {
492
482
  suggestedPrefix,
493
483
  };
494
484
 
485
+ // Set hidePersistentOption for out-of-bounds file operations
486
+ const fileTools = [
487
+ EDIT_TOOL_NAME,
488
+ MULTI_EDIT_TOOL_NAME,
489
+ DELETE_FILE_TOOL_NAME,
490
+ WRITE_TOOL_NAME,
491
+ ];
492
+ if (fileTools.includes(toolName)) {
493
+ const targetPath = (toolInput?.file_path || toolInput?.target_file) as
494
+ | string
495
+ | undefined;
496
+ const workdir = toolInput?.workdir as string | undefined;
497
+
498
+ if (targetPath) {
499
+ const { isInside } = this.isInsideSafeZone(targetPath, workdir);
500
+ if (!isInside) {
501
+ context.hidePersistentOption = true;
502
+ }
503
+ }
504
+ }
505
+
495
506
  // Set hidePersistentOption for dangerous or out-of-bounds bash commands
496
507
  if (toolName === BASH_TOOL_NAME && toolInput?.command) {
497
508
  const command = String(toolInput.command);
@@ -17,7 +17,7 @@ export class PlanManager {
17
17
  /**
18
18
  * Ensures the plan directory exists and generates a new plan file path with a random name
19
19
  */
20
- public async getOrGeneratePlanFilePath(): Promise<{
20
+ public async getOrGeneratePlanFilePath(seed?: string): Promise<{
21
21
  path: string;
22
22
  name: string;
23
23
  }> {
@@ -30,7 +30,7 @@ export class PlanManager {
30
30
  );
31
31
  throw error;
32
32
  }
33
- const name = generateRandomName();
33
+ const name = generateRandomName(seed);
34
34
  const filePath = path.join(this.planDir, `${name}.md`);
35
35
  this.logger?.info(`Generated plan file path: ${filePath}`);
36
36
  return { path: filePath, name };
@@ -1,6 +1,7 @@
1
1
  import type { MessageManager } from "./messageManager.js";
2
2
  import type { AIManager } from "./aiManager.js";
3
3
  import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
4
+ import type { TaskManager } from "../services/taskManager.js";
4
5
  import type {
5
6
  SlashCommand,
6
7
  CustomSlashCommand,
@@ -28,6 +29,7 @@ export interface SlashCommandManagerOptions {
28
29
  messageManager: MessageManager;
29
30
  aiManager: AIManager;
30
31
  backgroundTaskManager: BackgroundTaskManager;
32
+ taskManager: TaskManager;
31
33
  workdir: string;
32
34
  logger?: Logger;
33
35
  }
@@ -38,6 +40,7 @@ export class SlashCommandManager {
38
40
  private messageManager: MessageManager;
39
41
  private aiManager: AIManager;
40
42
  private backgroundTaskManager: BackgroundTaskManager;
43
+ private taskManager: TaskManager;
41
44
  private workdir: string;
42
45
  private logger?: Logger;
43
46
 
@@ -45,6 +48,7 @@ export class SlashCommandManager {
45
48
  this.messageManager = options.messageManager;
46
49
  this.aiManager = options.aiManager;
47
50
  this.backgroundTaskManager = options.backgroundTaskManager;
51
+ this.taskManager = options.taskManager;
48
52
  this.workdir = options.workdir;
49
53
  this.logger = options.logger;
50
54
 
@@ -61,8 +65,13 @@ export class SlashCommandManager {
61
65
  handler: () => {
62
66
  // Clear chat messages
63
67
  this.messageManager.clearMessages();
64
- // Clear terminal screen
65
- process.stdout.write("\x1Bc");
68
+
69
+ // Reset task list if WAVE_TASK_LIST_ID is not set
70
+ if (!process.env.WAVE_TASK_LIST_ID) {
71
+ const newTaskListId = this.messageManager.getRootSessionId();
72
+ this.taskManager.setTaskListId(newTaskListId);
73
+ this.taskManager.emit("tasksChange", newTaskListId);
74
+ }
66
75
  },
67
76
  });
68
77