wave-agent-sdk 0.19.9 → 1.0.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 (89) hide show
  1. package/builtin/plugins/sdd/scripts/session-start.js +1 -1
  2. package/builtin/plugins/sdd/skills/specify/SKILL.md +3 -4
  3. package/builtin/skills/settings/ENV.md +15 -9
  4. package/builtin/skills/settings/HOOKS.md +27 -2
  5. package/dist/agent.d.ts +9 -20
  6. package/dist/agent.js +28 -99
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/managers/aiManager.d.ts +71 -8
  10. package/dist/managers/aiManager.js +290 -85
  11. package/dist/managers/backgroundTaskManager.d.ts +6 -0
  12. package/dist/managers/backgroundTaskManager.js +11 -0
  13. package/dist/managers/bangManager.d.ts +6 -0
  14. package/dist/managers/bangManager.js +11 -0
  15. package/dist/managers/hookManager.d.ts +8 -2
  16. package/dist/managers/hookManager.js +14 -4
  17. package/dist/managers/mcpManager.d.ts +18 -4
  18. package/dist/managers/mcpManager.js +40 -18
  19. package/dist/managers/messageManager.d.ts +9 -5
  20. package/dist/managers/messageManager.js +36 -12
  21. package/dist/managers/subagentManager.d.ts +6 -0
  22. package/dist/managers/subagentManager.js +33 -22
  23. package/dist/managers/toolManager.js +5 -0
  24. package/dist/prompts/index.d.ts +0 -1
  25. package/dist/prompts/index.js +0 -4
  26. package/dist/services/aiService.d.ts +1 -34
  27. package/dist/services/aiService.js +18 -130
  28. package/dist/services/autoMemoryService.d.ts +27 -2
  29. package/dist/services/autoMemoryService.js +124 -36
  30. package/dist/services/configurationService.d.ts +21 -2
  31. package/dist/services/configurationService.js +86 -24
  32. package/dist/services/initializationService.js +14 -4
  33. package/dist/services/interactionService.js +35 -7
  34. package/dist/services/remoteSettingsService.d.ts +12 -0
  35. package/dist/services/remoteSettingsService.js +15 -1
  36. package/dist/services/session.d.ts +13 -0
  37. package/dist/services/session.js +64 -0
  38. package/dist/services/taskManager.js +7 -1
  39. package/dist/tools/bashTool.js +1 -0
  40. package/dist/tools/enterWorktreeTool.js +14 -3
  41. package/dist/tools/exitWorktreeTool.js +11 -10
  42. package/dist/tools/types.d.ts +7 -0
  43. package/dist/types/agent.d.ts +0 -2
  44. package/dist/types/config.d.ts +9 -0
  45. package/dist/types/core.d.ts +1 -1
  46. package/dist/types/hooks.d.ts +2 -2
  47. package/dist/utils/containerSetup.js +13 -4
  48. package/dist/utils/openaiClient.js +2 -1
  49. package/dist/utils/pathEncoder.js +7 -2
  50. package/dist/utils/worktreeUtils.d.ts +17 -0
  51. package/dist/utils/worktreeUtils.js +339 -1
  52. package/package.json +1 -1
  53. package/src/agent.ts +43 -112
  54. package/src/index.ts +1 -0
  55. package/src/managers/aiManager.ts +389 -110
  56. package/src/managers/backgroundTaskManager.ts +15 -0
  57. package/src/managers/bangManager.ts +15 -0
  58. package/src/managers/hookManager.ts +20 -5
  59. package/src/managers/mcpManager.ts +60 -18
  60. package/src/managers/messageManager.ts +51 -23
  61. package/src/managers/subagentManager.ts +36 -25
  62. package/src/managers/toolManager.ts +7 -0
  63. package/src/prompts/index.ts +0 -4
  64. package/src/services/aiService.ts +25 -203
  65. package/src/services/autoMemoryService.ts +145 -39
  66. package/src/services/configurationService.ts +100 -24
  67. package/src/services/initializationService.ts +17 -4
  68. package/src/services/interactionService.ts +49 -6
  69. package/src/services/remoteSettingsService.ts +16 -1
  70. package/src/services/session.ts +68 -0
  71. package/src/services/taskManager.ts +10 -1
  72. package/src/tools/bashTool.ts +1 -0
  73. package/src/tools/enterWorktreeTool.ts +19 -2
  74. package/src/tools/exitWorktreeTool.ts +15 -12
  75. package/src/tools/types.ts +7 -0
  76. package/src/types/agent.ts +0 -6
  77. package/src/types/config.ts +9 -0
  78. package/src/types/core.ts +1 -1
  79. package/src/types/hooks.ts +2 -2
  80. package/src/utils/containerSetup.ts +15 -5
  81. package/src/utils/openaiClient.ts +2 -0
  82. package/src/utils/pathEncoder.ts +7 -2
  83. package/src/utils/worktreeUtils.ts +401 -1
  84. package/dist/constants/goalPrompts.d.ts +0 -1
  85. package/dist/constants/goalPrompts.js +0 -10
  86. package/dist/managers/goalManager.d.ts +0 -42
  87. package/dist/managers/goalManager.js +0 -177
  88. package/src/constants/goalPrompts.ts +0 -10
  89. package/src/managers/goalManager.ts +0 -232
