wave-agent-sdk 0.2.1 → 0.5.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 (194) hide show
  1. package/dist/agent.d.ts +66 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +156 -83
  4. package/dist/constants/prompts.d.ts +7 -2
  5. package/dist/constants/prompts.d.ts.map +1 -1
  6. package/dist/constants/prompts.js +41 -5
  7. package/dist/constants/tools.d.ts +2 -2
  8. package/dist/constants/tools.js +2 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  13. package/dist/managers/MemoryRuleManager.js +16 -2
  14. package/dist/managers/aiManager.d.ts +14 -4
  15. package/dist/managers/aiManager.d.ts.map +1 -1
  16. package/dist/managers/aiManager.js +61 -9
  17. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  18. package/dist/managers/backgroundBashManager.js +1 -0
  19. package/dist/managers/backgroundTaskManager.d.ts +35 -0
  20. package/dist/managers/backgroundTaskManager.d.ts.map +1 -0
  21. package/dist/managers/backgroundTaskManager.js +249 -0
  22. package/dist/managers/bashManager.d.ts.map +1 -1
  23. package/dist/managers/bashManager.js +0 -3
  24. package/dist/managers/foregroundTaskManager.d.ts +9 -0
  25. package/dist/managers/foregroundTaskManager.d.ts.map +1 -0
  26. package/dist/managers/foregroundTaskManager.js +20 -0
  27. package/dist/managers/liveConfigManager.d.ts +1 -1
  28. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  29. package/dist/managers/lspManager.d.ts.map +1 -1
  30. package/dist/managers/lspManager.js +3 -1
  31. package/dist/managers/messageManager.d.ts +34 -4
  32. package/dist/managers/messageManager.d.ts.map +1 -1
  33. package/dist/managers/messageManager.js +104 -13
  34. package/dist/managers/permissionManager.d.ts.map +1 -1
  35. package/dist/managers/permissionManager.js +11 -13
  36. package/dist/managers/pluginManager.d.ts.map +1 -1
  37. package/dist/managers/pluginManager.js +3 -2
  38. package/dist/managers/pluginScopeManager.d.ts +13 -2
  39. package/dist/managers/pluginScopeManager.d.ts.map +1 -1
  40. package/dist/managers/pluginScopeManager.js +38 -0
  41. package/dist/managers/reversionManager.d.ts +39 -0
  42. package/dist/managers/reversionManager.d.ts.map +1 -0
  43. package/dist/managers/reversionManager.js +118 -0
  44. package/dist/managers/slashCommandManager.d.ts +4 -1
  45. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  46. package/dist/managers/slashCommandManager.js +16 -6
  47. package/dist/managers/subagentManager.d.ts +13 -2
  48. package/dist/managers/subagentManager.d.ts.map +1 -1
  49. package/dist/managers/subagentManager.js +144 -35
  50. package/dist/managers/toolManager.d.ts +11 -1
  51. package/dist/managers/toolManager.d.ts.map +1 -1
  52. package/dist/managers/toolManager.js +11 -3
  53. package/dist/services/GitService.d.ts.map +1 -1
  54. package/dist/services/GitService.js +6 -2
  55. package/dist/services/MarketplaceService.d.ts +14 -1
  56. package/dist/services/MarketplaceService.d.ts.map +1 -1
  57. package/dist/services/MarketplaceService.js +72 -4
  58. package/dist/services/MemoryRuleService.d.ts +1 -1
  59. package/dist/services/MemoryRuleService.d.ts.map +1 -1
  60. package/dist/services/MemoryRuleService.js +13 -2
  61. package/dist/services/aiService.js +1 -1
  62. package/dist/services/configurationService.d.ts +18 -2
  63. package/dist/services/configurationService.d.ts.map +1 -1
  64. package/dist/services/configurationService.js +62 -0
  65. package/dist/services/fileWatcher.d.ts +0 -5
  66. package/dist/services/fileWatcher.d.ts.map +1 -1
  67. package/dist/services/fileWatcher.js +0 -11
  68. package/dist/services/memory.js +1 -1
  69. package/dist/services/pluginLoader.d.ts.map +1 -1
  70. package/dist/services/pluginLoader.js +6 -1
  71. package/dist/services/reversionService.d.ts +24 -0
  72. package/dist/services/reversionService.d.ts.map +1 -0
  73. package/dist/services/reversionService.js +76 -0
  74. package/dist/services/session.d.ts +7 -0
  75. package/dist/services/session.d.ts.map +1 -1
  76. package/dist/services/session.js +126 -3
  77. package/dist/tools/bashTool.d.ts +0 -8
  78. package/dist/tools/bashTool.d.ts.map +1 -1
  79. package/dist/tools/bashTool.js +52 -174
  80. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  81. package/dist/tools/deleteFileTool.js +9 -0
  82. package/dist/tools/editTool.d.ts.map +1 -1
  83. package/dist/tools/editTool.js +15 -4
  84. package/dist/tools/multiEditTool.d.ts.map +1 -1
  85. package/dist/tools/multiEditTool.js +16 -5
  86. package/dist/tools/taskOutputTool.d.ts +3 -0
  87. package/dist/tools/taskOutputTool.d.ts.map +1 -0
  88. package/dist/tools/taskOutputTool.js +149 -0
  89. package/dist/tools/taskStopTool.d.ts +3 -0
  90. package/dist/tools/taskStopTool.d.ts.map +1 -0
  91. package/dist/tools/taskStopTool.js +65 -0
  92. package/dist/tools/taskTool.d.ts.map +1 -1
  93. package/dist/tools/taskTool.js +105 -63
  94. package/dist/tools/types.d.ts +7 -0
  95. package/dist/tools/types.d.ts.map +1 -1
  96. package/dist/tools/writeTool.d.ts.map +1 -1
  97. package/dist/tools/writeTool.js +9 -0
  98. package/dist/types/commands.d.ts +1 -0
  99. package/dist/types/commands.d.ts.map +1 -1
  100. package/dist/types/configuration.d.ts +3 -0
  101. package/dist/types/configuration.d.ts.map +1 -1
  102. package/dist/types/environment.d.ts +2 -1
  103. package/dist/types/environment.d.ts.map +1 -1
  104. package/dist/types/environment.js +0 -6
  105. package/dist/types/history.d.ts +5 -0
  106. package/dist/types/history.d.ts.map +1 -0
  107. package/dist/types/history.js +1 -0
  108. package/dist/types/index.d.ts +1 -0
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types/index.js +1 -0
  111. package/dist/types/marketplace.d.ts +4 -0
  112. package/dist/types/marketplace.d.ts.map +1 -1
  113. package/dist/types/messaging.d.ts +7 -1
  114. package/dist/types/messaging.d.ts.map +1 -1
  115. package/dist/types/processes.d.ts +24 -4
  116. package/dist/types/processes.d.ts.map +1 -1
  117. package/dist/types/reversion.d.ts +29 -0
  118. package/dist/types/reversion.d.ts.map +1 -0
  119. package/dist/types/reversion.js +1 -0
  120. package/dist/utils/builtinSubagents.d.ts.map +1 -1
  121. package/dist/utils/builtinSubagents.js +16 -0
  122. package/dist/utils/constants.d.ts +2 -2
  123. package/dist/utils/constants.d.ts.map +1 -1
  124. package/dist/utils/constants.js +2 -2
  125. package/dist/utils/editUtils.d.ts +4 -9
  126. package/dist/utils/editUtils.d.ts.map +1 -1
  127. package/dist/utils/editUtils.js +54 -55
  128. package/dist/utils/messageOperations.d.ts +3 -1
  129. package/dist/utils/messageOperations.d.ts.map +1 -1
  130. package/dist/utils/messageOperations.js +8 -1
  131. package/dist/utils/openaiClient.d.ts.map +1 -1
  132. package/dist/utils/openaiClient.js +56 -26
  133. package/dist/utils/promptHistory.d.ts +20 -0
  134. package/dist/utils/promptHistory.d.ts.map +1 -0
  135. package/dist/utils/promptHistory.js +117 -0
  136. package/package.json +5 -3
  137. package/src/agent.ts +193 -109
  138. package/src/constants/prompts.ts +45 -5
  139. package/src/constants/tools.ts +2 -2
  140. package/src/index.ts +1 -1
  141. package/src/managers/MemoryRuleManager.ts +18 -2
  142. package/src/managers/aiManager.ts +87 -18
  143. package/src/managers/backgroundBashManager.ts +1 -0
  144. package/src/managers/backgroundTaskManager.ts +306 -0
  145. package/src/managers/bashManager.ts +0 -4
  146. package/src/managers/foregroundTaskManager.ts +26 -0
  147. package/src/managers/liveConfigManager.ts +2 -1
  148. package/src/managers/lspManager.ts +3 -1
  149. package/src/managers/messageManager.ts +136 -18
  150. package/src/managers/permissionManager.ts +11 -13
  151. package/src/managers/pluginManager.ts +4 -3
  152. package/src/managers/pluginScopeManager.ts +57 -8
  153. package/src/managers/reversionManager.ts +152 -0
  154. package/src/managers/slashCommandManager.ts +30 -7
  155. package/src/managers/subagentManager.ts +176 -31
  156. package/src/managers/toolManager.ts +23 -4
  157. package/src/services/GitService.ts +6 -2
  158. package/src/services/MarketplaceService.ts +100 -4
  159. package/src/services/MemoryRuleService.ts +18 -6
  160. package/src/services/aiService.ts +1 -1
  161. package/src/services/configurationService.ts +79 -1
  162. package/src/services/fileWatcher.ts +0 -13
  163. package/src/services/memory.ts +1 -1
  164. package/src/services/pluginLoader.ts +7 -1
  165. package/src/services/reversionService.ts +94 -0
  166. package/src/services/session.ts +161 -3
  167. package/src/tools/bashTool.ts +73 -200
  168. package/src/tools/deleteFileTool.ts +15 -0
  169. package/src/tools/editTool.ts +20 -10
  170. package/src/tools/multiEditTool.ts +21 -11
  171. package/src/tools/taskOutputTool.ts +174 -0
  172. package/src/tools/taskStopTool.ts +72 -0
  173. package/src/tools/taskTool.ts +130 -74
  174. package/src/tools/types.ts +7 -0
  175. package/src/tools/writeTool.ts +14 -0
  176. package/src/types/commands.ts +3 -0
  177. package/src/types/configuration.ts +4 -0
  178. package/src/types/environment.ts +3 -1
  179. package/src/types/history.ts +4 -0
  180. package/src/types/index.ts +1 -0
  181. package/src/types/marketplace.ts +5 -0
  182. package/src/types/messaging.ts +9 -1
  183. package/src/types/processes.ts +33 -4
  184. package/src/types/reversion.ts +29 -0
  185. package/src/utils/builtinSubagents.ts +18 -0
  186. package/src/utils/constants.ts +2 -2
  187. package/src/utils/editUtils.ts +66 -58
  188. package/src/utils/messageOperations.ts +10 -0
  189. package/src/utils/openaiClient.ts +69 -35
  190. package/src/utils/promptHistory.ts +133 -0
  191. package/dist/utils/bashHistory.d.ts +0 -50
  192. package/dist/utils/bashHistory.d.ts.map +0 -1
  193. package/dist/utils/bashHistory.js +0 -256
  194. package/src/utils/bashHistory.ts +0 -320
