wave-agent-sdk 1.0.5 → 1.0.7

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 (202) hide show
  1. package/dist/agent.d.ts +34 -1
  2. package/dist/agent.js +49 -4
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +1 -1
  5. package/dist/managers/aiManager.d.ts +36 -0
  6. package/dist/managers/aiManager.js +240 -11
  7. package/dist/managers/messageManager.d.ts +2 -4
  8. package/dist/managers/messageManager.js +14 -7
  9. package/dist/managers/slashCommandManager.js +28 -0
  10. package/dist/managers/subagentManager.d.ts +0 -2
  11. package/dist/services/aiService.d.ts +2 -3
  12. package/dist/services/aiService.js +4 -18
  13. package/dist/services/configurationService.d.ts +7 -0
  14. package/dist/services/configurationService.js +10 -0
  15. package/dist/telemetry/instrumentation.d.ts +1 -1
  16. package/dist/telemetry/instrumentation.js +57 -28
  17. package/dist/tools/bashTool.js +16 -6
  18. package/dist/tools/grepTool.js +11 -3
  19. package/dist/types/core.d.ts +1 -34
  20. package/dist/types/index.d.ts +4 -0
  21. package/dist/types/messaging.d.ts +2 -0
  22. package/dist/utils/cacheControlUtils.d.ts +5 -27
  23. package/dist/utils/cacheControlUtils.js +12 -73
  24. package/dist/utils/containerSetup.js +0 -3
  25. package/dist/utils/messageOperations.d.ts +2 -1
  26. package/dist/utils/messageOperations.js +2 -1
  27. package/dist/utils/notificationXml.js +5 -0
  28. package/dist/utils/ripgrep.d.ts +4 -4
  29. package/dist/utils/ripgrep.js +4 -27
  30. package/package.json +20 -8
  31. package/bin/rg +0 -79
  32. package/dist/managers/forkedAgentManager.d.ts +0 -50
  33. package/dist/managers/forkedAgentManager.js +0 -130
  34. package/scripts/install_ripgrep.js +0 -111
  35. package/scripts/postinstall.js +0 -38
  36. package/src/agent.ts +0 -1271
  37. package/src/constants/subagents.ts +0 -4
  38. package/src/constants/toolLimits.ts +0 -15
  39. package/src/constants/tools.ts +0 -24
  40. package/src/core/plugin.ts +0 -237
  41. package/src/core/session.ts +0 -9
  42. package/src/index.ts +0 -38
  43. package/src/managers/MemoryRuleManager.ts +0 -198
  44. package/src/managers/aiManager.ts +0 -2429
  45. package/src/managers/backgroundTaskManager.ts +0 -480
  46. package/src/managers/bangManager.ts +0 -119
  47. package/src/managers/cronManager.ts +0 -401
  48. package/src/managers/foregroundTaskManager.ts +0 -30
  49. package/src/managers/forkedAgentManager.ts +0 -216
  50. package/src/managers/hookManager.ts +0 -1089
  51. package/src/managers/liveConfigManager.ts +0 -445
  52. package/src/managers/lspManager.ts +0 -470
  53. package/src/managers/mcpManager.ts +0 -1010
  54. package/src/managers/messageManager.ts +0 -1184
  55. package/src/managers/messageQueue.ts +0 -157
  56. package/src/managers/permissionManager.ts +0 -1140
  57. package/src/managers/planManager.ts +0 -112
  58. package/src/managers/pluginManager.ts +0 -330
  59. package/src/managers/pluginScopeManager.ts +0 -124
  60. package/src/managers/reversionManager.ts +0 -178
  61. package/src/managers/skillManager.ts +0 -578
  62. package/src/managers/slashCommandManager.ts +0 -564
  63. package/src/managers/subagentManager.ts +0 -915
  64. package/src/managers/toolManager.ts +0 -489
  65. package/src/managers/workflowManager.ts +0 -493
  66. package/src/prompts/autoMemory.ts +0 -33
  67. package/src/prompts/autoMemoryExtraction.ts +0 -146
  68. package/src/prompts/index.ts +0 -506
  69. package/src/prompts/planModeReminders.ts +0 -126
  70. package/src/services/GitService.ts +0 -131
  71. package/src/services/MarketplaceService.ts +0 -1052
  72. package/src/services/MemoryRuleService.ts +0 -71
  73. package/src/services/aiService.ts +0 -919
  74. package/src/services/authService.ts +0 -565
  75. package/src/services/autoMemoryService.ts +0 -280
  76. package/src/services/configurationService.ts +0 -1520
  77. package/src/services/fileWatcher.ts +0 -319
  78. package/src/services/hook.ts +0 -348
  79. package/src/services/initializationService.ts +0 -357
  80. package/src/services/interactionService.ts +0 -229
  81. package/src/services/jsonlHandler.ts +0 -295
  82. package/src/services/memory.ts +0 -234
  83. package/src/services/pluginLoader.ts +0 -252
  84. package/src/services/remoteSettingsService.ts +0 -366
  85. package/src/services/reversionService.ts +0 -122
  86. package/src/services/session.ts +0 -917
  87. package/src/services/taskManager.ts +0 -321
  88. package/src/telemetry/events.ts +0 -65
  89. package/src/telemetry/instrumentation.ts +0 -499
  90. package/src/telemetry/sessionTracing.ts +0 -348
  91. package/src/tools/agentTool.ts +0 -322
  92. package/src/tools/askUserQuestion.ts +0 -160
  93. package/src/tools/bashTool.ts +0 -621
  94. package/src/tools/buildTool.ts +0 -61
  95. package/src/tools/cronCreateTool.ts +0 -161
  96. package/src/tools/cronDeleteTool.ts +0 -51
  97. package/src/tools/cronListTool.ts +0 -47
  98. package/src/tools/editTool.ts +0 -313
  99. package/src/tools/enterPlanMode.ts +0 -124
  100. package/src/tools/enterWorktreeTool.ts +0 -190
  101. package/src/tools/exitPlanMode.ts +0 -139
  102. package/src/tools/exitWorktreeTool.ts +0 -236
  103. package/src/tools/globTool.ts +0 -167
  104. package/src/tools/grepTool.ts +0 -399
  105. package/src/tools/lspTool.ts +0 -883
  106. package/src/tools/readTool.ts +0 -426
  107. package/src/tools/skillTool.ts +0 -248
  108. package/src/tools/taskManagementTools.ts +0 -574
  109. package/src/tools/taskStopTool.ts +0 -78
  110. package/src/tools/types.ts +0 -137
  111. package/src/tools/webFetchTool.ts +0 -373
  112. package/src/tools/workflowTool.ts +0 -205
  113. package/src/tools/writeTool.ts +0 -267
  114. package/src/types/agent.ts +0 -128
  115. package/src/types/auth.ts +0 -19
  116. package/src/types/commands.ts +0 -30
  117. package/src/types/config.ts +0 -43
  118. package/src/types/configuration.ts +0 -168
  119. package/src/types/core.ts +0 -105
  120. package/src/types/cron.ts +0 -13
  121. package/src/types/environment.ts +0 -106
  122. package/src/types/fileSearch.ts +0 -4
  123. package/src/types/history.ts +0 -7
  124. package/src/types/hooks.ts +0 -256
  125. package/src/types/index.ts +0 -41
  126. package/src/types/lsp.ts +0 -98
  127. package/src/types/marketplace.ts +0 -71
  128. package/src/types/mcp.ts +0 -43
  129. package/src/types/memoryRule.ts +0 -31
  130. package/src/types/messaging.ts +0 -114
  131. package/src/types/permissions.ts +0 -80
  132. package/src/types/plugins.ts +0 -39
  133. package/src/types/processes.ts +0 -99
  134. package/src/types/reversion.ts +0 -29
  135. package/src/types/session.ts +0 -12
  136. package/src/types/skills.ts +0 -108
  137. package/src/types/tasks.ts +0 -13
  138. package/src/types/telemetry.ts +0 -98
  139. package/src/types/tools.ts +0 -44
  140. package/src/types/workflow.ts +0 -6
  141. package/src/utils/abortUtils.ts +0 -118
  142. package/src/utils/atomicWrite.ts +0 -61
  143. package/src/utils/bashParser.ts +0 -899
  144. package/src/utils/cacheControlUtils.ts +0 -426
  145. package/src/utils/commandArgumentParser.ts +0 -161
  146. package/src/utils/commandPathResolver.ts +0 -88
  147. package/src/utils/configPaths.ts +0 -220
  148. package/src/utils/configValidator.ts +0 -100
  149. package/src/utils/constants.ts +0 -39
  150. package/src/utils/container.ts +0 -92
  151. package/src/utils/containerSetup.ts +0 -371
  152. package/src/utils/convertMessagesForAPI.ts +0 -326
  153. package/src/utils/cronTasks.ts +0 -128
  154. package/src/utils/cronTasksLock.ts +0 -182
  155. package/src/utils/cronToHuman.ts +0 -99
  156. package/src/utils/customCommands.ts +0 -95
  157. package/src/utils/editUtils.ts +0 -20
  158. package/src/utils/fileFormat.ts +0 -40
  159. package/src/utils/fileSearch.ts +0 -151
  160. package/src/utils/fileUtils.ts +0 -223
  161. package/src/utils/gitUtils.ts +0 -315
  162. package/src/utils/globalLogger.ts +0 -128
  163. package/src/utils/groupMessagesByApiRound.ts +0 -121
  164. package/src/utils/hookMatcher.ts +0 -176
  165. package/src/utils/markdownParser.ts +0 -317
  166. package/src/utils/mcpUtils.ts +0 -138
  167. package/src/utils/messageOperations.ts +0 -659
  168. package/src/utils/modelCapabilities.ts +0 -30
  169. package/src/utils/nameGenerator.ts +0 -99
  170. package/src/utils/notificationXml.ts +0 -57
  171. package/src/utils/openaiClient.ts +0 -251
  172. package/src/utils/parseCronExpression.ts +0 -78
  173. package/src/utils/path.ts +0 -72
  174. package/src/utils/pathEncoder.ts +0 -400
  175. package/src/utils/pathSafety.ts +0 -40
  176. package/src/utils/promptHistory.ts +0 -168
  177. package/src/utils/ripgrep.ts +0 -35
  178. package/src/utils/shellResolver.ts +0 -190
  179. package/src/utils/skillParser.ts +0 -246
  180. package/src/utils/stringUtils.ts +0 -156
  181. package/src/utils/subagentParser.ts +0 -279
  182. package/src/utils/taskReminder.ts +0 -96
  183. package/src/utils/tokenCalculation.ts +0 -43
  184. package/src/utils/tokenEstimate.ts +0 -34
  185. package/src/utils/toolResultStorage.ts +0 -117
  186. package/src/utils/worktreeSession.ts +0 -26
  187. package/src/utils/worktreeUtils.ts +0 -786
  188. package/src/workflow/budgetTracker.ts +0 -34
  189. package/src/workflow/concurrencyLimiter.ts +0 -47
  190. package/src/workflow/journal.ts +0 -95
  191. package/src/workflow/progressReporter.ts +0 -141
  192. package/src/workflow/runState.ts +0 -65
  193. package/src/workflow/scriptRuntime.ts +0 -274
  194. package/src/workflow/structuredOutput.ts +0 -123
  195. package/src/workflow/types.ts +0 -101
  196. package/src/workflow/workflowApis.ts +0 -410
  197. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  198. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  199. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  200. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  201. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  202. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -1,917 +0,0 @@
