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
|
@@ -5,7 +5,59 @@
|
|
|
5
5
|
* Handles file watching with debouncing, error recovery, and graceful fallbacks.
|
|
6
6
|
*/
|
|
7
7
|
import * as chokidar from "chokidar";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
8
10
|
import { EventEmitter } from "events";
|
|
11
|
+
/**
|
|
12
|
+
* Expand a watch path to its canonical long form (Windows only).
|
|
13
|
+
*
|
|
14
|
+
* libuv's fs-event backend resolves each event path with `GetLongPathNameW()`
|
|
15
|
+
* and then asserts the result is prefixed by the watched directory string
|
|
16
|
+
* (`uv__relative_path()`, src\win\fs-event.c). libuv <= 1.51 expanded the
|
|
17
|
+
* watched directory itself; 1.52 dropped that step (it ships with Node 24.16+,
|
|
18
|
+
* 26.x, and Electron 43 — which bundles Node 24.18), so `handle->dirw`
|
|
19
|
+
* keeps whatever the caller passed, and watching an 8.3 short path
|
|
20
|
+
* (`C:\Users\LIUYIQ~1\...` — what %TEMP% yields when it is configured with a
|
|
21
|
+
* short name) aborts the whole process on the first event. The abort is not a
|
|
22
|
+
* catchable error, so the root must be canonicalized before it reaches
|
|
23
|
+
* chokidar. `fs.realpathSync()` alone is not enough: only the `.native()`
|
|
24
|
+
* variant expands short names.
|
|
25
|
+
*
|
|
26
|
+
* Paths that don't exist yet (`~/.wave/settings.json` on a fresh install) are
|
|
27
|
+
* handled by expanding the nearest existing ancestor and appending the
|
|
28
|
+
* remaining segments back; if nothing can be resolved the input is returned
|
|
29
|
+
* unchanged.
|
|
30
|
+
*/
|
|
31
|
+
function toLongFormPath(target) {
|
|
32
|
+
if (process.platform !== "win32")
|
|
33
|
+
return target;
|
|
34
|
+
// Only drive-qualified (`C:\...`) and UNC (`\\server\share`) paths are real
|
|
35
|
+
// Windows paths. POSIX-style input (what tests and other platforms use) is
|
|
36
|
+
// passed through untouched rather than re-rooted onto the current drive.
|
|
37
|
+
if (!/^[a-zA-Z]:[\\/]/.test(target) && !target.startsWith("\\\\")) {
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
40
|
+
let candidate = target;
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (;;) {
|
|
43
|
+
try {
|
|
44
|
+
const resolved = fs.realpathSync.native(candidate);
|
|
45
|
+
return missing.length > 0 ? path.join(resolved, ...missing) : resolved;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
const parent = path.dirname(candidate);
|
|
49
|
+
if (parent === candidate)
|
|
50
|
+
return target;
|
|
51
|
+
missing.unshift(path.basename(candidate));
|
|
52
|
+
candidate = parent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Whether `filePath` (slash-normalized) is the watched root or inside it. */
|
|
57
|
+
function isWithin(filePath, root) {
|
|
58
|
+
const normalizedRoot = root.replace(/\\/g, "/");
|
|
59
|
+
return (filePath === normalizedRoot || filePath.startsWith(normalizedRoot + "/"));
|
|
60
|
+
}
|
|
9
61
|
export class FileWatcherService extends EventEmitter {
|
|
10
62
|
constructor(logger, config) {
|
|
11
63
|
super();
|
|
@@ -38,6 +90,7 @@ export class FileWatcherService extends EventEmitter {
|
|
|
38
90
|
// Create new watcher entry
|
|
39
91
|
const entry = {
|
|
40
92
|
path,
|
|
93
|
+
watchPath: toLongFormPath(path),
|
|
41
94
|
watcher: null,
|
|
42
95
|
isActive: false,
|
|
43
96
|
lastEvent: Date.now(),
|
|
@@ -64,7 +117,7 @@ export class FileWatcherService extends EventEmitter {
|
|
|
64
117
|
return;
|
|
65
118
|
try {
|
|
66
119
|
if (entry.watcher) {
|
|
67
|
-
entry.watcher.unwatch(
|
|
120
|
+
entry.watcher.unwatch(entry.watchPath);
|
|
68
121
|
}
|
|
69
122
|
this.watchers.delete(path);
|
|
70
123
|
}
|
|
@@ -148,7 +201,7 @@ export class FileWatcherService extends EventEmitter {
|
|
|
148
201
|
this.setupGlobalWatcherEvents();
|
|
149
202
|
}
|
|
150
203
|
// Add path to global watcher
|
|
151
|
-
this.globalWatcher.add(entry.
|
|
204
|
+
this.globalWatcher.add(entry.watchPath);
|
|
152
205
|
entry.watcher = this.globalWatcher;
|
|
153
206
|
entry.isActive = true;
|
|
154
207
|
entry.errorCount = 0;
|
|
@@ -207,12 +260,14 @@ export class FileWatcherService extends EventEmitter {
|
|
|
207
260
|
timestamp: Date.now(),
|
|
208
261
|
size: stats?.size,
|
|
209
262
|
};
|
|
210
|
-
// Notify all watchers that match the path or are parents of the path
|
|
263
|
+
// Notify all watchers that match the path or are parents of the path.
|
|
264
|
+
// Events arrive under whichever form chokidar was given (the long form on
|
|
265
|
+
// Windows), while entries are keyed by the caller's original path, so both
|
|
266
|
+
// have to be considered.
|
|
211
267
|
for (const [watchedPath, entry] of this.watchers.entries()) {
|
|
212
268
|
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
normalizedFilePath.startsWith(normalizedWatchedPath + "/")) {
|
|
269
|
+
if (isWithin(normalizedFilePath, watchedPath) ||
|
|
270
|
+
isWithin(normalizedFilePath, entry.watchPath)) {
|
|
216
271
|
entry.lastEvent = event.timestamp;
|
|
217
272
|
// Notify all callbacks for this watcher
|
|
218
273
|
for (const callback of entry.callbacks) {
|
|
@@ -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();
|
|
@@ -84,8 +88,8 @@ export class InitializationService {
|
|
|
84
88
|
if (configResult.configuration.permissions.deny) {
|
|
85
89
|
permissionManager.updateDeniedRules(configResult.configuration.permissions.deny);
|
|
86
90
|
}
|
|
87
|
-
if (configResult.configuration.permissions.
|
|
88
|
-
permissionManager.updateConfiguredPermissionMode(configResult.configuration.permissions.
|
|
91
|
+
if (configResult.configuration.permissions.defaultMode) {
|
|
92
|
+
permissionManager.updateConfiguredPermissionMode(configResult.configuration.permissions.defaultMode);
|
|
89
93
|
}
|
|
90
94
|
if (configResult.configuration.permissions.additionalDirectories) {
|
|
91
95
|
permissionManager.updateAdditionalDirectories(configResult.configuration.permissions.additionalDirectories);
|
|
@@ -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,10 +95,42 @@ 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
|
*/
|
|
67
119
|
getLastMessage(filePath: string): Promise<Message | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Latest context-usage total for a session file.
|
|
122
|
+
*
|
|
123
|
+
* Sessions accumulate usage-less messages at the end — SessionStart hook
|
|
124
|
+
* meta messages are appended (and persisted) on every resume — so the last
|
|
125
|
+
* line alone cannot answer "how much context did this conversation use".
|
|
126
|
+
* Scan backwards over the file's tail window and take the newest message
|
|
127
|
+
* that carries usage.
|
|
128
|
+
*
|
|
129
|
+
* @param filePath - Path to the session JSONL file
|
|
130
|
+
* @returns total_tokens of the newest usage-bearing message, or 0 when the
|
|
131
|
+
* tail holds none (empty/unreadable file, no completed request yet)
|
|
132
|
+
*/
|
|
133
|
+
getLatestTotalTokens(filePath: string): Promise<number>;
|
|
68
134
|
/**
|
|
69
135
|
* Read the creation-time metadata from the session file's header line.
|
|
70
136
|
*
|
|
@@ -75,6 +141,38 @@ export declare class JsonlHandler {
|
|
|
75
141
|
* @returns The persisted metadata, or null when the file has no header
|
|
76
142
|
*/
|
|
77
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>;
|
|
78
176
|
/**
|
|
79
177
|
* Validate messages before writing
|
|
80
178
|
*/
|