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.
Files changed (99) hide show
  1. package/builtin/skills/settings/ENV.md +0 -1
  2. package/builtin/skills/settings/HOOKS.md +10 -0
  3. package/builtin/skills/settings/MODELS.md +37 -0
  4. package/builtin/skills/settings/SKILLS.md +6 -4
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +12 -5
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/managers/aiManager.d.ts.map +1 -1
  11. package/dist/managers/aiManager.js +20 -4
  12. package/dist/managers/mcpManager.d.ts.map +1 -1
  13. package/dist/managers/mcpManager.js +7 -2
  14. package/dist/managers/messageManager.d.ts +3 -5
  15. package/dist/managers/messageManager.d.ts.map +1 -1
  16. package/dist/managers/messageManager.js +17 -77
  17. package/dist/managers/skillManager.d.ts +1 -0
  18. package/dist/managers/skillManager.d.ts.map +1 -1
  19. package/dist/managers/skillManager.js +11 -2
  20. package/dist/prompts/index.d.ts +3 -2
  21. package/dist/prompts/index.d.ts.map +1 -1
  22. package/dist/prompts/index.js +5 -1
  23. package/dist/services/aiService.d.ts.map +1 -1
  24. package/dist/services/aiService.js +13 -9
  25. package/dist/services/configurationService.d.ts.map +1 -1
  26. package/dist/services/configurationService.js +22 -9
  27. package/dist/services/hook.d.ts.map +1 -1
  28. package/dist/services/hook.js +6 -0
  29. package/dist/services/initializationService.d.ts.map +1 -1
  30. package/dist/services/initializationService.js +1 -1
  31. package/dist/services/interactionService.js +1 -1
  32. package/dist/services/reversionService.d.ts +5 -1
  33. package/dist/services/reversionService.d.ts.map +1 -1
  34. package/dist/services/reversionService.js +50 -22
  35. package/dist/services/session.d.ts +4 -13
  36. package/dist/services/session.d.ts.map +1 -1
  37. package/dist/services/session.js +59 -215
  38. package/dist/telemetry/sessionTracing.d.ts +14 -4
  39. package/dist/telemetry/sessionTracing.d.ts.map +1 -1
  40. package/dist/telemetry/sessionTracing.js +134 -8
  41. package/dist/tools/bashTool.d.ts.map +1 -1
  42. package/dist/tools/bashTool.js +16 -1
  43. package/dist/types/config.d.ts +7 -0
  44. package/dist/types/config.d.ts.map +1 -1
  45. package/dist/types/hooks.d.ts +1 -0
  46. package/dist/types/hooks.d.ts.map +1 -1
  47. package/dist/types/messaging.d.ts +0 -1
  48. package/dist/types/messaging.d.ts.map +1 -1
  49. package/dist/types/skills.d.ts +1 -0
  50. package/dist/types/skills.d.ts.map +1 -1
  51. package/dist/types/telemetry.d.ts +14 -1
  52. package/dist/types/telemetry.d.ts.map +1 -1
  53. package/dist/utils/atomicWrite.d.ts +15 -0
  54. package/dist/utils/atomicWrite.d.ts.map +1 -0
  55. package/dist/utils/atomicWrite.js +59 -0
  56. package/dist/utils/cacheControlUtils.d.ts +3 -15
  57. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  58. package/dist/utils/cacheControlUtils.js +4 -35
  59. package/dist/utils/containerSetup.d.ts.map +1 -1
  60. package/dist/utils/containerSetup.js +5 -1
  61. package/dist/utils/convertMessagesForAPI.d.ts +9 -1
  62. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  63. package/dist/utils/convertMessagesForAPI.js +74 -55
  64. package/dist/utils/mcpUtils.d.ts.map +1 -1
  65. package/dist/utils/mcpUtils.js +2 -1
  66. package/dist/utils/modelCapabilities.d.ts +22 -0
  67. package/dist/utils/modelCapabilities.d.ts.map +1 -0
  68. package/dist/utils/modelCapabilities.js +24 -0
  69. package/dist/utils/skillParser.d.ts.map +1 -1
  70. package/dist/utils/skillParser.js +3 -1
  71. package/package.json +1 -1
  72. package/src/agent.ts +11 -5
  73. package/src/index.ts +1 -0
  74. package/src/managers/aiManager.ts +22 -2
  75. package/src/managers/mcpManager.ts +8 -2
  76. package/src/managers/messageManager.ts +22 -104
  77. package/src/managers/skillManager.ts +16 -2
  78. package/src/prompts/index.ts +6 -1
  79. package/src/services/aiService.ts +12 -5
  80. package/src/services/configurationService.ts +24 -24
  81. package/src/services/hook.ts +7 -0
  82. package/src/services/initializationService.ts +1 -3
  83. package/src/services/interactionService.ts +1 -1
  84. package/src/services/reversionService.ts +50 -27
  85. package/src/services/session.ts +63 -262
  86. package/src/telemetry/sessionTracing.ts +167 -12
  87. package/src/tools/bashTool.ts +23 -1
  88. package/src/types/config.ts +8 -0
  89. package/src/types/hooks.ts +1 -0
  90. package/src/types/messaging.ts +0 -1
  91. package/src/types/skills.ts +1 -0
  92. package/src/types/telemetry.ts +17 -1
  93. package/src/utils/atomicWrite.ts +61 -0
  94. package/src/utils/cacheControlUtils.ts +5 -38
  95. package/src/utils/containerSetup.ts +5 -1
  96. package/src/utils/convertMessagesForAPI.ts +87 -57
  97. package/src/utils/mcpUtils.ts +2 -1
  98. package/src/utils/modelCapabilities.ts +30 -0
  99. package/src/utils/skillParser.ts +3 -1
