wave-agent-sdk 0.0.6 → 0.0.8

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 (180) hide show
  1. package/dist/agent.d.ts +32 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +209 -24
  4. package/dist/constants/events.d.ts +28 -0
  5. package/dist/constants/events.d.ts.map +1 -0
  6. package/dist/constants/events.js +27 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2 -0
  10. package/dist/managers/aiManager.d.ts +34 -1
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +248 -132
  13. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  14. package/dist/managers/backgroundBashManager.js +7 -6
  15. package/dist/managers/hookManager.d.ts +13 -16
  16. package/dist/managers/hookManager.d.ts.map +1 -1
  17. package/dist/managers/hookManager.js +81 -44
  18. package/dist/managers/liveConfigManager.d.ts +58 -0
  19. package/dist/managers/liveConfigManager.d.ts.map +1 -0
  20. package/dist/managers/liveConfigManager.js +160 -0
  21. package/dist/managers/messageManager.d.ts +41 -24
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +168 -49
  24. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  25. package/dist/managers/slashCommandManager.js +9 -3
  26. package/dist/managers/subagentManager.d.ts +51 -0
  27. package/dist/managers/subagentManager.d.ts.map +1 -1
  28. package/dist/managers/subagentManager.js +190 -19
  29. package/dist/services/aiService.d.ts +13 -5
  30. package/dist/services/aiService.d.ts.map +1 -1
  31. package/dist/services/aiService.js +350 -74
  32. package/dist/services/configurationWatcher.d.ts +120 -0
  33. package/dist/services/configurationWatcher.d.ts.map +1 -0
  34. package/dist/services/configurationWatcher.js +439 -0
  35. package/dist/services/fileWatcher.d.ts +69 -0
  36. package/dist/services/fileWatcher.d.ts.map +1 -0
  37. package/dist/services/fileWatcher.js +213 -0
  38. package/dist/services/hook.d.ts +91 -9
  39. package/dist/services/hook.d.ts.map +1 -1
  40. package/dist/services/hook.js +393 -43
  41. package/dist/services/jsonlHandler.d.ts +62 -0
  42. package/dist/services/jsonlHandler.d.ts.map +1 -0
  43. package/dist/services/jsonlHandler.js +257 -0
  44. package/dist/services/memory.d.ts +9 -0
  45. package/dist/services/memory.d.ts.map +1 -1
  46. package/dist/services/memory.js +81 -12
  47. package/dist/services/memoryStore.d.ts +81 -0
  48. package/dist/services/memoryStore.d.ts.map +1 -0
  49. package/dist/services/memoryStore.js +200 -0
  50. package/dist/services/session.d.ts +64 -49
  51. package/dist/services/session.d.ts.map +1 -1
  52. package/dist/services/session.js +310 -132
  53. package/dist/tools/bashTool.d.ts.map +1 -1
  54. package/dist/tools/bashTool.js +5 -4
  55. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  56. package/dist/tools/deleteFileTool.js +2 -1
  57. package/dist/tools/editTool.d.ts.map +1 -1
  58. package/dist/tools/editTool.js +3 -2
  59. package/dist/tools/multiEditTool.d.ts.map +1 -1
  60. package/dist/tools/multiEditTool.js +4 -3
  61. package/dist/tools/readTool.d.ts.map +1 -1
  62. package/dist/tools/readTool.js +2 -1
  63. package/dist/tools/todoWriteTool.d.ts.map +1 -1
  64. package/dist/tools/todoWriteTool.js +3 -10
  65. package/dist/tools/writeTool.d.ts.map +1 -1
  66. package/dist/tools/writeTool.js +5 -6
  67. package/dist/types/commands.d.ts +4 -0
  68. package/dist/types/commands.d.ts.map +1 -1
  69. package/dist/types/core.d.ts +35 -0
  70. package/dist/types/core.d.ts.map +1 -1
  71. package/dist/types/environment.d.ts +42 -0
  72. package/dist/types/environment.d.ts.map +1 -0
  73. package/dist/types/environment.js +21 -0
  74. package/dist/types/hooks.d.ts +8 -2
  75. package/dist/types/hooks.d.ts.map +1 -1
  76. package/dist/types/hooks.js +8 -2
  77. package/dist/types/index.d.ts +2 -0
  78. package/dist/types/index.d.ts.map +1 -1
  79. package/dist/types/index.js +2 -0
  80. package/dist/types/memoryStore.d.ts +82 -0
  81. package/dist/types/memoryStore.d.ts.map +1 -0
  82. package/dist/types/memoryStore.js +7 -0
  83. package/dist/types/messaging.d.ts +21 -9
  84. package/dist/types/messaging.d.ts.map +1 -1
  85. package/dist/types/messaging.js +5 -1
  86. package/dist/types/session.d.ts +20 -0
  87. package/dist/types/session.d.ts.map +1 -0
  88. package/dist/types/session.js +7 -0
  89. package/dist/utils/bashHistory.d.ts.map +1 -1
  90. package/dist/utils/bashHistory.js +27 -26
  91. package/dist/utils/cacheControlUtils.d.ts +121 -0
  92. package/dist/utils/cacheControlUtils.d.ts.map +1 -0
  93. package/dist/utils/cacheControlUtils.js +367 -0
  94. package/dist/utils/commandPathResolver.d.ts +52 -0
  95. package/dist/utils/commandPathResolver.d.ts.map +1 -0
  96. package/dist/utils/commandPathResolver.js +145 -0
  97. package/dist/utils/configPaths.d.ts +85 -0
  98. package/dist/utils/configPaths.d.ts.map +1 -0
  99. package/dist/utils/configPaths.js +121 -0
  100. package/dist/utils/configResolver.d.ts +37 -10
  101. package/dist/utils/configResolver.d.ts.map +1 -1
  102. package/dist/utils/configResolver.js +127 -23
  103. package/dist/utils/constants.d.ts +1 -1
  104. package/dist/utils/constants.js +1 -1
  105. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  106. package/dist/utils/convertMessagesForAPI.js +8 -13
  107. package/dist/utils/customCommands.d.ts.map +1 -1
  108. package/dist/utils/customCommands.js +66 -21
  109. package/dist/utils/fileUtils.d.ts +15 -0
  110. package/dist/utils/fileUtils.d.ts.map +1 -0
  111. package/dist/utils/fileUtils.js +61 -0
  112. package/dist/utils/globalLogger.d.ts +102 -0
  113. package/dist/utils/globalLogger.d.ts.map +1 -0
  114. package/dist/utils/globalLogger.js +136 -0
  115. package/dist/utils/hookMatcher.d.ts +1 -6
  116. package/dist/utils/hookMatcher.d.ts.map +1 -1
  117. package/dist/utils/mcpUtils.d.ts.map +1 -1
  118. package/dist/utils/mcpUtils.js +25 -3
  119. package/dist/utils/messageOperations.d.ts +27 -27
  120. package/dist/utils/messageOperations.d.ts.map +1 -1
  121. package/dist/utils/messageOperations.js +46 -36
  122. package/dist/utils/pathEncoder.d.ts +104 -0
  123. package/dist/utils/pathEncoder.d.ts.map +1 -0
  124. package/dist/utils/pathEncoder.js +272 -0
  125. package/dist/utils/subagentParser.d.ts.map +1 -1
  126. package/dist/utils/subagentParser.js +2 -1
  127. package/dist/utils/tokenCalculation.d.ts +26 -0
  128. package/dist/utils/tokenCalculation.d.ts.map +1 -0
  129. package/dist/utils/tokenCalculation.js +36 -0
  130. package/package.json +6 -3
  131. package/src/agent.ts +301 -37
  132. package/src/constants/events.ts +38 -0
  133. package/src/index.ts +2 -0
  134. package/src/managers/aiManager.ts +325 -173
  135. package/src/managers/backgroundBashManager.ts +7 -6
  136. package/src/managers/hookManager.ts +106 -84
  137. package/src/managers/liveConfigManager.ts +248 -0
  138. package/src/managers/messageManager.ts +237 -100
  139. package/src/managers/slashCommandManager.ts +9 -7
  140. package/src/managers/subagentManager.ts +284 -22
  141. package/src/services/aiService.ts +474 -83
  142. package/src/services/configurationWatcher.ts +622 -0
  143. package/src/services/fileWatcher.ts +301 -0
  144. package/src/services/hook.ts +538 -47
  145. package/src/services/jsonlHandler.ts +319 -0
  146. package/src/services/memory.ts +92 -12
  147. package/src/services/memoryStore.ts +279 -0
  148. package/src/services/session.ts +381 -157
  149. package/src/tools/bashTool.ts +5 -4
  150. package/src/tools/deleteFileTool.ts +2 -1
  151. package/src/tools/editTool.ts +3 -2
  152. package/src/tools/multiEditTool.ts +4 -3
  153. package/src/tools/readTool.ts +2 -1
  154. package/src/tools/todoWriteTool.ts +3 -11
  155. package/src/tools/writeTool.ts +7 -6
  156. package/src/types/commands.ts +6 -0
  157. package/src/types/core.ts +44 -0
  158. package/src/types/environment.ts +60 -0
  159. package/src/types/hooks.ts +21 -8
  160. package/src/types/index.ts +2 -0
  161. package/src/types/memoryStore.ts +94 -0
  162. package/src/types/messaging.ts +21 -10
  163. package/src/types/session.ts +25 -0
  164. package/src/utils/bashHistory.ts +27 -27
  165. package/src/utils/cacheControlUtils.ts +540 -0
  166. package/src/utils/commandPathResolver.ts +189 -0
  167. package/src/utils/configPaths.ts +163 -0
  168. package/src/utils/configResolver.ts +182 -22
  169. package/src/utils/constants.ts +1 -1
  170. package/src/utils/convertMessagesForAPI.ts +8 -14
  171. package/src/utils/customCommands.ts +90 -22
  172. package/src/utils/fileUtils.ts +65 -0
  173. package/src/utils/globalLogger.ts +145 -0
  174. package/src/utils/hookMatcher.ts +1 -12
  175. package/src/utils/mcpUtils.ts +34 -3
  176. package/src/utils/messageOperations.ts +77 -60
  177. package/src/utils/pathEncoder.ts +379 -0
  178. package/src/utils/subagentParser.ts +2 -1
  179. package/src/utils/tokenCalculation.ts +43 -0
  180. package/src/types/index.ts.backup +0 -357
