wave-agent-sdk 0.18.7 → 0.19.1

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 (100) 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 +55 -10
  4. package/builtin/skills/settings/SKILL.md +8 -4
  5. package/builtin/skills/settings/SKILLS.md +6 -4
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +12 -5
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +20 -4
  13. package/dist/managers/mcpManager.d.ts.map +1 -1
  14. package/dist/managers/mcpManager.js +7 -2
  15. package/dist/managers/messageManager.d.ts +3 -5
  16. package/dist/managers/messageManager.d.ts.map +1 -1
  17. package/dist/managers/messageManager.js +17 -77
  18. package/dist/managers/skillManager.d.ts +1 -0
  19. package/dist/managers/skillManager.d.ts.map +1 -1
  20. package/dist/managers/skillManager.js +11 -2
  21. package/dist/prompts/index.d.ts +3 -2
  22. package/dist/prompts/index.d.ts.map +1 -1
  23. package/dist/prompts/index.js +5 -1
  24. package/dist/services/aiService.d.ts.map +1 -1
  25. package/dist/services/aiService.js +16 -47
  26. package/dist/services/configurationService.d.ts.map +1 -1
  27. package/dist/services/configurationService.js +25 -19
  28. package/dist/services/hook.d.ts.map +1 -1
  29. package/dist/services/hook.js +6 -0
  30. package/dist/services/initializationService.d.ts.map +1 -1
  31. package/dist/services/initializationService.js +1 -1
  32. package/dist/services/interactionService.js +1 -1
  33. package/dist/services/reversionService.d.ts +5 -1
  34. package/dist/services/reversionService.d.ts.map +1 -1
  35. package/dist/services/reversionService.js +50 -22
  36. package/dist/services/session.d.ts +4 -13
  37. package/dist/services/session.d.ts.map +1 -1
  38. package/dist/services/session.js +59 -215
  39. package/dist/telemetry/sessionTracing.d.ts +14 -4
  40. package/dist/telemetry/sessionTracing.d.ts.map +1 -1
  41. package/dist/telemetry/sessionTracing.js +134 -8
  42. package/dist/tools/bashTool.d.ts.map +1 -1
  43. package/dist/tools/bashTool.js +16 -1
  44. package/dist/types/config.d.ts +11 -2
  45. package/dist/types/config.d.ts.map +1 -1
  46. package/dist/types/hooks.d.ts +1 -0
  47. package/dist/types/hooks.d.ts.map +1 -1
  48. package/dist/types/messaging.d.ts +0 -1
  49. package/dist/types/messaging.d.ts.map +1 -1
  50. package/dist/types/skills.d.ts +1 -0
  51. package/dist/types/skills.d.ts.map +1 -1
  52. package/dist/types/telemetry.d.ts +14 -1
  53. package/dist/types/telemetry.d.ts.map +1 -1
  54. package/dist/utils/atomicWrite.d.ts +15 -0
  55. package/dist/utils/atomicWrite.d.ts.map +1 -0
  56. package/dist/utils/atomicWrite.js +59 -0
  57. package/dist/utils/cacheControlUtils.d.ts +3 -15
  58. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  59. package/dist/utils/cacheControlUtils.js +4 -35
  60. package/dist/utils/containerSetup.d.ts.map +1 -1
  61. package/dist/utils/containerSetup.js +5 -1
  62. package/dist/utils/convertMessagesForAPI.d.ts +9 -1
  63. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  64. package/dist/utils/convertMessagesForAPI.js +74 -55
  65. package/dist/utils/mcpUtils.d.ts.map +1 -1
  66. package/dist/utils/mcpUtils.js +2 -1
  67. package/dist/utils/modelCapabilities.d.ts +22 -0
  68. package/dist/utils/modelCapabilities.d.ts.map +1 -0
  69. package/dist/utils/modelCapabilities.js +24 -0
  70. package/dist/utils/skillParser.d.ts.map +1 -1
  71. package/dist/utils/skillParser.js +3 -1
  72. package/package.json +1 -1
  73. package/src/agent.ts +11 -5
  74. package/src/index.ts +1 -0
  75. package/src/managers/aiManager.ts +22 -2
  76. package/src/managers/mcpManager.ts +8 -2
  77. package/src/managers/messageManager.ts +22 -104
  78. package/src/managers/skillManager.ts +16 -2
  79. package/src/prompts/index.ts +6 -1
  80. package/src/services/aiService.ts +15 -85
  81. package/src/services/configurationService.ts +27 -41
  82. package/src/services/hook.ts +7 -0
  83. package/src/services/initializationService.ts +1 -3
  84. package/src/services/interactionService.ts +1 -1
  85. package/src/services/reversionService.ts +50 -27
  86. package/src/services/session.ts +63 -262
  87. package/src/telemetry/sessionTracing.ts +167 -12
  88. package/src/tools/bashTool.ts +23 -1
  89. package/src/types/config.ts +12 -2
  90. package/src/types/hooks.ts +1 -0
  91. package/src/types/messaging.ts +0 -1
  92. package/src/types/skills.ts +1 -0
  93. package/src/types/telemetry.ts +17 -1
  94. package/src/utils/atomicWrite.ts +61 -0
  95. package/src/utils/cacheControlUtils.ts +5 -38
  96. package/src/utils/containerSetup.ts +5 -1
  97. package/src/utils/convertMessagesForAPI.ts +87 -57
  98. package/src/utils/mcpUtils.ts +2 -1
  99. package/src/utils/modelCapabilities.ts +30 -0
  100. package/src/utils/skillParser.ts +3 -1
