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
|
@@ -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);
|
|
@@ -564,16 +619,31 @@ export class ConfigurationService {
|
|
|
564
619
|
baseConfig.fastModelOptions = fastModelSource.options;
|
|
565
620
|
}
|
|
566
621
|
|
|
622
|
+
// Resolve fast-model disable-thinking params from models[fastModel].disableThinkingOptions
|
|
623
|
+
const fastModelDisableThinking =
|
|
624
|
+
fastModelSource && fastModelSource.disableThinkingOptions
|
|
625
|
+
? fastModelSource.disableThinkingOptions
|
|
626
|
+
: undefined;
|
|
627
|
+
if (fastModelDisableThinking) {
|
|
628
|
+
baseConfig.disableThinkingOptions = fastModelDisableThinking;
|
|
629
|
+
}
|
|
630
|
+
|
|
567
631
|
// Merge model-specific settings from configuration
|
|
568
632
|
const modelSpecificConfig =
|
|
569
633
|
resolvedAgentModel &&
|
|
570
634
|
this.currentConfiguration?.models?.[resolvedAgentModel];
|
|
571
635
|
|
|
572
636
|
if (modelSpecificConfig) {
|
|
573
|
-
|
|
637
|
+
const resolved: ModelConfig = {
|
|
574
638
|
...baseConfig,
|
|
575
639
|
...modelSpecificConfig,
|
|
576
640
|
};
|
|
641
|
+
// Re-apply after the spread so the agent model's own
|
|
642
|
+
// disableThinkingOptions cannot clobber the fast-model value.
|
|
643
|
+
if (fastModelDisableThinking) {
|
|
644
|
+
resolved.disableThinkingOptions = fastModelDisableThinking;
|
|
645
|
+
}
|
|
646
|
+
return resolved;
|
|
577
647
|
}
|
|
578
648
|
|
|
579
649
|
return baseConfig;
|
|
@@ -596,8 +666,10 @@ export class ConfigurationService {
|
|
|
596
666
|
return this.options.maxInputTokens;
|
|
597
667
|
}
|
|
598
668
|
|
|
599
|
-
// Try env (settings.json) first, then process.env
|
|
600
|
-
const envMaxInputTokens =
|
|
669
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
670
|
+
const envMaxInputTokens =
|
|
671
|
+
this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
|
|
672
|
+
process.env.WAVE_MAX_INPUT_TOKENS;
|
|
601
673
|
if (envMaxInputTokens) {
|
|
602
674
|
const parsed = parseInt(envMaxInputTokens, 10);
|
|
603
675
|
if (!isNaN(parsed)) {
|
|
@@ -645,9 +717,9 @@ export class ConfigurationService {
|
|
|
645
717
|
return this.currentConfiguration.autoMemoryEnabled;
|
|
646
718
|
}
|
|
647
719
|
|
|
648
|
-
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable
|
|
720
|
+
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
|
|
649
721
|
const disableAutoMemory =
|
|
650
|
-
|
|
722
|
+
this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
|
|
651
723
|
process.env.WAVE_DISABLE_AUTO_MEMORY;
|
|
652
724
|
if (disableAutoMemory === "1" || disableAutoMemory === "true") {
|
|
653
725
|
return false;
|
|
@@ -681,9 +753,9 @@ export class ConfigurationService {
|
|
|
681
753
|
return this.currentConfiguration.autoMemoryFrequency;
|
|
682
754
|
}
|
|
683
755
|
|
|
684
|
-
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
|
|
756
|
+
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
|
|
685
757
|
const envFrequency =
|
|
686
|
-
|
|
758
|
+
this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
|
|
687
759
|
process.env.WAVE_AUTO_MEMORY_FREQUENCY;
|
|
688
760
|
if (envFrequency) {
|
|
689
761
|
const parsed = parseInt(envFrequency, 10);
|
|
@@ -713,8 +785,10 @@ export class ConfigurationService {
|
|
|
713
785
|
return this.options.maxTokens;
|
|
714
786
|
}
|
|
715
787
|
|
|
716
|
-
// Try env (settings.json) first, then process.env
|
|
717
|
-
const envMaxOutputTokens =
|
|
788
|
+
// Try env (settings.json snapshot) first, then process.env
|
|
789
|
+
const envMaxOutputTokens =
|
|
790
|
+
this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
|
|
791
|
+
process.env.WAVE_MAX_OUTPUT_TOKENS;
|
|
718
792
|
if (envMaxOutputTokens) {
|
|
719
793
|
const parsed = parseInt(envMaxOutputTokens, 10);
|
|
720
794
|
if (!isNaN(parsed) && parsed > 0) {
|
|
@@ -761,8 +835,10 @@ export class ConfigurationService {
|
|
|
761
835
|
getConfiguredModels(): string[] {
|
|
762
836
|
const models = new Set<string>();
|
|
763
837
|
|
|
764
|
-
// Add current model from options or environment
|
|
765
|
-
const currentModel =
|
|
838
|
+
// Add current model from options or environment (settings snapshot > OS env)
|
|
839
|
+
const currentModel =
|
|
840
|
+
this.options.model ||
|
|
841
|
+
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
766
842
|
if (currentModel) {
|
|
767
843
|
models.add(currentModel);
|
|
768
844
|
}
|
|
@@ -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,
|
package/src/services/session.ts
CHANGED
|
@@ -612,6 +612,74 @@ export async function cleanupEmptyProjectDirectories(): Promise<void> {
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Clean up "ghost" session files that contain only meta messages
|
|
617
|
+
* (isMeta: true) — e.g. sessions where a SessionStart hook injected a
|
|
618
|
+
* system-reminder but no real user/assistant message was ever sent.
|
|
619
|
+
*
|
|
620
|
+
* Such sessions are no longer created thanks to lazy materialization in
|
|
621
|
+
* saveSession(); this one-time sweep removes files that predate that change
|
|
622
|
+
* so they stop showing up as "0 tokens / No content" entries in the resume
|
|
623
|
+
* list.
|
|
624
|
+
*
|
|
625
|
+
* @returns Promise that resolves to the number of files deleted
|
|
626
|
+
*/
|
|
627
|
+
export async function cleanupMetaOnlySessions(): Promise<number> {
|
|
628
|
+
// Do not perform cleanup operations in test environment
|
|
629
|
+
if (process.env.NODE_ENV === "test") {
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let deletedCount = 0;
|
|
634
|
+
try {
|
|
635
|
+
const projectDirs = await fs.readdir(SESSION_DIR);
|
|
636
|
+
|
|
637
|
+
for (const projectDirName of projectDirs) {
|
|
638
|
+
const projectPath = join(SESSION_DIR, projectDirName);
|
|
639
|
+
try {
|
|
640
|
+
const stat = await fs.stat(projectPath);
|
|
641
|
+
if (!stat.isDirectory()) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const files = await fs.readdir(projectPath);
|
|
646
|
+
for (const file of files) {
|
|
647
|
+
if (!file.endsWith(".jsonl")) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const filePath = join(projectPath, file);
|
|
652
|
+
try {
|
|
653
|
+
// Fast path: a file whose last message is not meta contains a
|
|
654
|
+
// real message, so it can never be meta-only.
|
|
655
|
+
const jsonlHandler = new JsonlHandler();
|
|
656
|
+
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
657
|
+
if (!lastMessage?.isMeta) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const messages = await jsonlHandler.read(filePath);
|
|
662
|
+
if (messages.length > 0 && messages.every((m) => m.isMeta)) {
|
|
663
|
+
await fs.unlink(filePath);
|
|
664
|
+
deletedCount++;
|
|
665
|
+
}
|
|
666
|
+
} catch {
|
|
667
|
+
// Skip corrupted or unreadable files
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} catch {
|
|
672
|
+
// Skip directories we can't access
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return deletedCount;
|
|
681
|
+
}
|
|
682
|
+
|
|
615
683
|
/**
|
|
616
684
|
* Check if a session exists in JSONL storage (new approach)
|
|
617
685
|
*
|
|
@@ -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
|
);
|
|
@@ -162,16 +162,8 @@ export const exitWorktreeTool: ToolPlugin = {
|
|
|
162
162
|
session.originalHeadCommit,
|
|
163
163
|
) ?? { changedFiles: 0, commits: 0 };
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Clear session state and restore CWD
|
|
168
|
-
const aiManager = context.aiManager;
|
|
169
|
-
if (aiManager) {
|
|
170
|
-
aiManager.setWorktreeSession(null);
|
|
171
|
-
aiManager.setWorkdir(originalCwd);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Trigger WorktreeRemove hook (non-blocking)
|
|
165
|
+
// Trigger WorktreeRemove hook (non-blocking) BEFORE git removal so hooks
|
|
166
|
+
// can still read files inside the worktree to clean up external resources.
|
|
175
167
|
let hookTriggered = false;
|
|
176
168
|
if (context.hookManager) {
|
|
177
169
|
try {
|
|
@@ -182,11 +174,13 @@ export const exitWorktreeTool: ToolPlugin = {
|
|
|
182
174
|
projectDir: originalCwd,
|
|
183
175
|
timestamp: new Date(),
|
|
184
176
|
sessionId: context.sessionId ?? "",
|
|
185
|
-
transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
|
|
177
|
+
transcriptPath: context.messageManager?.getTranscriptPath?.() ?? "",
|
|
186
178
|
cwd: originalCwd,
|
|
187
179
|
worktreePath,
|
|
188
180
|
env: Object.fromEntries(
|
|
189
|
-
Object.entries(process.env).filter(
|
|
181
|
+
Object.entries(context.sessionEnv ?? process.env).filter(
|
|
182
|
+
(e) => e[1] !== undefined,
|
|
183
|
+
),
|
|
190
184
|
) as Record<string, string>,
|
|
191
185
|
},
|
|
192
186
|
);
|
|
@@ -206,6 +200,15 @@ export const exitWorktreeTool: ToolPlugin = {
|
|
|
206
200
|
}
|
|
207
201
|
}
|
|
208
202
|
|
|
203
|
+
removeWorktree(worktreeInfo);
|
|
204
|
+
|
|
205
|
+
// Clear session state and restore CWD
|
|
206
|
+
const aiManager = context.aiManager;
|
|
207
|
+
if (aiManager) {
|
|
208
|
+
aiManager.setWorktreeSession(null);
|
|
209
|
+
aiManager.setWorkdir(originalCwd);
|
|
210
|
+
}
|
|
211
|
+
|
|
209
212
|
const discardParts: string[] = [];
|
|
210
213
|
if (summary.commits > 0) {
|
|
211
214
|
discardParts.push(
|
package/src/tools/types.ts
CHANGED
|
@@ -127,4 +127,11 @@ export interface ToolContext {
|
|
|
127
127
|
originalWorkdir?: string;
|
|
128
128
|
/** Workflow manager instance for workflow orchestration */
|
|
129
129
|
workflowManager?: import("../managers/workflowManager.js").WorkflowManager;
|
|
130
|
+
/**
|
|
131
|
+
* Per-session merged environment (OS env overlaid with the settings env
|
|
132
|
+
* snapshot) for this session. Tools that spawn subprocesses (Bash, hooks)
|
|
133
|
+
* should merge this on top of `process.env` so settings `env` vars reach
|
|
134
|
+
* the subprocess without polluting other sessions in one stdio process.
|
|
135
|
+
*/
|
|
136
|
+
sessionEnv?: Record<string, string>;
|
|
130
137
|
}
|
package/src/types/agent.ts
CHANGED
|
@@ -119,10 +119,4 @@ export interface AgentCallbacks
|
|
|
119
119
|
onCommandRunningChange?: (running: boolean) => void;
|
|
120
120
|
onWorkdirChange?: (newCwd: string) => void;
|
|
121
121
|
onQueuedMessagesChange?: (messages: QueuedMessage[]) => void;
|
|
122
|
-
onGoalStateChange?: (
|
|
123
|
-
active: boolean,
|
|
124
|
-
condition?: string,
|
|
125
|
-
elapsed?: string,
|
|
126
|
-
) => void;
|
|
127
|
-
onGoalEvaluating?: (evaluating: boolean) => void;
|
|
128
122
|
}
|
package/src/types/config.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface GatewayConfig {
|
|
|
12
12
|
defaultHeaders?: Record<string, string>;
|
|
13
13
|
fetchOptions?: OpenAI["fetchOptions"];
|
|
14
14
|
fetch?: OpenAI["fetch"];
|
|
15
|
+
/** Session identifier, sent as the `x-session-id` request header for backend correlation. */
|
|
16
|
+
sessionId?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface ModelCapabilities {
|
|
@@ -31,4 +33,11 @@ export interface ModelConfig {
|
|
|
31
33
|
options?: Record<string, unknown>;
|
|
32
34
|
/** Fast model generation params (resolved from models[fastModel].options) */
|
|
33
35
|
fastModelOptions?: Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Fast-model-only disable-thinking params passed through verbatim
|
|
38
|
+
* (e.g. `{ thinking: { type: "disabled" } }`). Applied only in fast-model
|
|
39
|
+
* scenarios (webFetch content processing, `model: fastModel` subagents),
|
|
40
|
+
* never in the agent loop. `{}` clears the default.
|
|
41
|
+
*/
|
|
42
|
+
disableThinkingOptions?: Record<string, unknown>;
|
|
34
43
|
}
|
package/src/types/core.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface Usage {
|
|
|
25
25
|
completion_tokens: number; // Tokens generated in completions
|
|
26
26
|
total_tokens: number; // Sum of prompt + completion tokens
|
|
27
27
|
model?: string; // Model used for the operation (e.g., "gpt-4", "gpt-3.5-turbo")
|
|
28
|
-
operation_type?: "agent" | "compact"
|
|
28
|
+
operation_type?: "agent" | "compact"; // Type of operation that generated usage
|
|
29
29
|
|
|
30
30
|
// Cache-related tokens (Claude top-level + OpenAI prompt_tokens_details)
|
|
31
31
|
cache_read_input_tokens?: number; // Tokens read from cache (Claude) or cached_tokens (OpenAI prompt_tokens_details)
|