wave-agent-sdk 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +58 -4
- package/dist/agent.js +91 -19
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/skills/settings.js +1 -12
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +53 -13
- package/dist/core/plugin.js +134 -26
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -1
- package/dist/managers/aiManager.js +190 -21
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/hookManager.d.ts +13 -0
- package/dist/managers/hookManager.js +31 -4
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +103 -8
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +45 -10
- package/dist/managers/mcpManager.js +103 -1
- package/dist/managers/messageManager.d.ts +48 -5
- package/dist/managers/messageManager.js +107 -21
- package/dist/managers/permissionManager.d.ts +40 -0
- package/dist/managers/permissionManager.js +63 -8
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +19 -0
- package/dist/managers/skillManager.js +44 -0
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +35 -3
- package/dist/managers/subagentManager.d.ts +8 -0
- package/dist/managers/subagentManager.js +20 -0
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +318 -119
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +60 -9
- package/dist/services/configurationService.js +129 -54
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +19 -15
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +84 -0
- package/dist/services/jsonlHandler.js +209 -14
- package/dist/services/memory.d.ts +3 -1
- package/dist/services/memory.js +13 -9
- package/dist/services/officialMarketplaceMirror.js +3 -2
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +16 -2
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +144 -3
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/stdio/index.d.ts +3 -1
- package/dist/stdio/index.js +3 -1
- package/dist/stdio/notificationRouter.js +1 -0
- package/dist/stdio/stdioAgent.d.ts +14 -7
- package/dist/stdio/stdioAgent.js +19 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +8 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +20 -5
- package/dist/types/configuration.d.ts +39 -1
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +39 -0
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +15 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +43 -0
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +33 -0
- package/dist/utils/fileUtils.js +81 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -2
- package/dist/utils/messageOperations.js +40 -29
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +39 -43
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/package.json +10 -7
|
@@ -9,6 +9,25 @@ export class InitializationService {
|
|
|
9
9
|
const startTime = performance.now();
|
|
10
10
|
// Set global logger early so managers can use it during initialization
|
|
11
11
|
setGlobalLogger(logger || null);
|
|
12
|
+
// Load the remote settings disk cache synchronously, before anything reads
|
|
13
|
+
// the configuration chain. Two consumers depend on it being this early:
|
|
14
|
+
// loadMergedConfiguration (below) merges the cached managed settings (env,
|
|
15
|
+
// model, disallowedTools), and plugin loading (also below) reads the managed
|
|
16
|
+
// enabledPlugins / marketplaces so an admin-pushed plugin is installed and
|
|
17
|
+
// enabled on this very startup (spec enterprise server-managed-config
|
|
18
|
+
// 「托管配置下发插件市场与启用列表」场景 1). Settings `env` is stored in the
|
|
19
|
+
// per-session env snapshot (NOT process.env), except WAVE_SERVER_URL which is
|
|
20
|
+
// mirrored to process.env so the network fetch (below) can read it via
|
|
21
|
+
// authService.getServerUrl(); no race.
|
|
22
|
+
try {
|
|
23
|
+
const phaseStart = performance.now();
|
|
24
|
+
await remoteSettingsService.initialize();
|
|
25
|
+
logger?.debug(`Initialization Phase [Remote Settings Cache] took ${(performance.now() - phaseStart).toFixed(2)}ms`);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
logger?.error("Failed to initialize remote settings:", error);
|
|
29
|
+
// Don't throw error to prevent app startup failure - continue without remote settings
|
|
30
|
+
}
|
|
12
31
|
// Initialize managers first
|
|
13
32
|
try {
|
|
14
33
|
const phaseStart = performance.now();
|
|
@@ -44,21 +63,6 @@ export class InitializationService {
|
|
|
44
63
|
logger?.error("Failed to initialize MCP servers:", error);
|
|
45
64
|
// Don't throw error to prevent app startup failure
|
|
46
65
|
}
|
|
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.
|
|
53
|
-
try {
|
|
54
|
-
const phaseStart = performance.now();
|
|
55
|
-
await remoteSettingsService.initialize();
|
|
56
|
-
logger?.debug(`Initialization Phase [Remote Settings Cache] took ${(performance.now() - phaseStart).toFixed(2)}ms`);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
logger?.error("Failed to initialize remote settings:", error);
|
|
60
|
-
// Don't throw error to prevent app startup failure - continue without remote settings
|
|
61
|
-
}
|
|
62
66
|
// Initialize hooks configuration
|
|
63
67
|
try {
|
|
64
68
|
const phaseStart = performance.now();
|
|
@@ -18,11 +18,19 @@ export interface InteractionContext {
|
|
|
18
18
|
taskManager: TaskManager;
|
|
19
19
|
options: AgentOptions;
|
|
20
20
|
abortMessage: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Move every directory-derived piece of session state (session transcript
|
|
23
|
+
* location, project rules, memory caches) to another working directory.
|
|
24
|
+
* Used when restoring a session that lives in a different directory.
|
|
25
|
+
*/
|
|
26
|
+
switchWorkdir: (workdir: string) => Promise<void>;
|
|
21
27
|
}
|
|
22
28
|
export declare class InteractionService {
|
|
23
29
|
static sendMessage(context: InteractionContext, content: string, images?: Array<{
|
|
24
30
|
path: string;
|
|
25
31
|
mimeType: string;
|
|
26
32
|
}>): Promise<void>;
|
|
27
|
-
static restoreSession(context: InteractionContext, sessionId: string
|
|
33
|
+
static restoreSession(context: InteractionContext, sessionId: string, restoreOptions?: {
|
|
34
|
+
workdir?: string;
|
|
35
|
+
}): Promise<void>;
|
|
28
36
|
}
|
|
@@ -75,8 +75,14 @@ export class InteractionService {
|
|
|
75
75
|
// Loading state will be automatically updated by the useEffect that watches messages
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
static async restoreSession(context, sessionId) {
|
|
79
|
-
const { messageManager, hookManager, logger, subagentManager, taskManager, options, abortMessage, } = context;
|
|
78
|
+
static async restoreSession(context, sessionId, restoreOptions) {
|
|
79
|
+
const { messageManager, hookManager, logger, subagentManager, taskManager, options, abortMessage, switchWorkdir, } = context;
|
|
80
|
+
// The target session may live in another project directory. Everything
|
|
81
|
+
// below that reads a path (loading the transcript, the SessionStart hook's
|
|
82
|
+
// transcript path, the next append) must use the target directory, never
|
|
83
|
+
// the current one — otherwise the switch writes a second copy of the same
|
|
84
|
+
// session into the current project directory.
|
|
85
|
+
const targetWorkdir = restoreOptions?.workdir ?? messageManager.getWorkdir();
|
|
80
86
|
// 1. Validation
|
|
81
87
|
if (!sessionId || sessionId === messageManager.getSessionId()) {
|
|
82
88
|
return; // No-op if session ID is invalid or already current
|
|
@@ -89,6 +95,9 @@ export class InteractionService {
|
|
|
89
95
|
logger?.warn("Failed to save current session before restore:", error);
|
|
90
96
|
// Continue with restoration even if save fails
|
|
91
97
|
}
|
|
98
|
+
// 2b. Flush point for the session being left behind: it is about to be
|
|
99
|
+
// read from disk again by whoever lists sessions next.
|
|
100
|
+
await messageManager.reAppendSessionMetadata();
|
|
92
101
|
// 3. Run SessionEnd hooks for the current session (cleanup before switching)
|
|
93
102
|
const currentSessionId = messageManager.getSessionId();
|
|
94
103
|
const currentTranscriptPath = messageManager.getTranscriptPath();
|
|
@@ -101,18 +110,29 @@ export class InteractionService {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
// 4. Load target session
|
|
104
|
-
const sessionData = await loadSessionFromJsonl(sessionId,
|
|
113
|
+
const sessionData = await loadSessionFromJsonl(sessionId, targetWorkdir);
|
|
105
114
|
if (!sessionData) {
|
|
106
115
|
throw new Error(`Session not found: ${sessionId}`);
|
|
107
116
|
}
|
|
108
|
-
// 5.
|
|
117
|
+
// 5. Move this session to the target directory. Only after the target
|
|
118
|
+
// transcript has loaded successfully, so a failed restore leaves the
|
|
119
|
+
// current session (and its directory) untouched.
|
|
120
|
+
if (targetWorkdir !== messageManager.getWorkdir()) {
|
|
121
|
+
await switchWorkdir(targetWorkdir);
|
|
122
|
+
}
|
|
123
|
+
// 6. Clean current state
|
|
109
124
|
abortMessage(); // Abort any running operations
|
|
110
125
|
subagentManager.cleanup(); // Clean up active subagents
|
|
111
|
-
//
|
|
126
|
+
// 7. Rebuild usage (in correct order)
|
|
112
127
|
messageManager.rebuildUsageFromMessages(sessionData.messages);
|
|
113
|
-
//
|
|
128
|
+
// 8. Initialize session state last
|
|
114
129
|
messageManager.initializeFromSession(sessionData);
|
|
115
|
-
//
|
|
130
|
+
// 8b. Flush point for the session we just took over. `initializeFromSession`
|
|
131
|
+
// has adopted the title recovered by the load-time whole-file scan, so
|
|
132
|
+
// this is where a title that had slid out of the tail window is written
|
|
133
|
+
// back at EOF (Claude Code's `adoptResumedSessionFile` does the same).
|
|
134
|
+
await messageManager.reAppendSessionMetadata();
|
|
135
|
+
// 9. Run SessionStart hooks for the restored session and inject additional
|
|
116
136
|
// context as a meta user message (matches Claude Code's resume behavior:
|
|
117
137
|
// SessionEnd then SessionStart, hook messages appended to the conversation)
|
|
118
138
|
if (hookManager) {
|
|
@@ -131,7 +151,7 @@ export class InteractionService {
|
|
|
131
151
|
}
|
|
132
152
|
// Update task manager with the root session ID to ensure continuity across compactions
|
|
133
153
|
taskManager.setTaskListId(sessionData.id);
|
|
134
|
-
//
|
|
154
|
+
// 10. Load tasks for the restored session
|
|
135
155
|
const tasks = await taskManager.listTasks();
|
|
136
156
|
options.callbacks?.onTasksChange?.(tasks);
|
|
137
157
|
}
|
|
@@ -24,6 +24,20 @@ export interface SessionMetadataHeader {
|
|
|
24
24
|
/** Git branch at creation time (`git branch --show-current`), when the directory is a git repo. */
|
|
25
25
|
gitBranch?: string;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* User-set conversation title, appended as its own JSONL entry on rename.
|
|
29
|
+
* A dedicated entry type (rather than a field on the append-only metadata
|
|
30
|
+
* header) is what lets the title be changed after creation without rewriting
|
|
31
|
+
* the file: it is appended at rename time, and re-appended at EOF at the
|
|
32
|
+
* session's sparse flush points (`reAppendSessionMetadata` — exit, compaction,
|
|
33
|
+
* resume, rewind) rather than on every save.
|
|
34
|
+
*/
|
|
35
|
+
export interface CustomTitleEntry {
|
|
36
|
+
type: "custom-title";
|
|
37
|
+
customTitle: string;
|
|
38
|
+
/** Session the title belongs to (the file is per-session; kept for traceability). */
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
}
|
|
27
41
|
/**
|
|
28
42
|
* JSONL handler class for message persistence operations
|
|
29
43
|
*/
|
|
@@ -49,6 +63,26 @@ export declare class JsonlHandler {
|
|
|
49
63
|
* Append a single message to JSONL file
|
|
50
64
|
*/
|
|
51
65
|
appendMessage(filePath: string, message: Message): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Append a user-set conversation title (`{"type":"custom-title",...}`).
|
|
68
|
+
*
|
|
69
|
+
* Appending — never rewriting — keeps the file's append-only contract intact
|
|
70
|
+
* and leaves the metadata header and every message untouched. The entry has
|
|
71
|
+
* no `timestamp`, so it can never be mistaken for a message by readers that
|
|
72
|
+
* filter on timestamps.
|
|
73
|
+
*/
|
|
74
|
+
appendCustomTitle(filePath: string, customTitle: string, sessionId?: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Read the session's user-set title from the tail window, if present.
|
|
77
|
+
*
|
|
78
|
+
* Scans the tail and keeps the newest entry, so a rename always wins over an
|
|
79
|
+
* earlier one. Only the tail is read: this is the cheap path used by session
|
|
80
|
+
* listing and by `reAppendSessionMetadata`'s external-writer refresh, and the
|
|
81
|
+
* write side re-appends the entry at EOF at its sparse flush points. An entry
|
|
82
|
+
* that later messages have already pushed out of the window is invisible
|
|
83
|
+
* here — that is what `readMessagesAndCustomTitle` recovers on resume.
|
|
84
|
+
*/
|
|
85
|
+
readCustomTitle(filePath: string): Promise<string | undefined>;
|
|
52
86
|
/**
|
|
53
87
|
* Append multiple messages to JSONL file
|
|
54
88
|
*/
|
|
@@ -61,6 +95,24 @@ export declare class JsonlHandler {
|
|
|
61
95
|
* Read all messages from JSONL file (simplified - no metadata handling)
|
|
62
96
|
*/
|
|
63
97
|
read(filePath: string): Promise<Message[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Read the whole file once, returning every message plus the newest
|
|
100
|
+
* `custom-title` entry.
|
|
101
|
+
*
|
|
102
|
+
* `readCustomTitle` only scans the tail window, so a title that later
|
|
103
|
+
* messages pushed out of it is invisible there. The whole file still holds
|
|
104
|
+
* it, and the resume path collects it here: that recovered value is what a
|
|
105
|
+
* later `reAppendSessionMetadata` writes back at EOF, which is what makes an
|
|
106
|
+
* evicted title self-heal instead of being lost for good.
|
|
107
|
+
*
|
|
108
|
+
* @param filePath - Path to the session JSONL file
|
|
109
|
+
* @returns The messages (reserved entries filtered out) and the newest
|
|
110
|
+
* non-empty custom title, when the file carries one
|
|
111
|
+
*/
|
|
112
|
+
readMessagesAndCustomTitle(filePath: string): Promise<{
|
|
113
|
+
messages: Message[];
|
|
114
|
+
customTitle?: string;
|
|
115
|
+
}>;
|
|
64
116
|
/**
|
|
65
117
|
* Get the last message from JSONL file using efficient file reading (simplified)
|
|
66
118
|
*/
|
|
@@ -89,6 +141,38 @@ export declare class JsonlHandler {
|
|
|
89
141
|
* @returns The persisted metadata, or null when the file has no header
|
|
90
142
|
*/
|
|
91
143
|
readMetadata(filePath: string): Promise<SessionMetadataHeader | null>;
|
|
144
|
+
/**
|
|
145
|
+
* Rewrite a session file keeping only its first `keepMessageCount` messages.
|
|
146
|
+
*
|
|
147
|
+
* This is the one operation that removes history from a session file
|
|
148
|
+
* (`/rewind`); every other write appends. It is a filter over the file's
|
|
149
|
+
* original lines rather than a re-serialization of parsed messages:
|
|
150
|
+
*
|
|
151
|
+
* - Reserved entries (`metadata` header, `custom-title`, and any type added
|
|
152
|
+
* to `RESERVED_ENTRY_TYPES` later) are carried over byte for byte, wherever
|
|
153
|
+
* they sit in the file. Rebuilding the file from `Message[]` used to lose
|
|
154
|
+
* them — `read()` filters reserved entries out, so a rewrite could not see
|
|
155
|
+
* the header or the title — which silently degraded the session to the
|
|
156
|
+
* "legacy file without a header" shape: `createdAt` fell back to
|
|
157
|
+
* `new Date()`, `gitBranch` vanished, and the title could never be read
|
|
158
|
+
* back again.
|
|
159
|
+
* - Lines that are neither reserved nor a message (no `timestamp`) are also
|
|
160
|
+
* carried over, so an entry type written by a newer version survives a
|
|
161
|
+
* rewind performed by an older one.
|
|
162
|
+
* - Only message lines past `keepMessageCount` are dropped.
|
|
163
|
+
*
|
|
164
|
+
* The result is written to a temp file and renamed into place, so an
|
|
165
|
+
* interrupted rewrite leaves the original file untouched.
|
|
166
|
+
*
|
|
167
|
+
* The "is this a message line" test deliberately mirrors `read()`
|
|
168
|
+
* (`!isReservedEntry(type) && timestamp`) and the partial-trailing-line
|
|
169
|
+
* tolerance mirrors its interrupted-append handling, so the caller's message
|
|
170
|
+
* count and this file's line order stay in step.
|
|
171
|
+
*
|
|
172
|
+
* @param filePath - Session JSONL path (must already exist)
|
|
173
|
+
* @param keepMessageCount - Number of leading messages to keep
|
|
174
|
+
*/
|
|
175
|
+
truncateSession(filePath: string, keepMessageCount: number): Promise<void>;
|
|
92
176
|
/**
|
|
93
177
|
* Validate messages before writing
|
|
94
178
|
*/
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
* JSONL file operations service
|
|
3
3
|
* Handles reading and writing JSONL (JSON Lines) session files for improved performance
|
|
4
4
|
*/
|
|
5
|
-
import { appendFile, readFile, writeFile, stat, mkdir } from "fs/promises";
|
|
5
|
+
import { appendFile, readFile, writeFile, stat, mkdir, rename, unlink, } from "fs/promises";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
6
7
|
import { dirname } from "path";
|
|
7
|
-
import { getLastLine, readFirstNLines, readTailLines, } from "../utils/fileUtils.js";
|
|
8
|
+
import { forEachLine, getLastLine, readFirstNLines, readTailLines, } from "../utils/fileUtils.js";
|
|
8
9
|
import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
|
|
10
|
+
import { isReservedEntry } from "./sessionEntries.js";
|
|
11
|
+
/**
|
|
12
|
+
* Flush threshold for `truncateSession`'s output buffer. Only bounds the
|
|
13
|
+
* rewrite's memory — the file it produces is identical regardless of value.
|
|
14
|
+
*/
|
|
15
|
+
const TRUNCATE_FLUSH_BYTES = 64 * 1024;
|
|
9
16
|
/**
|
|
10
17
|
* JSONL handler class for message persistence operations
|
|
11
18
|
*/
|
|
@@ -43,6 +50,49 @@ export class JsonlHandler {
|
|
|
43
50
|
async appendMessage(filePath, message) {
|
|
44
51
|
return this.appendMessages(filePath, [message]);
|
|
45
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Append a user-set conversation title (`{"type":"custom-title",...}`).
|
|
55
|
+
*
|
|
56
|
+
* Appending — never rewriting — keeps the file's append-only contract intact
|
|
57
|
+
* and leaves the metadata header and every message untouched. The entry has
|
|
58
|
+
* no `timestamp`, so it can never be mistaken for a message by readers that
|
|
59
|
+
* filter on timestamps.
|
|
60
|
+
*/
|
|
61
|
+
async appendCustomTitle(filePath, customTitle, sessionId) {
|
|
62
|
+
const entry = {
|
|
63
|
+
type: "custom-title",
|
|
64
|
+
customTitle,
|
|
65
|
+
...(sessionId ? { sessionId } : {}),
|
|
66
|
+
};
|
|
67
|
+
await this.ensureDirectory(dirname(filePath));
|
|
68
|
+
await appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read the session's user-set title from the tail window, if present.
|
|
72
|
+
*
|
|
73
|
+
* Scans the tail and keeps the newest entry, so a rename always wins over an
|
|
74
|
+
* earlier one. Only the tail is read: this is the cheap path used by session
|
|
75
|
+
* listing and by `reAppendSessionMetadata`'s external-writer refresh, and the
|
|
76
|
+
* write side re-appends the entry at EOF at its sparse flush points. An entry
|
|
77
|
+
* that later messages have already pushed out of the window is invisible
|
|
78
|
+
* here — that is what `readMessagesAndCustomTitle` recovers on resume.
|
|
79
|
+
*/
|
|
80
|
+
async readCustomTitle(filePath) {
|
|
81
|
+
for (const line of (await readTailLines(filePath)).reverse()) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(line);
|
|
84
|
+
if (parsed.type !== "custom-title")
|
|
85
|
+
continue;
|
|
86
|
+
return typeof parsed.customTitle === "string" && parsed.customTitle
|
|
87
|
+
? parsed.customTitle
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Partial line at the tail window's boundary — keep looking.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
46
96
|
/**
|
|
47
97
|
* Append multiple messages to JSONL file
|
|
48
98
|
*/
|
|
@@ -87,6 +137,23 @@ export class JsonlHandler {
|
|
|
87
137
|
* Read all messages from JSONL file (simplified - no metadata handling)
|
|
88
138
|
*/
|
|
89
139
|
async read(filePath) {
|
|
140
|
+
return (await this.readMessagesAndCustomTitle(filePath)).messages;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Read the whole file once, returning every message plus the newest
|
|
144
|
+
* `custom-title` entry.
|
|
145
|
+
*
|
|
146
|
+
* `readCustomTitle` only scans the tail window, so a title that later
|
|
147
|
+
* messages pushed out of it is invisible there. The whole file still holds
|
|
148
|
+
* it, and the resume path collects it here: that recovered value is what a
|
|
149
|
+
* later `reAppendSessionMetadata` writes back at EOF, which is what makes an
|
|
150
|
+
* evicted title self-heal instead of being lost for good.
|
|
151
|
+
*
|
|
152
|
+
* @param filePath - Path to the session JSONL file
|
|
153
|
+
* @returns The messages (reserved entries filtered out) and the newest
|
|
154
|
+
* non-empty custom title, when the file carries one
|
|
155
|
+
*/
|
|
156
|
+
async readMessagesAndCustomTitle(filePath) {
|
|
90
157
|
try {
|
|
91
158
|
const content = await readFile(filePath, "utf8");
|
|
92
159
|
// append() always terminates a written batch with "\n" (see append),
|
|
@@ -98,16 +165,26 @@ export class JsonlHandler {
|
|
|
98
165
|
.map((line) => line.trim())
|
|
99
166
|
.filter((line) => line.length > 0);
|
|
100
167
|
if (lines.length === 0) {
|
|
101
|
-
return [];
|
|
168
|
+
return { messages: [] };
|
|
102
169
|
}
|
|
103
170
|
const allMessages = [];
|
|
104
|
-
|
|
171
|
+
let customTitle;
|
|
172
|
+
// Parse every line: real messages are kept, reserved entries (the
|
|
173
|
+
// metadata header, and custom titles) are read for their own fields.
|
|
105
174
|
for (let i = 0; i < lines.length; i++) {
|
|
106
175
|
const line = lines[i];
|
|
107
176
|
try {
|
|
108
177
|
const message = JSON.parse(line);
|
|
109
|
-
|
|
110
|
-
|
|
178
|
+
if (message.type === "custom-title") {
|
|
179
|
+
// Last one wins: a later rename supersedes an earlier one.
|
|
180
|
+
if (typeof message.customTitle === "string" &&
|
|
181
|
+
message.customTitle) {
|
|
182
|
+
customTitle = message.customTitle;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// Any other reserved entry (metadata header): not a message
|
|
187
|
+
if (isReservedEntry(message.type))
|
|
111
188
|
continue;
|
|
112
189
|
if (message.timestamp)
|
|
113
190
|
allMessages.push(message);
|
|
@@ -125,11 +202,11 @@ export class JsonlHandler {
|
|
|
125
202
|
throw new Error(`Invalid JSON at line ${i + 1}: ${error}`);
|
|
126
203
|
}
|
|
127
204
|
}
|
|
128
|
-
return allMessages;
|
|
205
|
+
return { messages: allMessages, customTitle };
|
|
129
206
|
}
|
|
130
207
|
catch (error) {
|
|
131
208
|
if (error.code === "ENOENT") {
|
|
132
|
-
return [];
|
|
209
|
+
return { messages: [] };
|
|
133
210
|
}
|
|
134
211
|
throw new Error(`Failed to read JSONL file "${filePath}": ${error}`);
|
|
135
212
|
}
|
|
@@ -156,15 +233,28 @@ export class JsonlHandler {
|
|
|
156
233
|
}
|
|
157
234
|
try {
|
|
158
235
|
const parsed = JSON.parse(lastLine);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return null;
|
|
236
|
+
if (!isReservedEntry(parsed.type)) {
|
|
237
|
+
return parsed;
|
|
162
238
|
}
|
|
163
|
-
return parsed;
|
|
164
239
|
}
|
|
165
240
|
catch (error) {
|
|
166
241
|
throw new Error(`Invalid JSON in last line of "${filePath}": ${error}`);
|
|
167
242
|
}
|
|
243
|
+
// The file ends with a reserved entry — a `custom-title` appended by a
|
|
244
|
+
// rename, or the metadata header of a session with no messages yet. Both
|
|
245
|
+
// are non-messages, so walk the tail window back to the newest message.
|
|
246
|
+
for (const line of (await readTailLines(filePath)).reverse()) {
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse(line);
|
|
249
|
+
if (isReservedEntry(parsed.type))
|
|
250
|
+
continue;
|
|
251
|
+
return parsed;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Partial line at the tail window's boundary — keep looking.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
168
258
|
}
|
|
169
259
|
catch (error) {
|
|
170
260
|
throw new Error(`Failed to get last message from "${filePath}": ${error}`);
|
|
@@ -188,8 +278,8 @@ export class JsonlHandler {
|
|
|
188
278
|
for (const line of await readTailLines(filePath)) {
|
|
189
279
|
try {
|
|
190
280
|
const parsed = JSON.parse(line);
|
|
191
|
-
//
|
|
192
|
-
if (parsed.type
|
|
281
|
+
// Reserved entry (metadata header, custom-title): not a message
|
|
282
|
+
if (isReservedEntry(parsed.type))
|
|
193
283
|
continue;
|
|
194
284
|
messages.push(parsed);
|
|
195
285
|
}
|
|
@@ -229,6 +319,111 @@ export class JsonlHandler {
|
|
|
229
319
|
}
|
|
230
320
|
return null;
|
|
231
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Rewrite a session file keeping only its first `keepMessageCount` messages.
|
|
324
|
+
*
|
|
325
|
+
* This is the one operation that removes history from a session file
|
|
326
|
+
* (`/rewind`); every other write appends. It is a filter over the file's
|
|
327
|
+
* original lines rather than a re-serialization of parsed messages:
|
|
328
|
+
*
|
|
329
|
+
* - Reserved entries (`metadata` header, `custom-title`, and any type added
|
|
330
|
+
* to `RESERVED_ENTRY_TYPES` later) are carried over byte for byte, wherever
|
|
331
|
+
* they sit in the file. Rebuilding the file from `Message[]` used to lose
|
|
332
|
+
* them — `read()` filters reserved entries out, so a rewrite could not see
|
|
333
|
+
* the header or the title — which silently degraded the session to the
|
|
334
|
+
* "legacy file without a header" shape: `createdAt` fell back to
|
|
335
|
+
* `new Date()`, `gitBranch` vanished, and the title could never be read
|
|
336
|
+
* back again.
|
|
337
|
+
* - Lines that are neither reserved nor a message (no `timestamp`) are also
|
|
338
|
+
* carried over, so an entry type written by a newer version survives a
|
|
339
|
+
* rewind performed by an older one.
|
|
340
|
+
* - Only message lines past `keepMessageCount` are dropped.
|
|
341
|
+
*
|
|
342
|
+
* The result is written to a temp file and renamed into place, so an
|
|
343
|
+
* interrupted rewrite leaves the original file untouched.
|
|
344
|
+
*
|
|
345
|
+
* The "is this a message line" test deliberately mirrors `read()`
|
|
346
|
+
* (`!isReservedEntry(type) && timestamp`) and the partial-trailing-line
|
|
347
|
+
* tolerance mirrors its interrupted-append handling, so the caller's message
|
|
348
|
+
* count and this file's line order stay in step.
|
|
349
|
+
*
|
|
350
|
+
* @param filePath - Session JSONL path (must already exist)
|
|
351
|
+
* @param keepMessageCount - Number of leading messages to keep
|
|
352
|
+
*/
|
|
353
|
+
async truncateSession(filePath, keepMessageCount) {
|
|
354
|
+
const tempPath = `${filePath}.tmp.${process.pid}.${randomUUID()}`;
|
|
355
|
+
try {
|
|
356
|
+
// Materialize the temp file up front so an empty result still renames.
|
|
357
|
+
await writeFile(tempPath, "", "utf8");
|
|
358
|
+
let keptMessages = 0;
|
|
359
|
+
let pendingBrokenLine = false;
|
|
360
|
+
let buffered = [];
|
|
361
|
+
let bufferedLength = 0;
|
|
362
|
+
const flush = async () => {
|
|
363
|
+
if (buffered.length === 0) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await appendFile(tempPath, buffered.join(""), "utf8");
|
|
367
|
+
buffered = [];
|
|
368
|
+
bufferedLength = 0;
|
|
369
|
+
};
|
|
370
|
+
const keepLine = async (line) => {
|
|
371
|
+
const chunk = `${line}\n`;
|
|
372
|
+
buffered.push(chunk);
|
|
373
|
+
bufferedLength += chunk.length;
|
|
374
|
+
if (bufferedLength >= TRUNCATE_FLUSH_BYTES) {
|
|
375
|
+
await flush();
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const handleLine = async (raw) => {
|
|
379
|
+
const line = raw.trim();
|
|
380
|
+
if (line.length === 0) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
let parsed;
|
|
384
|
+
try {
|
|
385
|
+
parsed = JSON.parse(line);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
if (pendingBrokenLine) {
|
|
389
|
+
throw new Error(`Invalid JSON in session file "${filePath}": ${error}`);
|
|
390
|
+
}
|
|
391
|
+
pendingBrokenLine = true;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// A parseable line after a broken one proves the broken line was not
|
|
395
|
+
// the file's last, so it is corruption rather than a partial write.
|
|
396
|
+
if (pendingBrokenLine) {
|
|
397
|
+
throw new Error(`Invalid JSON in session file "${filePath}": truncated trailing line`);
|
|
398
|
+
}
|
|
399
|
+
if (isReservedEntry(parsed.type) || !parsed.timestamp) {
|
|
400
|
+
await keepLine(line);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (keptMessages < keepMessageCount) {
|
|
404
|
+
keptMessages++;
|
|
405
|
+
await keepLine(line);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const { endsWithNewline } = await forEachLine(filePath, handleLine);
|
|
409
|
+
// A broken line is only acceptable as the last line of a file that lacks
|
|
410
|
+
// a trailing newline — the residue of an interrupted append.
|
|
411
|
+
if (pendingBrokenLine && endsWithNewline) {
|
|
412
|
+
throw new Error(`Invalid JSON in session file "${filePath}"`);
|
|
413
|
+
}
|
|
414
|
+
await flush();
|
|
415
|
+
await rename(tempPath, filePath);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
try {
|
|
419
|
+
await unlink(tempPath);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// Cleanup is best effort — the original error is more important.
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
232
427
|
/**
|
|
233
428
|
* Validate messages before writing
|
|
234
429
|
*/
|
|
@@ -19,7 +19,9 @@ export declare class MemoryService {
|
|
|
19
19
|
*/
|
|
20
20
|
ensureAutoMemoryDirectory(workdir: string): Promise<void>;
|
|
21
21
|
/**
|
|
22
|
-
* Get the
|
|
22
|
+
* Get the auto-memory entrypoint (`MEMORY.md`), bounded by both the line and
|
|
23
|
+
* the character cap. Truncation appends a warning naming the cap that fired,
|
|
24
|
+
* so the model can tell the index it is reading is incomplete.
|
|
23
25
|
*/
|
|
24
26
|
getAutoMemoryContent(workdir: string): Promise<string>;
|
|
25
27
|
ensureUserMemoryFile(): Promise<void>;
|
package/dist/services/memory.js
CHANGED
|
@@ -3,8 +3,11 @@ import * as path from "node:path";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { USER_MEMORY_FILE, DATA_DIRECTORY } from "../utils/constants.js";
|
|
5
5
|
import { logger } from "../utils/globalLogger.js";
|
|
6
|
+
import { atomicWriteFile } from "../utils/atomicWrite.js";
|
|
6
7
|
import { getGitCommonDir } from "../utils/gitUtils.js";
|
|
7
8
|
import { pathEncoder } from "../utils/pathEncoder.js";
|
|
9
|
+
import { MEMORY_ENTRYPOINT_NAME } from "../constants/memory.js";
|
|
10
|
+
import { truncateEntrypointContent } from "../utils/memoryEntrypoint.js";
|
|
8
11
|
export class MemoryService {
|
|
9
12
|
constructor(container) {
|
|
10
13
|
this.container = container;
|
|
@@ -51,7 +54,7 @@ export class MemoryService {
|
|
|
51
54
|
catch (error) {
|
|
52
55
|
if (error.code === "ENOENT") {
|
|
53
56
|
const initialContent = "# Project Memory\n\nThis file serves as an index for the project's auto-memory. Wave uses this to track knowledge across sessions.\n\n";
|
|
54
|
-
await
|
|
57
|
+
await atomicWriteFile(memoryFile, initialContent);
|
|
55
58
|
logger.debug(`Created auto-memory file: ${memoryFile}`);
|
|
56
59
|
}
|
|
57
60
|
else {
|
|
@@ -65,18 +68,19 @@ export class MemoryService {
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
68
|
-
* Get the
|
|
71
|
+
* Get the auto-memory entrypoint (`MEMORY.md`), bounded by both the line and
|
|
72
|
+
* the character cap. Truncation appends a warning naming the cap that fired,
|
|
73
|
+
* so the model can tell the index it is reading is incomplete.
|
|
69
74
|
*/
|
|
70
75
|
async getAutoMemoryContent(workdir) {
|
|
71
76
|
if (this._cachedAutoMemoryContent !== null) {
|
|
72
77
|
return this._cachedAutoMemoryContent;
|
|
73
78
|
}
|
|
74
79
|
const memoryDir = this.getAutoMemoryDirectory(workdir);
|
|
75
|
-
const memoryFile = path.join(memoryDir,
|
|
80
|
+
const memoryFile = path.join(memoryDir, MEMORY_ENTRYPOINT_NAME);
|
|
76
81
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
this._cachedAutoMemoryContent = lines.join("\n");
|
|
82
|
+
const raw = await fs.readFile(memoryFile, "utf-8");
|
|
83
|
+
this._cachedAutoMemoryContent = truncateEntrypointContent(raw).content;
|
|
80
84
|
return this._cachedAutoMemoryContent;
|
|
81
85
|
}
|
|
82
86
|
catch (error) {
|
|
@@ -103,7 +107,7 @@ export class MemoryService {
|
|
|
103
107
|
userMemoryFile: USER_MEMORY_FILE,
|
|
104
108
|
});
|
|
105
109
|
const initialContent = "# User Memory\n\nThis is the user-level memory file, recording important information and context across projects.\n\n";
|
|
106
|
-
await
|
|
110
|
+
await atomicWriteFile(USER_MEMORY_FILE, initialContent);
|
|
107
111
|
logger.debug(`Created user memory file: ${USER_MEMORY_FILE}`);
|
|
108
112
|
}
|
|
109
113
|
else {
|
|
@@ -223,7 +227,7 @@ export class MemoryService {
|
|
|
223
227
|
*/
|
|
224
228
|
async writeUserMemoryContent(content) {
|
|
225
229
|
await this.ensureUserMemoryFile();
|
|
226
|
-
await
|
|
230
|
+
await atomicWriteFile(USER_MEMORY_FILE, content);
|
|
227
231
|
this._cachedUserMemory = null;
|
|
228
232
|
this._cachedCombinedMemory = null;
|
|
229
233
|
logger.debug("User memory content written", {
|
|
@@ -240,7 +244,7 @@ export class MemoryService {
|
|
|
240
244
|
async writeProjectMemoryContent(workdir, content) {
|
|
241
245
|
const memoryFilePath = path.join(workdir, "AGENTS.md");
|
|
242
246
|
await fs.mkdir(workdir, { recursive: true });
|
|
243
|
-
await
|
|
247
|
+
await atomicWriteFile(memoryFilePath, content);
|
|
244
248
|
this._cachedProjectMemory = null;
|
|
245
249
|
this._cachedCombinedMemory = null;
|
|
246
250
|
logger.debug("Project memory content written", {
|
|
@@ -37,8 +37,9 @@ const STAGING_SUFFIX = ".staging";
|
|
|
37
37
|
* - TEST(验证用): `https://codechat.codewave-test.163yun.com/wave-plugins-official/`
|
|
38
38
|
*
|
|
39
39
|
* Override with env var `WAVE_OFFICIAL_MARKET_MIRROR_BASE_URL` (highest
|
|
40
|
-
* precedence).
|
|
41
|
-
*
|
|
40
|
+
* precedence). 两套环境的镜像通道均已上线且有内容(2026-09-23 实测:prod 与 test 的
|
|
41
|
+
* `/latest` 均返回 sha、`{sha}.zip` 均可下载)。镜像请求若失败(网络/超时/404/内容异常),
|
|
42
|
+
* 仍按既有设计回退 git 兜底(ALLOW_OFFICIAL_MARKET_GIT_FALLBACK),语义不变。
|
|
42
43
|
*/
|
|
43
44
|
const DEFAULT_OFFICIAL_MARKET_MIRROR_BASE_URL = "https://codechat.codewave.163.com/wave-plugins-official/";
|
|
44
45
|
/** Env var override for the mirror base URL (highest precedence). */
|