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
@@ -190,53 +190,160 @@ export interface SystemPromptBlock {
190
190
  cacheable: boolean;
191
191
  }
192
192
 
193
- 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.
193
+ // Aggressive no-tools preamble, aligned with Claude Code's NO_TOOLS_PREAMBLE.
194
+ // The fork path inherits the main conversation's full tool set (required for
195
+ // cache-key match), so the instruction must be explicit about rejection
196
+ // consequences to prevent wasted turns.
197
+ const COMPACT_NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
194
198
 
195
- First, write your analysis in <analysis> tags as a thinking scratchpad:
196
- - Chronologically review the conversation
197
- - Identify user intents and goals
198
- - Note files read/modified, approaches tried, decisions made
199
- - Check for accuracy and completeness — ensure nothing critical is missing
199
+ - Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
200
+ - You already have all the context you need in the conversation above.
201
+ - Tool calls will be REJECTED and will waste your only turn — you will fail the task.
202
+ - Your entire response must be plain text: an <analysis> block followed by a <summary> block.
200
203
 
201
- Then produce a structured summary in <summary> tags with these sections:
202
-
203
- ## Primary Request and Intent
204
- - The user's core request and success criteria
205
- - Clarifications, constraints, or scope changes
206
-
207
- ## Key Technical Concepts
208
- - Frameworks, libraries, patterns, architectural decisions
204
+ `;
209
205
 
210
- ## Files and Code Sections
211
- - Files read, modified, created (with full paths)
212
- - Critical code snippets (function signatures, bug fixes, key logic)
213
- - Focus on recent messages include full code for important sections
206
+ // Aligned with Claude Code's DETAILED_ANALYSIS_INSTRUCTION_BASE. The
207
+ // <analysis> block is a drafting scratchpad that formatCompactSummary()
208
+ // strips before the summary reaches context.
209
+ 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:
210
+
211
+ 1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
212
+ - The user's explicit requests and intents
213
+ - Your approach to addressing the user's requests
214
+ - Key decisions, technical concepts and code patterns
215
+ - Specific details like:
216
+ - file names
217
+ - full code snippets
218
+ - function signatures
219
+ - file edits
220
+ - Errors that you ran into and how you fixed them
221
+ - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
222
+ 2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.`;
223
+
224
+ // Aligned with Claude Code's BASE_COMPACT_PROMPT (9 sections + example).
225
+ 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.
226
+ 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.
227
+
228
+ ${COMPACT_DETAILED_ANALYSIS_INSTRUCTION}
229
+
230
+ Your summary should include the following sections:
231
+
232
+ 1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
233
+ 2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
234
+ 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.
235
+ 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.
236
+ 5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
237
+ 6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
238
+ 7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
239
+ 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.
240
+ 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.
241
+ 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.
242
+
243
+ Here's an example of how your output should be structured:
244
+
245
+ <example>
246
+ <analysis>
247
+ [Your thought process, ensuring all points are covered thoroughly and accurately]
248
+ </analysis>
249
+
250
+ <summary>
251
+ 1. Primary Request and Intent:
252
+ [Detailed description]
253
+
254
+ 2. Key Technical Concepts:
255
+ - [Concept 1]
256
+ - [Concept 2]
257
+ - [...]
258
+
259
+ 3. Files and Code Sections:
260
+ - [File Name 1]
261
+ - [Summary of why this file is important]
262
+ - [Summary of the changes made to this file, if any]
263
+ - [Important Code Snippet]
264
+ - [File Name 2]
265
+ - [Important Code Snippet]
266
+ - [...]
267
+
268
+ 4. Errors and fixes:
269
+ - [Detailed description of error 1]:
270
+ - [How you fixed the error]
271
+ - [User feedback on the error if any]
272
+ - [...]
273
+
274
+ 5. Problem Solving:
275
+ [Description of solved problems and ongoing troubleshooting]
276
+
277
+ 6. All user messages:
278
+ - [Detailed non tool use user message]
279
+ - [...]
280
+
281
+ 7. Pending Tasks:
282
+ - [Task 1]
283
+ - [Task 2]
284
+ - [...]
285
+
286
+ 8. Current Work:
287
+ [Precise description of current work]
288
+
289
+ 9. Optional Next Step:
290
+ [Optional Next step to take]
291
+
292
+ </summary>
293
+ </example>
294
+
295
+ Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
296
+
297
+ There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary.`;
298
+
299
+ const COMPACT_NO_TOOLS_TRAILER =
300
+ "\n\nREMINDER: Do NOT call any tools. Respond with plain text only — " +
301
+ "an <analysis> block followed by a <summary> block. " +
302
+ "Tool calls will be rejected and you will fail the task.";
214
303
 
215
- ## Errors and Fixes
216
- - Errors encountered, root causes, how they were resolved
217
- - Approaches tried that didn't work and why
304
+ /**
305
+ * Builds the compact instruction sent as the trailing user message on the
306
+ * fork path. Aligned with Claude Code's getCompactPrompt().
307
+ */
308
+ export function getCompactPrompt(customInstructions?: string): string {
309
+ let prompt = COMPACT_NO_TOOLS_PREAMBLE + BASE_COMPACT_PROMPT;
218
310
 
219
- ## Problem Solving
220
- - Approach evolution, trade-offs considered, decisions made
311
+ if (customInstructions && customInstructions.trim() !== "") {
312
+ prompt += `\n\nAdditional Instructions:\n${customInstructions}`;
313
+ }
221
314
 
222
- ## All User Messages
223
- - Complete list of all user messages (non-tool content)
224
- - Preserve exact wording where load-bearing
315
+ prompt += COMPACT_NO_TOOLS_TRAILER;
225
316
 
226
- ## Pending Tasks
227
- - Outstanding work, TODOs, unresolved questions
317
+ return prompt;
318
+ }
228
319
 
229
- ## Current Work
230
- - What was being worked on at the time of summarization
231
- - Exact state of in-progress changes
320
+ /**
321
+ * Formats the compact summary by stripping the <analysis> drafting scratchpad
322
+ * and extracting the <summary> section. Raw text passes through unchanged
323
+ * when no <summary> tag is present. Aligned with Claude Code's
324
+ * formatCompactSummary().
325
+ */
326
+ export function formatCompactSummary(summary: string): string {
327
+ let formattedSummary = summary;
328
+
329
+ formattedSummary = formattedSummary.replace(
330
+ /<analysis>[\s\S]*?<\/analysis>/,
331
+ "",
332
+ );
333
+
334
+ const summaryMatch = formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/);
335
+ if (summaryMatch) {
336
+ const content = summaryMatch[1] || "";
337
+ formattedSummary = formattedSummary.replace(
338
+ /<summary>[\s\S]*?<\/summary>/,
339
+ `Summary:\n${content.trim()}`,
340
+ );
341
+ }
232
342
 
233
- ## Optional Next Step
234
- - Immediate next action needed
235
- - Include verbatim quotes from recent conversation if relevant
343
+ formattedSummary = formattedSummary.replace(/\n\n+/g, "\n\n");
236
344
 
237
- Be concise but complete — include information that prevents duplicate work or repeated mistakes.
238
- Respond with text only. Do NOT call any tools.
239
- Wrap your summary in <summary></summary> tags.`;
345
+ return formattedSummary.trim();
346
+ }
240
347
 