@@ -14,7 +14,7 @@ const guidance = [
14
14
  "Spec-First Workflow(规格优先工作流):",
15
15
  "- 需求增加或变更时,优先更新 spec:先更新对应规格说明(新增用户故事、验收场景),待用户确认 spec 后再实现代码。spec 是功能设计的权威来源,不是 changelog。",
16
16
  "- 边界模糊时也先写 spec 草稿请用户确认,不要直接改代码。",
17
- "- 使用 /sdd:specify 技能创建或更新规格文件。",
17
+ "- 规格编写技能(specify)由 AI 自动触发:对话中涉及新需求或需求变更时主动创建或更新规格文件,不需要用户手动调用(不出现在斜杠命令列表中)。",
18
18
  `- 新增或修改 spec 后运行校验:${specCount}(自动检测 docs/specs/,否则 specs/,否则退出)。`,
19
19
  ].join("\n");
20
20
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: specify
3
3
  description: 根据自然语言描述创建或更新功能规格说明,生成包含用户故事与验收场景的规格文件。
4
+ user-invocable: false
4
5
  ---
5
6
 
6
7
  ## 用户输入
@@ -9,13 +10,11 @@ description: 根据自然语言描述创建或更新功能规格说明,生成
9
10
  $ARGUMENTS
10
11
  ```
11
12
 
12
- 你**必须**在继续之前考虑用户输入(如果不为空)。
13
-
14
13
  ## 流程
15
14
 
16
- 用户在 `/sdd:specify` 后输入的文本就是功能描述。不要让用户重复,除非他们提供了空命令。
15
+ 本技能由 AI 在会话中自动触发(不占用手动斜杠命令)。触发时机:用户提出新的需求、修改需求或涉及功能边界时,若对应规格尚未创建或已过期,则主动创建或更新规格说明。$ARGUMENTS 通常为空——需求描述直接来自对话上下文,不要让用户重复。
17
16
 
18
- 根据功能描述,执行以下步骤:
17
+ 根据对话中的功能描述,执行以下步骤:
19
18
 
20
19
  1. **确定规格文件路径**:
21
20
  - **确定规格根目录**:优先复用项目中已有的规格目录——若 `docs/specs/` 存在则用之,否则若 `specs/` 存在则用之,否则默认 `specs/`(并在完成报告中说明所选目录,便于用户纠正)。
@@ -17,13 +17,13 @@ Environment variables are configured in the `env` field of `settings.json`. It i
17
17
 
18
18
  ## Supported `WAVE_*` Environment Variables
19
19
 
20
- Wave uses several environment variables to control its core functionality.
20
+ Wave uses several environment variables to control its core functionality. Variables marked **OS env only** are read from the OS environment (or constructor / stdio `initialize` params) and are **NOT** read from settings.json `env` — set them in your shell, not in the `env` field.
21
21
 
22
22
  | Variable | Description | Default |
23
23
  | :--- | :--- | :--- |
24
24
  | `WAVE_API_KEY` | API key for the AI gateway. | - |
25
25
  | `WAVE_BASE_URL` | Base URL for the AI gateway. | - |
26
- | `WAVE_SERVER_URL` | Server URL for SSO authentication. | `https://codechat.codewave.163.com` |
26
+ | `WAVE_SERVER_URL` | Server URL for SSO authentication. **OS env only** — set via OS env or `options.serverUrl`; not read from settings.json `env` (avoids a startup 401 race). | `https://codechat.codewave.163.com` |
27
27
  | `WAVE_CUSTOM_HEADERS` | Custom HTTP headers for the AI gateway. Newline-separated `Key: Value` pairs (e.g., `"X-Foo: bar\nAuthorization: Bearer xxx"`). | - |
