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
|
@@ -18,10 +18,23 @@ export declare class ConfigurationService {
|
|
|
18
18
|
private currentConfiguration;
|
|
19
19
|
private options;
|
|
20
20
|
private _configuredEnvKeys;
|
|
21
|
+
private envSnapshot;
|
|
21
22
|
/**
|
|
22
23
|
* Set agent options for configuration resolution
|
|
23
24
|
*/
|
|
24
25
|
setOptions(options: AgentOptions): void;
|
|
26
|
+
/**
|
|
27
|
+
* Returns a copy of the per-session environment snapshot (settings.json `env`).
|
|
28
|
+
* Priority over OS env; does NOT include OS env. For subprocess spawning use
|
|
29
|
+
* {@link getMergedEnv} instead.
|
|
30
|
+
*/
|
|
31
|
+
getEnvSnapshot(): Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Returns OS env merged with the session snapshot (snapshot wins). Use this
|
|
34
|
+
* when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
|
|
35
|
+
* so they inherit both OS env and the session's settings env.
|
|
36
|
+
*/
|
|
37
|
+
getMergedEnv(): Record<string, string>;
|
|
25
38
|
/**
|
|
26
39
|
* Load and merge configuration with comprehensive validation
|
|
27
40
|
*/
|
|
@@ -35,8 +48,14 @@ export declare class ConfigurationService {
|
|
|
35
48
|
*/
|
|
36
49
|
validateConfigurationFile(filePath: string): ValidationResult;
|
|
37
50
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
51
|
+
* Store environment variables from configuration into the per-session
|
|
52
|
+
* snapshot (NOT process.env). Settings `env` shadows OS env for this session
|
|
53
|
+
* only — multiple sessions in one stdio process stay isolated.
|
|
54
|
+
*
|
|
55
|
+
* Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
|
|
56
|
+
* process-level singletons (AuthService, remoteSettingsService background
|
|
57
|
+
* fetch) need to read it and don't hold a per-session snapshot. Same value
|
|
58
|
+
* across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
|
|
40
59
|
*/
|
|
41
60
|
setEnvironmentVars(env: Record<string, string>): void;
|
|
42
61
|
/**
|
|
@@ -28,6 +28,10 @@ export class ConfigurationService {
|
|
|
28
28
|
this.currentConfiguration = null;
|
|
29
29
|
this.options = {};
|
|
30
30
|
this._configuredEnvKeys = new Set();
|
|
31
|
+
// Per-session environment snapshot: settings.json `env` is stored here (NOT
|
|
32
|
+
// written to process.env) so multiple sessions in one `wave --stdio` process
|
|
33
|
+
// don't cross-pollute. Resolve methods read `this.envSnapshot ?? process.env`.
|
|
34
|
+
this.envSnapshot = {};
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Set agent options for configuration resolution
|
|
@@ -35,6 +39,22 @@ export class ConfigurationService {
|
|
|
35
39
|
setOptions(options) {
|
|
36
40
|
this.options = options;
|
|
37
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns a copy of the per-session environment snapshot (settings.json `env`).
|
|
44
|
+
* Priority over OS env; does NOT include OS env. For subprocess spawning use
|
|
45
|
+
* {@link getMergedEnv} instead.
|
|
46
|
+
*/
|
|
47
|
+
getEnvSnapshot() {
|
|
48
|
+
return { ...this.envSnapshot };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns OS env merged with the session snapshot (snapshot wins). Use this
|
|
52
|
+
* when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
|
|
53
|
+
* so they inherit both OS env and the session's settings env.
|
|
54
|
+
*/
|
|
55
|
+
getMergedEnv() {
|
|
56
|
+
return Object.fromEntries(Object.entries({ ...process.env, ...this.envSnapshot }).filter(([, v]) => v !== undefined));
|
|
57
|
+
}
|
|
38
58
|
// Core loading operations
|
|
39
59
|
/**
|
|
40
60
|
* Load and merge configuration with comprehensive validation
|
|
@@ -292,16 +312,31 @@ export class ConfigurationService {
|
|
|
292
312
|
}
|
|
293
313
|
// Utility operations
|
|
294
314
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
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 &&
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
425
|
+
resolvedApiKey =
|
|
426
|
+
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
386
427
|
}
|
|
387
428
|
if (!resolvedBaseURL) {
|
|
388
|
-
resolvedBaseURL =
|
|
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 =
|
|
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 ||
|
|
473
|
+
const resolvedFastModel = fastModel ||
|
|
474
|
+
this.options.fastModel ||
|
|
475
|
+
(this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
|
|
430
476
|
// Resolve max output tokens
|
|
431
477
|
const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
|
|
432
478
|
const baseConfig = {
|
|
@@ -468,8 +514,9 @@ export class ConfigurationService {
|
|
|
468
514
|
if (this.options.maxInputTokens !== undefined) {
|
|
469
515
|
return this.options.maxInputTokens;
|
|
470
516
|
}
|
|
471
|
-
// Try env (settings.json) first, then process.env
|
|
472
|
-
const envMaxInputTokens =
|
|
517
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
518
|
+
const envMaxInputTokens = this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
|
|
519
|
+
process.env.WAVE_MAX_INPUT_TOKENS;
|
|
473
520
|
if (envMaxInputTokens) {
|
|
474
521
|
const parsed = parseInt(envMaxInputTokens, 10);
|
|
475
522
|
if (!isNaN(parsed)) {
|
|
@@ -510,8 +557,8 @@ export class ConfigurationService {
|
|
|
510
557
|
if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
|
|
511
558
|
return this.currentConfiguration.autoMemoryEnabled;
|
|
512
559
|
}
|
|
513
|
-
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable
|
|
514
|
-
const disableAutoMemory =
|
|
560
|
+
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
|
|
561
|
+
const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
|
|
515
562
|
process.env.WAVE_DISABLE_AUTO_MEMORY;
|
|
516
563
|
if (disableAutoMemory === "1" || disableAutoMemory === "true") {
|
|
517
564
|
return false;
|
|
@@ -541,8 +588,8 @@ export class ConfigurationService {
|
|
|
541
588
|
if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
|
|
542
589
|
return this.currentConfiguration.autoMemoryFrequency;
|
|
543
590
|
}
|
|
544
|
-
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
|
|
545
|
-
const envFrequency =
|
|
591
|
+
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
|
|
592
|
+
const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
|
|
546
593
|
process.env.WAVE_AUTO_MEMORY_FREQUENCY;
|
|
547
594
|
if (envFrequency) {
|
|
548
595
|
const parsed = parseInt(envFrequency, 10);
|
|
@@ -568,8 +615,9 @@ export class ConfigurationService {
|
|
|
568
615
|
if (this.options.maxTokens !== undefined) {
|
|
569
616
|
return this.options.maxTokens;
|
|
570
617
|
}
|
|
571
|
-
// Try env (settings.json) first, then process.env
|
|
572
|
-
const envMaxOutputTokens =
|
|
618
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
619
|
+
const envMaxOutputTokens = this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
|
|
620
|
+
process.env.WAVE_MAX_OUTPUT_TOKENS;
|
|
573
621
|
if (envMaxOutputTokens) {
|
|
574
622
|
const parsed = parseInt(envMaxOutputTokens, 10);
|
|
575
623
|
if (!isNaN(parsed) && parsed > 0) {
|
|
@@ -610,8 +658,9 @@ export class ConfigurationService {
|
|
|
610
658
|
*/
|
|
611
659
|
getConfiguredModels() {
|
|
612
660
|
const models = new Set();
|
|
613
|
-
// Add current model from options or environment
|
|
614
|
-
const currentModel = this.options.model ||
|
|
661
|
+
// Add current model from options or environment (settings snapshot > OS env)
|
|
662
|
+
const currentModel = this.options.model ||
|
|
663
|
+
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
615
664
|
if (currentModel) {
|
|
616
665
|
models.add(currentModel);
|
|
617
666
|
}
|
|
@@ -44,12 +44,16 @@ export class InitializationService {
|
|
|
44
44
|
logger?.error("Failed to initialize MCP servers:", error);
|
|
45
45
|
// Don't throw error to prevent app startup failure
|
|
46
46
|
}
|
|
47
|
-
//
|
|
48
|
-
// Must happen BEFORE loadMergedConfiguration so
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
//
|
|
108
|
+
// 5. Clean current state
|
|
98
109
|
abortMessage(); // Abort any running operations
|
|
99
110
|
subagentManager.cleanup(); // Clean up active subagents
|
|
100
|
-
//
|
|
111
|
+
// 6. Rebuild usage (in correct order)
|
|
101
112
|
messageManager.rebuildUsageFromMessages(sessionData.messages);
|
|
102
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
@@ -32,7 +32,13 @@ export class TaskManager extends EventEmitter {
|
|
|
32
32
|
if (!messageManager)
|
|
33
33
|
return;
|
|
34
34
|
const rootSessionId = messageManager.getRootSessionId();
|
|
35
|
-
|
|
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
|
}
|
package/dist/tools/bashTool.js
CHANGED
|
@@ -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
|
-
|
|
132
|
-
//
|
|
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"}`);
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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. */
|
package/dist/types/hooks.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ export declare class HookConfigurationError extends Error {
|
|
|
54
54
|
readonly validationErrors: string[];
|
|
55
55
|
constructor(configPath: string, validationErrors: string[]);
|
|
56
56
|
}
|
|
57
|
-
export type SessionStartSource = "startup" | "compact" | "clear";
|
|
58
|
-
export type SessionEndSource = "exit" | "stop" | "compact" | "clear";
|
|
57
|
+
export type SessionStartSource = "startup" | "resume" | "compact" | "clear";
|
|
58
|
+
export type SessionEndSource = "exit" | "resume" | "stop" | "compact" | "clear";
|
|
59
59
|
export declare function isValidHookEvent(event: string): event is HookEvent;
|
|
60
60
|
export declare function isValidHookCommand(cmd: unknown): cmd is HookCommand;
|
|
61
61
|
export declare function isValidHookEventConfig(config: unknown): config is HookEventConfig;
|
|
@@ -159,7 +159,7 @@ export function setupAgentContainer(setupOptions) {
|
|
|
159
159
|
toolName: context.toolName,
|
|
160
160
|
toolInput: context.toolInput,
|
|
161
161
|
planFilePath: permissionManager.getPlanFilePath(),
|
|
162
|
-
env: Object.fromEntries(Object.entries(
|
|
162
|
+
env: Object.fromEntries(Object.entries(configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
|
|
163
163
|
});
|
|
164
164
|
if (results.length > 0) {
|
|
165
165
|
const processResult = hookManager.processHookResults("PermissionRequest", results, messageManager);
|
|
@@ -36,11 +36,12 @@ export class OpenAIClient {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
async _create(params, options) {
|
|
39
|
-
const { baseURL, apiKey, defaultHeaders, fetchOptions, fetch: customFetch, } = this.config;
|
|
39
|
+
const { baseURL, apiKey, sessionId, defaultHeaders, fetchOptions, fetch: customFetch, } = this.config;
|
|
40
40
|
const url = `${baseURL}/chat/completions`;
|
|
41
41
|
const headers = {
|
|
42
42
|
"Content-Type": "application/json",
|
|
43
43
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
44
|
+
...(sessionId ? { "x-session-id": sessionId } : {}),
|
|
44
45
|
...defaultHeaders,
|
|
45
46
|
};
|
|
46
47
|
const fetchFn = customFetch || fetch;
|
|
@@ -127,8 +127,13 @@ export class PathEncoder {
|
|
|
127
127
|
// If realpath fails, use the absolute path
|
|
128
128
|
resolvedPath = absolutePath;
|
|
129
129
|
}
|
|
130
|
-
// Encode the resolved path
|
|
131
|
-
|
|
130
|
+
// Encode the resolved path. Use encodeSync (pure string transform) rather
|
|
131
|
+
// than encode(): realpath was already attempted above with its own fallback,
|
|
132
|
+
// so re-resolving via encode()->resolvePath() would throw ENOENT again when
|
|
133
|
+
// the workdir no longer exists (e.g. a deleted worktree during agent
|
|
134
|
+
// shutdown). Session files live under <baseSessionDir>/<encoded>/ regardless
|
|
135
|
+
// of the workdir's existence, so encoding must not require it to exist.
|
|
136
|
+
const encodedName = this.encodeSync(resolvedPath);
|
|
132
137
|
const encodedPath = join(baseSessionDir, encodedName);
|
|
133
138
|
// Generate hash if encoding resulted in truncation
|
|
134
139
|
let pathHash;
|
|
@@ -33,10 +33,27 @@ export declare function getHeadCommit(cwd: string): string;
|
|
|
33
33
|
export declare function createWorktree(name: string, cwd: string, options?: {
|
|
34
34
|
baseRef?: "fresh" | "head";
|
|
35
35
|
}): WorktreeInfo;
|
|
36
|
+
/**
|
|
37
|
+
* Set up a freshly created worktree: copy local settings and gitignored
|
|
38
|
+
* project files (via .worktreeinclude) from the main repo. Best-effort — any
|
|
39
|
+
* failure only logs a warning and never fails worktree creation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function performPostCreationSetup(worktreePath: string, repoRoot: string): Promise<void>;
|
|
36
42
|
/**
|
|
37
43
|
* Remove a git worktree and its branch.
|
|
38
44
|
*/
|
|
39
45
|
export declare function removeWorktree(info: WorktreeInfo): void;
|
|
46
|
+
/**
|
|
47
|
+
* Validate that a worktree path is safe to remove before running git removal.
|
|
48
|
+
* Aligns with Claude Code v2.1.216+ background-session checks:
|
|
49
|
+
* - rejects a path whose final component is a symlink;
|
|
50
|
+
* - rejects a path that resolves outside the repo root.
|
|
51
|
+
*
|
|
52
|
+
* A path that no longer exists (worktree already removed) is allowed so that
|
|
53
|
+
* removal stays best-effort/idempotent; its nearest existing ancestor is used
|
|
54
|
+
* for the containment check. Throws an Error on invalid paths.
|
|
55
|
+
*/
|
|
56
|
+
export declare function validateWorktreeRemovalPath(worktreePath: string, repoRoot: string): void;
|
|
40
57
|
/**
|
|
41
58
|
* Count uncommitted files and new commits in a worktree.
|
|
42
59
|
* Returns null if git commands fail (fail-closed).
|