wave-agent-sdk 0.13.6 → 0.14.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 (54) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +4 -2
  3. package/dist/managers/aiManager.d.ts +3 -0
  4. package/dist/managers/aiManager.d.ts.map +1 -1
  5. package/dist/managers/aiManager.js +93 -8
  6. package/dist/managers/messageManager.d.ts +15 -0
  7. package/dist/managers/messageManager.d.ts.map +1 -1
  8. package/dist/managers/messageManager.js +52 -2
  9. package/dist/managers/permissionManager.d.ts +4 -0
  10. package/dist/managers/permissionManager.d.ts.map +1 -1
  11. package/dist/managers/permissionManager.js +6 -0
  12. package/dist/managers/subagentManager.d.ts.map +1 -1
  13. package/dist/managers/subagentManager.js +23 -17
  14. package/dist/prompts/index.d.ts +2 -1
  15. package/dist/prompts/index.d.ts.map +1 -1
  16. package/dist/prompts/index.js +50 -25
  17. package/dist/services/aiService.d.ts.map +1 -1
  18. package/dist/services/aiService.js +11 -1
  19. package/dist/tools/agentTool.d.ts.map +1 -1
  20. package/dist/tools/agentTool.js +14 -2
  21. package/dist/tools/bashTool.d.ts.map +1 -1
  22. package/dist/tools/bashTool.js +27 -5
  23. package/dist/tools/types.d.ts +1 -0
  24. package/dist/tools/types.d.ts.map +1 -1
  25. package/dist/tools/webFetchTool.d.ts.map +1 -1
  26. package/dist/tools/webFetchTool.js +202 -78
  27. package/dist/types/messaging.d.ts +1 -0
  28. package/dist/types/messaging.d.ts.map +1 -1
  29. package/dist/utils/convertMessagesForAPI.js +1 -1
  30. package/dist/utils/groupMessagesByApiRound.d.ts +24 -0
  31. package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -0
  32. package/dist/utils/groupMessagesByApiRound.js +97 -0
  33. package/dist/utils/messageOperations.d.ts +1 -0
  34. package/dist/utils/messageOperations.d.ts.map +1 -1
  35. package/dist/utils/microcompact.d.ts +7 -0
  36. package/dist/utils/microcompact.d.ts.map +1 -0
  37. package/dist/utils/microcompact.js +78 -0
  38. package/package.json +2 -1
  39. package/src/agent.ts +4 -2
  40. package/src/managers/aiManager.ts +117 -15
  41. package/src/managers/messageManager.ts +64 -2
  42. package/src/managers/permissionManager.ts +7 -0
  43. package/src/managers/subagentManager.ts +28 -24
  44. package/src/prompts/index.ts +51 -25
  45. package/src/services/aiService.ts +14 -1
  46. package/src/tools/agentTool.ts +14 -2
  47. package/src/tools/bashTool.ts +27 -5
  48. package/src/tools/types.ts +1 -0
  49. package/src/tools/webFetchTool.ts +276 -86
  50. package/src/types/messaging.ts +1 -0
  51. package/src/utils/convertMessagesForAPI.ts +1 -1
  52. package/src/utils/groupMessagesByApiRound.ts +120 -0
  53. package/src/utils/messageOperations.ts +1 -0
  54. package/src/utils/microcompact.ts +101 -0
