wave-agent-sdk 0.13.6 → 0.14.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 (85) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +4 -2
  3. package/dist/core/plugin.d.ts +2 -2
  4. package/dist/core/plugin.d.ts.map +1 -1
  5. package/dist/core/plugin.js +7 -7
  6. package/dist/managers/aiManager.d.ts +3 -0
  7. package/dist/managers/aiManager.d.ts.map +1 -1
  8. package/dist/managers/aiManager.js +93 -8
  9. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  10. package/dist/managers/backgroundTaskManager.js +0 -12
  11. package/dist/managers/messageManager.d.ts +15 -0
  12. package/dist/managers/messageManager.d.ts.map +1 -1
  13. package/dist/managers/messageManager.js +52 -2
  14. package/dist/managers/permissionManager.d.ts +4 -0
  15. package/dist/managers/permissionManager.d.ts.map +1 -1
  16. package/dist/managers/permissionManager.js +6 -0
  17. package/dist/managers/pluginManager.d.ts.map +1 -1
  18. package/dist/managers/pluginManager.js +1 -1
  19. package/dist/managers/subagentManager.d.ts.map +1 -1
  20. package/dist/managers/subagentManager.js +23 -17
  21. package/dist/prompts/index.d.ts +2 -1
  22. package/dist/prompts/index.d.ts.map +1 -1
  23. package/dist/prompts/index.js +50 -25
  24. package/dist/services/MarketplaceService.d.ts +53 -12
  25. package/dist/services/MarketplaceService.d.ts.map +1 -1
  26. package/dist/services/MarketplaceService.js +311 -123
  27. package/dist/services/aiService.d.ts.map +1 -1
  28. package/dist/services/aiService.js +11 -1
  29. package/dist/services/configurationService.d.ts +17 -1
  30. package/dist/services/configurationService.d.ts.map +1 -1
  31. package/dist/services/configurationService.js +104 -0
  32. package/dist/services/pluginLoader.d.ts +6 -0
  33. package/dist/services/pluginLoader.d.ts.map +1 -1
  34. package/dist/services/pluginLoader.js +52 -7
  35. package/dist/tools/agentTool.d.ts.map +1 -1
  36. package/dist/tools/agentTool.js +14 -2
  37. package/dist/tools/bashTool.d.ts.map +1 -1
  38. package/dist/tools/bashTool.js +27 -5
  39. package/dist/tools/types.d.ts +1 -0
  40. package/dist/tools/types.d.ts.map +1 -1
  41. package/dist/tools/webFetchTool.d.ts.map +1 -1
  42. package/dist/tools/webFetchTool.js +202 -78
  43. package/dist/types/configuration.d.ts +7 -0
  44. package/dist/types/configuration.d.ts.map +1 -1
  45. package/dist/types/marketplace.d.ts +28 -1
  46. package/dist/types/marketplace.d.ts.map +1 -1
  47. package/dist/types/messaging.d.ts +1 -0
  48. package/dist/types/messaging.d.ts.map +1 -1
  49. package/dist/types/plugins.d.ts +13 -1
  50. package/dist/types/plugins.d.ts.map +1 -1
  51. package/dist/utils/convertMessagesForAPI.js +1 -1
  52. package/dist/utils/groupMessagesByApiRound.d.ts +24 -0
  53. package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -0
  54. package/dist/utils/groupMessagesByApiRound.js +97 -0
  55. package/dist/utils/messageOperations.d.ts +1 -0
  56. package/dist/utils/messageOperations.d.ts.map +1 -1
  57. package/dist/utils/microcompact.d.ts +7 -0
  58. package/dist/utils/microcompact.d.ts.map +1 -0
  59. package/dist/utils/microcompact.js +78 -0
  60. package/package.json +2 -1
  61. package/src/agent.ts +4 -2
  62. package/src/core/plugin.ts +13 -7
  63. package/src/managers/aiManager.ts +117 -15
  64. package/src/managers/backgroundTaskManager.ts +1 -20
  65. package/src/managers/messageManager.ts +64 -2
  66. package/src/managers/permissionManager.ts +7 -0
  67. package/src/managers/pluginManager.ts +4 -1
  68. package/src/managers/subagentManager.ts +28 -24
  69. package/src/prompts/index.ts +51 -25
  70. package/src/services/MarketplaceService.ts +425 -134
  71. package/src/services/aiService.ts +14 -1
  72. package/src/services/configurationService.ts +131 -0
  73. package/src/services/pluginLoader.ts +66 -7
  74. package/src/tools/agentTool.ts +14 -2
  75. package/src/tools/bashTool.ts +27 -5
  76. package/src/tools/types.ts +1 -0
  77. package/src/tools/webFetchTool.ts +276 -86
  78. package/src/types/configuration.ts +8 -0
  79. package/src/types/marketplace.ts +26 -1
  80. package/src/types/messaging.ts +1 -0
  81. package/src/types/plugins.ts +13 -1
  82. package/src/utils/convertMessagesForAPI.ts +1 -1
  83. package/src/utils/groupMessagesByApiRound.ts +120 -0
  84. package/src/utils/messageOperations.ts +1 -0
  85. package/src/utils/microcompact.ts +101 -0
