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
@@ -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 = {
@@ -442,14 +488,27 @@ export class ConfigurationService {
442
488
  if (fastModelSource && fastModelSource.options) {
443
489
  baseConfig.fastModelOptions = fastModelSource.options;
444
490
  }
491
+ // Resolve fast-model disable-thinking params from models[fastModel].disableThinkingOptions
492
+ const fastModelDisableThinking = fastModelSource && fastModelSource.disableThinkingOptions
493
+ ? fastModelSource.disableThinkingOptions
494
+ : undefined;
495
+ if (fastModelDisableThinking) {
496
+ baseConfig.disableThinkingOptions = fastModelDisableThinking;
497
+ }
445
498
  // Merge model-specific settings from configuration
446
499
  const modelSpecificConfig = resolvedAgentModel &&
447
500
  this.currentConfiguration?.models?.[resolvedAgentModel];
448
501
  if (modelSpecificConfig) {
449
- return {
502
+ const resolved = {
450
503
  ...baseConfig,
451
504
  ...modelSpecificConfig,
452
505
  };
506
+ // Re-apply after the spread so the agent model's own
507
+ // disableThinkingOptions cannot clobber the fast-model value.
508
+ if (fastModelDisableThinking) {
509
+ resolved.disableThinkingOptions = fastModelDisableThinking;
510
+ }
511
+ return resolved;
453
512
  }
454
513
  return baseConfig;
455
514
  }
@@ -468,8 +527,9 @@ export class ConfigurationService {
468
527
  if (this.options.maxInputTokens !== undefined) {
469
528
  return this.options.maxInputTokens;
470
529
  }
471
- // Try env (settings.json) first, then process.env
472
- const envMaxInputTokens = process.env.WAVE_MAX_INPUT_TOKENS;
530
+ // Try env (settings.json snapshot) first, then process.env
531
+ const envMaxInputTokens = this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
532
+ process.env.WAVE_MAX_INPUT_TOKENS;
473
533
  if (envMaxInputTokens) {
474
534
  const parsed = parseInt(envMaxInputTokens, 10);
475
535
  if (!isNaN(parsed)) {
@@ -510,8 +570,8 @@ export class ConfigurationService {
510
570
  if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
511
571
  return this.currentConfiguration.autoMemoryEnabled;
512
572
  }
513
- // 2. WAVE_DISABLE_AUTO_MEMORY environment variable
514
- const disableAutoMemory = process.env.WAVE_DISABLE_AUTO_MEMORY ||
573
+ // 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
574
+ const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
515
575
  process.env.WAVE_DISABLE_AUTO_MEMORY;
516
576
  if (disableAutoMemory === "1" || disableAutoMemory === "true") {
517
577
  return false;
@@ -541,8 +601,8 @@ export class ConfigurationService {
541
601
  if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
542
602
  return this.currentConfiguration.autoMemoryFrequency;
543
603
  }
544
- // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
545
- const envFrequency = process.env.WAVE_AUTO_MEMORY_FREQUENCY ||
604
+ // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
605
+ const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
546
606
  process.env.WAVE_AUTO_MEMORY_FREQUENCY;
547
607
  if (envFrequency) {
548
608
  const parsed = parseInt(envFrequency, 10);
@@ -568,8 +628,9 @@ export class ConfigurationService {
568
628
  if (this.options.maxTokens !== undefined) {
569
629
  return this.options.maxTokens;
570
630
  }
571
- // Try env (settings.json) first, then process.env
572
- const envMaxOutputTokens = process.env.WAVE_MAX_OUTPUT_TOKENS;
631
+ // Try env (settings.json snapshot) first, then process.env
632
+ const envMaxOutputTokens = this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
633
+ process.env.WAVE_MAX_OUTPUT_TOKENS;
573
634
  if (envMaxOutputTokens) {
574
635
  const parsed = parseInt(envMaxOutputTokens, 10);
575
636
  if (!isNaN(parsed) && parsed > 0) {
@@ -610,8 +671,9 @@ export class ConfigurationService {
610
671
  */
611
672
  getConfiguredModels() {
612
673
  const models = new Set();
613
- // Add current model from options or environment
614
- const currentModel = this.options.model || process.env.WAVE_MODEL;
674
+ // Add current model from options or environment (settings snapshot > OS env)
675
+ const currentModel = this.options.model ||
676
+ (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
615
677
  if (currentModel) {
616
678
  models.add(currentModel);
617
679
  }
@@ -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);
@@ -48,7 +48,7 @@ export class InteractionService {
48
48
  transcriptPath: messageManager.getTranscriptPath(),
49
49
  cwd: workdir,
50
50
  userPrompt: content,
51
- env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)), // Include environment variables
51
+ env: Object.fromEntries(Object.entries(context.configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)), // Include environment variables
52
52
  });
53
53
  // Process hook results and determine if we should continue
54
54
  const processResult = hookManager.processHookResults("UserPromptSubmit", hookResults, messageManager);
@@ -76,7 +76,7 @@ export class InteractionService {
76
76
  }
77
77
  }
78
78
  static async restoreSession(context, sessionId) {
79
- const { messageManager, logger, subagentManager, taskManager, options, abortMessage, } = context;
79
+ const { messageManager, hookManager, logger, subagentManager, taskManager, options, abortMessage, } = context;
80
80
  // 1. Validation
81
81
  if (!sessionId || sessionId === messageManager.getSessionId()) {
82
82
  return; // No-op if session ID is invalid or already current
@@ -89,21 +89,49 @@ export class InteractionService {
89
89
  logger?.warn("Failed to save current session before restore:", error);
90
90
  // Continue with restoration even if save fails
91
91
  }
92
- // 3. Load target session
92
+ // 3. Run SessionEnd hooks for the current session (cleanup before switching)
93
+ const currentSessionId = messageManager.getSessionId();
94
+ const currentTranscriptPath = messageManager.getTranscriptPath();
95
+ if (hookManager) {
96
+ try {
97
+ await hookManager.executeSessionEndHooks("resume", currentSessionId, currentTranscriptPath);
98
+ }
99
+ catch (error) {
100
+ logger?.warn(`SessionEnd hooks on restore failed: ${error.message}`);
101
+ }
102
+ }
103
+ // 4. Load target session
93
104
  const sessionData = await loadSessionFromJsonl(sessionId, messageManager.getWorkdir());
94
105
  if (!sessionData) {
95
106
  throw new Error(`Session not found: ${sessionId}`);
96
107
  }
97
- // 4. Clean current state
108
+ // 5. Clean current state
98
109
  abortMessage(); // Abort any running operations
99
110
  subagentManager.cleanup(); // Clean up active subagents
100
- // 5. Rebuild usage (in correct order)
111
+ // 6. Rebuild usage (in correct order)
101
112
  messageManager.rebuildUsageFromMessages(sessionData.messages);
102
- // 6. Initialize session state last
113
+ // 7. Initialize session state last
103
114
  messageManager.initializeFromSession(sessionData);
115
+ // 8. Run SessionStart hooks for the restored session and inject additional
116
+ // context as a meta user message (matches Claude Code's resume behavior:
117
+ // SessionEnd then SessionStart, hook messages appended to the conversation)
118
+ if (hookManager) {
119
+ try {
120
+ const sessionStartResult = await hookManager.executeSessionStartHooks("resume", sessionData.id, messageManager.getTranscriptPath());
121
+ if (sessionStartResult.additionalContext) {
122
+ messageManager.addUserMessage({
123
+ content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
124
+ isMeta: true,
125
+ });
126
+ }
127
+ }
128
+ catch (error) {
129
+ logger?.warn(`SessionStart hooks on restore failed: ${error.message}`);
130
+ }
131
+ }
104
132
  // Update task manager with the root session ID to ensure continuity across compactions
105
133
  taskManager.setTaskListId(sessionData.id);
106
- // 7. Load tasks for the restored session
134
+ // 9. Load tasks for the restored session
107
135
  const tasks = await taskManager.listTasks();
108
136
  options.callbacks?.onTasksChange?.(tasks);
109
137
  }
@@ -6,6 +6,17 @@ import type { WaveConfiguration } from "../types/configuration.js";
6
6
  */
7
7
  export declare function onSettingsUpdate(callback: () => void | Promise<void>): () => void;
8
8
  export declare function initialize(): void;
9
+ /**
10
+ * Start the fire-and-forget initial network fetch + background polling.
11
+ *
12
+ * Must be called AFTER loadMergedConfiguration() so disk-cached managed
13
+ * settings are merged before the fetch, and so settings `env` WAVE_SERVER_URL
14
+ * is mirrored to process.env (by setEnvironmentVars) before the fetch. The
15
+ * fetch uses authService.getServerUrl(), which reads process.env.WAVE_SERVER_URL
16
+ * — the settings value is visible there, so there is no init-ordering race
17
+ * that would fall back to DEFAULT_SERVER_URL (prod) and hit a test endpoint (401).
18
+ */
19
+ export declare function startBackgroundFetch(): void;
9
20
  export declare function getRemoteSettingsSync(): WaveConfiguration | null;
10
21
  export declare function refresh(): Promise<RemoteSettingsFetchResult>;
11
22
  export declare function clear(): void;
@@ -17,6 +28,7 @@ export declare function mergeRemoteSettings(localMerged: WaveConfiguration, remo
17
28
  */
18
29
  export declare const remoteSettingsService: {
19
30
  readonly initialize: typeof initialize;
31
+ readonly startBackgroundFetch: typeof startBackgroundFetch;
20
32
  readonly getRemoteSettingsSync: typeof getRemoteSettingsSync;
21
33
  readonly refresh: typeof refresh;
22
34
  readonly clear: typeof clear;
@@ -174,8 +174,21 @@ function startPolling() {
174
174
  _pollingTimer.unref();
175
175
  }
176
176
  export function initialize() {
177
+ // Load disk cache synchronously so getRemoteSettingsSync() returns cached
178
+ // managed settings during loadMergedConfiguration() (must run BEFORE it).
177
179
  loadCacheFromDisk();
178
- // Fire-and-forget the initial fetch, then start background polling
180
+ }
181
+ /**
182
+ * Start the fire-and-forget initial network fetch + background polling.
183
+ *
184
+ * Must be called AFTER loadMergedConfiguration() so disk-cached managed
185
+ * settings are merged before the fetch, and so settings `env` WAVE_SERVER_URL
186
+ * is mirrored to process.env (by setEnvironmentVars) before the fetch. The
187
+ * fetch uses authService.getServerUrl(), which reads process.env.WAVE_SERVER_URL
188
+ * — the settings value is visible there, so there is no init-ordering race
189
+ * that would fall back to DEFAULT_SERVER_URL (prod) and hit a test endpoint (401).
190
+ */
191
+ export function startBackgroundFetch() {
179
192
  fetchRemoteSettings()
180
193
  .then(() => startPolling())
181
194
  .catch((err) => {
@@ -302,6 +315,7 @@ export function mergeRemoteSettings(localMerged, remote) {
302
315
  */
303
316
  export const remoteSettingsService = {
304
317
  initialize,
318
+ startBackgroundFetch,
305
319
  getRemoteSettingsSync,
306
320
  refresh,
307
321
  clear,
@@ -139,6 +139,19 @@ export declare function cleanupExpiredSessionsFromJsonl(workdir: string): Promis
139
139
  * Clean up empty project directories in the session directory
140
140
  */
141
141
  export declare function cleanupEmptyProjectDirectories(): Promise<void>;
142
+ /**
143
+ * Clean up "ghost" session files that contain only meta messages
144
+ * (isMeta: true) — e.g. sessions where a SessionStart hook injected a
145
+ * system-reminder but no real user/assistant message was ever sent.
146
+ *
147
+ * Such sessions are no longer created thanks to lazy materialization in
148
+ * saveSession(); this one-time sweep removes files that predate that change
149
+ * so they stop showing up as "0 tokens / No content" entries in the resume
150
+ * list.
151
+ *
152
+ * @returns Promise that resolves to the number of files deleted
153
+ */
154
+ export declare function cleanupMetaOnlySessions(): Promise<number>;
142
155
  /**
143
156
  * Check if a session exists in JSONL storage (new approach)
144
157
  *
@@ -481,6 +481,70 @@ export async function cleanupEmptyProjectDirectories() {
481
481
  // Ignore errors if base directory doesn't exist or can't be accessed
482
482
  }
483
483
  }
484
+ /**
485
+ * Clean up "ghost" session files that contain only meta messages
486
+ * (isMeta: true) — e.g. sessions where a SessionStart hook injected a
487
+ * system-reminder but no real user/assistant message was ever sent.
488
+ *
489
+ * Such sessions are no longer created thanks to lazy materialization in
490
+ * saveSession(); this one-time sweep removes files that predate that change
491
+ * so they stop showing up as "0 tokens / No content" entries in the resume
492
+ * list.
493
+ *
494
+ * @returns Promise that resolves to the number of files deleted
495
+ */
496
+ export async function cleanupMetaOnlySessions() {
497
+ // Do not perform cleanup operations in test environment
498
+ if (process.env.NODE_ENV === "test") {
499
+ return 0;
500
+ }
501
+ let deletedCount = 0;
502
+ try {
503
+ const projectDirs = await fs.readdir(SESSION_DIR);
504
+ for (const projectDirName of projectDirs) {
505
+ const projectPath = join(SESSION_DIR, projectDirName);
506
+ try {
507
+ const stat = await fs.stat(projectPath);
508
+ if (!stat.isDirectory()) {
509
+ continue;
510
+ }
511
+ const files = await fs.readdir(projectPath);
512
+ for (const file of files) {
513
+ if (!file.endsWith(".jsonl")) {
514
+ continue;
515
+ }
516
+ const filePath = join(projectPath, file);
517
+ try {
518
+ // Fast path: a file whose last message is not meta contains a
519
+ // real message, so it can never be meta-only.
520
+ const jsonlHandler = new JsonlHandler();
521
+ const lastMessage = await jsonlHandler.getLastMessage(filePath);
522
+ if (!lastMessage?.isMeta) {
523
+ continue;
524
+ }
525
+ const messages = await jsonlHandler.read(filePath);
526
+ if (messages.length > 0 && messages.every((m) => m.isMeta)) {
527
+ await fs.unlink(filePath);
528
+ deletedCount++;
529
+ }
530
+ }
531
+ catch {
532
+ // Skip corrupted or unreadable files
533
+ continue;
534
+ }
535
+ }
536
+ }
537
+ catch {
538
+ // Skip directories we can't access
539
+ continue;
540
+ }
541
+ }
542
+ }
543
+ catch {
544
+ // Ignore errors if base directory doesn't exist or can't be accessed
545
+ }
546
+ return deletedCount;
547
+ }
484
548
  /**
485
549
  * Check if a session exists in JSONL storage (new approach)
486
550
  *
@@ -32,7 +32,13 @@ export class TaskManager extends EventEmitter {
32
32
  if (!messageManager)
33
33
  return;
34
34
  const rootSessionId = messageManager.getRootSessionId();
35
- if (this.taskListId !== rootSessionId && !process.env.WAVE_TASK_LIST_ID) {
35
+ // Read the per-session snapshot (not process.env) so multiple sessions in
36
+ // one `wave --stdio` process don't cross-pollute this flag.
37
+ const envSnap = this.container
38
+ .get("ConfigurationService")
39
+ ?.getEnvSnapshot() ?? {};
40
+ const pinnedTaskListId = envSnap.WAVE_TASK_LIST_ID ?? process.env.WAVE_TASK_LIST_ID;
41
+ if (this.taskListId !== rootSessionId && !pinnedTaskListId) {
36
42
  this.setTaskListId(rootSessionId);
37
43
  await this.refreshTasks();
38
44
  }
@@ -238,6 +238,7 @@ The working directory persists between commands. Try to maintain your current wo
238
238
  cwd: context.workdir,
239
239
  env: {
240
240
  ...process.env,
241
+ ...context.sessionEnv,
241
242
  },
242
243
  });
243
244
  let outputBuffer = "";
@@ -2,7 +2,7 @@
2
2
  * EnterWorktree tool - creates an isolated git worktree and switches the session into it.
3
3
  * Mirrors Claude Code's EnterWorktree tool behavior and prompt.
4
4
  */
5
- import { createWorktree, validateWorktreeName, generateWorktreeName, } from "../utils/worktreeUtils.js";
5
+ import { createWorktree, validateWorktreeName, generateWorktreeName, performPostCreationSetup, } from "../utils/worktreeUtils.js";
6
6
  import { getGitMainRepoRoot } from "../utils/gitUtils.js";
7
7
  import { ENTER_WORKTREE_TOOL_NAME } from "../constants/tools.js";
8
8
  import { logger } from "../utils/globalLogger.js";
@@ -85,6 +85,17 @@ export const enterWorktreeTool = {
85
85
  // Create the worktree (captures originalHeadCommit internally)
86
86
  const baseRef = context.aiManager?.getWorktreeBaseRef?.();
87
87
  const worktreeInfo = createWorktree(name, mainRepoRoot, { baseRef });
88
+ // Copy local settings (.wave/settings.local.json) and gitignored project
89
+ // files (.worktreeinclude, e.g. .env/.mcp.json) into a new worktree —
90
+ // mirrors the CLI createWorktree path. Best-effort, never fails the tool.
91
+ if (worktreeInfo.isNew) {
92
+ try {
93
+ await performPostCreationSetup(worktreeInfo.path, worktreeInfo.repoRoot);
94
+ }
95
+ catch (error) {
96
+ logger?.warn("Worktree post-creation setup failed:", error);
97
+ }
98
+ }
88
99
  // Build session state
89
100
  const session = {
90
101
  originalCwd: context.workdir,
@@ -110,10 +121,10 @@ export const enterWorktreeTool = {
110
121
  projectDir: worktreeInfo.path,
111
122
  timestamp: new Date(),
112
123
  sessionId: context.sessionId ?? "",
113
- transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
124
+ transcriptPath: context.messageManager?.getTranscriptPath?.() ?? "",
114
125
  cwd: worktreeInfo.path,
115
126
  worktreeName: worktreeInfo.name,
116
- env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
127
+ env: Object.fromEntries(Object.entries(context.sessionEnv ?? process.env).filter((e) => e[1] !== undefined)),
117
128
  });
118
129
  if (context.messageManager) {
119
130
  context.hookManager.processHookResults("WorktreeCreate", hookResults, context.messageManager);
@@ -128,14 +128,8 @@ export const exitWorktreeTool = {
128
128
  };
129
129
  // Count changes BEFORE removing the worktree (directory will be gone after)
130
130
  const summary = countWorktreeChanges(worktreePath, session.originalHeadCommit) ?? { changedFiles: 0, commits: 0 };
131
- removeWorktree(worktreeInfo);
132
- // Clear session state and restore CWD
133
- const aiManager = context.aiManager;
134
- if (aiManager) {
135
- aiManager.setWorktreeSession(null);
136
- aiManager.setWorkdir(originalCwd);
137
- }
138
- // Trigger WorktreeRemove hook (non-blocking)
131
+ // Trigger WorktreeRemove hook (non-blocking) BEFORE git removal so hooks
132
+ // can still read files inside the worktree to clean up external resources.
139
133
  let hookTriggered = false;
140
134
  if (context.hookManager) {
141
135
  try {
@@ -144,10 +138,10 @@ export const exitWorktreeTool = {
144
138
  projectDir: originalCwd,
145
139
  timestamp: new Date(),
146
140
  sessionId: context.sessionId ?? "",
147
- transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
141
+ transcriptPath: context.messageManager?.getTranscriptPath?.() ?? "",
148
142
  cwd: originalCwd,
149
143
  worktreePath,
150
- env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
144
+ env: Object.fromEntries(Object.entries(context.sessionEnv ?? process.env).filter((e) => e[1] !== undefined)),
151
145
  });
152
146
  if (context.messageManager) {
153
147
  context.hookManager.processHookResults("WorktreeRemove", hookResults, context.messageManager);
@@ -159,6 +153,13 @@ export const exitWorktreeTool = {
159
153
  logger?.warn("WorktreeRemove hooks execution failed:", error);
160
154
  }
161
155
  }
156
+ removeWorktree(worktreeInfo);
157
+ // Clear session state and restore CWD
158
+ const aiManager = context.aiManager;
159
+ if (aiManager) {
160
+ aiManager.setWorktreeSession(null);
161
+ aiManager.setWorkdir(originalCwd);
162
+ }
162
163
  const discardParts = [];
163
164
  if (summary.commits > 0) {
164
165
  discardParts.push(`${summary.commits} ${summary.commits === 1 ? "commit" : "commits"}`);
@@ -104,4 +104,11 @@ export interface ToolContext {
104
104
  originalWorkdir?: string;
105
105
  /** Workflow manager instance for workflow orchestration */
106
106
  workflowManager?: import("../managers/workflowManager.js").WorkflowManager;
107
+ /**
108
+ * Per-session merged environment (OS env overlaid with the settings env
109
+ * snapshot) for this session. Tools that spawn subprocesses (Bash, hooks)
110
+ * should merge this on top of `process.env` so settings `env` vars reach
111
+ * the subprocess without polluting other sessions in one stdio process.
112
+ */
113
+ sessionEnv?: Record<string, string>;
107
114
  }
@@ -98,6 +98,4 @@ export interface AgentCallbacks extends MessageManagerCallbacks, BackgroundTaskM
98
98
  onCommandRunningChange?: (running: boolean) => void;
99
99
  onWorkdirChange?: (newCwd: string) => void;
100
100
  onQueuedMessagesChange?: (messages: QueuedMessage[]) => void;
101
- onGoalStateChange?: (active: boolean, condition?: string, elapsed?: string) => void;
102
- onGoalEvaluating?: (evaluating: boolean) => void;
103
101
  }
@@ -10,6 +10,8 @@ export interface GatewayConfig {
10
10
  defaultHeaders?: Record<string, string>;
11
11
  fetchOptions?: OpenAI["fetchOptions"];
12
12
  fetch?: OpenAI["fetch"];
13
+ /** Session identifier, sent as the `x-session-id` request header for backend correlation. */
14
+ sessionId?: string;
13
15
  }
14
16
  export interface ModelCapabilities {
15
17
  /** Whether the model supports image/vision input. Default: true. */
@@ -27,4 +29,11 @@ export interface ModelConfig {
27
29
  options?: Record<string, unknown>;
28
30
  /** Fast model generation params (resolved from models[fastModel].options) */
29
31
  fastModelOptions?: Record<string, unknown>;
32
+ /**
33
+ * Fast-model-only disable-thinking params passed through verbatim
34
+ * (e.g. `{ thinking: { type: "disabled" } }`). Applied only in fast-model
35
+ * scenarios (webFetch content processing, `model: fastModel` subagents),
36
+ * never in the agent loop. `{}` clears the default.
37
+ */
38
+ disableThinkingOptions?: Record<string, unknown>;
30
39
  }
@@ -22,7 +22,7 @@ export interface Usage {
22
22
  completion_tokens: number;
23
23
  total_tokens: number;
24
24
  model?: string;
25
- operation_type?: "agent" | "compact" | "goal_evaluation";
25
+ operation_type?: "agent" | "compact";
26
26
  cache_read_input_tokens?: number;
27
27
  cache_creation_input_tokens?: number;
28
28
  cache_creation?: {