wave-agent-sdk 0.19.9 → 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.
- package/builtin/plugins/sdd/scripts/session-start.js +1 -1
- package/builtin/plugins/sdd/skills/specify/SKILL.md +3 -4
- package/builtin/skills/settings/ENV.md +15 -9
- package/builtin/skills/settings/HOOKS.md +27 -2
- package/dist/agent.js +5 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +8 -0
- package/dist/managers/aiManager.js +20 -5
- package/dist/managers/backgroundTaskManager.d.ts +6 -0
- package/dist/managers/backgroundTaskManager.js +11 -0
- package/dist/managers/bangManager.d.ts +6 -0
- package/dist/managers/bangManager.js +11 -0
- package/dist/managers/hookManager.d.ts +8 -2
- package/dist/managers/hookManager.js +14 -4
- package/dist/managers/mcpManager.d.ts +18 -4
- package/dist/managers/mcpManager.js +40 -18
- package/dist/managers/toolManager.js +5 -0
- package/dist/services/configurationService.d.ts +21 -2
- package/dist/services/configurationService.js +72 -23
- package/dist/services/initializationService.js +14 -4
- package/dist/services/interactionService.js +35 -7
- package/dist/services/remoteSettingsService.d.ts +12 -0
- package/dist/services/remoteSettingsService.js +15 -1
- package/dist/services/taskManager.js +7 -1
- package/dist/tools/bashTool.js +1 -0
- package/dist/tools/enterWorktreeTool.js +14 -3
- package/dist/tools/exitWorktreeTool.js +11 -10
- package/dist/tools/types.d.ts +7 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/utils/containerSetup.js +1 -1
- package/dist/utils/openaiClient.js +2 -1
- package/dist/utils/pathEncoder.js +7 -2
- package/dist/utils/worktreeUtils.d.ts +17 -0
- package/dist/utils/worktreeUtils.js +339 -1
- package/package.json +1 -1
- package/src/agent.ts +7 -2
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +23 -5
- package/src/managers/backgroundTaskManager.ts +15 -0
- package/src/managers/bangManager.ts +15 -0
- package/src/managers/hookManager.ts +20 -5
- package/src/managers/mcpManager.ts +60 -18
- package/src/managers/toolManager.ts +7 -0
- package/src/services/configurationService.ts +84 -23
- package/src/services/initializationService.ts +17 -4
- package/src/services/interactionService.ts +49 -6
- package/src/services/remoteSettingsService.ts +16 -1
- package/src/services/taskManager.ts +10 -1
- package/src/tools/bashTool.ts +1 -0
- package/src/tools/enterWorktreeTool.ts +19 -2
- package/src/tools/exitWorktreeTool.ts +15 -12
- package/src/tools/types.ts +7 -0
- package/src/types/config.ts +2 -0
- package/src/types/hooks.ts +2 -2
- package/src/utils/containerSetup.ts +3 -1
- package/src/utils/openaiClient.ts +2 -0
- package/src/utils/pathEncoder.ts +7 -2
- package/src/utils/worktreeUtils.ts +401 -1
|
@@ -9,6 +9,7 @@ import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
|
9
9
|
import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
|
|
10
10
|
import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
|
|
11
11
|
import { Container } from "../utils/container.js";
|
|
12
|
+
import type { ConfigurationService } from "../services/configurationService.js";
|
|
12
13
|
import type {
|
|
13
14
|
Logger,
|
|
14
15
|
McpServerConfig,
|
|
@@ -42,7 +43,21 @@ export interface McpManagerOptions {
|
|
|
42
43
|
*/
|
|
43
44
|
const WAVE_TEMPLATE_VARS = ["WAVE_PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Expand environment variables in a string value.
|
|
48
|
+
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
49
|
+
*
|
|
50
|
+
* @param env - Merged env to resolve against (session snapshot over OS env).
|
|
51
|
+
* Defaults to `process.env` for backward compatibility with tests
|
|
52
|
+
* and standalone callers.
|
|
53
|
+
*/
|
|
54
|
+
export function expandEnvVars(
|
|
55
|
+
value: string,
|
|
56
|
+
env: Record<string, string | undefined> = process.env as Record<
|
|
57
|
+
string,
|
|
58
|
+
string | undefined
|
|
59
|
+
>,
|
|
60
|
+
): string {
|
|
46
61
|
return value.replace(/\$\{([^}]+)\}/g, (_match, expr: string) => {
|
|
47
62
|
const [varName, ...rest] = expr.split(":-");
|
|
48
63
|
const defaultValue = rest.join(":-");
|
|
@@ -50,45 +65,53 @@ export function expandEnvVars(value: string): string {
|
|
|
50
65
|
if (WAVE_TEMPLATE_VARS.includes(varName)) {
|
|
51
66
|
return _match; // return original ${...} string untouched
|
|
52
67
|
}
|
|
53
|
-
return process.env[varName] ?? defaultValue;
|
|
68
|
+
return env[varName] ?? process.env[varName] ?? defaultValue;
|
|
54
69
|
});
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/**
|
|
58
73
|
* Walk an MCP config and resolve environment variables in all string fields.
|
|
59
|
-
*
|
|
60
|
-
* handled at spawn time
|
|
74
|
+
* Expands ${VAR} against the session env snapshot (over OS env); falls back to
|
|
75
|
+
* process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
|
|
61
76
|
*/
|
|
62
|
-
export function resolveMcpConfig(
|
|
77
|
+
export function resolveMcpConfig(
|
|
78
|
+
config: McpConfig,
|
|
79
|
+
env: Record<string, string | undefined> = process.env as Record<
|
|
80
|
+
string,
|
|
81
|
+
string | undefined
|
|
82
|
+
>,
|
|
83
|
+
): McpConfig {
|
|
63
84
|
const resolved: McpConfig = { mcpServers: {} };
|
|
64
85
|
|
|
65
86
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
66
87
|
const resolvedServer: McpServerConfig = { ...serverConfig };
|
|
67
88
|
|
|
68
89
|
if (resolvedServer.command) {
|
|
69
|
-
resolvedServer.command = expandEnvVars(resolvedServer.command);
|
|
90
|
+
resolvedServer.command = expandEnvVars(resolvedServer.command, env);
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
if (resolvedServer.args) {
|
|
73
|
-
resolvedServer.args = resolvedServer.args.map(
|
|
94
|
+
resolvedServer.args = resolvedServer.args.map((a) =>
|
|
95
|
+
expandEnvVars(a, env),
|
|
96
|
+
);
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
if (resolvedServer.env) {
|
|
77
100
|
const resolvedEnv: Record<string, string> = {};
|
|
78
101
|
for (const [key, val] of Object.entries(resolvedServer.env)) {
|
|
79
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
102
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
80
103
|
}
|
|
81
104
|
resolvedServer.env = resolvedEnv;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
if (resolvedServer.url) {
|
|
85
|
-
resolvedServer.url = expandEnvVars(resolvedServer.url);
|
|
108
|
+
resolvedServer.url = expandEnvVars(resolvedServer.url, env);
|
|
86
109
|
}
|
|
87
110
|
|
|
88
111
|
if (resolvedServer.headers) {
|
|
89
112
|
const resolvedHeaders: Record<string, string> = {};
|
|
90
113
|
for (const [key, val] of Object.entries(resolvedServer.headers)) {
|
|
91
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
114
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
92
115
|
}
|
|
93
116
|
resolvedServer.headers = resolvedHeaders;
|
|
94
117
|
}
|
|
@@ -119,6 +142,19 @@ export class McpManager {
|
|
|
119
142
|
this.mcpServers = options.mcpServers;
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Merged env for this session: OS env overlaid with the per-session settings
|
|
147
|
+
* snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
|
|
148
|
+
* sessions in one `wave --stdio` process don't read each other's settings env.
|
|
149
|
+
*/
|
|
150
|
+
private get envSnapshot(): Record<string, string> {
|
|
151
|
+
return (
|
|
152
|
+
this.container
|
|
153
|
+
.get<ConfigurationService>("ConfigurationService")
|
|
154
|
+
?.getMergedEnv?.() ?? (process.env as Record<string, string>)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
122
158
|
/**
|
|
123
159
|
* Initialize MCP manager with working directory and optionally auto-connect
|
|
124
160
|
*/
|
|
@@ -189,7 +225,7 @@ export class McpManager {
|
|
|
189
225
|
try {
|
|
190
226
|
const configContent = await fs.readFile(this.configPath, "utf-8");
|
|
191
227
|
const rawConfig: McpConfig = JSON.parse(configContent);
|
|
192
|
-
const workspaceConfig = resolveMcpConfig(rawConfig);
|
|
228
|
+
const workspaceConfig = resolveMcpConfig(rawConfig, this.envSnapshot);
|
|
193
229
|
|
|
194
230
|
// Extract original (pre-resolution) URLs for safe display
|
|
195
231
|
const originalUrls: Record<string, string | undefined> = {};
|
|
@@ -284,28 +320,31 @@ export class McpManager {
|
|
|
284
320
|
// Capture original URL before any resolution for safe display
|
|
285
321
|
const originalUrl = config.url;
|
|
286
322
|
|
|
287
|
-
// Expand env vars
|
|
323
|
+
// Expand env vars against the session snapshot (over OS env, e.g. ${TAVILY_API_KEY})
|
|
324
|
+
const env = this.envSnapshot;
|
|
288
325
|
const resolvedConfig: McpServerConfig = { ...config };
|
|
289
326
|
if (resolvedConfig.command) {
|
|
290
|
-
resolvedConfig.command = expandEnvVars(resolvedConfig.command);
|
|
327
|
+
resolvedConfig.command = expandEnvVars(resolvedConfig.command, env);
|
|
291
328
|
}
|
|
292
329
|
if (resolvedConfig.args) {
|
|
293
|
-
resolvedConfig.args = resolvedConfig.args.map(
|
|
330
|
+
resolvedConfig.args = resolvedConfig.args.map((a) =>
|
|
331
|
+
expandEnvVars(a, env),
|
|
332
|
+
);
|
|
294
333
|
}
|
|
295
334
|
if (resolvedConfig.env) {
|
|
296
335
|
const resolvedEnv: Record<string, string> = {};
|
|
297
336
|
for (const [key, val] of Object.entries(resolvedConfig.env)) {
|
|
298
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
337
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
299
338
|
}
|
|
300
339
|
resolvedConfig.env = resolvedEnv;
|
|
301
340
|
}
|
|
302
341
|
if (resolvedConfig.url) {
|
|
303
|
-
resolvedConfig.url = expandEnvVars(resolvedConfig.url);
|
|
342
|
+
resolvedConfig.url = expandEnvVars(resolvedConfig.url, env);
|
|
304
343
|
}
|
|
305
344
|
if (resolvedConfig.headers) {
|
|
306
345
|
const resolvedHeaders: Record<string, string> = {};
|
|
307
346
|
for (const [key, val] of Object.entries(resolvedConfig.headers)) {
|
|
308
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
347
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
309
348
|
}
|
|
310
349
|
resolvedConfig.headers = resolvedHeaders;
|
|
311
350
|
}
|
|
@@ -433,8 +472,11 @@ export class McpManager {
|
|
|
433
472
|
);
|
|
434
473
|
}
|
|
435
474
|
|
|
475
|
+
// Base env = OS env overlaid with this session's settings snapshot, so
|
|
476
|
+
// custom env vars from settings.json reach the MCP server subprocess
|
|
477
|
+
// without polluting other sessions sharing one `wave --stdio` process.
|
|
436
478
|
const env: Record<string, string> = {
|
|
437
|
-
...
|
|
479
|
+
...this.envSnapshot,
|
|
438
480
|
...(server.config.env || {}),
|
|
439
481
|
};
|
|
440
482
|
|
|
@@ -253,6 +253,13 @@ class ToolManager {
|
|
|
253
253
|
"WorkflowManager",
|
|
254
254
|
)
|
|
255
255
|
: undefined,
|
|
256
|
+
sessionEnv: this.container.has("ConfigurationService")
|
|
257
|
+
? this.container
|
|
258
|
+
.get<
|
|
259
|
+
import("../services/configurationService.js").ConfigurationService
|
|
260
|
+
>("ConfigurationService")
|
|
261
|
+
?.getMergedEnv?.()
|
|
262
|
+
: undefined,
|
|
256
263
|
sessionId: context.sessionId,
|
|
257
264
|
toolCallId: context.toolCallId,
|
|
258
265
|
};
|
|
@@ -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
|
-
*
|
|
388
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
536
|
+
resolvedApiKey =
|
|
537
|
+
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
489
538
|
}
|
|
490
539
|
if (!resolvedBaseURL) {
|
|
491
|
-
resolvedBaseURL =
|
|
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 =
|
|
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 ||
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
130
|
-
// Must happen BEFORE loadMergedConfiguration so
|
|
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(
|
|
251
|
+
Object.entries(configurationService.getMergedEnv()).filter(
|
|
252
|
+
(e) => e[1] !== undefined,
|
|
253
|
+
),
|
|
241
254
|
) as Record<string, string>,
|
|
242
255
|
});
|
|
243
256
|
|
|
@@ -93,7 +93,9 @@ export class InteractionService {
|
|
|
93
93
|
cwd: workdir,
|
|
94
94
|
userPrompt: content,
|
|
95
95
|
env: Object.fromEntries(
|
|
96
|
-
Object.entries(
|
|
96
|
+
Object.entries(
|
|
97
|
+
context.configurationService.getMergedEnv(),
|
|
98
|
+
).filter((e) => e[1] !== undefined),
|
|
97
99
|
) as Record<string, string>, // Include environment variables
|
|
98
100
|
},
|
|
99
101
|
);
|
|
@@ -137,6 +139,7 @@ export class InteractionService {
|
|
|
137
139
|
): Promise<void> {
|
|
138
140
|
const {
|
|
139
141
|
messageManager,
|
|
142
|
+
hookManager,
|
|
140
143
|
logger,
|
|
141
144
|
subagentManager,
|
|
142
145
|
taskManager,
|
|
@@ -157,7 +160,24 @@ export class InteractionService {
|
|
|
157
160
|
// Continue with restoration even if save fails
|
|
158
161
|
}
|
|
159
162
|
|
|
160
|
-
// 3.
|
|
163
|
+
// 3. Run SessionEnd hooks for the current session (cleanup before switching)
|
|
164
|
+
const currentSessionId = messageManager.getSessionId();
|
|
165
|
+
const currentTranscriptPath = messageManager.getTranscriptPath();
|
|
166
|
+
if (hookManager) {
|
|
167
|
+
try {
|
|
168
|
+
await hookManager.executeSessionEndHooks(
|
|
169
|
+
"resume",
|
|
170
|
+
currentSessionId,
|
|
171
|
+
currentTranscriptPath,
|
|
172
|
+
);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
logger?.warn(
|
|
175
|
+
`SessionEnd hooks on restore failed: ${(error as Error).message}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 4. Load target session
|
|
161
181
|
const sessionData = await loadSessionFromJsonl(
|
|
162
182
|
sessionId,
|
|
163
183
|
messageManager.getWorkdir(),
|
|
@@ -166,20 +186,43 @@ export class InteractionService {
|
|
|
166
186
|
throw new Error(`Session not found: ${sessionId}`);
|
|
167
187
|
}
|
|
168
188
|
|
|
169
|
-
//
|
|
189
|
+
// 5. Clean current state
|
|
170
190
|
abortMessage(); // Abort any running operations
|
|
171
191
|
subagentManager.cleanup(); // Clean up active subagents
|
|
172
192
|
|
|
173
|
-
//
|
|
193
|
+
// 6. Rebuild usage (in correct order)
|
|
174
194
|
messageManager.rebuildUsageFromMessages(sessionData.messages);
|
|
175
195
|
|
|
176
|
-
//
|
|
196
|
+
// 7. Initialize session state last
|
|
177
197
|
messageManager.initializeFromSession(sessionData);
|
|
178
198
|
|
|
199
|
+
// 8. Run SessionStart hooks for the restored session and inject additional
|
|
200
|
+
// context as a meta user message (matches Claude Code's resume behavior:
|
|
201
|
+
// SessionEnd then SessionStart, hook messages appended to the conversation)
|
|
202
|
+
if (hookManager) {
|
|
203
|
+
try {
|
|
204
|
+
const sessionStartResult = await hookManager.executeSessionStartHooks(
|
|
205
|
+
"resume",
|
|
206
|
+
sessionData.id,
|
|
207
|
+
messageManager.getTranscriptPath(),
|
|
208
|
+
);
|
|
209
|
+
if (sessionStartResult.additionalContext) {
|
|
210
|
+
messageManager.addUserMessage({
|
|
211
|
+
content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
|
|
212
|
+
isMeta: true,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger?.warn(
|
|
217
|
+
`SessionStart hooks on restore failed: ${(error as Error).message}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
179
222
|
// Update task manager with the root session ID to ensure continuity across compactions
|
|
180
223
|
taskManager.setTaskListId(sessionData.id);
|
|
181
224
|
|
|
182
|
-
//
|
|
225
|
+
// 9. Load tasks for the restored session
|
|
183
226
|
const tasks = await taskManager.listTasks();
|
|
184
227
|
options.callbacks?.onTasksChange?.(tasks);
|
|
185
228
|
}
|
|
@@ -198,8 +198,22 @@ function startPolling(): void {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
export function initialize(): void {
|
|
201
|
+
// Load disk cache synchronously so getRemoteSettingsSync() returns cached
|
|
202
|
+
// managed settings during loadMergedConfiguration() (must run BEFORE it).
|
|
201
203
|
loadCacheFromDisk();
|
|
202
|
-
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Start the fire-and-forget initial network fetch + background polling.
|
|
208
|
+
*
|
|
209
|
+
* Must be called AFTER loadMergedConfiguration() so disk-cached managed
|
|
210
|
+
* settings are merged before the fetch, and so settings `env` WAVE_SERVER_URL
|
|
211
|
+
* is mirrored to process.env (by setEnvironmentVars) before the fetch. The
|
|
212
|
+
* fetch uses authService.getServerUrl(), which reads process.env.WAVE_SERVER_URL
|
|
213
|
+
* — the settings value is visible there, so there is no init-ordering race
|
|
214
|
+
* that would fall back to DEFAULT_SERVER_URL (prod) and hit a test endpoint (401).
|
|
215
|
+
*/
|
|
216
|
+
export function startBackgroundFetch(): void {
|
|
203
217
|
fetchRemoteSettings()
|
|
204
218
|
.then(() => startPolling())
|
|
205
219
|
.catch((err) => {
|
|
@@ -342,6 +356,7 @@ export function mergeRemoteSettings(
|
|
|
342
356
|
*/
|
|
343
357
|
export const remoteSettingsService = {
|
|
344
358
|
initialize,
|
|
359
|
+
startBackgroundFetch,
|
|
345
360
|
getRemoteSettingsSync,
|
|
346
361
|
refresh,
|
|
347
362
|
clear,
|
|
@@ -6,6 +6,7 @@ import { Task } from "../types/tasks.js";
|
|
|
6
6
|
import { logger } from "../utils/globalLogger.js";
|
|
7
7
|
import { Container } from "../utils/container.js";
|
|
8
8
|
import type { MessageManager } from "../managers/messageManager.js";
|
|
9
|
+
import type { ConfigurationService } from "./configurationService.js";
|
|
9
10
|
|
|
10
11
|
function byIdAsc(a: Task, b: Task) {
|
|
11
12
|
const aNum = parseInt(a.id, 10);
|
|
@@ -44,7 +45,15 @@ export class TaskManager extends EventEmitter {
|
|
|
44
45
|
if (!messageManager) return;
|
|
45
46
|
|
|
46
47
|
const rootSessionId = messageManager.getRootSessionId();
|
|
47
|
-
|
|
48
|
+
// Read the per-session snapshot (not process.env) so multiple sessions in
|
|
49
|
+
// one `wave --stdio` process don't cross-pollute this flag.
|
|
50
|
+
const envSnap =
|
|
51
|
+
this.container
|
|
52
|
+
.get<ConfigurationService>("ConfigurationService")
|
|
53
|
+
?.getEnvSnapshot() ?? {};
|
|
54
|
+
const pinnedTaskListId =
|
|
55
|
+
envSnap.WAVE_TASK_LIST_ID ?? process.env.WAVE_TASK_LIST_ID;
|
|
56
|
+
if (this.taskListId !== rootSessionId && !pinnedTaskListId) {
|
|
48
57
|
this.setTaskListId(rootSessionId);
|
|
49
58
|
await this.refreshTasks();
|
|
50
59
|
}
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createWorktree,
|
|
10
10
|
validateWorktreeName,
|
|
11
11
|
generateWorktreeName,
|
|
12
|
+
performPostCreationSetup,
|
|
12
13
|
} from "../utils/worktreeUtils.js";
|
|
13
14
|
import { getGitMainRepoRoot } from "../utils/gitUtils.js";
|
|
14
15
|
import { ENTER_WORKTREE_TOOL_NAME } from "../constants/tools.js";
|
|
@@ -105,6 +106,20 @@ export const enterWorktreeTool: ToolPlugin = {
|
|
|
105
106
|
const baseRef = context.aiManager?.getWorktreeBaseRef?.();
|
|
106
107
|
const worktreeInfo = createWorktree(name, mainRepoRoot, { baseRef });
|
|
107
108
|
|
|
109
|
+
// Copy local settings (.wave/settings.local.json) and gitignored project
|
|
110
|
+
// files (.worktreeinclude, e.g. .env/.mcp.json) into a new worktree —
|
|
111
|
+
// mirrors the CLI createWorktree path. Best-effort, never fails the tool.
|
|
112
|
+
if (worktreeInfo.isNew) {
|
|
113
|
+
try {
|
|
114
|
+
await performPostCreationSetup(
|
|
115
|
+
worktreeInfo.path,
|
|
116
|
+
worktreeInfo.repoRoot,
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger?.warn("Worktree post-creation setup failed:", error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
// Build session state
|
|
109
124
|
const session: WorktreeSession = {
|
|
110
125
|
originalCwd: context.workdir,
|
|
@@ -134,11 +149,13 @@ export const enterWorktreeTool: ToolPlugin = {
|
|
|
134
149
|
projectDir: worktreeInfo.path,
|
|
135
150
|
timestamp: new Date(),
|
|
136
151
|
sessionId: context.sessionId ?? "",
|
|
137
|
-
transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
|
|
152
|
+
transcriptPath: context.messageManager?.getTranscriptPath?.() ?? "",
|
|
138
153
|
cwd: worktreeInfo.path,
|
|
139
154
|
worktreeName: worktreeInfo.name,
|
|
140
155
|
env: Object.fromEntries(
|
|
141
|
-
Object.entries(process.env).filter(
|
|
156
|
+
Object.entries(context.sessionEnv ?? process.env).filter(
|
|
157
|
+
(e) => e[1] !== undefined,
|
|
158
|
+
),
|
|
142
159
|
) as Record<string, string>,
|
|
143
160
|
},
|
|
144
161
|
);
|