@@ -1,12 +1,15 @@
1
1
  import { promises as fs } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
+ import { v6 as uuidv6 } from "uuid";
4
5
  import type { Message } from "../types/index.js";
6
+ import type { SessionMessage } from "../types/session.js";
7
+ import { PathEncoder } from "../utils/pathEncoder.js";
8
+ import { JsonlHandler } from "../services/jsonlHandler.js";
9
+ import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
5
10
 
6
11
  export interface SessionData {
7
12
  id: string;
8
- timestamp: string;
9
- version: string;
10
13
  messages: Message[];
11
14
  metadata: {
12
15
  workdir: string;
@@ -18,82 +21,95 @@ export interface SessionData {
18
21
 
19
22
  export interface SessionMetadata {
20
23
  id: string;
21
- timestamp: string;
24
+ sessionType: "main" | "subagent";
25
+ parentSessionId?: string;
26
+ subagentType?: string;
22
27
  workdir: string;
23
- startedAt: string;
24
- lastActiveAt: string;
28
+ startedAt: Date;
29
+ lastActiveAt: Date;
25
30
  latestTotalTokens: number;
26
31
  }
27
32
 
28
- // Constants
29
- const SESSION_DIR = join(homedir(), ".wave", "sessions");
30
- const VERSION = "1.0.0";
31
- const MAX_SESSION_AGE_DAYS = 30;
32
-
33
33
  /**
34
- * Resolve session directory path with fallback to default
35
- * @param sessionDir Optional custom session directory
36
- * @returns Resolved session directory path
34
+ * Generate a new UUIDv6-based session ID
35
+ * @returns UUIDv6 string for time-ordered sessions
37
36
  */
38
- export function resolveSessionDir(sessionDir?: string): string {
39
- return sessionDir || SESSION_DIR;
37
+ export function generateSessionId(): string {
38
+ return uuidv6();
40
39
  }
41
40
 
41
+ // Constants
42
+ export const SESSION_DIR = join(homedir(), ".wave", "projects");
43
+ const MAX_SESSION_AGE_DAYS = 14;
44
+
42
45
  /**
43
46
  * Ensure session directory exists
44
- * @param sessionDir Optional custom session directory
45
47
  */
46
- export async function ensureSessionDir(sessionDir?: string): Promise<void> {
47
- const resolvedDir = resolveSessionDir(sessionDir);
48
+ export async function ensureSessionDir(): Promise<void> {
48
49
  try {
49
- await fs.mkdir(resolvedDir, { recursive: true });
50
+ await fs.mkdir(SESSION_DIR, { recursive: true });
50
51
  } catch (error) {
51
52
  throw new Error(`Failed to create session directory: ${error}`);
52
53
  }
53
54
  }
54
55
 
55
56
  /**
56
- * Generate session file path
57
+ * Generate session file path using project-based directory structure
58
+ * Note: With metadata-based approach, we no longer need separate subagent directories
59
+ * @param sessionId - UUIDv6 session identifier
60
+ * @param workdir - Working directory for the session
61
+ * @returns Promise resolving to full file path for the session JSONL file
57
62
  */
58
- export function getSessionFilePath(
63
+ export async function getSessionFilePath(
59
64
  sessionId: string,
60
- sessionDir?: string,
61
- ): string {
62
- const shortId = sessionId.split("_")[2] || sessionId.slice(-8);
63
- const resolvedDir = resolveSessionDir(sessionDir);
64
- return join(resolvedDir, `session_${shortId}.json`);
65
+ workdir: string,
66
+ ): Promise<string> {
67
+ const encoder = new PathEncoder();
68
+ const projectDir = await encoder.createProjectDirectory(workdir, SESSION_DIR);
69
+
70
+ // All sessions (main and subagent) now go in the same directory
71
+ // Session type is determined by metadata, not file path
72
+ return join(projectDir.encodedPath, `${sessionId}.jsonl`);
65
73
  }
66
74
 
67
75
  /**
68
- * Filter out diff blocks from messages to avoid saving unimportant data
76
+ * Create a new session with metadata
77
+ * @param sessionId - UUIDv6 session identifier
78
+ * @param workdir - Working directory for the session
79
+ * @param sessionType - Type of session ('main' or 'subagent')
80
+ * @param parentSessionId - Parent session ID for subagent sessions
81
+ * @param subagentType - Type of subagent for subagent sessions
69
82
  */
70
- function filterDiffBlocks(messages: Message[]): Message[] {
71
- return messages
72
- .map((message) => ({
73
- ...message,
74
- blocks: message.blocks.filter((block) => block.type !== "diff"),
75
- }))
76
- .filter((message) => message.blocks.length > 0);
83
+ export async function createSession(
84
+ sessionId: string,
85
+ workdir: string,
86
+ sessionType: "main" | "subagent" = "main",
87
+ parentSessionId?: string,
88
+ subagentType?: string,
89
+ ): Promise<void> {
90
+ const jsonlHandler = new JsonlHandler();
91
+ const filePath = await getSessionFilePath(sessionId, workdir);
92
+ await jsonlHandler.createSession(
93
+ filePath,
94
+ sessionId,
95
+ workdir,
96
+ sessionType,
97
+ parentSessionId,
98
+ subagentType,
99
+ );
77
100
  }
78
101
 
79
102
  /**
80
- * Save session data to storage
103
+ * Append messages to session using JSONL format (new approach)
81
104
  *
82
- * @param sessionId - Unique identifier for the session
83
- * @param messages - Array of messages to save
105
+ * @param sessionId - UUIDv6 session identifier
106
+ * @param newMessages - Array of messages to append
84
107
  * @param workdir - Working directory for the session
85
- * @param latestTotalTokens - Total tokens used in the session
86
- * @param startedAt - ISO timestamp when session started (defaults to current time)
87
- * @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
88
- * @throws {Error} When session cannot be saved due to permission or disk space issues
89
108
  */
90
- export async function saveSession(
109
+ export async function appendMessages(
91
110
  sessionId: string,
92
- messages: Message[],
111
+ newMessages: Message[],
93
112
  workdir: string,
94
- latestTotalTokens: number = 0,
95
- startedAt?: string,
96
- sessionDir?: string,
97
113
  ): Promise<void> {
98
114
  // Do not save session files in test environment
99
115
  if (process.env.NODE_ENV === "test") {
@@ -101,170 +117,319 @@ export async function saveSession(
101
117
  }
102
118
 
103
119
  // Do not save if there are no messages
104
- if (messages.length === 0) {
120
+ if (newMessages.length === 0) {
105
121
  return;
106
122
  }
107
123
 
108
- await ensureSessionDir(sessionDir);
109
-
110
- // Filter out diff blocks before saving
111
- const filteredMessages = filterDiffBlocks(messages);
112
-
113
- const now = new Date().toISOString();
114
- const sessionData: SessionData = {
115
- id: sessionId,
116
- timestamp: now,
117
- version: VERSION,
118
- messages: filteredMessages,
119
- metadata: {
120
- workdir: workdir,
121
- startedAt: startedAt || now,
122
- lastActiveAt: now,
123
- latestTotalTokens,
124
- },
125
- };
124
+ const jsonlHandler = new JsonlHandler();
125
+ const filePath = await getSessionFilePath(sessionId, workdir);
126
126
 
127
- const filePath = getSessionFilePath(sessionId, sessionDir);
127
+ // Check if session file exists, throw error if it doesn't
128
128
  try {
129
- await fs.writeFile(filePath, JSON.stringify(sessionData, null, 2), "utf-8");
129
+ await fs.access(filePath);
130
130
  } catch (error) {
131
- throw new Error(`Failed to save session ${sessionId}: ${error}`);
131
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
132
+ // File doesn't exist, throw error - sessions must be created explicitly
133
+ throw new Error(
134
+ `Session file not found: ${sessionId}. Use createSession() to create a new session first.`,
135
+ );
136
+ } else {
137
+ // Some other error accessing the file, re-throw it
138
+ throw error;
139
+ }
132
140
  }
141
+
142
+ const messagesWithTimestamp: SessionMessage[] = newMessages.map((msg) => ({
143
+ timestamp: new Date().toISOString(),
144
+ ...msg,
145
+ }));
146
+
147
+ await jsonlHandler.append(filePath, messagesWithTimestamp, { atomic: false });
133
148
  }
134
149
 
135
150
  /**
136
- * Load session data from storage
151
+ * Load session data from JSONL file (new approach)
137
152
  *
138
- * @param sessionId - Unique identifier for the session to load
139
- * @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
153
+ * @param sessionId - UUIDv6 session identifier
154
+ * @param workdir - Working directory for the session
140
155
  * @returns Promise that resolves to session data or null if session doesn't exist
141
- * @throws {Error} When session exists but cannot be read or contains invalid data
142
156
  */
143
- export async function loadSession(
157
+ export async function loadSessionFromJsonl(
144
158
  sessionId: string,
145
- sessionDir?: string,
159
+ workdir: string,
146
160
  ): Promise<SessionData | null> {
147
- const filePath = getSessionFilePath(sessionId, sessionDir);
148
-
149
161
  try {
150
- const content = await fs.readFile(filePath, "utf-8");
151
- const sessionData = JSON.parse(content) as SessionData;
162
+ const jsonlHandler = new JsonlHandler();
163
+ const filePath = await getSessionFilePath(sessionId, workdir);
152
164
 
153
- // Validate session data format
154
- if (!sessionData.id || !sessionData.messages || !sessionData.metadata) {
155
- throw new Error("Invalid session data format");
165
+ const messages = await jsonlHandler.read(filePath);
166
+
167
+ if (messages.length === 0) {
168
+ return null;
156
169
  }
157
170
 
171
+ // Extract metadata from messages
172
+ const firstMessage = messages[0];
173
+ const lastMessage = messages[messages.length - 1];
174
+
175
+ const sessionData: SessionData = {
176
+ id: sessionId,
177
+ messages: messages.map((msg) => {
178
+ // Remove timestamp property for backward compatibility
179
+ const { timestamp: _ignored, ...messageWithoutTimestamp } = msg;
180
+ void _ignored; // Use the variable to avoid eslint error
181
+ return messageWithoutTimestamp;
182
+ }),
183
+ metadata: {
184
+ workdir,
185
+ startedAt: firstMessage.timestamp,
186
+ lastActiveAt: lastMessage.timestamp,
187
+ latestTotalTokens: lastMessage.usage
188
+ ? extractLatestTotalTokens([lastMessage])
189
+ : 0,
190
+ },
191
+ };
192
+
158
193
  return sessionData;
159
194
  } catch (error) {
160
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
195
+ const errorMessage = error instanceof Error ? error.message : String(error);
196
+
197
+ // Check if the underlying error is ENOENT (file doesn't exist)
198
+ if (
199
+ errorMessage.includes("ENOENT") ||
200
+ (error as NodeJS.ErrnoException).code === "ENOENT"
201
+ ) {
161
202
  return null; // Session file does not exist
162
203
  }
204
+
205
+ // Check for JSON parsing errors (corrupted files)
206
+ if (
207
+ errorMessage.includes("Invalid JSON") ||
208
+ errorMessage.includes("Unexpected token")
209
+ ) {
210
+ return null; // Treat corrupted files as non-existent
211
+ }
212
+
163
213
  throw new Error(`Failed to load session ${sessionId}: ${error}`);
164
214
  }
165
215
  }
166
216
 
167
217
  /**
168
- * Get the most recent session for a specific working directory
218
+ * Get the most recently active session for a specific working directory (new JSONL approach)
219
+ * Only returns main sessions, skips subagent sessions
220
+ * Sessions are sorted by last active time (most recently active first)
169
221
  *
170
- * @param workdir - Working directory to find the most recent session for
171
- * @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
172
- * @returns Promise that resolves to the most recent session data or null if no sessions exist
173
- * @throws {Error} When session directory cannot be accessed or session data is corrupted
222
+ * @param workdir - Working directory to find the most recently active session for
223
+ * @returns Promise that resolves to the most recently active session data or null if no sessions exist
174
224
  */
175
- export async function getLatestSession(
225
+ export async function getLatestSessionFromJsonl(
176
226
  workdir: string,
177
- sessionDir?: string,
178
227
  ): Promise<SessionData | null> {
179
- const sessions = await listSessions(workdir, false, sessionDir);
228
+ const sessions = await listSessionsFromJsonl(workdir, false); // Uses default includeSubagentSessions = false
229
+
180
230
  if (sessions.length === 0) {
181
231
  return null;
182
232
  }
183
233
 
184
- // Sort by last active time, return the latest
234
+ // Sort by last active time (most recently active first)
185
235
  const latestSession = sessions.sort(
186
- (a, b) =>
187
- new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime(),
236
+ (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
188
237
  )[0];
238
+ return loadSessionFromJsonl(latestSession.id, workdir);
239
+ }
189
240
 
190
- return loadSession(latestSession.id, sessionDir);
241
+ /**
242
+ * List all sessions for a specific working directory (convenience wrapper)
243
+ * Only returns main sessions, skips subagent sessions
244
+ *
245
+ * @param workdir - Working directory to filter sessions by
246
+ * @returns Promise that resolves to array of session metadata objects
247
+ */
248
+ export async function listSessions(
249
+ workdir: string,
250
+ ): Promise<SessionMetadata[]> {
251
+ return listSessionsFromJsonl(workdir, false); // Uses default includeSubagentSessions = false
191
252
  }
192
253
 
193
254
  /**
194
- * List all sessions for a specific working directory or across all working directories
255
+ * List all sessions for a specific working directory using JSONL format (new approach)
195
256
  *
196
257
  * @param workdir - Working directory to filter sessions by
197
258
  * @param includeAllWorkdirs - If true, returns sessions from all working directories
198
- * @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
259
+ * @param includeSubagentSessions - If true, includes subagent sessions (default: false for user-facing operations)
199
260
  * @returns Promise that resolves to array of session metadata objects
200
- * @throws {Error} When session directory cannot be accessed or read
201
261
  */
202
- export async function listSessions(
262
+ export async function listSessionsFromJsonl(
203
263
  workdir: string,
204
264
  includeAllWorkdirs = false,
205
- sessionDir?: string,
265
+ includeSubagentSessions = false,
206
266
  ): Promise<SessionMetadata[]> {
207
267
  try {
208
- await ensureSessionDir(sessionDir);
209
- const resolvedDir = resolveSessionDir(sessionDir);
210
- const files = await fs.readdir(resolvedDir);
268
+ const encoder = new PathEncoder();
269
+ const baseDir = SESSION_DIR;
211
270
 
212
- const sessions: SessionMetadata[] = [];
271
+ // If not including all workdirs, just scan the specific project directory
272
+ if (!includeAllWorkdirs) {
273
+ const projectDir = await encoder.createProjectDirectory(workdir, baseDir);
274
+ const files = await fs.readdir(projectDir.encodedPath);
213
275
 
214
- for (const file of files) {
215
- if (!file.startsWith("session_") || !file.endsWith(".json")) {
216
- continue;
276
+ const sessions: SessionMetadata[] = [];
277
+
278
+ for (const file of files) {
279
+ if (!file.endsWith(".jsonl")) {
280
+ continue;
281
+ }
282
+
283
+ const sessionId = file.replace(".jsonl", "");
284
+ try {
285
+ const jsonlHandler = new JsonlHandler();
286
+ const filePath = join(projectDir.encodedPath, file);
287
+
288
+ // Read metadata (efficient O(1) operation)
289
+ const metadata = await jsonlHandler.readMetadata(filePath);
290
+ if (metadata) {
291
+ // For lastActiveAt and latestTotalTokens, we need the last message
292
+ const lastMessage = await jsonlHandler.getLastMessage(filePath);
293
+
294
+ sessions.push({
295
+ id: sessionId,
296
+ sessionType: metadata.sessionType,
297
+ parentSessionId: metadata.parentSessionId,
298
+ subagentType: metadata.subagentType,
299
+ workdir: metadata.workdir,
300
+ startedAt: new Date(metadata.startedAt),
301
+ lastActiveAt: lastMessage
302
+ ? new Date(lastMessage.timestamp)
303
+ : new Date(metadata.startedAt),
304
+ latestTotalTokens: lastMessage?.usage
305
+ ? extractLatestTotalTokens([lastMessage])
306
+ : 0,
307
+ });
308
+ }
309
+ } catch {
310
+ // Skip corrupted session files
311
+ continue;
312
+ }
217
313
  }
218
314
 
219
- try {
220
- const filePath = join(resolvedDir, file);
221
- const content = await fs.readFile(filePath, "utf-8");
222
- const sessionData = JSON.parse(content) as SessionData;
315
+ // Sort by last active time (most recently active first)
316
+ const sortedSessions = sessions.sort(
317
+ (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
318
+ );
223
319
 
224
- // Only return sessions for the current working directory, unless includeAllWorkdirs is true
225
- if (!includeAllWorkdirs && sessionData.metadata.workdir !== workdir) {
320
+ // Filter out subagent sessions if requested
321
+ if (!includeSubagentSessions) {
322
+ return sortedSessions.filter(
323
+ (session) => session.sessionType === "main",
324
+ );
325
+ }
326
+
327
+ return sortedSessions;
328
+ }
329
+
330
+ // For all workdirs, scan all project directories
331
+ const sessions: SessionMetadata[] = [];
332
+ try {
333
+ const projectDirs = await fs.readdir(baseDir);
334
+
335
+ for (const projectDirName of projectDirs) {
336
+ const projectPath = join(baseDir, projectDirName);
337
+ const stat = await fs.stat(projectPath);
338
+
339
+ if (!stat.isDirectory()) {
226
340
  continue;
227
341
  }
228
342
 
229
- sessions.push({
230
- id: sessionData.id,
231
- timestamp: sessionData.timestamp,
232
- workdir: sessionData.metadata.workdir,
233
- startedAt: sessionData.metadata.startedAt,
234
- lastActiveAt: sessionData.metadata.lastActiveAt,
235
- latestTotalTokens: sessionData.metadata.latestTotalTokens,
236
- });
237
- } catch {
238
- // Skip corrupted session files and continue processing others
239
- continue;
343
+ const files = await fs.readdir(projectPath);
344
+
345
+ for (const file of files) {
346
+ if (!file.endsWith(".jsonl")) {
347
+ continue;
348
+ }
349
+
350
+ const sessionId = file.replace(".jsonl", "");
351
+ try {
352
+ const jsonlHandler = new JsonlHandler();
353
+ const filePath = join(projectPath, file);
354
+
355
+ // Read metadata (efficient O(1) operation)
356
+ const metadata = await jsonlHandler.readMetadata(filePath);
357
+ if (metadata) {
358
+ // For lastActiveAt and latestTotalTokens, we need the last message
359
+ const lastMessage = await jsonlHandler.getLastMessage(filePath);
360
+
361
+ sessions.push({
362
+ id: sessionId,
363
+ sessionType: metadata.sessionType,
364
+ parentSessionId: metadata.parentSessionId,
365
+ subagentType: metadata.subagentType,
366
+ workdir: metadata.workdir,
367
+ startedAt: new Date(metadata.startedAt),
368
+ lastActiveAt: lastMessage
369
+ ? new Date(lastMessage.timestamp)
370
+ : new Date(metadata.startedAt),
371
+ latestTotalTokens: lastMessage?.usage
372
+ ? extractLatestTotalTokens([lastMessage])
373
+ : 0,
374
+ });
375
+ }
376
+ } catch {
377
+ // Skip corrupted session files
378
+ continue;
379
+ }
380
+ }
240
381
  }
382
+ } catch {
383
+ // If base directory doesn't exist, return empty array
384
+ return [];
241
385
  }
242
386
 
243
- return sessions.sort(
244
- (a, b) =>
245
- new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime(),
387
+ // Sort by last active time (most recently active first)
388
+ const sortedSessions = sessions.sort(
389
+ (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
246
390
  );
391
+
392
+ // Filter out subagent sessions if requested
393
+ if (!includeSubagentSessions) {
394
+ return sortedSessions.filter((session) => session.sessionType === "main");
395
+ }
396
+
397
+ return sortedSessions;
247
398
  } catch (error) {
248
399
  throw new Error(`Failed to list sessions: ${error}`);
249
400
  }
250
401
  }
251
402
 
252
403
  /**
253
- * Delete a session from storage
404
+ * Delete a session from JSONL storage (new approach)
254
405
  *
255
- * @param sessionId - Unique identifier for the session to delete
256
- * @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
406
+ * @param sessionId - UUIDv6 session identifier
407
+ * @param workdir - Working directory for the session
257
408
  * @returns Promise that resolves to true if session was deleted, false if it didn't exist
258
- * @throws {Error} When session exists but cannot be deleted due to permission issues
259
409
  */
260
- export async function deleteSession(
410
+ export async function deleteSessionFromJsonl(
261
411
  sessionId: string,
262
- sessionDir?: string,
412
+ workdir: string,
263
413
  ): Promise<boolean> {
264
- const filePath = getSessionFilePath(sessionId, sessionDir);
265
-
266
414
  try {
415
+ const filePath = await getSessionFilePath(sessionId, workdir);
267
416
  await fs.unlink(filePath);
417
+
418
+ // Try to clean up empty project directory
419
+ const encoder = new PathEncoder();
420
+ const projectDir = await encoder.createProjectDirectory(
421
+ workdir,
422
+ SESSION_DIR,
423
+ );
424
+ try {
425
+ const files = await fs.readdir(projectDir.encodedPath);
426
+ if (files.length === 0) {
427
+ await fs.rmdir(projectDir.encodedPath);
428
+ }
429
+ } catch {
430
+ // Ignore errors if directory is not empty or can't be removed
431
+ }
432
+
268
433
  return true;
269
434
  } catch (error) {
270
435
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
@@ -275,59 +440,118 @@ export async function deleteSession(
275
440
  }
276
441
 
277
442
  /**
278
- * Clean up expired sessions older than the configured maximum age
443
+ * Clean up expired sessions older than 14 days based on file modification time
279
444
  *
280
445
  * @param workdir - Working directory to clean up sessions for
281
- * @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
282
446
  * @returns Promise that resolves to the number of sessions that were deleted
283
- * @throws {Error} When session directory cannot be accessed or sessions cannot be deleted
284
447
  */
285
- export async function cleanupExpiredSessions(
448
+ export async function cleanupExpiredSessionsFromJsonl(
286
449
  workdir: string,
287
- sessionDir?: string,
288
450
  ): Promise<number> {
289
451
  // Do not perform cleanup operations in test environment
290
452
  if (process.env.NODE_ENV === "test") {
291
453
  return 0;
292
454
  }
293
455
 
294
- const sessions = await listSessions(workdir, true, sessionDir);
295
- const now = new Date();
296
- const maxAge = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000; // Convert to milliseconds
456
+ try {
457
+ const encoder = new PathEncoder();
458
+ const projectDir = await encoder.createProjectDirectory(
459
+ workdir,
460
+ SESSION_DIR,
461
+ );
462
+ const files = await fs.readdir(projectDir.encodedPath);
463
+
464
+ const now = new Date();
465
+ const maxAge = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000; // Convert to milliseconds
466
+ let deletedCount = 0;
297
467
 
298
- let deletedCount = 0;
468
+ for (const file of files) {
469
+ if (!file.endsWith(".jsonl")) {
470
+ continue;
471
+ }
299
472
 
300
- for (const session of sessions) {
301
- const sessionAge = now.getTime() - new Date(session.lastActiveAt).getTime();
473
+ const filePath = join(projectDir.encodedPath, file);
302
474
 
303
- if (sessionAge > maxAge) {
304
475
  try {
305
- await deleteSession(session.id, sessionDir);
306
- deletedCount++;
476
+ const stat = await fs.stat(filePath);
477
+ const fileAge = now.getTime() - stat.mtime.getTime();
478
+
479
+ if (fileAge > maxAge) {
480
+ await fs.unlink(filePath);
481
+ deletedCount++;
482
+ }
307
483
  } catch {
308
- // Skip failed deletions and continue processing other sessions
484
+ // Skip failed operations and continue processing other files
309
485
  continue;
310
486
  }
311
487
  }
488
+
489
+ // Clean up empty project directory if no files remain
490
+ try {
491
+ const remainingFiles = await fs.readdir(projectDir.encodedPath);
492
+ if (remainingFiles.length === 0) {
493
+ await fs.rmdir(projectDir.encodedPath);
494
+ }
495
+ } catch {
496
+ // Ignore errors if directory is not empty or can't be removed
497
+ }
498
+
499
+ return deletedCount;
500
+ } catch {
501
+ // Return 0 if project directory doesn't exist or can't be accessed
502
+ return 0;
312
503
  }
504
+ }
313
505
 
314
- return deletedCount;
506
+ /**
507
+ * Clean up empty project directories in the session directory
508
+ */
509
+ export async function cleanupEmptyProjectDirectories(): Promise<void> {
510
+ // Do not perform cleanup operations in test environment
511
+ if (process.env.NODE_ENV === "test") {
512
+ return;
513
+ }
514
+
515
+ try {
516
+ const baseDir = SESSION_DIR;
517
+ const projectDirs = await fs.readdir(baseDir);
518
+
519
+ for (const projectDirName of projectDirs) {
520
+ const projectPath = join(baseDir, projectDirName);
521
+ const stat = await fs.stat(projectPath);
522
+
523
+ if (stat.isDirectory()) {
524
+ try {
525
+ const files = await fs.readdir(projectPath);
526
+
527
+ // If directory is empty, remove it
528
+ if (files.length === 0) {
529
+ await fs.rmdir(projectPath);
530
+ }
531
+ } catch {
532
+ // Skip errors for directories we can't read or remove
533
+ continue;
534
+ }
535
+ }
536
+ }
537
+ } catch {
538
+ // Ignore errors if base directory doesn't exist or can't be accessed
539
+ }
315
540
  }
316
541
 
317
542
  /**
318
- * Check if a session exists in storage
543
+ * Check if a session exists in JSONL storage (new approach)
319
544
  *
320
- * @param sessionId - Unique identifier for the session to check
321
- * @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
545
+ * @param sessionId - UUIDv6 session identifier
546
+ * @param workdir - Working directory for the session
322
547
  * @returns Promise that resolves to true if session exists, false otherwise
323
548
  */
324
- export async function sessionExists(
549
+ export async function sessionExistsInJsonl(
325
550
  sessionId: string,
326
- sessionDir?: string,
551
+ workdir: string,
327
552
  ): Promise<boolean> {
328
- const filePath = getSessionFilePath(sessionId, sessionDir);
329
-
330
553
  try {
554
+ const filePath = await getSessionFilePath(sessionId, workdir);
331
555
  await fs.access(filePath);
332
556
  return true;
333
557
  } catch {