@@ -30,6 +30,7 @@ import { logger } from "../utils/globalLogger.js";
30
30
  export class SkillManager extends EventEmitter {
31
31
  private personalSkillsPath: string;
32
32
  private personalClaudeSkillsPath: string;
33
+ private personalAgentsSkillsPath: string;
33
34
  private scanTimeout: number;
34
35
  private workdir: string;
35
36
 
@@ -50,6 +51,8 @@ export class SkillManager extends EventEmitter {
50
51
  options.personalSkillsPath || join(homedir(), ".wave", "skills");
51
52
  this.personalClaudeSkillsPath =
52
53
  options.personalClaudeSkillsPath || join(homedir(), ".claude", "skills");
54
+ this.personalAgentsSkillsPath =
55
+ options.personalAgentsSkillsPath || join(homedir(), ".agents", "skills");
53
56
  this.scanTimeout = options.scanTimeout || 5000;
54
57
  this.workdir = options.workdir || process.cwd();
55
58
  this.watchEnabled = options.watch ?? false;
@@ -131,8 +134,10 @@ export class SkillManager extends EventEmitter {
131
134
  const pathsToWatch = [
132
135
  this.personalSkillsPath,
133
136
  this.personalClaudeSkillsPath,
137
+ this.personalAgentsSkillsPath,
134
138
  join(this.workdir, ".wave", "skills"),
135
139
  join(this.workdir, ".claude", "skills"),
140
+ join(this.workdir, ".agents", "skills"),
136
141
  ];
137
142
 
138
143
  logger?.debug(`Setting up skill watcher for: ${pathsToWatch.join(", ")}`);
@@ -225,6 +230,11 @@ export class SkillManager extends EventEmitter {
225
230
  "builtin",
226
231
  );
227
232
 
233
+ const personalAgentsCollection = await this.discoverSkillCollection(
234
+ this.personalAgentsSkillsPath,
235
+ "personal",
236
+ );
237
+
228
238
  const personalClaudeCollection = await this.discoverSkillCollection(
229
239
  this.personalClaudeSkillsPath,
230
240
  "personal",
@@ -243,12 +253,14 @@ export class SkillManager extends EventEmitter {
243
253
  return {
244
254
  builtinSkills: builtinCollection.skills,
245
255
  personalSkills: new Map([
246
- ...personalClaudeCollection.skills,
256
+ ...personalAgentsCollection.skills, // lowest priority
257
+ ...personalClaudeCollection.skills, // overrides .agents
247
258
  ...personalCollection.skills, // .wave overrides .claude
248
259
  ]),
249
260
  projectSkills: projectCollection.skills,
250
261
  errors: [
251
262
  ...builtinCollection.errors,
263
+ ...personalAgentsCollection.errors,
252
264
  ...personalClaudeCollection.errors,
253
265
  ...personalCollection.errors,
254
266
  ...projectCollection.errors,
@@ -271,9 +283,11 @@ export class SkillManager extends EventEmitter {
271
283
  };
272
284
 
273
285
  if (type === "project") {
274
- // Scan .claude/skills first, then .wave/skills (wave overrides)
286
+ // Scan .agents/skills first, then .claude/skills, then .wave/skills (wave overrides)
287
+ const agentsPath = join(basePath, ".agents", "skills");
275
288
  const claudePath = join(basePath, ".claude", "skills");
276
289
  const wavePath = join(basePath, ".wave", "skills");
290
+ await this.scanSkillPath(agentsPath, collection);
277
291
  await this.scanSkillPath(claudePath, collection);
278
292
  await this.scanSkillPath(wavePath, collection);
279
293
  } else {
@@ -19,7 +19,11 @@ import {
19
19
  GREP_TOOL_NAME,
20
20
  } from "../constants/tools.js";
21
21
 
22
- export const BASE_SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
22
+ export const BASE_SYSTEM_PROMPT = `You are a coding agent developed by NetEase that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
23
+
24
+ export const MODEL_IDENTITY_PROMPT = `# Model identity
25
+
26
+ If asked about your model, model version, underlying architecture, or product name, identify yourself simply as a coding agent developed by NetEase. Do not reveal the specific model ID, model family, model provider, or product name. Do not mention or hint at any of these details.`;
23
27
 
24
28
  export const DOING_TASKS_PROMPT = `# Doing tasks
25
29
  - The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.
@@ -265,6 +269,7 @@ export function buildSystemPrompt(
265
269
 
266
270
  staticText += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
267
271
  staticText += `\n\n${TONE_AND_STYLE_PROMPT}`;
272
+ staticText += `\n\n${MODEL_IDENTITY_PROMPT}`;
268
273
 
269
274
  const blocks: SystemPromptBlock[] = [{ text: staticText, cacheable: true }];
270
275
 
@@ -13,11 +13,11 @@ import type { GatewayConfig, ModelConfig } from "../types/index.js";
13
13
  import { ConfigurationError, CONFIG_ERRORS } from "../types/index.js";
14
14
  import {
15
15
  transformMessagesForExplicitCache,
16
- supportsPromptCaching,
17
16
  extendUsageWithCacheMetrics,
18
17
  type ClaudeUsage,
19
18
  type ClaudeChatCompletionContentPartText,
20
19
  } from "../utils/cacheControlUtils.js";
20
+ import { supportsPromptCaching } from "../utils/modelCapabilities.js";
21
21
 
22
22
  import * as os from "os";
23
23
  import * as fs from "fs";
@@ -265,13 +265,12 @@ export async function callAgent(
265
265
  });
266
266
 
267
267
  // Determine model early (needed for system prompt construction)
268
- const currentModel = model || modelConfig.model;
269
268
  const resolvedMaxTokens = options.maxTokens ?? modelConfig.maxTokens;
270
269
 
271
270
  // Build system message content
272
271
  let systemMessage: ChatCompletionMessageParam;
273
272
  if (Array.isArray(systemPrompt)) {
274
- if (supportsPromptCaching(currentModel)) {
273
+ if (supportsPromptCaching(modelConfig.capabilities)) {
275
274
  // For Claude models, map blocks to content parts with cache_control on cacheable blocks
276
275
  const contentParts: ClaudeChatCompletionContentPartText[] =
277
276
  systemPrompt.map((block) => {
@@ -307,29 +306,16 @@ export async function callAgent(
307
306
 
308
307
  processedTools = tools;
309
308
 
310
- if (supportsPromptCaching(currentModel)) {
309
+ if (supportsPromptCaching(modelConfig.capabilities)) {
311
310
  openaiMessages = transformMessagesForExplicitCache(
312
311
  openaiMessages,
313
- currentModel,
312
+ modelConfig.capabilities,
314
313
  );
315
314
  }
316
315
 
317
- // Get model configuration - use injected modelConfig with optional override
318
- const {
319
- model: _model,
320
- fastModel: _fastModel,
321
- maxTokens: _maxTokens,
322
- permissionMode: _permissionMode,
323
- ...extraParams
324
- } = modelConfig;
325
- void _model;
326
- void _fastModel;
327
- void _maxTokens;
328
- void _permissionMode;
329
-
330
316
  const openaiModelConfig = getModelConfig(model || modelConfig.model, {
331
317
  max_tokens: resolvedMaxTokens,
332
- ...extraParams,
318
+ ...(modelConfig.options || {}),
333
319
  });
334
320
 
335
321
  // Determine if streaming is needed
@@ -860,25 +846,11 @@ export async function compactMessages(
860
846
  fetch: gatewayConfig.fetch,
861
847
  });
862
848
 
863
- // Get model configuration - use injected agent model
864
- const {
865
- model: _model,
866
- fastModel: _fastModel,
867
- maxTokens: _maxTokens,
868
- permissionMode: _permissionMode,
869
- fastModelConfig: _fastModelConfig,
870
- ...extraParams
871
- } = modelConfig;
872
- void _model;
873
- void _fastModel;
874
- void _maxTokens;
875
- void _permissionMode;
876
-
877
- // When a fast model override is provided, use the fast model's hyperparams
878
- // (if configured); otherwise fall back to the agent model's extraParams.
849
+ // When a fast model override is provided, use the fast model's options
850
+ // (if configured); otherwise fall back to the agent model's options.
879
851
  const activeExtraParams = options.model
880
- ? _fastModelConfig || {}
881
- : extraParams;
852
+ ? modelConfig.fastModelOptions || {}
853
+ : modelConfig.options || {};
882
854
 
883
855
  const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
884
856
  temperature: 0.1,
@@ -983,25 +955,11 @@ export async function processWebContent(
983
955
  fetch: gatewayConfig.fetch,
984
956
  });
985
957
 
986
- // Get model configuration - use injected agent model
987
- const {
988
- model: _model,
989
- fastModel: _fastModel,
990
- maxTokens: _maxTokens,
991
- permissionMode: _permissionMode,
992
- fastModelConfig: _fastModelConfig,
993
- ...extraParams
994
- } = modelConfig;
995
- void _model;
996
- void _fastModel;
997
- void _maxTokens;
998
- void _permissionMode;
999
-
1000
- // When a fast model override is provided, use the fast model's hyperparams
1001
- // (if configured); otherwise fall back to the agent model's extraParams.
958
+ // When a fast model override is provided, use the fast model's options
959
+ // (if configured); otherwise fall back to the agent model's options.
1002
960
  const activeExtraParams = options.model
1003
- ? _fastModelConfig || {}
1004
- : extraParams;
961
+ ? modelConfig.fastModelOptions || {}
962
+ : modelConfig.options || {};
1005
963
 
1006
964
  const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
1007
965
  temperature: 0.1,
@@ -1100,23 +1058,10 @@ export async function btw(options: BtwOptions): Promise<BtwResult> {
1100
1058
  fetch: gatewayConfig.fetch,
1101
1059
  });
1102
1060
 
1103
- // Get model configuration - use injected agent model
1104
- const {
1105
- model: _model,
1106
- fastModel: _fastModel,
1107
- maxTokens: _maxTokens,
1108
- permissionMode: _permissionMode,
1109
- ...extraParams
1110
- } = modelConfig;
1111
- void _model;
1112
- void _fastModel;
1113
- void _maxTokens;
1114
- void _permissionMode;
1115
-
1116
1061
  const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
1117
1062
  temperature: 0.1,
1118
1063
  max_tokens: 4096,
1119
- ...extraParams,
1064
+ ...(modelConfig.options || {}),
1120
1065
  });
1121
1066
 
1122
1067
  try {
@@ -1207,25 +1152,10 @@ export async function evaluateGoal(
1207
1152
  fetch: gatewayConfig.fetch,
1208
1153
  });
1209
1154
 
1210
- const {
1211
- model: _model,
1212
- fastModel: _fastModel,
1213
- maxTokens: _maxTokens,
1214
- permissionMode: _permissionMode,
1215
- fastModelConfig: _fastModelConfig,
1216
- } = modelConfig;
1217
- void _model;
1218
- void _fastModel;
1219
- void _maxTokens;
1220
- void _permissionMode;
1221
-
1222
- // evaluateGoal always uses the fast model; use its hyperparams if configured.
1223
- const activeExtraParams = _fastModelConfig || {};
1224
-
1225
1155
  const openaiModelConfig = getModelConfig(model, {
1226
1156
  temperature: 0,
1227
1157
  max_tokens: 200,
1228
- ...activeExtraParams,
1158
+ ...(modelConfig.fastModelOptions || {}),
1229
1159
  });
1230
1160
 
1231
1161
  // Strip images from messages to reduce token usage (same as compact)
@@ -48,6 +48,7 @@ import {
48
48
  } from "./remoteSettingsService.js";
49
49
  import { createAuthAwareFetch } from "./authService.js";
50
50
  import { ensureWaveRuntimeFilesExcluded } from "../utils/gitUtils.js";
51
+ import { atomicWriteFile } from "../utils/atomicWrite.js";
51
52
 
52
53
  /**
53
54
  * Default ConfigurationService implementation
@@ -533,25 +534,13 @@ export class ConfigurationService {
533
534
  permissionMode: permissionMode ?? this.options.permissionMode,
534
535
  };
535
536
 
536
- // Resolve fast model hyperparams from models[fastModel], stripping
537
- // structural fields so only hyperparameters remain. Set on baseConfig
538
- // before the modelSpecificConfig spread so it isn't overwritten.
539
- const fastModelConfigSource =
537
+ // Resolve fast model generation params from models[fastModel].options.
538
+ // Set on baseConfig before the modelSpecificConfig spread so it isn't overwritten.
539
+ const fastModelSource =
540
540
  resolvedFastModel &&
541
541
  this.currentConfiguration?.models?.[resolvedFastModel];
542
- if (fastModelConfigSource) {
543
- const {
544
- model: _fm,
545
- fastModel: _ffm,
546
- maxTokens: _fmt,
547
- permissionMode: _fpm,
548
- ...fastHyperparams
549
- } = fastModelConfigSource;
550
- void _fm;
551
- void _ffm;
552
- void _fmt;
553
- void _fpm;
554
- baseConfig.fastModelConfig = fastHyperparams;
542
+ if (fastModelSource && fastModelSource.options) {
543
+ baseConfig.fastModelOptions = fastModelSource.options;
555
544
  }
556
545
 
557
546
  // Merge model-specific settings from configuration
@@ -729,7 +718,7 @@ export class ConfigurationService {
729
718
  }
730
719
 
731
720
  config.model = model;
732
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
721
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
733
722
  }
734
723
 
735
724
  /**
@@ -816,11 +805,7 @@ export class ConfigurationService {
816
805
 
817
806
  if (!config.permissions.allow.includes(rule)) {
818
807
  config.permissions.allow.push(rule);
819
- await fs.writeFile(
820
- localConfigPath,
821
- JSON.stringify(config, null, 2),
822
- "utf-8",
823
- );
808
+ await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
824
809
  }
825
810
  }
826
811
 
@@ -868,7 +853,7 @@ export class ConfigurationService {
868
853
 
869
854
  config.enabledPlugins[pluginId] = enabled;
870
855
 
871
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
856
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
872
857
  }
873
858
 
874
859
  /**
@@ -940,11 +925,7 @@ export class ConfigurationService {
940
925
  }
941
926
  fileConfig.marketplaces[name] = config;
942
927
 
943
- await fs.writeFile(
944
- configPath,
945
- JSON.stringify(fileConfig, null, 2),
946
- "utf-8",
947
- );
928
+ await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
948
929
  }
949
930
 
950
931
  /**
@@ -978,11 +959,7 @@ export class ConfigurationService {
978
959
 
979
960
  if (fileConfig.marketplaces && name in fileConfig.marketplaces) {
980
961
  delete fileConfig.marketplaces[name];
981
- await fs.writeFile(
982
- configPath,
983
- JSON.stringify(fileConfig, null, 2),
984
- "utf-8",
985
- );
962
+ await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
986
963
  }
987
964
  } catch {
988
965
  // Ignore errors for corrupted or non-existent files
@@ -1020,11 +997,7 @@ export class ConfigurationService {
1020
997
 
1021
998
  if (config.enabledPlugins && pluginId in config.enabledPlugins) {
1022
999
  delete config.enabledPlugins[pluginId];
1023
- await fs.writeFile(
1024
- configPath,
1025
- JSON.stringify(config, null, 2),
1026
- "utf-8",
1027
- );
1000
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
1028
1001
  }
1029
1002
  } catch {
1030
1003
  // Ignore errors for corrupted or non-existent files
@@ -1171,6 +1144,15 @@ export function loadWaveConfigFromFile(
1171
1144
 
1172
1145
  try {
1173
1146
  const content = readFileSync(filePath, "utf-8");
1147
+
1148
+ // Tolerate empty/whitespace-only files: a concurrent writer may have the
1149
+ // file truncated mid-write. Returning null lets callers skip this source
1150
+ // instead of crashing (consistent with the "file not found" path).
1151
+ if (content.trim() === "") {
1152
+ logger.warn(`Empty configuration file (likely mid-write): ${filePath}`);
1153
+ return null;
1154
+ }
1155
+
1174
1156
  const config = JSON.parse(content) as WaveConfiguration;
1175
1157
 
1176
1158
  return {
@@ -1189,10 +1171,14 @@ export function loadWaveConfigFromFile(
1189
1171
  };
1190
1172
  } catch (error) {
1191
1173
  if (error instanceof SyntaxError) {
1192
- throw new Error(`Invalid JSON syntax in ${filePath}: ${error.message}`);
1174
+ // Tolerate corrupt JSON: a concurrent writer may leave the file in a
1175
+ // partially-written state. Warn and return null so callers skip this
1176
+ // source instead of crashing.
1177
+ logger.warn(`Invalid JSON syntax in ${filePath}: ${error.message}`);
1178
+ return null;
1193
1179
  }
1194
1180
 
1195
- // Re-throw validation errors and other errors as-is
1181
+ // Re-throw other errors as-is
1196
1182
  throw error;
1197
1183
  }
1198
1184
  }
@@ -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";