wave-agent-sdk 0.18.6 → 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 (129) 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 +31 -8
  12. package/dist/managers/cronManager.d.ts.map +1 -1
  13. package/dist/managers/cronManager.js +2 -0
  14. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  15. package/dist/managers/liveConfigManager.js +0 -9
  16. package/dist/managers/mcpManager.d.ts.map +1 -1
  17. package/dist/managers/mcpManager.js +13 -3
  18. package/dist/managers/messageManager.d.ts +4 -6
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +27 -81
  21. package/dist/managers/skillManager.d.ts +1 -0
  22. package/dist/managers/skillManager.d.ts.map +1 -1
  23. package/dist/managers/skillManager.js +11 -2
  24. package/dist/managers/toolManager.d.ts.map +1 -1
  25. package/dist/managers/toolManager.js +2 -0
  26. package/dist/prompts/index.d.ts +3 -2
  27. package/dist/prompts/index.d.ts.map +1 -1
  28. package/dist/prompts/index.js +5 -1
  29. package/dist/services/aiService.d.ts.map +1 -1
  30. package/dist/services/aiService.js +13 -9
  31. package/dist/services/configurationService.d.ts.map +1 -1
  32. package/dist/services/configurationService.js +24 -9
  33. package/dist/services/hook.d.ts.map +1 -1
  34. package/dist/services/hook.js +6 -0
  35. package/dist/services/initializationService.d.ts.map +1 -1
  36. package/dist/services/initializationService.js +1 -1
  37. package/dist/services/interactionService.js +1 -1
  38. package/dist/services/reversionService.d.ts +5 -1
  39. package/dist/services/reversionService.d.ts.map +1 -1
  40. package/dist/services/reversionService.js +50 -22
  41. package/dist/services/session.d.ts +4 -13
  42. package/dist/services/session.d.ts.map +1 -1
  43. package/dist/services/session.js +59 -215
  44. package/dist/telemetry/sessionTracing.d.ts +31 -20
  45. package/dist/telemetry/sessionTracing.d.ts.map +1 -1
  46. package/dist/telemetry/sessionTracing.js +192 -62
  47. package/dist/tools/bashTool.d.ts.map +1 -1
  48. package/dist/tools/bashTool.js +16 -1
  49. package/dist/tools/readTool.d.ts.map +1 -1
  50. package/dist/tools/readTool.js +6 -7
  51. package/dist/types/config.d.ts +7 -0
  52. package/dist/types/config.d.ts.map +1 -1
  53. package/dist/types/hooks.d.ts +1 -0
  54. package/dist/types/hooks.d.ts.map +1 -1
  55. package/dist/types/messaging.d.ts +0 -1
  56. package/dist/types/messaging.d.ts.map +1 -1
  57. package/dist/types/skills.d.ts +1 -0
  58. package/dist/types/skills.d.ts.map +1 -1
  59. package/dist/types/telemetry.d.ts +16 -1
  60. package/dist/types/telemetry.d.ts.map +1 -1
  61. package/dist/utils/atomicWrite.d.ts +15 -0
  62. package/dist/utils/atomicWrite.d.ts.map +1 -0
  63. package/dist/utils/atomicWrite.js +59 -0
  64. package/dist/utils/cacheControlUtils.d.ts +3 -15
  65. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  66. package/dist/utils/cacheControlUtils.js +4 -35
  67. package/dist/utils/containerSetup.d.ts.map +1 -1
  68. package/dist/utils/containerSetup.js +5 -1
  69. package/dist/utils/convertMessagesForAPI.d.ts +9 -1
  70. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  71. package/dist/utils/convertMessagesForAPI.js +74 -55
  72. package/dist/utils/fileUtils.d.ts +0 -6
  73. package/dist/utils/fileUtils.d.ts.map +1 -1
  74. package/dist/utils/fileUtils.js +0 -43
  75. package/dist/utils/gitUtils.d.ts +11 -0
  76. package/dist/utils/gitUtils.d.ts.map +1 -1
  77. package/dist/utils/gitUtils.js +51 -0
  78. package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -1
  79. package/dist/utils/groupMessagesByApiRound.js +7 -6
  80. package/dist/utils/mcpUtils.d.ts.map +1 -1
  81. package/dist/utils/mcpUtils.js +2 -1
  82. package/dist/utils/modelCapabilities.d.ts +22 -0
  83. package/dist/utils/modelCapabilities.d.ts.map +1 -0
  84. package/dist/utils/modelCapabilities.js +24 -0
  85. package/dist/utils/skillParser.d.ts.map +1 -1
  86. package/dist/utils/skillParser.js +3 -1
  87. package/dist/utils/tokenEstimate.d.ts +24 -0
  88. package/dist/utils/tokenEstimate.d.ts.map +1 -0
  89. package/dist/utils/tokenEstimate.js +30 -0
  90. package/dist/utils/worktreeUtils.d.ts.map +1 -1
  91. package/dist/utils/worktreeUtils.js +3 -1
  92. package/package.json +1 -1
  93. package/src/agent.ts +11 -5
  94. package/src/index.ts +1 -0
  95. package/src/managers/aiManager.ts +36 -7
  96. package/src/managers/cronManager.ts +2 -0
  97. package/src/managers/liveConfigManager.ts +0 -12
  98. package/src/managers/mcpManager.ts +16 -3
  99. package/src/managers/messageManager.ts +32 -109
  100. package/src/managers/skillManager.ts +16 -2
  101. package/src/managers/toolManager.ts +2 -0
  102. package/src/prompts/index.ts +6 -1
  103. package/src/services/aiService.ts +12 -5
  104. package/src/services/configurationService.ts +26 -24
  105. package/src/services/hook.ts +7 -0
  106. package/src/services/initializationService.ts +1 -3
  107. package/src/services/interactionService.ts +1 -1
  108. package/src/services/reversionService.ts +50 -27
  109. package/src/services/session.ts +63 -262
  110. package/src/telemetry/sessionTracing.ts +225 -73
  111. package/src/tools/bashTool.ts +23 -1
  112. package/src/tools/readTool.ts +6 -12
  113. package/src/types/config.ts +8 -0
  114. package/src/types/hooks.ts +1 -0
  115. package/src/types/messaging.ts +0 -1
  116. package/src/types/skills.ts +1 -0
  117. package/src/types/telemetry.ts +19 -1
  118. package/src/utils/atomicWrite.ts +61 -0
  119. package/src/utils/cacheControlUtils.ts +5 -38
  120. package/src/utils/containerSetup.ts +5 -1
  121. package/src/utils/convertMessagesForAPI.ts +87 -57
  122. package/src/utils/fileUtils.ts +0 -46
  123. package/src/utils/gitUtils.ts +54 -0
  124. package/src/utils/groupMessagesByApiRound.ts +7 -6
  125. package/src/utils/mcpUtils.ts +2 -1
  126. package/src/utils/modelCapabilities.ts +30 -0
  127. package/src/utils/skillParser.ts +3 -1
  128. package/src/utils/tokenEstimate.ts +34 -0
  129. package/src/utils/worktreeUtils.ts +8 -1