@@ -13,6 +13,7 @@ import type {
13
13
  ValidationResult,
14
14
  ConfigurationPaths,
15
15
  WaveConfiguration,
16
+ Scope,
16
17
  } from "../types/configuration.js";
17
18
  import {
18
19
  getAllConfigPaths,
@@ -492,6 +493,26 @@ export class ConfigurationService {
492
493
  return DEFAULT_WAVE_MAX_INPUT_TOKENS;
493
494
  }
494
495
 
496
+ /**
497
+ * Resolves preferred language with fallbacks
498
+ * Resolution priority: options > settings.json > undefined
499
+ * @param constructorLanguage - Language from constructor (optional)
500
+ * @returns Resolved language or undefined
501
+ */
502
+ resolveLanguage(constructorLanguage?: string): string | undefined {
503
+ // 1. Constructor options (highest priority)
504
+ if (constructorLanguage !== undefined) {
505
+ return constructorLanguage;
506
+ }
507
+
508
+ // 2. settings.json (merged)
509
+ if (this.currentConfiguration?.language) {
510
+ return this.currentConfiguration.language;
511
+ }
512
+
513
+ return undefined;
514
+ }
515
+
495
516
  /**
496
517
  * Resolves max output tokens with fallbacks
497
518
  * Resolution priority: options > env (from settings.json) > process.env > default
@@ -577,7 +598,7 @@ export class ConfigurationService {
577
598
  */
578
599
  async updateEnabledPlugin(
579
600
  workdir: string,
580
- scope: "user" | "project" | "local",
601
+ scope: Scope,
581
602
  pluginId: string,
582
603
  enabled: boolean,
583
604
  ): Promise<void> {
@@ -619,6 +640,48 @@ export class ConfigurationService {
619
640
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
620
641
  }
621
642
 
643
+ /**
644
+ * Remove a plugin from the enabled plugins in the specified scope
645
+ */
646
+ async removeEnabledPlugin(
647
+ workdir: string,
648
+ scope: Scope,
649
+ pluginId: string,
650
+ ): Promise<void> {
651
+ if (scope !== "user" && !existsSync(workdir)) {
652
+ throw new Error(`Working directory does not exist: ${workdir}`);
653
+ }
654
+
655
+ let configPath: string;
656
+ if (scope === "user") {
657
+ configPath = getUserConfigPaths()[1]; // settings.json
658
+ } else if (scope === "project") {
659
+ configPath = getProjectConfigPaths(workdir)[1]; // settings.json
660
+ } else {
661
+ configPath = getProjectConfigPaths(workdir)[0]; // settings.local.json
662
+ }
663
+
664
+ if (!existsSync(configPath)) {
665
+ return; // Nothing to remove
666
+ }
667
+
668
+ try {
669
+ const content = await fs.readFile(configPath, "utf-8");
670
+ const config: WaveConfiguration = JSON.parse(content);
671
+
672
+ if (config.enabledPlugins && pluginId in config.enabledPlugins) {
673
+ delete config.enabledPlugins[pluginId];
674
+ await fs.writeFile(
675
+ configPath,
676
+ JSON.stringify(config, null, 2),
677
+ "utf-8",
678
+ );
679
+ }
680
+ } catch {
681
+ // Ignore errors for corrupted or non-existent files
682
+ }
683
+ }
684
+
622
685
  /**
623
686
  * Get merged enabled plugins from all scopes
624
687
  */
@@ -626,6 +689,14 @@ export class ConfigurationService {
626
689
  const mergedConfig = loadMergedWaveConfig(workdir);
627
690
  return mergedConfig?.enabledPlugins || {};
628
691
  }
692
+
693
+ /**
694
+ * Load Wave configuration from a JSON file
695
+ * Supports both hooks and environment variables with proper validation
696
+ */
697
+ loadWaveConfigFromFile(filePath: string): WaveConfiguration | null {
698
+ return loadWaveConfigFromFile(filePath);
699
+ }
629
700
  }
630
701
  // =============================================================================
631
702
  // Extracted Configuration Functions
@@ -781,6 +852,7 @@ export function loadWaveConfigFromFile(
781
852
  env: config.env || undefined,
782
853
  permissions: config.permissions || undefined,
783
854
  enabledPlugins: config.enabledPlugins || undefined,
855
+ language: config.language || undefined,
784
856
  };
785
857
  } catch (error) {
786
858
  if (error instanceof SyntaxError) {
@@ -932,6 +1004,11 @@ export function loadMergedWaveConfig(
932
1004
  if (!mergedConfig.enabledPlugins) mergedConfig.enabledPlugins = {};
933
1005
  Object.assign(mergedConfig.enabledPlugins, config.enabledPlugins);
934
1006
  }
1007
+
1008
+ // Merge language (last one wins)
1009
+ if (config.language !== undefined) {
1010
+ mergedConfig.language = config.language;
1011
+ }
935
1012
  }
936
1013
 
937
1014
  return {
@@ -953,5 +1030,6 @@ export function loadMergedWaveConfig(
953
1030
  Object.keys(mergedConfig.enabledPlugins).length > 0
954
1031
  ? mergedConfig.enabledPlugins
955
1032
  : undefined,
1033
+ language: mergedConfig.language,
956
1034
  };
957
1035
  }
@@ -163,19 +163,6 @@ export class FileWatcherService extends EventEmitter {
163
163
  .filter((status): status is FileWatcherStatus => status !== null);
164
164
  }
165
165
 
166
- /**
167
- * Configure watcher behavior
168
- * Runtime configuration updates
169
- */
170
- updateConfig(config: Partial<FileWatcherConfig>): void {
171
- this.defaultConfig = { ...this.defaultConfig, ...config };
172
-
173
- // Update existing watchers with new config
174
- for (const entry of this.watchers.values()) {
175
- entry.config = { ...entry.config, ...config };
176
- }
177
- }
178
-
179
166
  /**
180
167
  * Cleanup all watchers
181
168
  */
@@ -142,7 +142,7 @@ export const readMemoryFile = async (workdir: string): Promise<string> => {
142
142
  return "";
143
143
  }
144
144
  logger.error("Failed to read memory file", { memoryFilePath, error });
145
- throw error;
145
+ return "";
146
146
  }
