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.
- 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.d.ts +9 -20
- package/dist/agent.js +28 -99
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +71 -8
- package/dist/managers/aiManager.js +290 -85
- 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/messageManager.d.ts +9 -5
- package/dist/managers/messageManager.js +36 -12
- package/dist/managers/subagentManager.d.ts +6 -0
- package/dist/managers/subagentManager.js +33 -22
- package/dist/managers/toolManager.js +5 -0
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/services/aiService.d.ts +1 -34
- package/dist/services/aiService.js +18 -130
- package/dist/services/autoMemoryService.d.ts +27 -2
- package/dist/services/autoMemoryService.js +124 -36
- package/dist/services/configurationService.d.ts +21 -2
- package/dist/services/configurationService.js +86 -24
- 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/session.d.ts +13 -0
- package/dist/services/session.js +64 -0
- 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/agent.d.ts +0 -2
- package/dist/types/config.d.ts +9 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/hooks.d.ts +2 -2
- package/dist/utils/containerSetup.js +13 -4
- 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 +43 -112
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +389 -110
- 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/messageManager.ts +51 -23
- package/src/managers/subagentManager.ts +36 -25
- package/src/managers/toolManager.ts +7 -0
- package/src/prompts/index.ts +0 -4
- package/src/services/aiService.ts +25 -203
- package/src/services/autoMemoryService.ts +145 -39
- package/src/services/configurationService.ts +100 -24
- package/src/services/initializationService.ts +17 -4
- package/src/services/interactionService.ts +49 -6
- package/src/services/remoteSettingsService.ts +16 -1
- package/src/services/session.ts +68 -0
- 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/agent.ts +0 -6
- package/src/types/config.ts +9 -0
- package/src/types/core.ts +1 -1
- package/src/types/hooks.ts +2 -2
- package/src/utils/containerSetup.ts +15 -5
- package/src/utils/openaiClient.ts +2 -0
- package/src/utils/pathEncoder.ts +7 -2
- package/src/utils/worktreeUtils.ts +401 -1
- package/dist/constants/goalPrompts.d.ts +0 -1
- package/dist/constants/goalPrompts.js +0 -10
- package/dist/managers/goalManager.d.ts +0 -42
- package/dist/managers/goalManager.js +0 -177
- package/src/constants/goalPrompts.ts +0 -10
- 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
|
-
*
|
|
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 = {
|
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
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
|
-
//
|
|
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,
|
|
@@ -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
|
*
|
package/dist/services/session.js
CHANGED
|
@@ -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
|
-
|
|
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/agent.d.ts
CHANGED
|
@@ -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
|
}
|
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. */
|
|
@@ -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
|
}
|
package/dist/types/core.d.ts
CHANGED
|
@@ -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"
|
|
25
|
+
operation_type?: "agent" | "compact";
|
|
26
26
|
cache_read_input_tokens?: number;
|
|
27
27
|
cache_creation_input_tokens?: number;
|
|
28
28
|
cache_creation?: {
|