wave-agent-sdk 0.0.7 → 0.0.10
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 +105 -24
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +438 -53
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/managers/aiManager.d.ts +18 -7
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +254 -142
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/managers/backgroundBashManager.js +11 -9
- package/dist/managers/hookManager.d.ts +6 -6
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +81 -39
- package/dist/managers/liveConfigManager.d.ts +95 -0
- package/dist/managers/liveConfigManager.d.ts.map +1 -0
- package/dist/managers/liveConfigManager.js +442 -0
- package/dist/managers/lspManager.d.ts +43 -0
- package/dist/managers/lspManager.d.ts.map +1 -0
- package/dist/managers/lspManager.js +326 -0
- package/dist/managers/messageManager.d.ts +41 -24
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +184 -73
- package/dist/managers/permissionManager.d.ts +66 -0
- package/dist/managers/permissionManager.d.ts.map +1 -0
- package/dist/managers/permissionManager.js +208 -0
- package/dist/managers/skillManager.d.ts +1 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +2 -1
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +4 -2
- package/dist/managers/subagentManager.d.ts +42 -6
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +213 -62
- package/dist/managers/toolManager.d.ts +38 -1
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +66 -2
- package/dist/services/aiService.d.ts +15 -5
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +446 -77
- package/dist/services/configurationService.d.ts +116 -0
- package/dist/services/configurationService.d.ts.map +1 -0
- package/dist/services/configurationService.js +585 -0
- package/dist/services/fileWatcher.d.ts +69 -0
- package/dist/services/fileWatcher.d.ts.map +1 -0
- package/dist/services/fileWatcher.js +212 -0
- package/dist/services/hook.d.ts +5 -40
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +47 -109
- package/dist/services/jsonlHandler.d.ts +71 -0
- package/dist/services/jsonlHandler.d.ts.map +1 -0
- package/dist/services/jsonlHandler.js +236 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +33 -11
- package/dist/services/session.d.ts +116 -52
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +415 -143
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +77 -17
- package/dist/tools/deleteFileTool.d.ts.map +1 -1
- package/dist/tools/deleteFileTool.js +27 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +33 -8
- package/dist/tools/lspTool.d.ts +6 -0
- package/dist/tools/lspTool.d.ts.map +1 -0
- package/dist/tools/lspTool.js +589 -0
- package/dist/tools/multiEditTool.d.ts.map +1 -1
- package/dist/tools/multiEditTool.js +30 -10
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +113 -3
- package/dist/tools/skillTool.js +2 -2
- package/dist/tools/todoWriteTool.d.ts.map +1 -1
- package/dist/tools/todoWriteTool.js +23 -0
- package/dist/tools/types.d.ts +11 -8
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +30 -15
- package/dist/types/commands.d.ts +4 -1
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/config.d.ts +4 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +69 -0
- package/dist/types/configuration.d.ts.map +1 -0
- package/dist/types/configuration.js +8 -0
- package/dist/types/core.d.ts +45 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/environment.d.ts +83 -0
- package/dist/types/environment.d.ts.map +1 -0
- package/dist/types/environment.js +21 -0
- package/dist/types/fileSearch.d.ts +5 -0
- package/dist/types/fileSearch.d.ts.map +1 -0
- package/dist/types/fileSearch.js +1 -0
- package/dist/types/hooks.d.ts +18 -3
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +8 -8
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +7 -0
- package/dist/types/lsp.d.ts +90 -0
- package/dist/types/lsp.d.ts.map +1 -0
- package/dist/types/lsp.js +4 -0
- package/dist/types/messaging.d.ts +19 -12
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/permissions.d.ts +35 -0
- package/dist/types/permissions.d.ts.map +1 -0
- package/dist/types/permissions.js +12 -0
- package/dist/types/session.d.ts +15 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +7 -0
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/tools.d.ts +35 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +4 -0
- package/dist/utils/abortUtils.d.ts +34 -0
- package/dist/utils/abortUtils.d.ts.map +1 -0
- package/dist/utils/abortUtils.js +92 -0
- package/dist/utils/bashHistory.d.ts +4 -0
- package/dist/utils/bashHistory.d.ts.map +1 -1
- package/dist/utils/bashHistory.js +48 -30
- package/dist/utils/builtinSubagents.d.ts +7 -0
- package/dist/utils/builtinSubagents.d.ts.map +1 -0
- package/dist/utils/builtinSubagents.js +65 -0
- package/dist/utils/cacheControlUtils.d.ts +96 -0
- package/dist/utils/cacheControlUtils.d.ts.map +1 -0
- package/dist/utils/cacheControlUtils.js +324 -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/constants.d.ts +1 -13
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +2 -14
- package/dist/utils/convertMessagesForAPI.d.ts +2 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +39 -18
- package/dist/utils/customCommands.d.ts.map +1 -1
- package/dist/utils/customCommands.js +66 -21
- package/dist/utils/fileSearch.d.ts +14 -0
- package/dist/utils/fileSearch.d.ts.map +1 -0
- package/dist/utils/fileSearch.js +88 -0
- package/dist/utils/fileUtils.d.ts +27 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +145 -0
- package/dist/utils/globalLogger.d.ts +88 -0
- package/dist/utils/globalLogger.d.ts.map +1 -0
- package/dist/utils/globalLogger.js +120 -0
- package/dist/utils/largeOutputHandler.d.ts +15 -0
- package/dist/utils/largeOutputHandler.d.ts.map +1 -0
- package/dist/utils/largeOutputHandler.js +40 -0
- package/dist/utils/markdownParser.d.ts.map +1 -1
- package/dist/utils/markdownParser.js +1 -17
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +25 -3
- package/dist/utils/messageOperations.d.ts +20 -18
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +30 -38
- package/dist/utils/pathEncoder.d.ts +108 -0
- package/dist/utils/pathEncoder.d.ts.map +1 -0
- package/dist/utils/pathEncoder.js +279 -0
- package/dist/utils/subagentParser.d.ts +2 -2
- package/dist/utils/subagentParser.d.ts.map +1 -1
- package/dist/utils/subagentParser.js +12 -8
- 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/dist/utils/tokenEstimator.d.ts +39 -0
- package/dist/utils/tokenEstimator.d.ts.map +1 -0
- package/dist/utils/tokenEstimator.js +55 -0
- package/package.json +6 -6
- package/src/agent.ts +586 -78
- package/src/index.ts +4 -0
- package/src/managers/aiManager.ts +341 -192
- package/src/managers/backgroundBashManager.ts +11 -9
- package/src/managers/hookManager.ts +102 -54
- package/src/managers/liveConfigManager.ts +634 -0
- package/src/managers/lspManager.ts +434 -0
- package/src/managers/messageManager.ts +258 -121
- package/src/managers/permissionManager.ts +276 -0
- package/src/managers/skillManager.ts +3 -1
- package/src/managers/slashCommandManager.ts +5 -3
- package/src/managers/subagentManager.ts +295 -76
- package/src/managers/toolManager.ts +95 -3
- package/src/services/aiService.ts +656 -84
- package/src/services/configurationService.ts +762 -0
- package/src/services/fileWatcher.ts +300 -0
- package/src/services/hook.ts +54 -144
- package/src/services/jsonlHandler.ts +303 -0
- package/src/services/memory.ts +34 -11
- package/src/services/session.ts +522 -173
- package/src/tools/bashTool.ts +94 -20
- package/src/tools/deleteFileTool.ts +38 -1
- package/src/tools/editTool.ts +44 -9
- package/src/tools/lspTool.ts +760 -0
- package/src/tools/multiEditTool.ts +41 -11
- package/src/tools/readTool.ts +127 -3
- package/src/tools/skillTool.ts +2 -2
- package/src/tools/todoWriteTool.ts +33 -1
- package/src/tools/types.ts +15 -9
- package/src/tools/writeTool.ts +43 -16
- package/src/types/commands.ts +6 -1
- package/src/types/config.ts +5 -0
- package/src/types/configuration.ts +73 -0
- package/src/types/core.ts +55 -0
- package/src/types/environment.ts +104 -0
- package/src/types/fileSearch.ts +4 -0
- package/src/types/hooks.ts +32 -16
- package/src/types/index.ts +7 -0
- package/src/types/lsp.ts +96 -0
- package/src/types/messaging.ts +21 -14
- package/src/types/permissions.ts +48 -0
- package/src/types/session.ts +20 -0
- package/src/types/skills.ts +1 -0
- package/src/types/tools.ts +38 -0
- package/src/utils/abortUtils.ts +118 -0
- package/src/utils/bashHistory.ts +55 -31
- package/src/utils/builtinSubagents.ts +71 -0
- package/src/utils/cacheControlUtils.ts +475 -0
- package/src/utils/commandPathResolver.ts +189 -0
- package/src/utils/configPaths.ts +163 -0
- package/src/utils/constants.ts +2 -17
- package/src/utils/convertMessagesForAPI.ts +44 -18
- package/src/utils/customCommands.ts +90 -22
- package/src/utils/fileSearch.ts +107 -0
- package/src/utils/fileUtils.ts +160 -0
- package/src/utils/globalLogger.ts +128 -0
- package/src/utils/largeOutputHandler.ts +55 -0
- package/src/utils/markdownParser.ts +1 -19
- package/src/utils/mcpUtils.ts +34 -3
- package/src/utils/messageOperations.ts +47 -53
- package/src/utils/pathEncoder.ts +394 -0
- package/src/utils/subagentParser.ts +13 -9
- package/src/utils/tokenCalculation.ts +43 -0
- package/src/utils/tokenEstimator.ts +68 -0
- package/dist/utils/configResolver.d.ts +0 -38
- package/dist/utils/configResolver.d.ts.map +0 -1
- package/dist/utils/configResolver.js +0 -106
- package/src/utils/configResolver.ts +0 -142
package/dist/services/session.js
CHANGED
|
@@ -1,251 +1,523 @@
|
|
|
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
|
+
*/
|
|
1
17
|
import { promises as fs } from "fs";
|
|
2
18
|
import { join } from "path";
|
|
3
19
|
import { homedir } from "os";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
import { PathEncoder } from "../utils/pathEncoder.js";
|
|
22
|
+
import { JsonlHandler } from "../services/jsonlHandler.js";
|
|
23
|
+
import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
|
|
24
|
+
import { logger } from "../utils/globalLogger.js";
|
|
8
25
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @
|
|
11
|
-
* @returns Resolved session directory path
|
|
26
|
+
* Generate a new session ID using Node.js native crypto.randomUUID()
|
|
27
|
+
* @returns UUID string for session identification
|
|
12
28
|
*/
|
|
13
|
-
export function
|
|
14
|
-
return
|
|
29
|
+
export function generateSessionId() {
|
|
30
|
+
return randomUUID();
|
|
15
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate filename for subagent sessions
|
|
34
|
+
* @param sessionId - UUID session identifier
|
|
35
|
+
* @returns Filename with subagent prefix for subagent sessions
|
|
36
|
+
*/
|
|
37
|
+
export function generateSubagentFilename(sessionId) {
|
|
38
|
+
return `subagent-${sessionId}.jsonl`;
|
|
39
|
+
}
|
|
40
|
+
// Constants
|
|
41
|
+
export const SESSION_DIR = join(homedir(), ".wave", "projects");
|
|
42
|
+
const MAX_SESSION_AGE_DAYS = 14;
|
|
16
43
|
/**
|
|
17
44
|
* Ensure session directory exists
|
|
18
|
-
* @param sessionDir Optional custom session directory
|
|
19
45
|
*/
|
|
20
|
-
export async function ensureSessionDir(
|
|
21
|
-
const resolvedDir = resolveSessionDir(sessionDir);
|
|
46
|
+
export async function ensureSessionDir() {
|
|
22
47
|
try {
|
|
23
|
-
await fs.mkdir(
|
|
48
|
+
await fs.mkdir(SESSION_DIR, { recursive: true });
|
|
24
49
|
}
|
|
25
50
|
catch (error) {
|
|
26
51
|
throw new Error(`Failed to create session directory: ${error}`);
|
|
27
52
|
}
|
|
28
53
|
}
|
|
29
54
|
/**
|
|
30
|
-
* Generate session file path
|
|
55
|
+
* Generate session file path without creating directories
|
|
56
|
+
* @param sessionId - UUID session identifier
|
|
57
|
+
* @param workdir - Working directory for the session
|
|
58
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
59
|
+
* @returns Promise resolving to full file path for the session JSONL file
|
|
31
60
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
61
|
+
export async function generateSessionFilePath(sessionId, workdir, sessionType = "main") {
|
|
62
|
+
const encoder = new PathEncoder();
|
|
63
|
+
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
64
|
+
// Generate filename based on session type
|
|
65
|
+
const jsonlHandler = new JsonlHandler();
|
|
66
|
+
const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
|
|
67
|
+
return join(projectDir.encodedPath, filename);
|
|
36
68
|
}
|
|
37
69
|
/**
|
|
38
|
-
*
|
|
70
|
+
* Generate session file path using project-based directory structure
|
|
71
|
+
* @param sessionId - UUID session identifier
|
|
72
|
+
* @param workdir - Working directory for the session
|
|
73
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
74
|
+
* @returns Promise resolving to full file path for the session JSONL file
|
|
75
|
+
*/
|
|
76
|
+
export async function getSessionFilePath(sessionId, workdir, sessionType = "main") {
|
|
77
|
+
const encoder = new PathEncoder();
|
|
78
|
+
const projectDir = await encoder.createProjectDirectory(workdir, SESSION_DIR);
|
|
79
|
+
// Generate filename based on session type
|
|
80
|
+
const jsonlHandler = new JsonlHandler();
|
|
81
|
+
const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
|
|
82
|
+
return join(projectDir.encodedPath, filename);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a new session
|
|
86
|
+
* @param sessionId - UUID session identifier
|
|
87
|
+
* @param workdir - Working directory for the session
|
|
88
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
39
89
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
blocks: message.blocks.filter((block) => block.type !== "diff"),
|
|
45
|
-
}))
|
|
46
|
-
.filter((message) => message.blocks.length > 0);
|
|
90
|
+
export async function createSession(sessionId, workdir, sessionType = "main") {
|
|
91
|
+
const jsonlHandler = new JsonlHandler();
|
|
92
|
+
const filePath = await getSessionFilePath(sessionId, workdir, sessionType);
|
|
93
|
+
await jsonlHandler.createSession(filePath);
|
|
47
94
|
}
|
|
48
95
|
/**
|
|
49
|
-
*
|
|
96
|
+
* Append messages to session using JSONL format (new approach)
|
|
50
97
|
*
|
|
51
|
-
* @param sessionId -
|
|
52
|
-
* @param
|
|
98
|
+
* @param sessionId - UUID session identifier
|
|
99
|
+
* @param newMessages - Array of messages to append
|
|
53
100
|
* @param workdir - Working directory for the session
|
|
54
|
-
* @param
|
|
55
|
-
* @param startedAt - ISO timestamp when session started (defaults to current time)
|
|
56
|
-
* @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
|
|
57
|
-
* @throws {Error} When session cannot be saved due to permission or disk space issues
|
|
101
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
58
102
|
*/
|
|
59
|
-
export async function
|
|
103
|
+
export async function appendMessages(sessionId, newMessages, workdir, sessionType = "main") {
|
|
60
104
|
// Do not save session files in test environment
|
|
61
105
|
if (process.env.NODE_ENV === "test") {
|
|
62
106
|
return;
|
|
63
107
|
}
|
|
64
108
|
// Do not save if there are no messages
|
|
65
|
-
if (
|
|
109
|
+
if (newMessages.length === 0) {
|
|
66
110
|
return;
|
|
67
111
|
}
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const sessionData = {
|
|
73
|
-
id: sessionId,
|
|
74
|
-
timestamp: now,
|
|
75
|
-
version: VERSION,
|
|
76
|
-
messages: filteredMessages,
|
|
77
|
-
metadata: {
|
|
78
|
-
workdir: workdir,
|
|
79
|
-
startedAt: startedAt || now,
|
|
80
|
-
lastActiveAt: now,
|
|
81
|
-
latestTotalTokens,
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
112
|
+
const jsonlHandler = new JsonlHandler();
|
|
113
|
+
// Generate the session file path directly using known session type
|
|
114
|
+
const filePath = await generateSessionFilePath(sessionId, workdir, sessionType);
|
|
115
|
+
// Check if the session file exists
|
|
85
116
|
try {
|
|
86
|
-
await fs.
|
|
117
|
+
await fs.access(filePath);
|
|
87
118
|
}
|
|
88
|
-
catch
|
|
89
|
-
throw new Error(`
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error(`Session file not found: ${sessionId}. Use createSession() to create a new session first.`);
|
|
90
121
|
}
|
|
122
|
+
const messagesWithTimestamp = newMessages.map((msg) => ({
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
...msg,
|
|
125
|
+
}));
|
|
126
|
+
await jsonlHandler.append(filePath, messagesWithTimestamp, {
|
|
127
|
+
atomic: false,
|
|
128
|
+
});
|
|
91
129
|
}
|
|
92
130
|
/**
|
|
93
|
-
* Load session data from
|
|
131
|
+
* Load session data from JSONL file (new approach)
|
|
94
132
|
*
|
|
95
|
-
* @param sessionId -
|
|
96
|
-
* @param
|
|
133
|
+
* @param sessionId - UUID session identifier
|
|
134
|
+
* @param workdir - Working directory for the session
|
|
135
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
97
136
|
* @returns Promise that resolves to session data or null if session doesn't exist
|
|
98
|
-
* @throws {Error} When session exists but cannot be read or contains invalid data
|
|
99
137
|
*/
|
|
100
|
-
export async function
|
|
101
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
138
|
+
export async function loadSessionFromJsonl(sessionId, workdir, sessionType = "main") {
|
|
102
139
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
const jsonlHandler = new JsonlHandler();
|
|
141
|
+
// Generate the session file path directly using known session type
|
|
142
|
+
const filePath = await generateSessionFilePath(sessionId, workdir, sessionType);
|
|
143
|
+
const messages = await jsonlHandler.read(filePath);
|
|
144
|
+
if (messages.length === 0) {
|
|
145
|
+
return null;
|
|
108
146
|
}
|
|
147
|
+
// Extract metadata from messages
|
|
148
|
+
const lastMessage = messages[messages.length - 1];
|
|
149
|
+
const sessionData = {
|
|
150
|
+
id: sessionId,
|
|
151
|
+
messages: messages.map((msg) => {
|
|
152
|
+
// Remove timestamp property for backward compatibility
|
|
153
|
+
const { timestamp: _ignored, ...messageWithoutTimestamp } = msg;
|
|
154
|
+
void _ignored; // Use the variable to avoid eslint error
|
|
155
|
+
return messageWithoutTimestamp;
|
|
156
|
+
}),
|
|
157
|
+
metadata: {
|
|
158
|
+
workdir,
|
|
159
|
+
lastActiveAt: lastMessage.timestamp,
|
|
160
|
+
latestTotalTokens: lastMessage.usage
|
|
161
|
+
? extractLatestTotalTokens([lastMessage])
|
|
162
|
+
: 0,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
109
165
|
return sessionData;
|
|
110
166
|
}
|
|
111
167
|
catch (error) {
|
|
112
|
-
|
|
168
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
169
|
+
// Check if the underlying error is ENOENT (file doesn't exist)
|
|
170
|
+
if (errorMessage.includes("ENOENT") ||
|
|
171
|
+
error.code === "ENOENT") {
|
|
113
172
|
return null; // Session file does not exist
|
|
114
173
|
}
|
|
174
|
+
// Check for JSON parsing errors (corrupted files)
|
|
175
|
+
if (errorMessage.includes("Invalid JSON") ||
|
|
176
|
+
errorMessage.includes("Unexpected token")) {
|
|
177
|
+
return null; // Treat corrupted files as non-existent
|
|
178
|
+
}
|
|
115
179
|
throw new Error(`Failed to load session ${sessionId}: ${error}`);
|
|
116
180
|
}
|
|
117
181
|
}
|
|
118
182
|
/**
|
|
119
|
-
* Get the most
|
|
183
|
+
* Get the most recently active session for a specific working directory (new JSONL approach)
|
|
184
|
+
* Only returns main sessions, skips subagent sessions
|
|
185
|
+
* Uses listSessionsFromJsonl which already sorts sessions by last active time (most recent first)
|
|
120
186
|
*
|
|
121
|
-
* @param workdir - Working directory to find the most
|
|
122
|
-
* @
|
|
123
|
-
* @returns Promise that resolves to the most recent session data or null if no sessions exist
|
|
124
|
-
* @throws {Error} When session directory cannot be accessed or session data is corrupted
|
|
187
|
+
* @param workdir - Working directory to find the most recently active session for
|
|
188
|
+
* @returns Promise that resolves to the most recently active session data or null if no sessions exist
|
|
125
189
|
*/
|
|
126
|
-
export async function
|
|
127
|
-
const sessions = await
|
|
190
|
+
export async function getLatestSessionFromJsonl(workdir) {
|
|
191
|
+
const sessions = await listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
|
|
128
192
|
if (sessions.length === 0) {
|
|
129
193
|
return null;
|
|
130
194
|
}
|
|
131
|
-
//
|
|
132
|
-
const latestSession = sessions
|
|
133
|
-
return
|
|
195
|
+
// Sessions are already sorted by lastActiveAt from listSessionsFromJsonl (most recent first)
|
|
196
|
+
const latestSession = sessions[0];
|
|
197
|
+
return loadSessionFromJsonl(latestSession.id, workdir);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* List all sessions for a specific working directory (convenience wrapper)
|
|
201
|
+
* Only returns main sessions, skips subagent sessions
|
|
202
|
+
*
|
|
203
|
+
* @param workdir - Working directory to filter sessions by
|
|
204
|
+
* @returns Promise that resolves to array of session metadata objects
|
|
205
|
+
*/
|
|
206
|
+
export async function listSessions(workdir) {
|
|
207
|
+
return listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
|
|
134
208
|
}
|
|
135
209
|
/**
|
|
136
|
-
* List all sessions for a specific working directory
|
|
210
|
+
* List all sessions for a specific working directory using JSONL format (optimized approach)
|
|
211
|
+
*
|
|
212
|
+
* PERFORMANCE OPTIMIZATION:
|
|
213
|
+
* - Uses filename parsing exclusively for session metadata
|
|
214
|
+
* - Only reads last message for timestamps and token counts
|
|
215
|
+
* - Eliminates O(n*2) file operations, achieving O(n) performance
|
|
216
|
+
* - Returns simplified session metadata objects
|
|
217
|
+
* - Only includes main sessions, excludes subagent sessions
|
|
137
218
|
*
|
|
138
219
|
* @param workdir - Working directory to filter sessions by
|
|
139
|
-
* @param includeAllWorkdirs - If true, returns sessions from all working directories
|
|
140
|
-
* @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
|
|
141
220
|
* @returns Promise that resolves to array of session metadata objects
|
|
142
|
-
* @throws {Error} When session directory cannot be accessed or read
|
|
143
221
|
*/
|
|
144
|
-
export async function
|
|
222
|
+
export async function listSessionsFromJsonl(workdir) {
|
|
145
223
|
try {
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
const
|
|
224
|
+
const encoder = new PathEncoder();
|
|
225
|
+
const baseDir = SESSION_DIR;
|
|
226
|
+
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
227
|
+
let files;
|
|
228
|
+
try {
|
|
229
|
+
files = await fs.readdir(projectDir.encodedPath);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
// If project directory doesn't exist, return empty array
|
|
233
|
+
if (error.code === "ENOENT") {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
149
238
|
const sessions = [];
|
|
150
239
|
for (const file of files) {
|
|
151
|
-
if (!file.
|
|
240
|
+
if (!file.endsWith(".jsonl")) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
// EARLY FILTERING: Skip subagent sessions by filename prefix for maximum performance
|
|
244
|
+
if (file.startsWith("subagent-")) {
|
|
152
245
|
continue;
|
|
153
246
|
}
|
|
154
247
|
try {
|
|
155
|
-
const filePath = join(
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
continue;
|
|
248
|
+
const filePath = join(projectDir.encodedPath, file);
|
|
249
|
+
// Validate main session filename format (UUID.jsonl)
|
|
250
|
+
const uuidMatch = file.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
|
|
251
|
+
if (!uuidMatch) {
|
|
252
|
+
continue; // Skip invalid filenames
|
|
161
253
|
}
|
|
254
|
+
const sessionId = uuidMatch[1];
|
|
255
|
+
// PERFORMANCE OPTIMIZATION: Only read the last message for timestamps and tokens
|
|
256
|
+
const jsonlHandler = new JsonlHandler();
|
|
257
|
+
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
258
|
+
// Handle timing information efficiently
|
|
259
|
+
let lastActiveAt;
|
|
260
|
+
if (lastMessage) {
|
|
261
|
+
lastActiveAt = new Date(lastMessage.timestamp);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Empty session file - use file modification time
|
|
265
|
+
const stats = await fs.stat(filePath);
|
|
266
|
+
lastActiveAt = stats.mtime;
|
|
267
|
+
}
|
|
268
|
+
// Return inline object for performance (no interface instantiation overhead)
|
|
162
269
|
sessions.push({
|
|
163
|
-
id:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
lastActiveAt
|
|
168
|
-
latestTotalTokens:
|
|
270
|
+
id: sessionId,
|
|
271
|
+
sessionType: "main",
|
|
272
|
+
subagentType: undefined, // No longer stored in metadata
|
|
273
|
+
workdir: projectDir.originalPath,
|
|
274
|
+
lastActiveAt,
|
|
275
|
+
latestTotalTokens: lastMessage?.usage
|
|
276
|
+
? extractLatestTotalTokens([lastMessage])
|
|
277
|
+
: 0,
|
|
169
278
|
});
|
|
170
279
|
}
|
|
171
280
|
catch {
|
|
172
|
-
// Skip corrupted session files
|
|
281
|
+
// Skip corrupted session files
|
|
173
282
|
continue;
|
|
174
283
|
}
|
|
175
284
|
}
|
|
176
|
-
|
|
285
|
+
// Sort by last active time (most recently active first)
|
|
286
|
+
return sessions.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
|
|
177
287
|
}
|
|
178
288
|
catch (error) {
|
|
179
289
|
throw new Error(`Failed to list sessions: ${error}`);
|
|
180
290
|
}
|
|
181
291
|
}
|
|
182
292
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* @param sessionId - Unique identifier for the session to delete
|
|
186
|
-
* @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
|
|
187
|
-
* @returns Promise that resolves to true if session was deleted, false if it didn't exist
|
|
188
|
-
* @throws {Error} When session exists but cannot be deleted due to permission issues
|
|
189
|
-
*/
|
|
190
|
-
export async function deleteSession(sessionId, sessionDir) {
|
|
191
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
192
|
-
try {
|
|
193
|
-
await fs.unlink(filePath);
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
if (error.code === "ENOENT") {
|
|
198
|
-
return false; // File does not exist
|
|
199
|
-
}
|
|
200
|
-
throw new Error(`Failed to delete session ${sessionId}: ${error}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Clean up expired sessions older than the configured maximum age
|
|
293
|
+
* Clean up expired sessions older than 14 days based on file modification time
|
|
205
294
|
*
|
|
206
295
|
* @param workdir - Working directory to clean up sessions for
|
|
207
|
-
* @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
|
|
208
296
|
* @returns Promise that resolves to the number of sessions that were deleted
|
|
209
|
-
* @throws {Error} When session directory cannot be accessed or sessions cannot be deleted
|
|
210
297
|
*/
|
|
211
|
-
export async function
|
|
298
|
+
export async function cleanupExpiredSessionsFromJsonl(workdir) {
|
|
212
299
|
// Do not perform cleanup operations in test environment
|
|
213
300
|
if (process.env.NODE_ENV === "test") {
|
|
214
301
|
return 0;
|
|
215
302
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
303
|
+
try {
|
|
304
|
+
const encoder = new PathEncoder();
|
|
305
|
+
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
306
|
+
const files = await fs.readdir(projectDir.encodedPath);
|
|
307
|
+
const now = new Date();
|
|
308
|
+
const maxAge = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000; // Convert to milliseconds
|
|
309
|
+
let deletedCount = 0;
|
|
310
|
+
for (const file of files) {
|
|
311
|
+
if (!file.endsWith(".jsonl")) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const filePath = join(projectDir.encodedPath, file);
|
|
223
315
|
try {
|
|
224
|
-
await
|
|
225
|
-
|
|
316
|
+
const stat = await fs.stat(filePath);
|
|
317
|
+
const fileAge = now.getTime() - stat.mtime.getTime();
|
|
318
|
+
if (fileAge > maxAge) {
|
|
319
|
+
await fs.unlink(filePath);
|
|
320
|
+
deletedCount++;
|
|
321
|
+
}
|
|
226
322
|
}
|
|
227
323
|
catch {
|
|
228
|
-
// Skip failed
|
|
324
|
+
// Skip failed operations and continue processing other files
|
|
229
325
|
continue;
|
|
230
326
|
}
|
|
231
327
|
}
|
|
328
|
+
// Clean up empty project directory if no files remain
|
|
329
|
+
try {
|
|
330
|
+
const remainingFiles = await fs.readdir(projectDir.encodedPath);
|
|
331
|
+
if (remainingFiles.length === 0) {
|
|
332
|
+
await fs.rmdir(projectDir.encodedPath);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Ignore errors if directory is not empty or can't be removed
|
|
337
|
+
}
|
|
338
|
+
return deletedCount;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Return 0 if project directory doesn't exist or can't be accessed
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Clean up empty project directories in the session directory
|
|
347
|
+
*/
|
|
348
|
+
export async function cleanupEmptyProjectDirectories() {
|
|
349
|
+
// Do not perform cleanup operations in test environment
|
|
350
|
+
if (process.env.NODE_ENV === "test") {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const baseDir = SESSION_DIR;
|
|
355
|
+
const projectDirs = await fs.readdir(baseDir);
|
|
356
|
+
for (const projectDirName of projectDirs) {
|
|
357
|
+
const projectPath = join(baseDir, projectDirName);
|
|
358
|
+
const stat = await fs.stat(projectPath);
|
|
359
|
+
if (stat.isDirectory()) {
|
|
360
|
+
try {
|
|
361
|
+
const files = await fs.readdir(projectPath);
|
|
362
|
+
// If directory is empty, remove it
|
|
363
|
+
if (files.length === 0) {
|
|
364
|
+
await fs.rmdir(projectPath);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Skip errors for directories we can't read or remove
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
232
376
|
}
|
|
233
|
-
return deletedCount;
|
|
234
377
|
}
|
|
235
378
|
/**
|
|
236
|
-
* Check if a session exists in storage
|
|
379
|
+
* Check if a session exists in JSONL storage (new approach)
|
|
237
380
|
*
|
|
238
|
-
* @param sessionId -
|
|
239
|
-
* @param
|
|
381
|
+
* @param sessionId - UUID session identifier
|
|
382
|
+
* @param workdir - Working directory for the session
|
|
383
|
+
* @param sessionType - Type of session ("main" or "subagent"). If not provided, checks both types.
|
|
240
384
|
* @returns Promise that resolves to true if session exists, false otherwise
|
|
241
385
|
*/
|
|
242
|
-
export async function
|
|
243
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
386
|
+
export async function sessionExistsInJsonl(sessionId, workdir, sessionType) {
|
|
244
387
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
388
|
+
if (sessionType) {
|
|
389
|
+
// If session type is known, check directly
|
|
390
|
+
const filePath = await generateSessionFilePath(sessionId, workdir, sessionType);
|
|
391
|
+
await fs.access(filePath);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// If session type is unknown, try both
|
|
396
|
+
const mainPath = await generateSessionFilePath(sessionId, workdir, "main");
|
|
397
|
+
try {
|
|
398
|
+
await fs.access(mainPath);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
const subagentPath = await generateSessionFilePath(sessionId, workdir, "subagent");
|
|
403
|
+
try {
|
|
404
|
+
await fs.access(subagentPath);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
247
412
|
}
|
|
248
413
|
catch {
|
|
249
414
|
return false;
|
|
250
415
|
}
|
|
251
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Get the content of the first message in a session
|
|
419
|
+
* For user role: get text block content
|
|
420
|
+
* For assistant role: get compress block content
|
|
421
|
+
* @param sessionId - Session ID to get first message from
|
|
422
|
+
* @param workdir - Working directory for session operations
|
|
423
|
+
* @returns Promise that resolves to the first message content or null if not found
|
|
424
|
+
*/
|
|
425
|
+
export async function getFirstMessageContent(sessionId, workdir) {
|
|
426
|
+
try {
|
|
427
|
+
const encoder = new PathEncoder();
|
|
428
|
+
const baseDir = SESSION_DIR;
|
|
429
|
+
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
430
|
+
const filePath = join(projectDir.encodedPath, `${sessionId}.jsonl`);
|
|
431
|
+
// Read the first line of the file
|
|
432
|
+
const { readFirstLine } = await import("../utils/fileUtils.js");
|
|
433
|
+
const firstLine = await readFirstLine(filePath);
|
|
434
|
+
if (!firstLine) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const message = JSON.parse(firstLine);
|
|
439
|
+
// Find first available content block regardless of role
|
|
440
|
+
const textBlock = message.blocks.find((block) => block.type === "text");
|
|
441
|
+
if (textBlock && "content" in textBlock) {
|
|
442
|
+
return textBlock.content;
|
|
443
|
+
}
|
|
444
|
+
const commandBlock = message.blocks.find((block) => block.type === "command_output");
|
|
445
|
+
if (commandBlock && "command" in commandBlock) {
|
|
446
|
+
return commandBlock.command;
|
|
447
|
+
}
|
|
448
|
+
const compressBlock = message.blocks.find((block) => block.type === "compress");
|
|
449
|
+
if (compressBlock && "content" in compressBlock) {
|
|
450
|
+
return compressBlock.content;
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
logger.warn(`Failed to parse first message in session ${sessionId}:`, error);
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
logger.warn(`Failed to get first message content for session ${sessionId}:`, error);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Truncate content to a maximum length, adding ellipsis if truncated
|
|
466
|
+
* @param content - The content to truncate
|
|
467
|
+
* @param maxLength - Maximum length before truncation (default: 30)
|
|
468
|
+
* @returns Truncated content with ellipsis if needed
|
|
469
|
+
*/
|
|
470
|
+
export function truncateContent(content, maxLength = 30) {
|
|
471
|
+
if (content.length <= maxLength) {
|
|
472
|
+
return content;
|
|
473
|
+
}
|
|
474
|
+
return content.substring(0, maxLength) + "...";
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Handle session restoration logic
|
|
478
|
+
* @param restoreSessionId - Specific session ID to restore
|
|
479
|
+
* @param continueLastSession - Whether to continue the most recent session
|
|
480
|
+
* @param workdir - Working directory for session restoration
|
|
481
|
+
* @returns Promise that resolves to session data or undefined
|
|
482
|
+
*/
|
|
483
|
+
export async function handleSessionRestoration(restoreSessionId, continueLastSession, workdir) {
|
|
484
|
+
if (!workdir) {
|
|
485
|
+
throw new Error("Working directory is required for session restoration");
|
|
486
|
+
}
|
|
487
|
+
// Clean up expired sessions first
|
|
488
|
+
cleanupExpiredSessionsFromJsonl(workdir).catch((error) => {
|
|
489
|
+
logger.warn("Failed to cleanup expired sessions:", error);
|
|
490
|
+
});
|
|
491
|
+
if (!restoreSessionId && !continueLastSession) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
let sessionToRestore = null;
|
|
496
|
+
if (restoreSessionId) {
|
|
497
|
+
// Use only JSONL format - no legacy support
|
|
498
|
+
sessionToRestore = await loadSessionFromJsonl(restoreSessionId, workdir);
|
|
499
|
+
if (!sessionToRestore) {
|
|
500
|
+
console.error(`Session not found: ${restoreSessionId}`);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else if (continueLastSession) {
|
|
505
|
+
// Use only JSONL format - no legacy support
|
|
506
|
+
sessionToRestore = await getLatestSessionFromJsonl(workdir);
|
|
507
|
+
if (!sessionToRestore) {
|
|
508
|
+
console.error(`No previous session found for workdir: ${workdir}`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (sessionToRestore) {
|
|
513
|
+
console.log(`Restoring session: ${sessionToRestore.id}`);
|
|
514
|
+
// // Initialize from session data
|
|
515
|
+
// this.initializeFromSession();
|
|
516
|
+
return sessionToRestore;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
console.error("Failed to restore session:", error);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
}
|