@@ -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.1",
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 */
@@ -30,7 +30,10 @@ export class PluginCore {
30
30
  this.workdir = workdir;
31
31
  this.container = new Container();
32
32
  this.configurationService = new ConfigurationService();
33
- this.marketplaceService = new MarketplaceService();
33
+ this.marketplaceService = new MarketplaceService(
34
+ this.workdir,
35
+ this.configurationService,
36
+ );
34
37
 
35
38
  // Wire up ConfigurationService in the container for PluginManager to use
36
39
  this.container.register("ConfigurationService", this.configurationService);
@@ -122,7 +125,7 @@ export class PluginCore {
122
125
  for (const m of marketplaces) {
123
126
  try {
124
127
  const manifest = await this.marketplaceService.loadMarketplaceManifest(
125
- this.marketplaceService.getMarketplacePath(m),
128
+ this.marketplaceService.getMarketplacePath(m.source),
126
129
  );
127
130
  manifest.plugins.forEach((p) => {
128
131
  const pluginId = `${p.name}@${m.name}`;
@@ -154,15 +157,18 @@ export class PluginCore {
154
157
  /**
155
158
  * Adds a new marketplace
156
159
  */
157
- async addMarketplace(input: string): Promise<KnownMarketplace> {
158
- return await this.marketplaceService.addMarketplace(input);
160
+ async addMarketplace(
161
+ input: string,
162
+ scope: Scope = "user",
163
+ ): Promise<KnownMarketplace> {
164
+ return await this.marketplaceService.addMarketplace(input, scope);
159
165
  }
160
166
 
161
167
  /**
162
168
  * Removes a marketplace by name
163
169
  */
164
- async removeMarketplace(name: string): Promise<void> {
165
- await this.marketplaceService.removeMarketplace(name);
170
+ async removeMarketplace(name: string, scope?: Scope): Promise<void> {
171
+ await this.marketplaceService.removeMarketplace(name, scope);
166
172
  }
167
173
 
168
174
  /**
@@ -208,7 +214,7 @@ export class PluginCore {
208
214
  * Resolves the local path for a marketplace
209
215
  */
210
216
  getMarketplacePath(marketplace: KnownMarketplace): string {
211
- return this.marketplaceService.getMarketplacePath(marketplace);
217
+ return this.marketplaceService.getMarketplacePath(marketplace.source);
212
218
  }
213
219
 
214
220
  /**
@@ -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();
@@ -2,11 +2,7 @@ import { spawn, type ChildProcess } from "child_process";
2
2
  import * as os from "os";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
- import {
6
- BackgroundTask,
7
- BackgroundShell,
8
- BackgroundSubagent,
9
- } from "../types/processes.js";
5
+ import { BackgroundTask, BackgroundShell } from "../types/processes.js";
10
6
  import { stripAnsiColors } from "../utils/stringUtils.js";
11
7
  import { logger } from "../utils/globalLogger.js";
12
8
  import { Container } from "../utils/container.js";
@@ -427,21 +423,6 @@ export class BackgroundTaskManager {
427
423
  task.runtime = task.endTime - task.startTime;
428
424
  this.notifyTasksChange();
429
425
 
430
- // Enqueue killed notification
431
- const notificationQueue = this.container.has("NotificationQueue")
432
- ? this.container.get<NotificationQueue>("NotificationQueue")
433
- : undefined;
434
- if (notificationQueue) {
435
- const description = (task as BackgroundSubagent).description || "";
436
- const command = (task as BackgroundShell).command || "";
437
- const summary =
438
- task.type === "subagent"
439
- ? `Agent task "${description}" was stopped`
440
- : `Command "${command}" was stopped`;
441
- notificationQueue.enqueue(
442
- `<task-notification>\n<task-id>${id}</task-id>\n<task-type>${task.type}</task-type>\n<status>killed</status>\n<summary>${summary}</summary>\n</task-notification>`,
443
- );
444
- }
445
426
  return true;
446
427
  }
447
428
 
@@ -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
  */
@@ -72,7 +72,10 @@ export class PluginManager {
72
72
  );
73
73
  }
74
74
 
75
- const marketplaceService = new MarketplaceService();
75
+ const marketplaceService = new MarketplaceService(
76
+ this.workdir,
77
+ this.configurationService,
78
+ );
76
79
 
77
80
  // Trigger auto-update for marketplaces in the background
78
81
  if (!process.env.VITEST) {