wave-agent-sdk 0.19.8 → 1.0.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 (95) hide show
  1. package/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
  2. package/builtin/plugins/sdd/hooks/hooks.json +14 -0
  3. package/builtin/plugins/sdd/scripts/session-start.js +24 -0
  4. package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
  5. package/builtin/plugins/sdd/skills/specify/SKILL.md +47 -0
  6. package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
  7. package/builtin/skills/settings/ENV.md +15 -9
  8. package/builtin/skills/settings/HOOKS.md +27 -2
  9. package/dist/agent.d.ts +1 -0
  10. package/dist/agent.js +26 -12
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/managers/aiManager.d.ts +25 -0
  14. package/dist/managers/aiManager.js +172 -51
  15. package/dist/managers/backgroundTaskManager.d.ts +6 -0
  16. package/dist/managers/backgroundTaskManager.js +11 -0
  17. package/dist/managers/bangManager.d.ts +6 -0
  18. package/dist/managers/bangManager.js +11 -0
  19. package/dist/managers/hookManager.d.ts +8 -2
  20. package/dist/managers/hookManager.js +14 -4
  21. package/dist/managers/mcpManager.d.ts +18 -4
  22. package/dist/managers/mcpManager.js +40 -18
  23. package/dist/managers/permissionManager.d.ts +7 -0
  24. package/dist/managers/permissionManager.js +102 -142
  25. package/dist/managers/pluginManager.d.ts +7 -0
  26. package/dist/managers/pluginManager.js +31 -0
  27. package/dist/managers/toolManager.js +5 -0
  28. package/dist/prompts/index.d.ts +12 -1
  29. package/dist/prompts/index.js +133 -45
  30. package/dist/services/aiService.d.ts +1 -17
  31. package/dist/services/aiService.js +3 -85
  32. package/dist/services/configurationService.d.ts +21 -2
  33. package/dist/services/configurationService.js +72 -23
  34. package/dist/services/initializationService.js +14 -4
  35. package/dist/services/interactionService.js +35 -7
  36. package/dist/services/remoteSettingsService.d.ts +12 -0
  37. package/dist/services/remoteSettingsService.js +15 -1
  38. package/dist/services/session.d.ts +3 -1
  39. package/dist/services/session.js +12 -4
  40. package/dist/services/taskManager.d.ts +1 -0
  41. package/dist/services/taskManager.js +41 -6
  42. package/dist/tools/bashTool.js +1 -0
  43. package/dist/tools/editTool.js +24 -10
  44. package/dist/tools/enterWorktreeTool.js +14 -3
  45. package/dist/tools/exitWorktreeTool.js +11 -10
  46. package/dist/tools/grepTool.js +8 -2
  47. package/dist/tools/types.d.ts +7 -0
  48. package/dist/tools/writeTool.js +36 -0
  49. package/dist/types/config.d.ts +2 -0
  50. package/dist/types/hooks.d.ts +2 -2
  51. package/dist/utils/bashParser.d.ts +25 -0
  52. package/dist/utils/bashParser.js +103 -0
  53. package/dist/utils/configPaths.d.ts +4 -0
  54. package/dist/utils/configPaths.js +6 -0
  55. package/dist/utils/containerSetup.js +1 -1
  56. package/dist/utils/fileSearch.js +4 -2
  57. package/dist/utils/openaiClient.js +2 -1
  58. package/dist/utils/pathEncoder.js +7 -2
  59. package/dist/utils/worktreeUtils.d.ts +17 -0
  60. package/dist/utils/worktreeUtils.js +339 -1
  61. package/package.json +1 -1
  62. package/src/agent.ts +26 -12
  63. package/src/index.ts +1 -0
  64. package/src/managers/aiManager.ts +238 -66
  65. package/src/managers/backgroundTaskManager.ts +15 -0
  66. package/src/managers/bangManager.ts +15 -0
  67. package/src/managers/hookManager.ts +20 -5
  68. package/src/managers/mcpManager.ts +60 -18
  69. package/src/managers/permissionManager.ts +116 -168
  70. package/src/managers/pluginManager.ts +29 -0
  71. package/src/managers/toolManager.ts +7 -0
  72. package/src/prompts/index.ts +144 -37
  73. package/src/services/aiService.ts +9 -128
  74. package/src/services/configurationService.ts +84 -23
  75. package/src/services/initializationService.ts +17 -4
  76. package/src/services/interactionService.ts +49 -6
  77. package/src/services/remoteSettingsService.ts +16 -1
  78. package/src/services/session.ts +18 -4
  79. package/src/services/taskManager.ts +56 -8
  80. package/src/tools/bashTool.ts +1 -0
  81. package/src/tools/editTool.ts +29 -11
  82. package/src/tools/enterWorktreeTool.ts +19 -2
  83. package/src/tools/exitWorktreeTool.ts +15 -12
  84. package/src/tools/grepTool.ts +11 -2
  85. package/src/tools/types.ts +7 -0
  86. package/src/tools/writeTool.ts +43 -0
  87. package/src/types/config.ts +2 -0
  88. package/src/types/hooks.ts +2 -2
  89. package/src/utils/bashParser.ts +106 -0
  90. package/src/utils/configPaths.ts +7 -0
  91. package/src/utils/containerSetup.ts +3 -1
  92. package/src/utils/fileSearch.ts +6 -2
  93. package/src/utils/openaiClient.ts +2 -0
  94. package/src/utils/pathEncoder.ts +7 -2
  95. package/src/utils/worktreeUtils.ts +401 -1