28
28
  | `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
29
29
  | `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
@@ -32,23 +32,29 @@ Wave uses several environment variables to control its core functionality.
32
32
  | `WAVE_DISABLE_AUTO_MEMORY` | Set to `1` or `true` to disable the auto-memory feature. | `false` |
33
33
  | `WAVE_AUTO_MEMORY_FREQUENCY` | Auto memory update frequency. `1` = every turn, `2` = every 2 turns, etc. | `1` |
34
34
  | `WAVE_TASK_LIST_ID` | Explicitly set the task list ID for the session. | (Session ID) |
35
- | `WAVE_PLUGIN_GIT_TIMEOUT_MS` | Timeout in milliseconds for git operations when installing plugins. | `300000` |
35
+ | `WAVE_PLUGIN_GIT_TIMEOUT_MS` | Timeout in milliseconds for git operations when installing plugins. **OS env only** (infrastructure). | `300000` |
36
36
 
37
37
  ## Configuration Scopes
38
38
 
39
- Environment variables can be set in different scopes, with the following precedence (highest to lowest):
39
+ Environment variables can be set in different scopes. Wave merges scopes from lowest to highest priority and stores the result in the agent's **per-session environment snapshot**. The snapshot takes priority over OS environment variables but is **NOT written to `process.env`** — this keeps multiple sessions in one `wave --stdio` process from polluting each other.
40
+
41
+ Precedence (highest to lowest):
40
42
 
41
43
  1. **Local Scope**: `.wave/settings.local.json` (Local overrides, ignored by git)
42
44
  2. **Project Scope**: `.wave/settings.json` (Project-specific settings, shared via git)
43
45
  3. **User Scope**: `~/.wave/settings.json` (Global settings for all projects)
44
- 4. **System Environment**: Variables set in your shell (e.g., `export WAVE_API_KEY=...`)
46
+ 4. **System Environment**: Variables set in your shell (e.g., `export WAVE_API_KEY=...`). Used as a fallback when a key is absent from the settings snapshot.
47
+
48
+ > Settings `env` shadows (does not mutate) OS env: a key set in both settings.json `env` and the OS environment resolves to the settings value for that session, while the OS value remains untouched and visible to unrelated processes.
45
49
 
46
50
  ## Custom Environment Variables
47
51
 
48
- You can also define custom environment variables in the `env` field. These variables will be available to:
52
+ You can also define custom environment variables in the `env` field. These variables are stored in the session's environment snapshot and will be available to:
53
+
54
+ - **Hooks**: Any shell command executed as a hook will have these variables in its environment (merged on top of OS env).
55
+ - **Tools**: Tools like `Bash` will have access to these variables (merged on top of OS env).
49
56
 
50
- - **Hooks**: Any shell command executed as a hook will have these variables in its environment.
51
- - **Tools**: Tools like `Bash` will have access to these variables.
57
+ In `wave --stdio` mode one process hosts multiple sessions; each session keeps its own snapshot, so sessions with different `env` do not pollute each other (no "last session wins").
52
58
 
53
59
  Example:
54
60
  ```json
@@ -62,7 +68,7 @@ Example:
62
68
 
63
69
  ## Live Reload
64
70
 
65
- Environment variables configured in `settings.json` support **live reload**. When you modify the `env` field in any `settings.json` file (user, project, or local scope), the changes take effect immediately without requiring a Wave session restart.
71
+ Environment variables configured in `settings.json` support **live reload**. When you modify the `env` field in any `settings.json` file (user, project, or local scope), the changes take effect immediately without requiring a Wave session restart — the session's environment snapshot is refreshed and subsequent resolve calls / subprocess spawns use the new values.
66
72
 
67
73
  ## Best Practices
68
74
 
@@ -13,7 +13,7 @@ Wave supports the following hook events:
13
13
  - `Stop`: Triggered when Wave finishes its response cycle (no more tool calls).
14
14
  - `SubagentStop`: Triggered when a subagent finishes its response cycle.
15
15
  - `WorktreeCreate`: Triggered when a new worktree is created.
16
- - `WorktreeRemove`: Triggered when a worktree is removed (e.g., via ExitWorktree with `action: "remove"`). Non-blocking. The hook receives `worktree_path` in the JSON input. Useful for cleanup tasks (e.g., `docker compose -p $(basename "$worktree_path") down`) after worktree deletion.
16
+ - `WorktreeRemove`: Triggered before a worktree is removed (e.g., via ExitWorktree with `action: "remove"`). Non-blocking. Fires **before** the worktree directory is deleted so hooks can still read files inside it. The hook receives `worktree_path` in the JSON input. Useful for cleanup tasks (e.g., `docker compose -p $(basename "$worktree_path") down`).
17
17
  - `CwdChanged`: Triggered when the working directory changes (e.g., entering/exiting a worktree). Non-blocking.
18
18
  - `SessionStart`: Triggered during session initialization. Hooks can inject `additionalContext` and `initialUserMessage` via stdout.
19
19
  - `SessionEnd`: Triggered during agent destruction (fire-and-forget, non-blocking). Useful for cleanup, resource teardown, and analytics.
@@ -79,7 +79,7 @@ Wave provides detailed context to hook processes via `stdin` as a JSON object. T
79
79
  - `user_prompt`: (UserPromptSubmit) The text submitted by the user.
80
80
  - `subagent_type`: (If executed by a subagent) The type of the subagent.
81
81
  - `name`: (WorktreeCreate) The name of the new worktree.
82
- - `worktree_path`: (WorktreeRemove) The absolute path to the removed worktree.
82
+ - `worktree_path`: (WorktreeRemove) The absolute path of the worktree about to be removed. Derive the worktree name with `basename "$worktree_path"`.
83
83
  - `old_cwd`: (CwdChanged) The previous working directory.
84
84
  - `new_cwd`: (CwdChanged) The new working directory.
85
85
  - `compact_instructions`: (PreCompact) Custom instructions for the compaction, if any.
@@ -165,6 +165,31 @@ SessionEnd hooks receive `end_source` in the JSON input indicating how the sessi
165
165
  }
166
166
  ```
167
167
 
168
+ ## WorktreeRemove Hooks
169
+
170
+ `WorktreeRemove` hooks fire **before** the worktree directory is deleted, so they can still read files inside it. They are non-blocking (Notification type): the hook never replaces `git worktree remove` itself. Useful for cleaning up external resources that were provisioned for the worktree (databases, containers, etc.).
171
+
172
+ ### Input
173
+ WorktreeRemove hooks receive `worktree_path` in the JSON input (alongside the common fields `session_id`, `transcript_path`, `cwd`, `hook_event_name`). The worktree name can be derived via `basename "$worktree_path"`.
174
+
175
+ ### Example Configuration
176
+ ```json
177
+ {
178
+ "hooks": {
179
+ "WorktreeRemove": [
180
+ {
181
+ "hooks": [
182
+ {
183
+ "command": "worktree_path=$(jq -r '.worktree_path') && docker compose -p \"$(basename \"$worktree_path\")\" down || true",
184
+ "description": "Tear down the worktree's docker compose project before removal"
185
+ }
186
+ ]
187
+ }
188
+ ]
189
+ }
190
+ }
191
+ ```
192
+
168
193
  ## Live Reload
169
194
 
170
195
  Hook configurations support **live reload**. When you modify hooks in `settings.json`, the changes take effect immediately without restarting Wave.
