wave-agent-sdk 0.18.7 → 0.19.0
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/builtin/skills/settings/ENV.md +0 -1
- package/builtin/skills/settings/HOOKS.md +10 -0
- package/builtin/skills/settings/MODELS.md +37 -0
- package/builtin/skills/settings/SKILLS.md +6 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +20 -4
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +7 -2
- package/dist/managers/messageManager.d.ts +3 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +17 -77
- package/dist/managers/skillManager.d.ts +1 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +11 -2
- package/dist/prompts/index.d.ts +3 -2
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +5 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +13 -9
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +22 -9
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +6 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +1 -1
- package/dist/services/interactionService.js +1 -1
- package/dist/services/reversionService.d.ts +5 -1
- package/dist/services/reversionService.d.ts.map +1 -1
- package/dist/services/reversionService.js +50 -22
- package/dist/services/session.d.ts +4 -13
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +59 -215
- package/dist/telemetry/sessionTracing.d.ts +14 -4
- package/dist/telemetry/sessionTracing.d.ts.map +1 -1
- package/dist/telemetry/sessionTracing.js +134 -8
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +16 -1
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +0 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +14 -1
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/utils/atomicWrite.d.ts +15 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +59 -0
- package/dist/utils/cacheControlUtils.d.ts +3 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +4 -35
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +5 -1
- package/dist/utils/convertMessagesForAPI.d.ts +9 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +74 -55
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +2 -1
- package/dist/utils/modelCapabilities.d.ts +22 -0
- package/dist/utils/modelCapabilities.d.ts.map +1 -0
- package/dist/utils/modelCapabilities.js +24 -0
- package/dist/utils/skillParser.d.ts.map +1 -1
- package/dist/utils/skillParser.js +3 -1
- package/package.json +1 -1
- package/src/agent.ts +11 -5
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +22 -2
- package/src/managers/mcpManager.ts +8 -2
- package/src/managers/messageManager.ts +22 -104
- package/src/managers/skillManager.ts +16 -2
- package/src/prompts/index.ts +6 -1
- package/src/services/aiService.ts +12 -5
- package/src/services/configurationService.ts +24 -24
- package/src/services/hook.ts +7 -0
- package/src/services/initializationService.ts +1 -3
- package/src/services/interactionService.ts +1 -1
- package/src/services/reversionService.ts +50 -27
- package/src/services/session.ts +63 -262
- package/src/telemetry/sessionTracing.ts +167 -12
- package/src/tools/bashTool.ts +23 -1
- package/src/types/config.ts +8 -0
- package/src/types/hooks.ts +1 -0
- package/src/types/messaging.ts +0 -1
- package/src/types/skills.ts +1 -0
- package/src/types/telemetry.ts +17 -1
- package/src/utils/atomicWrite.ts +61 -0
- package/src/utils/cacheControlUtils.ts +5 -38
- package/src/utils/containerSetup.ts +5 -1
- package/src/utils/convertMessagesForAPI.ts +87 -57
- package/src/utils/mcpUtils.ts +2 -1
- package/src/utils/modelCapabilities.ts +30 -0
- package/src/utils/skillParser.ts +3 -1
package/src/services/hook.ts
CHANGED
|
@@ -89,6 +89,13 @@ async function buildHookJsonInput(
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Add worktree_path field for WorktreeRemove events
|
|
93
|
+
if (context.event === "WorktreeRemove") {
|
|
94
|
+
if (context.worktreePath !== undefined) {
|
|
95
|
+
jsonInput.worktree_path = context.worktreePath;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
// Add SessionStart-specific fields
|
|
93
100
|
if (context.event === "SessionStart") {
|
|
94
101
|
if (context.source !== undefined) {
|
|
@@ -326,9 +326,7 @@ export class InitializationService {
|
|
|
326
326
|
messageManager.initializeFromSession(sessionToRestore);
|
|
327
327
|
|
|
328
328
|
// Update task manager with the root session ID to ensure continuity across compactions
|
|
329
|
-
taskManager.setTaskListId(
|
|
330
|
-
sessionToRestore.rootSessionId || sessionToRestore.id,
|
|
331
|
-
);
|
|
329
|
+
taskManager.setTaskListId(sessionToRestore.id);
|
|
332
330
|
|
|
333
331
|
// After session is initialized, load tasks for the session
|
|
334
332
|
const tasks = await taskManager.listTasks();
|
|
@@ -166,7 +166,7 @@ export class InteractionService {
|
|
|
166
166
|
messageManager.initializeFromSession(sessionData);
|
|
167
167
|
|
|
168
168
|
// Update task manager with the root session ID to ensure continuity across compactions
|
|
169
|
-
taskManager.setTaskListId(sessionData.
|
|
169
|
+
taskManager.setTaskListId(sessionData.id);
|
|
170
170
|
|
|
171
171
|
// 7. Load tasks for the restored session
|
|
172
172
|
const tasks = await taskManager.listTasks();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { readFile, writeFile, mkdir, rm, readdir, stat } from "fs/promises";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { FileSnapshot } from "../types/reversion.js";
|
|
@@ -9,50 +9,49 @@ export class ReversionService {
|
|
|
9
9
|
private rootSessionId: string;
|
|
10
10
|
|
|
11
11
|
constructor(rootSessionId: string) {
|
|
12
|
-
this.rootSessionId = rootSessionId;
|
|
12
|
+
this.rootSessionId = rootSessionId || "default";
|
|
13
13
|
this.historyBaseDir = join(
|
|
14
14
|
homedir(),
|
|
15
15
|
".wave",
|
|
16
16
|
"file-history",
|
|
17
|
-
rootSessionId
|
|
17
|
+
this.rootSessionId,
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
private getFilePathHash(filePath: string): string {
|
|
22
|
-
return createHash("
|
|
22
|
+
return createHash("sha256").update(filePath).digest("hex").slice(0, 16);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
private async getNextVersion(
|
|
25
|
+
private async getNextVersion(filePath: string): Promise<number> {
|
|
26
|
+
const hash = this.getFilePathHash(filePath);
|
|
27
|
+
const prefix = `${hash}@v`;
|
|
26
28
|
try {
|
|
27
|
-
const files = await
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const files = await readdir(this.historyBaseDir);
|
|
30
|
+
let maxVersion = 0;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
if (file.startsWith(prefix)) {
|
|
33
|
+
const v = parseInt(file.slice(prefix.length), 10);
|
|
34
|
+
if (!isNaN(v) && v > maxVersion) {
|
|
35
|
+
maxVersion = v;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return maxVersion + 1;
|
|
33
40
|
} catch {
|
|
34
41
|
return 1;
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
private async updateVersionsFile(
|
|
39
|
-
fileHashDir: string,
|
|
40
|
-
version: number,
|
|
41
|
-
): Promise<void> {
|
|
42
|
-
await appendFile(join(fileHashDir, "versions"), `${version}\n`, "utf-8");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
45
|
/**
|
|
46
46
|
* Saves a single snapshot to the file history directory.
|
|
47
47
|
* Returns the snapshot path.
|
|
48
48
|
*/
|
|
49
49
|
async saveSnapshot(snapshot: FileSnapshot): Promise<string> {
|
|
50
50
|
const fileHash = this.getFilePathHash(snapshot.filePath);
|
|
51
|
-
|
|
52
|
-
await this.ensureDirectory(fileHashDir);
|
|
51
|
+
await this.ensureDirectory(this.historyBaseDir);
|
|
53
52
|
|
|
54
|
-
const version = await this.getNextVersion(
|
|
55
|
-
const snapshotPath = join(
|
|
53
|
+
const version = await this.getNextVersion(snapshot.filePath);
|
|
54
|
+
const snapshotPath = join(this.historyBaseDir, `${fileHash}@v${version}`);
|
|
56
55
|
|
|
57
56
|
const snapshotWithContent = snapshot as FileSnapshot & {
|
|
58
57
|
content: string | null;
|
|
@@ -65,7 +64,6 @@ export class ReversionService {
|
|
|
65
64
|
return ""; // Return empty string to indicate no snapshot file
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
await this.updateVersionsFile(fileHashDir, version);
|
|
69
67
|
return snapshotPath;
|
|
70
68
|
}
|
|
71
69
|
|
|
@@ -90,10 +88,35 @@ export class ReversionService {
|
|
|
90
88
|
await rm(this.historyBaseDir, { recursive: true, force: true });
|
|
91
89
|
}
|
|
92
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Cleans up session directories older than the given number of days.
|
|
93
|
+
* Skips the current session.
|
|
94
|
+
*/
|
|
95
|
+
async cleanupOldSessions(days: number = 30): Promise<void> {
|
|
96
|
+
const threshold = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
97
|
+
const parentDir = dirname(this.historyBaseDir);
|
|
98
|
+
try {
|
|
99
|
+
const entries = await readdir(parentDir);
|
|
100
|
+
await Promise.all(
|
|
101
|
+
entries.map(async (entry) => {
|
|
102
|
+
if (entry === this.rootSessionId) return;
|
|
103
|
+
const dirPath = join(parentDir, entry);
|
|
104
|
+
try {
|
|
105
|
+
const stats = await stat(dirPath);
|
|
106
|
+
if (stats.isDirectory() && stats.mtimeMs < threshold) {
|
|
107
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// ENOENT — concurrently deleted, skip
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
// parent dir doesn't exist — nothing to clean
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
private async ensureDirectory(dirPath: string): Promise<void> {
|
|
94
120
|
await mkdir(dirPath, { recursive: true });
|
|
95
121
|
}
|
|
96
122
|
}
|
|
97
|
-
|
|
98
|
-
// Helper to avoid appendFile import error if not imported
|
|
99
|
-
import { appendFile } from "fs/promises";
|
package/src/services/session.ts
CHANGED
|
@@ -28,8 +28,6 @@ import { getMessageContent } from "../utils/messageOperations.js";
|
|
|
28
28
|
|
|
29
29
|
export interface SessionData {
|
|
30
30
|
id: string;
|
|
31
|
-
rootSessionId?: string;
|
|
32
|
-
parentSessionId?: string;
|
|
33
31
|
messages: Message[];
|
|
34
32
|
metadata: {
|
|
35
33
|
workdir: string;
|
|
@@ -40,8 +38,6 @@ export interface SessionData {
|
|
|
40
38
|
|
|
41
39
|
export interface SessionMetadata {
|
|
42
40
|
id: string;
|
|
43
|
-
rootSessionId?: string;
|
|
44
|
-
parentSessionId?: string;
|
|
45
41
|
sessionType: "main" | "subagent";
|
|
46
42
|
subagentType?: string;
|
|
47
43
|
workdir: string;
|
|
@@ -51,17 +47,6 @@ export interface SessionMetadata {
|
|
|
51
47
|
firstMessage?: string;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
export interface SessionIndex {
|
|
55
|
-
sessions: Record<
|
|
56
|
-
string,
|
|
57
|
-
Omit<SessionMetadata, "id" | "lastActiveAt" | "createdAt"> & {
|
|
58
|
-
lastActiveAt: string;
|
|
59
|
-
createdAt: string;
|
|
60
|
-
}
|
|
61
|
-
>;
|
|
62
|
-
lastUpdated: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
50
|
/**
|
|
66
51
|
* Generate a new session ID using Node.js native crypto.randomUUID()
|
|
67
52
|
* @returns UUID string for session identification
|
|
@@ -82,40 +67,6 @@ export function generateSubagentFilename(sessionId: string): string {
|
|
|
82
67
|
// Constants
|
|
83
68
|
export const SESSION_DIR = join(homedir(), ".wave", "projects");
|
|
84
69
|
const MAX_SESSION_AGE_DAYS = 14;
|
|
85
|
-
const SESSION_INDEX_FILENAME = "sessions-index.json";
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Update the session index for a project directory
|
|
89
|
-
*/
|
|
90
|
-
async function updateSessionIndex(
|
|
91
|
-
projectDirPath: string,
|
|
92
|
-
metadata: SessionMetadata,
|
|
93
|
-
): Promise<void> {
|
|
94
|
-
const indexPath = join(projectDirPath, SESSION_INDEX_FILENAME);
|
|
95
|
-
let index: SessionIndex = {
|
|
96
|
-
sessions: {},
|
|
97
|
-
lastUpdated: new Date().toISOString(),
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const content = await fs.readFile(indexPath, "utf8");
|
|
102
|
-
index = JSON.parse(content);
|
|
103
|
-
} catch {
|
|
104
|
-
// Index doesn't exist or is invalid, start fresh
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const { id, ...rest } = metadata;
|
|
108
|
-
index.sessions[id] = {
|
|
109
|
-
...rest,
|
|
110
|
-
createdAt: metadata.createdAt.toISOString(),
|
|
111
|
-
lastActiveAt: metadata.lastActiveAt.toISOString(),
|
|
112
|
-
firstMessage: metadata.firstMessage || index.sessions[id]?.firstMessage,
|
|
113
|
-
parentSessionId: metadata.parentSessionId,
|
|
114
|
-
};
|
|
115
|
-
index.lastUpdated = new Date().toISOString();
|
|
116
|
-
|
|
117
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
118
|
-
}
|
|
119
70
|
|
|
120
71
|
/**
|
|
121
72
|
* Ensure session directory exists
|
|
@@ -201,8 +152,6 @@ export async function appendMessages(
|
|
|
201
152
|
newMessages: Message[],
|
|
202
153
|
workdir: string,
|
|
203
154
|
sessionType: "main" | "subagent" = "main",
|
|
204
|
-
rootSessionId?: string,
|
|
205
|
-
parentSessionId?: string,
|
|
206
155
|
): Promise<void> {
|
|
207
156
|
// Do not save session files in test environment
|
|
208
157
|
if (process.env.NODE_ENV === "test") {
|
|
@@ -235,50 +184,6 @@ export async function appendMessages(
|
|
|
235
184
|
await jsonlHandler.append(filePath, newMessages, {
|
|
236
185
|
atomic: false,
|
|
237
186
|
});
|
|
238
|
-
|
|
239
|
-
// Update index
|
|
240
|
-
const encoder = new PathEncoder();
|
|
241
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
242
|
-
const lastMessage = newMessages[newMessages.length - 1];
|
|
243
|
-
|
|
244
|
-
// Get first message content and createdAt from existing index
|
|
245
|
-
let firstMessage: string | undefined;
|
|
246
|
-
let createdAt: Date | undefined;
|
|
247
|
-
try {
|
|
248
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
249
|
-
const content = await fs.readFile(indexPath, "utf8");
|
|
250
|
-
const index = JSON.parse(content) as SessionIndex;
|
|
251
|
-
if (!index.sessions[sessionId]?.firstMessage) {
|
|
252
|
-
firstMessage =
|
|
253
|
-
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
254
|
-
}
|
|
255
|
-
if (index.sessions[sessionId]?.createdAt) {
|
|
256
|
-
createdAt = new Date(index.sessions[sessionId].createdAt);
|
|
257
|
-
}
|
|
258
|
-
} catch {
|
|
259
|
-
// If index doesn't exist, this might be the first message
|
|
260
|
-
firstMessage =
|
|
261
|
-
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Derive createdAt from session ID if not in index
|
|
265
|
-
if (!createdAt) {
|
|
266
|
-
createdAt = new Date();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
await updateSessionIndex(projectDir.encodedPath, {
|
|
270
|
-
id: sessionId,
|
|
271
|
-
rootSessionId,
|
|
272
|
-
parentSessionId,
|
|
273
|
-
sessionType,
|
|
274
|
-
workdir,
|
|
275
|
-
createdAt,
|
|
276
|
-
lastActiveAt: new Date(lastMessage.timestamp),
|
|
277
|
-
latestTotalTokens: lastMessage.usage
|
|
278
|
-
? extractLatestTotalTokens([lastMessage])
|
|
279
|
-
: 0,
|
|
280
|
-
firstMessage,
|
|
281
|
-
});
|
|
282
187
|
}
|
|
283
188
|
|
|
284
189
|
/**
|
|
@@ -314,34 +219,25 @@ export async function loadSessionFromJsonl(
|
|
|
314
219
|
throw error;
|
|
315
220
|
}
|
|
316
221
|
|
|
317
|
-
const
|
|
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;
|
|
318
234
|
|
|
319
235
|
// Extract metadata from messages
|
|
320
236
|
const lastMessage =
|
|
321
237
|
messages.length > 0 ? messages[messages.length - 1] : null;
|
|
322
238
|
|
|
323
|
-
// Try to get rootSessionId and parentSessionId from index
|
|
324
|
-
let rootSessionId: string | undefined;
|
|
325
|
-
let parentSessionId: string | undefined;
|
|
326
|
-
try {
|
|
327
|
-
const encoder = new PathEncoder();
|
|
328
|
-
const projectDir = await encoder.getProjectDirectory(
|
|
329
|
-
workdir,
|
|
330
|
-
SESSION_DIR,
|
|
331
|
-
);
|
|
332
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
333
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
334
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
335
|
-
rootSessionId = index.sessions[sessionId]?.rootSessionId;
|
|
336
|
-
parentSessionId = index.sessions[sessionId]?.parentSessionId;
|
|
337
|
-
} catch {
|
|
338
|
-
// Ignore index errors
|
|
339
|
-
}
|
|
340
|
-
|
|
341
239
|
const sessionData: SessionData = {
|
|
342
240
|
id: sessionId,
|
|
343
|
-
rootSessionId: rootSessionId || sessionId,
|
|
344
|
-
parentSessionId,
|
|
345
241
|
messages,
|
|
346
242
|
metadata: {
|
|
347
243
|
workdir,
|
|
@@ -435,33 +331,9 @@ export async function listSessionsFromJsonl(
|
|
|
435
331
|
|
|
436
332
|
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
437
333
|
|
|
438
|
-
// Try to read from index first
|
|
439
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
440
334
|
const sevenDaysAgo = new Date();
|
|
441
335
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
442
336
|
|
|
443
|
-
try {
|
|
444
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
445
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
446
|
-
const sessions: SessionMetadata[] = Object.entries(index.sessions)
|
|
447
|
-
.filter(([, meta]) => {
|
|
448
|
-
const lastActiveAt = new Date(meta.lastActiveAt);
|
|
449
|
-
return meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo;
|
|
450
|
-
})
|
|
451
|
-
.map(([id, meta]) => ({
|
|
452
|
-
id,
|
|
453
|
-
...meta,
|
|
454
|
-
createdAt: new Date(meta.createdAt),
|
|
455
|
-
lastActiveAt: new Date(meta.lastActiveAt),
|
|
456
|
-
}));
|
|
457
|
-
|
|
458
|
-
return sessions.sort(
|
|
459
|
-
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
460
|
-
);
|
|
461
|
-
} catch {
|
|
462
|
-
// Fallback to manual listing if index fails
|
|
463
|
-
}
|
|
464
|
-
|
|
465
337
|
let files: string[];
|
|
466
338
|
try {
|
|
467
339
|
files = await fs.readdir(projectDir.encodedPath);
|
|
@@ -521,7 +393,7 @@ export async function listSessionsFromJsonl(
|
|
|
521
393
|
const sessionMeta: SessionMetadata = {
|
|
522
394
|
id: sessionId,
|
|
523
395
|
sessionType: "main",
|
|
524
|
-
subagentType: undefined,
|
|
396
|
+
subagentType: undefined,
|
|
525
397
|
workdir: projectDir.originalPath,
|
|
526
398
|
createdAt: new Date(),
|
|
527
399
|
lastActiveAt,
|
|
@@ -530,7 +402,7 @@ export async function listSessionsFromJsonl(
|
|
|
530
402
|
: 0,
|
|
531
403
|
};
|
|
532
404
|
|
|
533
|
-
// Try to get first message content for
|
|
405
|
+
// Try to get first message content for display
|
|
534
406
|
try {
|
|
535
407
|
const firstContent = await getFirstMessageContent(sessionId, workdir);
|
|
536
408
|
if (firstContent) {
|
|
@@ -548,30 +420,9 @@ export async function listSessionsFromJsonl(
|
|
|
548
420
|
}
|
|
549
421
|
|
|
550
422
|
// Sort by last active time (most recently active first)
|
|
551
|
-
|
|
423
|
+
return sessions.sort(
|
|
552
424
|
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
553
425
|
);
|
|
554
|
-
|
|
555
|
-
// Rebuild index if we had to fall back
|
|
556
|
-
try {
|
|
557
|
-
const index: SessionIndex = {
|
|
558
|
-
sessions: {},
|
|
559
|
-
lastUpdated: new Date().toISOString(),
|
|
560
|
-
};
|
|
561
|
-
for (const session of sessions) {
|
|
562
|
-
const { id, ...rest } = session;
|
|
563
|
-
index.sessions[id] = {
|
|
564
|
-
...rest,
|
|
565
|
-
createdAt: session.createdAt.toISOString(),
|
|
566
|
-
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
570
|
-
} catch (error) {
|
|
571
|
-
logger.warn(`Failed to rebuild session index for ${workdir}:`, error);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return sortedSessions;
|
|
575
426
|
} catch (error) {
|
|
576
427
|
throw new Error(`Failed to list sessions: ${error}`);
|
|
577
428
|
}
|
|
@@ -607,28 +458,52 @@ export async function listAllSessions(): Promise<SessionMetadata[]> {
|
|
|
607
458
|
continue;
|
|
608
459
|
}
|
|
609
460
|
|
|
610
|
-
//
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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;
|
|
624
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;
|
|
625
503
|
}
|
|
626
|
-
} catch {
|
|
627
|
-
// If index fails, we skip this project directory
|
|
628
|
-
// In the future, we could scan for .jsonl files here
|
|
629
504
|
}
|
|
630
505
|
} catch {
|
|
631
|
-
// Skip if stat fails
|
|
506
|
+
// Skip if stat/readdir fails
|
|
632
507
|
}
|
|
633
508
|
}
|
|
634
509
|
|
|
@@ -677,38 +552,6 @@ export async function cleanupExpiredSessionsFromJsonl(
|
|
|
677
552
|
if (fileAge > maxAge) {
|
|
678
553
|
await fs.unlink(filePath);
|
|
679
554
|
deletedCount++;
|
|
680
|
-
|
|
681
|
-
// Remove from index if it exists
|
|
682
|
-
try {
|
|
683
|
-
const indexPath = join(
|
|
684
|
-
projectDir.encodedPath,
|
|
685
|
-
SESSION_INDEX_FILENAME,
|
|
686
|
-
);
|
|
687
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
688
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
689
|
-
const uuidMatch = file.match(
|
|
690
|
-
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
691
|
-
);
|
|
692
|
-
const subagentMatch = file.match(
|
|
693
|
-
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
694
|
-
);
|
|
695
|
-
const sessionId = uuidMatch
|
|
696
|
-
? uuidMatch[1]
|
|
697
|
-
: subagentMatch
|
|
698
|
-
? subagentMatch[1]
|
|
699
|
-
: null;
|
|
700
|
-
|
|
701
|
-
if (sessionId && index.sessions[sessionId]) {
|
|
702
|
-
delete index.sessions[sessionId];
|
|
703
|
-
await fs.writeFile(
|
|
704
|
-
indexPath,
|
|
705
|
-
JSON.stringify(index, null, 2),
|
|
706
|
-
"utf8",
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
} catch {
|
|
710
|
-
// Ignore index update errors during cleanup
|
|
711
|
-
}
|
|
712
555
|
}
|
|
713
556
|
} catch {
|
|
714
557
|
// Skip failed operations and continue processing other files
|
|
@@ -897,21 +740,6 @@ export async function deleteSession(
|
|
|
897
740
|
throw error;
|
|
898
741
|
}
|
|
899
742
|
}
|
|
900
|
-
|
|
901
|
-
// Also remove from index
|
|
902
|
-
try {
|
|
903
|
-
const encoder = new PathEncoder();
|
|
904
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
905
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
906
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
907
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
908
|
-
if (index.sessions[sessionId]) {
|
|
909
|
-
delete index.sessions[sessionId];
|
|
910
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
911
|
-
}
|
|
912
|
-
} catch {
|
|
913
|
-
// Ignore index errors
|
|
914
|
-
}
|
|
915
743
|
}
|
|
916
744
|
|
|
917
745
|
/**
|
|
@@ -995,7 +823,9 @@ export async function handleSessionRestoration(
|
|
|
995
823
|
}
|
|
996
824
|
|
|
997
825
|
/**
|
|
998
|
-
* Load the full message thread
|
|
826
|
+
* Load the full message thread for a session.
|
|
827
|
+
* With append-only compaction, all messages are in a single file.
|
|
828
|
+
* Returns the active messages (post-compact boundary).
|
|
999
829
|
* @param currentSessionId - The ID of the current session
|
|
1000
830
|
* @param workdir - Working directory for the session
|
|
1001
831
|
* @returns Promise that resolves to an array of all messages in the thread
|
|
@@ -1004,36 +834,7 @@ export async function loadFullMessageThread(
|
|
|
1004
834
|
currentSessionId: string,
|
|
1005
835
|
workdir: string,
|
|
1006
836
|
): Promise<{ messages: Message[]; sessionIds: string[] }> {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
while (currentId) {
|
|
1012
|
-
const sessionData = await loadSessionFromJsonl(currentId, workdir);
|
|
1013
|
-
if (!sessionData) break;
|
|
1014
|
-
|
|
1015
|
-
sessionIds.unshift(currentId);
|
|
1016
|
-
// Add messages from this session to the beginning of the list
|
|
1017
|
-
// But skip the "compact" block if it's not the first session in our traversal (which is the latest)
|
|
1018
|
-
// Actually, we should probably keep all messages and let the UI/logic handle it.
|
|
1019
|
-
// But wait, if we are concatenating, the "compact" block in session N summarizes session N-1.
|
|
1020
|
-
// So if we have session N-1 and session N, we should probably skip the compact block in session N.
|
|
1021
|
-
|
|
1022
|
-
const messages = sessionData.messages;
|
|
1023
|
-
if (allMessages.length > 0) {
|
|
1024
|
-
// If we already have messages (from "later" sessions),
|
|
1025
|
-
// we are now adding messages from an "earlier" session.
|
|
1026
|
-
// The later session's first message might be a "compact" block.
|
|
1027
|
-
if (allMessages[0].blocks.some((b) => b.type === "compact")) {
|
|
1028
|
-
// Remove the compact block from the later session's messages
|
|
1029
|
-
// because we are now providing the actual messages it summarized.
|
|
1030
|
-
allMessages.shift();
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
allMessages.unshift(...messages);
|
|
1035
|
-
currentId = sessionData.parentSessionId;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
return { messages: allMessages, sessionIds };
|
|
837
|
+
const sessionData = await loadSessionFromJsonl(currentSessionId, workdir);
|
|
838
|
+
if (!sessionData) return { messages: [], sessionIds: [] };
|
|
839
|
+
return { messages: sessionData.messages, sessionIds: [currentSessionId] };
|
|
1039
840
|
}
|