@@ -149,53 +149,141 @@ This is critical - your turn should only end with either using the ${ASK_USER_QU
149
149
  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.`;
150
150
  }
151
151
  export const DEFAULT_SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
152
- export const COMPACT_MESSAGES_SYSTEM_PROMPT = `You are continuing work on a software engineering task. Write a detailed continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary.
152
+ // Aggressive no-tools preamble, aligned with Claude Code's NO_TOOLS_PREAMBLE.
153
+ // The fork path inherits the main conversation's full tool set (required for
154
+ // cache-key match), so the instruction must be explicit about rejection
155
+ // consequences to prevent wasted turns.
156
+ const COMPACT_NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
153
157
 
154
- First, write your analysis in <analysis> tags as a thinking scratchpad:
155
- - Chronologically review the conversation
156
- - Identify user intents and goals
157
- - Note files read/modified, approaches tried, decisions made
158
- - Check for accuracy and completeness — ensure nothing critical is missing
158
+ - Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
159
+ - You already have all the context you need in the conversation above.
160
+ - Tool calls will be REJECTED and will waste your only turn — you will fail the task.
161
+ - Your entire response must be plain text: an <analysis> block followed by a <summary> block.
159
162
 
160
- Then produce a structured summary in <summary> tags with these sections:
161
-
162
- ## Primary Request and Intent
163
- - The user's core request and success criteria
164
- - Clarifications, constraints, or scope changes
165
-
166
- ## Key Technical Concepts
167
- - Frameworks, libraries, patterns, architectural decisions
168
-
169
- ## Files and Code Sections
170
- - Files read, modified, created (with full paths)
171
- - Critical code snippets (function signatures, bug fixes, key logic)
172
- - Focus on recent messages — include full code for important sections
173
-
174
- ## Errors and Fixes
175
- - Errors encountered, root causes, how they were resolved
176
- - Approaches tried that didn't work and why
177
-
178
- ## Problem Solving
179
- - Approach evolution, trade-offs considered, decisions made
180
-
181
- ## All User Messages
182
- - Complete list of all user messages (non-tool content)
183
- - Preserve exact wording where load-bearing
184
-
185
- ## Pending Tasks
186
- - Outstanding work, TODOs, unresolved questions
187
-
188
- ## Current Work
189
- - What was being worked on at the time of summarization
190
- - Exact state of in-progress changes
191
-
192
- ## Optional Next Step
193
- - Immediate next action needed
194
- - Include verbatim quotes from recent conversation if relevant
195
-
196
- Be concise but complete — include information that prevents duplicate work or repeated mistakes.
197
- Respond with text only. Do NOT call any tools.
198
- Wrap your summary in <summary></summary> tags.`;
163
+ `;
164
+ // Aligned with Claude Code's DETAILED_ANALYSIS_INSTRUCTION_BASE. The
165
+ // <analysis> block is a drafting scratchpad that formatCompactSummary()
166
+ // strips before the summary reaches context.
167
+ const COMPACT_DETAILED_ANALYSIS_INSTRUCTION = `Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
168
+
169
+ 1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
170
+ - The user's explicit requests and intents
171
+ - Your approach to addressing the user's requests
172
+ - Key decisions, technical concepts and code patterns
173
+ - Specific details like:
174
+ - file names
175
+ - full code snippets
176
+ - function signatures
177
+ - file edits
178
+ - Errors that you ran into and how you fixed them
179
+ - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
180
+ 2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.`;
181
+ // Aligned with Claude Code's BASE_COMPACT_PROMPT (9 sections + example).
182
+ const BASE_COMPACT_PROMPT = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
183
+ This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
184
+
185
+ ${COMPACT_DETAILED_ANALYSIS_INSTRUCTION}
186
+
187
+ Your summary should include the following sections:
188
+
189
+ 1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
190
+ 2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
191
+ 3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
192
+ 4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
193
+ 5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
194
+ 6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
195
+ 7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
196
+ 8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
197
+ 9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
198
+ If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
199
+
200
+ Here's an example of how your output should be structured:
201
+
202
+ <example>
203
+ <analysis>
204
+ [Your thought process, ensuring all points are covered thoroughly and accurately]
205
+ </analysis>
206
+
207
+ <summary>
208
+ 1. Primary Request and Intent:
209
+ [Detailed description]
210
+
211
+ 2. Key Technical Concepts:
212
+ - [Concept 1]
213
+ - [Concept 2]
214
+ - [...]
215
+
216
+ 3. Files and Code Sections:
217
+ - [File Name 1]
218
+ - [Summary of why this file is important]
219
+ - [Summary of the changes made to this file, if any]
220
+ - [Important Code Snippet]
221
+ - [File Name 2]
222
+ - [Important Code Snippet]
223
+ - [...]
224
+
225
+ 4. Errors and fixes:
226
+ - [Detailed description of error 1]:
227
+ - [How you fixed the error]
228
+ - [User feedback on the error if any]
229
+ - [...]
230
+
231
+ 5. Problem Solving:
232
+ [Description of solved problems and ongoing troubleshooting]
233
+
234
+ 6. All user messages:
235
+ - [Detailed non tool use user message]
236
+ - [...]
237
+
238
+ 7. Pending Tasks:
239
+ - [Task 1]
240
+ - [Task 2]
241
+ - [...]
242
+
243
+ 8. Current Work:
244
+ [Precise description of current work]
245
+
246
+ 9. Optional Next Step:
247
+ [Optional Next step to take]
248
+
249
+ </summary>
250
+ </example>
251
+
252
+ Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
253
+
254
+ There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary.`;
255
+ const COMPACT_NO_TOOLS_TRAILER = "\n\nREMINDER: Do NOT call any tools. Respond with plain text only — " +
256
+ "an <analysis> block followed by a <summary> block. " +
257
+ "Tool calls will be rejected and you will fail the task.";
258
+ /**
259
+ * Builds the compact instruction sent as the trailing user message on the
260
+ * fork path. Aligned with Claude Code's getCompactPrompt().
261
+ */
262
+ export function getCompactPrompt(customInstructions) {
263
+ let prompt = COMPACT_NO_TOOLS_PREAMBLE + BASE_COMPACT_PROMPT;
264
+ if (customInstructions && customInstructions.trim() !== "") {
265
+ prompt += `\n\nAdditional Instructions:\n${customInstructions}`;
266
+ }
267
+ prompt += COMPACT_NO_TOOLS_TRAILER;
268
+ return prompt;
269
+ }
270
+ /**
271
+ * Formats the compact summary by stripping the <analysis> drafting scratchpad
272
+ * and extracting the <summary> section. Raw text passes through unchanged
273
+ * when no <summary> tag is present. Aligned with Claude Code's
274
+ * formatCompactSummary().
275
+ */
276
+ export function formatCompactSummary(summary) {
277
+ let formattedSummary = summary;
278
+ formattedSummary = formattedSummary.replace(/<analysis>[\s\S]*?<\/analysis>/, "");
279
+ const summaryMatch = formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/);
280
+ if (summaryMatch) {
281
+ const content = summaryMatch[1] || "";
282
+ formattedSummary = formattedSummary.replace(/<summary>[\s\S]*?<\/summary>/, `Summary:\n${content.trim()}`);
283
+ }
284
+ formattedSummary = formattedSummary.replace(/\n\n+/g, "\n\n");
285
+ return formattedSummary.trim();
286
+ }
199
287
  export const WEB_CONTENT_SYSTEM_PROMPT = `You are a helpful assistant that extracts information from web content. The content is provided in Markdown format.`;
200
288
  export const BTW_SYSTEM_PROMPT = `You are a helpful assistant. Answer the user's side question based on the conversation history.
201
289
  Do NOT say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action.
@@ -24,6 +24,7 @@ export interface CallAgentOptions {
24
24
  name: string;
25
25
  };
26
26
  };
