wave-agent-sdk 1.0.5 → 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 (193) 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 +13 -6
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/telemetry/instrumentation.d.ts +1 -1
  10. package/dist/telemetry/instrumentation.js +57 -28
  11. package/dist/tools/bashTool.js +16 -6
  12. package/dist/tools/grepTool.js +11 -3
  13. package/dist/types/index.d.ts +3 -0
  14. package/dist/types/messaging.d.ts +2 -0
  15. package/dist/utils/containerSetup.js +0 -3
  16. package/dist/utils/messageOperations.d.ts +2 -1
  17. package/dist/utils/messageOperations.js +2 -1
  18. package/dist/utils/notificationXml.js +5 -0
  19. package/dist/utils/ripgrep.d.ts +4 -4
  20. package/dist/utils/ripgrep.js +4 -27
  21. package/package.json +20 -8
  22. package/bin/rg +0 -79
  23. package/dist/managers/forkedAgentManager.d.ts +0 -50
  24. package/dist/managers/forkedAgentManager.js +0 -130
  25. package/scripts/install_ripgrep.js +0 -111
  26. package/scripts/postinstall.js +0 -38
  27. package/src/agent.ts +0 -1271
  28. package/src/constants/subagents.ts +0 -4
  29. package/src/constants/toolLimits.ts +0 -15
  30. package/src/constants/tools.ts +0 -24
  31. package/src/core/plugin.ts +0 -237
  32. package/src/core/session.ts +0 -9
  33. package/src/index.ts +0 -38
  34. package/src/managers/MemoryRuleManager.ts +0 -198
  35. package/src/managers/aiManager.ts +0 -2429
  36. package/src/managers/backgroundTaskManager.ts +0 -480
  37. package/src/managers/bangManager.ts +0 -119
  38. package/src/managers/cronManager.ts +0 -401
  39. package/src/managers/foregroundTaskManager.ts +0 -30
  40. package/src/managers/forkedAgentManager.ts +0 -216
  41. package/src/managers/hookManager.ts +0 -1089
  42. package/src/managers/liveConfigManager.ts +0 -445
  43. package/src/managers/lspManager.ts +0 -470
  44. package/src/managers/mcpManager.ts +0 -1010
  45. package/src/managers/messageManager.ts +0 -1184
  46. package/src/managers/messageQueue.ts +0 -157
  47. package/src/managers/permissionManager.ts +0 -1140
  48. package/src/managers/planManager.ts +0 -112
  49. package/src/managers/pluginManager.ts +0 -330
  50. package/src/managers/pluginScopeManager.ts +0 -124
  51. package/src/managers/reversionManager.ts +0 -178
  52. package/src/managers/skillManager.ts +0 -578
  53. package/src/managers/slashCommandManager.ts +0 -564
  54. package/src/managers/subagentManager.ts +0 -915
  55. package/src/managers/toolManager.ts +0 -489
  56. package/src/managers/workflowManager.ts +0 -493
  57. package/src/prompts/autoMemory.ts +0 -33
  58. package/src/prompts/autoMemoryExtraction.ts +0 -146
  59. package/src/prompts/index.ts +0 -506
  60. package/src/prompts/planModeReminders.ts +0 -126
  61. package/src/services/GitService.ts +0 -131
  62. package/src/services/MarketplaceService.ts +0 -1052
  63. package/src/services/MemoryRuleService.ts +0 -71
  64. package/src/services/aiService.ts +0 -919
  65. package/src/services/authService.ts +0 -565
  66. package/src/services/autoMemoryService.ts +0 -280
  67. package/src/services/configurationService.ts +0 -1520
  68. package/src/services/fileWatcher.ts +0 -319
  69. package/src/services/hook.ts +0 -348
  70. package/src/services/initializationService.ts +0 -357
  71. package/src/services/interactionService.ts +0 -229
  72. package/src/services/jsonlHandler.ts +0 -295
  73. package/src/services/memory.ts +0 -234
  74. package/src/services/pluginLoader.ts +0 -252
  75. package/src/services/remoteSettingsService.ts +0 -366
  76. package/src/services/reversionService.ts +0 -122
  77. package/src/services/session.ts +0 -917
  78. package/src/services/taskManager.ts +0 -321
  79. package/src/telemetry/events.ts +0 -65
  80. package/src/telemetry/instrumentation.ts +0 -499
  81. package/src/telemetry/sessionTracing.ts +0 -348
  82. package/src/tools/agentTool.ts +0 -322
  83. package/src/tools/askUserQuestion.ts +0 -160
  84. package/src/tools/bashTool.ts +0 -621
  85. package/src/tools/buildTool.ts +0 -61
  86. package/src/tools/cronCreateTool.ts +0 -161
  87. package/src/tools/cronDeleteTool.ts +0 -51
  88. package/src/tools/cronListTool.ts +0 -47
  89. package/src/tools/editTool.ts +0 -313
  90. package/src/tools/enterPlanMode.ts +0 -124
  91. package/src/tools/enterWorktreeTool.ts +0 -190
  92. package/src/tools/exitPlanMode.ts +0 -139
  93. package/src/tools/exitWorktreeTool.ts +0 -236
  94. package/src/tools/globTool.ts +0 -167
  95. package/src/tools/grepTool.ts +0 -399
  96. package/src/tools/lspTool.ts +0 -883
  97. package/src/tools/readTool.ts +0 -426
  98. package/src/tools/skillTool.ts +0 -248
  99. package/src/tools/taskManagementTools.ts +0 -574
  100. package/src/tools/taskStopTool.ts +0 -78
  101. package/src/tools/types.ts +0 -137
  102. package/src/tools/webFetchTool.ts +0 -373
  103. package/src/tools/workflowTool.ts +0 -205
  104. package/src/tools/writeTool.ts +0 -267
  105. package/src/types/agent.ts +0 -128
  106. package/src/types/auth.ts +0 -19
  107. package/src/types/commands.ts +0 -30
  108. package/src/types/config.ts +0 -43
  109. package/src/types/configuration.ts +0 -168
  110. package/src/types/core.ts +0 -105
  111. package/src/types/cron.ts +0 -13
  112. package/src/types/environment.ts +0 -106
  113. package/src/types/fileSearch.ts +0 -4
  114. package/src/types/history.ts +0 -7
  115. package/src/types/hooks.ts +0 -256
  116. package/src/types/index.ts +0 -41
  117. package/src/types/lsp.ts +0 -98
  118. package/src/types/marketplace.ts +0 -71
  119. package/src/types/mcp.ts +0 -43
  120. package/src/types/memoryRule.ts +0 -31
  121. package/src/types/messaging.ts +0 -114
  122. package/src/types/permissions.ts +0 -80
  123. package/src/types/plugins.ts +0 -39
  124. package/src/types/processes.ts +0 -99
  125. package/src/types/reversion.ts +0 -29
  126. package/src/types/session.ts +0 -12
  127. package/src/types/skills.ts +0 -108
  128. package/src/types/tasks.ts +0 -13
  129. package/src/types/telemetry.ts +0 -98
  130. package/src/types/tools.ts +0 -44
  131. package/src/types/workflow.ts +0 -6
  132. package/src/utils/abortUtils.ts +0 -118
  133. package/src/utils/atomicWrite.ts +0 -61
  134. package/src/utils/bashParser.ts +0 -899
  135. package/src/utils/cacheControlUtils.ts +0 -426
  136. package/src/utils/commandArgumentParser.ts +0 -161
  137. package/src/utils/commandPathResolver.ts +0 -88
  138. package/src/utils/configPaths.ts +0 -220
  139. package/src/utils/configValidator.ts +0 -100
  140. package/src/utils/constants.ts +0 -39
  141. package/src/utils/container.ts +0 -92
  142. package/src/utils/containerSetup.ts +0 -371
  143. package/src/utils/convertMessagesForAPI.ts +0 -326
  144. package/src/utils/cronTasks.ts +0 -128
  145. package/src/utils/cronTasksLock.ts +0 -182
  146. package/src/utils/cronToHuman.ts +0 -99
  147. package/src/utils/customCommands.ts +0 -95
  148. package/src/utils/editUtils.ts +0 -20
  149. package/src/utils/fileFormat.ts +0 -40
  150. package/src/utils/fileSearch.ts +0 -151
  151. package/src/utils/fileUtils.ts +0 -223
  152. package/src/utils/gitUtils.ts +0 -315
  153. package/src/utils/globalLogger.ts +0 -128
  154. package/src/utils/groupMessagesByApiRound.ts +0 -121
  155. package/src/utils/hookMatcher.ts +0 -176
  156. package/src/utils/markdownParser.ts +0 -317
  157. package/src/utils/mcpUtils.ts +0 -138
  158. package/src/utils/messageOperations.ts +0 -659
  159. package/src/utils/modelCapabilities.ts +0 -30
  160. package/src/utils/nameGenerator.ts +0 -99
  161. package/src/utils/notificationXml.ts +0 -57
  162. package/src/utils/openaiClient.ts +0 -251
  163. package/src/utils/parseCronExpression.ts +0 -78
  164. package/src/utils/path.ts +0 -72
  165. package/src/utils/pathEncoder.ts +0 -400
  166. package/src/utils/pathSafety.ts +0 -40
  167. package/src/utils/promptHistory.ts +0 -168
  168. package/src/utils/ripgrep.ts +0 -35
  169. package/src/utils/shellResolver.ts +0 -190
  170. package/src/utils/skillParser.ts +0 -246
  171. package/src/utils/stringUtils.ts +0 -156
  172. package/src/utils/subagentParser.ts +0 -279
  173. package/src/utils/taskReminder.ts +0 -96
  174. package/src/utils/tokenCalculation.ts +0 -43
  175. package/src/utils/tokenEstimate.ts +0 -34
  176. package/src/utils/toolResultStorage.ts +0 -117
  177. package/src/utils/worktreeSession.ts +0 -26
  178. package/src/utils/worktreeUtils.ts +0 -786
  179. package/src/workflow/budgetTracker.ts +0 -34
  180. package/src/workflow/concurrencyLimiter.ts +0 -47
  181. package/src/workflow/journal.ts +0 -95
  182. package/src/workflow/progressReporter.ts +0 -141
  183. package/src/workflow/runState.ts +0 -65
  184. package/src/workflow/scriptRuntime.ts +0 -274
  185. package/src/workflow/structuredOutput.ts +0 -123
  186. package/src/workflow/types.ts +0 -101
  187. package/src/workflow/workflowApis.ts +0 -410
  188. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  189. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  190. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  191. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  192. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  193. 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
- }