@@ -0,0 +1,24 @@
1
+ import type { Message } from "../types/index.js";
2
+ export interface ApiRound {
3
+ messages: Message[];
4
+ estimatedTokens: number;
5
+ }
6
+ /**
7
+ * Groups messages into "API rounds" — each round corresponds to one API
8
+ * call-response cycle. This is critical because in agentic sessions with a
9
+ * single user prompt, Wave creates a new Message per API round (each recursive
10
+ * sendAIMessage call creates a new assistant message).
11
+ *
12
+ * Boundaries:
13
+ * - A new `role: "user"` message starts a new round.
14
+ * - A new `role: "assistant"` message with a different `id` starts a new round.
15
+ * - A message with a `compress` block is pushed as its own round and starts a
16
+ * new round after it.
17
+ */
18
+ export declare function groupMessagesByApiRound(messages: Message[]): ApiRound[];
19
+ /**
20
+ * Returns the last `roundCount` complete API rounds as a flat message array.
21
+ * Never splits a tool_use/tool_result pair. If fewer rounds exist, returns all.
22
+ */
23
+ export declare function getLastApiRounds(messages: Message[], roundCount: number): Message[];
24
+ //# sourceMappingURL=groupMessagesByApiRound.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groupMessagesByApiRound.d.ts","sourceRoot":"","sources":["../../src/utils/groupMessagesByApiRound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,QAAQ,EAAE,CA8DvE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,EAAE,CAIX"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Groups messages into "API rounds" — each round corresponds to one API
3
+ * call-response cycle. This is critical because in agentic sessions with a
4
+ * single user prompt, Wave creates a new Message per API round (each recursive
5
+ * sendAIMessage call creates a new assistant message).
6
+ *
7
+ * Boundaries:
8
+ * - A new `role: "user"` message starts a new round.
9
+ * - A new `role: "assistant"` message with a different `id` starts a new round.
10
+ * - A message with a `compress` block is pushed as its own round and starts a
11
+ * new round after it.
12
+ */
13
+ export function groupMessagesByApiRound(messages) {
14
+ const rounds = [];
15
+ let currentRound = [];
16
+ let lastAssistantId;
17
+ for (const msg of messages) {
18
+ let startNewRound = false;
19
+ if (msg.role === "user") {
20
+ startNewRound = true;
21
+ }
22
+ else if (msg.role === "assistant") {
23
+ // Compress block is always its own round
24
+ const hasCompress = msg.blocks.some((b) => b.type === "compress");
25
+ if (hasCompress) {
26
+ startNewRound = true;
27
+ }
28
+ else if (msg.id !== lastAssistantId) {
29
+ // New assistant id starts a new round.
30
+ // Exception: if the current round is [user] (first assistant after a
31
+ // user prompt in a normal conversation), keep them together as one
32
+ // round. But if we already have assistant(s) in this round (agentic
33
+ // tool loop), the new id starts a new round.
34
+ const roundHasOtherAssistant = currentRound.some((m) => m.role === "assistant" && m.id !== msg.id);
35
+ if (roundHasOtherAssistant) {
36
+ startNewRound = true;
37
+ }
38
+ }
39
+ lastAssistantId = msg.id;
40
+ }
41
+ if (startNewRound && currentRound.length > 0) {
42
+ rounds.push({
43
+ messages: currentRound,
44
+ estimatedTokens: estimateTokens(currentRound),
45
+ });
46
+ currentRound = [];
47
+ }
48
+ currentRound.push(msg);
49
+ // After pushing a compress message as its own round, flush immediately
50
+ if (msg.role === "assistant" &&
51
+ msg.blocks.some((b) => b.type === "compress")) {
52
+ rounds.push({
53
+ messages: currentRound,
54
+ estimatedTokens: estimateTokens(currentRound),
55
+ });
56
+ currentRound = [];
57
+ }
58
+ }
59
+ if (currentRound.length > 0) {
60
+ rounds.push({
61
+ messages: currentRound,
62
+ estimatedTokens: estimateTokens(currentRound),
63
+ });
64
+ }
65
+ return rounds;
66
+ }
67
+ /**
68
+ * Returns the last `roundCount` complete API rounds as a flat message array.
69
+ * Never splits a tool_use/tool_result pair. If fewer rounds exist, returns all.
70
+ */
71
+ export function getLastApiRounds(messages, roundCount) {
72
+ const rounds = groupMessagesByApiRound(messages);
73
+ const lastRounds = rounds.slice(-roundCount);
74
+ return lastRounds.flatMap((r) => r.messages);
75
+ }
76
+ /**
77
+ * Roughly estimate token count from character count (~4 chars per token).
78
+ */
79
+ function estimateTokens(messages) {
80
+ let chars = 0;
81
+ for (const msg of messages) {
82
+ for (const block of msg.blocks) {
83
+ if ("content" in block && typeof block.content === "string") {
84
+ chars += block.content.length;
85
+ }
86
+ if (block.type === "tool" &&
87
+ block.parameters &&
88
+ typeof block.parameters === "string") {
89
+ chars += block.parameters.length;
90
+ }
91
+ if (block.type === "tool" && block.result) {
92
+ chars += block.result.length;
93
+ }
94
+ }
95
+ }
96
+ return Math.ceil(chars / 4);
97
+ }
@@ -41,6 +41,7 @@ export interface UpdateToolBlockParams {
41
41
  compactParams?: string;
42
42
  parametersChunk?: string;
43
43
  isManuallyBackgrounded?: boolean;
44
+ timestamp?: number;
44
45
  }
