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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginManifest, CustomSlashCommand, Skill, LspConfig, McpConfig, PartialHookConfiguration } from "../types/index.js";
|
|
1
|
+
import { PluginManifest, CustomSlashCommand, Skill, LspConfig, McpConfig, McpServerConfig, PartialHookConfiguration } from "../types/index.js";
|
|
2
2
|
import { type SubagentConfiguration } from "../utils/subagentParser.js";
|
|
3
3
|
export declare class PluginLoader {
|
|
4
4
|
/**
|
|
@@ -21,9 +21,17 @@ export declare class PluginLoader {
|
|
|
21
21
|
*/
|
|
22
22
|
static loadLspConfig(pluginPath: string): Promise<LspConfig | undefined>;
|
|
23
23
|
/**
|
|
24
|
-
* Load MCP configuration from a plugin
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
* Load MCP configuration from a plugin.
|
|
25
|
+
*
|
|
26
|
+
* Accepts both declaration shapes found in the wild (spec plugin A-023): the
|
|
27
|
+
* wrapped `{"mcpServers": {…}}` and the flat `{"<serverName>": {…}}`, matching
|
|
28
|
+
* Claude Code's `parsed.mcpServers || parsed`. A plugin manifest that declares
|
|
29
|
+
* `mcpServers` inline is equivalent to a sibling `.mcp.json`; when both exist
|
|
30
|
+
* they are merged by server name, the file winning.
|
|
31
|
+
*
|
|
32
|
+
* @param manifestMcpServers Inline declarations from the plugin manifest
|
|
33
|
+
*/
|
|
34
|
+
static loadMcpConfig(pluginPath: string, manifestMcpServers?: Record<string, McpServerConfig>): Promise<McpConfig | undefined>;
|
|
27
35
|
/**
|
|
28
36
|
* Load hooks configuration from a plugin
|
|
29
37
|
*/
|
|
@@ -128,18 +128,33 @@ export class PluginLoader {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
|
-
* Load MCP configuration from a plugin
|
|
131
|
+
* Load MCP configuration from a plugin.
|
|
132
|
+
*
|
|
133
|
+
* Accepts both declaration shapes found in the wild (spec plugin A-023): the
|
|
134
|
+
* wrapped `{"mcpServers": {…}}` and the flat `{"<serverName>": {…}}`, matching
|
|
135
|
+
* Claude Code's `parsed.mcpServers || parsed`. A plugin manifest that declares
|
|
136
|
+
* `mcpServers` inline is equivalent to a sibling `.mcp.json`; when both exist
|
|
137
|
+
* they are merged by server name, the file winning.
|
|
138
|
+
*
|
|
139
|
+
* @param manifestMcpServers Inline declarations from the plugin manifest
|
|
132
140
|
*/
|
|
133
|
-
static async loadMcpConfig(pluginPath) {
|
|
141
|
+
static async loadMcpConfig(pluginPath, manifestMcpServers) {
|
|
142
|
+
const servers = {
|
|
143
|
+
...(manifestMcpServers ?? {}),
|
|
144
|
+
};
|
|
134
145
|
const mcpPath = path.join(pluginPath, ".mcp.json");
|
|
135
146
|
try {
|
|
136
147
|
const content = await fs.readFile(mcpPath, "utf-8");
|
|
148
|
+
const parsed = JSON.parse(content);
|
|
137
149
|
// Return raw config — let McpManager resolve templates and capture originalUrl
|
|
138
|
-
|
|
150
|
+
Object.assign(servers, extractMcpServers(parsed));
|
|
139
151
|
}
|
|
140
152
|
catch {
|
|
141
|
-
|
|
153
|
+
// no .mcp.json — inline declarations (if any) stand alone
|
|
142
154
|
}
|
|
155
|
+
if (Object.keys(servers).length === 0)
|
|
156
|
+
return undefined;
|
|
157
|
+
return { mcpServers: servers };
|
|
143
158
|
}
|
|
144
159
|
/**
|
|
145
160
|
* Load hooks configuration from a plugin
|
|
@@ -197,11 +212,27 @@ export class PluginLoader {
|
|
|
197
212
|
if (!manifest.description) {
|
|
198
213
|
throw new Error("Plugin manifest missing 'description'");
|
|
199
214
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
215
|
+
// `version` is optional: the Claude Code ecosystem omits it freely and the
|
|
216
|
+
// install path already defaults it (spec plugin A-022).
|
|
203
217
|
if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
204
218
|
throw new Error(`Invalid plugin name: ${manifest.name}. Only lowercase letters, numbers, and hyphens are allowed.`);
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Normalizes an MCP declaration to a server map, accepting both the wrapped and
|
|
224
|
+
* the flat shape (spec plugin A-023). Anything that is not an object of server
|
|
225
|
+
* configs yields an empty map rather than throwing, so one malformed declaration
|
|
226
|
+
* cannot fail the whole plugin.
|
|
227
|
+
*/
|
|
228
|
+
function extractMcpServers(parsed) {
|
|
229
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
const record = parsed;
|
|
233
|
+
const wrapped = record.mcpServers;
|
|
234
|
+
const servers = wrapped && typeof wrapped === "object" && !Array.isArray(wrapped)
|
|
235
|
+
? wrapped
|
|
236
|
+
: record;
|
|
237
|
+
return servers;
|
|
238
|
+
}
|
|
@@ -303,12 +303,26 @@ export function mergeRemoteSettings(localMerged, remote) {
|
|
|
303
303
|
result.worktree = remote.worktree;
|
|
304
304
|
if (remote.models !== undefined)
|
|
305
305
|
result.models = remote.models;
|
|
306
|
+
// Plugin / marketplace maps merge per key (remote wins per key), like `env`
|
|
307
|
+
// above: the managed layer's keys override same-named local ones while every
|
|
308
|
+
// key the admin did not mention stays as the user configured it. A wholesale
|
|
309
|
+
// replace would silently drop all of a user's own plugins the moment an admin
|
|
310
|
+
// pushes a single managed one (spec enterprise server-managed-config
|
|
311
|
+
// 「托管配置下发插件市场与启用列表」场景 5, matching Claude Code's policy layer).
|
|
306
312
|
if (remote.marketplaces !== undefined)
|
|
307
|
-
result.marketplaces =
|
|
313
|
+
result.marketplaces = {
|
|
314
|
+
...localMerged.marketplaces,
|
|
315
|
+
...remote.marketplaces,
|
|
316
|
+
};
|
|
308
317
|
if (remote.enabledPlugins !== undefined)
|
|
309
|
-
result.enabledPlugins =
|
|
318
|
+
result.enabledPlugins = {
|
|
319
|
+
...localMerged.enabledPlugins,
|
|
320
|
+
...remote.enabledPlugins,
|
|
321
|
+
};
|
|
310
322
|
if (remote.enableArtifact !== undefined)
|
|
311
323
|
result.enableArtifact = remote.enableArtifact;
|
|
324
|
+
if (remote.enableExec !== undefined)
|
|
325
|
+
result.enableExec = remote.enableExec;
|
|
312
326
|
return result;
|
|
313
327
|
}
|
|
314
328
|
/**
|
|
@@ -22,6 +22,13 @@ export interface SessionData {
|
|
|
22
22
|
workdir: string;
|
|
23
23
|
lastActiveAt: string;
|
|
24
24
|
latestTotalTokens: number;
|
|
25
|
+
/**
|
|
26
|
+
* User-set title recovered from the whole-file scan (see
|
|
27
|
+
* `readMessagesAndCustomTitle`). Held in memory by the host so a later
|
|
28
|
+
* `reAppendSessionMetadata` can write it back at EOF even after later
|
|
29
|
+
* messages pushed it out of the tail window.
|
|
30
|
+
*/
|
|
31
|
+
customTitle?: string;
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
export interface SessionMetadata {
|
|
@@ -33,6 +40,11 @@ export interface SessionMetadata {
|
|
|
33
40
|
lastActiveAt: Date;
|
|
34
41
|
latestTotalTokens: number;
|
|
35
42
|
firstMessage?: string;
|
|
43
|
+
/**
|
|
44
|
+
* User-set conversation title (from a `custom-title` entry in the session
|
|
45
|
+
* file). Takes precedence over `firstMessage` when rendering a label.
|
|
46
|
+
*/
|
|
47
|
+
customTitle?: string;
|
|
36
48
|
/** Git branch at session creation time (from the metadata header). */
|
|
37
49
|
branch?: string;
|
|
38
50
|
}
|
|
@@ -75,6 +87,21 @@ export declare function getSessionFilePath(sessionId: string, workdir: string, s
|
|
|
75
87
|
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
76
88
|
*/
|
|
77
89
|
export declare function createSession(sessionId: string, workdir: string, sessionType?: "main" | "subagent"): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Rewrite a session file down to its first `keepMessageCount` messages.
|
|
92
|
+
*
|
|
93
|
+
* The only caller is `/rewind`. Unlike `appendMessages` this removes history
|
|
94
|
+
* from the file, but it keeps every file-level reserved entry (metadata header,
|
|
95
|
+
* `custom-title`) byte for byte — dropping them would silently turn the session
|
|
96
|
+
* into the "legacy file without a header" shape, losing its creation time,
|
|
97
|
+
* git branch and user-set title.
|
|
98
|
+
*
|
|
99
|
+
* @param sessionId - UUID session identifier
|
|
100
|
+
* @param keepMessageCount - Number of leading messages to keep
|
|
101
|
+
* @param workdir - Working directory for the session
|
|
102
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
103
|
+
*/
|
|
104
|
+
export declare function truncateSession(sessionId: string, keepMessageCount: number, workdir: string, sessionType?: "main" | "subagent"): Promise<void>;
|
|
78
105
|
/**
|
|
79
106
|
* Append messages to session using JSONL format (new approach)
|
|
80
107
|
*
|
|
@@ -84,6 +111,53 @@ export declare function createSession(sessionId: string, workdir: string, sessio
|
|
|
84
111
|
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
85
112
|
*/
|
|
86
113
|
export declare function appendMessages(sessionId: string, newMessages: Message[], workdir: string, sessionType?: "main" | "subagent"): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Set (or replace) a session's user-set title.
|
|
116
|
+
*
|
|
117
|
+
* The title is persisted as a `custom-title` entry appended to the session's
|
|
118
|
+
* own JSONL file — never as a rewrite of the metadata header, and never in a
|
|
119
|
+
* sidecar index — so every host that reads session files sees it without any
|
|
120
|
+
* extra sync channel.
|
|
121
|
+
*
|
|
122
|
+
* A session whose transcript has not been materialized yet (only meta messages
|
|
123
|
+
* so far) is created here: renaming is an explicit user action, so it is not
|
|
124
|
+
* subject to the "meta-only sessions never hit disk" rule.
|
|
125
|
+
*
|
|
126
|
+
* An empty/blank title is a no-op — it is not a signal to clear the title.
|
|
127
|
+
*
|
|
128
|
+
* @param sessionId - UUID session identifier
|
|
129
|
+
* @param workdir - Working directory the session belongs to
|
|
130
|
+
* @param title - New title
|
|
131
|
+
* @returns The trimmed title that was written, or undefined for a blank no-op
|
|
132
|
+
*/
|
|
133
|
+
export declare function setSessionCustomTitle(sessionId: string, workdir: string, title: string): Promise<string | undefined>;
|
|
134
|
+
/**
|
|
135
|
+
* Re-append the session's reserved metadata entries at EOF.
|
|
136
|
+
*
|
|
137
|
+
* A listing reads only the tail window (64 KB) of each session file, so a
|
|
138
|
+
* `custom-title` entry that later messages pushed past that window becomes
|
|
139
|
+
* invisible — the session would flip back to showing its first message. This
|
|
140
|
+
* runs at the session's sparse flush points (spec `session-management.md`
|
|
141
|
+
* 验收场景 5): graceful exit, compaction, taking over a resumed session,
|
|
142
|
+
* switching away from one, and after `/rewind` rewrites the file. Claude Code
|
|
143
|
+
* solves the same problem the same way (`reAppendSessionMetadata`,
|
|
144
|
+
* `sessionStorage.ts:458/983/1533`, `compact.ts:711/1057`).
|
|
145
|
+
*
|
|
146
|
+
* The entry is refreshed from the tail first: another process (the CLI session
|
|
147
|
+
* picker, a second host) may have renamed the session while we held it, in
|
|
148
|
+
* which case its value — not our stale in-memory one — is the truth. When the
|
|
149
|
+
* tail holds nothing (already evicted, or never written by anyone else), the
|
|
150
|
+
* caller's in-memory value stands.
|
|
151
|
+
*
|
|
152
|
+
* A missing transcript file is a no-op: re-appending must never materialize a
|
|
153
|
+
* session that does not exist.
|
|
154
|
+
*
|
|
155
|
+
* @param sessionId - UUID session identifier
|
|
156
|
+
* @param workdir - Working directory the session belongs to
|
|
157
|
+
* @param customTitle - Title held in memory by the caller (may be undefined)
|
|
158
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
159
|
+
*/
|
|
160
|
+
export declare function reAppendSessionMetadata(sessionId: string, workdir: string, customTitle: string | undefined, sessionType?: "main" | "subagent"): Promise<void>;
|
|
87
161
|
/**
|
|
88
162
|
* Load session data from JSONL file (new approach)
|
|
89
163
|
*
|
package/dist/services/session.js
CHANGED
|
@@ -22,6 +22,7 @@ import { execFile } from "child_process";
|
|
|
22
22
|
import { promisify } from "util";
|
|
23
23
|
import { PathEncoder } from "../utils/pathEncoder.js";
|
|
24
24
|
import { JsonlHandler } from "../services/jsonlHandler.js";
|
|
25
|
+
import { isReservedEntry } from "../services/sessionEntries.js";
|
|
25
26
|
import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
|
|
26
27
|
import { logger } from "../utils/globalLogger.js";
|
|
27
28
|
import { getMessageContent } from "../utils/messageOperations.js";
|
|
@@ -119,6 +120,35 @@ export async function createSession(sessionId, workdir, sessionType = "main") {
|
|
|
119
120
|
gitBranch: await getGitBranch(workdir),
|
|
120
121
|
});
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Rewrite a session file down to its first `keepMessageCount` messages.
|
|
125
|
+
*
|
|
126
|
+
* The only caller is `/rewind`. Unlike `appendMessages` this removes history
|
|
127
|
+
* from the file, but it keeps every file-level reserved entry (metadata header,
|
|
128
|
+
* `custom-title`) byte for byte — dropping them would silently turn the session
|
|
129
|
+
* into the "legacy file without a header" shape, losing its creation time,
|
|
130
|
+
* git branch and user-set title.
|
|
131
|
+
*
|
|
132
|
+
* @param sessionId - UUID session identifier
|
|
133
|
+
* @param keepMessageCount - Number of leading messages to keep
|
|
134
|
+
* @param workdir - Working directory for the session
|
|
135
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
136
|
+
*/
|
|
137
|
+
export async function truncateSession(sessionId, keepMessageCount, workdir, sessionType = "main") {
|
|
138
|
+
// Do not save session files in test environment
|
|
139
|
+
if (process.env.NODE_ENV === "test") {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const jsonlHandler = new JsonlHandler();
|
|
143
|
+
const filePath = await generateSessionFilePath(sessionId, workdir, sessionType);
|
|
144
|
+
try {
|
|
145
|
+
await fs.access(filePath);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new Error(`Session file not found: ${sessionId}. Cannot rewind a session that was never persisted.`);
|
|
149
|
+
}
|
|
150
|
+
await jsonlHandler.truncateSession(filePath, keepMessageCount);
|
|
151
|
+
}
|
|
122
152
|
/**
|
|
123
153
|
* Append messages to session using JSONL format (new approach)
|
|
124
154
|
*
|
|
@@ -146,10 +176,114 @@ export async function appendMessages(sessionId, newMessages, workdir, sessionTyp
|
|
|
146
176
|
catch {
|
|
147
177
|
throw new Error(`Session file not found: ${sessionId}. Use createSession() to create a new session first.`);
|
|
148
178
|
}
|
|
179
|
+
// Append only. Keeping the title inside the listing's tail window is the
|
|
180
|
+
// job of `reAppendSessionMetadata`, which runs at the session's sparse flush
|
|
181
|
+
// points — not here: appending the entry on every save costs a tail read and
|
|
182
|
+
// an extra line per save, for a guarantee only needed once the session is
|
|
183
|
+
// about to be read from disk by someone else.
|
|
149
184
|
await jsonlHandler.append(filePath, newMessages, {
|
|
150
185
|
atomic: false,
|
|
151
186
|
});
|
|
152
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Set (or replace) a session's user-set title.
|
|
190
|
+
*
|
|
191
|
+
* The title is persisted as a `custom-title` entry appended to the session's
|
|
192
|
+
* own JSONL file — never as a rewrite of the metadata header, and never in a
|
|
193
|
+
* sidecar index — so every host that reads session files sees it without any
|
|
194
|
+
* extra sync channel.
|
|
195
|
+
*
|
|
196
|
+
* A session whose transcript has not been materialized yet (only meta messages
|
|
197
|
+
* so far) is created here: renaming is an explicit user action, so it is not
|
|
198
|
+
* subject to the "meta-only sessions never hit disk" rule.
|
|
199
|
+
*
|
|
200
|
+
* An empty/blank title is a no-op — it is not a signal to clear the title.
|
|
201
|
+
*
|
|
202
|
+
* @param sessionId - UUID session identifier
|
|
203
|
+
* @param workdir - Working directory the session belongs to
|
|
204
|
+
* @param title - New title
|
|
205
|
+
* @returns The trimmed title that was written, or undefined for a blank no-op
|
|
206
|
+
*/
|
|
207
|
+
export async function setSessionCustomTitle(sessionId, workdir, title) {
|
|
208
|
+
const trimmed = title.trim();
|
|
209
|
+
if (!trimmed) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const jsonlHandler = new JsonlHandler();
|
|
213
|
+
let filePath = await generateSessionFilePath(sessionId, workdir, "main");
|
|
214
|
+
try {
|
|
215
|
+
await fs.access(filePath);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// The session may live in another project directory (created from a
|
|
219
|
+
// different cwd, or a git worktree that no longer maps to this workdir).
|
|
220
|
+
const fallback = await findSessionFileAcrossProjects(sessionId, "main");
|
|
221
|
+
if (fallback) {
|
|
222
|
+
filePath = fallback;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
await createSession(sessionId, workdir, "main");
|
|
226
|
+
filePath = await generateSessionFilePath(sessionId, workdir, "main");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await jsonlHandler.appendCustomTitle(filePath, trimmed, sessionId);
|
|
230
|
+
return trimmed;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Re-append the session's reserved metadata entries at EOF.
|
|
234
|
+
*
|
|
235
|
+
* A listing reads only the tail window (64 KB) of each session file, so a
|
|
236
|
+
* `custom-title` entry that later messages pushed past that window becomes
|
|
237
|
+
* invisible — the session would flip back to showing its first message. This
|
|
238
|
+
* runs at the session's sparse flush points (spec `session-management.md`
|
|
239
|
+
* 验收场景 5): graceful exit, compaction, taking over a resumed session,
|
|
240
|
+
* switching away from one, and after `/rewind` rewrites the file. Claude Code
|
|
241
|
+
* solves the same problem the same way (`reAppendSessionMetadata`,
|
|
242
|
+
* `sessionStorage.ts:458/983/1533`, `compact.ts:711/1057`).
|
|
243
|
+
*
|
|
244
|
+
* The entry is refreshed from the tail first: another process (the CLI session
|
|
245
|
+
* picker, a second host) may have renamed the session while we held it, in
|
|
246
|
+
* which case its value — not our stale in-memory one — is the truth. When the
|
|
247
|
+
* tail holds nothing (already evicted, or never written by anyone else), the
|
|
248
|
+
* caller's in-memory value stands.
|
|
249
|
+
*
|
|
250
|
+
* A missing transcript file is a no-op: re-appending must never materialize a
|
|
251
|
+
* session that does not exist.
|
|
252
|
+
*
|
|
253
|
+
* @param sessionId - UUID session identifier
|
|
254
|
+
* @param workdir - Working directory the session belongs to
|
|
255
|
+
* @param customTitle - Title held in memory by the caller (may be undefined)
|
|
256
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
257
|
+
*/
|
|
258
|
+
export async function reAppendSessionMetadata(sessionId, workdir, customTitle, sessionType = "main") {
|
|
259
|
+
const jsonlHandler = new JsonlHandler();
|
|
260
|
+
let filePath = await generateSessionFilePath(sessionId, workdir, sessionType);
|
|
261
|
+
try {
|
|
262
|
+
await fs.access(filePath);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// The session may live in another project directory (created from a
|
|
266
|
+
// different cwd, or a git worktree). That sweep costs a directory walk per
|
|
267
|
+
// flush point, so it runs only when there is a title to write: with no
|
|
268
|
+
// in-memory title there is nothing this call could add that the entry
|
|
269
|
+
// already in the file does not say.
|
|
270
|
+
if (!customTitle) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const fallback = await findSessionFileAcrossProjects(sessionId, sessionType);
|
|
274
|
+
if (!fallback) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
filePath = fallback;
|
|
278
|
+
}
|
|
279
|
+
// Absorb a fresher value written by another process since we last looked.
|
|
280
|
+
const tailTitle = await jsonlHandler.readCustomTitle(filePath);
|
|
281
|
+
const title = tailTitle ?? customTitle;
|
|
282
|
+
if (!title) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await jsonlHandler.appendCustomTitle(filePath, title, sessionId);
|
|
286
|
+
}
|
|
153
287
|
/**
|
|
154
288
|
* Scan all project directories for a session file by ID. Used as a fallback
|
|
155
289
|
* when the session is not found in the given working directory (e.g. resuming
|
|
@@ -214,7 +348,7 @@ export async function loadSessionFromJsonl(sessionId, workdir, sessionType = "ma
|
|
|
214
348
|
throw error;
|
|
215
349
|
}
|
|
216
350
|
}
|
|
217
|
-
const allMessages = await jsonlHandler.
|
|
351
|
+
const { messages: allMessages, customTitle } = await jsonlHandler.readMessagesAndCustomTitle(resolvedPath);
|
|
218
352
|
// Transcripts written by older CLI versions re-appended the preserved
|
|
219
353
|
// rounds at each compaction point, so a message id can appear twice.
|
|
220
354
|
// Keep the first occurrence — duplicates would render twice once the
|
|
@@ -244,6 +378,10 @@ export async function loadSessionFromJsonl(sessionId, workdir, sessionType = "ma
|
|
|
244
378
|
// messages appended (SessionStart hooks), which would otherwise report
|
|
245
379
|
// 0 and blank the context-usage indicator for that conversation.
|
|
246
380
|
latestTotalTokens: extractLatestTotalTokens(messages),
|
|
381
|
+
// Recovered from the whole-file scan, not the tail window: a title the
|
|
382
|
+
// conversation has long outgrown is only reachable this way. The host
|
|
383
|
+
// holds it in memory so `reAppendSessionMetadata` puts it back at EOF.
|
|
384
|
+
customTitle,
|
|
247
385
|
},
|
|
248
386
|
};
|
|
249
387
|
return sessionData;
|
|
@@ -377,6 +515,8 @@ export async function listSessionsFromJsonl(workdir) {
|
|
|
377
515
|
catch {
|
|
378
516
|
// Ignore errors getting first message
|
|
379
517
|
}
|
|
518
|
+
// A user-set title, when present, wins over the first-message label.
|
|
519
|
+
sessionMeta.customTitle = await jsonlHandler.readCustomTitle(filePath);
|
|
380
520
|
sessions.push(sessionMeta);
|
|
381
521
|
}
|
|
382
522
|
catch {
|
|
@@ -553,6 +693,7 @@ export async function listAllSessions(options) {
|
|
|
553
693
|
// usage-less hook message, so the line alone would report 0.
|
|
554
694
|
latestTotalTokens: await jsonlHandler.getLatestTotalTokens(filePath),
|
|
555
695
|
firstMessage,
|
|
696
|
+
customTitle: await jsonlHandler.readCustomTitle(filePath),
|
|
556
697
|
branch: header?.gitBranch,
|
|
557
698
|
});
|
|
558
699
|
}
|
|
@@ -733,8 +874,8 @@ export async function getFirstMessageContentFromFile(filePath) {
|
|
|
733
874
|
for (const line of lines) {
|
|
734
875
|
try {
|
|
735
876
|
const message = JSON.parse(line);
|
|
736
|
-
// Skip
|
|
737
|
-
if (message.type
|
|
877
|
+
// Skip reserved entries (metadata header, custom-title) and meta messages
|
|
878
|
+
if (isReservedEntry(message.type) || message.isMeta) {
|
|
738
879
|
continue;
|
|
739
880
|
}
|
|
740
881
|
const content = getMessageContent(message);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved (non-message) entry types found in a session JSONL file.
|
|
3
|
+
*
|
|
4
|
+
* A session file is an append-only log of messages plus a few reserved
|
|
5
|
+
* bookkeeping entries: the `metadata` header written once at creation, and a
|
|
6
|
+
* `custom-title` entry appended on every rename. Every reader must skip all of
|
|
7
|
+
* them — a reserved entry parsed as a message yields `lastActiveAt: Invalid
|
|
8
|
+
* Date`, corrupts the token scan, or surfaces in the conversation.
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own dependency-free module because both the writer
|
|
11
|
+
* (`jsonlHandler.ts`) and the reader (`session.ts`) need it, and the session
|
|
12
|
+
* tests mock `jsonlHandler` wholesale.
|
|
13
|
+
*
|
|
14
|
+
* Add a new file-level entry type here when adding one.
|
|
15
|
+
*/
|
|
16
|
+
const RESERVED_ENTRY_TYPES = new Set(["metadata", "custom-title"]);
|
|
17
|
+
/** True when a parsed JSONL line is a reserved (non-message) entry. */
|
|
18
|
+
export function isReservedEntry(type) {
|
|
19
|
+
return type !== undefined && RESERVED_ENTRY_TYPES.has(type);
|
|
20
|
+
}
|
package/dist/stdio/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* app: a typed StdioAgent wrapper (mirrors the CLI Agent API over JSON-RPC)
|
|
4
4
|
* plus the sessionId-demultiplexing NotificationRouter.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Re-exported by the `wave-agent-sdk/host` entry — import it from there. It has
|
|
7
|
+
* no subpath of its own: the RPC layer is only reachable through `/host`, so a
|
|
8
|
+
* host cannot pick it up by accident while reaching for a shared literal.
|
|
7
9
|
*/
|
|
8
10
|
export * from "./rpcClient.js";
|
|
9
11
|
export * from "./notificationRouter.js";
|
package/dist/stdio/index.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* app: a typed StdioAgent wrapper (mirrors the CLI Agent API over JSON-RPC)
|
|
4
4
|
* plus the sessionId-demultiplexing NotificationRouter.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Re-exported by the `wave-agent-sdk/host` entry — import it from there. It has
|
|
7
|
+
* no subpath of its own: the RPC layer is only reachable through `/host`, so a
|
|
8
|
+
* host cannot pick it up by accident while reaching for a shared literal.
|
|
7
9
|
*/
|
|
8
10
|
export * from "./rpcClient.js";
|
|
9
11
|
export * from "./notificationRouter.js";
|
|
@@ -30,9 +30,6 @@ export interface InitializeParams {
|
|
|
30
30
|
defaultHeaders?: Record<string, string>;
|
|
31
31
|
model?: string;
|
|
32
32
|
fastModel?: string;
|
|
33
|
-
language?: string;
|
|
34
|
-
autoMemoryEnabled?: boolean;
|
|
35
|
-
autoMemoryFrequency?: number;
|
|
36
33
|
permissionMode?: PermissionMode;
|
|
37
34
|
tools?: string[];
|
|
38
35
|
allowedTools?: string[];
|
|
@@ -48,6 +45,12 @@ export interface InitializeResult {
|
|
|
48
45
|
permissionMode: PermissionMode;
|
|
49
46
|
latestTotalTokens: number;
|
|
50
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Session-level overrides only. 用户偏好(语言 / 上下文长度 / 自动记忆开关与频率)
|
|
50
|
+
* **不得**经本层下发——它们写用户级 `~/.wave/settings.json` 并由实时重载生效;
|
|
51
|
+
* 覆盖层优先级高于 settings.json,会把实时值永久遮蔽(docs/specs/core/agent-config.md
|
|
52
|
+
* 「分层职责」)。
|
|
53
|
+
*/
|
|
51
54
|
export interface UpdateConfigParams {
|
|
52
55
|
apiKey?: string;
|
|
53
56
|
baseURL?: string;
|
|
@@ -55,10 +58,6 @@ export interface UpdateConfigParams {
|
|
|
55
58
|
defaultHeaders?: Record<string, string>;
|
|
56
59
|
model?: string;
|
|
57
60
|
fastModel?: string;
|
|
58
|
-
language?: string;
|
|
59
|
-
contextLength?: number;
|
|
60
|
-
autoMemoryEnabled?: boolean;
|
|
61
|
-
autoMemoryFrequency?: number;
|
|
62
61
|
}
|
|
63
62
|
export interface StdioAgentCallbacks {
|
|
64
63
|
onUserMessageAdded?: (message: Message) => void;
|
|
@@ -78,6 +77,8 @@ export interface StdioAgentCallbacks {
|
|
|
78
77
|
onCompactBlockAdded?: (content: string) => void;
|
|
79
78
|
onCompactionStateChange?: (isCompacting: boolean) => void;
|
|
80
79
|
onCompactionContentUpdate?: (content: string) => void;
|
|
80
|
+
/** Full plan-file content after the model writes it (spec: 计划文件更新后刷新计划面板). */
|
|
81
|
+
onPlanFileUpdated?: (content: string) => void;
|
|
81
82
|
onLoadingChange?: (loading: boolean) => void;
|
|
82
83
|
onContextUsage?: (percent: number) => void;
|
|
83
84
|
onCommandRunningChange?: (running: boolean) => void;
|
|
@@ -134,6 +135,12 @@ export declare class StdioAgent {
|
|
|
134
135
|
initialize(params: InitializeParams): Promise<InitializeResult>;
|
|
135
136
|
destroy(): Promise<void>;
|
|
136
137
|
restoreSession(sessionId: string): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Set this session's custom title (spec session-management.md「会话自定义标题
|
|
140
|
+
* (重命名)」). The title is appended to the session's own JSONL file as a
|
|
141
|
+
* reserved `custom-title` entry, so every host reading that file sees it.
|
|
142
|
+
*/
|
|
143
|
+
renameSession(title: string): Promise<void>;
|
|
137
144
|
updateConfig(params: UpdateConfigParams): Promise<{
|
|
138
145
|
sessionId: string;
|
|
139
146
|
}>;
|
package/dist/stdio/stdioAgent.js
CHANGED
|
@@ -63,6 +63,20 @@ export class StdioAgent {
|
|
|
63
63
|
async restoreSession(sessionId) {
|
|
64
64
|
await this.client.request("restoreSession", { sessionId }, this.sessionId);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Set this session's custom title (spec session-management.md「会话自定义标题
|
|
68
|
+
* (重命名)」). The title is appended to the session's own JSONL file as a
|
|
69
|
+
* reserved `custom-title` entry, so every host reading that file sees it.
|
|
70
|
+
*/
|
|
71
|
+
async renameSession(title) {
|
|
72
|
+
await this.client.request("renameSession", {
|
|
73
|
+
sessionId: this.sessionId,
|
|
74
|
+
// The session file lives under the session root; a cwd drifted by `cd`
|
|
75
|
+
// inside a turn must not address it (see sessionCwd).
|
|
76
|
+
workdir: this.sessionCwd ?? this.workingDirectory,
|
|
77
|
+
title,
|
|
78
|
+
}, this.sessionId);
|
|
79
|
+
}
|
|
66
80
|
async updateConfig(params) {
|
|
67
81
|
const oldSessionId = this.sessionId;
|
|
68
82
|
const result = (await this.client.request("updateConfig", params, this.sessionId));
|
|
@@ -261,6 +275,11 @@ export class StdioAgent {
|
|
|
261
275
|
this.callbacks.onCompactionContentUpdate?.(p.content);
|
|
262
276
|
break;
|
|
263
277
|
}
|
|
278
|
+
case "planFileUpdated": {
|
|
279
|
+
const p = params;
|
|
280
|
+
this.callbacks.onPlanFileUpdated?.(p.content);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
264
283
|
case "loadingChange": {
|
|
265
284
|
const p = params;
|
|
266
285
|
if (p.latestTotalTokens !== undefined) {
|