wave-agent-sdk 1.0.4 → 1.0.6

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