45
46
  export type AgentToolBlockUpdateParams = Omit<UpdateToolBlockParams, "messages">;
46
47
  export interface AddErrorBlockParams {
@@ -1 +1 @@
1
- {"version":3,"file":"messageOperations.d.ts","sourceRoot":"","sources":["../../src/utils/messageOperations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"messageOperations.d.ts","sourceRoot":"","sources":["../../src/utils/messageOperations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Message } from "../types/messaging.js";
2
+ export interface MicrocompactOptions {
3
+ timeThresholdMS: number;
4
+ recentResultsToKeep: number;
5
+ }
6
+ export declare function microcompactMessages(messages: Message[], options: MicrocompactOptions): Message[];
7
+ //# sourceMappingURL=microcompact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microcompact.d.ts","sourceRoot":"","sources":["../../src/utils/microcompact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,uBAAuB,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,EAAE,CAwFX"}
@@ -0,0 +1,78 @@
1
+ const CLEARED_RESULT = "[Old tool result content cleared]";
2
+ export function microcompactMessages(messages, options) {
3
+ const { timeThresholdMS, recentResultsToKeep } = options;
4
+ // 1. Find the latest tool block timestamp across all assistant messages
5
+ let lastAssistantTime = 0;
6
+ for (const msg of messages) {
7
+ if (msg.role === "assistant") {
8
+ for (const block of msg.blocks) {
9
+ if (block.type === "tool" && block.stage === "end" && block.timestamp) {
10
+ if (block.timestamp > lastAssistantTime) {
11
+ lastAssistantTime = block.timestamp;
12
+ }
13
+ }
14
+ }
15
+ }
16
+ }
17
+ // 2. If no prior assistant messages with completed tools, return unchanged
18
+ if (lastAssistantTime === 0) {
19
+ return messages;
20
+ }
21
+ // 3. If within threshold, return unchanged
22
+ if (Date.now() - lastAssistantTime < timeThresholdMS) {
23
+ return messages;
24
+ }
25
+ const toolRefs = [];
26
+ for (let mi = 0; mi < messages.length; mi++) {
27
+ const msg = messages[mi];
28
+ if (msg.role === "assistant") {
29
+ for (let bi = 0; bi < msg.blocks.length; bi++) {
30
+ const block = msg.blocks[bi];
31
+ if (block.type === "tool" && block.stage === "end" && block.timestamp) {
32
+ toolRefs.push({
33
+ msgIndex: mi,
34
+ blockIndex: bi,
35
+ timestamp: block.timestamp,
36
+ });
37
+ }
38
+ }
39
+ }
40
+ }
41
+ toolRefs.sort((a, b) => b.timestamp - a.timestamp);
42
+ // 5. Mark the top N as "keep"
43
+ const keepSet = new Set();
44
+ for (let i = 0; i < Math.min(recentResultsToKeep, toolRefs.length); i++) {
45
+ const ref = toolRefs[i];
46
+ keepSet.add(`${ref.msgIndex}:${ref.blockIndex}`);
47
+ }
48
+ // 6. Deep-copy messages and clear result + shortResult on non-kept blocks
49
+ const result = messages.map((msg) => ({
50
+ ...msg,
51
+ blocks: msg.blocks.map((block) => {
52
+ if (block.type === "tool" && block.stage === "end" && block.timestamp) {
53
+ return { ...block };
54
+ }
55
+ return block;
56
+ }),
57
+ }));
58
+ // Clear non-kept tool blocks
59
+ for (const ref of toolRefs) {
60
+ const key = `${ref.msgIndex}:${ref.blockIndex}`;
61
+ if (!keepSet.has(key)) {
62
+ result[ref.msgIndex] = {
63
+ ...result[ref.msgIndex],
64
+ blocks: result[ref.msgIndex].blocks.map((b, idx) => {
65
+ if (idx === ref.blockIndex && b.type === "tool") {
66
+ return {
67
+ ...b,
68
+ result: CLEARED_RESULT,
69
+ shortResult: undefined,
70
+ };
71
+ }
72
+ return b;
73
+ }),
74
+ };
75
+ }
76
+ }
77
+ return result;
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.13.6",
3
+ "version": "0.14.0",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -33,6 +33,7 @@
33
33
  "cron-parser": "^5.5.0",
34
34
  "fuzzysort": "^3.1.0",
35
35
  "glob": "^13.0.0",
36
+ "lru-cache": "^11.3.5",
36
37
  "minimatch": "^10.0.3",
37
38
  "openai": "^5.12.2",
38
39
  "turndown": "^7.2.2"
package/src/agent.ts CHANGED
@@ -579,11 +579,13 @@ export class Agent {
579
579
 
580
580
  /** Unified interrupt method, interrupts both AI messages and command execution */
581
581
  public abortMessage(): void {
582
+ // Clear queue first to prevent processQueuedMessage from dequeuing
583
+ // when abortAIMessage triggers onLoadingChange(false)
584
+ this.messageQueue.clear();
585
+ this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
582
586
  this.abortAIMessage(); // This will abort tools including Agent tool (subagents)
583
587
  this.abortBashCommand();
584
588
  this.abortSlashCommand();
585
- this.messageQueue.clear();
586
- this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
587
589
  }
588
590
 
589
591
  /** Interrupt bash command execution */
@@ -1,6 +1,7 @@
1
1
  import { type CallAgentOptions } from "../services/aiService.js";
2
2
  import * as aiService from "../services/aiService.js";
3
3
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
4
+ import { microcompactMessages } from "../utils/microcompact.js";
4
5
  import { parseTaskNotificationXml } from "../utils/notificationXml.js";
5
6
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
6
7
  import * as fs from "node:fs/promises";
@@ -15,7 +16,6 @@ import type { ToolManager } from "./toolManager.js";
15
16
  import type { ToolContext, ToolResult } from "../tools/types.js";
16
17
  import type { MessageManager } from "./messageManager.js";
17
18
  import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
18
- import type { NotificationQueue } from "./notificationQueue.js";
19
19
  import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
20
20
  import type { HookManager } from "./hookManager.js";
21
21
  import type { ExtendedHookExecutionContext } from "../types/hooks.js";
@@ -25,6 +25,7 @@ import type { SkillManager } from "./skillManager.js";
25
25
  import { buildSystemPrompt } from "../prompts/index.js";
26
26
  import { Container } from "../utils/container.js";
27
27
  import { ConfigurationService } from "../services/configurationService.js";
28
+ import type { NotificationQueue } from "./notificationQueue.js";
28
29
 
29
30
  import { logger } from "../utils/globalLogger.js";
30
31
 
@@ -51,11 +52,13 @@ export class AIManager {
51
52
  onLoadingChange?: (loading: boolean) => void;
52
53
  private toolAbortController: AbortController | null = null;
53
54
  private workdir: string;
55
+ private originalWorkdir: string;
54
56
  private systemPrompt?: string;
55
57
  private subagentType?: string; // Store subagent type for hook context
56
58
  private stream: boolean; // Streaming mode flag
57
59
  private modelOverride?: string;
58
60
  private _onCwdChange?: (newCwd: string) => void; // Store callback for CWD changes
61
+ private consecutiveCompressionFailures: number = 0;
59
62
 
60
63
  // Service overrides
61
64
  constructor(
@@ -63,6 +66,7 @@ export class AIManager {
63
66
  options: AIManagerOptions,
64
67
  ) {
65
68
  this.workdir = options.workdir;
69
+ this.originalWorkdir = options.workdir;
66
70
  this.systemPrompt = options.systemPrompt;
67
71
  this.subagentType = options.subagentType; // Store subagent type
68
72
  this.stream = options.stream ?? true; // Default to true if not specified
@@ -165,6 +169,10 @@ export class AIManager {
165
169
  return this.workdir;
166
170
  }
167
171
 
172
+ public getOriginalWorkdir(): string {
173
+ return this.originalWorkdir;
174
+ }
175
+
168
176
  public setOnCwdChange(callback: (newCwd: string) => void): void {
169
177
  this._onCwdChange = callback;
170
178
  }
@@ -234,6 +242,7 @@ export class AIManager {
234
242
  if (toolPlugin?.formatCompactParams) {
235
243
  const context: ToolContext = {
236
244
  workdir: this.workdir,
245
+ originalWorkdir: this.originalWorkdir,
237
246
  taskManager: this.taskManager,
238
247
  };
239
248
  return toolPlugin.formatCompactParams(toolArgs, context);
@@ -248,7 +257,6 @@ export class AIManager {
248
257
  private async handleTokenUsageAndCompression(
249
258
  usage: Usage | undefined,
250
259
  abortController: AbortController,
251
- model?: string,
252
260
  ): Promise<void> {
253
261
  if (!usage) return;
254
262
 
@@ -272,6 +280,14 @@ export class AIManager {
272
280
 
273
281
  // If there are messages to compress, perform compression
274
282
  if (messagesToCompress.length > 0) {
283
+ // Circuit breaker: skip compression after 3 consecutive failures
284
+ if (this.consecutiveCompressionFailures >= 3) {
285
+ logger?.warn(
286
+ `Skipping compression: ${this.consecutiveCompressionFailures} consecutive failures`,
287
+ );
288
+ return;
289
+ }
290
+
275
291
  const recentChatMessages = convertMessagesForAPI(messagesToCompress);
276
292
 
277
293
  // Save session before compression to preserve original messages
@@ -284,7 +300,7 @@ export class AIManager {
284
300
  modelConfig: this.getModelConfig(),
285
301
  messages: recentChatMessages,
286
302
  abortSignal: abortController.signal,
287
- model: model,
303
+ model: this.getModelConfig().fastModel,
288
304
  });
289
305
 
290
306
  // Handle usage tracking for compression operations
@@ -294,14 +310,91 @@ export class AIManager {
294
310
  prompt_tokens: compressionResult.usage.prompt_tokens,
295
311
  completion_tokens: compressionResult.usage.completion_tokens,
296
312
  total_tokens: compressionResult.usage.total_tokens,
297
- model: model || this.getModelConfig().model,
313
+ model: this.getModelConfig().fastModel,
298
314
  operation_type: "compress",
299
315
  };
300
316
  }
301
317
 
318
+ // Build post-compact context restoration
319
+ const POST_COMPACT_TOKEN_BUDGET = 50_000;
320
+ const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000;
321
+ const POST_COMPACT_MAX_FILES_TO_RESTORE = 5;
322
+ const contextParts: string[] = [];
323
+
324
+ // 1. File context restoration
325
+ const recentFiles = this.messageManager.getRecentFileReads(
326
+ POST_COMPACT_MAX_FILES_TO_RESTORE,
327
+ POST_COMPACT_MAX_TOKENS_PER_FILE,
328
+ );
329
+ let usedTokens = 0;
330
+ for (const file of recentFiles) {
331
+ const fileTokens = Math.ceil(file.content.length / 4);
332
+ if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE)
333
+ continue;
334
+ if (fileTokens > 0) usedTokens += fileTokens;
335
+ contextParts.push(
336
+ `\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``,
337
+ );
338
+ if (contextParts.length >= POST_COMPACT_MAX_FILES_TO_RESTORE) break;
339
+ if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
340
+ }
341
+
342
+ // 2. Working directory
343
+ contextParts.push(
344
+ `\n\n[Working Directory]\nCurrent working directory: ${this.workdir}`,
345
+ );
346
+
347
+ // 3. Plan mode context
348
+ const currentMode = this.permissionManager?.getCurrentEffectiveMode(
349
+ this.getModelConfig().permissionMode,
350
+ );
351
+ if (currentMode === "plan") {
352
+ const planFilePath = this.permissionManager?.getPlanFilePath();
353
+ if (planFilePath) {
354
+ let planExists = false;
355
+ try {
356
+ await fs.access(planFilePath);
357
+ planExists = true;
358
+ } catch {
359
+ // Plan file doesn't exist yet
360
+ }
361
+ contextParts.push(
362
+ `\n\n[Plan Mode]\nYou are in plan mode. Plan file: ${planFilePath} (exists: ${planExists})`,
363
+ );
364
+ }
365
+ }
366
+
367
+ // 4. Skills context
368
+ const skills =
369
+ this.skillManager
370
+ ?.getAvailableSkills()
371
+ .filter((s) => !s.disableModelInvocation) || [];
372
+ if (skills.length > 0) {
373
+ const skillList = skills
374
+ .map((s) => `- ${s.name}: ${s.description || ""}`)
375
+ .join("\n");
376
+ contextParts.push(`\n\n[Available Skills]\n${skillList}`);
377
+ }
378
+
379
+ // 5. Background agents status
380
+ const agents = this.backgroundTaskManager?.getAllTasks() || [];
381
+ if (agents.length > 0) {
382
+ const agentList = agents
383
+ .map((a) => `- Agent "${a.description}": ${a.status}`)
384
+ .join("\n");
385
+ contextParts.push(`\n\n[Background Tasks]\n${agentList}`);
386
+ }
387
+
388
+ // Merge context restoration into summary
389
+ const enhancedSummary =
390
+ compressionResult.content +
391
+ (contextParts.length > 0
392
+ ? `\n\n[Context Restoration]` + contextParts.join("")
393
+ : "");
394
+
302
395
  // Execute message reconstruction and sessionId update after compression
303
396
  this.messageManager.compressMessagesAndUpdateSession(
304
- compressionResult.content,
397
+ enhancedSummary,
305
398
  compressionUsage,
306
399
  );
307
400
 
@@ -313,8 +406,13 @@ export class AIManager {
313
406
  logger?.debug(
314
407
  `Successfully compressed ${messagesToCompress.length} messages and updated session`,
315
408
  );
409
+ this.consecutiveCompressionFailures = 0;
316
410
  } catch (compressError) {
317
- logger?.error("Failed to compress messages:", compressError);
411
+ this.consecutiveCompressionFailures++;
412
+ logger?.error(
413
+ `Failed to compress messages (${this.consecutiveCompressionFailures} consecutive):`,
414
+ compressError,
415
+ );
318
416
  this.messageManager.addErrorBlock(
319
417
  `Failed to compress conversation history: ${compressError instanceof Error ? compressError.message : String(compressError)}. You may encounter context limit issues.`,
320
418
  );
@@ -403,10 +501,13 @@ export class AIManager {
403
501
  toolAbortController = this.toolAbortController!;
404
502
  }
405
503
 
406
- // Get recent message history
407
- const recentMessages = convertMessagesForAPI(
408
- this.messageManager.getMessages(),
409
- );
504
+ // Get recent message history with microcompact applied
505
+ const rawMessages = this.messageManager.getMessages();
506
+ const microcompactedMessages = microcompactMessages(rawMessages, {
507
+ timeThresholdMS: 30 * 60 * 1000, // 30 minutes
508
+ recentResultsToKeep: 3,
509
+ });
510
+ const recentMessages = convertMessagesForAPI(microcompactedMessages);
410
511
 
411
512
  try {
412
513
  // Get combined memory content
@@ -472,6 +573,7 @@ export class AIManager {
472
573
  filteredToolPlugins,
473
574
  {
474
575
  workdir: this.workdir,
576
+ originalWorkdir: this.originalWorkdir,
475
577
  memory: combinedMemory,
476
578
  language: this.getLanguage(),
477
579
  isSubagent: !!this.subagentType,
@@ -658,6 +760,7 @@ export class AIManager {
658
760
  stage: "end",
659
761
  name: toolName,
660
762
  compactParams: "",
763
+ timestamp: Date.now(),
661
764
  });
662
765
  return;
663
766
  }
@@ -710,6 +813,7 @@ export class AIManager {
710
813
  abortSignal: toolAbortController.signal,
711
814
  backgroundTaskManager: this.backgroundTaskManager,
712
815
  workdir: this.workdir,
816
+ originalWorkdir: this.originalWorkdir,
713
817
  messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
714
818
  sessionId: this.messageManager.getSessionId(),
715
819
  toolCallId: toolId,
@@ -774,6 +878,7 @@ export class AIManager {
774
878
  shortResult: toolResult.shortResult,
775
879
  isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
776
880
  startLineNumber: toolResult.startLineNumber,
881
+ timestamp: Date.now(),
777
882
  });
778
883
 
779
884
  // Execute PostToolUse hooks after successful tool completion
@@ -799,6 +904,7 @@ export class AIManager {
799
904
  name: toolName,
800
905
  compactParams,
801
906
  isManuallyBackgrounded: false,
907
+ timestamp: Date.now(),
802
908
  });
803
909
  }
804
910
  },
@@ -809,11 +915,7 @@ export class AIManager {
809
915
  }
810
916
 
811
917
  // Handle token statistics and message compression
812
- await this.handleTokenUsageAndCompression(
813
- result.usage,
814
- abortController,
815
- model,
816
- );
918
+ await this.handleTokenUsageAndCompression(result.usage, abortController);
817
919
 
818
920
  // Finalize text/reasoning blocks for the final response (no tools)
819
921
  this.messageManager.finalizeStreamingBlocks();
@@ -16,6 +16,7 @@ import {
16
16
  generateMessageId,
17
17
  } from "../utils/messageOperations.js";
18
18
  import type { Message, Usage } from "../types/index.js";
19
+ import { getLastApiRounds } from "../utils/groupMessagesByApiRound.js";
19
20
  import { join, isAbsolute, relative } from "path";
20
21
  import {
21
22
  appendMessages,
@@ -89,6 +90,8 @@ export class MessageManager {
89
90
  private transcriptPath: string; // Cached transcript path
90
91
  private savedMessageCount: number; // Track how many messages have been saved to prevent duplication
91
92
  private filesInContext: Set<string> = new Set(); // Track files mentioned in the conversation
93
+ private recentFileReads: Map<string, { content: string; timestamp: number }> =
94
+ new Map(); // Track file read contents
92
95
  private sessionType: "main" | "subagent";
93
96
  private subagentType?: string;
94
97
  private _usages: Usage[] = [];
@@ -266,11 +269,13 @@ export class MessageManager {
266
269
  const newMessages = messages.slice(oldLength);
267
270
  for (const message of newMessages) {
268
271
  this.addPathsFromMessage(message);
272
+ this.extractFileReadsFromMessage(message);
269
273
  }
270
274
 
271
275
  // Also check if the last message was updated (common for tool blocks)
272
276
  if (messages.length > 0 && messages.length === oldLength) {
273
277
  this.addPathsFromMessage(messages[messages.length - 1]);
278
+ this.extractFileReadsFromMessage(messages[messages.length - 1]);
274
279
  }
275
280
 
276
281
  this.callbacks.onMessagesChange?.([...messages]);
@@ -495,8 +500,8 @@ export class MessageManager {
495
500
  compressedContent: string,
496
501
  usage?: Usage,
497
502
  ): void {
498
- // Get last 3 messages to preserve
499
- const lastThreeMessages = this.messages.slice(-3);
503
+ // Get last 2 API rounds to preserve (structurally safe boundary)
504
+ const lastThreeMessages = getLastApiRounds(this.messages, 2);
500
505
 
501
506
  // Create compressed message
502
507
  const compressMessage: Message = {
@@ -994,4 +999,61 @@ export class MessageManager {
994
999
 
995
1000
  return paths;
996
1001
  }
1002
+
1003
+ /**
1004
+ * Extract file read contents from tool result blocks in a message.
1005
+ */
1006
+ private extractFileReadsFromMessage(message: Message): void {
1007
+ for (const block of message.blocks) {
1008
+ if (
1009
+ block.type === "tool" &&
1010
+ block.name === "read" &&
1011
+ block.stage === "end" &&
1012
+ block.result &&
1013
+ block.parameters
1014
+ ) {
1015
+ let filePath: string | undefined;
1016
+ try {
1017
+ const params = JSON.parse(block.parameters) as Record<
1018
+ string,
1019
+ unknown
1020
+ >;
1021
+ filePath = params.file_path as string | undefined;
1022
+ } catch {
1023
+ // Ignore parse errors
1024
+ }
1025
+ if (filePath) {
1026
+ this.recentFileReads.set(filePath, {
1027
+ content: block.result,
1028
+ timestamp: Date.now(),
1029
+ });
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ /**
1036
+ * Get recent file read contents, sorted by timestamp (newest first).
1037
+ * @param maxFiles - Maximum number of files to return
1038
+ * @param maxTokensPerFile - Maximum tokens per file (~4 chars/token)
1039
+ * @returns Array of { path, content } sorted by recency
1040
+ */
1041
+ public getRecentFileReads(
1042
+ maxFiles = 5,
1043
+ maxTokensPerFile = 5000,
1044
+ ): Array<{ path: string; content: string }> {
1045
+ const sorted = Array.from(this.recentFileReads.entries())
1046
+ .sort(([, a], [, b]) => b.timestamp - a.timestamp)
1047
+ .slice(0, maxFiles);
1048
+
1049
+ const result: Array<{ path: string; content: string }> = [];
1050
+ for (const [path, { content }] of sorted) {
1051
+ const truncated =
1052
+ content.length > maxTokensPerFile * 4
1053
+ ? content.slice(0, maxTokensPerFile * 4)
1054
+ : content;
1055
+ result.push({ path, content: truncated });
1056
+ }
1057
+ return result;
1058
+ }
997
1059
  }
@@ -315,6 +315,13 @@ export class PermissionManager {
315
315
  return this.planFilePath;
316
316
  }
317
317
 
318
+ /**
319
+ * Public wrapper for isInsideSafeZone to check if a path is in the safe zone
320
+ */
321
+ public isPathInSafeZone(targetPath: string): boolean {
322
+ return this.isInsideSafeZone(targetPath).isInside;
323
+ }
324
+
318
325
  /**
319
326
  * Check if a path is inside the Safe Zone (workdir + additionalDirectories)
320
327
  */