wave-agent-sdk 0.16.12 → 0.17.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 (98) hide show
  1. package/builtin/skills/settings/MCP.md +49 -4
  2. package/builtin/skills/settings/PERMISSIONS.md +31 -0
  3. package/dist/managers/aiManager.d.ts +19 -0
  4. package/dist/managers/aiManager.d.ts.map +1 -1
  5. package/dist/managers/aiManager.js +338 -210
  6. package/dist/managers/backgroundTaskManager.js +1 -1
  7. package/dist/managers/bangManager.js +1 -1
  8. package/dist/managers/hookManager.d.ts +22 -0
  9. package/dist/managers/hookManager.d.ts.map +1 -1
  10. package/dist/managers/hookManager.js +97 -18
  11. package/dist/managers/mcpManager.d.ts.map +1 -1
  12. package/dist/managers/mcpManager.js +53 -41
  13. package/dist/managers/messageManager.d.ts +4 -0
  14. package/dist/managers/messageManager.d.ts.map +1 -1
  15. package/dist/managers/messageManager.js +9 -0
  16. package/dist/managers/permissionManager.d.ts +6 -0
  17. package/dist/managers/permissionManager.d.ts.map +1 -1
  18. package/dist/managers/permissionManager.js +14 -0
  19. package/dist/managers/planManager.d.ts.map +1 -1
  20. package/dist/managers/planManager.js +10 -0
  21. package/dist/managers/pluginManager.d.ts.map +1 -1
  22. package/dist/managers/pluginManager.js +28 -3
  23. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  24. package/dist/managers/slashCommandManager.js +14 -0
  25. package/dist/managers/subagentManager.d.ts.map +1 -1
  26. package/dist/managers/subagentManager.js +4 -0
  27. package/dist/prompts/index.d.ts +0 -4
  28. package/dist/prompts/index.d.ts.map +1 -1
  29. package/dist/prompts/index.js +0 -3
  30. package/dist/prompts/planModeReminders.d.ts +6 -0
  31. package/dist/prompts/planModeReminders.d.ts.map +1 -0
  32. package/dist/prompts/planModeReminders.js +112 -0
  33. package/dist/services/aiService.d.ts +1 -0
  34. package/dist/services/aiService.d.ts.map +1 -1
  35. package/dist/services/aiService.js +3 -1
  36. package/dist/services/configurationService.d.ts.map +1 -1
  37. package/dist/services/configurationService.js +5 -3
  38. package/dist/services/initializationService.d.ts.map +1 -1
  39. package/dist/services/initializationService.js +13 -12
  40. package/dist/services/jsonlHandler.d.ts +1 -1
  41. package/dist/services/jsonlHandler.d.ts.map +1 -1
  42. package/dist/services/jsonlHandler.js +22 -7
  43. package/dist/services/session.d.ts +3 -2
  44. package/dist/services/session.d.ts.map +1 -1
  45. package/dist/services/session.js +30 -13
  46. package/dist/tools/agentTool.js +1 -1
  47. package/dist/tools/bashTool.d.ts.map +1 -1
  48. package/dist/tools/bashTool.js +8 -12
  49. package/dist/tools/editTool.d.ts.map +1 -1
  50. package/dist/tools/editTool.js +21 -8
  51. package/dist/tools/exitPlanMode.d.ts.map +1 -1
  52. package/dist/tools/exitPlanMode.js +2 -0
  53. package/dist/tools/readTool.d.ts.map +1 -1
  54. package/dist/tools/readTool.js +19 -4
  55. package/dist/tools/types.d.ts +2 -0
  56. package/dist/tools/types.d.ts.map +1 -1
  57. package/dist/types/agent.d.ts +4 -0
  58. package/dist/types/agent.d.ts.map +1 -1
  59. package/dist/types/hooks.d.ts +5 -1
  60. package/dist/types/hooks.d.ts.map +1 -1
  61. package/dist/types/hooks.js +2 -0
  62. package/dist/types/mcp.d.ts +1 -0
  63. package/dist/types/mcp.d.ts.map +1 -1
  64. package/dist/utils/containerSetup.d.ts.map +1 -1
  65. package/dist/utils/containerSetup.js +6 -1
  66. package/dist/utils/editUtils.d.ts +3 -2
  67. package/dist/utils/editUtils.d.ts.map +1 -1
  68. package/dist/utils/editUtils.js +5 -3
  69. package/package.json +2 -2
  70. package/src/managers/aiManager.ts +420 -256
  71. package/src/managers/backgroundTaskManager.ts +1 -1
  72. package/src/managers/bangManager.ts +1 -1
  73. package/src/managers/hookManager.ts +125 -21
  74. package/src/managers/mcpManager.ts +65 -49
  75. package/src/managers/messageManager.ts +10 -0
  76. package/src/managers/permissionManager.ts +18 -0
  77. package/src/managers/planManager.ts +11 -0
  78. package/src/managers/pluginManager.ts +52 -6
  79. package/src/managers/slashCommandManager.ts +17 -0
  80. package/src/managers/subagentManager.ts +4 -0
  81. package/src/prompts/index.ts +0 -8
  82. package/src/prompts/planModeReminders.ts +138 -0
  83. package/src/services/aiService.ts +4 -1
  84. package/src/services/configurationService.ts +5 -3
  85. package/src/services/initializationService.ts +16 -15
  86. package/src/services/jsonlHandler.ts +27 -7
  87. package/src/services/session.ts +33 -13
  88. package/src/tools/agentTool.ts +1 -1
  89. package/src/tools/bashTool.ts +8 -11
  90. package/src/tools/editTool.ts +25 -8
  91. package/src/tools/exitPlanMode.ts +3 -0
  92. package/src/tools/readTool.ts +23 -5
  93. package/src/tools/types.ts +2 -0
  94. package/src/types/agent.ts +4 -0
  95. package/src/types/hooks.ts +9 -1
  96. package/src/types/mcp.ts +1 -0
  97. package/src/utils/containerSetup.ts +7 -3
  98. package/src/utils/editUtils.ts +6 -3
