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
|
@@ -7,8 +7,7 @@ import { supportsPromptCaching } from "../utils/modelCapabilities.js";
|
|
|
7
7
|
import * as os from "os";
|
|
8
8
|
import * as fs from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
|
-
import { WEB_CONTENT_SYSTEM_PROMPT,
|
|
11
|
-
import { GOAL_EVALUATION_SYSTEM_PROMPT } from "../constants/goalPrompts.js";
|
|
10
|
+
import { WEB_CONTENT_SYSTEM_PROMPT, } from "../prompts/index.js";
|
|
12
11
|
// Global rate limiter state for 1 QPS
|
|
13
12
|
let nextAllowedTime = 0;
|
|
14
13
|
const MIN_INTERVAL = 1000; // 1 second for 1 QPS
|
|
@@ -66,6 +65,15 @@ function getModelConfig(modelName, baseConfig = {}) {
|
|
|
66
65
|
}
|
|
67
66
|
return config;
|
|
68
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Effective disable-thinking params for a model config. No default: these
|
|
70
|
+
* params are only sent when the user explicitly configures
|
|
71
|
+
* `models[X].disableThinkingOptions` (an empty object clears them), so a
|
|
72
|
+
* gateway that doesn't understand the params is never hit with them.
|
|
73
|
+
*/
|
|
74
|
+
function effectiveDisableThinkingOptions(modelConfig) {
|
|
75
|
+
return modelConfig.disableThinkingOptions;
|
|
76
|
+
}
|
|
69
77
|
function validateModelConfig(modelConfig) {
|
|
70
78
|
if (!modelConfig.model) {
|
|
71
79
|
throw new ConfigurationError(CONFIG_ERRORS.MISSING_MODEL, "model", {
|
|
@@ -81,7 +89,7 @@ function validateModelConfig(modelConfig) {
|
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
export async function callAgent(options) {
|
|
84
|
-
const { gatewayConfig, modelConfig, messages, abortSignal, workdir, tools, model, systemPrompt, onContentUpdate, onToolUpdate, onReasoningUpdate, } = options;
|
|
92
|
+
const { gatewayConfig, modelConfig, messages, abortSignal, workdir, tools, model, systemPrompt, onContentUpdate, onToolUpdate, onReasoningUpdate, disableThinkingOptions, } = options;
|
|
85
93
|
// Validate model config at call time
|
|
86
94
|
validateModelConfig(modelConfig);
|
|
87
95
|
// Apply global 1 QPS rate limit
|
|
@@ -147,6 +155,7 @@ export async function callAgent(options) {
|
|
|
147
155
|
const openaiModelConfig = getModelConfig(model || modelConfig.model, {
|
|
148
156
|
max_tokens: resolvedMaxTokens,
|
|
149
157
|
...(modelConfig.options || {}),
|
|
158
|
+
...(disableThinkingOptions ?? {}),
|
|
150
159
|
});
|
|
151
160
|
// Determine if streaming is needed
|
|
152
161
|
const isStreaming = options.stream === true ||
|
|
@@ -509,10 +518,16 @@ export async function processWebContent(options) {
|
|
|
509
518
|
const activeExtraParams = options.model
|
|
510
519
|
? modelConfig.fastModelOptions || {}
|
|
511
520
|
: modelConfig.options || {};
|
|
521
|
+
// Disable-thinking params only apply to the fast-model override path;
|
|
522
|
+
// the agent-model path is untouched.
|
|
523
|
+
const disableThinking = options.model
|
|
524
|
+
? effectiveDisableThinkingOptions(modelConfig)
|
|
525
|
+
: undefined;
|
|
512
526
|
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
513
527
|
temperature: 0.1,
|
|
514
528
|
max_tokens: 4096,
|
|
515
529
|
...activeExtraParams,
|
|
530
|
+
...(disableThinking || {}),
|
|
516
531
|
});
|
|
517
532
|
try {
|
|
518
533
|
const response = await openai.chat.completions.create({
|
|
@@ -555,130 +570,3 @@ export async function processWebContent(options) {
|
|
|
555
570
|
throw error;
|
|
556
571
|
}
|
|
557
572
|
}
|
|
558
|
-
export async function btw(options) {
|
|
559
|
-
const { gatewayConfig, modelConfig, messages, question, abortSignal } = options;
|
|
560
|
-
// Validate model config at call time
|
|
561
|
-
validateModelConfig(modelConfig);
|
|
562
|
-
// Apply global 1 QPS rate limit
|
|
563
|
-
if (process.env.NODE_ENV !== "test" ||
|
|
564
|
-
modelConfig.model === "rate-limit-test") {
|
|
565
|
-
await acquireSlot(abortSignal);
|
|
566
|
-
}
|
|
567
|
-
// Create OpenAI client with injected configuration
|
|
568
|
-
const openai = new OpenAIClient({
|
|
569
|
-
apiKey: gatewayConfig.apiKey,
|
|
570
|
-
baseURL: gatewayConfig.baseURL,
|
|
571
|
-
defaultHeaders: gatewayConfig.defaultHeaders,
|
|
572
|
-
fetchOptions: gatewayConfig.fetchOptions,
|
|
573
|
-
fetch: gatewayConfig.fetch,
|
|
574
|
-
});
|
|
575
|
-
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
576
|
-
temperature: 0.1,
|
|
577
|
-
max_tokens: 4096,
|
|
578
|
-
...(modelConfig.options || {}),
|
|
579
|
-
});
|
|
580
|
-
try {
|
|
581
|
-
const response = await openai.chat.completions.create({
|
|
582
|
-
...openaiModelConfig,
|
|
583
|
-
messages: [
|
|
584
|
-
{
|
|
585
|
-
role: "system",
|
|
586
|
-
content: BTW_SYSTEM_PROMPT,
|
|
587
|
-
},
|
|
588
|
-
...messages,
|
|
589
|
-
{
|
|
590
|
-
role: "user",
|
|
591
|
-
content: question,
|
|
592
|
-
},
|
|
593
|
-
],
|
|
594
|
-
}, {
|
|
595
|
-
signal: abortSignal,
|
|
596
|
-
});
|
|
597
|
-
const result = response.choices[0]?.message?.content?.trim();
|
|
598
|
-
if (!result) {
|
|
599
|
-
throw new Error("Failed to process side question: Empty response from AI");
|
|
600
|
-
}
|
|
601
|
-
const usage = response.usage
|
|
602
|
-
? {
|
|
603
|
-
prompt_tokens: response.usage.prompt_tokens,
|
|
604
|
-
completion_tokens: response.usage.completion_tokens,
|
|
605
|
-
total_tokens: response.usage.total_tokens,
|
|
606
|
-
}
|
|
607
|
-
: undefined;
|
|
608
|
-
return {
|
|
609
|
-
content: result,
|
|
610
|
-
usage,
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
catch (error) {
|
|
614
|
-
if (error.name === "AbortError") {
|
|
615
|
-
logger.info("Side question request was aborted");
|
|
616
|
-
throw new Error("Side question request was aborted");
|
|
617
|
-
}
|
|
618
|
-
logger.error("Failed to process side question:", error);
|
|
619
|
-
throw error;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
export async function evaluateGoal(options) {
|
|
623
|
-
const { gatewayConfig, modelConfig, model, goalCondition, messages, abortSignal, } = options;
|
|
624
|
-
// Create OpenAI client with injected configuration (no rate limiter — bypasses 1 QPS)
|
|
625
|
-
const openai = new OpenAIClient({
|
|
626
|
-
apiKey: gatewayConfig.apiKey,
|
|
627
|
-
baseURL: gatewayConfig.baseURL,
|
|
628
|
-
defaultHeaders: gatewayConfig.defaultHeaders,
|
|
629
|
-
fetchOptions: gatewayConfig.fetchOptions,
|
|
630
|
-
fetch: gatewayConfig.fetch,
|
|
631
|
-
});
|
|
632
|
-
const openaiModelConfig = getModelConfig(model, {
|
|
633
|
-
temperature: 0,
|
|
634
|
-
max_tokens: 200,
|
|
635
|
-
...(modelConfig.fastModelOptions || {}),
|
|
636
|
-
});
|
|
637
|
-
// Strip images from messages to reduce token usage (same as compact)
|
|
638
|
-
const cleanedMessages = messages.map((msg) => {
|
|
639
|
-
if (Array.isArray(msg.content)) {
|
|
640
|
-
const textParts = msg.content.filter((part) => part.type === "text");
|
|
641
|
-
const text = textParts.map((p) => p.text).join("\n");
|
|
642
|
-
return { ...msg, content: text || "(empty message)" };
|
|
643
|
-
}
|
|
644
|
-
return msg;
|
|
645
|
-
});
|
|
646
|
-
try {
|
|
647
|
-
const response = await openai.chat.completions.create({
|
|
648
|
-
...openaiModelConfig,
|
|
649
|
-
messages: [
|
|
650
|
-
{
|
|
651
|
-
role: "system",
|
|
652
|
-
content: GOAL_EVALUATION_SYSTEM_PROMPT,
|
|
653
|
-
},
|
|
654
|
-
...cleanedMessages,
|
|
655
|
-
{
|
|
656
|
-
role: "user",
|
|
657
|
-
content: `Goal condition: ${goalCondition}\n\nHas this goal been achieved based on the conversation above?`,
|
|
658
|
-
},
|
|
659
|
-
],
|
|
660
|
-
}, {
|
|
661
|
-
signal: abortSignal,
|
|
662
|
-
});
|
|
663
|
-
const result = response.choices[0]?.message?.content?.trim();
|
|
664
|
-
if (!result) {
|
|
665
|
-
throw new Error("Goal evaluation returned empty response");
|
|
666
|
-
}
|
|
667
|
-
const usage = response.usage
|
|
668
|
-
? {
|
|
669
|
-
prompt_tokens: response.usage.prompt_tokens,
|
|
670
|
-
completion_tokens: response.usage.completion_tokens,
|
|
671
|
-
total_tokens: response.usage.total_tokens,
|
|
672
|
-
}
|
|
673
|
-
: undefined;
|
|
674
|
-
return { content: result, usage };
|
|
675
|
-
}
|
|
676
|
-
catch (error) {
|
|
677
|
-
if (error.name === "AbortError") {
|
|
678
|
-
logger.info("Goal evaluation was aborted");
|
|
679
|
-
throw new Error("Goal evaluation was aborted");
|
|
680
|
-
}
|
|
681
|
-
logger.error("Goal evaluation failed:", error);
|
|
682
|
-
throw error;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
@@ -7,9 +7,11 @@ export declare class AutoMemoryService {
|
|
|
7
7
|
private container;
|
|
8
8
|
private lastMemoryMessageId;
|
|
9
9
|
private turnsSinceLastExtraction;
|
|
10
|
+
private extractionInProgress;
|
|
11
|
+
private pendingExtraction;
|
|
10
12
|
constructor(container: Container);
|
|
11
13
|
private get messageManager();
|
|
12
|
-
private get
|
|
14
|
+
private get aiManager();
|
|
13
15
|
private get memoryService();
|
|
14
16
|
private get configurationService();
|
|
15
17
|
/**
|
|
@@ -17,7 +19,30 @@ export declare class AutoMemoryService {
|
|
|
17
19
|
*/
|
|
18
20
|
onTurnEnd(workdir: string): Promise<void>;
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
22
|
+
* Wait for an in-flight extraction to settle. Called from Agent.dispose so
|
|
23
|
+
* the process doesn't exit while the extraction fork is mid-flight.
|
|
24
|
+
*/
|
|
25
|
+
drain(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Initialize and execute the extraction in a perfect fork: same system
|
|
28
|
+
* prompt, tools, model, and message prefix as the main conversation, so the
|
|
29
|
+
* prompt cache is reused. A tool gate confines the fork to read-only
|
|
30
|
+
* inspection and memory-directory writes. Runs in-process; callers treat it
|
|
31
|
+
* as fire-and-forget.
|
|
21
32
|
*/
|
|
22
33
|
private runExtraction;
|
|
34
|
+
/**
|
|
35
|
+
* Tool gate for the extraction fork: Read/Grep/Glob are always allowed;
|
|
36
|
+
* Write/Edit only when the target path is inside the memory directory; Bash
|
|
37
|
+
* only for read-only commands (aligned with the permission manager's
|
|
38
|
+
* read-only bash classification). Everything else — Bash rm, MCP tools,
|
|
39
|
+
* Agent, out-of-dir writes — is denied.
|
|
40
|
+
*/
|
|
41
|
+
private isAllowedForkTool;
|
|
42
|
+
/**
|
|
43
|
+
* A bash command is read-only when every part is a READ_ONLY_COMMANDS entry
|
|
44
|
+
* without write redirections, command/process substitution, sed -i, or
|
|
45
|
+
* dangerous find flags. Mirrors PermissionManager.isAutoAllowedPart.
|
|
46
|
+
*/
|
|
47
|
+
private isReadOnlyBashCommand;
|
|
23
48
|
}
|
|
@@ -3,6 +3,14 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import { logger } from "../utils/globalLogger.js";
|
|
4
4
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
5
5
|
import { buildAutoMemoryExtractionPrompt } from "../prompts/autoMemoryExtraction.js";
|
|
6
|
+
import { READ_ONLY_COMMANDS, splitBashCommand, hasWriteRedirections, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, isDangerousFind, } from "../utils/bashParser.js";
|
|
7
|
+
/**
|
|
8
|
+
* Message fed back to the extraction fork when the model requests a tool
|
|
9
|
+
* outside the allowed set (Bash rm, MCP tools, Agent, out-of-dir writes...).
|
|
10
|
+
*/
|
|
11
|
+
const DENIED_TOOL_MESSAGE = "This tool call was denied during auto-memory extraction. Available tools: " +
|
|
12
|
+
"Read, Grep, Glob, read-only Bash commands, and Write/Edit inside the " +
|
|
13
|
+
"memory directory only.";
|
|
6
14
|
/**
|
|
7
15
|
* Service responsible for managing the auto-memory background agent lifecycle.
|
|
8
16
|
* Extracts and updates persistent project-level memory from conversation history.
|
|
@@ -12,12 +20,14 @@ export class AutoMemoryService {
|
|
|
12
20
|
this.container = container;
|
|
13
21
|
this.lastMemoryMessageId = null;
|
|
14
22
|
this.turnsSinceLastExtraction = 0;
|
|
23
|
+
this.extractionInProgress = false;
|
|
24
|
+
this.pendingExtraction = null;
|
|
15
25
|
}
|
|
16
26
|
get messageManager() {
|
|
17
27
|
return this.container.get("MessageManager");
|
|
18
28
|
}
|
|
19
|
-
get
|
|
20
|
-
return this.container.get("
|
|
29
|
+
get aiManager() {
|
|
30
|
+
return this.container.get("AIManager");
|
|
21
31
|
}
|
|
22
32
|
get memoryService() {
|
|
23
33
|
return this.container.get("MemoryService");
|
|
@@ -54,7 +64,12 @@ export class AutoMemoryService {
|
|
|
54
64
|
const recentMessages = messages.slice(startIndex);
|
|
55
65
|
const hasManualMemoryWrite = recentMessages.some((m) => m.role === "assistant" &&
|
|
56
66
|
m.blocks.some((b) => {
|
|
57
|
-
if (b.type === "tool" &&
|
|
67
|
+
if (b.type === "tool" &&
|
|
68
|
+
(b.name === "Write" || b.name === "Edit") &&
|
|
69
|
+
// Only a successful manual write counts as a manual update. A
|
|
70
|
+
// denied/failed write didn't touch memory, so the extraction fork
|
|
71
|
+
// must still run or the information is lost.
|
|
72
|
+
b.success !== false) {
|
|
58
73
|
try {
|
|
59
74
|
const params = b.parameters ? JSON.parse(b.parameters) : null;
|
|
60
75
|
const filePath = params?.file_path || params?.path;
|
|
@@ -77,20 +92,51 @@ export class AutoMemoryService {
|
|
|
77
92
|
this.turnsSinceLastExtraction = 0;
|
|
78
93
|
return;
|
|
79
94
|
}
|
|
80
|
-
// 3.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
catch (error) {
|
|
87
|
-
logger.error("Auto-memory extraction failed to trigger:", error);
|
|
95
|
+
// 3. Concurrency guard: if an extraction is already in flight, skip this
|
|
96
|
+
// turn. turnsSinceLastExtraction is intentionally NOT reset so the next
|
|
97
|
+
// eligible turn retriggers the extraction.
|
|
98
|
+
if (this.extractionInProgress) {
|
|
99
|
+
logger.debug("Skipping auto-memory extraction: another extraction is still in progress.");
|
|
100
|
+
return;
|
|
88
101
|
}
|
|
102
|
+
// 4. Trigger the perfect-fork extraction fire-and-forget. The message
|
|
103
|
+
// snapshot and new-message count are computed now; lastMemoryMessageId
|
|
104
|
+
// advances at trigger time so the next extraction starts from a later
|
|
105
|
+
// window even while this one runs.
|
|
106
|
+
const lastExtractedIndex = this.lastMemoryMessageId
|
|
107
|
+
? messages.findIndex((m) => m.id === this.lastMemoryMessageId)
|
|
108
|
+
: -1;
|
|
109
|
+
const newMessageCount = lastExtractedIndex === -1
|
|
110
|
+
? messages.length
|
|
111
|
+
: messages.length - 1 - lastExtractedIndex;
|
|
112
|
+
this.turnsSinceLastExtraction = 0;
|
|
113
|
+
this.lastMemoryMessageId = messages[messages.length - 1].id || null;
|
|
114
|
+
this.extractionInProgress = true;
|
|
115
|
+
const extraction = this.runExtraction(workdir, messages, newMessageCount)
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
logger.error("Auto-memory extraction failed:", error);
|
|
118
|
+
})
|
|
119
|
+
.finally(() => {
|
|
120
|
+
this.extractionInProgress = false;
|
|
121
|
+
this.pendingExtraction = null;
|
|
122
|
+
});
|
|
123
|
+
this.pendingExtraction = extraction;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Wait for an in-flight extraction to settle. Called from Agent.dispose so
|
|
127
|
+
* the process doesn't exit while the extraction fork is mid-flight.
|
|
128
|
+
*/
|
|
129
|
+
async drain() {
|
|
130
|
+
await this.pendingExtraction;
|
|
89
131
|
}
|
|
90
132
|
/**
|
|
91
|
-
* Initialize and execute the
|
|
133
|
+
* Initialize and execute the extraction in a perfect fork: same system
|
|
134
|
+
* prompt, tools, model, and message prefix as the main conversation, so the
|
|
135
|
+
* prompt cache is reused. A tool gate confines the fork to read-only
|
|
136
|
+
* inspection and memory-directory writes. Runs in-process; callers treat it
|
|
137
|
+
* as fire-and-forget.
|
|
92
138
|
*/
|
|
93
|
-
async runExtraction(workdir, messages) {
|
|
139
|
+
async runExtraction(workdir, messages, newMessageCount) {
|
|
94
140
|
const memoryDir = this.memoryService.getAutoMemoryDirectory(workdir);
|
|
95
141
|
// Ensure memory directory exists before starting
|
|
96
142
|
await this.memoryService.ensureAutoMemoryDirectory(workdir);
|
|
@@ -106,30 +152,72 @@ export class AutoMemoryService {
|
|
|
106
152
|
catch {
|
|
107
153
|
// Ignore if directory doesn't exist yet
|
|
108
154
|
}
|
|
109
|
-
// Calculate how many new messages to analyze
|
|
110
|
-
let newMessageCount = messages.length;
|
|
111
|
-
if (this.lastMemoryMessageId) {
|
|
112
|
-
const lastIndex = messages.findIndex((m) => m.id === this.lastMemoryMessageId);
|
|
113
|
-
if (lastIndex !== -1) {
|
|
114
|
-
newMessageCount = messages.length - 1 - lastIndex;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
155
|
const prompt = buildAutoMemoryExtractionPrompt(newMessageCount, existingMemoriesManifest);
|
|
118
|
-
|
|
119
|
-
await this.forkedAgentManager.forkAndExecute("general-purpose", messages, {
|
|
120
|
-
description: "Auto-memory extraction background agent",
|
|
121
|
-
allowedTools: [
|
|
122
|
-
"Read",
|
|
123
|
-
"Glob",
|
|
124
|
-
"Grep",
|
|
125
|
-
`Write(${memoryDir}/**/*)`,
|
|
126
|
-
`Edit(${memoryDir}/**/*)`,
|
|
127
|
-
`Bash(rm ${memoryDir}/**/*)`,
|
|
128
|
-
],
|
|
129
|
-
model: "fastModel", // Use fast model for background tasks to reduce latency and cost
|
|
130
|
-
permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user
|
|
156
|
+
await this.aiManager.runAutoMemoryFork(messages, `${prompt}\n\nThe memory directory for this project is: ${memoryDir}`, {
|
|
131
157
|
maxTurns: 5, // Limit turns to prevent verification rabbit-holes
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
canUseTool: (name, args) => this.isAllowedForkTool(name, args, memoryDir, workdir),
|
|
159
|
+
deniedToolMessage: DENIED_TOOL_MESSAGE,
|
|
160
|
+
});
|
|
161
|
+
logger.debug("Auto-memory extraction completed.");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Tool gate for the extraction fork: Read/Grep/Glob are always allowed;
|
|
165
|
+
* Write/Edit only when the target path is inside the memory directory; Bash
|
|
166
|
+
* only for read-only commands (aligned with the permission manager's
|
|
167
|
+
* read-only bash classification). Everything else — Bash rm, MCP tools,
|
|
168
|
+
* Agent, out-of-dir writes — is denied.
|
|
169
|
+
*/
|
|
170
|
+
isAllowedForkTool(name, args, memoryDir, workdir) {
|
|
171
|
+
if (name === "Read" || name === "Grep" || name === "Glob") {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (name === "Write" || name === "Edit") {
|
|
175
|
+
const filePath = args.file_path ?? args.path;
|
|
176
|
+
if (typeof filePath !== "string" || !filePath)
|
|
177
|
+
return false;
|
|
178
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
179
|
+
? filePath
|
|
180
|
+
: path.resolve(workdir, filePath);
|
|
181
|
+
return isPathInside(absolutePath, memoryDir);
|
|
182
|
+
}
|
|
183
|
+
if (name === "Bash") {
|
|
184
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
185
|
+
return this.isReadOnlyBashCommand(command);
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A bash command is read-only when every part is a READ_ONLY_COMMANDS entry
|
|
191
|
+
* without write redirections, command/process substitution, sed -i, or
|
|
192
|
+
* dangerous find flags. Mirrors PermissionManager.isAutoAllowedPart.
|
|
193
|
+
*/
|
|
194
|
+
isReadOnlyBashCommand(command) {
|
|
195
|
+
if (!command.trim())
|
|
196
|
+
return false;
|
|
197
|
+
if (hasWriteRedirections(command))
|
|
198
|
+
return false;
|
|
199
|
+
if (hasCommandSubstitution(command))
|
|
200
|
+
return false;
|
|
201
|
+
if (hasProcessSubstitution(command))
|
|
202
|
+
return false;
|
|
203
|
+
if (hasSedInPlace(command))
|
|
204
|
+
return false;
|
|
205
|
+
const parts = splitBashCommand(command);
|
|
206
|
+
if (parts.length === 0)
|
|
207
|
+
return false;
|
|
208
|
+
return parts.every((part) => {
|
|
209
|
+
const trimmed = part.trim();
|
|
210
|
+
if (!trimmed)
|
|
211
|
+
return true;
|
|
212
|
+
const commandMatch = trimmed.match(/^(\w+)(\s+.*)?$/);
|
|
213
|
+
if (!commandMatch)
|
|
214
|
+
return false;
|
|
215
|
+
const cmd = commandMatch[1];
|
|
216
|
+
if (!READ_ONLY_COMMANDS.includes(cmd))
|
|
217
|
+
return false;
|
|
218
|
+
if (cmd === "find" && isDangerousFind(part))
|
|
219
|
+
return false;
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
134
222
|
}
|
|
135
223
|
}
|
|
@@ -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
|
/**
|