wave-agent-sdk 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +19 -9
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/services/session.js +2 -9
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +10 -1
- package/dist/utils/messageOperations.js +19 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1180
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -922
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -641
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
package/src/services/session.ts
DELETED
|
@@ -1,922 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Management Service - JSONL Format Implementation
|
|
3
|
-
*
|
|
4
|
-
* OPTIMIZED IMPLEMENTATION (Phase 6 Complete):
|
|
5
|
-
* - Filename-based session type identification
|
|
6
|
-
* - Minimal file I/O for metadata extraction
|
|
7
|
-
* - Eliminated metadata headers for cleaner session files
|
|
8
|
-
* - Backward compatible with existing session files
|
|
9
|
-
* - 8-10x performance improvement in session listing operations
|
|
10
|
-
*
|
|
11
|
-
* Key Features:
|
|
12
|
-
* - Session creation without metadata headers
|
|
13
|
-
* - Subagent sessions identified by filename prefix
|
|
14
|
-
* - Performance-optimized session listing
|
|
15
|
-
* - Full backward compatibility maintained
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { promises as fs } from "fs";
|
|
19
|
-
import { join } from "path";
|
|
20
|
-
import { homedir } from "os";
|
|
21
|
-
import { randomUUID } from "crypto";
|
|
22
|
-
import type { Message } from "../types/index.js";
|
|
23
|
-
import { PathEncoder } from "../utils/pathEncoder.js";
|
|
24
|
-
import { JsonlHandler } from "../services/jsonlHandler.js";
|
|
25
|
-
import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
|
|
26
|
-
import { logger } from "../utils/globalLogger.js";
|
|
27
|
-
import { getMessageContent } from "../utils/messageOperations.js";
|
|
28
|
-
|
|
29
|
-
export interface SessionData {
|
|
30
|
-
id: string;
|
|
31
|
-
messages: Message[];
|
|
32
|
-
metadata: {
|
|
33
|
-
workdir: string;
|
|
34
|
-
lastActiveAt: string;
|
|
35
|
-
latestTotalTokens: number;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface SessionMetadata {
|
|
40
|
-
id: string;
|
|
41
|
-
sessionType: "main" | "subagent";
|
|
42
|
-
subagentType?: string;
|
|
43
|
-
workdir: string;
|
|
44
|
-
createdAt: Date;
|
|
45
|
-
lastActiveAt: Date;
|
|
46
|
-
latestTotalTokens: number;
|
|
47
|
-
firstMessage?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Generate a new session ID using Node.js native crypto.randomUUID()
|
|
52
|
-
* @returns UUID string for session identification
|
|
53
|
-
*/
|
|
54
|
-
export function generateSessionId(): string {
|
|
55
|
-
return randomUUID();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Generate filename for subagent sessions
|
|
60
|
-
* @param sessionId - UUID session identifier
|
|
61
|
-
* @returns Filename with subagent prefix for subagent sessions
|
|
62
|
-
*/
|
|
63
|
-
export function generateSubagentFilename(sessionId: string): string {
|
|
64
|
-
return `subagent-${sessionId}.jsonl`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Constants
|
|
68
|
-
export const SESSION_DIR = join(homedir(), ".wave", "projects");
|
|
69
|
-
const MAX_SESSION_AGE_DAYS = 14;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Ensure session directory exists
|
|
73
|
-
*/
|
|
74
|
-
export async function ensureSessionDir(): Promise<void> {
|
|
75
|
-
try {
|
|
76
|
-
await fs.mkdir(SESSION_DIR, { recursive: true });
|
|
77
|
-
} catch (error) {
|
|
78
|
-
throw new Error(`Failed to create session directory: ${error}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Generate session file path without creating directories
|
|
84
|
-
* @param sessionId - UUID session identifier
|
|
85
|
-
* @param workdir - Working directory for the session
|
|
86
|
-
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
87
|
-
* @returns Promise resolving to full file path for the session JSONL file
|
|
88
|
-
*/
|
|
89
|
-
export async function generateSessionFilePath(
|
|
90
|
-
sessionId: string,
|
|
91
|
-
workdir: string,
|
|
92
|
-
sessionType: "main" | "subagent" = "main",
|
|
93
|
-
): Promise<string> {
|
|
94
|
-
const encoder = new PathEncoder();
|
|
95
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
96
|
-
|
|
97
|
-
// Generate filename based on session type
|
|
98
|
-
const jsonlHandler = new JsonlHandler();
|
|
99
|
-
const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
|
|
100
|
-
|
|
101
|
-
return join(projectDir.encodedPath, filename);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Generate session file path using project-based directory structure
|
|
106
|
-
* @param sessionId - UUID session identifier
|
|
107
|
-
* @param workdir - Working directory for the session
|
|
108
|
-
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
109
|
-
* @returns Promise resolving to full file path for the session JSONL file
|
|
110
|
-
*/
|
|
111
|
-
export async function getSessionFilePath(
|
|
112
|
-
sessionId: string,
|
|
113
|
-
workdir: string,
|
|
114
|
-
sessionType: "main" | "subagent" = "main",
|
|
115
|
-
): Promise<string> {
|
|
116
|
-
const encoder = new PathEncoder();
|
|
117
|
-
const projectDir = await encoder.createProjectDirectory(workdir, SESSION_DIR);
|
|
118
|
-
|
|
119
|
-
// Generate filename based on session type
|
|
120
|
-
const jsonlHandler = new JsonlHandler();
|
|
121
|
-
const filename = jsonlHandler.generateSessionFilename(sessionId, sessionType);
|
|
122
|
-
|
|
123
|
-
return join(projectDir.encodedPath, filename);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Create a new session
|
|
128
|
-
* @param sessionId - UUID session identifier
|
|
129
|
-
* @param workdir - Working directory for the session
|
|
130
|
-
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
131
|
-
*/
|
|
132
|
-
export async function createSession(
|
|
133
|
-
sessionId: string,
|
|
134
|
-
workdir: string,
|
|
135
|
-
sessionType: "main" | "subagent" = "main",
|
|
136
|
-
): Promise<void> {
|
|
137
|
-
const jsonlHandler = new JsonlHandler();
|
|
138
|
-
const filePath = await getSessionFilePath(sessionId, workdir, sessionType);
|
|
139
|
-
await jsonlHandler.createSession(filePath);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Append messages to session using JSONL format (new approach)
|
|
144
|
-
*
|
|
145
|
-
* @param sessionId - UUID session identifier
|
|
146
|
-
* @param newMessages - Array of messages to append
|
|
147
|
-
* @param workdir - Working directory for the session
|
|
148
|
-
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
149
|
-
*/
|
|
150
|
-
export async function appendMessages(
|
|
151
|
-
sessionId: string,
|
|
152
|
-
newMessages: Message[],
|
|
153
|
-
workdir: string,
|
|
154
|
-
sessionType: "main" | "subagent" = "main",
|
|
155
|
-
): Promise<void> {
|
|
156
|
-
// Do not save session files in test environment
|
|
157
|
-
if (process.env.NODE_ENV === "test") {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Do not save if there are no messages
|
|
162
|
-
if (newMessages.length === 0) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const jsonlHandler = new JsonlHandler();
|
|
167
|
-
|
|
168
|
-
// Generate the session file path directly using known session type
|
|
169
|
-
const filePath = await generateSessionFilePath(
|
|
170
|
-
sessionId,
|
|
171
|
-
workdir,
|
|
172
|
-
sessionType,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Check if the session file exists
|
|
176
|
-
try {
|
|
177
|
-
await fs.access(filePath);
|
|
178
|
-
} catch {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`Session file not found: ${sessionId}. Use createSession() to create a new session first.`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
await jsonlHandler.append(filePath, newMessages, {
|
|
185
|
-
atomic: false,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Load session data from JSONL file (new approach)
|
|
191
|
-
*
|
|
192
|
-
* @param sessionId - UUID session identifier
|
|
193
|
-
* @param workdir - Working directory for the session
|
|
194
|
-
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
195
|
-
* @returns Promise that resolves to session data or null if session doesn't exist
|
|
196
|
-
*/
|
|
197
|
-
export async function loadSessionFromJsonl(
|
|
198
|
-
sessionId: string,
|
|
199
|
-
workdir: string,
|
|
200
|
-
sessionType: "main" | "subagent" = "main",
|
|
201
|
-
): Promise<SessionData | null> {
|
|
202
|
-
try {
|
|
203
|
-
const jsonlHandler = new JsonlHandler();
|
|
204
|
-
|
|
205
|
-
// Generate the session file path directly using known session type
|
|
206
|
-
const filePath = await generateSessionFilePath(
|
|
207
|
-
sessionId,
|
|
208
|
-
workdir,
|
|
209
|
-
sessionType,
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
// Check if file exists
|
|
213
|
-
try {
|
|
214
|
-
await fs.access(filePath);
|
|
215
|
-
} catch (error) {
|
|
216
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const allMessages = await jsonlHandler.read(filePath);
|
|
223
|
-
|
|
224
|
-
// Find the last compact boundary — only return messages from there forward
|
|
225
|
-
let lastCompactIndex = -1;
|
|
226
|
-
for (let i = allMessages.length - 1; i >= 0; i--) {
|
|
227
|
-
if (allMessages[i].blocks?.some((b) => b.type === "compact")) {
|
|
228
|
-
lastCompactIndex = i;
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const messages =
|
|
233
|
-
lastCompactIndex >= 0 ? allMessages.slice(lastCompactIndex) : allMessages;
|
|
234
|
-
|
|
235
|
-
// Extract metadata from messages
|
|
236
|
-
const lastMessage =
|
|
237
|
-
messages.length > 0 ? messages[messages.length - 1] : null;
|
|
238
|
-
|
|
239
|
-
const sessionData: SessionData = {
|
|
240
|
-
id: sessionId,
|
|
241
|
-
messages,
|
|
242
|
-
metadata: {
|
|
243
|
-
workdir,
|
|
244
|
-
lastActiveAt: lastMessage
|
|
245
|
-
? lastMessage.timestamp
|
|
246
|
-
: new Date().toISOString(),
|
|
247
|
-
latestTotalTokens: lastMessage?.usage
|
|
248
|
-
? extractLatestTotalTokens([lastMessage])
|
|
249
|
-
: 0,
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
return sessionData;
|
|
254
|
-
} catch (error) {
|
|
255
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
256
|
-
|
|
257
|
-
// Check if the underlying error is ENOENT (file doesn't exist)
|
|
258
|
-
if (
|
|
259
|
-
errorMessage.includes("ENOENT") ||
|
|
260
|
-
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
261
|
-
) {
|
|
262
|
-
return null; // Session file does not exist
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Check for JSON parsing errors (corrupted files)
|
|
266
|
-
if (
|
|
267
|
-
errorMessage.includes("Invalid JSON") ||
|
|
268
|
-
errorMessage.includes("Unexpected token")
|
|
269
|
-
) {
|
|
270
|
-
return null; // Treat corrupted files as non-existent
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
throw new Error(`Failed to load session ${sessionId}: ${error}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Get the most recently active session for a specific working directory (new JSONL approach)
|
|
279
|
-
* Only returns main sessions, skips subagent sessions
|
|
280
|
-
* Uses listSessionsFromJsonl which already sorts sessions by last active time (most recent first)
|
|
281
|
-
*
|
|
282
|
-
* @param workdir - Working directory to find the most recently active session for
|
|
283
|
-
* @returns Promise that resolves to the most recently active session data or null if no sessions exist
|
|
284
|
-
*/
|
|
285
|
-
export async function getLatestSessionFromJsonl(
|
|
286
|
-
workdir: string,
|
|
287
|
-
): Promise<SessionData | null> {
|
|
288
|
-
const sessions = await listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
|
|
289
|
-
|
|
290
|
-
if (sessions.length === 0) {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Sessions are already sorted by lastActiveAt from listSessionsFromJsonl (most recent first)
|
|
295
|
-
const latestSession = sessions[0];
|
|
296
|
-
return loadSessionFromJsonl(latestSession.id, workdir);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* List all sessions for a specific working directory (convenience wrapper)
|
|
301
|
-
* Only returns main sessions, skips subagent sessions
|
|
302
|
-
*
|
|
303
|
-
* @param workdir - Working directory to filter sessions by
|
|
304
|
-
* @returns Promise that resolves to array of session metadata objects
|
|
305
|
-
*/
|
|
306
|
-
export async function listSessions(
|
|
307
|
-
workdir: string,
|
|
308
|
-
): Promise<SessionMetadata[]> {
|
|
309
|
-
return listSessionsFromJsonl(workdir); // Excludes subagent sessions by default
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* List all sessions for a specific working directory using JSONL format (optimized approach)
|
|
314
|
-
*
|
|
315
|
-
* PERFORMANCE OPTIMIZATION:
|
|
316
|
-
* - Uses filename parsing exclusively for session metadata
|
|
317
|
-
* - Only reads last message for timestamps and token counts
|
|
318
|
-
* - Eliminates O(n*2) file operations, achieving O(n) performance
|
|
319
|
-
* - Returns simplified session metadata objects
|
|
320
|
-
* - Only includes main sessions, excludes subagent sessions
|
|
321
|
-
*
|
|
322
|
-
* @param workdir - Working directory to filter sessions by
|
|
323
|
-
* @returns Promise that resolves to array of session metadata objects
|
|
324
|
-
*/
|
|
325
|
-
export async function listSessionsFromJsonl(
|
|
326
|
-
workdir: string,
|
|
327
|
-
): Promise<SessionMetadata[]> {
|
|
328
|
-
try {
|
|
329
|
-
const encoder = new PathEncoder();
|
|
330
|
-
const baseDir = SESSION_DIR;
|
|
331
|
-
|
|
332
|
-
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
333
|
-
|
|
334
|
-
const sevenDaysAgo = new Date();
|
|
335
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
336
|
-
|
|
337
|
-
let files: string[];
|
|
338
|
-
try {
|
|
339
|
-
files = await fs.readdir(projectDir.encodedPath);
|
|
340
|
-
} catch (error) {
|
|
341
|
-
// If project directory doesn't exist, return empty array
|
|
342
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
345
|
-
throw error;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const sessions: SessionMetadata[] = [];
|
|
349
|
-
|
|
350
|
-
for (const file of files) {
|
|
351
|
-
if (!file.endsWith(".jsonl")) {
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// EARLY FILTERING: Skip subagent sessions by filename prefix for maximum performance
|
|
356
|
-
if (file.startsWith("subagent-")) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
const filePath = join(projectDir.encodedPath, file);
|
|
362
|
-
|
|
363
|
-
// Validate main session filename format (UUID.jsonl)
|
|
364
|
-
const uuidMatch = file.match(
|
|
365
|
-
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
366
|
-
);
|
|
367
|
-
if (!uuidMatch) {
|
|
368
|
-
continue; // Skip invalid filenames
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const sessionId = uuidMatch[1];
|
|
372
|
-
|
|
373
|
-
// PERFORMANCE OPTIMIZATION: Only read the last message for timestamps and tokens
|
|
374
|
-
const jsonlHandler = new JsonlHandler();
|
|
375
|
-
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
376
|
-
|
|
377
|
-
// Handle timing information efficiently
|
|
378
|
-
let lastActiveAt: Date;
|
|
379
|
-
|
|
380
|
-
if (lastMessage) {
|
|
381
|
-
lastActiveAt = new Date(lastMessage.timestamp);
|
|
382
|
-
} else {
|
|
383
|
-
// Empty session file - use file modification time
|
|
384
|
-
const stats = await fs.stat(filePath);
|
|
385
|
-
lastActiveAt = stats.mtime;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (lastActiveAt < sevenDaysAgo) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Return inline object for performance (no interface instantiation overhead)
|
|
393
|
-
const sessionMeta: SessionMetadata = {
|
|
394
|
-
id: sessionId,
|
|
395
|
-
sessionType: "main",
|
|
396
|
-
subagentType: undefined,
|
|
397
|
-
workdir: projectDir.originalPath,
|
|
398
|
-
createdAt: new Date(),
|
|
399
|
-
lastActiveAt,
|
|
400
|
-
latestTotalTokens: lastMessage?.usage
|
|
401
|
-
? extractLatestTotalTokens([lastMessage])
|
|
402
|
-
: 0,
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
// Try to get first message content for display
|
|
406
|
-
try {
|
|
407
|
-
const firstContent = await getFirstMessageContent(sessionId, workdir);
|
|
408
|
-
if (firstContent) {
|
|
409
|
-
sessionMeta.firstMessage = firstContent;
|
|
410
|
-
}
|
|
411
|
-
} catch {
|
|
412
|
-
// Ignore errors getting first message
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
sessions.push(sessionMeta);
|
|
416
|
-
} catch {
|
|
417
|
-
// Skip corrupted session files
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Sort by last active time (most recently active first)
|
|
423
|
-
return sessions.sort(
|
|
424
|
-
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
425
|
-
);
|
|
426
|
-
} catch (error) {
|
|
427
|
-
throw new Error(`Failed to list sessions: ${error}`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* List all sessions across all project directories
|
|
433
|
-
*
|
|
434
|
-
* @returns Promise that resolves to array of session metadata objects
|
|
435
|
-
*/
|
|
436
|
-
export async function listAllSessions(): Promise<SessionMetadata[]> {
|
|
437
|
-
try {
|
|
438
|
-
const baseDir = SESSION_DIR;
|
|
439
|
-
let projectDirs: string[];
|
|
440
|
-
try {
|
|
441
|
-
projectDirs = await fs.readdir(baseDir);
|
|
442
|
-
} catch (error) {
|
|
443
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
444
|
-
return [];
|
|
445
|
-
}
|
|
446
|
-
throw error;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const allSessions: SessionMetadata[] = [];
|
|
450
|
-
const sevenDaysAgo = new Date();
|
|
451
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
452
|
-
|
|
453
|
-
for (const projectDirName of projectDirs) {
|
|
454
|
-
const projectPath = join(baseDir, projectDirName);
|
|
455
|
-
try {
|
|
456
|
-
const stat = await fs.stat(projectPath);
|
|
457
|
-
if (!stat.isDirectory()) {
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Scan .jsonl files in the project directory
|
|
462
|
-
const files = await fs.readdir(projectPath);
|
|
463
|
-
|
|
464
|
-
for (const file of files) {
|
|
465
|
-
if (!file.endsWith(".jsonl") || file.startsWith("subagent-")) {
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
const filePath = join(projectPath, file);
|
|
471
|
-
const uuidMatch = file.match(
|
|
472
|
-
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
473
|
-
);
|
|
474
|
-
if (!uuidMatch) continue;
|
|
475
|
-
|
|
476
|
-
const sessionId = uuidMatch[1];
|
|
477
|
-
const jsonlHandler = new JsonlHandler();
|
|
478
|
-
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
479
|
-
|
|
480
|
-
let lastActiveAt: Date;
|
|
481
|
-
if (lastMessage) {
|
|
482
|
-
lastActiveAt = new Date(lastMessage.timestamp);
|
|
483
|
-
} else {
|
|
484
|
-
const stats = await fs.stat(filePath);
|
|
485
|
-
lastActiveAt = stats.mtime;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (lastActiveAt < sevenDaysAgo) continue;
|
|
489
|
-
|
|
490
|
-
allSessions.push({
|
|
491
|
-
id: sessionId,
|
|
492
|
-
sessionType: "main" as const,
|
|
493
|
-
subagentType: undefined,
|
|
494
|
-
workdir: projectDirName,
|
|
495
|
-
createdAt: new Date(),
|
|
496
|
-
lastActiveAt,
|
|
497
|
-
latestTotalTokens: lastMessage?.usage
|
|
498
|
-
? extractLatestTotalTokens([lastMessage])
|
|
499
|
-
: 0,
|
|
500
|
-
});
|
|
501
|
-
} catch {
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
} catch {
|
|
506
|
-
// Skip if stat/readdir fails
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return allSessions.sort(
|
|
511
|
-
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
512
|
-
);
|
|
513
|
-
} catch (error) {
|
|
514
|
-
throw new Error(`Failed to list all sessions: ${error}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Clean up expired sessions older than 14 days based on file modification time
|
|
520
|
-
*
|
|
521
|
-
* @param workdir - Working directory to clean up sessions for
|
|
522
|
-
* @returns Promise that resolves to the number of sessions that were deleted
|
|
523
|
-
*/
|
|
524
|
-
export async function cleanupExpiredSessionsFromJsonl(
|
|
525
|
-
workdir: string,
|
|
526
|
-
): Promise<number> {
|
|
527
|
-
// Do not perform cleanup operations in test environment
|
|
528
|
-
if (process.env.NODE_ENV === "test") {
|
|
529
|
-
return 0;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
try {
|
|
533
|
-
const encoder = new PathEncoder();
|
|
534
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
535
|
-
const files = await fs.readdir(projectDir.encodedPath);
|
|
536
|
-
|
|
537
|
-
const now = new Date();
|
|
538
|
-
const maxAge = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000; // Convert to milliseconds
|
|
539
|
-
let deletedCount = 0;
|
|
540
|
-
|
|
541
|
-
for (const file of files) {
|
|
542
|
-
if (!file.endsWith(".jsonl")) {
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const filePath = join(projectDir.encodedPath, file);
|
|
547
|
-
|
|
548
|
-
try {
|
|
549
|
-
const stat = await fs.stat(filePath);
|
|
550
|
-
const fileAge = now.getTime() - stat.mtime.getTime();
|
|
551
|
-
|
|
552
|
-
if (fileAge > maxAge) {
|
|
553
|
-
await fs.unlink(filePath);
|
|
554
|
-
deletedCount++;
|
|
555
|
-
}
|
|
556
|
-
} catch {
|
|
557
|
-
// Skip failed operations and continue processing other files
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Clean up empty project directory if no files remain
|
|
563
|
-
try {
|
|
564
|
-
const remainingFiles = await fs.readdir(projectDir.encodedPath);
|
|
565
|
-
if (remainingFiles.length === 0) {
|
|
566
|
-
await fs.rmdir(projectDir.encodedPath);
|
|
567
|
-
}
|
|
568
|
-
} catch {
|
|
569
|
-
// Ignore errors if directory is not empty or can't be removed
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return deletedCount;
|
|
573
|
-
} catch {
|
|
574
|
-
// Return 0 if project directory doesn't exist or can't be accessed
|
|
575
|
-
return 0;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Clean up empty project directories in the session directory
|
|
581
|
-
*/
|
|
582
|
-
export async function cleanupEmptyProjectDirectories(): Promise<void> {
|
|
583
|
-
// Do not perform cleanup operations in test environment
|
|
584
|
-
if (process.env.NODE_ENV === "test") {
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
try {
|
|
589
|
-
const baseDir = SESSION_DIR;
|
|
590
|
-
const projectDirs = await fs.readdir(baseDir);
|
|
591
|
-
|
|
592
|
-
for (const projectDirName of projectDirs) {
|
|
593
|
-
const projectPath = join(baseDir, projectDirName);
|
|
594
|
-
const stat = await fs.stat(projectPath);
|
|
595
|
-
|
|
596
|
-
if (stat.isDirectory()) {
|
|
597
|
-
try {
|
|
598
|
-
const files = await fs.readdir(projectPath);
|
|
599
|
-
|
|
600
|
-
// If directory is empty, remove it
|
|
601
|
-
if (files.length === 0) {
|
|
602
|
-
await fs.rmdir(projectPath);
|
|
603
|
-
}
|
|
604
|
-
} catch {
|
|
605
|
-
// Skip errors for directories we can't read or remove
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
} catch {
|
|
611
|
-
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* Clean up "ghost" session files that contain only meta messages
|
|
617
|
-
* (isMeta: true) — e.g. sessions where a SessionStart hook injected a
|
|
618
|
-
* system-reminder but no real user/assistant message was ever sent.
|
|
619
|
-
*
|
|
620
|
-
* Such sessions are no longer created thanks to lazy materialization in
|
|
621
|
-
* saveSession(); this one-time sweep removes files that predate that change
|
|
622
|
-
* so they stop showing up as "0 tokens / No content" entries in the resume
|
|
623
|
-
* list.
|
|
624
|
-
*
|
|
625
|
-
* @returns Promise that resolves to the number of files deleted
|
|
626
|
-
*/
|
|
627
|
-
export async function cleanupMetaOnlySessions(): Promise<number> {
|
|
628
|
-
// Do not perform cleanup operations in test environment
|
|
629
|
-
if (process.env.NODE_ENV === "test") {
|
|
630
|
-
return 0;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
let deletedCount = 0;
|
|
634
|
-
try {
|
|
635
|
-
const projectDirs = await fs.readdir(SESSION_DIR);
|
|
636
|
-
|
|
637
|
-
for (const projectDirName of projectDirs) {
|
|
638
|
-
const projectPath = join(SESSION_DIR, projectDirName);
|
|
639
|
-
try {
|
|
640
|
-
const stat = await fs.stat(projectPath);
|
|
641
|
-
if (!stat.isDirectory()) {
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const files = await fs.readdir(projectPath);
|
|
646
|
-
for (const file of files) {
|
|
647
|
-
if (!file.endsWith(".jsonl")) {
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const filePath = join(projectPath, file);
|
|
652
|
-
try {
|
|
653
|
-
// Fast path: a file whose last message is not meta contains a
|
|
654
|
-
// real message, so it can never be meta-only.
|
|
655
|
-
const jsonlHandler = new JsonlHandler();
|
|
656
|
-
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
657
|
-
if (!lastMessage?.isMeta) {
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
const messages = await jsonlHandler.read(filePath);
|
|
662
|
-
if (messages.length > 0 && messages.every((m) => m.isMeta)) {
|
|
663
|
-
await fs.unlink(filePath);
|
|
664
|
-
deletedCount++;
|
|
665
|
-
}
|
|
666
|
-
} catch {
|
|
667
|
-
// Skip corrupted or unreadable files
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
} catch {
|
|
672
|
-
// Skip directories we can't access
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
} catch {
|
|
677
|
-
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
return deletedCount;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Check if a session exists in JSONL storage (new approach)
|
|
685
|
-
*
|
|
686
|
-
* @param sessionId - UUID session identifier
|
|
687
|
-
* @param workdir - Working directory for the session
|
|
688
|
-
* @param sessionType - Type of session ("main" or "subagent"). If not provided, checks both types.
|
|
689
|
-
* @returns Promise that resolves to true if session exists, false otherwise
|
|
690
|
-
*/
|
|
691
|
-
export async function sessionExistsInJsonl(
|
|
692
|
-
sessionId: string,
|
|
693
|
-
workdir: string,
|
|
694
|
-
sessionType?: "main" | "subagent",
|
|
695
|
-
): Promise<boolean> {
|
|
696
|
-
try {
|
|
697
|
-
if (sessionType) {
|
|
698
|
-
// If session type is known, check directly
|
|
699
|
-
const filePath = await generateSessionFilePath(
|
|
700
|
-
sessionId,
|
|
701
|
-
workdir,
|
|
702
|
-
sessionType,
|
|
703
|
-
);
|
|
704
|
-
await fs.access(filePath);
|
|
705
|
-
return true;
|
|
706
|
-
} else {
|
|
707
|
-
// If session type is unknown, try both
|
|
708
|
-
const mainPath = await generateSessionFilePath(
|
|
709
|
-
sessionId,
|
|
710
|
-
workdir,
|
|
711
|
-
"main",
|
|
712
|
-
);
|
|
713
|
-
try {
|
|
714
|
-
await fs.access(mainPath);
|
|
715
|
-
return true;
|
|
716
|
-
} catch {
|
|
717
|
-
const subagentPath = await generateSessionFilePath(
|
|
718
|
-
sessionId,
|
|
719
|
-
workdir,
|
|
720
|
-
"subagent",
|
|
721
|
-
);
|
|
722
|
-
try {
|
|
723
|
-
await fs.access(subagentPath);
|
|
724
|
-
return true;
|
|
725
|
-
} catch {
|
|
726
|
-
return false;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
} catch {
|
|
731
|
-
return false;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Get the content of the first non-meta message in a session
|
|
737
|
-
* For user role: get text block content
|
|
738
|
-
* For assistant role: get compact block content
|
|
739
|
-
* Skips meta messages (isMeta: true) to find the first meaningful message
|
|
740
|
-
* @param sessionId - Session ID to get first message from
|
|
741
|
-
* @param workdir - Working directory for session operations
|
|
742
|
-
* @returns Promise that resolves to the first non-meta message content or null if not found
|
|
743
|
-
*/
|
|
744
|
-
export async function getFirstMessageContent(
|
|
745
|
-
sessionId: string,
|
|
746
|
-
workdir: string,
|
|
747
|
-
): Promise<string | null> {
|
|
748
|
-
try {
|
|
749
|
-
const encoder = new PathEncoder();
|
|
750
|
-
const baseDir = SESSION_DIR;
|
|
751
|
-
|
|
752
|
-
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
753
|
-
const filePath = join(projectDir.encodedPath, `${sessionId}.jsonl`);
|
|
754
|
-
|
|
755
|
-
// Read first N lines to skip meta messages
|
|
756
|
-
const { readFirstNLines } = await import("../utils/fileUtils.js");
|
|
757
|
-
const lines = await readFirstNLines(filePath, 10);
|
|
758
|
-
|
|
759
|
-
for (const line of lines) {
|
|
760
|
-
try {
|
|
761
|
-
const message = JSON.parse(line) as Message;
|
|
762
|
-
|
|
763
|
-
// Skip meta messages
|
|
764
|
-
if (message.isMeta) {
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const content = getMessageContent(message);
|
|
769
|
-
if (content) {
|
|
770
|
-
return content;
|
|
771
|
-
}
|
|
772
|
-
} catch (error) {
|
|
773
|
-
logger.warn(`Failed to parse message in session ${sessionId}:`, error);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return null;
|
|
778
|
-
} catch (error) {
|
|
779
|
-
logger.warn(
|
|
780
|
-
`Failed to get first message content for session ${sessionId}:`,
|
|
781
|
-
error,
|
|
782
|
-
);
|
|
783
|
-
return null;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* Delete a session
|
|
789
|
-
* @param sessionId - UUID session identifier
|
|
790
|
-
* @param workdir - Working directory for the session
|
|
791
|
-
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
792
|
-
*/
|
|
793
|
-
export async function deleteSession(
|
|
794
|
-
sessionId: string,
|
|
795
|
-
workdir: string,
|
|
796
|
-
sessionType: "main" | "subagent" = "main",
|
|
797
|
-
): Promise<void> {
|
|
798
|
-
const filePath = await generateSessionFilePath(
|
|
799
|
-
sessionId,
|
|
800
|
-
workdir,
|
|
801
|
-
sessionType,
|
|
802
|
-
);
|
|
803
|
-
try {
|
|
804
|
-
await fs.unlink(filePath);
|
|
805
|
-
} catch (error) {
|
|
806
|
-
// Ignore if file doesn't exist
|
|
807
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
808
|
-
throw error;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* Truncate content to a maximum length, adding ellipsis if truncated.
|
|
815
|
-
* Also replaces real newlines with the literal string "\n".
|
|
816
|
-
* @param content - The content to truncate
|
|
817
|
-
* @param maxLength - Maximum length before truncation (default: 100)
|
|
818
|
-
* @returns Truncated content with ellipsis if needed
|
|
819
|
-
*/
|
|
820
|
-
export function truncateContent(
|
|
821
|
-
content: string,
|
|
822
|
-
maxLength: number = 100,
|
|
823
|
-
): string {
|
|
824
|
-
// Replace real newlines with literal "\n"
|
|
825
|
-
const singleLineContent = content.replace(/\n/g, "\\n");
|
|
826
|
-
|
|
827
|
-
if (singleLineContent.length <= maxLength) {
|
|
828
|
-
return singleLineContent;
|
|
829
|
-
}
|
|
830
|
-
return singleLineContent.substring(0, maxLength) + "...";
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Handle session restoration logic
|
|
835
|
-
* @param restoreSessionId - Specific session ID to restore
|
|
836
|
-
* @param continueLastSession - Whether to continue the most recent session
|
|
837
|
-
* @param workdir - Working directory for session restoration
|
|
838
|
-
* @returns Promise that resolves to session data or undefined
|
|
839
|
-
*/
|
|
840
|
-
export async function handleSessionRestoration(
|
|
841
|
-
restoreSessionId?: string,
|
|
842
|
-
continueLastSession?: boolean,
|
|
843
|
-
workdir?: string,
|
|
844
|
-
): Promise<SessionData | undefined> {
|
|
845
|
-
if (!workdir) {
|
|
846
|
-
throw new Error("Working directory is required for session restoration");
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// Clean up expired sessions first
|
|
850
|
-
cleanupExpiredSessionsFromJsonl(workdir).catch((error) => {
|
|
851
|
-
logger.warn("Failed to cleanup expired sessions:", error);
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
if (!restoreSessionId && !continueLastSession) {
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
try {
|
|
859
|
-
let sessionToRestore: SessionData | null = null;
|
|
860
|
-
|
|
861
|
-
if (restoreSessionId) {
|
|
862
|
-
// Use only JSONL format - no legacy support
|
|
863
|
-
sessionToRestore = await loadSessionFromJsonl(restoreSessionId, workdir);
|
|
864
|
-
if (!sessionToRestore) {
|
|
865
|
-
// Session doesn't exist on disk (e.g. new project with no messages saved yet).
|
|
866
|
-
// Gracefully fall back to starting fresh instead of throwing.
|
|
867
|
-
logger?.warn(
|
|
868
|
-
`Session ${restoreSessionId} not found on disk, starting fresh session`,
|
|
869
|
-
);
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
} else if (continueLastSession) {
|
|
873
|
-
// Use only JSONL format - no legacy support
|
|
874
|
-
sessionToRestore = await getLatestSessionFromJsonl(workdir);
|
|
875
|
-
if (!sessionToRestore) {
|
|
876
|
-
throw new Error(`No previous session found for workdir: ${workdir}`);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (sessionToRestore) {
|
|
881
|
-
console.log(`Restoring session: ${sessionToRestore.id}`);
|
|
882
|
-
|
|
883
|
-
// // Initialize from session data
|
|
884
|
-
// this.initializeFromSession();
|
|
885
|
-
return sessionToRestore;
|
|
886
|
-
}
|
|
887
|
-
} catch (error) {
|
|
888
|
-
console.error("Failed to restore session:", error);
|
|
889
|
-
throw error;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
/**
|
|
894
|
-
* Load the full message thread for a session.
|
|
895
|
-
* With append-only compaction, all messages are in a single file.
|
|
896
|
-
* Unlike loadSessionFromJsonl, this returns every message in the file,
|
|
897
|
-
* including those before the last compact boundary — rewind needs the
|
|
898
|
-
* complete history to allow rewinding past compaction points.
|
|
899
|
-
* @param currentSessionId - The ID of the current session
|
|
900
|
-
* @param workdir - Working directory for the session
|
|
901
|
-
* @returns Promise that resolves to an array of all messages in the thread
|
|
902
|
-
*/
|
|
903
|
-
export async function loadFullMessageThread(
|
|
904
|
-
currentSessionId: string,
|
|
905
|
-
workdir: string,
|
|
906
|
-
): Promise<{ messages: Message[]; sessionIds: string[] }> {
|
|
907
|
-
const jsonlHandler = new JsonlHandler();
|
|
908
|
-
const filePath = await generateSessionFilePath(
|
|
909
|
-
currentSessionId,
|
|
910
|
-
workdir,
|
|
911
|
-
"main",
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
try {
|
|
915
|
-
await fs.access(filePath);
|
|
916
|
-
} catch {
|
|
917
|
-
return { messages: [], sessionIds: [] };
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const messages = await jsonlHandler.read(filePath);
|
|
921
|
-
return { messages, sessionIds: [currentSessionId] };
|
|
922
|
-
}
|