wave-agent-sdk 1.1.5 → 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 +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- 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 +54 -10
- package/dist/core/plugin.js +137 -23
- 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 +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- 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 +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- 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/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- 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 +323 -102
- 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 +92 -9
- package/dist/services/configurationService.js +246 -64
- 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 +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -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/exitPlanMode.js +10 -2
- 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 +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- 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 +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- 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 +44 -0
- package/dist/utils/fileUtils.js +118 -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 -20
- package/dist/utils/messageOperations.js +40 -91
- 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 +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
package/dist/services/session.js
CHANGED
|
@@ -22,9 +22,10 @@ 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
|
-
import { getMessageContent
|
|
28
|
+
import { getMessageContent } from "../utils/messageOperations.js";
|
|
28
29
|
const execFileAsync = promisify(execFile);
|
|
29
30
|
/**
|
|
30
31
|
* Generate a new session ID using Node.js native crypto.randomUUID()
|
|
@@ -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,9 +348,21 @@ export async function loadSessionFromJsonl(sessionId, workdir, sessionType = "ma
|
|
|
214
348
|
throw error;
|
|
215
349
|
}
|
|
216
350
|
}
|
|
217
|
-
const allMessages = await jsonlHandler.
|
|
218
|
-
//
|
|
219
|
-
|
|
351
|
+
const { messages: allMessages, customTitle } = await jsonlHandler.readMessagesAndCustomTitle(resolvedPath);
|
|
352
|
+
// Transcripts written by older CLI versions re-appended the preserved
|
|
353
|
+
// rounds at each compaction point, so a message id can appear twice.
|
|
354
|
+
// Keep the first occurrence — duplicates would render twice once the
|
|
355
|
+
// full-history restore below feeds them to displayMessages. Append-only
|
|
356
|
+
// transcripts (current write side) have unique ids, making this a no-op.
|
|
357
|
+
const seenIds = new Set();
|
|
358
|
+
const messages = [];
|
|
359
|
+
for (const message of allMessages) {
|
|
360
|
+
if (message.id && seenIds.has(message.id))
|
|
361
|
+
continue;
|
|
362
|
+
if (message.id)
|
|
363
|
+
seenIds.add(message.id);
|
|
364
|
+
messages.push(message);
|
|
365
|
+
}
|
|
220
366
|
// Extract metadata from messages
|
|
221
367
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
222
368
|
const sessionData = {
|
|
@@ -227,9 +373,15 @@ export async function loadSessionFromJsonl(sessionId, workdir, sessionType = "ma
|
|
|
227
373
|
lastActiveAt: lastMessage
|
|
228
374
|
? lastMessage.timestamp
|
|
229
375
|
: new Date().toISOString(),
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
376
|
+
// Scan backwards for the last usage-bearing message instead of only
|
|
377
|
+
// testing the tail: a resumed conversation gets usage-less meta
|
|
378
|
+
// messages appended (SessionStart hooks), which would otherwise report
|
|
379
|
+
// 0 and blank the context-usage indicator for that conversation.
|
|
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,
|
|
233
385
|
},
|
|
234
386
|
};
|
|
235
387
|
return sessionData;
|
|
@@ -348,9 +500,9 @@ export async function listSessionsFromJsonl(workdir) {
|
|
|
348
500
|
? new Date(header.createdAt)
|
|
349
501
|
: new Date(),
|
|
350
502
|
lastActiveAt,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
503
|
+
// Scans the file tail: a resumed session's last line is a usage-less
|
|
504
|
+
// hook message, so the line alone would report 0 tokens here.
|
|
505
|
+
latestTotalTokens: await jsonlHandler.getLatestTotalTokens(filePath),
|
|
354
506
|
branch: header?.gitBranch,
|
|
355
507
|
};
|
|
356
508
|
// Try to get first message content for display
|
|
@@ -363,6 +515,8 @@ export async function listSessionsFromJsonl(workdir) {
|
|
|
363
515
|
catch {
|
|
364
516
|
// Ignore errors getting first message
|
|
365
517
|
}
|
|
518
|
+
// A user-set title, when present, wins over the first-message label.
|
|
519
|
+
sessionMeta.customTitle = await jsonlHandler.readCustomTitle(filePath);
|
|
366
520
|
sessions.push(sessionMeta);
|
|
367
521
|
}
|
|
368
522
|
catch {
|
|
@@ -535,10 +689,11 @@ export async function listAllSessions(options) {
|
|
|
535
689
|
? new Date(header.createdAt)
|
|
536
690
|
: new Date(),
|
|
537
691
|
lastActiveAt,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
692
|
+
// Scans the file tail: a resumed session's last line is a
|
|
693
|
+
// usage-less hook message, so the line alone would report 0.
|
|
694
|
+
latestTotalTokens: await jsonlHandler.getLatestTotalTokens(filePath),
|
|
541
695
|
firstMessage,
|
|
696
|
+
customTitle: await jsonlHandler.readCustomTitle(filePath),
|
|
542
697
|
branch: header?.gitBranch,
|
|
543
698
|
});
|
|
544
699
|
}
|
|
@@ -719,8 +874,8 @@ export async function getFirstMessageContentFromFile(filePath) {
|
|
|
719
874
|
for (const line of lines) {
|
|
720
875
|
try {
|
|
721
876
|
const message = JSON.parse(line);
|
|
722
|
-
// Skip
|
|
723
|
-
if (message.type
|
|
877
|
+
// Skip reserved entries (metadata header, custom-title) and meta messages
|
|
878
|
+
if (isReservedEntry(message.type) || message.isMeta) {
|
|
724
879
|
continue;
|
|
725
880
|
}
|
|
726
881
|
const content = getMessageContent(message);
|
|
@@ -828,7 +983,10 @@ export async function handleSessionRestoration(restoreSessionId, continueLastSes
|
|
|
828
983
|
}
|
|
829
984
|
}
|
|
830
985
|
if (sessionToRestore) {
|
|
831
|
-
|
|
986
|
+
// Diagnostics only — never stdout: `wave --stdio`'s stdout carries the
|
|
987
|
+
// JSON-RPC channel, and a stray line there makes the host log a parse
|
|
988
|
+
// failure for every restore (the logger writes to the log file).
|
|
989
|
+
logger.info(`Restoring session: ${sessionToRestore.id}`);
|
|
832
990
|
// // Initialize from session data
|
|
833
991
|
// this.initializeFromSession();
|
|
834
992
|
return sessionToRestore;
|
|
@@ -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
|
+
}
|
|
@@ -58,7 +58,12 @@ export async function executeWorktreeRemoveHook(worktreePath, configuration, con
|
|
|
58
58
|
const results = await runWorktreeHooks("WorktreeRemove", hookConfigs, context, { worktreePath });
|
|
59
59
|
for (const result of results) {
|
|
60
60
|
if (!result.success) {
|
|
61
|
-
|
|
61
|
+
// The path identifies which worktree survived; exit/timeout/duration tell
|
|
62
|
+
// apart "the hook failed on its own" from "the hook was killed by the
|
|
63
|
+
// hook timeout" when failures are aggregated later.
|
|
64
|
+
logger?.error(`WorktreeRemove hook failed [${result.command}] worktree=${worktreePath} ` +
|
|
65
|
+
`exit=${result.exitCode ?? "n/a"} timedOut=${result.timedOut} ` +
|
|
66
|
+
`durationMs=${result.duration}: ${result.stderr || "no output"}`);
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
return true;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side stdio RPC layer shared by the VS Code extension and the desktop
|
|
3
|
+
* app: a typed StdioAgent wrapper (mirrors the CLI Agent API over JSON-RPC)
|
|
4
|
+
* plus the sessionId-demultiplexing NotificationRouter.
|
|
5
|
+
*
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./rpcClient.js";
|
|
11
|
+
export * from "./notificationRouter.js";
|
|
12
|
+
export * from "./stdioAgent.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side stdio RPC layer shared by the VS Code extension and the desktop
|
|
3
|
+
* app: a typed StdioAgent wrapper (mirrors the CLI Agent API over JSON-RPC)
|
|
4
|
+
* plus the sessionId-demultiplexing NotificationRouter.
|
|
5
|
+
*
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./rpcClient.js";
|
|
11
|
+
export * from "./notificationRouter.js";
|
|
12
|
+
export * from "./stdioAgent.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationRouter — demultiplexes server→client notifications by sessionId.
|
|
3
|
+
*
|
|
4
|
+
* In the single-shared-process architecture, all sessions share one StdioClient.
|
|
5
|
+
* The server tags each session-scoped notification with `sessionId` on the
|
|
6
|
+
* JSON-RPC envelope. The router inspects that field and dispatches to the
|
|
7
|
+
* matching StdioAgent; notifications without sessionId (e.g. `authUrl`) go to
|
|
8
|
+
* a global handler.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle:
|
|
11
|
+
* 1. `attach()` once — subscribes to every notification method on the shared client
|
|
12
|
+
* 2. Per session: `register(sessionId, agent)` after `initialize` response
|
|
13
|
+
* 3. On `sessionIdChange`: router auto-rekeys the agent (old → new)
|
|
14
|
+
* 4. On `destroy`: `unregister(sessionId)` removes the agent
|
|
15
|
+
*/
|
|
16
|
+
import type { RpcClient } from "./rpcClient.js";
|
|
17
|
+
type GlobalHandler = (params: unknown) => void;
|
|
18
|
+
import type { StdioAgent } from "./stdioAgent.js";
|
|
19
|
+
export declare class NotificationRouter {
|
|
20
|
+
private client;
|
|
21
|
+
private sessions;
|
|
22
|
+
private globalHandlers;
|
|
23
|
+
private attached;
|
|
24
|
+
constructor(client: RpcClient);
|
|
25
|
+
/**
|
|
26
|
+
* Subscribe to all notification methods on the shared client.
|
|
27
|
+
* Must be called once after construction. Idempotent.
|
|
28
|
+
*/
|
|
29
|
+
attach(): void;
|
|
30
|
+
/** Register a session-scoped agent. Call after `initialize` response. */
|
|
31
|
+
register(sessionId: string, agent: StdioAgent): void;
|
|
32
|
+
/** Unregister a session. Call on destroy. */
|
|
33
|
+
unregister(sessionId: string): void;
|
|
34
|
+
/** Register a global (non-session-scoped) notification handler. */
|
|
35
|
+
registerGlobal(method: string, handler: GlobalHandler): void;
|
|
36
|
+
private dispatch;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationRouter — demultiplexes server→client notifications by sessionId.
|
|
3
|
+
*
|
|
4
|
+
* In the single-shared-process architecture, all sessions share one StdioClient.
|
|
5
|
+
* The server tags each session-scoped notification with `sessionId` on the
|
|
6
|
+
* JSON-RPC envelope. The router inspects that field and dispatches to the
|
|
7
|
+
* matching StdioAgent; notifications without sessionId (e.g. `authUrl`) go to
|
|
8
|
+
* a global handler.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle:
|
|
11
|
+
* 1. `attach()` once — subscribes to every notification method on the shared client
|
|
12
|
+
* 2. Per session: `register(sessionId, agent)` after `initialize` response
|
|
13
|
+
* 3. On `sessionIdChange`: router auto-rekeys the agent (old → new)
|
|
14
|
+
* 4. On `destroy`: `unregister(sessionId)` removes the agent
|
|
15
|
+
*/
|
|
16
|
+
const ALL_NOTIFICATION_METHODS = [
|
|
17
|
+
"userMessageAdded",
|
|
18
|
+
"assistantMessageAdded",
|
|
19
|
+
"assistantContentUpdated",
|
|
20
|
+
"assistantReasoningUpdated",
|
|
21
|
+
"toolBlockUpdated",
|
|
22
|
+
"errorBlockAdded",
|
|
23
|
+
"loadingChange",
|
|
24
|
+
"contextUsage",
|
|
25
|
+
"commandRunningChange",
|
|
26
|
+
"queuedMessagesChange",
|
|
27
|
+
"tasksChange",
|
|
28
|
+
"sessionIdChange",
|
|
29
|
+
"permissionModeChange",
|
|
30
|
+
"mcpServersChange",
|
|
31
|
+
"workdirChange",
|
|
32
|
+
"notificationMessageAdded",
|
|
33
|
+
"permissionRequest",
|
|
34
|
+
"authUrl",
|
|
35
|
+
"btwContent",
|
|
36
|
+
"compactBlockAdded",
|
|
37
|
+
"compactionStateChange",
|
|
38
|
+
"compactionContentUpdate",
|
|
39
|
+
"planFileUpdated",
|
|
40
|
+
"backgroundTasksChange",
|
|
41
|
+
];
|
|
42
|
+
export class NotificationRouter {
|
|
43
|
+
constructor(client) {
|
|
44
|
+
this.client = client;
|
|
45
|
+
this.sessions = new Map();
|
|
46
|
+
this.globalHandlers = new Map();
|
|
47
|
+
this.attached = false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Subscribe to all notification methods on the shared client.
|
|
51
|
+
* Must be called once after construction. Idempotent.
|
|
52
|
+
*/
|
|
53
|
+
attach() {
|
|
54
|
+
if (this.attached)
|
|
55
|
+
return;
|
|
56
|
+
this.attached = true;
|
|
57
|
+
for (const method of ALL_NOTIFICATION_METHODS) {
|
|
58
|
+
this.client.onNotification(method, (params, sessionId) => {
|
|
59
|
+
this.dispatch(method, params, sessionId);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Register a session-scoped agent. Call after `initialize` response. */
|
|
64
|
+
register(sessionId, agent) {
|
|
65
|
+
this.sessions.set(sessionId, agent);
|
|
66
|
+
}
|
|
67
|
+
/** Unregister a session. Call on destroy. */
|
|
68
|
+
unregister(sessionId) {
|
|
69
|
+
this.sessions.delete(sessionId);
|
|
70
|
+
}
|
|
71
|
+
/** Register a global (non-session-scoped) notification handler. */
|
|
72
|
+
registerGlobal(method, handler) {
|
|
73
|
+
this.globalHandlers.set(method, handler);
|
|
74
|
+
}
|
|
75
|
+
dispatch(method, params, sessionId) {
|
|
76
|
+
if (sessionId) {
|
|
77
|
+
const agent = this.sessions.get(sessionId);
|
|
78
|
+
if (!agent)
|
|
79
|
+
return; // early notification before register — drop (v1)
|
|
80
|
+
// sessionIdChange requires rekeying the agent in the map
|
|
81
|
+
if (method === "sessionIdChange") {
|
|
82
|
+
const newId = params.sessionId;
|
|
83
|
+
if (newId && newId !== sessionId) {
|
|
84
|
+
this.sessions.delete(sessionId);
|
|
85
|
+
this.sessions.set(newId, agent);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
agent.handleNotification(method, params);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Global notification (e.g. authUrl)
|
|
92
|
+
const handler = this.globalHandlers.get(method);
|
|
93
|
+
if (handler)
|
|
94
|
+
handler(params);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RpcClient — the structural transport surface StdioAgent + NotificationRouter
|
|
3
|
+
* depend on.
|
|
4
|
+
*
|
|
5
|
+
* Both hosts keep their own transports (vscode's `StdioClient`; desktop's
|
|
6
|
+
* `JsonRpcClient` subclasses `StdioClient` / `SocketClient`); each satisfies
|
|
7
|
+
* this interface structurally (TypeScript), so the shared stdio layer never
|
|
8
|
+
* needs to know which transport backs it.
|
|
9
|
+
*/
|
|
10
|
+
export type NotificationHandler = (params: unknown, sessionId?: string) => void;
|
|
11
|
+
export interface RpcClient {
|
|
12
|
+
/** Send a request; resolves with the JSON-RPC result. */
|
|
13
|
+
request(method: string, params?: unknown, sessionId?: string): Promise<unknown>;
|
|
14
|
+
/** Fire-and-forget JSON-RPC notification (no response expected). */
|
|
15
|
+
notify(method: string, params?: unknown, sessionId?: string): void;
|
|
16
|
+
/** Subscribe to a server→client notification method. */
|
|
17
|
+
onNotification(method: string, handler: NotificationHandler): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RpcClient — the structural transport surface StdioAgent + NotificationRouter
|
|
3
|
+
* depend on.
|
|
4
|
+
*
|
|
5
|
+
* Both hosts keep their own transports (vscode's `StdioClient`; desktop's
|
|
6
|
+
* `JsonRpcClient` subclasses `StdioClient` / `SocketClient`); each satisfies
|
|
7
|
+
* this interface structurally (TypeScript), so the shared stdio layer never
|
|
8
|
+
* needs to know which transport backs it.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|