27
+ stream?: boolean;
27
28
  onContentUpdate?: (content: string) => void;
28
29
  onToolUpdate?: (toolCall: {
29
30
  id: string;
@@ -44,23 +45,6 @@ export interface CallAgentResult {
44
45
  additionalFields?: Record<string, unknown>;
45
46
  }
46
47
  export declare function callAgent(options: CallAgentOptions): Promise<CallAgentResult>;
47
- export interface CompactMessagesOptions {
48
- gatewayConfig: GatewayConfig;
49
- modelConfig: ModelConfig;
50
- messages: ChatCompletionMessageParam[];
51
- abortSignal?: AbortSignal;
52
- model?: string;
53
- customInstructions?: string;
54
- }
55
- export interface CompactMessagesResult {
56
- content: string;
57
- usage?: {
58
- prompt_tokens: number;
59
- completion_tokens: number;
60
- total_tokens: number;
61
- };
62
- }
63
- export declare function compactMessages(options: CompactMessagesOptions): Promise<CompactMessagesResult>;
64
48
  export interface ProcessWebContentOptions {
65
49
  gatewayConfig: GatewayConfig;
66
50
  modelConfig: ModelConfig;
@@ -7,7 +7,7 @@ import { supportsPromptCaching } from "../utils/modelCapabilities.js";
7
7
  import * as os from "os";
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
10
- import { COMPACT_MESSAGES_SYSTEM_PROMPT, WEB_CONTENT_SYSTEM_PROMPT, BTW_SYSTEM_PROMPT, } from "../prompts/index.js";
10
+ import { WEB_CONTENT_SYSTEM_PROMPT, BTW_SYSTEM_PROMPT, } from "../prompts/index.js";
11
11
  import { GOAL_EVALUATION_SYSTEM_PROMPT } from "../constants/goalPrompts.js";
12
12
  // Global rate limiter state for 1 QPS
13
13
  let nextAllowedTime = 0;
@@ -149,9 +149,8 @@ export async function callAgent(options) {
149
149
  ...(modelConfig.options || {}),
150
150
  });
151
151
  // Determine if streaming is needed
152
- const isStreaming = !!(onContentUpdate ||
153
- onToolUpdate ||
154
- onReasoningUpdate);
152
+ const isStreaming = options.stream === true ||
153
+ !!(onContentUpdate || onToolUpdate || onReasoningUpdate);
155
154
  // Prepare API call parameters
156
155
  createParams = {
157
156
  ...openaiModelConfig,
@@ -488,87 +487,6 @@ async function processStreamingResponse(stream, onContentUpdate, onToolUpdate, o
488
487
  }
489
488
  return result;
490
489
  }
491
- export async function compactMessages(options) {
492
- const { gatewayConfig, modelConfig, messages, abortSignal } = options;
493
- // Validate model config at call time
494
- validateModelConfig(modelConfig);
495
- // Apply global 1 QPS rate limit
496
- if (process.env.NODE_ENV !== "test" ||
497
- modelConfig.model === "rate-limit-test") {
498
- await acquireSlot(abortSignal);
499
- }
500
- // Strip images from messages before compact API call to reduce token usage
501
- const cleanedMessages = messages.map((msg) => {
502
- // Handle user/assistant messages with array content
503
- if (Array.isArray(msg.content)) {
504
- const textParts = msg.content.filter((part) => part.type === "text");
505
- const text = textParts.map((p) => p.text).join("\n");
506
- return { ...msg, content: text || "(empty message)" };
507
- }
508
- return msg;
509
- });
510
- // Create OpenAI client with injected configuration
511
- const openai = new OpenAIClient({
512
- apiKey: gatewayConfig.apiKey,
513
- baseURL: gatewayConfig.baseURL,
514
- defaultHeaders: gatewayConfig.defaultHeaders,
515
- fetchOptions: gatewayConfig.fetchOptions,
516
- fetch: gatewayConfig.fetch,
517
- });
518
- // When a fast model override is provided, use the fast model's options
519
- // (if configured); otherwise fall back to the agent model's options.
520
- const activeExtraParams = options.model
521
- ? modelConfig.fastModelOptions || {}
522
- : modelConfig.options || {};
523
- const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
524
- temperature: 0.1,
525
- max_tokens: 8192,
526
- ...activeExtraParams,
527
- });
528
- try {
529
- const response = await openai.chat.completions.create({
530
- ...openaiModelConfig,
531
- messages: [
532
- {
533
- role: "system",
534
- content: COMPACT_MESSAGES_SYSTEM_PROMPT,
535
- },
536
- ...cleanedMessages,
537
- {
538
- role: "user",
539
- content: options.customInstructions
540
- ? `Please create a detailed summary of the conversation so far. Pay special attention to these instructions: ${options.customInstructions}`
541
- : `Please create a detailed summary of the conversation so far.`,
542
- },
543
- ],
544
- }, {
545
- signal: abortSignal,
546
- });
547
- const content = response.choices[0]?.message?.content?.trim();
548
- if (!content) {
549
- throw new Error("Failed to compact conversation history: Empty response from AI");
550
- }
551
- const usage = response.usage
552
- ? {
553
- prompt_tokens: response.usage.prompt_tokens,
554
- completion_tokens: response.usage.completion_tokens,
555
- total_tokens: response.usage.total_tokens,
556
- }
557
- : undefined;
558
- return {
559
- content,
560
- usage,
561
- };
562
- }
563
- catch (error) {
564
- if (error.name === "AbortError") {
565
- logger.info("Compaction request was aborted");
566
- throw new Error("Compaction request was aborted");
567
- }
568
- logger.error("Failed to compact messages:", error);
569
- throw error;
570
- }
571
- }
572
490
  export async function processWebContent(options) {
573
491
  const { gatewayConfig, modelConfig, content, prompt, abortSignal } = options;
574
492
  // Validate model config at call time
@@ -18,10 +18,23 @@ export declare class ConfigurationService {
18
18
  private currentConfiguration;
19
19
  private options;
20
20
  private _configuredEnvKeys;
21
+ private envSnapshot;
21
22
  /**
22
23
  * Set agent options for configuration resolution
23
24
  */
24
25
  setOptions(options: AgentOptions): void;
26
+ /**
27
+ * Returns a copy of the per-session environment snapshot (settings.json `env`).
28
+ * Priority over OS env; does NOT include OS env. For subprocess spawning use
29
+ * {@link getMergedEnv} instead.
30
+ */
31
+ getEnvSnapshot(): Record<string, string>;
32
+ /**
33
+ * Returns OS env merged with the session snapshot (snapshot wins). Use this
34
+ * when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
35
+ * so they inherit both OS env and the session's settings env.
36
+ */
37
+ getMergedEnv(): Record<string, string>;
25
38
  /**
26
39
  * Load and merge configuration with comprehensive validation
27
40
  */
@@ -35,8 +48,14 @@ export declare class ConfigurationService {
35
48
  */
36
49
  validateConfigurationFile(filePath: string): ValidationResult;
37
50
  /**
38
- * Set environment variables from configuration
39
- * This replaces direct process.env modification
51
+ * Store environment variables from configuration into the per-session
52
+ * snapshot (NOT process.env). Settings `env` shadows OS env for this session
53
+ * only — multiple sessions in one stdio process stay isolated.
54
+ *
55
+ * Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
56
+ * process-level singletons (AuthService, remoteSettingsService background
57
+ * fetch) need to read it and don't hold a per-session snapshot. Same value
58
+ * across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
40
59
  */
41
60
  setEnvironmentVars(env: Record<string, string>): void;
42
61
  /**
@@ -28,6 +28,10 @@ export class ConfigurationService {
28
28
  this.currentConfiguration = null;
29
29
  this.options = {};
30
30
  this._configuredEnvKeys = new Set();
31
+ // Per-session environment snapshot: settings.json `env` is stored here (NOT
32
+ // written to process.env) so multiple sessions in one `wave --stdio` process
33
+ // don't cross-pollute. Resolve methods read `this.envSnapshot ?? process.env`.
34
+ this.envSnapshot = {};
31
35
  }
32
36
  /**
33
37
  * Set agent options for configuration resolution
@@ -35,6 +39,22 @@ export class ConfigurationService {
35
39
  setOptions(options) {
36
40
  this.options = options;
37
41
  }
42
+ /**
43
+ * Returns a copy of the per-session environment snapshot (settings.json `env`).
44
+ * Priority over OS env; does NOT include OS env. For subprocess spawning use
45
+ * {@link getMergedEnv} instead.
46
+ */
47
+ getEnvSnapshot() {
48
+ return { ...this.envSnapshot };
49
+ }
50
+ /**
51
+ * Returns OS env merged with the session snapshot (snapshot wins). Use this
52
+ * when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
53
+ * so they inherit both OS env and the session's settings env.
54
+ */
55
+ getMergedEnv() {
56
+ return Object.fromEntries(Object.entries({ ...process.env, ...this.envSnapshot }).filter(([, v]) => v !== undefined));
57
+ }
38
58
  // Core loading operations
39
59
  /**
40
60
  * Load and merge configuration with comprehensive validation
@@ -292,16 +312,31 @@ export class ConfigurationService {
292
312
  }
293
313
  // Utility operations
294
314
  /**
295
- * Set environment variables from configuration
296
- * This replaces direct process.env modification
315
+ * Store environment variables from configuration into the per-session
316
+ * snapshot (NOT process.env). Settings `env` shadows OS env for this session
317
+ * only — multiple sessions in one stdio process stay isolated.
318
+ *
319
+ * Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
320
+ * process-level singletons (AuthService, remoteSettingsService background
321
+ * fetch) need to read it and don't hold a per-session snapshot. Same value
322
+ * across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
297
323
  */
298
324
  setEnvironmentVars(env) {
299
325
  for (const [key, value] of Object.entries(env)) {
300
- if (process.env[key] !== undefined && !this._configuredEnvKeys.has(key)) {
326
+ if (process.env[key] !== undefined &&
327
+ !this._configuredEnvKeys.has(key) &&
328
+ process.env[key] !== value) {
301
329
  logger.warn(`Overriding environment variable: ${key}`);
302
330
  }
303
- process.env[key] = value;
331
+ this.envSnapshot[key] = value;
304
332
  this._configuredEnvKeys.add(key);
333
+ // WAVE_SERVER_URL is consumed by process-level singletons (AuthService,
334
+ // remoteSettingsService) that can't see the per-session snapshot — mirror
335
+ // it to process.env so they read the settings value. Same value across
336
+ // sessions ⇒ no last-session-wins cross-pollution.
337
+ if (key === "WAVE_SERVER_URL") {
338
+ process.env[key] = value;
339
+ }
305
340
  }
306
341
  }
307
342
  // =============================================================================
@@ -337,8 +372,11 @@ export class ConfigurationService {
337
372
  * @returns Resolved model configuration (model/fastModel may be undefined if not yet configured)
338
373
  */
339
374
  resolveGatewayConfig(apiKey, baseURL, defaultHeaders, fetchOptions, fetch) {
340
- // Check for SSO token first - if present and server URL is available, use SSO mode
341
- // Server URL resolution: options > process.env > default
375
+ // Check for SSO token first - if present and server URL is available, use SSO mode.
376
+ // Server URL resolution: options.serverUrl > process.env.WAVE_SERVER_URL > default.
377
+ // settings.json `env` WAVE_SERVER_URL is mirrored to process.env by
378
+ // setEnvironmentVars (so process-level singletons AuthService / remoteSettings
379
+ // can read it), then read here. See docs/specs/core/agent-config.md.
342
380
  const ssoToken = this.readSSOToken();
343
381
  const serverUrl = this.options.serverUrl ||
344
382
  process.env.WAVE_SERVER_URL ||
@@ -366,7 +404,8 @@ export class ConfigurationService {
366
404
  resolvedApiKey = this.options.apiKey;
367
405
  }
368
406
  else {
369
- resolvedApiKey = process.env.WAVE_API_KEY;
407
+ resolvedApiKey =
408
+ this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
370
409
  }
371
410
  // Resolve base URL: override > options > env (settings.json) > process.env
372
411
  // Note: Explicitly provided empty strings should be treated as invalid, not fall back to env
@@ -378,21 +417,26 @@ export class ConfigurationService {
378
417
  resolvedBaseURL = this.options.baseURL;
379
418
  }
380
419
  else {
381
- resolvedBaseURL = process.env.WAVE_BASE_URL;
420
+ resolvedBaseURL =
421
+ this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
382
422
  }
383
423
  // Fallback to process.env if still not resolved (for dynamic updates in tests)
384
424
  if (resolvedApiKey === undefined) {
385
- resolvedApiKey = process.env.WAVE_API_KEY;
425
+ resolvedApiKey =
426
+ this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
386
427
  }
387
428
  if (!resolvedBaseURL) {
388
- resolvedBaseURL = process.env.WAVE_BASE_URL;
429
+ resolvedBaseURL =
430
+ this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
389
431
  }
390
432
  // Treat empty string as not provided
391
433
  if (resolvedBaseURL?.trim() === "") {
392
434
  resolvedBaseURL = undefined;
393
435
  }
394
436
  // Resolve custom headers from environment: env (settings.json) > process.env
395
- const envCustomHeaders = process.env.WAVE_CUSTOM_HEADERS || "";
437
+ const envCustomHeaders = this.envSnapshot.WAVE_CUSTOM_HEADERS ??
438
+ process.env.WAVE_CUSTOM_HEADERS ??
439
+ "";
396
440
  const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
397
441
  // Merge headers: env headers < options < override
398
442
  const resolvedHeaders = {
@@ -424,9 +468,11 @@ export class ConfigurationService {
424
468
  const resolvedAgentModel = model ||
425
469
  this.options.model ||
426
470
  this.currentConfiguration?.model ||
427
- process.env.WAVE_MODEL;
471
+ (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
428
472
  // Resolve fast model: override > options > process.env (includes settings.json env)
429
- const resolvedFastModel = fastModel || this.options.fastModel || process.env.WAVE_FAST_MODEL;
473
+ const resolvedFastModel = fastModel ||
474
+ this.options.fastModel ||
475
+ (this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
430
476
  // Resolve max output tokens
431
477
  const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
432
478
  const baseConfig = {
@@ -468,8 +514,9 @@ export class ConfigurationService {
468
514
  if (this.options.maxInputTokens !== undefined) {
469
515
  return this.options.maxInputTokens;
470
516
  }
471
- // Try env (settings.json) first, then process.env
472
- const envMaxInputTokens = process.env.WAVE_MAX_INPUT_TOKENS;
517
+ // Try env (settings.json snapshot) first, then process.env
518
+ const envMaxInputTokens = this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
519
+ process.env.WAVE_MAX_INPUT_TOKENS;
473
520
  if (envMaxInputTokens) {
474
521
  const parsed = parseInt(envMaxInputTokens, 10);
475
522
  if (!isNaN(parsed)) {
@@ -510,8 +557,8 @@ export class ConfigurationService {
510
557
  if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
511
558
  return this.currentConfiguration.autoMemoryEnabled;
512
559
  }
513
- // 2. WAVE_DISABLE_AUTO_MEMORY environment variable
514
- const disableAutoMemory = process.env.WAVE_DISABLE_AUTO_MEMORY ||
560
+ // 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
561
+ const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
515
562
  process.env.WAVE_DISABLE_AUTO_MEMORY;
516
563
  if (disableAutoMemory === "1" || disableAutoMemory === "true") {
517
564
  return false;
@@ -541,8 +588,8 @@ export class ConfigurationService {
541
588
  if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
542
589
  return this.currentConfiguration.autoMemoryFrequency;
543
590
  }
544
- // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
545
- const envFrequency = process.env.WAVE_AUTO_MEMORY_FREQUENCY ||
591
+ // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
592
+ const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
546
593
  process.env.WAVE_AUTO_MEMORY_FREQUENCY;
547
594
  if (envFrequency) {
548
595
  const parsed = parseInt(envFrequency, 10);
@@ -568,8 +615,9 @@ export class ConfigurationService {
568
615
  if (this.options.maxTokens !== undefined) {
569
616
  return this.options.maxTokens;
570
617
  }
571
- // Try env (settings.json) first, then process.env
572
- const envMaxOutputTokens = process.env.WAVE_MAX_OUTPUT_TOKENS;
618
+ // Try env (settings.json snapshot) first, then process.env
619
+ const envMaxOutputTokens = this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
620
+ process.env.WAVE_MAX_OUTPUT_TOKENS;
573
621
  if (envMaxOutputTokens) {
574
622
  const parsed = parseInt(envMaxOutputTokens, 10);
575
623
  if (!isNaN(parsed) && parsed > 0) {
@@ -610,8 +658,9 @@ export class ConfigurationService {
610
658
  */
611
659
  getConfiguredModels() {
612
660
  const models = new Set();
613
- // Add current model from options or environment
614
- const currentModel = this.options.model || process.env.WAVE_MODEL;
661
+ // Add current model from options or environment (settings snapshot > OS env)
662
+ const currentModel = this.options.model ||
663
+ (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
615
664
  if (currentModel) {
616
665
  models.add(currentModel);
617
666
  }
@@ -44,12 +44,16 @@ export class InitializationService {
44
44
  logger?.error("Failed to initialize MCP servers:", error);
45
45
  // Don't throw error to prevent app startup failure
46
46
  }
47
- // Initialize remote settings (load disk cache synchronously, then fetch in background)
48
- // Must happen BEFORE loadMergedConfiguration so remote env vars are available
47
+ // Load remote settings disk cache synchronously.
48
+ // Must happen BEFORE loadMergedConfiguration so cached managed settings
49
+ // (env, model, disallowedTools) are merged into the config. Settings `env`
50
+ // is stored in the per-session env snapshot (NOT process.env), except
51
+ // WAVE_SERVER_URL which is mirrored to process.env so the network fetch
52
+ // (below) can read it via authService.getServerUrl(); no race.
49
53
  try {
50
54
  const phaseStart = performance.now();
51
55
  await remoteSettingsService.initialize();
52
- logger?.debug(`Initialization Phase [Remote Settings] took ${(performance.now() - phaseStart).toFixed(2)}ms`);
56
+ logger?.debug(`Initialization Phase [Remote Settings Cache] took ${(performance.now() - phaseStart).toFixed(2)}ms`);
53
57
  }
54
58
  catch (error) {
55
59
  logger?.error("Failed to initialize remote settings:", error);
@@ -89,6 +93,12 @@ export class InitializationService {
89
93
  logger?.error("Failed to initialize hooks system:", error);
90
94
  // Don't throw error to prevent app startup failure
91
95
  }
96
+ // Start remote settings network fetch + polling now that the config is
97
+ // merged. Settings `env` WAVE_SERVER_URL was mirrored to process.env by
98
+ // loadMergedConfiguration → setEnvironmentVars, so the fetch reads it via
99
+ // authService.getServerUrl(); fire-and-forget, failures fall back to the
100
+ // cached/merged settings.
101
+ remoteSettingsService.startBackgroundFetch();
92
102
  // Execute SessionStart hooks
93
103
  try {
94
104
  const phaseStart = performance.now();
@@ -124,7 +134,7 @@ export class InitializationService {
124
134
  transcriptPath: messageManager.getTranscriptPath(),
125
135
  cwd: workdir,
126
136
  worktreeName: agentOptions.worktreeName,
127
- env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
137
+ env: Object.fromEntries(Object.entries(configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
128
138
  });
129
139
  // Process hook results
130
140
  hookManager.processHookResults("WorktreeCreate", hookResults, messageManager);