@@ -68,7 +68,7 @@ export class BackgroundTaskManager {
68
68
  stdio: "pipe",
69
69
  detached: true,
70
70
  cwd: this.workdir,
71
- env: {
71
+ env: this.container.get<Record<string, string>>("MergedEnv") || {
72
72
  ...process.env,
73
73
  },
74
74
  });
@@ -48,7 +48,7 @@ export class BangManager {
48
48
  shell: true,
49
49
  stdio: "pipe",
50
50
  cwd: this.workdir,
51
- env: {
51
+ env: this.container.get<Record<string, string>>("MergedEnv") || {
52
52
  ...process.env,
53
53
  },
54
54
  });
@@ -31,6 +31,9 @@ import { logger } from "../utils/globalLogger.js";
31
31
 
32
32
  export class HookManager {
33
33
  private configuration: PartialHookConfiguration | undefined;
34
+ private programmaticHooks: PartialHookConfiguration = {};
35
+ private pluginHooks: PartialHookConfiguration = {};
36
+ private waveConfigHooks: PartialHookConfiguration = {};
34
37
  private readonly matcher: HookMatcher;
35
38
  private readonly workdir: string;
36
39
 
@@ -47,14 +50,16 @@ export class HookManager {
47
50
  * Load hook configuration from programmatic source (AgentOptions.hooks)
48
51
  */
49
52
  loadConfiguration(hooks?: PartialHookConfiguration): void {
50
- const merged: PartialHookConfiguration = {};
53
+ this.programmaticHooks = {};
51
54
 
52
55
  if (hooks) {
53
- this.mergeHooksConfiguration(merged, hooks);
56
+ this.mergeHooksConfiguration(this.programmaticHooks, hooks);
54
57
  }
55
58
 
56
59
  // Validate merged configuration
57
- const validation = this.validatePartialConfiguration(merged);
60
+ const validation = this.validatePartialConfiguration(
61
+ this.programmaticHooks,
62
+ );
58
63
  if (!validation.valid) {
59
64
  throw new HookConfigurationError(
60
65
  "merged configuration",
@@ -62,7 +67,7 @@ export class HookManager {
62
67
  );
63
68
  }
64
69
 
65
- this.configuration = merged;
70
+ this.rebuildConfiguration();
66
71
  }
67
72
 
68
73
  /**
@@ -71,8 +76,9 @@ export class HookManager {
71
76
  */
72
77
  loadConfigurationFromWaveConfig(waveConfig: WaveConfiguration | null): void {
73
78
  try {
74
- // Merge Wave configuration hooks with existing plugin hooks
75
- // (plugin hooks were registered earlier via registerPluginHooks)
79
+ // Replace (not append) wave config hooks to avoid duplicates on reload
80
+ this.waveConfigHooks = {};
81
+
76
82
  if (waveConfig?.hooks) {
77
83
  const validation = this.validatePartialConfiguration(waveConfig.hooks);
78
84
  if (!validation.valid) {
@@ -81,11 +87,10 @@ export class HookManager {
81
87
  validation.errors,
82
88
  );
83
89
  }
84
- if (!this.configuration) {
85
- this.configuration = {};
86
- }
87
- this.mergeHooksConfiguration(this.configuration, waveConfig.hooks);
90
+ this.waveConfigHooks = { ...waveConfig.hooks };
88
91
  }
92
+
93
+ this.rebuildConfiguration();
89
94
  } catch (error) {
90
95
  // Re-throw configuration errors, but handle other errors gracefully
91
96
  if (error instanceof HookConfigurationError) {
@@ -98,6 +103,19 @@ export class HookManager {
98
103
  }
99
104
  }
100
105
 
106
+ /**
107
+ * Rebuild the full configuration from all sources:
108
+ * programmatic (AgentOptions.hooks) + plugin + wave config (settings.json)
109
+ * Order determines precedence on conflict (later sources append).
110
+ */
111
+ private rebuildConfiguration(): void {
112
+ const rebuilt: PartialHookConfiguration = {};
113
+ this.mergeHooksConfiguration(rebuilt, this.programmaticHooks);
114
+ this.mergeHooksConfiguration(rebuilt, this.pluginHooks);
115
+ this.mergeHooksConfiguration(rebuilt, this.waveConfigHooks);
116
+ this.configuration = Object.keys(rebuilt).length > 0 ? rebuilt : undefined;
117
+ }
118
+
101
119
  /**
102
120
  * Execute hooks for a specific event
103
121
  */
@@ -382,6 +400,16 @@ export class HookManager {
382
400
  messageManager.addErrorBlock(errorMessage);
383
401
  return { shouldBlock: false };
384
402
 
403
+ case "PreCompact":
404
+ // Non-blocking for compaction, show error in error block
405
+ messageManager.addErrorBlock(errorMessage);
406
+ return { shouldBlock: false };
407
+
408
+ case "PostCompact":
409
+ // Non-blocking for compaction, show error in error block
410
+ messageManager.addErrorBlock(errorMessage);
411
+ return { shouldBlock: false };
412
+
385
413
  default:
386
414
  return { shouldBlock: false };
387
415
  }
@@ -588,7 +616,9 @@ export class HookManager {
588
616
  event === "WorktreeCreate" ||
589
617
  event === "WorktreeRemove" ||
590
618
  event === "SessionStart" ||
591
- event === "SessionEnd") &&
619
+ event === "SessionEnd" ||
620
+ event === "PreCompact" ||
621
+ event === "PostCompact") &&
592
622
  context.toolName !== undefined
593
623
  ) {
594
624
  logger?.warn(
@@ -671,7 +701,9 @@ export class HookManager {
671
701
  event === "WorktreeRemove" ||
672
702
  event === "CwdChanged" ||
673
703
  event === "SessionStart" ||
674
- event === "SessionEnd"
704
+ event === "SessionEnd" ||
705
+ event === "PreCompact" ||
706
+ event === "PostCompact"
675
707
  ) {
676
708
  return true;
677
709
  }
@@ -734,7 +766,9 @@ export class HookManager {
734
766
  event === "WorktreeCreate" ||
735
767
  event === "WorktreeRemove" ||
736
768
  event === "SessionStart" ||
737
- event === "SessionEnd") &&
769
+ event === "SessionEnd" ||
770
+ event === "PreCompact" ||
771
+ event === "PostCompact") &&
738
772
  config.matcher
739
773
  ) {
740
774
  errors.push(`${prefix}: Event ${event} should not have a matcher`);
@@ -778,6 +812,8 @@ export class HookManager {
778
812
  CwdChanged: 0,
779
813
  SessionStart: 0,
780
814
  SessionEnd: 0,
815
+ PreCompact: 0,
816
+ PostCompact: 0,
781
817
  },
782
818
  };
783
819
  }
@@ -794,6 +830,8 @@ export class HookManager {
794
830
  CwdChanged: 0,
795
831
  SessionStart: 0,
796
832
  SessionEnd: 0,
833
+ PreCompact: 0,
834
+ PostCompact: 0,
797
835
  };
798
836
 
799
837
  let totalConfigs = 0;
@@ -854,10 +892,6 @@ export class HookManager {
854
892
  pluginRoot: string,
855
893
  hooks: PartialHookConfiguration,
856
894
  ): void {
857
- if (!this.configuration) {
858
- this.configuration = {};
859
- }
860
-
861
895
  // Stamp pluginRoot on each hook command
862
896
  const stampedHooks: PartialHookConfiguration = {};
863
897
  for (const [event, configs] of Object.entries(hooks)) {
@@ -868,7 +902,8 @@ export class HookManager {
868
902
  }));
869
903
  }
870
904
 
871
- this.mergeHooksConfiguration(this.configuration, stampedHooks);
905
+ this.mergeHooksConfiguration(this.pluginHooks, stampedHooks);
906
+ this.rebuildConfiguration();
872
907
  }
