wave-agent-sdk 1.0.5 → 1.0.7

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 (202) hide show
  1. package/dist/agent.d.ts +34 -1
  2. package/dist/agent.js +49 -4
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +1 -1
  5. package/dist/managers/aiManager.d.ts +36 -0
  6. package/dist/managers/aiManager.js +240 -11
  7. package/dist/managers/messageManager.d.ts +2 -4
  8. package/dist/managers/messageManager.js +14 -7
  9. package/dist/managers/slashCommandManager.js +28 -0
  10. package/dist/managers/subagentManager.d.ts +0 -2
  11. package/dist/services/aiService.d.ts +2 -3
  12. package/dist/services/aiService.js +4 -18
  13. package/dist/services/configurationService.d.ts +7 -0
  14. package/dist/services/configurationService.js +10 -0
  15. package/dist/telemetry/instrumentation.d.ts +1 -1
  16. package/dist/telemetry/instrumentation.js +57 -28
  17. package/dist/tools/bashTool.js +16 -6
  18. package/dist/tools/grepTool.js +11 -3
  19. package/dist/types/core.d.ts +1 -34
  20. package/dist/types/index.d.ts +4 -0
  21. package/dist/types/messaging.d.ts +2 -0
  22. package/dist/utils/cacheControlUtils.d.ts +5 -27
  23. package/dist/utils/cacheControlUtils.js +12 -73
  24. package/dist/utils/containerSetup.js +0 -3
  25. package/dist/utils/messageOperations.d.ts +2 -1
  26. package/dist/utils/messageOperations.js +2 -1
  27. package/dist/utils/notificationXml.js +5 -0
  28. package/dist/utils/ripgrep.d.ts +4 -4
  29. package/dist/utils/ripgrep.js +4 -27
  30. package/package.json +20 -8
  31. package/bin/rg +0 -79
  32. package/dist/managers/forkedAgentManager.d.ts +0 -50
  33. package/dist/managers/forkedAgentManager.js +0 -130
  34. package/scripts/install_ripgrep.js +0 -111
  35. package/scripts/postinstall.js +0 -38
  36. package/src/agent.ts +0 -1271
  37. package/src/constants/subagents.ts +0 -4
  38. package/src/constants/toolLimits.ts +0 -15
  39. package/src/constants/tools.ts +0 -24
  40. package/src/core/plugin.ts +0 -237
  41. package/src/core/session.ts +0 -9
  42. package/src/index.ts +0 -38
  43. package/src/managers/MemoryRuleManager.ts +0 -198
  44. package/src/managers/aiManager.ts +0 -2429
  45. package/src/managers/backgroundTaskManager.ts +0 -480
  46. package/src/managers/bangManager.ts +0 -119
  47. package/src/managers/cronManager.ts +0 -401
  48. package/src/managers/foregroundTaskManager.ts +0 -30
  49. package/src/managers/forkedAgentManager.ts +0 -216
  50. package/src/managers/hookManager.ts +0 -1089
  51. package/src/managers/liveConfigManager.ts +0 -445
  52. package/src/managers/lspManager.ts +0 -470
  53. package/src/managers/mcpManager.ts +0 -1010
  54. package/src/managers/messageManager.ts +0 -1184
  55. package/src/managers/messageQueue.ts +0 -157
  56. package/src/managers/permissionManager.ts +0 -1140
  57. package/src/managers/planManager.ts +0 -112
  58. package/src/managers/pluginManager.ts +0 -330
  59. package/src/managers/pluginScopeManager.ts +0 -124
  60. package/src/managers/reversionManager.ts +0 -178
  61. package/src/managers/skillManager.ts +0 -578
  62. package/src/managers/slashCommandManager.ts +0 -564
  63. package/src/managers/subagentManager.ts +0 -915
  64. package/src/managers/toolManager.ts +0 -489
  65. package/src/managers/workflowManager.ts +0 -493
  66. package/src/prompts/autoMemory.ts +0 -33
  67. package/src/prompts/autoMemoryExtraction.ts +0 -146
  68. package/src/prompts/index.ts +0 -506
  69. package/src/prompts/planModeReminders.ts +0 -126
  70. package/src/services/GitService.ts +0 -131
  71. package/src/services/MarketplaceService.ts +0 -1052
  72. package/src/services/MemoryRuleService.ts +0 -71
  73. package/src/services/aiService.ts +0 -919
  74. package/src/services/authService.ts +0 -565
  75. package/src/services/autoMemoryService.ts +0 -280
  76. package/src/services/configurationService.ts +0 -1520
  77. package/src/services/fileWatcher.ts +0 -319
  78. package/src/services/hook.ts +0 -348
  79. package/src/services/initializationService.ts +0 -357
  80. package/src/services/interactionService.ts +0 -229
  81. package/src/services/jsonlHandler.ts +0 -295
  82. package/src/services/memory.ts +0 -234
  83. package/src/services/pluginLoader.ts +0 -252
  84. package/src/services/remoteSettingsService.ts +0 -366
  85. package/src/services/reversionService.ts +0 -122
  86. package/src/services/session.ts +0 -917
  87. package/src/services/taskManager.ts +0 -321
  88. package/src/telemetry/events.ts +0 -65
  89. package/src/telemetry/instrumentation.ts +0 -499
  90. package/src/telemetry/sessionTracing.ts +0 -348
  91. package/src/tools/agentTool.ts +0 -322
  92. package/src/tools/askUserQuestion.ts +0 -160
  93. package/src/tools/bashTool.ts +0 -621
  94. package/src/tools/buildTool.ts +0 -61
  95. package/src/tools/cronCreateTool.ts +0 -161
  96. package/src/tools/cronDeleteTool.ts +0 -51
  97. package/src/tools/cronListTool.ts +0 -47
  98. package/src/tools/editTool.ts +0 -313
  99. package/src/tools/enterPlanMode.ts +0 -124
  100. package/src/tools/enterWorktreeTool.ts +0 -190
  101. package/src/tools/exitPlanMode.ts +0 -139
  102. package/src/tools/exitWorktreeTool.ts +0 -236
  103. package/src/tools/globTool.ts +0 -167
  104. package/src/tools/grepTool.ts +0 -399
  105. package/src/tools/lspTool.ts +0 -883
  106. package/src/tools/readTool.ts +0 -426
  107. package/src/tools/skillTool.ts +0 -248
  108. package/src/tools/taskManagementTools.ts +0 -574
  109. package/src/tools/taskStopTool.ts +0 -78
  110. package/src/tools/types.ts +0 -137
  111. package/src/tools/webFetchTool.ts +0 -373
  112. package/src/tools/workflowTool.ts +0 -205
  113. package/src/tools/writeTool.ts +0 -267
  114. package/src/types/agent.ts +0 -128
  115. package/src/types/auth.ts +0 -19
  116. package/src/types/commands.ts +0 -30
  117. package/src/types/config.ts +0 -43
  118. package/src/types/configuration.ts +0 -168
  119. package/src/types/core.ts +0 -105
  120. package/src/types/cron.ts +0 -13
  121. package/src/types/environment.ts +0 -106
  122. package/src/types/fileSearch.ts +0 -4
  123. package/src/types/history.ts +0 -7
  124. package/src/types/hooks.ts +0 -256
  125. package/src/types/index.ts +0 -41
  126. package/src/types/lsp.ts +0 -98
  127. package/src/types/marketplace.ts +0 -71
  128. package/src/types/mcp.ts +0 -43
  129. package/src/types/memoryRule.ts +0 -31
  130. package/src/types/messaging.ts +0 -114
  131. package/src/types/permissions.ts +0 -80
  132. package/src/types/plugins.ts +0 -39
  133. package/src/types/processes.ts +0 -99
  134. package/src/types/reversion.ts +0 -29
  135. package/src/types/session.ts +0 -12
  136. package/src/types/skills.ts +0 -108
  137. package/src/types/tasks.ts +0 -13
  138. package/src/types/telemetry.ts +0 -98
  139. package/src/types/tools.ts +0 -44
  140. package/src/types/workflow.ts +0 -6
  141. package/src/utils/abortUtils.ts +0 -118
  142. package/src/utils/atomicWrite.ts +0 -61
  143. package/src/utils/bashParser.ts +0 -899
  144. package/src/utils/cacheControlUtils.ts +0 -426
  145. package/src/utils/commandArgumentParser.ts +0 -161
  146. package/src/utils/commandPathResolver.ts +0 -88
  147. package/src/utils/configPaths.ts +0 -220
  148. package/src/utils/configValidator.ts +0 -100
  149. package/src/utils/constants.ts +0 -39
  150. package/src/utils/container.ts +0 -92
  151. package/src/utils/containerSetup.ts +0 -371
  152. package/src/utils/convertMessagesForAPI.ts +0 -326
  153. package/src/utils/cronTasks.ts +0 -128
  154. package/src/utils/cronTasksLock.ts +0 -182
  155. package/src/utils/cronToHuman.ts +0 -99
  156. package/src/utils/customCommands.ts +0 -95
  157. package/src/utils/editUtils.ts +0 -20
  158. package/src/utils/fileFormat.ts +0 -40
  159. package/src/utils/fileSearch.ts +0 -151
  160. package/src/utils/fileUtils.ts +0 -223
  161. package/src/utils/gitUtils.ts +0 -315
  162. package/src/utils/globalLogger.ts +0 -128
  163. package/src/utils/groupMessagesByApiRound.ts +0 -121
  164. package/src/utils/hookMatcher.ts +0 -176
  165. package/src/utils/markdownParser.ts +0 -317
  166. package/src/utils/mcpUtils.ts +0 -138
  167. package/src/utils/messageOperations.ts +0 -659
  168. package/src/utils/modelCapabilities.ts +0 -30
  169. package/src/utils/nameGenerator.ts +0 -99
  170. package/src/utils/notificationXml.ts +0 -57
  171. package/src/utils/openaiClient.ts +0 -251
  172. package/src/utils/parseCronExpression.ts +0 -78
  173. package/src/utils/path.ts +0 -72
  174. package/src/utils/pathEncoder.ts +0 -400
  175. package/src/utils/pathSafety.ts +0 -40
  176. package/src/utils/promptHistory.ts +0 -168
  177. package/src/utils/ripgrep.ts +0 -35
  178. package/src/utils/shellResolver.ts +0 -190
  179. package/src/utils/skillParser.ts +0 -246
  180. package/src/utils/stringUtils.ts +0 -156
  181. package/src/utils/subagentParser.ts +0 -279
  182. package/src/utils/taskReminder.ts +0 -96
  183. package/src/utils/tokenCalculation.ts +0 -43
  184. package/src/utils/tokenEstimate.ts +0 -34
  185. package/src/utils/toolResultStorage.ts +0 -117
  186. package/src/utils/worktreeSession.ts +0 -26
  187. package/src/utils/worktreeUtils.ts +0 -786
  188. package/src/workflow/budgetTracker.ts +0 -34
  189. package/src/workflow/concurrencyLimiter.ts +0 -47
  190. package/src/workflow/journal.ts +0 -95
  191. package/src/workflow/progressReporter.ts +0 -141
  192. package/src/workflow/runState.ts +0 -65
  193. package/src/workflow/scriptRuntime.ts +0 -274
  194. package/src/workflow/structuredOutput.ts +0 -123
  195. package/src/workflow/types.ts +0 -101
  196. package/src/workflow/workflowApis.ts +0 -410
  197. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  198. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  199. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  200. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  201. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  202. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -1,1184 +0,0 @@