241
348
  export const WEB_CONTENT_SYSTEM_PROMPT = `You are a helpful assistant that extracts information from web content. The content is provided in Markdown format.`;
242
349
  export const BTW_SYSTEM_PROMPT = `You are a helpful assistant. Answer the user's side question based on the conversation history.
@@ -24,7 +24,6 @@ import * as fs from "fs";
24
24
  import * as path from "path";
25
25
 
26
26
  import {
27
- COMPACT_MESSAGES_SYSTEM_PROMPT,
28
27
  WEB_CONTENT_SYSTEM_PROMPT,
29
28
  BTW_SYSTEM_PROMPT,
30
29
  type SystemPromptBlock,
@@ -169,6 +168,12 @@ export interface CallAgentOptions {
169
168
  | "required"
170
169
  | { type: "function"; function: { name: string } }; // Force tool selection
171
170
 
171
+ // Force SSE streaming independent of callback presence. Long-running
172
+ // non-interactive calls (e.g. the compaction fork) need streaming so a
173
+ // slow reasoning model isn't killed by a gateway idle timeout before the
174
+ // first byte arrives.
175
+ stream?: boolean;
176
+
172
177
  // NEW: Streaming callbacks
173
178
  onContentUpdate?: (content: string) => void;
174
179
  onToolUpdate?: (toolCall: {
@@ -319,11 +324,9 @@ export async function callAgent(
319
324
  });
320
325
 
321
326
  // Determine if streaming is needed
322
- const isStreaming = !!(
323
- onContentUpdate ||
324
- onToolUpdate ||
325
- onReasoningUpdate
326
- );
327
+ const isStreaming =
328
+ options.stream === true ||
329
+ !!(onContentUpdate || onToolUpdate || onReasoningUpdate);
327
330
 
328
331
  // Prepare API call parameters
329
332
  createParams = {
@@ -787,128 +790,6 @@ async function processStreamingResponse(
787
790
  return result;
788
791
  }
789
792
 
790
- export interface CompactMessagesOptions {
791
- // Resolved configuration
792
- gatewayConfig: GatewayConfig;
793
- modelConfig: ModelConfig;
794
-
795
- // Existing parameters
796
- messages: ChatCompletionMessageParam[];
797
- abortSignal?: AbortSignal;
798
- model?: string;
799
- customInstructions?: string;
800
- }
801
-
802
- export interface CompactMessagesResult {
803
- content: string;
804
- usage?: {
805
- prompt_tokens: number;
806
- completion_tokens: number;
807
- total_tokens: number;
808
- };
809
- }
810
-
811
- export async function compactMessages(
812
- options: CompactMessagesOptions,
813
- ): Promise<CompactMessagesResult> {
814
- const { gatewayConfig, modelConfig, messages, abortSignal } = options;
815
-
816
- // Validate model config at call time
817
- validateModelConfig(modelConfig);
818
-
819
- // Apply global 1 QPS rate limit
820
- if (
821
- process.env.NODE_ENV !== "test" ||
822
- modelConfig.model === "rate-limit-test"
823
- ) {
824
- await acquireSlot(abortSignal);
825
- }
826
-
827
- // Strip images from messages before compact API call to reduce token usage
828
- const cleanedMessages = messages.map((msg) => {
829
- // Handle user/assistant messages with array content
830
- if (Array.isArray(msg.content)) {
831
- const textParts = msg.content.filter(
832
- (part) => part.type === "text",
833
- ) as import("openai/resources.js").ChatCompletionContentPartText[];
834
- const text = textParts.map((p) => p.text).join("\n");
835
- return { ...msg, content: text || "(empty message)" };
836
- }
837
- return msg;
838
- });
839
-
840
- // Create OpenAI client with injected configuration
841
- const openai = new OpenAIClient({
842
- apiKey: gatewayConfig.apiKey,
843
- baseURL: gatewayConfig.baseURL,
844
- defaultHeaders: gatewayConfig.defaultHeaders,
845
- fetchOptions: gatewayConfig.fetchOptions,
846
- fetch: gatewayConfig.fetch,
847
- });
848
-
849
- // When a fast model override is provided, use the fast model's options
850
- // (if configured); otherwise fall back to the agent model's options.
851
- const activeExtraParams = options.model
852
- ? modelConfig.fastModelOptions || {}
853
- : modelConfig.options || {};
854
-
855
- const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
856
- temperature: 0.1,
857
- max_tokens: 8192,
858
- ...activeExtraParams,
859
- });
860
-
861
- try {
862
- const response = await openai.chat.completions.create(
863
- {
864
- ...openaiModelConfig,
865
- messages: [
866
- {
867
- role: "system",
868
- content: COMPACT_MESSAGES_SYSTEM_PROMPT,
869
- },
870
- ...cleanedMessages,
871
- {
872
- role: "user",
873
- content: options.customInstructions
874
- ? `Please create a detailed summary of the conversation so far. Pay special attention to these instructions: ${options.customInstructions}`
875
- : `Please create a detailed summary of the conversation so far.`,
876
- },
877
- ],
878
- },
879
- {
880
- signal: abortSignal,
881
- },
882
- );
883
-
884
- const content = response.choices[0]?.message?.content?.trim();
885
- if (!content) {
886
- throw new Error(
887
- "Failed to compact conversation history: Empty response from AI",
888
- );
889
- }
890
- const usage = response.usage
891
- ? {
892
- prompt_tokens: response.usage.prompt_tokens,
893
- completion_tokens: response.usage.completion_tokens,
894
- total_tokens: response.usage.total_tokens,
895
- }
896
- : undefined;
897
-
898
- return {
899
- content,
900
- usage,
901
- };
902
- } catch (error) {
903
- if ((error as Error).name === "AbortError") {
904
- logger.info("Compaction request was aborted");
905
- throw new Error("Compaction request was aborted");
906
- }
907
- logger.error("Failed to compact messages:", error);
908
- throw error;
909
- }
910
- }
911
-
912
793
  export interface ProcessWebContentOptions {
913
794
  // Resolved configuration
914
795
  gatewayConfig: GatewayConfig;
@@ -61,6 +61,10 @@ export class ConfigurationService {
61
61
  private currentConfiguration: WaveConfiguration | null = null;
62
62
  private options: AgentOptions = {};
63
63
  private _configuredEnvKeys = new Set<string>();
64
+ // Per-session environment snapshot: settings.json `env` is stored here (NOT
65
+ // written to process.env) so multiple sessions in one `wave --stdio` process
66
+ // don't cross-pollute. Resolve methods read `this.envSnapshot ?? process.env`.
67
+ private envSnapshot: Record<string, string> = {};
64
68
 
65
69
  /**
66
70
  * Set agent options for configuration resolution
@@ -69,6 +73,28 @@ export class ConfigurationService {
69
73
  this.options = options;
70
74
  }
71
75
 
76
+ /**
77
+ * Returns a copy of the per-session environment snapshot (settings.json `env`).
78
+ * Priority over OS env; does NOT include OS env. For subprocess spawning use
79
+ * {@link getMergedEnv} instead.
80
+ */
81
+ getEnvSnapshot(): Record<string, string> {
82
+ return { ...this.envSnapshot };
83
+ }
84
+
85
+ /**
86
+ * Returns OS env merged with the session snapshot (snapshot wins). Use this
87
+ * when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
88
+ * so they inherit both OS env and the session's settings env.
89
+ */
90
+ getMergedEnv(): Record<string, string> {
91
+ return Object.fromEntries(
92
+ Object.entries({ ...process.env, ...this.envSnapshot }).filter(
93
+ ([, v]) => v !== undefined,
94
+ ),
95
+ ) as Record<string, string>;
96
+ }
97
+
72
98
  // Core loading operations
73
99
 
74
100
  /**
@@ -384,16 +410,33 @@ export class ConfigurationService {
384
410
  // Utility operations
385
411
 
386
412
  /**
387
- * Set environment variables from configuration
388
- * This replaces direct process.env modification
413
+ * Store environment variables from configuration into the per-session
414
+ * snapshot (NOT process.env). Settings `env` shadows OS env for this session
415
+ * only — multiple sessions in one stdio process stay isolated.
416
+ *
417
+ * Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
418
+ * process-level singletons (AuthService, remoteSettingsService background
419
+ * fetch) need to read it and don't hold a per-session snapshot. Same value
420
+ * across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
389
421
  */
390
422
  setEnvironmentVars(env: Record<string, string>): void {
391
423
  for (const [key, value] of Object.entries(env)) {
392
- if (process.env[key] !== undefined && !this._configuredEnvKeys.has(key)) {
424
+ if (
425
+ process.env[key] !== undefined &&
426
+ !this._configuredEnvKeys.has(key) &&
427
+ process.env[key] !== value
428
+ ) {
393
429
  logger.warn(`Overriding environment variable: ${key}`);
394
430
  }
395
- process.env[key] = value;
431
+ this.envSnapshot[key] = value;
396
432
  this._configuredEnvKeys.add(key);
433
+ // WAVE_SERVER_URL is consumed by process-level singletons (AuthService,
434
+ // remoteSettingsService) that can't see the per-session snapshot — mirror
435
+ // it to process.env so they read the settings value. Same value across
436
+ // sessions ⇒ no last-session-wins cross-pollution.
437
+ if (key === "WAVE_SERVER_URL") {
438
+ process.env[key] = value;
439
+ }
397
440
  }
398
441
  }
399
442
 
@@ -437,8 +480,11 @@ export class ConfigurationService {
437
480
  fetchOptions?: ClientOptions["fetchOptions"],
438
481
  fetch?: ClientOptions["fetch"],
439
482
  ): GatewayConfig {
440
- // Check for SSO token first - if present and server URL is available, use SSO mode
441
- // Server URL resolution: options > process.env > default
483
+ // Check for SSO token first - if present and server URL is available, use SSO mode.
484
+ // Server URL resolution: options.serverUrl > process.env.WAVE_SERVER_URL > default.
485
+ // settings.json `env` WAVE_SERVER_URL is mirrored to process.env by
486
+ // setEnvironmentVars (so process-level singletons AuthService / remoteSettings
487
+ // can read it), then read here. See docs/specs/core/agent-config.md.
442
488
  const ssoToken = this.readSSOToken();
443
489
  const serverUrl =
444
490
  this.options.serverUrl ||
@@ -469,7 +515,8 @@ export class ConfigurationService {
469
515
  } else if (this.options.apiKey !== undefined) {
470
516
  resolvedApiKey = this.options.apiKey;
471
517
  } else {
472
- resolvedApiKey = process.env.WAVE_API_KEY;
518
+ resolvedApiKey =
519
+ this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
473
520
  }
474
521
 
475
522
  // Resolve base URL: override > options > env (settings.json) > process.env
@@ -480,15 +527,18 @@ export class ConfigurationService {
480
527
  } else if (this.options.baseURL !== undefined) {
481
528
  resolvedBaseURL = this.options.baseURL;
482
529
  } else {
483
- resolvedBaseURL = process.env.WAVE_BASE_URL;
530
+ resolvedBaseURL =
531
+ this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
484
532
  }
485
533
 
486
534
  // Fallback to process.env if still not resolved (for dynamic updates in tests)
487
535
  if (resolvedApiKey === undefined) {
488
- resolvedApiKey = process.env.WAVE_API_KEY;
536
+ resolvedApiKey =
537
+ this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
489
538
  }
490
539
  if (!resolvedBaseURL) {
491
- resolvedBaseURL = process.env.WAVE_BASE_URL;
540
+ resolvedBaseURL =
541
+ this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
492
542
  }
493
543
 
494
544
  // Treat empty string as not provided
@@ -497,7 +547,10 @@ export class ConfigurationService {
497
547
  }
498
548
 
499
549
  // Resolve custom headers from environment: env (settings.json) > process.env
500
- const envCustomHeaders = process.env.WAVE_CUSTOM_HEADERS || "";
550
+ const envCustomHeaders =
551
+ this.envSnapshot.WAVE_CUSTOM_HEADERS ??
552
+ process.env.WAVE_CUSTOM_HEADERS ??
553
+ "";
501
554
  const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
502
555
 
503
556
  // Merge headers: env headers < options < override
@@ -539,11 +592,13 @@ export class ConfigurationService {
539
592
  model ||
540
593
  this.options.model ||
541
594
  this.currentConfiguration?.model ||
542
- process.env.WAVE_MODEL;
595
+ (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
543
596
 
544
597
  // Resolve fast model: override > options > process.env (includes settings.json env)
545
598
  const resolvedFastModel =
546
- fastModel || this.options.fastModel || process.env.WAVE_FAST_MODEL;
599
+ fastModel ||
600
+ this.options.fastModel ||
601
+ (this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
547
602
 
548
603
  // Resolve max output tokens
549
604
  const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
@@ -596,8 +651,10 @@ export class ConfigurationService {
596
651
  return this.options.maxInputTokens;
597
652
  }
598
653
 
599
- // Try env (settings.json) first, then process.env
600
- const envMaxInputTokens = process.env.WAVE_MAX_INPUT_TOKENS;
654
+ // Try env (settings.json snapshot) first, then process.env
655
+ const envMaxInputTokens =
656
+ this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
657
+ process.env.WAVE_MAX_INPUT_TOKENS;
601
658
  if (envMaxInputTokens) {
602
659
  const parsed = parseInt(envMaxInputTokens, 10);
603
660
  if (!isNaN(parsed)) {
@@ -645,9 +702,9 @@ export class ConfigurationService {
645
702
  return this.currentConfiguration.autoMemoryEnabled;
646
703
  }
647
704
 
648
- // 2. WAVE_DISABLE_AUTO_MEMORY environment variable
705
+ // 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
649
706
  const disableAutoMemory =
650
- process.env.WAVE_DISABLE_AUTO_MEMORY ||
707
+ this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
651
708
  process.env.WAVE_DISABLE_AUTO_MEMORY;
652
709
  if (disableAutoMemory === "1" || disableAutoMemory === "true") {
653
710
  return false;
@@ -681,9 +738,9 @@ export class ConfigurationService {
681
738
  return this.currentConfiguration.autoMemoryFrequency;
682
739
  }
683
740
 
684
- // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
741
+ // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
685
742
  const envFrequency =
686
- process.env.WAVE_AUTO_MEMORY_FREQUENCY ||
743
+ this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
687
744
  process.env.WAVE_AUTO_MEMORY_FREQUENCY;
688
745
  if (envFrequency) {
689
746
  const parsed = parseInt(envFrequency, 10);
@@ -713,8 +770,10 @@ export class ConfigurationService {
713
770
  return this.options.maxTokens;
714
771
  }
715
772
 
716
- // Try env (settings.json) first, then process.env
717
- const envMaxOutputTokens = process.env.WAVE_MAX_OUTPUT_TOKENS;
773
+ // Try env (settings.json snapshot) first, then process.env
774
+ const envMaxOutputTokens =
775
+ this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
776
+ process.env.WAVE_MAX_OUTPUT_TOKENS;
718
777
  if (envMaxOutputTokens) {
719
778
  const parsed = parseInt(envMaxOutputTokens, 10);
720
779
  if (!isNaN(parsed) && parsed > 0) {
@@ -761,8 +820,10 @@ export class ConfigurationService {
761
820
  getConfiguredModels(): string[] {
762
821
  const models = new Set<string>();
763
822
 
764
- // Add current model from options or environment
765
- const currentModel = this.options.model || process.env.WAVE_MODEL;
823
+ // Add current model from options or environment (settings snapshot > OS env)
824
+ const currentModel =
825
+ this.options.model ||
826
+ (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
766
827
  if (currentModel) {
767
828
  models.add(currentModel);
768
829
  }
@@ -126,13 +126,17 @@ export class InitializationService {
126
126
  // Don't throw error to prevent app startup failure
127
127
  }
128
128
 
129
- // Initialize remote settings (load disk cache synchronously, then fetch in background)
130
- // Must happen BEFORE loadMergedConfiguration so remote env vars are available
129
+ // Load remote settings disk cache synchronously.
130
+ // Must happen BEFORE loadMergedConfiguration so cached managed settings
131
+ // (env, model, disallowedTools) are merged into the config. Settings `env`
132
+ // is stored in the per-session env snapshot (NOT process.env), except
133
+ // WAVE_SERVER_URL which is mirrored to process.env so the network fetch
134
+ // (below) can read it via authService.getServerUrl(); no race.
131
135
  try {
132
136
  const phaseStart = performance.now();
133
137
  await remoteSettingsService.initialize();
134
138
  logger?.debug(
135
- `Initialization Phase [Remote Settings] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
139
+ `Initialization Phase [Remote Settings Cache] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
136
140
  );
137
141
  } catch (error) {
138
142
  logger?.error("Failed to initialize remote settings:", error);
@@ -190,6 +194,13 @@ export class InitializationService {
190
194
  // Don't throw error to prevent app startup failure
191
195
  }
192
196
 
197
+ // Start remote settings network fetch + polling now that the config is
198
+ // merged. Settings `env` WAVE_SERVER_URL was mirrored to process.env by
199
+ // loadMergedConfiguration → setEnvironmentVars, so the fetch reads it via
200
+ // authService.getServerUrl(); fire-and-forget, failures fall back to the
201
+ // cached/merged settings.
202
+ remoteSettingsService.startBackgroundFetch();
203
+
193
204
  // Execute SessionStart hooks
194
205
  try {
195
206
  const phaseStart = performance.now();
@@ -237,7 +248,9 @@ export class InitializationService {
237
248
  cwd: workdir,
238
249
  worktreeName: agentOptions.worktreeName,
239
250
  env: Object.fromEntries(
240
- Object.entries(process.env).filter((e) => e[1] !== undefined),
251
+ Object.entries(configurationService.getMergedEnv()).filter(
252
+ (e) => e[1] !== undefined,
253
+ ),
241
254
  ) as Record<string, string>,
242
255
  });
243
256