wave-agent-sdk 0.17.6 → 0.17.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,6 @@ export class SlashCommandManager {
13
13
  this.workdir = options.workdir;
14
14
  }
15
15
  initialize() {
16
- this.initializeBuiltinCommands();
17
16
  this.loadCustomCommands();
18
17
  // Listen for skill refreshes and update skill commands
19
18
  const skillManager = this.container.get("SkillManager");
@@ -29,181 +28,12 @@ export class SlashCommandManager {
29
28
  get aiManager() {
30
29
  return this.container.get("AIManager");
31
30
  }
32
- get backgroundTaskManager() {
33
- return this.container.get("BackgroundTaskManager");
34
- }
35
- get taskManager() {
36
- return this.container.get("TaskManager");
37
- }
38
31
  get skillManager() {
39
32
  return this.container.get("SkillManager");
40
33
  }
41
34
  get subagentManager() {
42
35
  return this.container.get("SubagentManager");
43
36
  }
44
- get memoryService() {
45
- return this.container.get("MemoryService");
46
- }
47
- get hookManager() {
48
- return this.container.get("HookManager");
49
- }
50
- get goalManager() {
51
- return this.container.get("GoalManager");
52
- }
53
- initializeBuiltinCommands() {
54
- // Register built-in clear command
55
- this.registerCommand({
56
- id: "clear",
57
- name: "clear",
58
- description: "Clear conversation history and reset session",
59
- immediate: true,
60
- handler: async () => {
61
- this.aiManager.abortAIMessage();
62
- // Clear any active goal
63
- this.goalManager?.clearGoal();
64
- // Capture old session info before clearing
65
- const oldSessionId = this.messageManager.getSessionId();
66
- const transcriptPath = this.messageManager.getTranscriptPath();
67
- // Run SessionEnd hooks (cleanup before clear)
68
- if (this.hookManager) {
69
- try {
70
- await this.hookManager.executeSessionEndHooks("clear", oldSessionId, transcriptPath);
71
- }
72
- catch (error) {
73
- logger?.warn(`SessionEnd hooks on clear failed: ${error.message}`);
74
- }
75
- }
76
- // Clear messages and generate new session
77
- this.messageManager.clearMessages();
78
- this.memoryService.clearCache();
79
- await this.taskManager.syncWithSession();
80
- // Run SessionStart hooks (restore context for new session)
81
- if (this.hookManager) {
82
- try {
83
- const newSessionId = this.messageManager.getSessionId();
84
- const sessionStartResult = await this.hookManager.executeSessionStartHooks("clear", newSessionId, this.messageManager.getTranscriptPath());
85
- // Inject additionalContext as a meta user message
86
- if (sessionStartResult.additionalContext) {
87
- this.messageManager.addUserMessage({
88
- content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
89
- isMeta: true,
90
- });
91
- }
92
- // Inject initialUserMessage as a meta user message
93
- if (sessionStartResult.initialUserMessage) {
94
- this.messageManager.addUserMessage({
95
- content: sessionStartResult.initialUserMessage,
96
- isMeta: true,
97
- });
98
- }
99
- }
100
- catch (error) {
101
- logger?.warn(`SessionStart hooks on clear failed: ${error.message}`);
102
- }
103
- }
104
- },
105
- });
106
- // Register built-in compact command
107
- this.registerCommand({
108
- id: "compact",
109
- name: "compact",
110
- description: "Compact conversation history to reduce context usage",
111
- immediate: true,
112
- handler: async (args, signal) => {
113
- this.aiManager.abortAIMessage();
114
- const customInstructions = args?.trim() || undefined;
115
- await this.aiManager.compactConversation({
116
- customInstructions,
117
- abortSignal: signal,
118
- });
119
- },
120
- });
121
- // Register built-in goal command
122
- this.registerCommand({
123
- id: "goal",
124
- name: "goal",
125
- description: "Set, check, or clear an autonomous goal for the session",
126
- immediate: (args) => {
127
- const trimmed = args?.trim() ?? "";
128
- return (!trimmed ||
129
- ["clear", "stop", "off", "reset", "none", "cancel"].includes(trimmed));
130
- },
131
- handler: async (args) => {
132
- const goalManager = this.goalManager;
133
- if (!goalManager) {
134
- this.messageManager.addUserMessage({
135
- content: "Goal manager is not available",
136
- isMeta: true,
137
- });
138
- return;
139
- }
140
- const trimmed = args?.trim() ?? "";
141
- // Clear aliases
142
- if (["clear", "stop", "off", "reset", "none", "cancel"].includes(trimmed)) {
143
- if (goalManager.isGoalActive()) {
144
- goalManager.clearGoal();
145
- this.messageManager.addUserMessage({
146
- content: "<system-reminder>Goal cleared.</system-reminder>",
147
- isMeta: true,
148
- });
149
- }
150
- else {
151
- this.messageManager.addUserMessage({
152
- content: "<system-reminder>No active goal to clear.</system-reminder>",
153
- isMeta: true,
154
- });
155
- }
156
- return;
157
- }
158
- // Show status
159
- if (!trimmed) {
160
- if (goalManager.isGoalActive()) {
161
- this.messageManager.addUserMessage({
162
- content: `<system-reminder>${goalManager.getStatusString()}</system-reminder>`,
163
- isMeta: true,
164
- });
165
- }
166
- else {
167
- this.messageManager.addUserMessage({
168
- content: "<system-reminder>No active goal. Use /goal <condition> to set one.</system-reminder>",
169
- isMeta: true,
170
- });
171
- }
172
- return;
173
- }
174
- // Check plan mode
175
- const permissionMode = this.container.has("PermissionMode")
176
- ? this.container.get("PermissionMode")
177
- : undefined;
178
- if (permissionMode === "plan") {
179
- this.messageManager.addUserMessage({
180
- content: "<system-reminder>Cannot set a goal in plan mode. Exit plan mode first.</system-reminder>",
181
- isMeta: true,
182
- });
183
- return;
184
- }
185
- // Set goal
186
- try {
187
- goalManager.setGoal(trimmed);
188
- this.messageManager.addUserMessage({
189
- content: `<system-reminder>Goal set: ${trimmed}. The agent will work autonomously until this goal is achieved.</system-reminder>`,
190
- isMeta: true,
191
- });
192
- // Add the goal as a user directive to start working
193
- this.messageManager.addUserMessage({
194
- content: trimmed,
195
- });
196
- this.aiManager.sendAIMessage();
197
- }
198
- catch (error) {
199
- this.messageManager.addUserMessage({
200
- content: `<system-reminder>Failed to set goal: ${error.message}</system-reminder>`,
201
- isMeta: true,
202
- });
203
- }
204
- },
205
- });
206
- }
207
37
  /**
208
38
  * Load custom commands from filesystem
209
39
  */
@@ -1 +1 @@
1
- {"version":3,"file":"toolManager.d.ts","sourceRoot":"","sources":["../../src/managers/toolManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AA6B7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,mBAAmB,CAAC;AAO3B,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIlD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,SAAS,CAAC;IACrB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,wDAAwD;IACxD,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,cAAM,WAAW;IACf,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,KAAK,CAAC,CAAW;IACzB,OAAO,CAAC,WAAW,CAAC,CAAe;IACnC,OAAO,CAAC,SAAS,CAAY;gBAEjB,OAAO,EAAE,kBAAkB;IAMvC,OAAO,KAAK,UAAU,GAErB;IAED;;OAEG;IACI,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAIvC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACI,sBAAsB,IAAI,IAAI;IA0CrC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiBxB;;;;;;;;;;;;OAYG;IACG,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IAqJtB,IAAI,IAAI,UAAU,EAAE;IAYpB,cAAc,CAAC,OAAO,CAAC,EAAE;QACvB,kBAAkB,CAAC,EAAE,qBAAqB,EAAE,CAAC;QAC7C,eAAe,CAAC,EAAE,aAAa,EAAE,CAAC;QAClC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,OAAO,CAAC;KACtB,GAAG,0BAA0B,EAAE;IAuDhC;;OAEG;IACI,QAAQ,IAAI,UAAU,EAAE;IAI/B;;OAEG;IACI,iBAAiB,IAAI,cAAc;IAa1C;;;OAGG;IACI,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAIpD;;OAEG;IACI,oBAAoB,IAAI,iBAAiB,GAAG,SAAS;IAI5D;;OAEG;IACI,cAAc,IACjB,OAAO,4BAA4B,EAAE,WAAW,GAChD,SAAS;CAKd;AAGD,OAAO,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"toolManager.d.ts","sourceRoot":"","sources":["../../src/managers/toolManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AA6B7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,mBAAmB,CAAC;AAO3B,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIlD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,SAAS,CAAC;IACrB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,wDAAwD;IACxD,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,cAAM,WAAW;IACf,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,KAAK,CAAC,CAAW;IACzB,OAAO,CAAC,WAAW,CAAC,CAAe;IACnC,OAAO,CAAC,SAAS,CAAY;gBAEjB,OAAO,EAAE,kBAAkB;IAMvC,OAAO,KAAK,UAAU,GAErB;IAED;;OAEG;IACI,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAIvC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACI,sBAAsB,IAAI,IAAI;IA0CrC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiBxB;;;;;;;;;;;;OAYG;IACG,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IAqJtB,IAAI,IAAI,UAAU,EAAE;IAYpB,cAAc,CAAC,OAAO,CAAC,EAAE;QACvB,kBAAkB,CAAC,EAAE,qBAAqB,EAAE,CAAC;QAC7C,eAAe,CAAC,EAAE,aAAa,EAAE,CAAC;QAClC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,OAAO,CAAC;KACtB,GAAG,0BAA0B,EAAE;IAgDhC;;OAEG;IACI,QAAQ,IAAI,UAAU,EAAE;IAI/B;;OAEG;IACI,iBAAiB,IAAI,cAAc;IAa1C;;;OAGG;IACI,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAIpD;;OAEG;IACI,oBAAoB,IAAI,iBAAiB,GAAG,SAAS;IAI5D;;OAEG;IACI,cAAc,IACjB,OAAO,4BAA4B,EAAE,WAAW,GAChD,SAAS;CAKd;AAGD,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -272,9 +272,7 @@ class ToolManager {
272
272
  return false;
273
273
  }
274
274
  if (effectivePermissionMode === "bypassPermissions") {
275
- if (tool.name === "ExitPlanMode" ||
276
- tool.name === "AskUserQuestion" ||
277
- tool.name === "EnterPlanMode") {
275
+ if (tool.name === "ExitPlanMode") {
278
276
  return false;
279
277
  }
280
278
  }
@@ -282,8 +280,7 @@ class ToolManager {
282
280
  return effectivePermissionMode === "plan";
283
281
  }
284
282
  if (tool.name === "EnterPlanMode") {
285
- return (effectivePermissionMode !== "plan" &&
286
- effectivePermissionMode !== "bypassPermissions");
283
+ return effectivePermissionMode !== "plan";
287
284
  }
288
285
  return true;
289
286
  })
@@ -1,6 +1,5 @@
1
1
  export declare function wrapInSystemReminder(content: string): string;
2
2
  export declare function buildPlanModeReminder(planFilePath: string, planExists: boolean, isSubagent?: boolean): string;
3
- export declare function buildPlanModeSparseReminder(planFilePath: string): string;
4
3
  export declare function buildPlanModeReEntryReminder(planFilePath: string): string;
5
4
  export declare function buildExitedPlanModeReminder(planFilePath?: string, planExists?: boolean): string;
6
5
  //# sourceMappingURL=planModeReminders.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"planModeReminders.d.ts","sourceRoot":"","sources":["../../src/prompts/planModeReminders.ts"],"names":[],"mappings":"AAYA,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,OAAO,EACnB,UAAU,GAAE,OAAe,GAC1B,MAAM,CAsFR;AAED,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAIxE;AAED,wBAAgB,4BAA4B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAczE;AAED,wBAAgB,2BAA2B,CACzC,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,OAAO,GACnB,MAAM,CAIR"}
1
+ {"version":3,"file":"planModeReminders.d.ts","sourceRoot":"","sources":["../../src/prompts/planModeReminders.ts"],"names":[],"mappings":"AAYA,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,OAAO,EACnB,UAAU,GAAE,OAAe,GAC1B,MAAM,CAsFR;AAED,wBAAgB,4BAA4B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAQzE;AAED,wBAAgB,2BAA2B,CACzC,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,OAAO,GACnB,MAAM,CAIR"}
@@ -87,23 +87,14 @@ This is critical - your turn should only end with either using the ${ASK_USER_QU
87
87
 
88
88
  NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications using the ${ASK_USER_QUESTION_TOOL_NAME} tool. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.`);
89
89
  }
90
- export function buildPlanModeSparseReminder(planFilePath) {
91
- return wrapInSystemReminder(`Plan mode still active (see full instructions earlier in conversation). Read-only except plan file at ${planFilePath}. End turns with ${ASK_USER_QUESTION_TOOL_NAME} or ${EXIT_PLAN_MODE_TOOL_NAME}.`);
92
- }
93
90
  export function buildPlanModeReEntryReminder(planFilePath) {
94
91
  return wrapInSystemReminder(`## Re-entering Plan Mode
95
92
 
96
- You are returning to plan mode after having previously exited it. A plan file exists at ${planFilePath} from your previous planning session.
93
+ You are returning to plan mode. A plan file exists at ${planFilePath} from your previous session.
97
94
 
98
- **Before proceeding with any new planning, you should:**
99
95
  1. Read the existing plan file to understand what was previously planned
100
- 2. Evaluate the user's current request against that plan
101
- 3. Decide how to proceed:
102
- - **Different task**: If the user's request is for a different task—even if it's similar or related—start fresh by overwriting the existing plan
103
- - **Same task, continuing**: If this is explicitly a continuation or refinement of the exact same task, modify the existing plan while cleaning up outdated or irrelevant sections
104
- 4. Continue on with the plan process and most importantly you should always edit the plan file one way or the other before calling ${EXIT_PLAN_MODE_TOOL_NAME}
105
-
106
- Treat this as a fresh planning session. Do not assume the existing plan is relevant without evaluating it first.`);
96
+ 2. Decide: if the user's request is a different task, start fresh by overwriting the plan; if it's a continuation, modify the existing plan
97
+ 3. Edit the plan file as needed, then call ${EXIT_PLAN_MODE_TOOL_NAME}`);
107
98
  }
108
99
  export function buildExitedPlanModeReminder(planFilePath, planExists) {
109
100
  return wrapInSystemReminder(`## Exited Plan Mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
package/src/agent.ts CHANGED
@@ -685,7 +685,152 @@ export class Agent {
685
685
  }
686
686
 
687
687
  public async clearMessages(): Promise<void> {
688
- await this.slashCommandManager.executeCommand("clear");
688
+ this.aiManager.abortAIMessage();
689
+
690
+ // Clear any active goal
691
+ this.goalManager.clearGoal();
692
+
693
+ // Capture old session info before clearing
694
+ const oldSessionId = this.messageManager.getSessionId();
695
+ const transcriptPath = this.messageManager.getTranscriptPath();
696
+
697
+ // Run SessionEnd hooks (cleanup before clear)
698
+ try {
699
+ await this.hookManager.executeSessionEndHooks(
700
+ "clear",
701
+ oldSessionId,
702
+ transcriptPath,
703
+ );
704
+ } catch (error) {
705
+ this.logger?.warn(
706
+ `SessionEnd hooks on clear failed: ${(error as Error).message}`,
707
+ );
708
+ }
709
+
710
+ // Clear messages and generate new session
711
+ this.messageManager.clearMessages();
712
+ const memoryService =
713
+ this.container.get<import("./services/memory.js").MemoryService>(
714
+ "MemoryService",
715
+ );
716
+ memoryService?.clearCache();
717
+ await this.taskManager.syncWithSession();
718
+
719
+ // Run SessionStart hooks (restore context for new session)
720
+ try {
721
+ const newSessionId = this.messageManager.getSessionId();
722
+ const sessionStartResult =
723
+ await this.hookManager.executeSessionStartHooks(
724
+ "clear",
725
+ newSessionId,
726
+ this.messageManager.getTranscriptPath(),
727
+ );
728
+
729
+ // Inject additionalContext as a meta user message
730
+ if (sessionStartResult.additionalContext) {
731
+ this.messageManager.addUserMessage({
732
+ content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
733
+ isMeta: true,
734
+ });
735
+ }
736
+
737
+ // Inject initialUserMessage as a meta user message
738
+ if (sessionStartResult.initialUserMessage) {
739
+ this.messageManager.addUserMessage({
740
+ content: sessionStartResult.initialUserMessage,
741
+ isMeta: true,
742
+ });
743
+ }
744
+ } catch (error) {
745
+ this.logger?.warn(
746
+ `SessionStart hooks on clear failed: ${(error as Error).message}`,
747
+ );
748
+ }
749
+
750
+ await this.messageManager.saveSession();
751
+ }
752
+
753
+ /**
754
+ * Compact conversation history to reduce context usage
755
+ * @param customInstructions - Optional custom instructions for compaction
756
+ */
757
+ public async compact(customInstructions?: string): Promise<void> {
758
+ this.aiManager.abortAIMessage();
759
+
760
+ await this.aiManager.compactConversation({
761
+ customInstructions,
762
+ });
763
+
764
+ await this.messageManager.saveSession();
765
+ }
766
+
767
+ /**
768
+ * Set an autonomous goal for the session
769
+ * @param condition - The goal condition to achieve
770
+ */
771
+ public async setGoal(condition: string): Promise<void> {
772
+ // Check plan mode
773
+ if (this.getPermissionMode() === "plan") {
774
+ this.messageManager.addUserMessage({
775
+ content:
776
+ "<system-reminder>Cannot set a goal in plan mode. Exit plan mode first.</system-reminder>",
777
+ isMeta: true,
778
+ });
779
+ return;
780
+ }
781
+
782
+ this.goalManager.setGoal(condition);
783
+ this.messageManager.addUserMessage({
784
+ content: `<system-reminder>Goal set: ${condition}. The agent will work autonomously until this goal is achieved.</system-reminder>`,
785
+ isMeta: true,
786
+ });
787
+ // Add the goal as a user directive to start working
788
+ this.messageManager.addUserMessage({
789
+ content: condition,
790
+ });
791
+ this.aiManager.sendAIMessage();
792
+
793
+ await this.messageManager.saveSession();
794
+ }
795
+
796
+ /**
797
+ * Clear the current autonomous goal
798
+ */
799
+ public async clearGoal(): Promise<void> {
800
+ if (this.goalManager.isGoalActive()) {
801
+ this.goalManager.clearGoal();
802
+ this.messageManager.addUserMessage({
803
+ content: "<system-reminder>Goal cleared.</system-reminder>",
804
+ isMeta: true,
805
+ });
806
+ } else {
807
+ this.messageManager.addUserMessage({
808
+ content: "<system-reminder>No active goal to clear.</system-reminder>",
809
+ isMeta: true,
810
+ });
811
+ }
812
+
813
+ await this.messageManager.saveSession();
814
+ }
815
+
816
+ /**
817
+ * Show the current goal status
818
+ */
819
+ public async showGoalStatus(): Promise<void> {
820
+ if (this.goalManager.isGoalActive()) {
821
+ this.messageManager.addUserMessage({
822
+ content: `<system-reminder>${this.goalManager.getStatusString()}</system-reminder>`,
823
+ isMeta: true,
824
+ });
825
+ } else {
826
+ this.messageManager.addUserMessage({
827
+ content:
828
+ "<system-reminder>No active goal. Use /goal <condition> to set one.</system-reminder>",
829
+ isMeta: true,
830
+ });
831
+ }
832
+
833
+ await this.messageManager.saveSession();
689
834
  }
690
835
 
691
836
  /** Unified interrupt method, interrupts both AI messages and command execution */
@@ -26,7 +26,6 @@ import type { SkillManager } from "./skillManager.js";
26
26
  import { buildSystemPrompt } from "../prompts/index.js";
27
27
  import {
28
28
  buildPlanModeReminder,
29
- buildPlanModeSparseReminder,
30
29
  buildPlanModeReEntryReminder,
31
30
  buildExitedPlanModeReminder,
32
31
  } from "../prompts/planModeReminders.js";
@@ -138,6 +137,14 @@ export class AIManager {
138
137
  return this.container.get<PermissionManager>("PermissionManager");
139
138
  }
140
139
 
140
+ private get planManager():
141
+ | import("./planManager.js").PlanManager
142
+ | undefined {
143
+ return this.container.get<import("./planManager.js").PlanManager>(
144
+ "PlanManager",
145
+ );
146
+ }
147
+
141
148
  private get configurationService(): ConfigurationService {
142
149
  return this.container.get<ConfigurationService>("ConfigurationService")!;
143
150
  }
@@ -253,7 +260,6 @@ export class AIManager {
253
260
  this.permissionManager.setNeedsPlanModeExitAttachment(false);
254
261
  }
255
262
 
256
- // Handle plan mode reminders
257
263
  if (currentMode !== "plan") return messages;
258
264
 
259
265
  const planFilePath = this.permissionManager.getPlanFilePath();
@@ -261,83 +267,27 @@ export class AIManager {
261
267
 
262
268
  const planExists = existsSync(planFilePath);
263
269
 
264
- // Check for re-entry: flag is set AND plan file exists
265
- if (this.permissionManager.hasExitedPlanModeInSession() && planExists) {
266
- messages.push({
267
- role: "user",
268
- content: buildPlanModeReEntryReminder(planFilePath),
269
- });
270
- this.permissionManager.setHasExitedPlanMode(false); // One-time
271
- }
272
-
273
- // Count plan_mode system-reminders in existing messages to determine full vs sparse
274
- // and count human turns since last reminder for throttling
275
- const recentApiMessages = this.messageManager.getMessages();
276
- let planModeReminderCount = 0;
277
- let humanTurnsSinceLastReminder = 0;
278
- let foundLastReminder = false;
279
-
280
- for (let i = recentApiMessages.length - 1; i >= 0; i--) {
281
- const msg = recentApiMessages[i];
282
- if (msg.role === "user" && !msg.isMeta) {
283
- // Count human turns (non-meta user messages without tool results)
284
- const hasToolResult = msg.blocks?.some(
285
- (b: { type: string }) => b.type === "tool",
286
- );
287
- if (!hasToolResult) {
288
- if (!foundLastReminder) {
289
- humanTurnsSinceLastReminder++;
290
- }
291
- }
292
- }
293
- // Check for existing plan mode system-reminders
294
- if (msg.role === "user" && msg.isMeta) {
295
- const textContent = msg.blocks
296
- ?.filter((b) => b.type === "text")
297
- .map((b) => ("content" in b ? b.content : ""))
298
- .join("");
299
- if (
300
- textContent?.includes("Plan mode is active") ||
301
- textContent?.includes("Plan mode still active")
302
- ) {
303
- planModeReminderCount++;
304
- if (!foundLastReminder) {
305
- foundLastReminder = true;
306
- }
307
- }
270
+ // One-time plan entry reminder
271
+ const planMgr = this.planManager;
272
+ if (planMgr?.isPlanEntryReminderPending()) {
273
+ if (this.permissionManager.hasExitedPlanModeInSession() && planExists) {
274
+ // Re-entry: use small reminder
275
+ messages.push({
276
+ role: "user",
277
+ content: buildPlanModeReEntryReminder(planFilePath),
278
+ });
279
+ } else {
280
+ // First entry: use full reminder
281
+ messages.push({
282
+ role: "user",
283
+ content: buildPlanModeReminder(
284
+ planFilePath,
285
+ planExists,
286
+ !!this.subagentType,
287
+ ),
288
+ });
308
289
  }
309
- }
310
-
311
- // Throttle: only inject every 5 human turns (but always inject on first turn)
312
- const TURNS_BETWEEN_REMINDERS = 5;
313
- const FULL_REMINDER_EVERY_N = 5;
314
-
315
- if (
316
- foundLastReminder &&
317
- humanTurnsSinceLastReminder < TURNS_BETWEEN_REMINDERS
318
- ) {
319
- return messages; // Throttled — skip reminder
320
- }
321
-
322
- // Determine full vs sparse
323
- // Every 5th reminder is full; rest are sparse
324
- const reminderNumber = planModeReminderCount + 1;
325
- const isFull = reminderNumber % FULL_REMINDER_EVERY_N === 1;
326
-
327
- if (isFull) {
328
- messages.push({
329
- role: "user",
330
- content: buildPlanModeReminder(
331
- planFilePath,
332
- planExists,
333
- !!this.subagentType,
334
- ),
335
- });
336
- } else {
337
- messages.push({
338
- role: "user",
339
- content: buildPlanModeSparseReminder(planFilePath),
340
- });
290
+ planMgr.consumePlanEntryReminder();
341
291
  }
342
292
 
343
293
  return messages;
@@ -548,11 +548,6 @@ export class MessageManager {
548
548
  this.setSessionId(generateSessionId());
549
549
  this.parentSessionId = oldSessionId;
550
550
 
551
- // Trigger task list update if this is the main session to ensure continuity
552
- if (this.sessionType === "main") {
553
- this.callbacks.onSessionIdChange?.(this.sessionId);
554
- }
555
-
556
551
  // Set new message list
557
552
  this.setMessages(newMessages);
558
553
 
@@ -31,6 +31,7 @@ import {
31
31
  EDIT_TOOL_NAME,
32
32
  WRITE_TOOL_NAME,
33
33
  READ_TOOL_NAME,
34
+ ASK_USER_QUESTION_TOOL_NAME,
34
35
  } from "../constants/tools.js";
35
36
  import { Container } from "../utils/container.js";
36
37
  import { ConfigurationService } from "../services/configurationService.js";
@@ -440,8 +441,14 @@ export class PermissionManager {
440
441
  }
441
442
 
442
443
  // 1. If bypassPermissions mode, always allow
444
+ // Exception: tools that require user interaction (e.g. AskUserQuestion)
445
+ // must still prompt the user, matching Claude Code's requiresUserInteraction behavior.
443
446
  if (context.permissionMode === "bypassPermissions") {
444
- return { behavior: "allow" };
447
+ const requiresUserInteraction =
448
+ context.toolName === ASK_USER_QUESTION_TOOL_NAME;
449
+ if (!requiresUserInteraction) {
450
+ return { behavior: "allow" };
451
+ }
445
452
  }
446
453
 
447
454
  // 1.0 Check worktree safety for Write and Edit tools
@@ -16,6 +16,7 @@ import { logger } from "../utils/globalLogger.js";
16
16
  export class PlanManager {
17
17
  private planDir: string;
18
18
  private currentPlanFilePath: string | null = null;
19
+ private planEntryReminderPending: boolean = false;
19
20
 
20
21
  constructor(private container: Container) {
21
22
  this.planDir = path.join(os.homedir(), ".wave", "plans");
@@ -80,6 +81,7 @@ export class PlanManager {
80
81
  // Entering plan mode: clear any pending exit attachment
81
82
  // (prevents sending both plan_mode and plan_mode_exit on rapid toggle)
82
83
  permissionManager?.setNeedsPlanModeExitAttachment(false);
84
+ this.planEntryReminderPending = true;
83
85
 
84
86
  this.getOrGeneratePlanFilePath(messageManager?.getRootSessionId())
85
87
  .then(({ path }) => {
@@ -94,8 +96,17 @@ export class PlanManager {
94
96
  permissionManager?.setHasExitedPlanMode(true);
95
97
  permissionManager?.setNeedsPlanModeExitAttachment(true);
96
98
  permissionManager?.setPlanFilePath(undefined);
99
+ this.planEntryReminderPending = false;
97
100
  } else {
98
101
  permissionManager?.setPlanFilePath(undefined);
99
102
  }
100
103
  }
104
+
105
+ public isPlanEntryReminderPending(): boolean {
106
+ return this.planEntryReminderPending;
107
+ }
108
+
109
+ public consumePlanEntryReminder(): void {
110
+ this.planEntryReminderPending = false;
111
+ }
101
112
  }