1
- import {
2
- addAssistantMessageToMessages,
3
- updateToolBlockInMessage,
4
- addErrorBlockToMessage,
5
- addUserMessageToMessages,
6
- updateUserMessageInMessages,
7
- addBangMessage,
8
- updateBangInMessage,
9
- completeBangInMessage,
10
- removeLastUserMessage,
11
- addToolBlockToMessageInMessages,
12
- addNotificationMessageToMessages,
13
- UserMessageParams,
14
- type AgentToolBlockUpdateParams,
15
- type ToolBlockUpdateCallbackParams,
16
- type AddNotificationMessageParams,
17
- generateMessageId,
18
- sliceFromLastCompact,
19
- } from "../utils/messageOperations.js";
20
- import type { Message, Usage, ToolBlock } from "../types/index.js";
21
- import { getLastApiRounds } from "../utils/groupMessagesByApiRound.js";
22
- import { join, isAbsolute, relative } from "path";
23
- import {
24
- appendMessages,
25
- createSession,
26
- generateSessionId,
27
- SessionData,
28
- SESSION_DIR,
29
- } from "../services/session.js";
30
- import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
31
- import type { MemoryRuleManager } from "./MemoryRuleManager.js";
32
- import type { MemoryRule } from "../types/memoryRule.js";
33
- import type { MemoryService } from "../services/memory.js";
34
- import { pathEncoder } from "../utils/pathEncoder.js";
35
- import { estimateTokens } from "../utils/tokenEstimate.js";
36
- import { READ_TOOL_NAME } from "../constants/tools.js";
37
-
38
- import { Container } from "../utils/container.js";
39
-
40
- export interface MessageManagerCallbacks {
41
- onSessionIdChange?: (sessionId: string) => void;
42
- onLatestTotalTokensChange?: (latestTotalTokens: number) => void;
43
- onUsagesChange?: (usages: Usage[]) => void;
44
- // Incremental callback
45
- onUserMessageAdded?: (params: UserMessageParams) => void;
46
- // MODIFIED: Remove arguments for separation of concerns
47
- onAssistantMessageAdded?: (messageId: string) => void;
48
- // NEW: Streaming content callback - FR-001: receives chunk and accumulated content
49
- onAssistantContentUpdated?: (params: {
50
- messageId: string;
51
- chunk: string;
52
- accumulated: string;
53
- stage: "streaming" | "end";
54
- }) => void;
55
- // NEW: Streaming reasoning callback
56
- onAssistantReasoningUpdated?: (params: {
57
- messageId: string;
58
- chunk: string;
59
- accumulated: string;
60
- stage: "streaming" | "end";
61
- }) => void;
62
- onToolBlockUpdated?: (params: ToolBlockUpdateCallbackParams) => void;
63
- onErrorBlockAdded?: (error: string) => void;
64
- onCompactBlockAdded?: (content: string) => void;
65
- onCompactionStateChange?: (isCompacting: boolean) => void;
66
- // Bang callback
67
- onAddBangMessage?: (command: string, messageId: string) => void;
68
- onUpdateBangMessage?: (
69
- command: string,
70
- output: string,
71
- messageId: string,
72
- ) => void;
73
- onCompleteBangMessage?: (
74
- command: string,
75
- exitCode: number,
76
- messageId: string,
77
- ) => void;
78
- onInfoBlockAdded?: (content: string) => void;
79
- // Rewind callbacks
80
- onShowRewind?: () => void;
81
- onFileHistoryBlockAdded?: (
82
- snapshots: import("../types/reversion.js").FileSnapshot[],
83
- ) => void;
84
- // Notification callback
85
- onNotificationMessageAdded?: (params: {
86
- taskId: string;
87
- taskType: "shell" | "agent" | "workflow";
88
- status: "completed" | "failed" | "killed" | "aborted";
89
- summary: string;
90
- }) => void;
91
- }
92
-
93
- import { logger } from "../utils/globalLogger.js";
94
-
95
- export interface MessageManagerOptions {
96
- callbacks: MessageManagerCallbacks;
97
- workdir: string;
98
- sessionType?: "main" | "subagent";
99
- subagentType?: string;
100
- }
101
-
102
- export class MessageManager {
103
- // Private state properties
104
- private sessionId: string;
105
- private messages: Message[];
106
- private latestTotalTokens: number;
107
- private workdir: string;
108
- private encodedWorkdir: string; // Cached encoded workdir
109
- private callbacks: MessageManagerCallbacks;
110
- private transcriptPath: string; // Cached transcript path
111
- private savedMessageCount: number; // Track how many messages have been saved to prevent duplication
112
- private pendingFileReadTriggers: Set<string> = new Set(); // File paths read via Read tool, awaiting rule matching
113
- private loadedRuleIds: Set<string> = new Set(); // IDs of conditional rules already injected as meta messages
114
- private recentFileReads: Map<string, { content: string; timestamp: number }> =
115
- new Map(); // Track file read contents
116
- private invokedSkills: Map<string, { skillName: string; timestamp: number }> =
117
- new Map(); // Track invoked skill names
118
- private sessionType: "main" | "subagent";
119
- private subagentType?: string;
120
- private _usages: Usage[] = [];
121
-
122
- constructor(
123
- private container: Container,
124
- options: MessageManagerOptions,
125
- ) {
126
- this.sessionId = generateSessionId();
127
- this.messages = [];
128
- this.latestTotalTokens = 0;
129
- this.workdir = options.workdir;
130
- this.encodedWorkdir = pathEncoder.encodeSync(this.workdir); // Cache encoded workdir
131
- this.callbacks = options.callbacks;
132
- this.savedMessageCount = 0; // Initialize saved message count tracker
133
- this.sessionType = options.sessionType || "main";
134
- this.subagentType = options.subagentType;
135
-
136
- // Compute and cache the transcript path
137
- this.transcriptPath = this.computeTranscriptPath();
138
- }
139
-
140
- private get memoryRuleManager(): MemoryRuleManager | undefined {
141
- return this.container.has("MemoryRuleManager")
142
- ? this.container.get<MemoryRuleManager>("MemoryRuleManager")
143
- : undefined;
144
- }
145
-
146
- private get memoryService(): MemoryService {
147
- const service = this.container.get<MemoryService>("MemoryService");
148
- if (!service) {
149
- throw new Error("MemoryService not found in container");
150
- }
151
- return service;
152
- }
153
-
154
- // Getter methods
155
- public getSessionId(): string {
156
- return this.sessionId;
157
- }
158
-
159
- public getRootSessionId(): string {
160
- return this.sessionId;
161
- }
162
-
163
- public getMessages(): Message[] {
164
- return [...this.messages];
165
- }
166
-
167
- public getUsages(): Usage[] {
168
- return [...this._usages];
169
- }
170
-
171
- public getLatestTotalTokens(): number {
172
- return this.latestTotalTokens;
173
- }
174
-
175
- public getWorkdir(): string {
176
- return this.workdir;
177
- }
178
-
179
- /**
180
- * Process pending file read triggers: match against conditional rules,
181
- * return rules not yet loaded (dedup via loadedRuleIds), mark them as loaded.
182
- * Clears triggers after processing.
183
- */
184
- public processTriggeredRules(): MemoryRule[] {
185
- if (!this.memoryRuleManager || this.pendingFileReadTriggers.size === 0) {
186
- this.pendingFileReadTriggers.clear();
187
- return [];
188
- }
189
- const filePaths = Array.from(this.pendingFileReadTriggers);
190
- this.pendingFileReadTriggers.clear();
191
-
192
- const { conditional } =
193
- this.memoryRuleManager.getActiveRulesSplit(filePaths);
194
- const newRules = conditional.filter(
195
- (rule) => !this.loadedRuleIds.has(rule.id),
196
- );
197
- for (const rule of newRules) {
198
- this.loadedRuleIds.add(rule.id);
199
- }
200
- return newRules;
201
- }
202
-
203
- public getSessionDir(): string {
204
- return SESSION_DIR;
205
- }
206
-
207
- public getTranscriptPath(): string {
208
- return this.transcriptPath;
209
- }
210
-
211
- /**
212
- * Get combined memory content (project memory + user memory + unconditional rules)
213
- * Note: conditional rules are handled separately via processTriggeredRules().
214
- */
215
- public async getCombinedMemory(): Promise<string> {
216
- let combined = await this.memoryService.getCombinedMemoryContent(
217
- this.workdir,
218
- );
219
-
220
- if (this.memoryRuleManager) {
221
- const { unconditional } = this.memoryRuleManager.getActiveRulesSplit([]);
222
- if (unconditional.length > 0) {
223
- combined += "\n\n";
224
- combined += unconditional.map((r) => r.content).join("\n\n");
225
- }
226
- }
227
-
228
- return combined;
229
- }
230
-
231
- /**
232
- * Get memory content for message-array injection:
233
- * - prependContent: AGENTS.md + user memory + unconditional rules (for head injection)
234
- * Conditional rules are handled separately via processTriggeredRules().
235
- */
236
- public async getMemoryForInjection(): Promise<{
237
- prependContent: string;
238
- }> {
239
- const baseMemory = await this.memoryService.getCombinedMemoryContent(
240
- this.workdir,
241
- );
242
-
243
- let prependContent = baseMemory;
244
-
245
- if (this.memoryRuleManager) {
246
- const { unconditional } = this.memoryRuleManager.getActiveRulesSplit([]);
247
- if (unconditional.length > 0) {
248
- prependContent += "\n\n";
249
- prependContent += unconditional.map((r) => r.content).join("\n\n");
250
- }
251
- }
252
-
253
- return { prependContent };
254
- }
255
-
256
- /**
257
- * Compute the transcript path using cached encoded workdir
258
- * Called during construction and when sessionId changes
259
- */
260
- private computeTranscriptPath(): string {
261
- const baseDir = join(SESSION_DIR, this.encodedWorkdir);
262
-
263
- // All sessions now go in the same directory
264
- // Session type is determined by metadata, not file path
265
- return join(baseDir, `${this.sessionId}.jsonl`);
266
- }
267
-
268
- // Setter methods, will trigger callbacks
269
- public setSessionId(sessionId: string): void {
270
- if (this.sessionId !== sessionId) {
271
- this.sessionId = sessionId;
272
- // Reset saved message count for new session
273
- this.savedMessageCount = 0;
274
- // Recompute transcript path since session ID changed
275
- this.transcriptPath = this.computeTranscriptPath();
276
-
277
- this.callbacks.onSessionIdChange?.(sessionId);
278
- }
279
- }
280
-
281
- /**
282
- * Create session if needed (async helper)
283
- */
284
- private async createSessionIfNeeded(): Promise<void> {
285
- try {
286
- await createSession(this.sessionId, this.workdir, this.sessionType);
287
- } catch (error) {
288
- logger?.error("Failed to create session:", error);
289
- }
290
- }
291
-
292
- /**
293
- * Record a file path read via the Read tool, to be matched against
294
- * conditional memory rules before the next AI call.
295
- */
296
- public triggerFileRead(filePath: string): void {
297
- const normalizedPath = isAbsolute(filePath)
298
- ? relative(this.workdir, filePath)
299
- : filePath;
300
- this.pendingFileReadTriggers.add(normalizedPath);
301
- }
302
-
303
- /**
304
- * Check if a file has been read (for read-before-edit enforcement).
305
- * Uses recentFileReads Map populated from Read tool execution.
306
- */
307
- public hasFileBeenRead(filePath: string): boolean {
308
- const normalizedPath = isAbsolute(filePath)
309
- ? relative(this.workdir, filePath)
310
- : filePath;
311
- return (
312
- this.recentFileReads.has(normalizedPath) ||
313
- this.recentFileReads.has(filePath)
314
- );
315
- }
316
-
317
- public setMessages(messages: Message[]): void {
318
- const oldLength = this.messages.length;
319
- this.messages = [...messages];
320
-
321
- // Incrementally extract metadata from new messages
322
- const newMessages = messages.slice(oldLength);
323
- for (const message of newMessages) {
324
- this.extractFileReadsFromMessage(message);
325
- this.extractSkillInvocationsFromMessage(message);
326
- }
327
-
328
- // Also check if the last message was updated (common for tool blocks)
329
- if (messages.length > 0 && messages.length === oldLength) {
330
- this.extractFileReadsFromMessage(messages[messages.length - 1]);
331
- this.extractSkillInvocationsFromMessage(messages[messages.length - 1]);
332
- }
333
- }
334
-
335
- /**
336
- * Save current session
337
- */
338
- public async saveSession(): Promise<void> {
339
- try {
340
- // Only save messages that haven't been saved yet
341
- const unsavedMessages = this.messages.slice(this.savedMessageCount);
342
-
343
- if (unsavedMessages.length === 0) {
344
- // No new messages to save
345
- return;
346
- }
347
-
348
- // CC-aligned lazy materialization: when the session file has not been
349
- // materialized yet (no saved messages) and every unsaved message is a
350
- // meta message (isMeta: true, e.g. SessionStart hook context), skip
351
- // persistence. Persisting meta-only sessions would create "0 tokens /
352
- // No content" ghost entries in the resume list. The meta messages stay
353
- // in memory and are flushed together with the first real user/assistant
354
- // message (matches CC's pendingEntries buffering).
355
- if (
356
- this.savedMessageCount === 0 &&
357
- unsavedMessages.every((m) => m.isMeta)
358
- ) {
359
- return;
360
- }
361
-
362
- // Create session if needed (only when we have messages to save)
363
- if (this.savedMessageCount === 0) {
364
- // This is the first time saving messages, so create the session
365
- await this.createSessionIfNeeded();
366
- }
367
-
368
- // Use JSONL format for new sessions
369
- await appendMessages(
370
- this.sessionId,
371
- unsavedMessages,
372
- this.workdir,
373
- this.sessionType,
374
- );
375
-
376
- // Update the saved message count
377
- this.savedMessageCount = this.messages.length;
378
- } catch (error) {
379
- logger?.error("Failed to save session:", error);
380
- }
381
- }
382
-
383
- public setlatestTotalTokens(latestTotalTokens: number): void {
384
- if (this.latestTotalTokens !== latestTotalTokens) {
385
- this.latestTotalTokens = latestTotalTokens;
386
-
387
- this.callbacks.onLatestTotalTokensChange?.(latestTotalTokens);
388
- }
389
- }
390
-
391
- /**
392
- * Clear messages
393
- */
394
- public clearMessages(): void {
395
- this.setMessages([]);
396
- const newSessionId = generateSessionId();
397
- this.setSessionId(newSessionId);
398
- this.setlatestTotalTokens(0);
399
- this.savedMessageCount = 0; // Reset saved message count
400
- }
401
-
402
- /**
403
- * Trigger the rewind UI callback
404
- */
405
- public triggerShowRewind(): void {
406
- this.callbacks.onShowRewind?.();
407
- }
408
-
409
- // Initialize state from session data
410
- public initializeFromSession(sessionData: SessionData): void {
411
- this.setSessionId(sessionData.id);
412
- this.setMessages([...sessionData.messages]);
413
- this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
414
-
415
- // Rebuild loadedRuleIds from persisted meta messages (session restore)
416
- this.rebuildLoadedRuleIds();
417
-
418
- // Set saved message count to the number of loaded messages since they're already saved
419
- // This must be done after setSessionId which resets it to 0
420
- this.savedMessageCount = sessionData.messages.length;
421
- }
422
-
423
- // Encapsulated message operation functions
424
- public addUserMessage(params: UserMessageParams): string {
425
- const id = generateMessageId();
426
- const newMessages = addUserMessageToMessages({
427
- messages: this.messages,
428
- ...params,
429
- id,
430
- });
431
- this.setMessages(newMessages);
432
- this.callbacks.onUserMessageAdded?.(params);
433
-
434
- // Note: Subagent-specific callbacks are now handled by SubagentManager
435
- return id;
436
- }
437
-
438
- /**
439
- * Update an existing user message by its ID.
440
- */
441
- public updateUserMessage(
442
- id: string,
443
- params: Partial<UserMessageParams>,
444
- ): void {
445
- const newMessages = updateUserMessageInMessages(this.messages, id, params);
446
- this.setMessages(newMessages);
447
- }
448
-
449
- public addAssistantMessage(
450
- content?: string,
451
- toolCalls?: ChatCompletionMessageFunctionToolCall[],
452
- usage?: Usage,
453
- additionalFields?: Record<string, unknown>,
454
- ): void {
455
- const additionalFieldsRecord = additionalFields
456
- ? Object.fromEntries(
457
- Object.entries(additionalFields).filter(
458
- ([, value]) => value !== undefined,
459
- ),
460
- )
461
- : undefined;
462
-
463
- const newMessages = addAssistantMessageToMessages(
464
- this.messages,
465
- content,
466
- toolCalls,
467
- usage,
468
- additionalFieldsRecord,
469
- );
470
- this.setMessages(newMessages);
471
- const messageId = this.messages[this.messages.length - 1]?.id;
472
- this.callbacks.onAssistantMessageAdded?.(messageId);
473
-
474
- // Note: Subagent-specific callbacks are now handled by SubagentManager
475
- }
476
-
477
- public mergeAssistantAdditionalFields(
478
- additionalFields: Record<string, unknown>,
479
- ): void {
480
- if (!additionalFields || Object.keys(additionalFields).length === 0) {
481
- return;
482
- }
483
-
484
- const newMessages = [...this.messages];
485
- for (let i = newMessages.length - 1; i >= 0; i--) {
486
- const message = newMessages[i];
487
- if (message.role === "assistant") {
488
- const mergedAdditionalFields = {
489
- ...(message.additionalFields || {}),
490
- } as Record<string, unknown>;
491
-
492
- for (const [key, value] of Object.entries(additionalFields)) {
493
- if (value === undefined) {
494
- continue;
495
- }
496
- mergedAdditionalFields[key] = value;
497
- }
498
-
499
- if (Object.keys(mergedAdditionalFields).length === 0) {
500
- return;
501
- }
502
-
503
- message.additionalFields = mergedAdditionalFields;
504
- this.setMessages(newMessages);
505
- return;
506
- }
507
- }
508
- }
509
-
510
- public updateToolBlock(params: AgentToolBlockUpdateParams): void {
511
- // Finalize any streaming text/reasoning blocks before adding/updating a tool block
512
- this.finalizeStreamingBlocks();
513
- const { messages: newMessages, messageId: resolvedMessageId } =
514
- updateToolBlockInMessage({
515
- messages: this.messages,
516
- ...params,
517
- });
518
- this.setMessages(newMessages);
519
- const callbackParams: ToolBlockUpdateCallbackParams = {
520
- ...params,
521
- messageId:
522
- params.messageId ||
523
- resolvedMessageId ||
524
- this.messages[this.messages.length - 1]?.id ||
525
- "",
526
- };
527
- this.callbacks.onToolBlockUpdated?.(callbackParams);
528
-
529
- // Note: Subagent-specific callbacks are now handled by SubagentManager
530
- }
531
-
532
- /**
533
- * Add a tool block to a specific message by ID.
534
- */
535
- public addToolBlockToMessage(
536
- messageId: string,
537
- params: Omit<AgentToolBlockUpdateParams, "id">,
538
- ): string {
539
- // Finalize any streaming text/reasoning blocks before adding a tool block
540
- this.finalizeStreamingBlocks();
541
- const { messages: newMessages, toolBlockId } =
542
- addToolBlockToMessageInMessages(this.messages, messageId, params);
543
- this.setMessages(newMessages);
544
- this.callbacks.onToolBlockUpdated?.({
545
- ...params,
546
- id: toolBlockId,
547
- messageId,
548
- });
549
- return toolBlockId;
550
- }
551
-
552
- public addErrorBlock(error: string): void {
553
- const newMessages = addErrorBlockToMessage({
554
- messages: this.messages,
555
- error,
556
- });
557
- this.setMessages(newMessages);
558
- this.callbacks.onErrorBlockAdded?.(error);
559
- }
560
-
561
- public addInfoBlock(content: string): void {
562
- const lastMessage = this.messages[this.messages.length - 1];
563
- if (lastMessage && lastMessage.role === "assistant") {
564
- lastMessage.blocks.push({
565
- type: "info",
566
- content,
567
- } as unknown as import("../types/index.js").MessageBlock);
568
- this.setMessages([...this.messages]);
569
- this.callbacks.onInfoBlockAdded?.(content);
570
- }
571
- }
572
-
573
- /**
574
- * Compact messages and update session — append compact message to same file (append-only).
575
- * After compaction, in-memory messages are [compactMessage, ...lastThreeMessages].
576
- */
577
- public async compactMessagesAndUpdateSession(
578
- compactedContent: string,
579
- usage?: Usage,
580
- ): Promise<void> {
581
- // Get last 2 API rounds to preserve (structurally safe boundary)
582
- const lastThreeMessages = getLastApiRounds(this.messages, 2);
583
-
584
- // Create compacted message
585
- const compactMessage: Message = {
586
- id: generateMessageId(),
587
- role: "assistant",
588
- blocks: [
589
- {
590
- type: "compact",
591
- content: compactedContent,
592
- },
593
- ],
594
- timestamp: new Date().toISOString(),
595
- ...(usage && { usage }),
596
- };
597
-
598
- // Build new message array: keep the compacted message and last messages
599
- const newMessages: Message[] = [compactMessage, ...lastThreeMessages];
600
-
601
- // APPEND to the same session file (append-only, like Claude Code)
602
- const { appendMessages } = await import("../services/session.js");
603
- await appendMessages(
604
- this.sessionId,
605
- newMessages,
606
- this.workdir,
607
- this.sessionType,
608
- );
609
-
610
- // Update in-memory state
611
- this.setMessages(newMessages);
612
- this.savedMessageCount = newMessages.length;
613
-
614
- // Clear and rebuild loaded rule IDs from remaining meta messages
615
- this.clearLoadedRuleIds();
616
- this.rebuildLoadedRuleIds();
617
-
618
- // Trigger compaction callback
619
- this.callbacks.onCompactBlockAdded?.(compactedContent);
620
- }
621
-
622
- public addFileHistoryBlock(
623
- snapshots: import("../types/reversion.js").FileSnapshot[],
624
- ): void {
625
- if (snapshots.length === 0) return;
626
-
627
- const lastMessage = this.messages[this.messages.length - 1];
628
- if (lastMessage && lastMessage.role === "assistant") {
629
- lastMessage.blocks.push({
630
- type: "file_history",
631
- snapshots,
632
- } as unknown as import("../types/index.js").MessageBlock);
633
- this.setMessages([...this.messages]);
634
- this.callbacks.onFileHistoryBlockAdded?.(snapshots);
635
- }
636
- }
637
-
638
- // Bang related message operations
639
- public addBangMessage(command: string): void {
640
- const updatedMessages = addBangMessage({
641
- messages: this.messages,
642
- command,
643
- });
644
- this.setMessages(updatedMessages);
645
- // The bang message is appended as the last message
646
- const messageId = this.messages[this.messages.length - 1]?.id ?? "";
647
- this.callbacks.onAddBangMessage?.(command, messageId);
648
- }
649
-
650
- public updateBangMessage(command: string, output: string): void {
651
- const updatedMessages = updateBangInMessage({
652
- messages: this.messages,
653
- command,
654
- output,
655
- });
656
- this.setMessages(updatedMessages);
657
- const messageId = this.findBangMessageId(command) ?? "";
658
- this.callbacks.onUpdateBangMessage?.(command, output, messageId);
659
- }
660
-
661
- public completeBangMessage(
662
- command: string,
663
- exitCode: number,
664
- output?: string,
665
- ): void {
666
- const updatedMessages = completeBangInMessage({
667
- messages: this.messages,
668
- command,
669
- exitCode,
670
- output,
671
- });
672
- this.setMessages(updatedMessages);
673
- const messageId = this.findBangMessageId(command) ?? "";
674
- this.callbacks.onCompleteBangMessage?.(command, exitCode, messageId);
675
- }
676
-
677
- /**
678
- * Find the message ID of the most recent message containing a bang block
679
- * for the given command. Bang callbacks do not carry a block ID, so the
680
- * message is located by matching the command against bang blocks.
681
- */
682
- private findBangMessageId(command: string): string | undefined {
683
- for (let i = this.messages.length - 1; i >= 0; i--) {
684
- const message = this.messages[i];
685
- if (message.role !== "user") continue;
686
- const hasBangBlock = message.blocks?.some(
687
- (block) => block.type === "bang" && block.command === command,
688
- );
689
- if (hasBangBlock) return message.id;
690
- }
691
- return undefined;
692
- }
693
-
694
- public addNotificationMessage(
695
- params: Omit<AddNotificationMessageParams, "messages">,
696
- ): void {
697
- const newMessages = addNotificationMessageToMessages({
698
- messages: this.messages,
699
- ...params,
700
- });
701
- this.setMessages(newMessages);
702
- this.callbacks.onNotificationMessageAdded?.({
703
- taskId: params.taskId,
704
- taskType: params.taskType,
705
- status: params.status,
706
- summary: params.summary,
707
- });
708
- }
709
-
710
- /**
711
- * Rebuild usage array from messages containing usage metadata
712
- * Called during session restoration to reconstruct usage tracking
713
- */
714
- public rebuildUsageFromMessages(messages: Message[]): void {
715
- this._usages = [];
716
- messages.forEach((message) => {
717
- if (message.role === "assistant" && message.usage) {
718
- this._usages.push(message.usage);
719
- }
720
- });
721
- // Trigger callback after rebuilding usage array
722
- this.triggerUsageChange();
723
- }
724
-
725
- /**
726
- * Add usage data to the tracking array and trigger callbacks
727
- * @param usage Usage data from AI operations
728
- */
729
- public addUsage(usage: Usage): void {
730
- this._usages.push(usage);
731
- this.triggerUsageChange();
732
- }
733
-
734
- /**
735
- * Trigger usage change callback with all usage data from assistant messages
736
- */
737
- public triggerUsageChange(): void {
738
- this.callbacks.onUsagesChange?.([...this._usages]);
739
- }
740
-
741
- /**
742
- * Finalize a streaming block of the given type by setting its stage to "end".
743
- * Fires the corresponding incremental callback with chunk="" to signal finalization.
744
- * Returns true if a block was finalized.
745
- */
746
- private finalizeStreamingBlock(
747
- lastMessage: Message,
748
- type: "text" | "reasoning",
749
- ): boolean {
750
- const index = lastMessage.blocks.findIndex(
751
- (block) =>
752
- block.type === type &&
753
- (block as { stage?: string }).stage === "streaming",
754
- );
755
- if (index < 0) return false;
756
-
757
- const block = lastMessage.blocks[index] as {
758
- type: "text" | "reasoning";
759
- content: string;
760
- stage?: string;
761
- startTime?: number;
762
- };
763
- if (block.type === "reasoning") {
764
- lastMessage.blocks[index] = {
765
- type: "reasoning",
766
- content: block.content,
767
- stage: "end" as const,
768
- startTime: block.startTime,
769
- endTime: Date.now(),
770
- };
771
- } else {
772
- lastMessage.blocks[index] = {
773
- type: block.type,
774
- content: block.content,
775
- stage: "end" as const,
776
- };
777
- }
778
-
779
- // Fire incremental callback to signal finalization
780
- const callbackParams = {
781
- messageId: lastMessage.id,
782
- chunk: "",
783
- accumulated: block.content,
784
- stage: "end" as const,
785
- };
786
- if (type === "text") {
787
- this.callbacks.onAssistantContentUpdated?.(callbackParams);
788
- } else {
789
- this.callbacks.onAssistantReasoningUpdated?.(callbackParams);
790
- }
791
- return true;
792
- }
793
-
794
- /**
795
- * Update the current assistant message content during streaming
796
- * This method updates the last assistant message's content without creating a new message
797
- * FR-001: Tracks and provides both chunk (new content) and accumulated (total content)
798
- */
799
- public updateCurrentMessageContent(newAccumulatedContent: string): void {
800
- if (this.messages.length === 0) return;
801
-
802
- const lastMessage = this.messages[this.messages.length - 1];
803
- if (lastMessage.role !== "assistant") return;
804
-
805
- // Finalize any streaming reasoning blocks before text content arrives
806
- this.finalizeStreamingBlock(lastMessage, "reasoning");
807
-
808
- // Get the current content to calculate the chunk
809
- const textBlockIndex = lastMessage.blocks.findIndex(
810
- (block) => block.type === "text",
811
- );
812
- const currentContent =
813
- textBlockIndex >= 0
814
- ? (
815
- lastMessage.blocks[textBlockIndex] as {
816
- type: "text";
817
- content: string;
818
- }
819
- ).content || ""
820
- : "";
821
-
822
- // Calculate the chunk (new content since last update)
823
- const chunk = newAccumulatedContent.slice(currentContent.length);
824
-
825
- if (textBlockIndex >= 0) {
826
- // Update existing text block
827
- lastMessage.blocks[textBlockIndex] = {
828
- type: "text",
829
- content: newAccumulatedContent,
830
- stage: "streaming",
831
- };
832
- } else {
833
- // Add new text block if none exists
834
- lastMessage.blocks.push({
835
- type: "text",
836
- content: newAccumulatedContent,
837
- stage: "streaming",
838
- });
839
- }
840
-
841
- // FR-001: Trigger callbacks with chunk and accumulated content
842
- this.callbacks.onAssistantContentUpdated?.({
843
- messageId: lastMessage.id,
844
- chunk,
845
- accumulated: newAccumulatedContent,
846
- stage: "streaming",
847
- });
848
-
849
- // Note: Subagent-specific callbacks are now handled by SubagentManager
850
- }
851
-
852
- /**
853
- * Update the current assistant message reasoning during streaming
854
- * This method updates the last assistant message's reasoning content without creating a new message
855
- */
856
- public updateCurrentMessageReasoning(newAccumulatedReasoning: string): void {
857
- if (this.messages.length === 0) return;
858
-
859
- const lastMessage = this.messages[this.messages.length - 1];
860
- if (lastMessage.role !== "assistant") return;
861
-
862
- // Finalize any streaming text blocks before reasoning content arrives
863
- this.finalizeStreamingBlock(lastMessage, "text");
864
-
865
- // Get the current reasoning content to calculate the chunk
866
- const reasoningBlockIndex = lastMessage.blocks.findIndex(
867
- (block) => block.type === "reasoning",
868
- );
869
- const currentReasoning =
870
- reasoningBlockIndex >= 0
871
- ? (
872
- lastMessage.blocks[reasoningBlockIndex] as {
873
- type: "reasoning";
874
- content: string;
875
- }
876
- ).content || ""
877
- : "";
878
-
879
- // Calculate the chunk (new content since last update)
880
- const chunk = newAccumulatedReasoning.slice(currentReasoning.length);
881
-
882
- if (reasoningBlockIndex >= 0) {
883
- // Update existing reasoning block
884
- const existingStartTime = (
885
- lastMessage.blocks[reasoningBlockIndex] as {
886
- startTime?: number;
887
- }
888
- ).startTime;
889
- lastMessage.blocks[reasoningBlockIndex] = {
890
- type: "reasoning",
891
- content: newAccumulatedReasoning,
892
- stage: "streaming",
893
- startTime: existingStartTime,
894
- };
895
- } else {
896
- // Add new reasoning block if none exists
897
- lastMessage.blocks.push({
898
- type: "reasoning",
899
- content: newAccumulatedReasoning,
900
- stage: "streaming",
901
- startTime: Date.now(),
902
- });
903
- }
904
-
905
- // Trigger callbacks with chunk and accumulated reasoning content
906
- this.callbacks.onAssistantReasoningUpdated?.({
907
- messageId: lastMessage.id,
908
- chunk,
909
- accumulated: newAccumulatedReasoning,
910
- stage: "streaming",
911
- });
912
- }
913
-
914
- /**
915
- * Finalize streaming text/reasoning blocks by setting their stage to "end".
916
- * Called when a new block (e.g. tool) is appended during streaming, or when streaming completes.
917
- * Fires incremental callbacks for each finalized block.
918
- */
919
- public finalizeStreamingBlocks(): void {
920
- if (this.messages.length === 0) return;
921
- const lastMessage = this.messages[this.messages.length - 1];
922
- if (lastMessage.role !== "assistant") return;
923
-
924
- this.finalizeStreamingBlock(lastMessage, "text");
925
- this.finalizeStreamingBlock(lastMessage, "reasoning");
926
- }
927
-
928
- /**
929
- * Finalize any tool blocks still in a non-terminal stage (start/streaming/running)
930
- * by marking them as ended with an error. Called when the AI call is aborted or
931
- * fails mid-tool-stream so the UI stops showing the "running" spinner.
932
- * Fires onToolBlockUpdated for each finalized tool block.
933
- */
934
- public finalizeAbortedToolBlocks(error?: string): void {
935
- if (this.messages.length === 0) return;
936
- const lastMessage = this.messages[this.messages.length - 1];
937
- if (lastMessage.role !== "assistant") return;
938
-
939
- const errorMessage = error ?? "Tool execution was aborted";
940
- let finalized = false;
941
-
942
- for (let i = 0; i < lastMessage.blocks.length; i++) {
943
- const block = lastMessage.blocks[i] as ToolBlock;
944
- if (block.type !== "tool" || block.stage === "end") continue;
945
-
946
- const timestamp = Date.now();
947
- lastMessage.blocks[i] = {
948
- ...block,
949
- stage: "end",
950
- success: false,
951
- error: errorMessage,
952
- timestamp,
953
- };
954
- finalized = true;
955
-
956
- this.callbacks.onToolBlockUpdated?.({
957
- id: block.id ?? "",
958
- messageId: lastMessage.id ?? "",
959
- name: block.name,
960
- parameters: block.parameters,
961
- parametersChunk: block.parametersChunk,
962
- compactParams: block.compactParams,
963
- stage: "end",
964
- success: false,
965
- error: errorMessage,
966
- isManuallyBackgrounded: block.isManuallyBackgrounded,
967
- timestamp,
968
- });
969
- }
970
-
971
- if (finalized) {
972
- this.setMessages([...this.messages]);
973
- }
974
- }
975
-
976
- /**
977
- * Remove the last user message from the conversation
978
- * Used for hook error handling when the user prompt needs to be erased
979
- */
980
- public removeLastUserMessage(): void {
981
- const newMessages = removeLastUserMessage(this.messages);
982
- this.setMessages(newMessages);
983
- }
984
-
985
- public async getFullMessageThread(): Promise<{
986
- messages: Message[];
987
- sessionIds: string[];
988
- }> {
989
- const { loadFullMessageThread } = await import("../services/session.js");
990
- return loadFullMessageThread(this.sessionId, this.workdir);
991
- }
992
-
993
- /**
994
- * Truncate history to a specific index and revert file changes.
995
- * @param index - The index of the user message to truncate to.
996
- * @param reversionManager - Optional ReversionManager to handle file rollbacks.
997
- */
998
- public async truncateHistory(
999
- index: number,
1000
- reversionManager?: import("./reversionManager.js").ReversionManager,
1001
- ): Promise<void> {
1002
- const { messages } = await this.getFullMessageThread();
1003
-
1004
- if (index < 0 || index >= messages.length) {
1005
- throw new Error(`Invalid message index: ${index}`);
1006
- }
1007
-
1008
- const newMessages = messages.slice(0, index);
1009
- const messagesToRemove = messages.slice(index);
1010
- const messageIdsToRemove = messagesToRemove
1011
- .map((m) => m.id as string)
1012
- .filter((id) => !!id);
1013
-
1014
- if (reversionManager && messageIdsToRemove.length > 0) {
1015
- await reversionManager.revertTo(messageIdsToRemove, messages);
1016
- }
1017
-
1018
- // Rewrite file with truncated messages (full history kept on disk)
1019
- await this.rewriteSessionFile(newMessages);
1020
- // Fold the in-memory messages at the last compact boundary so the agent
1021
- // only sees messages from the latest compact summary forward — matching
1022
- // the compact and resume behaviors (which also fold memory).
1023
- this.setMessages(sliceFromLastCompact(newMessages));
1024
- this.savedMessageCount = newMessages.length;
1025
- }
1026
-
1027
- /**
1028
- * Rewrite the session file with the current messages.
1029
- */
1030
- private async rewriteSessionFile(messages: Message[]): Promise<void> {
1031
- try {
1032
- const { writeFile } = await import("fs/promises");
1033
-
1034
- const content =
1035
- messages
1036
- .map((m) => {
1037
- const { timestamp, ...rest } = m;
1038
- return JSON.stringify({ timestamp, ...rest });
1039
- })
1040
- .join("\n") + (messages.length > 0 ? "\n" : "");
1041
-
1042
- await writeFile(this.transcriptPath, content, "utf8");
1043
- } catch (error) {
1044
- logger?.error("Failed to rewrite session file:", error);
1045
- }
1046
- }
1047
-
1048
- /**
1049
- * Extract file read contents from tool result blocks in a message.
1050
- */
1051
- private extractFileReadsFromMessage(message: Message): void {
1052
- for (const block of message.blocks) {
1053
- if (
1054
- block.type === "tool" &&
1055
- block.name === READ_TOOL_NAME &&
1056
- block.stage === "end" &&
1057
- block.result &&
1058
- block.parameters
1059
- ) {
1060
- let filePath: string | undefined;
1061
- try {
1062
- const params = JSON.parse(block.parameters) as Record<
1063
- string,
1064
- unknown
1065
- >;
1066
- filePath = params.file_path as string | undefined;
1067
- } catch {
1068
- // Ignore parse errors
1069
- }
1070
- if (filePath) {
1071
- this.recentFileReads.set(filePath, {
1072
- content: block.result,
1073
- timestamp: Date.now(),
1074
- });
1075
- }
1076
- }
1077
- }
1078
- }
1079
-
1080
- /**
1081
- * Get recent file read contents, sorted by timestamp (newest first).
1082
- * @param maxFiles - Maximum number of files to return
1083
- * @param maxTokensPerFile - Maximum tokens per file (CJK-aware estimation)
1084
- * @returns Array of { path, content } sorted by recency
1085
- */
1086
- public getRecentFileReads(
1087
- maxFiles = 5,
1088
- maxTokensPerFile = 5000,
1089
- ): Array<{ path: string; content: string }> {
1090
- const sorted = Array.from(this.recentFileReads.entries())
1091
- .sort(([, a], [, b]) => b.timestamp - a.timestamp)
1092
- .slice(0, maxFiles);
1093
-
1094
- const result: Array<{ path: string; content: string }> = [];
1095
- for (const [path, { content }] of sorted) {
1096
- const tokenCount = estimateTokens(content);
1097
- let truncated = content;
1098
- if (tokenCount > maxTokensPerFile) {
1099
- // Truncate proportionally to fit within the token budget
1100
- const ratio = maxTokensPerFile / tokenCount;
1101
- const cutIndex = Math.floor(content.length * ratio);
1102
- truncated = content.slice(0, cutIndex);
1103
- }
1104
- result.push({ path, content: truncated });
1105
- }
1106
- return result;
1107
- }
1108
-
1109
- /**
1110
- * Extract skill invocations from tool blocks in a message.
1111
- */
1112
- private extractSkillInvocationsFromMessage(message: Message): void {
1113
- for (const block of message.blocks) {
1114
- if (
1115
- block.type === "tool" &&
1116
- block.name === "Skill" &&
1117
- block.stage === "end" &&
1118
- block.parameters
1119
- ) {
1120
- try {
1121
- const params = JSON.parse(block.parameters) as Record<
1122
- string,
1123
- unknown
1124
- >;
1125
- const skillName = params.skill_name as string | undefined;
1126
- if (skillName) {
1127
- this.invokedSkills.set(skillName, {
1128
- skillName,
1129
- timestamp: Date.now(),
1130
- });
1131
- }
1132
- } catch {
1133
- // Ignore parse errors
1134
- }
1135
- }
1136
- }
1137
- }
1138
-
1139
- /**
1140
- * Get recently invoked skill names, sorted by timestamp (newest first).
1141
- * @param maxSkills - Maximum number of skills to return
1142
- * @returns Array of skill names sorted by recency
1143
- */
1144
- public getInvokedSkillNames(maxSkills = 10): string[] {
1145
- const sorted = Array.from(this.invokedSkills.entries())
1146
- .sort(([, a], [, b]) => b.timestamp - a.timestamp)
1147
- .slice(0, maxSkills);
1148
-
1149
- return sorted.map(([, { skillName }]) => skillName);
1150
- }
1151
-
1152
- /**
1153
- * Clear all invoked skills (e.g., after compaction).
1154
- */
1155
- public clearInvokedSkills(): void {
1156
- this.invokedSkills.clear();
1157
- }
1158
-
1159
- /**
1160
- * Clear the loaded rule IDs set (e.g., after compaction resets context).
1161
- */
1162
- public clearLoadedRuleIds(): void {
1163
- this.loadedRuleIds.clear();
1164
- }
1165
-
1166
- /**
1167
- * Rebuild loadedRuleIds from persisted messages (session restore / post-compaction).
1168
- * Scans for <!-- rule: {ruleId} --> markers in user meta message content.
1169
- */
1170
- public rebuildLoadedRuleIds(): void {
1171
- for (const message of this.messages) {
1172
- if (message.role !== "user" || !message.isMeta) continue;
1173
- for (const block of message.blocks) {
1174
- if (block.type === "text") {
1175
- const content = (block as { content: string }).content;
1176
- const match = content.match(/<!-- rule: (.+?) -->/);
1177
- if (match) {
1178
- this.loadedRuleIds.add(match[1]);
1179
- }
1180
- }
1181
- }
1182
- }
1183
- }
1184
- }