1
- /**
2
- * Session Management Service - JSONL Format Implementation
3
- *
4
- * OPTIMIZED IMPLEMENTATION (Phase 6 Complete):
5
- * - Filename-based session type identification
6
- * - Minimal file I/O for metadata extraction
7
- * - Eliminated metadata headers for cleaner session files
8
- * - Backward compatible with existing session files
9
- * - 8-10x performance improvement in session listing operations
10
- *
11
- * Key Features:
12
- * - Session creation without metadata headers
13
- * - Subagent sessions identified by filename prefix
14
- * - Performance-optimized session listing
15
- * - Full backward compatibility maintained
16
- */
17
-
18
- import { promises as fs } from "fs";
19
- import { join } from "path";
20
- import { homedir } from "os";
21
- import { randomUUID } from "crypto";
22
- import type { Message } from "../types/index.js";
23
- import { PathEncoder } from "../utils/pathEncoder.js";
24
- import { JsonlHandler } from "../services/jsonlHandler.js";
25
- import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
26
- import { logger } from "../utils/globalLogger.js";
27
- import {
28
- getMessageContent,
29
- sliceFromLastCompact,
30
- } from "../utils/messageOperations.js";
31
-
32
- export interface SessionData {
33
- id: string;
34
- messages: Message[];
35
- metadata: {
36
- workdir: string;
37
- lastActiveAt: string;
38
- latestTotalTokens: number;
39
- };
40
- }
41
-
42
- export interface SessionMetadata {
43
- id: string;
44
- sessionType: "main" | "subagent";
45
- subagentType?: string;
46
- workdir: string;
47
- createdAt: Date;
48
- lastActiveAt: Date;
49
- latestTotalTokens: number;
50
- firstMessage?: string;
51
- }
52
-
53
- /**
54
- * Generate a new session ID using Node.js native crypto.randomUUID()
55
- * @returns UUID string for session identification
56
- */
57
- export function generateSessionId(): string {
58
- return randomUUID();
59
- }
60
-
61
- /**
62
- * Generate filename for subagent sessions
63
- * @param sessionId - UUID session identifier
64
- * @returns Filename with subagent prefix for subagent sessions
65
- */
66
- export function generateSubagentFilename(sessionId: string): string {
67
- return `subagent-${sessionId}.jsonl`;
68
- }
69
-
70
- // Constants
71
- export const SESSION_DIR = join(homedir(), ".wave", "projects");
72
- const MAX_SESSION_AGE_DAYS = 14;
73
-
74
- /**
75
- * Ensure session directory exists
76
- */
77
- export async function ensureSessionDir(): Promise<void> {
78
- try {
79
- await fs.mkdir(SESSION_DIR, { recursive: true });
80
- } catch (error) {
81
- throw new Error(`Failed to create session directory: ${error}`);
82
- }
83
- }
84
-
85
- /**
86
- * Generate session file path without creating directories
87
- * @param sessionId - UUID session identifier
88
- * @param workdir - Working directory for the session
89
- * @param sessionType - Type of session ("main" or "subagent", defaults to "main")
90
- * @returns Promise resolving to full file path for the session JSONL file
91
- */
92
- export async function generateSessionFilePath(
93
- sessionId: string,
94
- workdir: string,
95
- sessionType: "main" | "subagent" = "main",
96
- ): Promise<string> {
97
- const encoder = new PathEncoder();
98
- const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
99
-
100
- // Generate filename based on session type
101
- const jsonlHandler = new JsonlHandler();
102
- const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
103
-
104
- return join(projectDir.encodedPath, filename);
105
- }
106
-
107
- /**
108
- * Generate session file path using project-based directory structure
109
- * @param sessionId - UUID session identifier
110
- * @param workdir - Working directory for the session
111
- * @param sessionType - Type of session ("main" or "subagent", defaults to "main")
112
- * @returns Promise resolving to full file path for the session JSONL file
113
- */
114
- export async function getSessionFilePath(
115
- sessionId: string,
116
- workdir: string,
117
- sessionType: "main" | "subagent" = "main",
118
- ): Promise<string> {
119
- const encoder = new PathEncoder();
120
- const projectDir = await encoder.createProjectDirectory(workdir, SESSION_DIR);
121
-
122
- // Generate filename based on session type
123
- const jsonlHandler = new JsonlHandler();
124
- const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
125
-
126
- return join(projectDir.encodedPath, filename);
127
- }
128
-
129
- /**
130
- * Create a new session
131
- * @param sessionId - UUID session identifier
132
- * @param workdir - Working directory for the session
133
- * @param sessionType - Type of session ("main" or "subagent", defaults to "main")
134
- */
135
- export async function createSession(
136
- sessionId: string,
137
- workdir: string,
138
- sessionType: "main" | "subagent" = "main",
139
- ): Promise<void> {
140
- const jsonlHandler = new JsonlHandler();
141
- const filePath = await getSessionFilePath(sessionId, workdir, sessionType);
142
- await jsonlHandler.createSession(filePath);
143
- }
144
-
145
- /**
146
- * Append messages to session using JSONL format (new approach)
147
- *
148
- * @param sessionId - UUID session identifier
149
- * @param newMessages - Array of messages to append
150
- * @param workdir - Working directory for the session
151
- * @param sessionType - Type of session ("main" or "subagent", defaults to "main")
152
- */
153
- export async function appendMessages(
154
- sessionId: string,
155
- newMessages: Message[],
156
- workdir: string,
157
- sessionType: "main" | "subagent" = "main",
158
- ): Promise<void> {
159
- // Do not save session files in test environment
160
- if (process.env.NODE_ENV === "test") {
161
- return;
162
- }
163
-
164
- // Do not save if there are no messages
165
- if (newMessages.length === 0) {
166
- return;
167
- }
168
-
169
- const jsonlHandler = new JsonlHandler();
170
-
171
- // Generate the session file path directly using known session type
172
- const filePath = await generateSessionFilePath(
173
- sessionId,
174
- workdir,
175
- sessionType,
176
- );
177
-
178
- // Check if the session file exists
179
- try {
180
- await fs.access(filePath);
181
- } catch {
182
- throw new Error(
183
- `Session file not found: ${sessionId}. Use createSession() to create a new session first.`,
184
- );
185
- }
186
-
187
- await jsonlHandler.append(filePath, newMessages, {
188
- atomic: false,
189
- });
190
- }
191
-
192
- /**
193
- * Load session data from JSONL file (new approach)
194
- *
195
- * @param sessionId - UUID session identifier
196
- * @param workdir - Working directory for the session
197
- * @param sessionType - Type of session ("main" or "subagent", defaults to "main")
198
- * @returns Promise that resolves to session data or null if session doesn't exist
199
- */
200
- export async function loadSessionFromJsonl(
201
- sessionId: string,
202
- workdir: string,
203
- sessionType: "main" | "subagent" = "main",
204
- ): Promise<SessionData | null> {
205
- try {
206
- const jsonlHandler = new JsonlHandler();
207
-
208
- // Generate the session file path directly using known session type
209
- const filePath = await generateSessionFilePath(
210
- sessionId,
211
- workdir,
212
- sessionType,
213
- );
214
-
215
- // Check if file exists
216
- try {
217
- await fs.access(filePath);
218
- } catch (error) {
219
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
220
- return null;
221
- }
222
- throw error;
223
- }
224
-
225
- const allMessages = await jsonlHandler.read(filePath);
226
-
227
- // Find the last compact boundary — only return messages from there forward
228
- const messages = sliceFromLastCompact(allMessages);
229
-
230
- // Extract metadata from messages
231
- const lastMessage =
232
- messages.length > 0 ? messages[messages.length - 1] : null;
233
-
234
- const sessionData: SessionData = {
235
- id: sessionId,
236
- messages,
237
- metadata: {
238
- workdir,
239
- lastActiveAt: lastMessage
240
- ? lastMessage.timestamp
241
- : new Date().toISOString(),
242
- latestTotalTokens: lastMessage?.usage
243
- ? extractLatestTotalTokens([lastMessage])
244
- : 0,
245
- },
246
- };
247
-
248
- return sessionData;
249
- } catch (error) {
250
- const errorMessage = error instanceof Error ? error.message : String(error);
251
-
252
- // Check if the underlying error is ENOENT (file doesn't exist)
253
- if (
254
- errorMessage.includes("ENOENT") ||
255
- (error as NodeJS.ErrnoException).code === "ENOENT"
256
- ) {
257
- return null; // Session file does not exist
258
- }
259
-
260
- // Check for JSON parsing errors (corrupted files)
261
- if (
262
- errorMessage.includes("Invalid JSON") ||
263
- errorMessage.includes("Unexpected token")
264
- ) {
265
- return null; // Treat corrupted files as non-existent
266
- }
267
-
268
- throw new Error(`Failed to load session ${sessionId}: ${error}`);
269
- }
270
- }
271
-
272
- /**
273
- * Get the most recently active session for a specific working directory (new JSONL approach)
274
- * Only returns main sessions, skips subagent sessions
275
- * Uses listSessionsFromJsonl which already sorts sessions by last active time (most recent first)
276
- *
277
- * @param workdir - Working directory to find the most recently active session for
278
- * @returns Promise that resolves to the most recently active session data or null if no sessions exist
279
- */
280
- export async function getLatestSessionFromJsonl(
281
- workdir: string,
282
- ): Promise<SessionData | null> {
283
- const sessions = await listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
284
-
285
- if (sessions.length === 0) {
286
- return null;
287
- }
288
-
289
- // Sessions are already sorted by lastActiveAt from listSessionsFromJsonl (most recent first)
290
- const latestSession = sessions[0];
291
- return loadSessionFromJsonl(latestSession.id, workdir);
292
- }
293
-
294
- /**
295
- * List all sessions for a specific working directory (convenience wrapper)
296
- * Only returns main sessions, skips subagent sessions
297
- *
298
- * @param workdir - Working directory to filter sessions by
299
- * @returns Promise that resolves to array of session metadata objects
300
- */
301
- export async function listSessions(
302
- workdir: string,
303
- ): Promise<SessionMetadata[]> {
304
- return listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
305
- }
306
-
307
- /**
308
- * List all sessions for a specific working directory using JSONL format (optimized approach)
309
- *
310
- * PERFORMANCE OPTIMIZATION:
311
- * - Uses filename parsing exclusively for session metadata
312
- * - Only reads last message for timestamps and token counts
313
- * - Eliminates O(n*2) file operations, achieving O(n) performance
314
- * - Returns simplified session metadata objects
315
- * - Only includes main sessions, excludes subagent sessions
316
- *
317
- * @param workdir - Working directory to filter sessions by
318
- * @returns Promise that resolves to array of session metadata objects
319
- */
320
- export async function listSessionsFromJsonl(
321
- workdir: string,
322
- ): Promise<SessionMetadata[]> {
323
- try {
324
- const encoder = new PathEncoder();
325
- const baseDir = SESSION_DIR;
326
-
327
- const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
328
-
329
- const sevenDaysAgo = new Date();
330
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
331
-
332
- let files: string[];
333
- try {
334
- files = await fs.readdir(projectDir.encodedPath);
335
- } catch (error) {
336
- // If project directory doesn't exist, return empty array
337
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
338
- return [];
339
- }
340
- throw error;
341
- }
342
-
343
- const sessions: SessionMetadata[] = [];
344
-
345
- for (const file of files) {
346
- if (!file.endsWith(".jsonl")) {
347
- continue;
348
- }
349
-
350
- // EARLY FILTERING: Skip subagent sessions by filename prefix for maximum performance
351
- if (file.startsWith("subagent-")) {
352
- continue;
353
- }
354
-
355
- try {
356
- const filePath = join(projectDir.encodedPath, file);
357
-
358
- // Validate main session filename format (UUID.jsonl)
359
- const uuidMatch = file.match(
360
- /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
361
- );
362
- if (!uuidMatch) {
363
- continue; // Skip invalid filenames
364
- }
365
-
366
- const sessionId = uuidMatch[1];
367
-
368
- // PERFORMANCE OPTIMIZATION: Only read the last message for timestamps and tokens
369
- const jsonlHandler = new JsonlHandler();
370
- const lastMessage = await jsonlHandler.getLastMessage(filePath);
371
-
372
- // Handle timing information efficiently
373
- let lastActiveAt: Date;
374
-
375
- if (lastMessage) {
376
- lastActiveAt = new Date(lastMessage.timestamp);
377
- } else {
378
- // Empty session file - use file modification time
379
- const stats = await fs.stat(filePath);
380
- lastActiveAt = stats.mtime;
381
- }
382
-
383
- if (lastActiveAt < sevenDaysAgo) {
384
- continue;
385
- }
386
-
387
- // Return inline object for performance (no interface instantiation overhead)
388
- const sessionMeta: SessionMetadata = {
389
- id: sessionId,
390
- sessionType: "main",
391
- subagentType: undefined,
392
- workdir: projectDir.originalPath,
393
- createdAt: new Date(),
394
- lastActiveAt,
395
- latestTotalTokens: lastMessage?.usage
396
- ? extractLatestTotalTokens([lastMessage])
397
- : 0,
398
- };
399
-
400
- // Try to get first message content for display
401
- try {
402
- const firstContent = await getFirstMessageContent(sessionId, workdir);
403
- if (firstContent) {
404
- sessionMeta.firstMessage = firstContent;
405
- }
406
- } catch {
407
- // Ignore errors getting first message
408
- }
409
-
410
- sessions.push(sessionMeta);
411
- } catch {
412
- // Skip corrupted session files
413
- continue;
414
- }
415
- }
416
-
417
- // Sort by last active time (most recently active first)
418
- return sessions.sort(
419
- (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
420
- );
421
- } catch (error) {
422
- throw new Error(`Failed to list sessions: ${error}`);
423
- }
424
- }
425
-
426
- /**
427
- * List all sessions across all project directories
428
- *
429
- * @returns Promise that resolves to array of session metadata objects
430
- */
431
- export async function listAllSessions(): Promise<SessionMetadata[]> {
432
- try {
433
- const baseDir = SESSION_DIR;
434
- let projectDirs: string[];
435
- try {
436
- projectDirs = await fs.readdir(baseDir);
437
- } catch (error) {
438
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
439
- return [];
440
- }
441
- throw error;
442
- }
443
-
444
- const allSessions: SessionMetadata[] = [];
445
- const sevenDaysAgo = new Date();
446
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
447
-
448
- for (const projectDirName of projectDirs) {
449
- const projectPath = join(baseDir, projectDirName);
450
- try {
451
- const stat = await fs.stat(projectPath);
452
- if (!stat.isDirectory()) {
453
- continue;
454
- }
455
-
456
- // Scan .jsonl files in the project directory
457
- const files = await fs.readdir(projectPath);
458
-
459
- for (const file of files) {
460
- if (!file.endsWith(".jsonl") || file.startsWith("subagent-")) {
461
- continue;
462
- }
463
-
464
- try {
465
- const filePath = join(projectPath, file);
466
- const uuidMatch = file.match(
467
- /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
468
- );
469
- if (!uuidMatch) continue;
470
-
471
- const sessionId = uuidMatch[1];
472
- const jsonlHandler = new JsonlHandler();
473
- const lastMessage = await jsonlHandler.getLastMessage(filePath);
474
-
475
- let lastActiveAt: Date;
476
- if (lastMessage) {
477
- lastActiveAt = new Date(lastMessage.timestamp);
478
- } else {
479
- const stats = await fs.stat(filePath);
480
- lastActiveAt = stats.mtime;
481
- }
482
-
483
- if (lastActiveAt < sevenDaysAgo) continue;
484
-
485
- allSessions.push({
486
- id: sessionId,
487
- sessionType: "main" as const,
488
- subagentType: undefined,
489
- workdir: projectDirName,
490
- createdAt: new Date(),
491
- lastActiveAt,
492
- latestTotalTokens: lastMessage?.usage
493
- ? extractLatestTotalTokens([lastMessage])
494
- : 0,
495
- });
496
- } catch {
497
- continue;
498
- }
499
- }
500
- } catch {
501
- // Skip if stat/readdir fails
502
- }
503
- }
504
-
505
- return allSessions.sort(
506
- (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
507
- );
508
- } catch (error) {
509
- throw new Error(`Failed to list all sessions: ${error}`);
510
- }
511
- }
512
-
513
- /**
514
- * Clean up expired sessions older than 14 days based on file modification time
515
- *
516
- * @param workdir - Working directory to clean up sessions for
517
- * @returns Promise that resolves to the number of sessions that were deleted
518
- */
519
- export async function cleanupExpiredSessionsFromJsonl(
520
- workdir: string,
521
- ): Promise<number> {
522
- // Do not perform cleanup operations in test environment
523
- if (process.env.NODE_ENV === "test") {
524
- return 0;
525
- }
526
-
527
- try {
528
- const encoder = new PathEncoder();
529
- const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
530
- const files = await fs.readdir(projectDir.encodedPath);
531
-
532
- const now = new Date();
533
- const maxAge = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000; // Convert to milliseconds
534
- let deletedCount = 0;
535
-
536
- for (const file of files) {
537
- if (!file.endsWith(".jsonl")) {
538
- continue;
539
- }
540
-
541
- const filePath = join(projectDir.encodedPath, file);
542
-
543
- try {
544
- const stat = await fs.stat(filePath);
545
- const fileAge = now.getTime() - stat.mtime.getTime();
546
-
547
- if (fileAge > maxAge) {
548
- await fs.unlink(filePath);
549
- deletedCount++;
550
- }
551
- } catch {
552
- // Skip failed operations and continue processing other files
553
- continue;
554
- }
555
- }
556
-
557
- // Clean up empty project directory if no files remain
558
- try {
559
- const remainingFiles = await fs.readdir(projectDir.encodedPath);
560
- if (remainingFiles.length === 0) {
561
- await fs.rmdir(projectDir.encodedPath);
562
- }
563
- } catch {
564
- // Ignore errors if directory is not empty or can't be removed
565
- }
566
-
567
- return deletedCount;
568
- } catch {
569
- // Return 0 if project directory doesn't exist or can't be accessed
570
- return 0;
571
- }
572
- }
573
-
574
- /**
575
- * Clean up empty project directories in the session directory
576
- */
577
- export async function cleanupEmptyProjectDirectories(): Promise<void> {
578
- // Do not perform cleanup operations in test environment
579
- if (process.env.NODE_ENV === "test") {
580
- return;
581
- }
582
-
583
- try {
584
- const baseDir = SESSION_DIR;
585
- const projectDirs = await fs.readdir(baseDir);
586
-
587
- for (const projectDirName of projectDirs) {
588
- const projectPath = join(baseDir, projectDirName);
589
- const stat = await fs.stat(projectPath);
590
-
591
- if (stat.isDirectory()) {
592
- try {
593
- const files = await fs.readdir(projectPath);
594
-
595
- // If directory is empty, remove it
596
- if (files.length === 0) {
597
- await fs.rmdir(projectPath);
598
- }
599
- } catch {
600
- // Skip errors for directories we can't read or remove
601
- continue;
602
- }
603
- }
604
- }
605
- } catch {
606
- // Ignore errors if base directory doesn't exist or can't be accessed
607
- }
608
- }
609
-
610
- /**
611
- * Clean up "ghost" session files that contain only meta messages
612
- * (isMeta: true) — e.g. sessions where a SessionStart hook injected a
613
- * system-reminder but no real user/assistant message was ever sent.
614
- *
615
- * Such sessions are no longer created thanks to lazy materialization in
616
- * saveSession(); this one-time sweep removes files that predate that change
617
- * so they stop showing up as "0 tokens / No content" entries in the resume
618
- * list.
619
- *
620
- * @returns Promise that resolves to the number of files deleted
621
- */
622
- export async function cleanupMetaOnlySessions(): Promise<number> {
623
- // Do not perform cleanup operations in test environment
624
- if (process.env.NODE_ENV === "test") {
625
- return 0;
626
- }
627
-
628
- let deletedCount = 0;
629
- try {
630
- const projectDirs = await fs.readdir(SESSION_DIR);
631
-
632
- for (const projectDirName of projectDirs) {
633
- const projectPath = join(SESSION_DIR, projectDirName);
634
- try {
635
- const stat = await fs.stat(projectPath);
636
- if (!stat.isDirectory()) {
637
- continue;
638
- }
639
-
640
- const files = await fs.readdir(projectPath);
641
- for (const file of files) {
642
- if (!file.endsWith(".jsonl")) {
643
- continue;
644
- }
645
-
646
- const filePath = join(projectPath, file);
647
- try {
648
- // Fast path: a file whose last message is not meta contains a
649
- // real message, so it can never be meta-only.
650
- const jsonlHandler = new JsonlHandler();
651
- const lastMessage = await jsonlHandler.getLastMessage(filePath);
652
- if (!lastMessage?.isMeta) {
653
- continue;
654
- }
655
-
656
- const messages = await jsonlHandler.read(filePath);
657
- if (messages.length > 0 && messages.every((m) => m.isMeta)) {
658
- await fs.unlink(filePath);
659
- deletedCount++;
660
- }
661
- } catch {
662
- // Skip corrupted or unreadable files
663
- continue;
664
- }
665
- }
666
- } catch {
667
- // Skip directories we can't access
668
- continue;
669
- }
670
- }
671
- } catch {
672
- // Ignore errors if base directory doesn't exist or can't be accessed
673
- }
674
-
675
- return deletedCount;
676
- }
677
-
678
- /**
679
- * Check if a session exists in JSONL storage (new approach)
680
- *
681
- * @param sessionId - UUID session identifier
682
- * @param workdir - Working directory for the session
683
- * @param sessionType - Type of session ("main" or "subagent"). If not provided, checks both types.
684
- * @returns Promise that resolves to true if session exists, false otherwise
685
- */
686
- export async function sessionExistsInJsonl(
687
- sessionId: string,
688
- workdir: string,
689
- sessionType?: "main" | "subagent",
690
- ): Promise<boolean> {
691
- try {
692
- if (sessionType) {
693
- // If session type is known, check directly
694
- const filePath = await generateSessionFilePath(
695
- sessionId,
696
- workdir,
697
- sessionType,
698
- );
699
- await fs.access(filePath);
700
- return true;
701
- } else {
702
- // If session type is unknown, try both
703
- const mainPath = await generateSessionFilePath(
704
- sessionId,
705
- workdir,
706
- "main",
707
- );
708
- try {
709
- await fs.access(mainPath);
710
- return true;
711
- } catch {
712
- const subagentPath = await generateSessionFilePath(
713
- sessionId,
714
- workdir,
715
- "subagent",
716
- );
717
- try {
718
- await fs.access(subagentPath);
719
- return true;
720
- } catch {
721
- return false;
722
- }
723
- }
724
- }
725
- } catch {
726
- return false;
727
- }
728
- }
729
-
730
- /**
731
- * Get the content of the first non-meta message in a session
732
- * For user role: get text block content
733
- * For assistant role: get compact block content
734
- * Skips meta messages (isMeta: true) to find the first meaningful message
735
- * @param sessionId - Session ID to get first message from
736
- * @param workdir - Working directory for session operations
737
- * @returns Promise that resolves to the first non-meta message content or null if not found
738
- */
739
- export async function getFirstMessageContent(
740
- sessionId: string,
741
- workdir: string,
742
- ): Promise<string | null> {
743
- try {
744
- const encoder = new PathEncoder();
745
- const baseDir = SESSION_DIR;
746
-
747
- const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
748
- const filePath = join(projectDir.encodedPath, `${sessionId}.jsonl`);
749
-
750
- // Read first N lines to skip meta messages
751
- const { readFirstNLines } = await import("../utils/fileUtils.js");
752
- const lines = await readFirstNLines(filePath, 10);
753
-
754
- for (const line of lines) {
755
- try {
756
- const message = JSON.parse(line) as Message;
757
-
758
- // Skip meta messages
759
- if (message.isMeta) {
760
- continue;
761
- }
762
-
763
- const content = getMessageContent(message);
764
- if (content) {
765
- return content;
766
- }
767
- } catch (error) {
768
- logger.warn(`Failed to parse message in session ${sessionId}:`, error);
769
- }
770
- }
771
-
772
- return null;
773
- } catch (error) {
774
- logger.warn(
775
- `Failed to get first message content for session ${sessionId}:`,
776
- error,
777
- );
778
- return null;
779
- }
780
- }
781
-
782
- /**
783
- * Delete a session
784
- * @param sessionId - UUID session identifier
785
- * @param workdir - Working directory for the session
786
- * @param sessionType - Type of session ("main" or "subagent", defaults to "main")
787
- */
788
- export async function deleteSession(
789
- sessionId: string,
790
- workdir: string,
791
- sessionType: "main" | "subagent" = "main",
792
- ): Promise<void> {
793
- const filePath = await generateSessionFilePath(
794
- sessionId,
795
- workdir,
796
- sessionType,
797
- );
798
- try {
799
- await fs.unlink(filePath);
800
- } catch (error) {
801
- // Ignore if file doesn't exist
802
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
803
- throw error;
804
- }
805
- }
806
- }
807
-
808
- /**
809
- * Truncate content to a maximum length, adding ellipsis if truncated.
810
- * Also replaces real newlines with the literal string "\n".
811
- * @param content - The content to truncate
812
- * @param maxLength - Maximum length before truncation (default: 100)
813
- * @returns Truncated content with ellipsis if needed
814
- */
815
- export function truncateContent(
816
- content: string,
817
- maxLength: number = 100,
818
- ): string {
819
- // Replace real newlines with literal "\n"
820
- const singleLineContent = content.replace(/\n/g, "\\n");
821
-
822
- if (singleLineContent.length <= maxLength) {
823
- return singleLineContent;
824
- }
825
- return singleLineContent.substring(0, maxLength) + "...";
826
- }
827
-
828
- /**
829
- * Handle session restoration logic
830
- * @param restoreSessionId - Specific session ID to restore
831
- * @param continueLastSession - Whether to continue the most recent session
832
- * @param workdir - Working directory for session restoration
833
- * @returns Promise that resolves to session data or undefined
834
- */
835
- export async function handleSessionRestoration(
836
- restoreSessionId?: string,
837
- continueLastSession?: boolean,
838
- workdir?: string,
839
- ): Promise<SessionData | undefined> {
840
- if (!workdir) {
841
- throw new Error("Working directory is required for session restoration");
842
- }
843
-
844
- // Clean up expired sessions first
845
- cleanupExpiredSessionsFromJsonl(workdir).catch((error) => {
846
- logger.warn("Failed to cleanup expired sessions:", error);
847
- });
848
-
849
- if (!restoreSessionId && !continueLastSession) {
850
- return;
851
- }
852
-
853
- try {
854
- let sessionToRestore: SessionData | null = null;
855
-
856
- if (restoreSessionId) {
857
- // Use only JSONL format - no legacy support
858
- sessionToRestore = await loadSessionFromJsonl(restoreSessionId, workdir);
859
- if (!sessionToRestore) {
860
- // Session doesn't exist on disk (e.g. new project with no messages saved yet).
861
- // Gracefully fall back to starting fresh instead of throwing.
862
- logger?.warn(
863
- `Session ${restoreSessionId} not found on disk, starting fresh session`,
864
- );
865
- return;
866
- }
867
- } else if (continueLastSession) {
868
- // Use only JSONL format - no legacy support
869
- sessionToRestore = await getLatestSessionFromJsonl(workdir);
870
- if (!sessionToRestore) {
871
- throw new Error(`No previous session found for workdir: ${workdir}`);
872
- }
873
- }
874
-
875
- if (sessionToRestore) {
876
- console.log(`Restoring session: ${sessionToRestore.id}`);
877
-
878
- // // Initialize from session data
879
- // this.initializeFromSession();
880
- return sessionToRestore;
881
- }
882
- } catch (error) {
883
- console.error("Failed to restore session:", error);
884
- throw error;
885
- }
886
- }
887
-
888
- /**
889
- * Load the full message thread for a session.
890
- * With append-only compaction, all messages are in a single file.
891
- * Unlike loadSessionFromJsonl, this returns every message in the file,
892
- * including those before the last compact boundary — rewind needs the
893
- * complete history to allow rewinding past compaction points.
894
- * @param currentSessionId - The ID of the current session
895
- * @param workdir - Working directory for the session
896
- * @returns Promise that resolves to an array of all messages in the thread
897
- */
898
- export async function loadFullMessageThread(
899
- currentSessionId: string,
900
- workdir: string,
901
- ): Promise<{ messages: Message[]; sessionIds: string[] }> {
902
- const jsonlHandler = new JsonlHandler();
903
- const filePath = await generateSessionFilePath(
904
- currentSessionId,
905
- workdir,
906
- "main",
907
- );
908
-
909
- try {
910
- await fs.access(filePath);
911
- } catch {
912
- return { messages: [], sessionIds: [] };
913
- }
914
-
915
- const messages = await jsonlHandler.read(filePath);
916
- return { messages, sessionIds: [currentSessionId] };
917
- }