package/dist/agent.d.ts CHANGED
@@ -19,7 +19,6 @@ export declare class Agent {
19
19
  private pluginManager;
20
20
  private skillManager;
21
21
  private cronManager;
22
- private goalManager;
23
22
  private hookManager;
24
23
  private reversionManager;
25
24
  private messageQueue;
@@ -92,10 +91,6 @@ export declare class Agent {
92
91
  get isCommandRunning(): boolean;
93
92
  /** Get queued user-facing messages (excludes background notifications) */
94
93
  get queuedMessages(): QueuedMessage[];
95
- /** Get goal status string */
96
- get goalStatus(): string;
97
- /** Check if a goal is active */
98
- get isGoalActive(): boolean;
99
94
  /**
100
95
  * Remove a queued message by index
101
96
  * @param index - The index of the message to remove
@@ -231,19 +226,6 @@ export declare class Agent {
231
226
  * @param customInstructions - Optional custom instructions for compaction
232
227
  */
233
228
  compact(customInstructions?: string): Promise<void>;
234
- /**
235
- * Set an autonomous goal for the session
236
- * @param condition - The goal condition to achieve
237
- */
238
- setGoal(condition: string): Promise<void>;
239
- /**
240
- * Clear the current autonomous goal
241
- */
242
- clearGoal(): Promise<void>;
243
- /**
244
- * Show the current goal status
245
- */
246
- showGoalStatus(): Promise<void>;
247
229
  /** Unified interrupt method, interrupts both AI messages and command execution */
248
230
  abortMessage(): void;
249
231
  /** Interrupt bash command execution */
@@ -275,11 +257,18 @@ export declare class Agent {
275
257
  */
276
258
  triggerShowRewind(): void;
277
259
  /**
278
- * Ask a side question without tool use
260
+ * Ask a side question without interrupting the main agent.
261
+ *
262
+ * Runs a single-turn fork of the conversation that reuses the main loop's
263
+ * system prompt, tools, and memory injection (prompt-cache friendly) and
264
+ * never executes tools. The abort signal lets the UI cancel a pending
265
+ * question (loading dismiss); aborts propagate as thrown errors.
266
+ *
279
267
  * @param question - The side question to ask
268
+ * @param abortSignal - Optional signal to abort the pending request
280
269
  * @returns Promise that resolves to the AI's answer
281
270
  */
282
- askBtw(question: string): Promise<string>;
271
+ askBtw(question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void, onReasoning?: (content: string) => void): Promise<string>;
283
272
  /**
284
273
  * Send a message to the AI agent with optional images
285
274
  *
package/dist/agent.js CHANGED
@@ -1,8 +1,5 @@
1
1
  import { LspManager } from "./managers/lspManager.js";
2
2
  import { configValidator } from "./utils/configValidator.js";
3
- import { btw } from "./services/aiService.js";
4
- import { convertMessagesForAPI } from "./utils/convertMessagesForAPI.js";
5
- import { supportsVision } from "./utils/modelCapabilities.js";
6
3
  import { parseTaskNotificationXml } from "./utils/notificationXml.js";
7
4
  import { InitializationService } from "./services/initializationService.js";
8
5
  import { InteractionService } from "./services/interactionService.js";
@@ -14,7 +11,10 @@ import { remoteSettingsService } from "./services/remoteSettingsService.js";
14
11
  export class Agent {
15
12
  // Dynamic configuration getter methods
16
13
  getGatewayConfig() {
17
- return this.configurationService.resolveGatewayConfig();
14
+ return {
15
+ ...this.configurationService.resolveGatewayConfig(),
16
+ sessionId: this.messageManager.getSessionId(),
17
+ };
18
18
  }
19
19
  getModelConfig() {
20
20
  return this.configurationService.resolveModelConfig(undefined, undefined, undefined, this.getPermissionMode());
@@ -111,7 +111,6 @@ export class Agent {
111
111
  this.pluginManager = this.container.get("PluginManager");
112
112
  this.bangManager = this.container.get("BangManager");
113
113
  this.cronManager = this.container.get("CronManager");
114
- this.goalManager = this.container.get("GoalManager");
115
114
  this.messageQueue = this.container.get("MessageQueue");
116
115
  // Wire up CWD change callback from AIManager to sync Agent's workdir
117
116
  this.aiManager.setOnCwdChange((newCwd) => {
@@ -138,14 +137,6 @@ export class Agent {
138
137
  if (options.permissionMode) {
139
138
  this.setPermissionMode(options.permissionMode);
140
139
  }
141
- // Wire up goal state change callback
142
- this.goalManager.setOnGoalStateChange((active, condition, elapsed) => {
143
- this.options.callbacks?.onGoalStateChange?.(active, condition, elapsed);
144
- });
145
- // Wire up goal evaluating callback
146
- this.goalManager.setOnGoalEvaluating((evaluating) => {
147
- this.options.callbacks?.onGoalEvaluating?.(evaluating);
148
- });
149
140
  }
150
141
  // Public getter methods
151
142
  get sessionId() {
@@ -216,14 +207,6 @@ export class Agent {
216
207
  .getQueue()
217
208
  .filter((m) => m.type !== "notification");
218
209
  }
219
- /** Get goal status string */
220
- get goalStatus() {
221
- return this.goalManager.getStatusString();
222
- }
223
- /** Check if a goal is active */
224
- get isGoalActive() {
225
- return this.goalManager.isGoalActive();
226
- }
227
210
  /**
228
211
  * Remove a queued message by index
229
212
  * @param index - The index of the message to remove
@@ -521,8 +504,6 @@ export class Agent {
521
504
  }
522
505
  async clearMessages() {
523
506
  this.aiManager.abortAIMessage();
524
- // Clear any active goal
525
- this.goalManager.clearGoal();
526
507
  // Capture old session info before clearing
527
508
  const oldSessionId = this.messageManager.getSessionId();
528
509
  const transcriptPath = this.messageManager.getTranscriptPath();
@@ -571,68 +552,6 @@ export class Agent {
571
552
  });
572
553
  await this.messageManager.saveSession();
573
554
  }
574
- /**
575
- * Set an autonomous goal for the session
576
- * @param condition - The goal condition to achieve
577
- */
578
- async setGoal(condition) {
579
- // Check plan mode
580
- if (this.getPermissionMode() === "plan") {
581
- this.messageManager.addUserMessage({
582
- content: "<system-reminder>Cannot set a goal in plan mode. Exit plan mode first.</system-reminder>",
583
- isMeta: true,
584
- });
585
- return;
586
- }
587
- this.goalManager.setGoal(condition);
588
- this.messageManager.addUserMessage({
589
- content: `<system-reminder>Goal set: ${condition}. The agent will work autonomously until this goal is achieved.</system-reminder>`,
590
- isMeta: true,
591
- });
592
- // Add the goal as a user directive to start working
593
- this.messageManager.addUserMessage({
594
- content: condition,
595
- });
596
- this.aiManager.sendAIMessage();
597
- await this.messageManager.saveSession();
598
- }
599
- /**
600
- * Clear the current autonomous goal
601
- */
602
- async clearGoal() {
603
- if (this.goalManager.isGoalActive()) {
604
- this.goalManager.clearGoal();
605
- this.messageManager.addUserMessage({
606
- content: "<system-reminder>Goal cleared.</system-reminder>",
607
- isMeta: true,
608
- });
609
- }
610
- else {
611
- this.messageManager.addUserMessage({
612
- content: "<system-reminder>No active goal to clear.</system-reminder>",
613
- isMeta: true,
614
- });
615
- }
616
- await this.messageManager.saveSession();
617
- }
618
- /**
619
- * Show the current goal status
620
- */
621
- async showGoalStatus() {
622
- if (this.goalManager.isGoalActive()) {
623
- this.messageManager.addUserMessage({
624
- content: `<system-reminder>${this.goalManager.getStatusString()}</system-reminder>`,
625
- isMeta: true,
626
- });
627
- }
628
- else {
629
- this.messageManager.addUserMessage({
630
- content: "<system-reminder>No active goal. Use /goal <condition> to set one.</system-reminder>",
631
- isMeta: true,
632
- });
633
- }
634
- await this.messageManager.saveSession();
635
- }
636
555
  /** Unified interrupt method, interrupts both AI messages and command execution */
637
556
  abortMessage() {
638
557
  // Guard: prevent tryDispatch (triggered by abortAIMessage → setIsLoading(false))
@@ -702,7 +621,7 @@ export class Agent {
702
621
  transcriptPath,
703
622
  cwd: this.workdir,
704
623
  worktreePath,
705
- env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
624
+ env: Object.fromEntries(Object.entries(this.configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
706
625
  });
707
626
  // Process results via messageManager (may not be visible during shutdown)
708
627
  this.hookManager.processHookResults("WorktreeRemove", hookResults, this.messageManager);
@@ -754,6 +673,17 @@ export class Agent {
754
673
  this.subagentManager.cleanup();
755
674
  // Cleanup forked agent manager
756
675
  await this.forkedAgentManager.cleanup();
676
+ // Drain an in-flight auto-memory extraction fork so the process doesn't
677
+ // exit mid-extraction
678
+ try {
679
+ const autoMemoryService = this.container.get("AutoMemoryService");
680
+ if (autoMemoryService) {
681
+ await autoMemoryService.drain();
682
+ }
683
+ }
684
+ catch (error) {
685
+ this.logger?.warn(`Auto-memory extraction drain failed: ${error.message}`);
686
+ }
757
687
  // Cleanup skill manager
758
688
  await this.skillManager.destroy();
759
689
  // Cleanup live configuration reload
@@ -774,21 +704,20 @@ export class Agent {
774
704
  this.messageManager.triggerShowRewind();
775
705
  }
776
706
  /**
777
- * Ask a side question without tool use
707
+ * Ask a side question without interrupting the main agent.
708
+ *
709
+ * Runs a single-turn fork of the conversation that reuses the main loop's
710
+ * system prompt, tools, and memory injection (prompt-cache friendly) and
711
+ * never executes tools. The abort signal lets the UI cancel a pending
712
+ * question (loading dismiss); aborts propagate as thrown errors.
713
+ *
778
714
  * @param question - The side question to ask
715
+ * @param abortSignal - Optional signal to abort the pending request
779
716
  * @returns Promise that resolves to the AI's answer
780
717
  */
781
- async askBtw(question) {
782
- const messages = convertMessagesForAPI(this.messageManager.getMessages(), {
783
- supportsVision: supportsVision(this.getModelConfig().capabilities),
784
- });
785
- const result = await btw({
786
- gatewayConfig: this.getGatewayConfig(),
787
- modelConfig: this.getModelConfig(),
788
- messages,
789
- question,
790
- });
791
- return result.content;
718
+ async askBtw(question, abortSignal, onContent, onReasoning) {
719
+ const result = await this.aiManager.runBtwFork(question, abortSignal, onContent, onReasoning);
720
+ return result.content ?? result.error ?? "No response received";
792
721
  }
793
722
  /**
794
723
  * Send a message to the AI agent with optional images
@@ -818,7 +747,7 @@ export class Agent {
818
747
  */
819
748
  async sendMessage(content, images) {
820
749
  // If the agent is busy, enqueue the message — unless it's an immediate
821
- // slash command (e.g., /goal clear, /clear, /compact) that should execute
750
+ // slash command (e.g., /clear, /compact) that should execute
822
751
  // right away even while AI is processing
823
752
  if (this.aiManager.isLoading || this.isCommandRunning) {
824
753
  const trimmed = content.trim();
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export * from "./utils/tokenCalculation.js";
22
22
  export * from "./utils/gitUtils.js";
23
23
  export * from "./utils/nameGenerator.js";
24
24
  export * from "./utils/worktreeSession.js";
25
+ export * from "./utils/worktreeUtils.js";
25
26
  export { loadMergedWaveConfig } from "./services/configurationService.js";
26
27
  export * from "./types/index.js";
27
28
  export * from "./tools/buildTool.js";
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export * from "./utils/tokenCalculation.js";
26
26
  export * from "./utils/gitUtils.js";
27
27
  export * from "./utils/nameGenerator.js";
28
28
  export * from "./utils/worktreeSession.js";
29
+ export * from "./utils/worktreeUtils.js";
29
30
  export { loadMergedWaveConfig } from "./services/configurationService.js";
30
31
  export * from "./types/index.js";
31
32
  // Export tool building utilities
@@ -1,6 +1,15 @@
1
- import type { GatewayConfig, ModelConfig, Usage } from "../types/index.js";
1
+ import type { GatewayConfig, ModelConfig, Usage, Message } from "../types/index.js";
2
2
  import { Container } from "../utils/container.js";
3
3
  import type { WorktreeSession } from "../utils/worktreeSession.js";
4
+ /** Result of a fork-path agent loop (compaction or auto-memory extraction). */
5
+ interface ForkLoopResult {
6
+ content?: string;
7
+ usage?: {
8
+ prompt_tokens: number;
9
+ completion_tokens: number;
10
+ total_tokens: number;
11
+ };
12
+ }
4
13
  export interface AIManagerCallbacks {
5
14
  onCompactionStateChange?: (isCompacting: boolean) => void;
6
15
  onUsageAdded?: (usage: Usage) => void;
@@ -53,6 +62,14 @@ export declare class AIManager {
53
62
  private get permissionManager();
54
63
  private get planManager();
55
64
  private get configurationService();
65
+ /**
66
+ * OS env merged with the per-session env snapshot. Falls back to process.env
67
+ * when ConfigurationService is absent or its getMergedEnv is missing (e.g. in
68
+ * unit tests with partial mocks), so hook-context env construction never
69
+ * throws. Use this (not the non-null `configurationService` getter) when
70
+ * building hook context env.
71
+ */
72
+ private get mergedEnv();
56
73
  getGatewayConfig(): GatewayConfig;
57
74
  getModelConfig(): ModelConfig;
58
75
  getMaxInputTokens(): number;
@@ -111,15 +128,60 @@ export declare class AIManager {
111
128
  private buildMainSystemPrompt;
112
129
  private resolveFilteredTools;
113
130
  /**
114
- * Fork-path compaction: run a bounded agent loop over a copy of the
115
- * conversation using the same system prompt, tools, model, and generation
116
- * params as the main loop, so the forked request prefix matches exactly
117
- * and the prompt cache is reused. Tool calls are denied locally (the model
118
- * is told to summarize, not act) and their rejections are fed back for
119
- * another turn. Returns undefined content when the model never produces
120
- * text; the caller treats that as a compaction failure.
131
+ * Fork-path loop: run a bounded agent loop over a copy of the conversation
132
+ * using the same system prompt, tools, model, and generation params as the
133
+ * main loop, so the forked request prefix matches exactly and the prompt
134
+ * cache is reused. A `canUseTool` gate decides whether each tool call
135
+ * executes locally (with a stripped context) or is denied and fed back to
136
+ * the model for another turn. Returns undefined content when the model never
137
+ * produces text; the caller treats that as a failure.
138
+ */
139
+ private runForkLoop;
140
+ /**
141
+ * Fork-path compaction: deny all tool calls locally (the model is told to
142
+ * summarize, not act) and feed the rejections back for another turn.
121
143
  */
122
144
  private runCompactFork;
145
+ /**
146
+ * Auto-memory extraction via the perfect fork: the extraction prompt is run
147
+ * against the same request prefix as the main conversation (same system
148
+ * prompt, tools, model, and message history) so the prompt cache is reused.
149
+ * Gate-approved tools execute locally in a stripped context; everything else
150
+ * is denied. Usage is reported with operation_type "agent" so extraction
151
+ * token costs stay visible in session accounting.
152
+ */
153
+ runAutoMemoryFork(messages: Message[], prompt: string, options: {
154
+ canUseTool: (name: string, args: Record<string, unknown>) => boolean;
155
+ deniedToolMessage?: string;
156
+ maxTurns?: number;
157
+ }, abortSignal?: AbortSignal): Promise<ForkLoopResult>;
158
+ /**
159
+ * Parse a fork tool call's JSON arguments, recovering truncated JSON the
160
+ * same way the main loop does. Unparseable arguments fall back to `{}` and
161
+ * are rejected by the gate or the tool's own parameter validation.
162
+ */
163
+ private parseForkToolArgs;
164
+ /**
165
+ * Execute a single tool call inside a fork with a stripped context: no
166
+ * permission manager (never prompts the user), no message manager (no
167
+ * conditional-rule triggering), no messageId (no file-history snapshots),
168
+ * and no background task manager (commands run in the foreground). Only
169
+ * gate-approved tool names reach this path.
170
+ */
171
+ private executeForkTool;
172
+ /**
173
+ * Fork-path side question ("/btw"): run a single-turn fork of the
174
+ * conversation using the same system prompt, tools, model, and generation
175
+ * params as the main loop, so the forked request prefix matches exactly and
176
+ * the prompt cache is reused. The in-progress assistant message (if any) is
177
+ * stripped so the fork starts from the last completed request prefix. Tools
178
+ * are never executed — the wrapped question instructs the model to answer
179
+ * directly; an attempted tool call is surfaced as an error string.
180
+ */
181
+ runBtwFork(question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void, onReasoning?: (content: string) => void): Promise<{
182
+ content?: string;
183
+ error?: string;
184
+ }>;
123
185
  /**
124
186
  * Build post-compact context restoration content.
125
187
  * Restores file reads, working directory, plan mode, skills, and background tasks.
@@ -158,3 +220,4 @@ export declare class AIManager {
158
220
  */
159
221
  private executePostToolUseHooks;
160
222
  }
223
+ export {};