@@ -31,6 +31,7 @@ import type { MemoryRuleManager } from "./MemoryRuleManager.js";
31
31
  import type { MemoryRule } from "../types/memoryRule.js";
32
32
  import type { MemoryService } from "../services/memory.js";
33
33
  import { pathEncoder } from "../utils/pathEncoder.js";
34
+ import { estimateTokens } from "../utils/tokenEstimate.js";
34
35
  import { READ_TOOL_NAME } from "../constants/tools.js";
35
36
 
36
37
  import { Container } from "../utils/container.js";
@@ -93,8 +94,6 @@ export interface MessageManagerOptions {
93
94
  export class MessageManager {
94
95
  // Private state properties
95
96
  private sessionId: string;
96
- private rootSessionId: string;
97
- private parentSessionId?: string;
98
97
  private messages: Message[];
99
98
  private latestTotalTokens: number;
100
99
  private workdir: string;
@@ -117,7 +116,6 @@ export class MessageManager {
117
116
  options: MessageManagerOptions,
118
117
  ) {
119
118
  this.sessionId = generateSessionId();
120
- this.rootSessionId = this.sessionId;
121
119
  this.messages = [];
122
120
  this.latestTotalTokens = 0;
123
121
  this.workdir = options.workdir;
@@ -151,11 +149,7 @@ export class MessageManager {
151
149
  }
152
150
 
153
151
  public getRootSessionId(): string {
154
- return this.rootSessionId;
155
- }
156
-
157
- public getParentSessionId(): string | undefined {
158
- return this.parentSessionId;
152
+ return this.sessionId;
159
153
  }
160
154
 
161
155
  public getMessages(): Message[] {
@@ -357,8 +351,6 @@ export class MessageManager {
357
351
  unsavedMessages,
358
352
  this.workdir,
359
353
  this.sessionType,
360
- this.rootSessionId,
361
- this.parentSessionId,
362
354
  );
363
355
 
364
356
  // Update the saved message count
@@ -382,7 +374,6 @@ export class MessageManager {
382
374
  public clearMessages(): void {
383
375
  this.setMessages([]);
384
376
  const newSessionId = generateSessionId();
385
- this.rootSessionId = newSessionId;
386
377
  this.setSessionId(newSessionId);
387
378
  this.setlatestTotalTokens(0);
388
379
  this.savedMessageCount = 0; // Reset saved message count
@@ -398,8 +389,6 @@ export class MessageManager {
398
389
  // Initialize state from session data
399
390
  public initializeFromSession(sessionData: SessionData): void {
400
391
  this.setSessionId(sessionData.id);
401
- this.rootSessionId = sessionData.rootSessionId || sessionData.id;
402
- this.parentSessionId = sessionData.parentSessionId;
403
392
  this.setMessages([...sessionData.messages]);
404
393
  this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
405
394
 
@@ -562,12 +551,13 @@ export class MessageManager {
562
551
  }
563
552
 
564
553
  /**
565
- * Compact messages and update session, delete compacted messages, only keep compacted messages and last 3 messages
554
+ * Compact messages and update session append compact message to same file (append-only).
555
+ * After compaction, in-memory messages are [compactMessage, ...lastThreeMessages].
566
556
  */
567
- public compactMessagesAndUpdateSession(
557
+ public async compactMessagesAndUpdateSession(
568
558
  compactedContent: string,
569
559
  usage?: Usage,
570
- ): void {
560
+ ): Promise<void> {
571
561
  // Get last 2 API rounds to preserve (structurally safe boundary)
572
562
  const lastThreeMessages = getLastApiRounds(this.messages, 2);
573
563
 
@@ -579,23 +569,27 @@ export class MessageManager {
579
569
  {
580
570
  type: "compact",
581
571
  content: compactedContent,
582
- sessionId: this.sessionId,
583
572
  },
584
573
  ],
585
574
  timestamp: new Date().toISOString(),
586
575
  ...(usage && { usage }),
587
576
  };
588
577
 
589
- // Build new message array: keep the compacted message and last 3 messages
578
+ // Build new message array: keep the compacted message and last messages
590
579
  const newMessages: Message[] = [compactMessage, ...lastThreeMessages];
591
580
 
592
- // Update sessionId and parentSessionId
593
- const oldSessionId = this.sessionId;
594
- this.setSessionId(generateSessionId());
595
- this.parentSessionId = oldSessionId;
581
+ // APPEND to the same session file (append-only, like Claude Code)
582
+ const { appendMessages } = await import("../services/session.js");
583
+ await appendMessages(
584
+ this.sessionId,
585
+ newMessages,
586
+ this.workdir,
587
+ this.sessionType,
588
+ );
596
589
 
597
- // Set new message list
590
+ // Update in-memory state
598
591
  this.setMessages(newMessages);
592
+ this.savedMessageCount = newMessages.length;
599
593
 
600
594
  // Clear and rebuild loaded rule IDs from remaining meta messages
601
595
  this.clearLoadedRuleIds();
@@ -910,101 +904,26 @@ export class MessageManager {
910
904
  index: number,
911
905
  reversionManager?: import("./reversionManager.js").ReversionManager,
912
906
  ): Promise<void> {
913
- const { messages, sessionIds } = await this.getFullMessageThread();
907
+ const { messages } = await this.getFullMessageThread();
914
908
 
915
909
  if (index < 0 || index >= messages.length) {
916
910
  throw new Error(`Invalid message index: ${index}`);
917
911
  }
918
912
 
919
- // Find which session the index belongs to
920
- let targetSessionId = this.sessionId;
921
- let targetIndexInSession = index;
922
-
923
- // We need to be careful here because loadFullMessageThread might have removed "compact" blocks
924
- // Let's re-calculate based on the actual messages returned.
925
- // Actually, it's easier to just load sessions one by one again or keep track of counts.
926
-
927
- // For simplicity, let's assume we want to truncate the WHOLE thread.
928
- // If the index is in a previous session, we need to:
929
- // 1. Load that session.
930
- // 2. Truncate it.
931
- // 3. Make it the current session.
932
- // 4. Delete/Invalidate subsequent sessions.
933
-
934
- // To correctly map 'index' to a session, we need to know the message count of each session
935
- // as they appear in the concatenated 'messages' array.
936
-
937
- let remainingIndex = index;
938
- const { loadSessionFromJsonl } = await import("../services/session.js");
939
-
940
- for (const sid of sessionIds) {
941
- const sessionData = await loadSessionFromJsonl(sid, this.workdir);
942
- if (!sessionData) continue;
943
-
944
- const sessionMessages = sessionData.messages;
945
- // If this is not the first session in the thread, it might have a compact block at the start
946
- // that was removed in getFullMessageThread.
947
- const hasCompactBlock = sessionMessages[0]?.blocks.some(
948
- (b) => b.type === "compact",
949
- );
950
- const effectiveMessages =
951
- hasCompactBlock && sid !== sessionIds[0]
952
- ? sessionMessages.slice(1)
953
- : sessionMessages;
954
-
955
- if (remainingIndex < effectiveMessages.length) {
956
- targetSessionId = sid;
957
- targetIndexInSession = hasCompactBlock
958
- ? remainingIndex + 1
959
- : remainingIndex;
960
- break;
961
- }
962
- remainingIndex -= effectiveMessages.length;
963
- }
964
-
965
- // Load the target session to perform truncation
966
- const targetSessionData = await loadSessionFromJsonl(
967
- targetSessionId,
968
- this.workdir,
969
- );
970
- if (!targetSessionData)
971
- throw new Error(`Target session ${targetSessionId} not found`);
972
-
973
- // Identify messages to be removed (from the whole thread)
913
+ const newMessages = messages.slice(0, index);
974
914
  const messagesToRemove = messages.slice(index);
975
915
  const messageIdsToRemove = messagesToRemove
976
916
  .map((m) => m.id as string)
977
917
  .filter((id) => !!id);
978
918
 
979
- // Revert file changes if manager is provided
980
919
  if (reversionManager && messageIdsToRemove.length > 0) {
981
920
  await reversionManager.revertTo(messageIdsToRemove, messages);
982
921
  }
983
922
 
984
- // Truncate messages in the target session
985
- const newMessagesInSession = targetSessionData.messages.slice(
986
- 0,
987
- targetIndexInSession,
988
- );
989
-
990
- // Update target session file
991
- this.sessionId = targetSessionId;
992
- this.rootSessionId = targetSessionData.rootSessionId || targetSessionId;
993
- this.parentSessionId = targetSessionData.parentSessionId;
994
- this.transcriptPath = this.computeTranscriptPath();
995
-
996
- await this.rewriteSessionFile(newMessagesInSession);
997
-
998
- // Update in-memory messages to the truncated session messages
999
- // We do NOT include ancestor messages here to avoid exceeding context limits.
1000
- // The 'compact' block at the start of the session (if any) already summarizes them.
1001
- this.setMessages(newMessagesInSession);
1002
-
1003
- // Update saved message count
1004
- this.savedMessageCount = newMessagesInSession.length;
1005
-
1006
- // Notify session ID change if it changed
1007
- this.callbacks.onSessionIdChange?.(this.sessionId);
923
+ // Rewrite file with truncated messages
924
+ await this.rewriteSessionFile(newMessages);
925
+ this.setMessages(newMessages);
926
+ this.savedMessageCount = newMessages.length;
1008
927
  }
1009
928
 
1010
929
  /**
@@ -1063,7 +982,7 @@ export class MessageManager {
1063
982
  /**
1064
983
  * Get recent file read contents, sorted by timestamp (newest first).
1065
984
  * @param maxFiles - Maximum number of files to return
1066
- * @param maxTokensPerFile - Maximum tokens per file (~4 chars/token)
985
+ * @param maxTokensPerFile - Maximum tokens per file (CJK-aware estimation)
1067
986
  * @returns Array of { path, content } sorted by recency
1068
987
  */
1069
988
  public getRecentFileReads(
@@ -1076,10 +995,14 @@ export class MessageManager {
1076
995
 
1077
996
  const result: Array<{ path: string; content: string }> = [];
1078
997
  for (const [path, { content }] of sorted) {
1079
- const truncated =
1080
- content.length > maxTokensPerFile * 4
1081
- ? content.slice(0, maxTokensPerFile * 4)
1082
- : content;
998
+ const tokenCount = estimateTokens(content);
999
+ let truncated = content;
1000
+ if (tokenCount > maxTokensPerFile) {
1001
+ // Truncate proportionally to fit within the token budget
1002
+ const ratio = maxTokensPerFile / tokenCount;
1003
+ const cutIndex = Math.floor(content.length * ratio);
1004
+ truncated = content.slice(0, cutIndex);
1005
+ }
1083
1006
  result.push({ path, content: truncated });
1084
1007
  }
1085
1008
  return result;
@@ -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 {
@@ -279,6 +279,7 @@ class ToolManager {
279
279
  endToolSpan({
280
280
  success: result.success,
281
281
  durationMs: Date.now() - toolStartTime,
282
+ output: result.content,
282
283
  });
283
284
  return result;
284
285
  } catch (error) {
@@ -303,6 +304,7 @@ class ToolManager {
303
304
  endToolSpan({
304
305
  success: result.success,
305
306
  durationMs: Date.now() - toolStartTime,
307
+ output: result.content,
306
308
  });
307
309
  return result;
308
310
  } catch (error) {
@@ -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,10 +306,10 @@ 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
 
@@ -320,12 +319,14 @@ export async function callAgent(
320
319
  fastModel: _fastModel,
321
320
  maxTokens: _maxTokens,
322
321
  permissionMode: _permissionMode,
322
+ capabilities: _capabilities,
323
323
  ...extraParams
324
324
  } = modelConfig;
325
325
  void _model;
326
326
  void _fastModel;
327
327
  void _maxTokens;
328
328
  void _permissionMode;
329
+ void _capabilities;
329
330
 
330
331
  const openaiModelConfig = getModelConfig(model || modelConfig.model, {
331
332
  max_tokens: resolvedMaxTokens,
@@ -866,6 +867,7 @@ export async function compactMessages(
866
867
  fastModel: _fastModel,
867
868
  maxTokens: _maxTokens,
868
869
  permissionMode: _permissionMode,
870
+ capabilities: _capabilities,
869
871
  fastModelConfig: _fastModelConfig,
870
872
  ...extraParams
871
873
  } = modelConfig;
@@ -873,6 +875,7 @@ export async function compactMessages(
873
875
  void _fastModel;
874
876
  void _maxTokens;
875
877
  void _permissionMode;
878
+ void _capabilities;
876
879
 
877
880
  // When a fast model override is provided, use the fast model's hyperparams
878
881
  // (if configured); otherwise fall back to the agent model's extraParams.
@@ -989,6 +992,7 @@ export async function processWebContent(
989
992
  fastModel: _fastModel,
990
993
  maxTokens: _maxTokens,
991
994
  permissionMode: _permissionMode,
995
+ capabilities: _capabilities,
992
996
  fastModelConfig: _fastModelConfig,
993
997
  ...extraParams
994
998
  } = modelConfig;
@@ -996,6 +1000,7 @@ export async function processWebContent(
996
1000
  void _fastModel;
997
1001
  void _maxTokens;
998
1002
  void _permissionMode;
1003
+ void _capabilities;
999
1004
 
1000
1005
  // When a fast model override is provided, use the fast model's hyperparams
1001
1006
  // (if configured); otherwise fall back to the agent model's extraParams.
@@ -1106,12 +1111,14 @@ export async function btw(options: BtwOptions): Promise<BtwResult> {
1106
1111
  fastModel: _fastModel,
1107
1112
  maxTokens: _maxTokens,
1108
1113
  permissionMode: _permissionMode,
1114
+ capabilities: _capabilities,
1109
1115
  ...extraParams
1110
1116
  } = modelConfig;
1111
1117
  void _model;
1112
1118
  void _fastModel;
1113
1119
  void _maxTokens;
1114
1120
  void _permissionMode;
1121
+ void _capabilities;
1115
1122
 
1116
1123
  const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
1117
1124
  temperature: 0.1,
@@ -47,6 +47,8 @@ import {
47
47
  mergeRemoteSettings,
48
48
  } from "./remoteSettingsService.js";
49
49
  import { createAuthAwareFetch } from "./authService.js";
50
+ import { ensureWaveRuntimeFilesExcluded } from "../utils/gitUtils.js";
51
+ import { atomicWriteFile } from "../utils/atomicWrite.js";
50
52
 
51
53
  /**
52
54
  * Default ConfigurationService implementation
@@ -544,12 +546,14 @@ export class ConfigurationService {
544
546
  fastModel: _ffm,
545
547
  maxTokens: _fmt,
546
548
  permissionMode: _fpm,
549
+ capabilities: _fcap,
547
550
  ...fastHyperparams
548
551
  } = fastModelConfigSource;
549
552
  void _fm;
550
553
  void _ffm;
551
554
  void _fmt;
552
555
  void _fpm;
556
+ void _fcap;
553
557
  baseConfig.fastModelConfig = fastHyperparams;
554
558
  }
555
559
 
@@ -728,7 +732,7 @@ export class ConfigurationService {
728
732
  }
729
733
 
730
734
  config.model = model;
731
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
735
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
732
736
  }
733
737
 
734
738
  /**
@@ -791,6 +795,7 @@ export class ConfigurationService {
791
795
 
792
796
  // Ensure .wave directory exists
793
797
  const waveDir = path.join(workdir, ".wave");
798
+ ensureWaveRuntimeFilesExcluded(workdir);
794
799
  if (!existsSync(waveDir)) {
795
800
  await fs.mkdir(waveDir, { recursive: true });
796
801
  }
@@ -814,11 +819,7 @@ export class ConfigurationService {
814
819
 
815
820
  if (!config.permissions.allow.includes(rule)) {
816
821
  config.permissions.allow.push(rule);
817
- await fs.writeFile(
818
- localConfigPath,
819
- JSON.stringify(config, null, 2),
820
- "utf-8",
821
- );
822
+ await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
822
823
  }
823
824
  }
824
825
 
@@ -866,7 +867,7 @@ export class ConfigurationService {
866
867
 
867
868
  config.enabledPlugins[pluginId] = enabled;
868
869
 
869
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
870
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
870
871
  }
871
872
 
872
873
  /**
@@ -938,11 +939,7 @@ export class ConfigurationService {
938
939
  }
939
940
  fileConfig.marketplaces[name] = config;
940
941
 
941
- await fs.writeFile(
942
- configPath,
943
- JSON.stringify(fileConfig, null, 2),
944
- "utf-8",
945
- );
942
+ await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
946
943
  }
947
944
 
948
945
  /**
@@ -976,11 +973,7 @@ export class ConfigurationService {
976
973
 
977
974
  if (fileConfig.marketplaces && name in fileConfig.marketplaces) {
978
975
  delete fileConfig.marketplaces[name];
979
- await fs.writeFile(
980
- configPath,
981
- JSON.stringify(fileConfig, null, 2),
982
- "utf-8",
983
- );
976
+ await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
984
977
  }
985
978
  } catch {
986
979
  // Ignore errors for corrupted or non-existent files
@@ -1018,11 +1011,7 @@ export class ConfigurationService {
1018
1011
 
1019
1012
  if (config.enabledPlugins && pluginId in config.enabledPlugins) {
1020
1013
  delete config.enabledPlugins[pluginId];
1021
- await fs.writeFile(
1022
- configPath,
1023
- JSON.stringify(config, null, 2),
1024
- "utf-8",
1025
- );
1014
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
1026
1015
  }
1027
1016
  } catch {
1028
1017
  // Ignore errors for corrupted or non-existent files
@@ -1169,6 +1158,15 @@ export function loadWaveConfigFromFile(
1169
1158
 
1170
1159
  try {
1171
1160
  const content = readFileSync(filePath, "utf-8");
1161
+
1162
+ // Tolerate empty/whitespace-only files: a concurrent writer may have the
1163
+ // file truncated mid-write. Returning null lets callers skip this source
1164
+ // instead of crashing (consistent with the "file not found" path).
1165
+ if (content.trim() === "") {
1166
+ logger.warn(`Empty configuration file (likely mid-write): ${filePath}`);
1167
+ return null;
1168
+ }
1169
+
1172
1170
  const config = JSON.parse(content) as WaveConfiguration;
1173
1171
 
1174
1172
  return {
@@ -1187,10 +1185,14 @@ export function loadWaveConfigFromFile(
1187
1185
  };
1188
1186
  } catch (error) {
1189
1187
  if (error instanceof SyntaxError) {
1190
- throw new Error(`Invalid JSON syntax in ${filePath}: ${error.message}`);
1188
+ // Tolerate corrupt JSON: a concurrent writer may leave the file in a
1189
+ // partially-written state. Warn and return null so callers skip this
1190
+ // source instead of crashing.
1191
+ logger.warn(`Invalid JSON syntax in ${filePath}: ${error.message}`);
1192
+ return null;
1191
1193
  }
1192
1194
 
1193
- // Re-throw validation errors and other errors as-is
1195
+ // Re-throw other errors as-is
1194
1196
  throw error;
1195
1197
  }
1196
1198
  }
@@ -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();