873
908
 
874
909
  /**
@@ -948,9 +983,9 @@ export class HookManager {
948
983
  transcriptPath,
949
984
  cwd: this.workdir,
950
985
  endSource: source,
951
- env: Object.fromEntries(
952
- Object.entries(process.env).filter((e) => e[1] !== undefined),
953
- ) as Record<string, string>,
986
+ env:
987
+ this.container.get<Record<string, string>>("MergedEnv") ||
988
+ (process.env as Record<string, string>),
954
989
  };
955
990
 
956
991
  const results = await this.executeHooks("SessionEnd", context);
@@ -962,4 +997,73 @@ export class HookManager {
962
997
 
963
998
  return results;
964
999
  }
1000
+
1001
+ /**
1002
+ * Execute PreCompact hooks before compaction.
1003
+ * Returns custom instructions from hook stdout.
1004
+ */
1005
+ async executePreCompactHooks(
1006
+ sessionId: string,
1007
+ transcriptPath: string,
1008
+ customInstructions?: string,
1009
+ ): Promise<{
1010
+ results: HookExecutionResult[];
1011
+ additionalInstructions?: string;
1012
+ }> {
1013
+ const context: ExtendedHookExecutionContext = {
1014
+ event: "PreCompact",
1015
+ projectDir: this.workdir,
1016
+ timestamp: new Date(),
1017
+ sessionId,
1018
+ transcriptPath,
1019
+ cwd: this.workdir,
1020
+ compactInstructions: customInstructions,
1021
+ env: Object.fromEntries(
1022
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
1023
+ ) as Record<string, string>,
1024
+ };
1025
+
1026
+ const results = await this.executeHooks("PreCompact", context);
1027
+
1028
+ let additionalInstructions: string | undefined;
1029
+ for (const result of results) {
1030
+ if (result.success && result.stdout?.trim()) {
1031
+ const trimmed = result.stdout.trim();
1032
+ additionalInstructions = additionalInstructions
1033
+ ? additionalInstructions + "\n" + trimmed
1034
+ : trimmed;
1035
+ }
1036
+ }
1037
+
1038
+ return { results, additionalInstructions };
1039
+ }
1040
+
1041
+ /**
1042
+ * Execute PostCompact hooks after compaction.
1043
+ * Receives the compact summary text.
1044
+ */
1045
+ async executePostCompactHooks(
1046
+ sessionId: string,
1047
+ transcriptPath: string,
1048
+ compactSummary: string,
1049
+ ): Promise<HookExecutionResult[]> {
1050
+ const context: ExtendedHookExecutionContext = {
1051
+ event: "PostCompact",
1052
+ projectDir: this.workdir,
1053
+ timestamp: new Date(),
1054
+ sessionId,
1055
+ transcriptPath,
1056
+ cwd: this.workdir,
1057
+ compactSummary,
1058
+ env:
1059
+ this.container.get<Record<string, string>>("MergedEnv") ||
1060
+ (process.env as Record<string, string>),
1061
+ };
1062
+
1063
+ const results = await this.executeHooks("PostCompact", context);
1064
+ if (results.length > 0) {
1065
+ this.processHookResults("PostCompact", results);
1066
+ }
1067
+ return results;
1068
+ }
965
1069
  }
