wave-agent-sdk 1.2.0 → 1.3.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 (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -1,8 +1,9 @@
1
1
  import { UserMessageParams, type AgentToolBlockUpdateParams, type ToolBlockUpdateCallbackParams, type AddNotificationMessageParams } from "../utils/messageOperations.js";
2
- import type { Message, Usage } from "../types/index.js";
2
+ import type { Message, Usage, InvokedSkillRecord } from "../types/index.js";
3
3
  import { SessionData } from "../services/session.js";
4
4
  import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
5
5
  import type { MemoryRule } from "../types/memoryRule.js";
6
+ import { type NestedMemoryFile } from "../utils/nestedMemory.js";
6
7
  import { Container } from "../utils/container.js";
7
8
  export interface MessageManagerCallbacks {
8
9
  onSessionIdChange?: (sessionId: string) => void;
@@ -59,11 +60,21 @@ export declare class MessageManager {
59
60
  private savedMessageCount;
60
61
  private pendingFileReadTriggers;
61
62
  private loadedRuleIds;
63
+ private pendingNestedMemoryTriggers;
64
+ private loadedNestedMemoryPaths;
62
65
  private recentFileReads;
63
66
  private invokedSkills;
64
67
  private sessionType;
65
68
  private subagentType?;
66
69
  private _usages;
70
+ /**
71
+ * This session's user-set title, as last known to this process (set on
72
+ * rename, or recovered from the transcript on resume). It is the fallback
73
+ * `reAppendSessionMetadata` writes back at EOF once the `custom-title` entry
74
+ * has been pushed out of the listing's tail window — at which point the file
75
+ * itself can no longer answer "what was this session called".
76
+ */
77
+ private customTitle?;
67
78
  constructor(container: Container, options: MessageManagerOptions);
68
79
  private get memoryRuleManager();
69
80
  private get memoryService();
@@ -74,6 +85,13 @@ export declare class MessageManager {
74
85
  getDisplayMessages(): Message[];
75
86
  getUsages(): Usage[];
76
87
  getLatestTotalTokens(): number;
88
+ /**
89
+ * Re-point this manager at another working directory (in-session session
90
+ * switch). The encoded workdir and the transcript path are both derived from
91
+ * it, so they must move together with it: otherwise the next append would
92
+ * create a second copy of the same session file under the old project dir.
93
+ */
94
+ setWorkdir(workdir: string): void;
77
95
  getWorkdir(): string;
78
96
  /**
79
97
  * Process pending file read triggers: match against conditional rules,
@@ -111,6 +129,19 @@ export declare class MessageManager {
111
129
  * conditional memory rules before the next AI call.
112
130
  */
113
131
  triggerFileRead(filePath: string): void;
132
+ /**
133
+ * Record an absolute path whose read may pull in a nested memory file
134
+ * (`AGENTS.md` in one of its ancestor directories). Separate from
135
+ * `triggerFileRead`: rule matching needs workdir-relative paths, the nested
136
+ * walk needs absolute ones.
137
+ */
138
+ triggerNestedMemory(filePath: string): void;
139
+ /**
140
+ * Resolve nested memory files for the paths read since the last call,
141
+ * outermost first. Clears the pending set; dedup is session-scoped via
142
+ * `loadedNestedMemoryPaths`.
143
+ */
144
+ collectNestedMemoryFiles(): Promise<NestedMemoryFile[]>;
114
145
  /**
115
146
  * Check if a file has been read (for read-before-edit enforcement).
116
147
  * Uses recentFileReads Map populated from Read tool execution.
@@ -124,6 +155,16 @@ export declare class MessageManager {
124
155
  */
125
156
  saveSession(): Promise<void>;
126
157
  setlatestTotalTokens(latestTotalTokens: number): void;
158
+ getCustomTitle(): string | undefined;
159
+ setCustomTitle(customTitle: string | undefined): void;
160
+ /**
161
+ * Re-append the session's metadata entries at EOF, so they stay inside the
162
+ * tail window the session list reads. Called at the sparse points where the
163
+ * transcript is about to be read back by another process (exit, compaction,
164
+ * resume, rewind); doing it per save would cost a tail read plus a line on
165
+ * every message.
166
+ */
167
+ reAppendSessionMetadata(): Promise<void>;
127
168
  /**
128
169
  * Clear messages
129
170
  */
@@ -214,7 +255,9 @@ export declare class MessageManager {
214
255
  */
215
256
  truncateHistory(index: number, reversionManager?: import("./reversionManager.js").ReversionManager): Promise<void>;
216
257
  /**
217
- * Rewrite the session file with the current messages.
258
+ * Drop everything past the first `keepMessageCount` messages from the
259
+ * session file. The rewrite is a filter over the file's own lines, so the
260
+ * metadata header, the user-set title and any other reserved entry survive.
218
261
  */
219
262
  private rewriteSessionFile;
220
263
  /**
@@ -242,11 +285,11 @@ export declare class MessageManager {
242
285
  */
243
286
  private extractSkillInvocationsFromMessage;
244
287
  /**
245
- * Get recently invoked skill names, sorted by timestamp (newest first).
288
+ * Get recently invoked skills, sorted by timestamp (newest first).
246
289
  * @param maxSkills - Maximum number of skills to return
247
- * @returns Array of skill names sorted by recency
290
+ * @returns Invoked skill records sorted by recency
248
291
  */
249
- getInvokedSkillNames(maxSkills?: number): string[];
292
+ getInvokedSkills(maxSkills?: number): InvokedSkillRecord[];
250
293
  /**
251
294
  * Clear all invoked skills (e.g., after compaction).
252
295
  */
@@ -1,9 +1,12 @@
1
1
  import { addAssistantMessageToMessages, updateToolBlockInMessage, addErrorBlockToMessage, addUserMessageToMessages, updateUserMessageInMessages, removeLastUserMessage, addToolBlockToMessageInMessages, addNotificationMessageToMessages, generateMessageId, sliceFromLastCompact, } from "../utils/messageOperations.js";
2
2
  import { getLastApiRounds } from "../utils/groupMessagesByApiRound.js";
3
- import { join, isAbsolute, relative } from "path";
4
- import { appendMessages, createSession, generateSessionId, SESSION_DIR, } from "../services/session.js";
3
+ import { join, isAbsolute, relative, resolve } from "path";
4
+ import { appendMessages, createSession, generateSessionId,
5
+ // Aliased: this class exposes a `reAppendSessionMetadata` method of its own.
6
+ reAppendSessionMetadata as reAppendSessionMetadataToFile, truncateSession, SESSION_DIR, } from "../services/session.js";
5
7
  import { pathEncoder } from "../utils/pathEncoder.js";
6
8
  import { estimateTokens } from "../utils/tokenEstimate.js";
9
+ import { findNestedMemoryFiles, } from "../utils/nestedMemory.js";
7
10
  import { READ_TOOL_NAME } from "../constants/tools.js";
8
11
  // Cap for recentFileReads: content is only consumed by getRecentFileReads
9
12
  // (top 5 for post-compact restoration), so bound both the entry count and the
@@ -25,8 +28,10 @@ export class MessageManager {
25
28
  this.displayMessages = [];
26
29
  this.pendingFileReadTriggers = new Set(); // File paths read via Read tool, awaiting rule matching
27
30
  this.loadedRuleIds = new Set(); // IDs of conditional rules already injected as meta messages
31
+ this.pendingNestedMemoryTriggers = new Set(); // Absolute paths read since the last nested-memory scan
32
+ this.loadedNestedMemoryPaths = new Set(); // Nested memory files already injected (session-scoped, never evicted)
28
33
  this.recentFileReads = new Map(); // Track file read contents
29
- this.invokedSkills = new Map(); // Track invoked skill names
34
+ this.invokedSkills = new Map(); // Track invoked skills
30
35
  this._usages = [];
31
36
  this.sessionId = generateSessionId();
32
37
  this.messages = [];
@@ -72,6 +77,20 @@ export class MessageManager {
72
77
  getLatestTotalTokens() {
73
78
  return this.latestTotalTokens;
74
79
  }
80
+ /**
81
+ * Re-point this manager at another working directory (in-session session
82
+ * switch). The encoded workdir and the transcript path are both derived from
83
+ * it, so they must move together with it: otherwise the next append would
84
+ * create a second copy of the same session file under the old project dir.
85
+ */
86
+ setWorkdir(workdir) {
87
+ if (this.workdir === workdir) {
88
+ return;
89
+ }
90
+ this.workdir = workdir;
91
+ this.encodedWorkdir = pathEncoder.encodeSync(workdir);
92
+ this.transcriptPath = this.computeTranscriptPath();
93
+ }
75
94
  getWorkdir() {
76
95
  return this.workdir;
77
96
  }
@@ -174,6 +193,31 @@ export class MessageManager {
174
193
  : filePath;
175
194
  this.pendingFileReadTriggers.add(normalizedPath);
176
195
  }
196
+ /**
197
+ * Record an absolute path whose read may pull in a nested memory file
198
+ * (`AGENTS.md` in one of its ancestor directories). Separate from
199
+ * `triggerFileRead`: rule matching needs workdir-relative paths, the nested
200
+ * walk needs absolute ones.
201
+ */
202
+ triggerNestedMemory(filePath) {
203
+ this.pendingNestedMemoryTriggers.add(isAbsolute(filePath) ? filePath : resolve(this.workdir, filePath));
204
+ }
205
+ /**
206
+ * Resolve nested memory files for the paths read since the last call,
207
+ * outermost first. Clears the pending set; dedup is session-scoped via
208
+ * `loadedNestedMemoryPaths`.
209
+ */
210
+ async collectNestedMemoryFiles() {
211
+ if (this.pendingNestedMemoryTriggers.size === 0)
212
+ return [];
213
+ const filePaths = Array.from(this.pendingNestedMemoryTriggers);
214
+ this.pendingNestedMemoryTriggers.clear();
215
+ const memories = [];
216
+ for (const filePath of filePaths) {
217
+ memories.push(...(await findNestedMemoryFiles(filePath, this.workdir, this.loadedNestedMemoryPaths)));
218
+ }
219
+ return memories;
220
+ }
177
221
  /**
178
222
  * Check if a file has been read (for read-before-edit enforcement).
179
223
  * Uses recentFileReads Map populated from Read tool execution.
@@ -268,6 +312,27 @@ export class MessageManager {
268
312
  this.callbacks.onLatestTotalTokensChange?.(latestTotalTokens);
269
313
  }
270
314
  }
315
+ getCustomTitle() {
316
+ return this.customTitle;
317
+ }
318
+ setCustomTitle(customTitle) {
319
+ this.customTitle = customTitle;
320
+ }
321
+ /**
322
+ * Re-append the session's metadata entries at EOF, so they stay inside the
323
+ * tail window the session list reads. Called at the sparse points where the
324
+ * transcript is about to be read back by another process (exit, compaction,
325
+ * resume, rewind); doing it per save would cost a tail read plus a line on
326
+ * every message.
327
+ */
328
+ async reAppendSessionMetadata() {
329
+ try {
330
+ await reAppendSessionMetadataToFile(this.sessionId, this.workdir, this.customTitle, this.sessionType);
331
+ }
332
+ catch (error) {
333
+ logger?.error("Failed to re-append session metadata:", error);
334
+ }
335
+ }
271
336
  /**
272
337
  * Clear messages
273
338
  */
@@ -277,6 +342,9 @@ export class MessageManager {
277
342
  this.setSessionId(newSessionId);
278
343
  this.setlatestTotalTokens(0);
279
344
  this.savedMessageCount = 0; // Reset saved message count
345
+ // The title belongs to the session that was just abandoned: the new
346
+ // session id starts untitled until the user renames it.
347
+ this.customTitle = undefined;
280
348
  }
281
349
  /**
282
350
  * Trigger the rewind UI callback
@@ -295,6 +363,10 @@ export class MessageManager {
295
363
  });
296
364
  this.displayMessages = [...sessionData.messages];
297
365
  this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
366
+ // Recovered by the whole-file scan on load: from here on it is the
367
+ // in-memory fallback that survives the title entry sliding out of the
368
+ // tail window.
369
+ this.customTitle = sessionData.metadata.customTitle;
298
370
  // Rebuild loadedRuleIds from persisted meta messages (session restore)
299
371
  this.rebuildLoadedRuleIds();
300
372
  // Set saved message count to the number of loaded messages since they're already saved
@@ -448,6 +520,9 @@ export class MessageManager {
448
520
  // message id appearing exactly once (dedup-skipped, aligned with CC).
449
521
  const { appendMessages } = await import("../services/session.js");
450
522
  await appendMessages(this.sessionId, [compactMessage], this.workdir, this.sessionType);
523
+ // Compaction leaves the transcript readable by the next process that
524
+ // lists sessions, so this is a flush point for the metadata entries.
525
+ await this.reAppendSessionMetadata();
451
526
  // Update in-memory state: the API context folds to [compact, ...last
452
527
  // rounds], while the UI display stream keeps the full history and appends
453
528
  // the compact block at the end.
@@ -459,6 +534,11 @@ export class MessageManager {
459
534
  // Clear and rebuild loaded rule IDs from remaining meta messages
460
535
  this.clearLoadedRuleIds();
461
536
  this.rebuildLoadedRuleIds();
537
+ // Nested memory has no marker to rebuild from, so the dedup set is dropped
538
+ // outright: compaction may have summarized the injected text away, and
539
+ // re-injecting it on the next read of that subtree beats never showing it
540
+ // again in this session.
541
+ this.loadedNestedMemoryPaths.clear();
462
542
  // Trigger compaction callback
463
543
  this.callbacks.onCompactBlockAdded?.(compactedContent);
464
544
  }
@@ -737,8 +817,16 @@ export class MessageManager {
737
817
  if (reversionManager && messageIdsToRemove.length > 0) {
738
818
  await reversionManager.revertTo(messageIdsToRemove, messages);
739
819
  }
740
- // Rewrite file with truncated messages (full history kept on disk)
741
- await this.rewriteSessionFile(newMessages);
820
+ // Keep the first `newMessages.length` messages on disk. Note the count is
821
+ // the message index, not a file operation: the rewrite walks the file's own
822
+ // lines and carries reserved entries (metadata header, custom-title) over.
823
+ await this.rewriteSessionFile(newMessages.length);
824
+ // The rewrite drops messages but keeps the reserved entries where they sit,
825
+ // which is not enough for the title: the session list reads only the file's
826
+ // tail window, and a title written early can end up outside it (the
827
+ // rewrite keeps the *first* messages, so the survivors after it are exactly
828
+ // what pushes it away). Re-append it at EOF like the other flush points do.
829
+ await this.reAppendSessionMetadata();
742
830
  // The UI keeps the full truncated thread; the agent context folds at the
743
831
  // last compact boundary so the agent only sees messages from the latest
744
832
  // compact summary forward — matching the compact and resume behaviors.
@@ -751,18 +839,13 @@ export class MessageManager {
751
839
  this.savedMessageCount = this.messages.length;
752
840
  }
753
841
  /**
754
- * Rewrite the session file with the current messages.
842
+ * Drop everything past the first `keepMessageCount` messages from the
843
+ * session file. The rewrite is a filter over the file's own lines, so the
844
+ * metadata header, the user-set title and any other reserved entry survive.
755
845
  */
756
- async rewriteSessionFile(messages) {
846
+ async rewriteSessionFile(keepMessageCount) {
757
847
  try {
758
- const { writeFile } = await import("fs/promises");
759
- const content = messages
760
- .map((m) => {
761
- const { timestamp, ...rest } = m;
762
- return JSON.stringify({ timestamp, ...rest });
763
- })
764
- .join("\n") + (messages.length > 0 ? "\n" : "");
765
- await writeFile(this.transcriptPath, content, "utf8");
848
+ await truncateSession(this.sessionId, keepMessageCount, this.workdir, this.sessionType);
766
849
  }
767
850
  catch (error) {
768
851
  logger?.error("Failed to rewrite session file:", error);
@@ -859,6 +942,10 @@ export class MessageManager {
859
942
  this.invokedSkills.set(skillName, {
860
943
  skillName,
861
944
  timestamp: Date.now(),
945
+ // 模型当时实际看到的那份正文(已替换 ${WAVE_SKILL_DIR} 等占位符、
946
+ // 首行带 `Base directory for this skill:`)——压缩后回灌直接复用它,
947
+ // 不能重新读文件原文(spec core/message-compact 场景 4)。
948
+ content: block.result,
862
949
  });
863
950
  }
864
951
  }
@@ -869,15 +956,14 @@ export class MessageManager {
869
956
  }
870
957
  }
871
958
  /**
872
- * Get recently invoked skill names, sorted by timestamp (newest first).
959
+ * Get recently invoked skills, sorted by timestamp (newest first).
873
960
  * @param maxSkills - Maximum number of skills to return
874
- * @returns Array of skill names sorted by recency
961
+ * @returns Invoked skill records sorted by recency
875
962
  */
876
- getInvokedSkillNames(maxSkills = 10) {
877
- const sorted = Array.from(this.invokedSkills.entries())
878
- .sort(([, a], [, b]) => b.timestamp - a.timestamp)
963
+ getInvokedSkills(maxSkills = 10) {
964
+ return Array.from(this.invokedSkills.values())
965
+ .sort((a, b) => b.timestamp - a.timestamp)
879
966
  .slice(0, maxSkills);
880
- return sorted.map(([, { skillName }]) => skillName);
881
967
  }
882
968
  /**
883
969
  * Clear all invoked skills (e.g., after compaction).
@@ -18,6 +18,12 @@ export declare function resolveAdditionalDirectory(dir: string, workdir: string
18
18
  export interface PermissionManagerOptions {
19
19
  /** Configured permission mode from settings */
20
20
  configuredPermissionMode?: PermissionMode;
21
+ /**
22
+ * Session-level bypass authorization (see `captureBypassAuthorization`).
23
+ * Passed explicitly by subagents so they inherit the parent session's value
24
+ * instead of re-deriving it from their own (mid-session) mode.
25
+ */
26
+ bypassAuthorization?: boolean;
21
27
  /** Allowed rules from settings */
22
28
  allowedRules?: string[];
23
29
  /** Denied rules from settings */
@@ -40,6 +46,15 @@ export interface PermissionManagerOptions {
40
46
  export declare class PermissionManager {
41
47
  private container;
42
48
  private configuredPermissionMode?;
49
+ /**
50
+ * Session-level bypass authorization: the permission mode in effect when the
51
+ * session was created was `bypassPermissions`. Frozen at session start, so a
52
+ * later mode switch (or config reload) can never grant it. Its only effect is
53
+ * that plan mode skips permission checks instead of re-asking — mirroring
54
+ * Claude Code's `isBypassPermissionsModeAvailable`. `undefined` = not
55
+ * captured yet (subagents get an explicit value instead).
56
+ */
57
+ private bypassAuthorization?;
43
58
  private allowedRules;
44
59
  private deniedRules;
45
60
  private instanceAllowedRules;
@@ -73,6 +88,17 @@ export declare class PermissionManager {
73
88
  * Get the configured default mode
74
89
  */
75
90
  getConfiguredPermissionMode(): PermissionMode | undefined;
91
+ /**
92
+ * Freeze the session-level bypass authorization from the permission mode in
93
+ * effect when the session was created (CLI mode > configured default mode >
94
+ * `default`). No-op once captured, so switching to `bypassPermissions`
95
+ * mid-session does not grant it.
96
+ */
97
+ captureBypassAuthorization(): void;
98
+ /**
99
+ * Whether this session keeps bypass semantics while in plan mode
100
+ */
101
+ getBypassAuthorization(): boolean;
76
102
  /**
77
103
  * Get all currently allowed rules (user-defined)
78
104
  */
@@ -94,6 +120,13 @@ export declare class PermissionManager {
94
120
  * Unlike addPermissionRule, this does NOT persist to settings.local.json.
95
121
  */
96
122
  addInstanceAllowedRule(rule: string): void;
123
+ /**
124
+ * Remove an instance-level allowed rule added via addInstanceAllowedRule.
125
+ * Used when a builtin plugin that contributed a helper-script grant is
126
+ * unloaded, so the grant does not outlive the plugin.
127
+ * @returns true if the rule was present
128
+ */
129
+ removeInstanceAllowedRule(rule: string): boolean;
97
130
  /**
98
131
  * Get all additional directories
99
132
  */
@@ -143,6 +176,13 @@ export declare class PermissionManager {
143
176
  * Add a system-level additional directory that is persistent across configuration reloads
144
177
  */
145
178
  addSystemAdditionalDirectory(directory: string): void;
179
+ /**
180
+ * Remove a system-level additional directory (idempotent). Used when a
181
+ * settings.json change revokes a privilege that was granted at construction
182
+ * time — e.g. turning auto-memory off removes its directory from the safe
183
+ * zone (core/agent-config.md scenario 6).
184
+ */
185
+ removeSystemAdditionalDirectory(directory: string): void;
146
186
  /**
147
187
  * Set the current plan file path
148
188
  */
@@ -8,12 +8,12 @@
8
8
  import path from "node:path";
9
9
  import os from "node:os";
10
10
  import { minimatch } from "minimatch";
11
- import { RESTRICTED_TOOLS } from "../types/permissions.js";
11
+ import { RESTRICTED_TOOLS, USER_INTERACTION_REQUIRED_TOOLS, } from "../types/permissions.js";
12
12
  import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, stripGitScopePrefix, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, PATH_COMMANDS, extractPathArgs, } from "../utils/bashParser.js";
13
13
  import { parseBashStructure, } from "../utils/bashStructure/index.js";
14
14
  import { isPathInside } from "../utils/pathSafety.js";
15
15
  import { toWindowsPath } from "../utils/path.js";
16
- import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "../constants/tools.js";
16
+ import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, } from "../constants/tools.js";
17
17
  const DEFAULT_ALLOWED_RULES = [
18
18
  "Bash(git status*)",
19
19
  "Bash(git diff*)",
@@ -87,6 +87,7 @@ export class PermissionManager {
87
87
  this.hasExitedPlanMode = false;
88
88
  this.needsPlanModeExitAttachment = false;
89
89
  this.configuredPermissionMode = options.configuredPermissionMode;
90
+ this.bypassAuthorization = options.bypassAuthorization;
90
91
  this.allowedRules = options.allowedRules || [];
91
92
  this.deniedRules = options.deniedRules || [];
92
93
  this.instanceAllowedRules = options.instanceAllowedRules || [];
@@ -135,6 +136,28 @@ export class PermissionManager {
135
136
  getConfiguredPermissionMode() {
136
137
  return this.configuredPermissionMode;
137
138
  }
139
+ /**
140
+ * Freeze the session-level bypass authorization from the permission mode in
141
+ * effect when the session was created (CLI mode > configured default mode >
142
+ * `default`). No-op once captured, so switching to `bypassPermissions`
143
+ * mid-session does not grant it.
144
+ */
145
+ captureBypassAuthorization() {
146
+ if (this.bypassAuthorization !== undefined) {
147
+ return;
148
+ }
149
+ const sessionMode = this.container.has("PermissionMode")
150
+ ? this.container.get("PermissionMode")
151
+ : undefined;
152
+ this.bypassAuthorization =
153
+ this.getCurrentEffectiveMode(sessionMode) === "bypassPermissions";
154
+ }
155
+ /**
156
+ * Whether this session keeps bypass semantics while in plan mode
157
+ */
158
+ getBypassAuthorization() {
159
+ return this.bypassAuthorization === true;
160
+ }
138
161
  /**
139
162
  * Get all currently allowed rules (user-defined)
140
163
  */
@@ -168,6 +191,20 @@ export class PermissionManager {
168
191
  this.instanceAllowedRules.push(rule);
169
192
  }
170
193
  }
194
+ /**
195
+ * Remove an instance-level allowed rule added via addInstanceAllowedRule.
196
+ * Used when a builtin plugin that contributed a helper-script grant is
197
+ * unloaded, so the grant does not outlive the plugin.
198
+ * @returns true if the rule was present
199
+ */
200
+ removeInstanceAllowedRule(rule) {
201
+ const index = this.instanceAllowedRules.indexOf(rule);
202
+ if (index === -1) {
203
+ return false;
204
+ }
205
+ this.instanceAllowedRules.splice(index, 1);
206
+ return true;
207
+ }
171
208
  /**
172
209
  * Get all additional directories
173
210
  */
@@ -258,6 +295,16 @@ export class PermissionManager {
258
295
  this.systemAdditionalDirectories.push(resolvedPath);
259
296
  }
260
297
  }
298
+ /**
299
+ * Remove a system-level additional directory (idempotent). Used when a
300
+ * settings.json change revokes a privilege that was granted at construction
301
+ * time — e.g. turning auto-memory off removes its directory from the safe
302
+ * zone (core/agent-config.md scenario 6).
303
+ */
304
+ removeSystemAdditionalDirectory(directory) {
305
+ const resolvedPath = resolveAdditionalDirectory(directory, this.workdir);
306
+ this.systemAdditionalDirectories = this.systemAdditionalDirectories.filter((dir) => dir !== resolvedPath);
307
+ }
261
308
  /**
262
309
  * Set the current plan file path
263
310
  */
@@ -434,12 +481,17 @@ export class PermissionManager {
434
481
  }
435
482
  }
436
483
  }
437
- // If bypassPermissions mode, always allow
438
- // Exception: tools that require user interaction (e.g. AskUserQuestion)
439
- // must still prompt the user, matching Claude Code's requiresUserInteraction behavior.
484
+ // If bypassPermissions mode — or plan mode in a session that was authorized
485
+ // to bypass at creation time — always allow.
486
+ // Exception: tools that require user interaction (AskUserQuestion and
487
+ // ExitPlanMode) must still prompt, matching Claude Code's
488
+ // requiresUserInteraction behavior (its step 1e returns the tool's ask
489
+ // result before the bypass step).
440
490
  // Worktree safety check above runs unconditionally, so bypass never skips it.
441
- if (context.permissionMode === "bypassPermissions") {
442
- const requiresUserInteraction = context.toolName === ASK_USER_QUESTION_TOOL_NAME;
491
+ const bypassesPermissionChecks = context.permissionMode === "bypassPermissions" ||
492
+ (context.permissionMode === "plan" && this.getBypassAuthorization());
493
+ if (bypassesPermissionChecks) {
494
+ const requiresUserInteraction = USER_INTERACTION_REQUIRED_TOOLS.includes(context.toolName);
443
495
  if (!requiresUserInteraction) {
444
496
  return { behavior: "allow" };
445
497
  }
@@ -615,9 +667,12 @@ export class PermissionManager {
615
667
  const targetPath = toolInput?.file_path;
616
668
  const workdir = toolInput?.workdir;
617
669
  if (targetPath) {
618
- const { isInside } = this.isInsideSafeZone(targetPath, workdir);
670
+ const { isInside, resolvedPath } = this.isInsideSafeZone(targetPath, workdir);
619
671
  if (!isInside) {
620
672
  context.hidePersistentOption = true;
673
+ // Surface the target's directory so the confirmation UI can offer
674
+ // adding it to the session Safe Zone.
675
+ context.outsideSafeZoneDirectory = path.dirname(resolvedPath);
621
676
  }
622
677
  }
623
678
  }
@@ -1,4 +1,4 @@
1
- import { Plugin, PluginConfig } from "../types/index.js";
1
+ import { Plugin, PluginConfig, PluginLoadFailure, PluginReloadResult } from "../types/index.js";
2
2
  import { Container } from "../utils/container.js";
3
3
  export interface PluginManagerOptions {
4
4
  workdir: string;
@@ -17,6 +17,17 @@ export declare class PluginManager {
17
17
  private plugins;
18
18
  private workdir;
19
19
  private enabledPlugins;
20
+ /** Explicit configs from the last load, replayed by reloadAllPlugins(). */
21
+ private lastLoadConfigs;
22
+ /** Failures collected by the current loadPlugins() run. */
23
+ private loadFailures;
24
+ /**
25
+ * Serializes plugin (re)loads. A managed-settings sync (or an early
26
+ * `/reload-plugins`) can ask for a reload while the startup load is still
27
+ * running; interleaving them would leave half-unloaded capability registries.
28
+ */
29
+ private loadChain;
30
+ private serialize;
20
31
  constructor(container: Container, options: PluginManagerOptions);
21
32
  private get permissionManager();
22
33
  private get skillManager();
@@ -41,8 +52,41 @@ export declare class PluginManager {
41
52
  /**
42
53
  * Load plugins from configuration
43
54
  * @param configs Array of plugin configurations
55
+ * @returns the plugins that failed to load
56
+ */
57
+ loadPlugins(configs: PluginConfig[]): Promise<PluginLoadFailure[]>;
58
+ /**
59
+ * The actual load. Reached through [loadPlugins] (which serializes concurrent
60
+ * requests) and directly from [reloadAllPlugins], which already holds the slot.
61
+ */
62
+ private doLoadPlugins;
63
+ /**
64
+ * Unload a single plugin: drop everything it contributed from the six
65
+ * capability registries, then forget it. Other plugins and the running
66
+ * session are untouched.
67
+ * @returns true if the plugin was loaded
68
+ */
69
+ unloadPlugin(name: string): Promise<boolean>;
70
+ /**
71
+ * Re-read every plugin in place — the `/reload-plugins` command.
72
+ *
73
+ * Unloads everything currently loaded, re-reads `enabledPlugins` from the
74
+ * configuration chain (so a settings edit is honored), then replays the
75
+ * original load order (explicit configs → installed → builtin). The session,
76
+ * transcript and sessionId are untouched; only the capability registries are
77
+ * swapped, which is what makes this an in-place reload instead of a rebuild.
78
+ *
79
+ * Unload must happen before any caller-side mutation of the plugin cache dir:
80
+ * hooks, MCP, LSP, skills and agents all bake absolute plugin paths at
81
+ * registration time, so an entry left pointing at a deleted directory is
82
+ * worse than a merely stale one.
83
+ */
84
+ reloadAllPlugins(): Promise<PluginReloadResult>;
85
+ /**
86
+ * Re-read enabled plugins from the merged configuration chain, falling back
87
+ * to the last known value when the service is unavailable.
44
88
  */
45
- loadPlugins(configs: PluginConfig[]): Promise<void>;
89
+ private refreshEnabledPlugins;
46
90
  /**
47
91
  * Load built-in plugins bundled with the SDK (e.g. sdd).
48
92
  * Opt-in via `enabledPlugins`, keyed `<name>@builtin` (default off, consistent