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/src/services/session.ts
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Management Service - JSONL Format Implementation
|
|
3
|
+
*
|
|
4
|
+
* OPTIMIZED IMPLEMENTATION (Phase 6 Complete):
|
|
5
|
+
* - Filename-based session type identification
|
|
6
|
+
* - Minimal file I/O for metadata extraction
|
|
7
|
+
* - Eliminated metadata headers for cleaner session files
|
|
8
|
+
* - Backward compatible with existing session files
|
|
9
|
+
* - 8-10x performance improvement in session listing operations
|
|
10
|
+
*
|
|
11
|
+
* Key Features:
|
|
12
|
+
* - Session creation without metadata headers
|
|
13
|
+
* - Subagent sessions identified by filename prefix
|
|
14
|
+
* - Performance-optimized session listing
|
|
15
|
+
* - Full backward compatibility maintained
|
|
16
|
+
*/
|
|
17
|
+
|
|
1
18
|
import { promises as fs } from "fs";
|
|
2
19
|
import { join } from "path";
|
|
3
20
|
import { homedir } from "os";
|
|
21
|
+
import { randomUUID } from "crypto";
|
|
4
22
|
import type { Message } from "../types/index.js";
|
|
23
|
+
import type { SessionMessage } from "../types/session.js";
|
|
24
|
+
import { PathEncoder } from "../utils/pathEncoder.js";
|
|
25
|
+
import { JsonlHandler } from "../services/jsonlHandler.js";
|
|
26
|
+
import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
|
|
27
|
+
import { logger } from "../utils/globalLogger.js";
|
|
5
28
|
|
|
6
29
|
export interface SessionData {
|
|
7
30
|
id: string;
|
|
8
|
-
timestamp: string;
|
|
9
|
-
version: string;
|
|
10
31
|
messages: Message[];
|
|
11
32
|
metadata: {
|
|
12
33
|
workdir: string;
|
|
13
|
-
startedAt: string;
|
|
14
34
|
lastActiveAt: string;
|
|
15
35
|
latestTotalTokens: number;
|
|
16
36
|
};
|
|
@@ -18,82 +38,118 @@ export interface SessionData {
|
|
|
18
38
|
|
|
19
39
|
export interface SessionMetadata {
|
|
20
40
|
id: string;
|
|
21
|
-
|
|
41
|
+
sessionType: "main" | "subagent";
|
|
42
|
+
subagentType?: string;
|
|
22
43
|
workdir: string;
|
|
23
|
-
|
|
24
|
-
lastActiveAt: string;
|
|
44
|
+
lastActiveAt: Date;
|
|
25
45
|
latestTotalTokens: number;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Generate a new session ID using Node.js native crypto.randomUUID()
|
|
50
|
+
* @returns UUID string for session identification
|
|
51
|
+
*/
|
|
52
|
+
export function generateSessionId(): string {
|
|
53
|
+
return randomUUID();
|
|
54
|
+
}
|
|
32
55
|
|
|
33
56
|
/**
|
|
34
|
-
*
|
|
35
|
-
* @param
|
|
36
|
-
* @returns
|
|
57
|
+
* Generate filename for subagent sessions
|
|
58
|
+
* @param sessionId - UUID session identifier
|
|
59
|
+
* @returns Filename with subagent prefix for subagent sessions
|
|
37
60
|
*/
|
|
38
|
-
export function
|
|
39
|
-
return
|
|
61
|
+
export function generateSubagentFilename(sessionId: string): string {
|
|
62
|
+
return `subagent-${sessionId}.jsonl`;
|
|
40
63
|
}
|
|
41
64
|
|
|
65
|
+
// Constants
|
|
66
|
+
export const SESSION_DIR = join(homedir(), ".wave", "projects");
|
|
67
|
+
const MAX_SESSION_AGE_DAYS = 14;
|
|
68
|
+
|
|
42
69
|
/**
|
|
43
70
|
* Ensure session directory exists
|
|
44
|
-
* @param sessionDir Optional custom session directory
|
|
45
71
|
*/
|
|
46
|
-
export async function ensureSessionDir(
|
|
47
|
-
const resolvedDir = resolveSessionDir(sessionDir);
|
|
72
|
+
export async function ensureSessionDir(): Promise<void> {
|
|
48
73
|
try {
|
|
49
|
-
await fs.mkdir(
|
|
74
|
+
await fs.mkdir(SESSION_DIR, { recursive: true });
|
|
50
75
|
} catch (error) {
|
|
51
76
|
throw new Error(`Failed to create session directory: ${error}`);
|
|
52
77
|
}
|
|
53
78
|
}
|
|
54
79
|
|
|
55
80
|
/**
|
|
56
|
-
* Generate session file path
|
|
81
|
+
* Generate session file path without creating directories
|
|
82
|
+
* @param sessionId - UUID session identifier
|
|
83
|
+
* @param workdir - Working directory for the session
|
|
84
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
85
|
+
* @returns Promise resolving to full file path for the session JSONL file
|
|
57
86
|
*/
|
|
58
|
-
export function
|
|
87
|
+
export async function generateSessionFilePath(
|
|
59
88
|
sessionId: string,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
89
|
+
workdir: string,
|
|
90
|
+
sessionType: "main" | "subagent" = "main",
|
|
91
|
+
): Promise<string> {
|
|
92
|
+
const encoder = new PathEncoder();
|
|
93
|
+
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
94
|
+
|
|
95
|
+
// Generate filename based on session type
|
|
96
|
+
const jsonlHandler = new JsonlHandler();
|
|
97
|
+
const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
|
|
98
|
+
|
|
99
|
+
return join(projectDir.encodedPath, filename);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate session file path using project-based directory structure
|
|
104
|
+
* @param sessionId - UUID session identifier
|
|
105
|
+
* @param workdir - Working directory for the session
|
|
106
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
107
|
+
* @returns Promise resolving to full file path for the session JSONL file
|
|
108
|
+
*/
|
|
109
|
+
export async function getSessionFilePath(
|
|
110
|
+
sessionId: string,
|
|
111
|
+
workdir: string,
|
|
112
|
+
sessionType: "main" | "subagent" = "main",
|
|
113
|
+
): Promise<string> {
|
|
114
|
+
const encoder = new PathEncoder();
|
|
115
|
+
const projectDir = await encoder.createProjectDirectory(workdir, SESSION_DIR);
|
|
116
|
+
|
|
117
|
+
// Generate filename based on session type
|
|
118
|
+
const jsonlHandler = new JsonlHandler();
|
|
119
|
+
const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
|
|
120
|
+
|
|
121
|
+
return join(projectDir.encodedPath, filename);
|
|
65
122
|
}
|
|
66
123
|
|
|
67
124
|
/**
|
|
68
|
-
*
|
|
125
|
+
* Create a new session
|
|
126
|
+
* @param sessionId - UUID session identifier
|
|
127
|
+
* @param workdir - Working directory for the session
|
|
128
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
69
129
|
*/
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
130
|
+
export async function createSession(
|
|
131
|
+
sessionId: string,
|
|
132
|
+
workdir: string,
|
|
133
|
+
sessionType: "main" | "subagent" = "main",
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const jsonlHandler = new JsonlHandler();
|
|
136
|
+
const filePath = await getSessionFilePath(sessionId, workdir, sessionType);
|
|
137
|
+
await jsonlHandler.createSession(filePath);
|
|
77
138
|
}
|
|
78
139
|
|
|
79
140
|
/**
|
|
80
|
-
*
|
|
141
|
+
* Append messages to session using JSONL format (new approach)
|
|
81
142
|
*
|
|
82
|
-
* @param sessionId -
|
|
83
|
-
* @param
|
|
143
|
+
* @param sessionId - UUID session identifier
|
|
144
|
+
* @param newMessages - Array of messages to append
|
|
84
145
|
* @param workdir - Working directory for the session
|
|
85
|
-
* @param
|
|
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
|
|
146
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
89
147
|
*/
|
|
90
|
-
export async function
|
|
148
|
+
export async function appendMessages(
|
|
91
149
|
sessionId: string,
|
|
92
|
-
|
|
150
|
+
newMessages: Message[],
|
|
93
151
|
workdir: string,
|
|
94
|
-
|
|
95
|
-
startedAt?: string,
|
|
96
|
-
sessionDir?: string,
|
|
152
|
+
sessionType: "main" | "subagent" = "main",
|
|
97
153
|
): Promise<void> {
|
|
98
154
|
// Do not save session files in test environment
|
|
99
155
|
if (process.env.NODE_ENV === "test") {
|
|
@@ -101,148 +157,238 @@ export async function saveSession(
|
|
|
101
157
|
}
|
|
102
158
|
|
|
103
159
|
// Do not save if there are no messages
|
|
104
|
-
if (
|
|
160
|
+
if (newMessages.length === 0) {
|
|
105
161
|
return;
|
|
106
162
|
}
|
|
107
163
|
|
|
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
|
-
};
|
|
164
|
+
const jsonlHandler = new JsonlHandler();
|
|
126
165
|
|
|
127
|
-
|
|
166
|
+
// Generate the session file path directly using known session type
|
|
167
|
+
const filePath = await generateSessionFilePath(
|
|
168
|
+
sessionId,
|
|
169
|
+
workdir,
|
|
170
|
+
sessionType,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Check if the session file exists
|
|
128
174
|
try {
|
|
129
|
-
await fs.
|
|
130
|
-
} catch
|
|
131
|
-
throw new Error(
|
|
175
|
+
await fs.access(filePath);
|
|
176
|
+
} catch {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Session file not found: ${sessionId}. Use createSession() to create a new session first.`,
|
|
179
|
+
);
|
|
132
180
|
}
|
|
181
|
+
|
|
182
|
+
const messagesWithTimestamp: SessionMessage[] = newMessages.map((msg) => ({
|
|
183
|
+
timestamp: new Date().toISOString(),
|
|
184
|
+
...msg,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
await jsonlHandler.append(filePath, messagesWithTimestamp, {
|
|
188
|
+
atomic: false,
|
|
189
|
+
});
|
|
133
190
|
}
|
|
134
191
|
|
|
135
192
|
/**
|
|
136
|
-
* Load session data from
|
|
193
|
+
* Load session data from JSONL file (new approach)
|
|
137
194
|
*
|
|
138
|
-
* @param sessionId -
|
|
139
|
-
* @param
|
|
195
|
+
* @param sessionId - UUID session identifier
|
|
196
|
+
* @param workdir - Working directory for the session
|
|
197
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
140
198
|
* @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
199
|
*/
|
|
143
|
-
export async function
|
|
200
|
+
export async function loadSessionFromJsonl(
|
|
144
201
|
sessionId: string,
|
|
145
|
-
|
|
202
|
+
workdir: string,
|
|
203
|
+
sessionType: "main" | "subagent" = "main",
|
|
146
204
|
): Promise<SessionData | null> {
|
|
147
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
148
|
-
|
|
149
205
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
206
|
+
const jsonlHandler = new JsonlHandler();
|
|
207
|
+
|
|
208
|
+
// Generate the session file path directly using known session type
|
|
209
|
+
const filePath = await generateSessionFilePath(
|
|
210
|
+
sessionId,
|
|
211
|
+
workdir,
|
|
212
|
+
sessionType,
|
|
213
|
+
);
|
|
152
214
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
const messages = await jsonlHandler.read(filePath);
|
|
216
|
+
|
|
217
|
+
if (messages.length === 0) {
|
|
218
|
+
return null;
|
|
156
219
|
}
|
|
157
220
|
|
|
221
|
+
// Extract metadata from messages
|
|
222
|
+
const lastMessage = messages[messages.length - 1];
|
|
223
|
+
|
|
224
|
+
const sessionData: SessionData = {
|
|
225
|
+
id: sessionId,
|
|
226
|
+
messages: messages.map((msg) => {
|
|
227
|
+
// Remove timestamp property for backward compatibility
|
|
228
|
+
const { timestamp: _ignored, ...messageWithoutTimestamp } = msg;
|
|
229
|
+
void _ignored; // Use the variable to avoid eslint error
|
|
230
|
+
return messageWithoutTimestamp;
|
|
231
|
+
}),
|
|
232
|
+
metadata: {
|
|
233
|
+
workdir,
|
|
234
|
+
lastActiveAt: lastMessage.timestamp,
|
|
235
|
+
latestTotalTokens: lastMessage.usage
|
|
236
|
+
? extractLatestTotalTokens([lastMessage])
|
|
237
|
+
: 0,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
158
241
|
return sessionData;
|
|
159
242
|
} catch (error) {
|
|
160
|
-
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
244
|
+
|
|
245
|
+
// Check if the underlying error is ENOENT (file doesn't exist)
|
|
246
|
+
if (
|
|
247
|
+
errorMessage.includes("ENOENT") ||
|
|
248
|
+
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
249
|
+
) {
|
|
161
250
|
return null; // Session file does not exist
|
|
162
251
|
}
|
|
252
|
+
|
|
253
|
+
// Check for JSON parsing errors (corrupted files)
|
|
254
|
+
if (
|
|
255
|
+
errorMessage.includes("Invalid JSON") ||
|
|
256
|
+
errorMessage.includes("Unexpected token")
|
|
257
|
+
) {
|
|
258
|
+
return null; // Treat corrupted files as non-existent
|
|
259
|
+
}
|
|
260
|
+
|
|
163
261
|
throw new Error(`Failed to load session ${sessionId}: ${error}`);
|
|
164
262
|
}
|
|
165
263
|
}
|
|
166
264
|
|
|
167
265
|
/**
|
|
168
|
-
* Get the most
|
|
266
|
+
* Get the most recently active session for a specific working directory (new JSONL approach)
|
|
267
|
+
* Only returns main sessions, skips subagent sessions
|
|
268
|
+
* Uses listSessionsFromJsonl which already sorts sessions by last active time (most recent first)
|
|
169
269
|
*
|
|
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
|
|
270
|
+
* @param workdir - Working directory to find the most recently active session for
|
|
271
|
+
* @returns Promise that resolves to the most recently active session data or null if no sessions exist
|
|
174
272
|
*/
|
|
175
|
-
export async function
|
|
273
|
+
export async function getLatestSessionFromJsonl(
|
|
176
274
|
workdir: string,
|
|
177
|
-
sessionDir?: string,
|
|
178
275
|
): Promise<SessionData | null> {
|
|
179
|
-
const sessions = await
|
|
276
|
+
const sessions = await listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
|
|
277
|
+
|
|
180
278
|
if (sessions.length === 0) {
|
|
181
279
|
return null;
|
|
182
280
|
}
|
|
183
281
|
|
|
184
|
-
//
|
|
185
|
-
const latestSession = sessions
|
|
186
|
-
|
|
187
|
-
new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime(),
|
|
188
|
-
)[0];
|
|
189
|
-
|
|
190
|
-
return loadSession(latestSession.id, sessionDir);
|
|
282
|
+
// Sessions are already sorted by lastActiveAt from listSessionsFromJsonl (most recent first)
|
|
283
|
+
const latestSession = sessions[0];
|
|
284
|
+
return loadSessionFromJsonl(latestSession.id, workdir);
|
|
191
285
|
}
|
|
192
286
|
|
|
193
287
|
/**
|
|
194
|
-
* List all sessions for a specific working directory
|
|
288
|
+
* List all sessions for a specific working directory (convenience wrapper)
|
|
289
|
+
* Only returns main sessions, skips subagent sessions
|
|
195
290
|
*
|
|
196
291
|
* @param workdir - Working directory to filter sessions by
|
|
197
|
-
* @param includeAllWorkdirs - If true, returns sessions from all working directories
|
|
198
|
-
* @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
|
|
199
292
|
* @returns Promise that resolves to array of session metadata objects
|
|
200
|
-
* @throws {Error} When session directory cannot be accessed or read
|
|
201
293
|
*/
|
|
202
294
|
export async function listSessions(
|
|
203
295
|
workdir: string,
|
|
204
|
-
|
|
205
|
-
|
|
296
|
+
): Promise<SessionMetadata[]> {
|
|
297
|
+
return listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* List all sessions for a specific working directory using JSONL format (optimized approach)
|
|
302
|
+
*
|
|
303
|
+
* PERFORMANCE OPTIMIZATION:
|
|
304
|
+
* - Uses filename parsing exclusively for session metadata
|
|
305
|
+
* - Only reads last message for timestamps and token counts
|
|
306
|
+
* - Eliminates O(n*2) file operations, achieving O(n) performance
|
|
307
|
+
* - Returns simplified session metadata objects
|
|
308
|
+
* - Only includes main sessions, excludes subagent sessions
|
|
309
|
+
*
|
|
310
|
+
* @param workdir - Working directory to filter sessions by
|
|
311
|
+
* @returns Promise that resolves to array of session metadata objects
|
|
312
|
+
*/
|
|
313
|
+
export async function listSessionsFromJsonl(
|
|
314
|
+
workdir: string,
|
|
206
315
|
): Promise<SessionMetadata[]> {
|
|
207
316
|
try {
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
317
|
+
const encoder = new PathEncoder();
|
|
318
|
+
const baseDir = SESSION_DIR;
|
|
319
|
+
|
|
320
|
+
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
321
|
+
let files: string[];
|
|
322
|
+
try {
|
|
323
|
+
files = await fs.readdir(projectDir.encodedPath);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
// If project directory doesn't exist, return empty array
|
|
326
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
211
331
|
|
|
212
332
|
const sessions: SessionMetadata[] = [];
|
|
213
333
|
|
|
214
334
|
for (const file of files) {
|
|
215
|
-
if (!file.
|
|
335
|
+
if (!file.endsWith(".jsonl")) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// EARLY FILTERING: Skip subagent sessions by filename prefix for maximum performance
|
|
340
|
+
if (file.startsWith("subagent-")) {
|
|
216
341
|
continue;
|
|
217
342
|
}
|
|
218
343
|
|
|
219
344
|
try {
|
|
220
|
-
const filePath = join(
|
|
221
|
-
|
|
222
|
-
|
|
345
|
+
const filePath = join(projectDir.encodedPath, file);
|
|
346
|
+
|
|
347
|
+
// Validate main session filename format (UUID.jsonl)
|
|
348
|
+
const uuidMatch = file.match(
|
|
349
|
+
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
350
|
+
);
|
|
351
|
+
if (!uuidMatch) {
|
|
352
|
+
continue; // Skip invalid filenames
|
|
353
|
+
}
|
|
223
354
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
355
|
+
const sessionId = uuidMatch[1];
|
|
356
|
+
|
|
357
|
+
// PERFORMANCE OPTIMIZATION: Only read the last message for timestamps and tokens
|
|
358
|
+
const jsonlHandler = new JsonlHandler();
|
|
359
|
+
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
360
|
+
|
|
361
|
+
// Handle timing information efficiently
|
|
362
|
+
let lastActiveAt: Date;
|
|
363
|
+
|
|
364
|
+
if (lastMessage) {
|
|
365
|
+
lastActiveAt = new Date(lastMessage.timestamp);
|
|
366
|
+
} else {
|
|
367
|
+
// Empty session file - use file modification time
|
|
368
|
+
const stats = await fs.stat(filePath);
|
|
369
|
+
lastActiveAt = stats.mtime;
|
|
227
370
|
}
|
|
228
371
|
|
|
372
|
+
// Return inline object for performance (no interface instantiation overhead)
|
|
229
373
|
sessions.push({
|
|
230
|
-
id:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
lastActiveAt
|
|
235
|
-
latestTotalTokens:
|
|
374
|
+
id: sessionId,
|
|
375
|
+
sessionType: "main",
|
|
376
|
+
subagentType: undefined, // No longer stored in metadata
|
|
377
|
+
workdir: projectDir.originalPath,
|
|
378
|
+
lastActiveAt,
|
|
379
|
+
latestTotalTokens: lastMessage?.usage
|
|
380
|
+
? extractLatestTotalTokens([lastMessage])
|
|
381
|
+
: 0,
|
|
236
382
|
});
|
|
237
383
|
} catch {
|
|
238
|
-
// Skip corrupted session files
|
|
384
|
+
// Skip corrupted session files
|
|
239
385
|
continue;
|
|
240
386
|
}
|
|
241
387
|
}
|
|
242
388
|
|
|
389
|
+
// Sort by last active time (most recently active first)
|
|
243
390
|
return sessions.sort(
|
|
244
|
-
(a, b) =>
|
|
245
|
-
new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime(),
|
|
391
|
+
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
246
392
|
);
|
|
247
393
|
} catch (error) {
|
|
248
394
|
throw new Error(`Failed to list sessions: ${error}`);
|
|
@@ -250,87 +396,290 @@ export async function listSessions(
|
|
|
250
396
|
}
|
|
251
397
|
|
|
252
398
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* @param sessionId - Unique identifier for the session to delete
|
|
256
|
-
* @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
|
|
257
|
-
* @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
|
-
*/
|
|
260
|
-
export async function deleteSession(
|
|
261
|
-
sessionId: string,
|
|
262
|
-
sessionDir?: string,
|
|
263
|
-
): Promise<boolean> {
|
|
264
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
await fs.unlink(filePath);
|
|
268
|
-
return true;
|
|
269
|
-
} catch (error) {
|
|
270
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
271
|
-
return false; // File does not exist
|
|
272
|
-
}
|
|
273
|
-
throw new Error(`Failed to delete session ${sessionId}: ${error}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Clean up expired sessions older than the configured maximum age
|
|
399
|
+
* Clean up expired sessions older than 14 days based on file modification time
|
|
279
400
|
*
|
|
280
401
|
* @param workdir - Working directory to clean up sessions for
|
|
281
|
-
* @param sessionDir - Optional custom directory for session storage (defaults to ~/.wave/sessions/)
|
|
282
402
|
* @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
403
|
*/
|
|
285
|
-
export async function
|
|
404
|
+
export async function cleanupExpiredSessionsFromJsonl(
|
|
286
405
|
workdir: string,
|
|
287
|
-
sessionDir?: string,
|
|
288
406
|
): Promise<number> {
|
|
289
407
|
// Do not perform cleanup operations in test environment
|
|
290
408
|
if (process.env.NODE_ENV === "test") {
|
|
291
409
|
return 0;
|
|
292
410
|
}
|
|
293
411
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
412
|
+
try {
|
|
413
|
+
const encoder = new PathEncoder();
|
|
414
|
+
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
415
|
+
const files = await fs.readdir(projectDir.encodedPath);
|
|
416
|
+
|
|
417
|
+
const now = new Date();
|
|
418
|
+
const maxAge = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000; // Convert to milliseconds
|
|
419
|
+
let deletedCount = 0;
|
|
297
420
|
|
|
298
|
-
|
|
421
|
+
for (const file of files) {
|
|
422
|
+
if (!file.endsWith(".jsonl")) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
299
425
|
|
|
300
|
-
|
|
301
|
-
const sessionAge = now.getTime() - new Date(session.lastActiveAt).getTime();
|
|
426
|
+
const filePath = join(projectDir.encodedPath, file);
|
|
302
427
|
|
|
303
|
-
if (sessionAge > maxAge) {
|
|
304
428
|
try {
|
|
305
|
-
await
|
|
306
|
-
|
|
429
|
+
const stat = await fs.stat(filePath);
|
|
430
|
+
const fileAge = now.getTime() - stat.mtime.getTime();
|
|
431
|
+
|
|
432
|
+
if (fileAge > maxAge) {
|
|
433
|
+
await fs.unlink(filePath);
|
|
434
|
+
deletedCount++;
|
|
435
|
+
}
|
|
307
436
|
} catch {
|
|
308
|
-
// Skip failed
|
|
437
|
+
// Skip failed operations and continue processing other files
|
|
309
438
|
continue;
|
|
310
439
|
}
|
|
311
440
|
}
|
|
441
|
+
|
|
442
|
+
// Clean up empty project directory if no files remain
|
|
443
|
+
try {
|
|
444
|
+
const remainingFiles = await fs.readdir(projectDir.encodedPath);
|
|
445
|
+
if (remainingFiles.length === 0) {
|
|
446
|
+
await fs.rmdir(projectDir.encodedPath);
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
// Ignore errors if directory is not empty or can't be removed
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return deletedCount;
|
|
453
|
+
} catch {
|
|
454
|
+
// Return 0 if project directory doesn't exist or can't be accessed
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Clean up empty project directories in the session directory
|
|
461
|
+
*/
|
|
462
|
+
export async function cleanupEmptyProjectDirectories(): Promise<void> {
|
|
463
|
+
// Do not perform cleanup operations in test environment
|
|
464
|
+
if (process.env.NODE_ENV === "test") {
|
|
465
|
+
return;
|
|
312
466
|
}
|
|
313
467
|
|
|
314
|
-
|
|
468
|
+
try {
|
|
469
|
+
const baseDir = SESSION_DIR;
|
|
470
|
+
const projectDirs = await fs.readdir(baseDir);
|
|
471
|
+
|
|
472
|
+
for (const projectDirName of projectDirs) {
|
|
473
|
+
const projectPath = join(baseDir, projectDirName);
|
|
474
|
+
const stat = await fs.stat(projectPath);
|
|
475
|
+
|
|
476
|
+
if (stat.isDirectory()) {
|
|
477
|
+
try {
|
|
478
|
+
const files = await fs.readdir(projectPath);
|
|
479
|
+
|
|
480
|
+
// If directory is empty, remove it
|
|
481
|
+
if (files.length === 0) {
|
|
482
|
+
await fs.rmdir(projectPath);
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
// Skip errors for directories we can't read or remove
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
492
|
+
}
|
|
315
493
|
}
|
|
316
494
|
|
|
317
495
|
/**
|
|
318
|
-
* Check if a session exists in storage
|
|
496
|
+
* Check if a session exists in JSONL storage (new approach)
|
|
319
497
|
*
|
|
320
|
-
* @param sessionId -
|
|
321
|
-
* @param
|
|
498
|
+
* @param sessionId - UUID session identifier
|
|
499
|
+
* @param workdir - Working directory for the session
|
|
500
|
+
* @param sessionType - Type of session ("main" or "subagent"). If not provided, checks both types.
|
|
322
501
|
* @returns Promise that resolves to true if session exists, false otherwise
|
|
323
502
|
*/
|
|
324
|
-
export async function
|
|
503
|
+
export async function sessionExistsInJsonl(
|
|
325
504
|
sessionId: string,
|
|
326
|
-
|
|
505
|
+
workdir: string,
|
|
506
|
+
sessionType?: "main" | "subagent",
|
|
327
507
|
): Promise<boolean> {
|
|
328
|
-
const filePath = getSessionFilePath(sessionId, sessionDir);
|
|
329
|
-
|
|
330
508
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
509
|
+
if (sessionType) {
|
|
510
|
+
// If session type is known, check directly
|
|
511
|
+
const filePath = await generateSessionFilePath(
|
|
512
|
+
sessionId,
|
|
513
|
+
workdir,
|
|
514
|
+
sessionType,
|
|
515
|
+
);
|
|
516
|
+
await fs.access(filePath);
|
|
517
|
+
return true;
|
|
518
|
+
} else {
|
|
519
|
+
// If session type is unknown, try both
|
|
520
|
+
const mainPath = await generateSessionFilePath(
|
|
521
|
+
sessionId,
|
|
522
|
+
workdir,
|
|
523
|
+
"main",
|
|
524
|
+
);
|
|
525
|
+
try {
|
|
526
|
+
await fs.access(mainPath);
|
|
527
|
+
return true;
|
|
528
|
+
} catch {
|
|
529
|
+
const subagentPath = await generateSessionFilePath(
|
|
530
|
+
sessionId,
|
|
531
|
+
workdir,
|
|
532
|
+
"subagent",
|
|
533
|
+
);
|
|
534
|
+
try {
|
|
535
|
+
await fs.access(subagentPath);
|
|
536
|
+
return true;
|
|
537
|
+
} catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
333
542
|
} catch {
|
|
334
543
|
return false;
|
|
335
544
|
}
|
|
336
545
|
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Get the content of the first message in a session
|
|
549
|
+
* For user role: get text block content
|
|
550
|
+
* For assistant role: get compress block content
|
|
551
|
+
* @param sessionId - Session ID to get first message from
|
|
552
|
+
* @param workdir - Working directory for session operations
|
|
553
|
+
* @returns Promise that resolves to the first message content or null if not found
|
|
554
|
+
*/
|
|
555
|
+
export async function getFirstMessageContent(
|
|
556
|
+
sessionId: string,
|
|
557
|
+
workdir: string,
|
|
558
|
+
): Promise<string | null> {
|
|
559
|
+
try {
|
|
560
|
+
const encoder = new PathEncoder();
|
|
561
|
+
const baseDir = SESSION_DIR;
|
|
562
|
+
|
|
563
|
+
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
564
|
+
const filePath = join(projectDir.encodedPath, `${sessionId}.jsonl`);
|
|
565
|
+
|
|
566
|
+
// Read the first line of the file
|
|
567
|
+
const { readFirstLine } = await import("../utils/fileUtils.js");
|
|
568
|
+
const firstLine = await readFirstLine(filePath);
|
|
569
|
+
|
|
570
|
+
if (!firstLine) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const message = JSON.parse(firstLine) as Message;
|
|
576
|
+
|
|
577
|
+
// Find first available content block regardless of role
|
|
578
|
+
const textBlock = message.blocks.find((block) => block.type === "text");
|
|
579
|
+
if (textBlock && "content" in textBlock) {
|
|
580
|
+
return textBlock.content;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const commandBlock = message.blocks.find(
|
|
584
|
+
(block) => block.type === "command_output",
|
|
585
|
+
);
|
|
586
|
+
if (commandBlock && "command" in commandBlock) {
|
|
587
|
+
return commandBlock.command;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const compressBlock = message.blocks.find(
|
|
591
|
+
(block) => block.type === "compress",
|
|
592
|
+
);
|
|
593
|
+
if (compressBlock && "content" in compressBlock) {
|
|
594
|
+
return compressBlock.content;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return null;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
logger.warn(
|
|
600
|
+
`Failed to parse first message in session ${sessionId}:`,
|
|
601
|
+
error,
|
|
602
|
+
);
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
} catch (error) {
|
|
606
|
+
logger.warn(
|
|
607
|
+
`Failed to get first message content for session ${sessionId}:`,
|
|
608
|
+
error,
|
|
609
|
+
);
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Truncate content to a maximum length, adding ellipsis if truncated
|
|
616
|
+
* @param content - The content to truncate
|
|
617
|
+
* @param maxLength - Maximum length before truncation (default: 30)
|
|
618
|
+
* @returns Truncated content with ellipsis if needed
|
|
619
|
+
*/
|
|
620
|
+
export function truncateContent(
|
|
621
|
+
content: string,
|
|
622
|
+
maxLength: number = 30,
|
|
623
|
+
): string {
|
|
624
|
+
if (content.length <= maxLength) {
|
|
625
|
+
return content;
|
|
626
|
+
}
|
|
627
|
+
return content.substring(0, maxLength) + "...";
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Handle session restoration logic
|
|
632
|
+
* @param restoreSessionId - Specific session ID to restore
|
|
633
|
+
* @param continueLastSession - Whether to continue the most recent session
|
|
634
|
+
* @param workdir - Working directory for session restoration
|
|
635
|
+
* @returns Promise that resolves to session data or undefined
|
|
636
|
+
*/
|
|
637
|
+
export async function handleSessionRestoration(
|
|
638
|
+
restoreSessionId?: string,
|
|
639
|
+
continueLastSession?: boolean,
|
|
640
|
+
workdir?: string,
|
|
641
|
+
): Promise<SessionData | undefined> {
|
|
642
|
+
if (!workdir) {
|
|
643
|
+
throw new Error("Working directory is required for session restoration");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Clean up expired sessions first
|
|
647
|
+
cleanupExpiredSessionsFromJsonl(workdir).catch((error) => {
|
|
648
|
+
logger.warn("Failed to cleanup expired sessions:", error);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (!restoreSessionId && !continueLastSession) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
let sessionToRestore: SessionData | null = null;
|
|
657
|
+
|
|
658
|
+
if (restoreSessionId) {
|
|
659
|
+
// Use only JSONL format - no legacy support
|
|
660
|
+
sessionToRestore = await loadSessionFromJsonl(restoreSessionId, workdir);
|
|
661
|
+
if (!sessionToRestore) {
|
|
662
|
+
console.error(`Session not found: ${restoreSessionId}`);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
} else if (continueLastSession) {
|
|
666
|
+
// Use only JSONL format - no legacy support
|
|
667
|
+
sessionToRestore = await getLatestSessionFromJsonl(workdir);
|
|
668
|
+
if (!sessionToRestore) {
|
|
669
|
+
console.error(`No previous session found for workdir: ${workdir}`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (sessionToRestore) {
|
|
675
|
+
console.log(`Restoring session: ${sessionToRestore.id}`);
|
|
676
|
+
|
|
677
|
+
// // Initialize from session data
|
|
678
|
+
// this.initializeFromSession();
|
|
679
|
+
return sessionToRestore;
|
|
680
|
+
}
|
|
681
|
+
} catch (error) {
|
|
682
|
+
console.error("Failed to restore session:", error);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
}
|