@@ -369,63 +369,74 @@ export class McpManager {
369
369
  version: "1.0.0",
370
370
  },
371
371
  {
372
- capabilities: {
373
- tools: {},
374
- },
372
+ capabilities: {},
375
373
  },
376
374
  );
377
375
 
378
- if (server.config.url) {
376
+ const serverType = server.config.type;
377
+
378
+ if (serverType === "http" || (!serverType && server.config.url)) {
379
+ if (!server.config.url) {
380
+ throw new Error(
381
+ `MCP server ${name} with type "http" requires a 'url'`,
382
+ );
383
+ }
379
384
  const url = new URL(server.config.url);
380
385
  const headers = server.config.headers;
381
-
382
- try {
383
- logger?.debug(
384
- `Attempting Streamable HTTP connection for ${name} at ${url.href}`,
386
+ logger?.debug(
387
+ `Connecting to MCP server ${name} using Streamable HTTP at ${url.href}`,
388
+ );
389
+ transport = new StreamableHTTPClientTransport(url, {
390
+ requestInit: { headers },
391
+ });
392
+ client = createClient();
393
+ await client.connect(transport);
394
+ const toolsResponse = await client.listTools();
395
+ tools =
396
+ toolsResponse.tools?.map((tool) => ({
397
+ name: tool.name,
398
+ description: tool.description,
399
+ inputSchema: tool.inputSchema,
400
+ })) || [];
401
+ logger?.info(`Connected to MCP server ${name} using Streamable HTTP`);
402
+ } else if (serverType === "sse") {
403
+ if (!server.config.url) {
404
+ throw new Error(
405
+ `MCP server ${name} with type "sse" requires a 'url'`,
385
406
  );
386
- const streamableTransport = new StreamableHTTPClientTransport(url, {
387
- requestInit: { headers },
388
- });
389
-
390
- const streamableClient = createClient();
391
- await streamableClient.connect(streamableTransport);
392
-
393
- // Try to list tools to verify connection works
394
- const toolsResponse = await streamableClient.listTools();
395
-
396
- transport = streamableTransport;
397
- client = streamableClient;
398
- tools =
399
- toolsResponse.tools?.map((tool) => ({
400
- name: tool.name,
401
- description: tool.description,
402
- inputSchema: tool.inputSchema,
403
- })) || [];
404
-
405
- logger?.info(`Connected to MCP server ${name} using Streamable HTTP`);
406
- } catch (error) {
407
- logger?.debug(
408
- `Streamable HTTP failed for ${name}, falling back to SSE: ${error instanceof Error ? error.message : String(error)}`,
407
+ }
408
+ const url = new URL(server.config.url);
409
+ const headers = server.config.headers;
410
+ logger?.debug(
411
+ `Connecting to MCP server ${name} using SSE at ${url.href}`,
412
+ );
413
+ transport = new SSEClientTransport(url, {
414
+ requestInit: { headers },
415
+ });
416
+ client = createClient();
417
+ await client.connect(transport);
418
+ const toolsResponse = await client.listTools();
419
+ tools =
420
+ toolsResponse.tools?.map((tool) => ({
421
+ name: tool.name,
422
+ description: tool.description,
423
+ inputSchema: tool.inputSchema,
424
+ })) || [];
425
+ logger?.info(`Connected to MCP server ${name} using SSE`);
426
+ } else if (
427
+ serverType === "stdio" ||
428
+ (!serverType && server.config.command)
429
+ ) {
430
+ if (!server.config.command) {
431
+ throw new Error(
432
+ `MCP server ${name} with type "stdio" requires a 'command'`,
409
433
  );
410
- transport = new SSEClientTransport(url, {
411
- requestInit: { headers },
412
- });
413
- client = createClient();
414
- await client.connect(transport);
415
-
416
- const toolsResponse = await client.listTools();
417
- tools =
418
- toolsResponse.tools?.map((tool) => ({
419
- name: tool.name,
420
- description: tool.description,
421
- inputSchema: tool.inputSchema,
422
- })) || [];
423
-
424
- logger?.info(`Connected to MCP server ${name} using SSE (fallback)`);
425
434
  }
426
- } else if (server.config.command) {
435
+ const agentEnv =
436
+ this.container.get<Record<string, string>>("MergedEnv") ||
437
+ (process.env as Record<string, string>);
427
438
  const env: Record<string, string> = {
428
- ...(process.env as Record<string, string>),
439
+ ...agentEnv,
429
440
  ...(server.config.env || {}),
430
441
  };
431
442
 
@@ -489,6 +500,11 @@ export class McpManager {
489
500
  description: tool.description,
490
501
  inputSchema: tool.inputSchema,
491
502
  })) || [];
503
+ } else if (serverType) {
504
+ // Unknown type value
505
+ throw new Error(
506
+ `MCP server ${name} has unknown type "${serverType}". Must be "stdio", "sse", or "http"`,
507
+ );
492
508
  } else {
493
509
  throw new Error(
494
510
  `MCP server ${name} configuration must include either 'command' or 'url'`,
@@ -798,7 +814,7 @@ export class McpManager {
798
814
  } else if (c.type === "resource") {
799
815
  textContent.push(`[Resource: ${c.resource?.uri || ""}]`);
800
816
  } else {
801
- textContent.push(JSON.stringify(c));
817
+ textContent.push(JSON.stringify(c, null, 2));
802
818
  }
803
819
  },
804
820
  );
@@ -241,6 +241,16 @@ export class MessageManager {
241
241
  this.filesInContext.add(normalizedPath);
242
242
  }
243
243
 
244
+ /**
245
+ * Checks if a file has been read or edited in this conversation.
246
+ */
247
+ public hasFileInContext(filePath: string): boolean {
248
+ const normalizedPath = isAbsolute(filePath)
249
+ ? relative(this.workdir, filePath)
250
+ : filePath;
251
+ return this.filesInContext.has(normalizedPath);
252
+ }
253
+
244
254
  /**
245
255
  * Extracts and adds file paths from a message's tool blocks.
246
256
  */
@@ -129,6 +129,8 @@ export class PermissionManager {
129
129
  private additionalDirectories: string[] = [];
130
130
  private systemAdditionalDirectories: string[] = [];
131
131
  private planFilePath?: string;
132
+ private hasExitedPlanMode: boolean = false;
133
+ private needsPlanModeExitAttachment: boolean = false;
132
134
  private workdir?: string;
133
135
  private worktreeName?: string;
134
136
  private mainRepoRoot?: string;
@@ -315,6 +317,22 @@ export class PermissionManager {
315
317
  return this.planFilePath;
316
318
  }
317
319
 
320
+ public setHasExitedPlanMode(value: boolean): void {
321
+ this.hasExitedPlanMode = value;
322
+ }
323
+
324
+ public hasExitedPlanModeInSession(): boolean {
325
+ return this.hasExitedPlanMode;
326
+ }
327
+
328
+ public setNeedsPlanModeExitAttachment(value: boolean): void {
329
+ this.needsPlanModeExitAttachment = value;
330
+ }
331
+
332
+ public getNeedsPlanModeExitAttachment(): boolean {
333
+ return this.needsPlanModeExitAttachment;
334
+ }
335
+
318
336
  /**
319
337
  * Public wrapper for isInsideSafeZone to check if a path is in the safe zone
320
338
  */
@@ -74,7 +74,13 @@ export class PlanManager {
74
74
  this.container.get<PermissionManager>("PermissionManager");
75
75
  const messageManager = this.container.get<MessageManager>("MessageManager");
76
76
 
77
+ const previousMode = this.container.get<PermissionMode>("PermissionMode");
78
+
77
79
  if (mode === "plan") {
80
+ // Entering plan mode: clear any pending exit attachment
81
+ // (prevents sending both plan_mode and plan_mode_exit on rapid toggle)
82
+ permissionManager?.setNeedsPlanModeExitAttachment(false);
83
+
78
84
  this.getOrGeneratePlanFilePath(messageManager?.getRootSessionId())
79
85
  .then(({ path }) => {
80
86
  logger?.debug("Plan file path generated", { path });
@@ -83,6 +89,11 @@ export class PlanManager {
83
89
  .catch((error) => {
84
90
  logger?.error("Failed to generate plan file path", error);
85
91
  });
92
+ } else if (previousMode === "plan") {
93
+ // Leaving plan mode: set flags for exit notification and re-entry detection
94
+ permissionManager?.setHasExitedPlanMode(true);
95
+ permissionManager?.setNeedsPlanModeExitAttachment(true);
96
+ permissionManager?.setPlanFilePath(undefined);
86
97
  } else {
87
98
  permissionManager?.setPlanFilePath(undefined);
88
99
  }
@@ -109,14 +109,60 @@ export class PluginManager {
109
109
  );
110
110
 
111
111
  if (isMarketplaceKnown) {
112
+ // Pre-check: verify the plugin still exists in the marketplace manifest
113
+ // before acquiring the lock in installPlugin (which can block ~8s during autoUpdate)
114
+ const marketplace = knownMarketplaces.find(
115
+ (m) => m.name === marketplaceName,
116
+ );
117
+ if (!marketplace) continue;
118
+ try {
119
+ const marketplacePath = marketplaceService.getMarketplacePath(
120
+ marketplace.source,
121
+ );
122
+ const manifest =
123
+ await marketplaceService.loadMarketplaceManifest(
124
+ marketplacePath,
125
+ );
126
+ const pluginExists = manifest.plugins.some(
127
+ (p) => p.name === name,
128
+ );
129
+ if (!pluginExists) {
130
+ logger?.warn(
131
+ `Plugin ${pluginId} is enabled but no longer exists in marketplace ${marketplaceName}. Removing from enabledPlugins.`,
132
+ );
133
+ await this.configurationService?.removeEnabledPlugin(
134
+ this.workdir,
135
+ "user",
136
+ pluginId,
137
+ );
138
+ continue;
139
+ }
140
+ } catch {
141
+ // Manifest read failed (marketplace not cloned yet?) — fall through to installPlugin
142
+ }
143
+
112
144
  logger?.info(`Auto-installing missing plugin: ${pluginId}`);
113
145
  try {
114
146
  await marketplaceService.installPlugin(pluginId);
115
147
  } catch (installError) {
116
- logger?.error(
117
- `Failed to auto-install plugin ${pluginId}:`,
118
- installError,
119
- );
148
+ if (
149
+ installError instanceof Error &&
150
+ installError.message.includes("not found in marketplace")
151
+ ) {
152
+ logger?.warn(
153
+ `Plugin ${pluginId} no longer found in marketplace. Removing from enabledPlugins.`,
154
+ );
155
+ await this.configurationService?.removeEnabledPlugin(
156
+ this.workdir,
157
+ "user",
158
+ pluginId,
159
+ );
160
+ } else {
161
+ logger?.error(
162
+ `Failed to auto-install plugin ${pluginId}:`,
163
+ installError,
164
+ );
165
+ }
120
166
  }
121
167
  } else {
122
168
  logger?.warn(
@@ -132,7 +178,7 @@ export class PluginManager {
132
178
  for (const p of installedRegistry.plugins) {
133
179
  const pluginId = `${p.name}@${p.marketplace}`;
134
180
  if (this.enabledPlugins[pluginId] !== true) {
135
- logger?.info(`Plugin ${pluginId} is not enabled via configuration`);
181
+ logger?.debug(`Plugin ${pluginId} is not enabled via configuration`);
136
182
  continue;
137
183
  }
138
184
  await this.loadSinglePlugin(p.cachePath);
@@ -203,7 +249,7 @@ export class PluginManager {
203
249
  }
204
250
 
205
251
  this.plugins.set(manifest.name, plugin);
206
- logger?.info(`Loaded plugin: ${manifest.name} v${manifest.version}`);
252
+ logger?.debug(`Loaded plugin: ${manifest.name} v${manifest.version}`);
207
253
  } catch (error) {
208
254
  logger?.error(`Failed to load plugin from ${absolutePath}`, error);
209
255
  }
@@ -158,6 +158,23 @@ export class SlashCommandManager {
158
158
  }
159
159
  },
160
160
  });
161
+
162
+ // Register built-in compact command
163
+ this.registerCommand({
164
+ id: "compact",
165
+ name: "compact",
166
+ description: "Compact conversation history to reduce context usage",
167
+ handler: async (args?: string, signal?: AbortSignal) => {
168
+ this.aiManager.abortAIMessage();
169
+
170
+ const customInstructions = args?.trim() || undefined;
171
+
172
+ await this.aiManager.compactConversation({
173
+ customInstructions,
174
+ abortSignal: signal,
175
+ });
176
+ },
177
+ });
161
178
  }
162
179
 
163
180
  /**
@@ -287,6 +287,10 @@ export class SubagentManager {
287
287
  if (parentOptions) {
288
288
  const subagentOptions: import("../types/agent.js").AgentOptions = {
289
289
  ...parentOptions,
290
+ defaultHeaders: {
291
+ ...parentOptions.defaultHeaders,
292
+ ...parentOptions.subagentHeaders?.[parameters.subagent_type],
293
+ },
290
294
  callbacks: {
291
295
  ...parentOptions.callbacks,
292
296
  onLoadingChange: undefined,
@@ -239,10 +239,6 @@ export function buildSystemPrompt(
239
239
  memory?: string;
240
240
  language?: string;
241
241
  isSubagent?: boolean;
242
- planMode?: {
243
- planFilePath: string;
244
- planExists: boolean;
245
- };
246
242
  autoMemory?: {
247
243
  directory: string;
248
244
  content: string;
@@ -269,10 +265,6 @@ export function buildSystemPrompt(
269
265
  prompt += `\n\n# Language\nAlways respond in ${options.language}. Use ${options.language} for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.`;
270
266
  }
271
267
 
272
- if (options.planMode) {
273
- prompt += `\n\n${buildPlanModePrompt(options.planMode.planFilePath, options.planMode.planExists, options.isSubagent)}`;
274
- }
275
-
276
268
  if (options.workdir) {
277
269
  const isGitRepo = isGitRepository(options.workdir);
278
270
  const platform = os.platform();