wave-agent-sdk 0.0.7 → 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.
- package/dist/agent.d.ts +32 -20
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +202 -20
- package/dist/constants/events.d.ts +28 -0
- package/dist/constants/events.d.ts.map +1 -0
- package/dist/constants/events.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/managers/aiManager.d.ts +34 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +243 -128
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/managers/backgroundBashManager.js +7 -6
- package/dist/managers/hookManager.d.ts +9 -4
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +62 -30
- package/dist/managers/liveConfigManager.d.ts +58 -0
- package/dist/managers/liveConfigManager.d.ts.map +1 -0
- package/dist/managers/liveConfigManager.js +160 -0
- package/dist/managers/messageManager.d.ts +38 -13
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +163 -30
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +4 -1
- package/dist/managers/subagentManager.d.ts +51 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +189 -18
- package/dist/services/aiService.d.ts +13 -5
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +350 -74
- package/dist/services/configurationWatcher.d.ts +120 -0
- package/dist/services/configurationWatcher.d.ts.map +1 -0
- package/dist/services/configurationWatcher.js +439 -0
- package/dist/services/fileWatcher.d.ts +69 -0
- package/dist/services/fileWatcher.d.ts.map +1 -0
- package/dist/services/fileWatcher.js +213 -0
- package/dist/services/hook.d.ts +91 -9
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +393 -43
- package/dist/services/jsonlHandler.d.ts +62 -0
- package/dist/services/jsonlHandler.d.ts.map +1 -0
- package/dist/services/jsonlHandler.js +257 -0
- package/dist/services/memory.d.ts +9 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +81 -12
- package/dist/services/memoryStore.d.ts +81 -0
- package/dist/services/memoryStore.d.ts.map +1 -0
- package/dist/services/memoryStore.js +200 -0
- package/dist/services/session.d.ts +64 -49
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +310 -132
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +5 -4
- package/dist/tools/deleteFileTool.d.ts.map +1 -1
- package/dist/tools/deleteFileTool.js +2 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +3 -2
- package/dist/tools/multiEditTool.d.ts.map +1 -1
- package/dist/tools/multiEditTool.js +4 -3
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +2 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +5 -6
- package/dist/types/commands.d.ts +4 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/core.d.ts +35 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/environment.d.ts +42 -0
- package/dist/types/environment.d.ts.map +1 -0
- package/dist/types/environment.js +21 -0
- package/dist/types/hooks.d.ts +8 -2
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +8 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/memoryStore.d.ts +82 -0
- package/dist/types/memoryStore.d.ts.map +1 -0
- package/dist/types/memoryStore.js +7 -0
- package/dist/types/messaging.d.ts +14 -2
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/session.d.ts +20 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +7 -0
- package/dist/utils/bashHistory.d.ts.map +1 -1
- package/dist/utils/bashHistory.js +27 -26
- package/dist/utils/cacheControlUtils.d.ts +121 -0
- package/dist/utils/cacheControlUtils.d.ts.map +1 -0
- package/dist/utils/cacheControlUtils.js +367 -0
- package/dist/utils/commandPathResolver.d.ts +52 -0
- package/dist/utils/commandPathResolver.d.ts.map +1 -0
- package/dist/utils/commandPathResolver.js +145 -0
- package/dist/utils/configPaths.d.ts +85 -0
- package/dist/utils/configPaths.d.ts.map +1 -0
- package/dist/utils/configPaths.js +121 -0
- package/dist/utils/configResolver.d.ts +37 -10
- package/dist/utils/configResolver.d.ts.map +1 -1
- package/dist/utils/configResolver.js +127 -23
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +7 -5
- package/dist/utils/customCommands.d.ts.map +1 -1
- package/dist/utils/customCommands.js +66 -21
- package/dist/utils/fileUtils.d.ts +15 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +61 -0
- package/dist/utils/globalLogger.d.ts +102 -0
- package/dist/utils/globalLogger.d.ts.map +1 -0
- package/dist/utils/globalLogger.js +136 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +25 -3
- package/dist/utils/messageOperations.d.ts +20 -8
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +25 -16
- package/dist/utils/pathEncoder.d.ts +104 -0
- package/dist/utils/pathEncoder.d.ts.map +1 -0
- package/dist/utils/pathEncoder.js +272 -0
- package/dist/utils/subagentParser.d.ts.map +1 -1
- package/dist/utils/subagentParser.js +2 -1
- package/dist/utils/tokenCalculation.d.ts +26 -0
- package/dist/utils/tokenCalculation.d.ts.map +1 -0
- package/dist/utils/tokenCalculation.js +36 -0
- package/package.json +6 -3
- package/src/agent.ts +298 -34
- package/src/constants/events.ts +38 -0
- package/src/index.ts +2 -0
- package/src/managers/aiManager.ts +323 -170
- package/src/managers/backgroundBashManager.ts +7 -6
- package/src/managers/hookManager.ts +83 -40
- package/src/managers/liveConfigManager.ts +248 -0
- package/src/managers/messageManager.ts +230 -63
- package/src/managers/slashCommandManager.ts +4 -1
- package/src/managers/subagentManager.ts +283 -21
- package/src/services/aiService.ts +474 -83
- package/src/services/configurationWatcher.ts +622 -0
- package/src/services/fileWatcher.ts +301 -0
- package/src/services/hook.ts +538 -47
- package/src/services/jsonlHandler.ts +319 -0
- package/src/services/memory.ts +92 -12
- package/src/services/memoryStore.ts +279 -0
- package/src/services/session.ts +381 -157
- package/src/tools/bashTool.ts +5 -4
- package/src/tools/deleteFileTool.ts +2 -1
- package/src/tools/editTool.ts +3 -2
- package/src/tools/multiEditTool.ts +4 -3
- package/src/tools/readTool.ts +2 -1
- package/src/tools/writeTool.ts +7 -6
- package/src/types/commands.ts +6 -0
- package/src/types/core.ts +44 -0
- package/src/types/environment.ts +60 -0
- package/src/types/hooks.ts +21 -8
- package/src/types/index.ts +2 -0
- package/src/types/memoryStore.ts +94 -0
- package/src/types/messaging.ts +14 -2
- package/src/types/session.ts +25 -0
- package/src/utils/bashHistory.ts +27 -27
- package/src/utils/cacheControlUtils.ts +540 -0
- package/src/utils/commandPathResolver.ts +189 -0
- package/src/utils/configPaths.ts +163 -0
- package/src/utils/configResolver.ts +182 -22
- package/src/utils/constants.ts +1 -1
- package/src/utils/convertMessagesForAPI.ts +7 -5
- package/src/utils/customCommands.ts +90 -22
- package/src/utils/fileUtils.ts +65 -0
- package/src/utils/globalLogger.ts +145 -0
- package/src/utils/mcpUtils.ts +34 -3
- package/src/utils/messageOperations.ts +42 -20
- package/src/utils/pathEncoder.ts +379 -0
- package/src/utils/subagentParser.ts +2 -1
- package/src/utils/tokenCalculation.ts +43 -0
package/src/services/session.ts
CHANGED
|
@@ -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
|
-
|
|
24
|
+
sessionType: "main" | "subagent";
|
|
25
|
+
parentSessionId?: string;
|
|
26
|
+
subagentType?: string;
|
|
22
27
|
workdir: string;
|
|
23
|
-
startedAt:
|
|
24
|
-
lastActiveAt:
|
|
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
|
-
*
|
|
35
|
-
* @
|
|
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
|
|
39
|
-
return
|
|
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(
|
|
47
|
-
const resolvedDir = resolveSessionDir(sessionDir);
|
|
48
|
+
export async function ensureSessionDir(): Promise<void> {
|
|
48
49
|
try {
|
|
49
|
-
await fs.mkdir(
|
|
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
|
-
|
|
61
|
-
): string {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
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
|
-
*
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
*
|
|
103
|
+
* Append messages to session using JSONL format (new approach)
|
|
81
104
|
*
|
|
82
|
-
* @param sessionId -
|
|
83
|
-
* @param
|
|
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
|
|
109
|
+
export async function appendMessages(
|
|
91
110
|
sessionId: string,
|
|
92
|
-
|
|
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 (
|
|
120
|
+
if (newMessages.length === 0) {
|
|
105
121
|
return;
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
|
|
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
|
-
|
|
127
|
+
// Check if session file exists, throw error if it doesn't
|
|
128
128
|
try {
|
|
129
|
-
await fs.
|
|
129
|
+
await fs.access(filePath);
|
|
130
130
|
} catch (error) {
|
|
131
|
-
|
|
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
|
|
151
|
+
* Load session data from JSONL file (new approach)
|
|
137
152
|
*
|
|
138
|
-
* @param sessionId -
|
|
139
|
-
* @param
|
|
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
|
|
157
|
+
export async function loadSessionFromJsonl(
|
|
144
158
|
sessionId: string,
|
|
145
|
-
|
|
159
|
+
workdir: string,
|
|
146
160
|
): Promise<SessionData | null> {
|
|
147
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
148
|
-
|
|
149
161
|
try {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
162
|
+
const jsonlHandler = new JsonlHandler();
|
|
163
|
+
const filePath = await getSessionFilePath(sessionId, workdir);
|
|
152
164
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
171
|
-
* @
|
|
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
|
|
225
|
+
export async function getLatestSessionFromJsonl(
|
|
176
226
|
workdir: string,
|
|
177
|
-
sessionDir?: string,
|
|
178
227
|
): Promise<SessionData | null> {
|
|
179
|
-
const sessions = await
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
262
|
+
export async function listSessionsFromJsonl(
|
|
203
263
|
workdir: string,
|
|
204
264
|
includeAllWorkdirs = false,
|
|
205
|
-
|
|
265
|
+
includeSubagentSessions = false,
|
|
206
266
|
): Promise<SessionMetadata[]> {
|
|
207
267
|
try {
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
const files = await fs.readdir(resolvedDir);
|
|
268
|
+
const encoder = new PathEncoder();
|
|
269
|
+
const baseDir = SESSION_DIR;
|
|
211
270
|
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 -
|
|
256
|
-
* @param
|
|
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
|
|
410
|
+
export async function deleteSessionFromJsonl(
|
|
261
411
|
sessionId: string,
|
|
262
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
if (!file.endsWith(".jsonl")) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
299
472
|
|
|
300
|
-
|
|
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
|
|
306
|
-
|
|
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
|
|
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
|
-
|
|
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 -
|
|
321
|
-
* @param
|
|
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
|
|
549
|
+
export async function sessionExistsInJsonl(
|
|
325
550
|
sessionId: string,
|
|
326
|
-
|
|
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 {
|