147
147
  };
148
148
 
@@ -67,7 +67,13 @@ export class PluginLoader {
67
67
  */
68
68
  static loadCommands(pluginPath: string): CustomSlashCommand[] {
69
69
  const commandsPath = path.join(pluginPath, "commands");
70
- return scanCommandsDirectory(commandsPath);
70
+ const commands = scanCommandsDirectory(commandsPath);
71
+
72
+ // Attach plugin path to each command for WAVE_PLUGIN_ROOT support
73
+ return commands.map((command) => ({
74
+ ...command,
75
+ pluginPath,
76
+ }));
71
77
  }
72
78
 
73
79
  /**
@@ -0,0 +1,94 @@
1
+ import { readFile, writeFile, mkdir, rm } from "fs/promises";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { createHash } from "crypto";
5
+ import { FileSnapshot } from "../types/reversion.js";
6
+
7
+ export class ReversionService {
8
+ private historyBaseDir: string;
9
+ private sessionId: string;
10
+
11
+ constructor(sessionId: string) {
12
+ this.sessionId = sessionId;
13
+ this.historyBaseDir = join(homedir(), ".wave", "file-history", sessionId);
14
+ }
15
+
16
+ private getFilePathHash(filePath: string): string {
17
+ return createHash("md5").update(filePath).digest("hex");
18
+ }
19
+
20
+ private async getNextVersion(fileHashDir: string): Promise<number> {
21
+ try {
22
+ const files = await readFile(join(fileHashDir, "versions"), "utf-8");
23
+ const versions = files
24
+ .split("\n")
25
+ .map((v) => parseInt(v, 10))
26
+ .filter((v) => !isNaN(v));
27
+ return versions.length > 0 ? Math.max(...versions) + 1 : 1;
28
+ } catch {
29
+ return 1;
30
+ }
31
+ }
32
+
33
+ private async updateVersionsFile(
34
+ fileHashDir: string,
35
+ version: number,
36
+ ): Promise<void> {
37
+ await appendFile(join(fileHashDir, "versions"), `${version}\n`, "utf-8");
38
+ }
39
+
40
+ /**
41
+ * Saves a single snapshot to the file history directory.
42
+ * Returns the snapshot path.
43
+ */
44
+ async saveSnapshot(snapshot: FileSnapshot): Promise<string> {
45
+ const fileHash = this.getFilePathHash(snapshot.filePath);
46
+ const fileHashDir = join(this.historyBaseDir, fileHash);
47
+ await this.ensureDirectory(fileHashDir);
48
+
49
+ const version = await this.getNextVersion(fileHashDir);
50
+ const snapshotPath = join(fileHashDir, `v${version}`);
51
+
52
+ const snapshotWithContent = snapshot as FileSnapshot & {
53
+ content: string | null;
54
+ };
55
+ if (snapshotWithContent.content !== null) {
56
+ await writeFile(snapshotPath, snapshotWithContent.content, "utf-8");
57
+ } else {
58
+ // For 'create' operation, the file didn't exist, so we don't write a content file.
59
+ // The absence of the file at snapshotPath will indicate it should be deleted on reversion.
60
+ return ""; // Return empty string to indicate no snapshot file
61
+ }
62
+
63
+ await this.updateVersionsFile(fileHashDir, version);
64
+ return snapshotPath;
65
+ }
66
+
67
+ /**
68
+ * Reads snapshot content from the given path.
69
+ */
70
+ async readSnapshotContent(snapshotPath: string): Promise<string | null> {
71
+ try {
72
+ return await readFile(snapshotPath, "utf-8");
73
+ } catch (error) {
74
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
75
+ return null;
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Deletes all snapshots for this session.
83
+ */
84
+ async deleteSessionHistory(): Promise<void> {
85
+ await rm(this.historyBaseDir, { recursive: true, force: true });
86
+ }
87
+
88
+ private async ensureDirectory(dirPath: string): Promise<void> {
89
+ await mkdir(dirPath, { recursive: true });
90
+ }
91
+ }
92
+
93
+ // Helper to avoid appendFile import error if not imported
94
+ import { appendFile } from "fs/promises";
@@ -43,6 +43,15 @@ export interface SessionMetadata {
43
43
  workdir: string;
44
44
  lastActiveAt: Date;
45
45
  latestTotalTokens: number;
46
+ firstMessage?: string;
47
+ }
48
+
49
+ export interface SessionIndex {
50
+ sessions: Record<
51
+ string,
52
+ Omit<SessionMetadata, "id" | "lastActiveAt"> & { lastActiveAt: string }
53
+ >;
54
+ lastUpdated: string;
46
55
  }
47
56
 
48
57
  /**
@@ -65,6 +74,38 @@ export function generateSubagentFilename(sessionId: string): string {
65
74
  // Constants
66
75
  export const SESSION_DIR = join(homedir(), ".wave", "projects");
67
76
  const MAX_SESSION_AGE_DAYS = 14;
77
+ const SESSION_INDEX_FILENAME = "sessions-index.json";
78
+
79
+ /**
80
+ * Update the session index for a project directory
81
+ */
82
+ async function updateSessionIndex(
83
+ projectDirPath: string,
84
+ metadata: SessionMetadata,
85
+ ): Promise<void> {
86
+ const indexPath = join(projectDirPath, SESSION_INDEX_FILENAME);
87
+ let index: SessionIndex = {
88
+ sessions: {},
89
+ lastUpdated: new Date().toISOString(),
90
+ };
91
+
92
+ try {
93
+ const content = await fs.readFile(indexPath, "utf8");
94
+ index = JSON.parse(content);
95
+ } catch {
96
+ // Index doesn't exist or is invalid, start fresh
97
+ }
98
+
99
+ const { id, ...rest } = metadata;
100
+ index.sessions[id] = {
101
+ ...rest,
102
+ lastActiveAt: metadata.lastActiveAt.toISOString(),
103
+ firstMessage: metadata.firstMessage || index.sessions[id]?.firstMessage,
104
+ };
105
+ index.lastUpdated = new Date().toISOString();
106
+
107
+ await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
108
+ }
68
109
 
69
110
  /**
70
111
  * Ensure session directory exists
@@ -187,6 +228,38 @@ export async function appendMessages(
187
228
  await jsonlHandler.append(filePath, messagesWithTimestamp, {
188
229
  atomic: false,
189
230
  });
231
+
232
+ // Update index
233
+ const encoder = new PathEncoder();
234
+ const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
235
+ const lastMessage = messagesWithTimestamp[messagesWithTimestamp.length - 1];
236
+
237
+ // Get first message content if it's a new session or we don't have it
238
+ let firstMessage: string | undefined;
239
+ try {
240
+ const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
241
+ const content = await fs.readFile(indexPath, "utf8");
242
+ const index = JSON.parse(content) as SessionIndex;
243
+ if (!index.sessions[sessionId]?.firstMessage) {
244
+ firstMessage =
245
+ (await getFirstMessageContent(sessionId, workdir)) || undefined;
246
+ }
247
+ } catch {
248
+ // If index doesn't exist, this might be the first message
249
+ firstMessage =
250
+ (await getFirstMessageContent(sessionId, workdir)) || undefined;
251
+ }
252
+
253
+ await updateSessionIndex(projectDir.encodedPath, {
254
+ id: sessionId,
255
+ sessionType,
256
+ workdir,
257
+ lastActiveAt: new Date(lastMessage.timestamp),
258
+ latestTotalTokens: lastMessage.usage
259
+ ? extractLatestTotalTokens([lastMessage])
260
+ : 0,
261
+ firstMessage,
262
+ });
190
263
  }
191
264
 
192
265
  /**
@@ -318,6 +391,27 @@ export async function listSessionsFromJsonl(
318
391
  const baseDir = SESSION_DIR;
319
392
 
320
393
  const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
394
+
395
+ // Try to read from index first
396
+ const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
397
+ try {
398
+ const indexContent = await fs.readFile(indexPath, "utf8");
399
+ const index = JSON.parse(indexContent) as SessionIndex;
400
+ const sessions: SessionMetadata[] = Object.entries(index.sessions)
401
+ .filter(([, meta]) => meta.sessionType === "main")
402
+ .map(([id, meta]) => ({
403
+ id,
404
+ ...meta,
405
+ lastActiveAt: new Date(meta.lastActiveAt),
406
+ }));
407
+
408
+ return sessions.sort(
409
+ (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
410
+ );
411
+ } catch {
412
+ // Fallback to manual listing if index fails
413
+ }
414
+
321
415
  let files: string[];
322
416
  try {
323
417
  files = await fs.readdir(projectDir.encodedPath);
@@ -370,7 +464,7 @@ export async function listSessionsFromJsonl(
370
464
  }
371
465
 
372
466
  // Return inline object for performance (no interface instantiation overhead)
373
- sessions.push({
467
+ const sessionMeta: SessionMetadata = {
374
468
  id: sessionId,
375
469
  sessionType: "main",
376
470
  subagentType: undefined, // No longer stored in metadata
@@ -379,7 +473,19 @@ export async function listSessionsFromJsonl(
379
473
  latestTotalTokens: lastMessage?.usage
380
474
  ? extractLatestTotalTokens([lastMessage])
381
475
  : 0,
382
- });
476
+ };
477
+
478
+ // Try to get first message content for the fallback/rebuild case
479
+ try {
480
+ const firstContent = await getFirstMessageContent(sessionId, workdir);
481
+ if (firstContent) {
482
+ sessionMeta.firstMessage = firstContent;
483
+ }
484
+ } catch {
485
+ // Ignore errors getting first message
486
+ }
487
+
488
+ sessions.push(sessionMeta);
383
489
  } catch {
384
490
  // Skip corrupted session files
385
491
  continue;
@@ -387,9 +493,29 @@ export async function listSessionsFromJsonl(
387
493
  }
388
494
 
389
495
  // Sort by last active time (most recently active first)
390
- return sessions.sort(
496
+ const sortedSessions = sessions.sort(
391
497
  (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
392
498
  );
499
+
500
+ // Rebuild index if we had to fall back
501
+ try {
502
+ const index: SessionIndex = {
503
+ sessions: {},
504
+ lastUpdated: new Date().toISOString(),
505
+ };
506
+ for (const session of sessions) {
507
+ const { id, ...rest } = session;
508
+ index.sessions[id] = {
509
+ ...rest,
510
+ lastActiveAt: session.lastActiveAt.toISOString(),
511
+ };
512
+ }
513
+ await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
514
+ } catch (error) {
515
+ logger.warn(`Failed to rebuild session index for ${workdir}:`, error);
516
+ }
517
+
518
+ return sortedSessions;
393
519
  } catch (error) {
394
520
  throw new Error(`Failed to list sessions: ${error}`);
395
521
  }
@@ -432,6 +558,38 @@ export async function cleanupExpiredSessionsFromJsonl(
432
558
  if (fileAge > maxAge) {
433
559
  await fs.unlink(filePath);
434
560
  deletedCount++;
561
+
562
+ // Remove from index if it exists
563
+ try {
564
+ const indexPath = join(
565
+ projectDir.encodedPath,
566
+ SESSION_INDEX_FILENAME,
567
+ );
568
+ const indexContent = await fs.readFile(indexPath, "utf8");
569
+ const index = JSON.parse(indexContent) as SessionIndex;
570
+ const uuidMatch = file.match(
571
+ /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
572
+ );
573
+ const subagentMatch = file.match(
574
+ /^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
575
+ );
576
+ const sessionId = uuidMatch
577
+ ? uuidMatch[1]
578
+ : subagentMatch
579
+ ? subagentMatch[1]
580
+ : null;
581
+
582
+ if (sessionId && index.sessions[sessionId]) {
583
+ delete index.sessions[sessionId];
584
+ await fs.writeFile(
585
+ indexPath,
586
+ JSON.stringify(index, null, 2),
587
+ "utf8",
588
+ );
589
+ }
590
+ } catch {
591
+ // Ignore index update errors during cleanup
592
+ }
435
593
  }
436
594
  } catch {
437
595
  // Skip failed operations and continue processing other files