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.
Files changed (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. 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(path);
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.path);
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
- const normalizedWatchedPath = watchedPath.replace(/\\/g, "/");
214
- if (normalizedFilePath === normalizedWatchedPath ||
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.permissionMode) {
88
- permissionManager.updateConfiguredPermissionMode(configResult.configuration.permissions.permissionMode);
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): Promise<void>;
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, messageManager.getWorkdir());
113
+ const sessionData = await loadSessionFromJsonl(sessionId, targetWorkdir);
105
114
  if (!sessionData) {
106
115
  throw new Error(`Session not found: ${sessionId}`);
107
116
  }
108
- // 5. Clean current state
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
- // 6. Rebuild usage (in correct order)
126
+ // 7. Rebuild usage (in correct order)
112
127
  messageManager.rebuildUsageFromMessages(sessionData.messages);
113
- // 7. Initialize session state last
128
+ // 8. Initialize session state last
114
129
  messageManager.initializeFromSession(sessionData);
115
- // 8. Run SessionStart hooks for the restored session and inject additional
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
- // 9. Load tasks for the restored session
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
  */