@@ -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.rootSessionId || sessionData.id);
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 || "default",
17
+ this.rootSessionId,
18
18
  );
19
19
  }
20
20
 
21
21
  private getFilePathHash(filePath: string): string {
22
- return createHash("md5").update(filePath).digest("hex");
22
+ return createHash("sha256").update(filePath).digest("hex").slice(0, 16);
23
23
  }
24
24
 
25
- private async getNextVersion(fileHashDir: string): Promise<number> {
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 readFile(join(fileHashDir, "versions"), "utf-8");
28
- const versions = files
29
- .split("\n")
30
- .map((v) => parseInt(v, 10))
31
- .filter((v) => !isNaN(v));
32
- return versions.length > 0 ? Math.max(...versions) + 1 : 1;
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
- const fileHashDir = join(this.historyBaseDir, fileHash);
52
- await this.ensureDirectory(fileHashDir);
51
+ await this.ensureDirectory(this.historyBaseDir);
53
52
 
54
- const version = await this.getNextVersion(fileHashDir);
55
- const snapshotPath = join(fileHashDir, `v${version}`);
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";
@@ -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 messages = await jsonlHandler.read(filePath);
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, // No longer stored in metadata
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 the fallback/rebuild case
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
- const sortedSessions = sessions.sort(
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
- // Try to read from index first
611
- const indexPath = join(projectPath, SESSION_INDEX_FILENAME);
612
- try {
613
- const indexContent = await fs.readFile(indexPath, "utf8");
614
- const index = JSON.parse(indexContent) as SessionIndex;
615
- for (const [id, meta] of Object.entries(index.sessions)) {
616
- const lastActiveAt = new Date(meta.lastActiveAt);
617
- if (meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo) {
618
- allSessions.push({
619
- id,
620
- ...meta,
621
- createdAt: new Date(meta.createdAt),
622
- lastActiveAt,
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 by following parentSessionId links
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 sessionIds: string[] = [];
1008
- let currentId: string | undefined = currentSessionId;
1009
- const allMessages: Message[] = [];
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
  }