explorbot 0.1.31 → 0.2.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 (288) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +8 -6
  17. package/dist/src/action-result.d.ts +131 -0
  18. package/dist/src/action-result.js +82 -159
  19. package/dist/src/action.d.ts +43 -0
  20. package/dist/src/action.js +18 -106
  21. package/dist/src/activity.d.ts +26 -0
  22. package/dist/src/ai/agent.d.ts +3 -0
  23. package/dist/src/ai/captain/file-tools.d.ts +28 -0
  24. package/dist/src/ai/captain/idle-mode.d.ts +11 -0
  25. package/dist/src/ai/captain/mixin.d.ts +10 -0
  26. package/dist/src/ai/captain/test-mode.d.ts +13 -0
  27. package/dist/src/ai/captain/web-mode.d.ts +13 -0
  28. package/dist/src/ai/captain/web-mode.js +0 -7
  29. package/dist/src/ai/captain.d.ts +141 -0
  30. package/dist/src/ai/captain.js +4 -19
  31. package/dist/src/ai/conversation.d.ts +28 -0
  32. package/dist/src/ai/conversation.js +9 -28
  33. package/dist/src/ai/driller.d.ts +157 -0
  34. package/dist/src/ai/driller.js +3 -12
  35. package/dist/src/ai/experience-compactor.d.ts +53 -0
  36. package/dist/src/ai/experience-compactor.js +16 -33
  37. package/dist/src/ai/fisherman-tools.d.ts +90 -0
  38. package/dist/src/ai/fisherman-tools.js +1 -1
  39. package/dist/src/ai/fisherman.d.ts +29 -0
  40. package/dist/src/ai/fisherman.js +0 -3
  41. package/dist/src/ai/historian/codeceptjs.d.ts +17 -0
  42. package/dist/src/ai/historian/codeceptjs.js +3 -2
  43. package/dist/src/ai/historian/experience.d.ts +40 -0
  44. package/dist/src/ai/historian/mixin.d.ts +2 -0
  45. package/dist/src/ai/historian/playwright.d.ts +22 -0
  46. package/dist/src/ai/historian/playwright.js +3 -2
  47. package/dist/src/ai/historian/screencast.d.ts +32 -0
  48. package/dist/src/ai/historian/utils.d.ts +3 -0
  49. package/dist/src/ai/historian/utils.js +0 -1
  50. package/dist/src/ai/historian.d.ts +109 -0
  51. package/dist/src/ai/historian.js +1 -5
  52. package/dist/src/ai/navigator.d.ts +54 -0
  53. package/dist/src/ai/navigator.js +11 -46
  54. package/dist/src/ai/pilot.d.ts +81 -0
  55. package/dist/src/ai/pilot.js +6 -17
  56. package/dist/src/ai/planner/session-dedup.d.ts +12 -0
  57. package/dist/src/ai/planner/styles.d.ts +6 -0
  58. package/dist/src/ai/planner/subpages.d.ts +35 -0
  59. package/dist/src/ai/planner.d.ts +79 -0
  60. package/dist/src/ai/planner.js +49 -37
  61. package/dist/src/ai/provider.d.ts +53 -0
  62. package/dist/src/ai/provider.js +78 -130
  63. package/dist/src/ai/quartermaster.d.ts +56 -0
  64. package/dist/src/ai/quartermaster.js +4 -10
  65. package/dist/src/ai/rerunner.d.ts +48 -0
  66. package/dist/src/ai/rerunner.js +14 -22
  67. package/dist/src/ai/researcher/cache.d.ts +6 -0
  68. package/dist/src/ai/researcher/cache.js +11 -18
  69. package/dist/src/ai/researcher/coordinates.d.ts +56 -0
  70. package/dist/src/ai/researcher/deep-analysis.d.ts +74 -0
  71. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  72. package/dist/src/ai/researcher/fingerprint-worker.d.ts +1 -0
  73. package/dist/src/ai/researcher/focus.d.ts +8 -0
  74. package/dist/src/ai/researcher/locators.d.ts +36 -0
  75. package/dist/src/ai/researcher/mixin.d.ts +2 -0
  76. package/dist/src/ai/researcher/parser.d.ts +35 -0
  77. package/dist/src/ai/researcher/parser.js +1 -1
  78. package/dist/src/ai/researcher/research-result.d.ts +15 -0
  79. package/dist/src/ai/researcher/research-result.js +0 -19
  80. package/dist/src/ai/researcher/sections.d.ts +19 -0
  81. package/dist/src/ai/researcher/sections.js +5 -1
  82. package/dist/src/ai/researcher.js +12 -53
  83. package/dist/src/ai/rules.d.ts +16 -0
  84. package/dist/src/ai/rules.js +22 -15
  85. package/dist/src/ai/session-analyst.d.ts +11 -0
  86. package/dist/src/ai/session-analyst.js +8 -5
  87. package/dist/src/ai/task-agent.d.ts +28 -0
  88. package/dist/src/ai/task-agent.js +2 -27
  89. package/dist/src/ai/tester.d.ts +101 -0
  90. package/dist/src/ai/tester.js +10 -61
  91. package/dist/src/ai/tools.d.ts +52 -0
  92. package/dist/src/ai/tools.js +119 -101
  93. package/dist/src/api/api-client.d.ts +17 -0
  94. package/dist/src/api/request-result.d.ts +44 -0
  95. package/dist/src/api/request-store.d.ts +29 -0
  96. package/dist/src/api/spec-reader.d.ts +5 -0
  97. package/dist/src/api/xhr-capture.d.ts +10 -0
  98. package/dist/src/browser-server.d.ts +10 -0
  99. package/dist/src/command-handler.d.ts +68 -0
  100. package/dist/src/commands/add-rule-command.d.ts +11 -0
  101. package/dist/src/commands/base-command.d.ts +26 -0
  102. package/dist/src/commands/clean-command.d.ts +12 -0
  103. package/dist/src/commands/compact-command.d.ts +20 -0
  104. package/dist/src/commands/compact-command.js +6 -6
  105. package/dist/src/commands/context-aria-command.d.ts +6 -0
  106. package/dist/src/commands/context-command.d.ts +11 -0
  107. package/dist/src/commands/context-command.js +1 -1
  108. package/dist/src/commands/context-data-command.d.ts +6 -0
  109. package/dist/src/commands/context-experience-command.d.ts +6 -0
  110. package/dist/src/commands/context-html-command.d.ts +6 -0
  111. package/dist/src/commands/context-knowledge-command.d.ts +6 -0
  112. package/dist/src/commands/context-knowledge-command.js +1 -1
  113. package/dist/src/commands/debug-command.d.ts +6 -0
  114. package/dist/src/commands/drill-command.d.ts +10 -0
  115. package/dist/src/commands/exit-command.d.ts +7 -0
  116. package/dist/src/commands/experience-command.d.ts +15 -0
  117. package/dist/src/commands/experience-command.js +1 -1
  118. package/dist/src/commands/explore-command.d.ts +51 -0
  119. package/dist/src/commands/freesail-command.d.ts +12 -0
  120. package/dist/src/commands/help-command.d.ts +6 -0
  121. package/dist/src/commands/index.d.ts +4 -0
  122. package/dist/src/commands/init-command.d.ts +7 -0
  123. package/dist/src/commands/knows-command.d.ts +10 -0
  124. package/dist/src/commands/knows-command.js +1 -1
  125. package/dist/src/commands/learn-command.d.ts +7 -0
  126. package/dist/src/commands/learn-command.js +3 -1
  127. package/dist/src/commands/navigate-command.d.ts +7 -0
  128. package/dist/src/commands/path-command.d.ts +10 -0
  129. package/dist/src/commands/plan-clear-command.d.ts +7 -0
  130. package/dist/src/commands/plan-command.d.ts +14 -0
  131. package/dist/src/commands/plan-edit-command.d.ts +7 -0
  132. package/dist/src/commands/plan-load-command.d.ts +7 -0
  133. package/dist/src/commands/plan-reload-command.d.ts +7 -0
  134. package/dist/src/commands/plan-save-command.d.ts +7 -0
  135. package/dist/src/commands/plans-command.d.ts +21 -0
  136. package/dist/src/commands/rerun-command.d.ts +7 -0
  137. package/dist/src/commands/research-command.d.ts +11 -0
  138. package/dist/src/commands/runs-command.d.ts +7 -0
  139. package/dist/src/commands/start-command.d.ts +7 -0
  140. package/dist/src/commands/status-command.d.ts +6 -0
  141. package/dist/src/commands/test-command.d.ts +11 -0
  142. package/dist/src/components/ActivityPane.d.ts +6 -0
  143. package/dist/src/components/AddKnowledge.d.ts +10 -0
  144. package/dist/src/components/AddKnowledge.js +3 -7
  145. package/dist/src/components/AddRule.d.ts +9 -0
  146. package/dist/src/components/App.d.ts +9 -0
  147. package/dist/src/components/App.js +2 -2
  148. package/dist/src/components/Autocomplete.d.ts +3 -0
  149. package/dist/src/components/InputPane.d.ts +13 -0
  150. package/dist/src/components/InputReadline.d.ts +17 -0
  151. package/dist/src/components/LogPane.d.ts +6 -0
  152. package/dist/src/components/PlanEditor.d.ts +9 -0
  153. package/dist/src/components/PlanPane.d.ts +13 -0
  154. package/dist/src/components/SessionTimer.d.ts +6 -0
  155. package/dist/src/components/StateTransitionPane.d.ts +8 -0
  156. package/dist/src/components/StatusPane.d.ts +4 -0
  157. package/dist/src/components/TaskPane.d.ts +9 -0
  158. package/dist/src/components/Welcome.d.ts +3 -0
  159. package/dist/src/components/WelcomeChecklist.d.ts +9 -0
  160. package/dist/src/components/WelcomeCommands.d.ts +6 -0
  161. package/dist/src/components/autocomplete-store.d.ts +10 -0
  162. package/dist/src/components/parse-keypress.d.ts +12 -0
  163. package/dist/src/config.d.ts +253 -0
  164. package/dist/src/config.js +12 -0
  165. package/dist/src/execution-controller.d.ts +25 -0
  166. package/dist/src/experience-tracker.d.ts +112 -0
  167. package/dist/src/experience-tracker.js +83 -155
  168. package/dist/src/explorbot.d.ts +106 -0
  169. package/dist/src/explorbot.js +17 -51
  170. package/dist/src/explorer.d.ts +129 -0
  171. package/dist/src/explorer.js +31 -77
  172. package/dist/src/index.d.ts +6 -0
  173. package/dist/src/index.js +2 -55
  174. package/dist/src/knowledge-tracker.d.ts +33 -0
  175. package/dist/src/knowledge-tracker.js +47 -47
  176. package/dist/src/observability.d.ts +13 -0
  177. package/dist/src/observability.js +2 -1
  178. package/dist/src/playwright-recorder.d.ts +31 -0
  179. package/dist/src/reporter.d.ts +34 -0
  180. package/dist/src/state-manager.d.ts +160 -0
  181. package/dist/src/state-manager.js +10 -136
  182. package/dist/src/stats.d.ts +24 -0
  183. package/dist/src/suite.d.ts +24 -0
  184. package/dist/src/test-plan.d.ts +166 -0
  185. package/dist/src/utils/aria.d.ts +25 -0
  186. package/dist/src/utils/aria.js +4 -1
  187. package/dist/src/utils/browser-errors.d.ts +8 -0
  188. package/dist/src/utils/cache.d.ts +15 -0
  189. package/dist/src/utils/cache.js +34 -0
  190. package/dist/src/utils/cli-name.d.ts +1 -0
  191. package/dist/src/utils/code-extractor.d.ts +1 -0
  192. package/dist/src/utils/context-formatter.d.ts +31 -0
  193. package/dist/src/utils/context-formatter.js +15 -27
  194. package/dist/src/utils/error-page.d.ts +12 -0
  195. package/dist/src/utils/expandable.d.ts +4 -0
  196. package/dist/src/utils/hooks-runner.d.ts +15 -0
  197. package/dist/src/utils/html-diff.d.ts +19 -0
  198. package/dist/src/utils/html.d.ts +129 -0
  199. package/dist/src/utils/html.js +71 -0
  200. package/dist/src/utils/log-filters.d.ts +6 -0
  201. package/dist/src/utils/logger.d.ts +35 -0
  202. package/dist/src/utils/loop.d.ts +35 -0
  203. package/dist/src/utils/markdown-files.d.ts +10 -0
  204. package/dist/src/utils/markdown-files.js +21 -0
  205. package/dist/src/utils/markdown-parser.d.ts +17 -0
  206. package/dist/src/utils/markdown-query.d.ts +46 -0
  207. package/dist/src/utils/markdown-query.js +14 -1
  208. package/dist/src/utils/markdown-terminal.d.ts +1 -0
  209. package/dist/src/utils/next-steps.d.ts +11 -0
  210. package/dist/src/utils/page-readiness.d.ts +7 -0
  211. package/dist/src/utils/page-readiness.js +1 -1
  212. package/dist/src/utils/research-parser.d.ts +1 -0
  213. package/dist/src/utils/retry.d.ts +8 -0
  214. package/dist/src/utils/rules-loader.d.ts +12 -0
  215. package/dist/src/utils/secrets.d.ts +4 -0
  216. package/dist/src/utils/secrets.js +28 -0
  217. package/dist/src/utils/step-analyzer.d.ts +11 -0
  218. package/dist/src/utils/strings.d.ts +5 -0
  219. package/dist/src/utils/strings.js +10 -0
  220. package/dist/src/utils/test-files.d.ts +3 -0
  221. package/dist/src/utils/test-plan-markdown.d.ts +9 -0
  222. package/dist/src/utils/throttle.d.ts +2 -0
  223. package/dist/src/utils/unique-names.d.ts +2 -0
  224. package/dist/src/utils/url-matcher.d.ts +6 -0
  225. package/dist/src/utils/web-element.d.ts +45 -0
  226. package/dist/src/utils/web-element.js +3 -0
  227. package/dist/src/utils/web-sandbox.d.ts +4 -0
  228. package/dist/src/utils/web-sandbox.js +37 -0
  229. package/dist/src/utils/xpath.d.ts +17 -0
  230. package/package.json +8 -6
  231. package/src/action-result.ts +80 -168
  232. package/src/action.ts +16 -123
  233. package/src/ai/captain/web-mode.ts +0 -7
  234. package/src/ai/captain.ts +5 -17
  235. package/src/ai/conversation.ts +9 -32
  236. package/src/ai/driller.ts +3 -16
  237. package/src/ai/experience-compactor.ts +16 -35
  238. package/src/ai/fisherman-tools.ts +1 -1
  239. package/src/ai/fisherman.ts +0 -4
  240. package/src/ai/historian/codeceptjs.ts +4 -2
  241. package/src/ai/historian/playwright.ts +4 -2
  242. package/src/ai/historian/utils.ts +0 -1
  243. package/src/ai/historian.ts +4 -7
  244. package/src/ai/navigator.ts +13 -49
  245. package/src/ai/pilot.ts +6 -16
  246. package/src/ai/planner.ts +56 -44
  247. package/src/ai/provider.ts +86 -139
  248. package/src/ai/quartermaster.ts +34 -41
  249. package/src/ai/rerunner.ts +17 -21
  250. package/src/ai/researcher/cache.ts +10 -16
  251. package/src/ai/researcher/deep-analysis.ts +3 -6
  252. package/src/ai/researcher/parser.ts +1 -1
  253. package/src/ai/researcher/research-result.ts +0 -18
  254. package/src/ai/researcher/sections.ts +5 -1
  255. package/src/ai/researcher.ts +11 -59
  256. package/src/ai/rules.ts +24 -13
  257. package/src/ai/session-analyst.ts +8 -5
  258. package/src/ai/task-agent.ts +3 -30
  259. package/src/ai/tester.ts +12 -73
  260. package/src/ai/tools.ts +119 -106
  261. package/src/commands/compact-command.ts +7 -8
  262. package/src/commands/context-command.ts +1 -1
  263. package/src/commands/context-knowledge-command.ts +1 -1
  264. package/src/commands/experience-command.ts +1 -1
  265. package/src/commands/knows-command.ts +1 -1
  266. package/src/commands/learn-command.ts +3 -1
  267. package/src/components/AddKnowledge.tsx +5 -7
  268. package/src/components/App.tsx +2 -2
  269. package/src/config.ts +12 -0
  270. package/src/experience-tracker.ts +90 -152
  271. package/src/explorbot.ts +17 -51
  272. package/src/explorer.ts +34 -81
  273. package/src/index.ts +9 -0
  274. package/src/knowledge-tracker.ts +49 -50
  275. package/src/observability.ts +2 -1
  276. package/src/state-manager.ts +15 -164
  277. package/src/utils/aria.ts +11 -2
  278. package/src/utils/cache.ts +40 -0
  279. package/src/utils/context-formatter.ts +12 -15
  280. package/src/utils/html.ts +79 -0
  281. package/src/utils/markdown-files.ts +30 -0
  282. package/src/utils/markdown-query.ts +15 -1
  283. package/src/utils/page-readiness.ts +1 -1
  284. package/src/utils/secrets.ts +28 -0
  285. package/src/utils/strings.ts +12 -0
  286. package/src/utils/web-element.ts +4 -0
  287. package/src/utils/web-sandbox.ts +43 -0
  288. package/src/index.tsx +0 -62
@@ -11,7 +11,7 @@ import { Stats } from '../stats.ts';
11
11
  import { createDebug, tag } from '../utils/logger.js';
12
12
  import { type RetryOptions, withRetry } from '../utils/retry.js';
13
13
  import { RulesLoader } from '../utils/rules-loader.ts';
14
- import { Conversation } from './conversation.js';
14
+ import { Conversation, toToolExecution } from './conversation.js';
15
15
 
16
16
  const debugLog = createDebug('explorbot:provider');
17
17
  const promptLog = createDebug('explorbot:provider:out');
@@ -22,6 +22,8 @@ export class ContextLengthError extends Error {}
22
22
 
23
23
  let telemetryRegistered = false;
24
24
 
25
+ const CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
26
+
25
27
  function extractCachedTokens(usage: any): number {
26
28
  if (!usage) return 0;
27
29
  const direct = usage.inputTokenDetails?.cacheReadTokens ?? usage.cachedInputTokens;
@@ -31,20 +33,27 @@ function extractCachedTokens(usage: any): number {
31
33
  return typeof fromRaw === 'number' ? fromRaw : 0;
32
34
  }
33
35
 
34
- function rejectAfterIdle(ms: number, signal: { cancelled: boolean }): Promise<never> {
36
+ function abortAfterIdle(ms: number, cancel: { cancelled: boolean }, controller: AbortController): Promise<never> {
35
37
  return new Promise((_, reject) => {
36
38
  const tick = () => {
37
- if (signal.cancelled) return;
39
+ if (cancel.cancelled) return;
38
40
  if (executionController.isAwaitingInput()) {
39
41
  setTimeout(tick, ms);
40
42
  return;
41
43
  }
44
+ controller.abort();
42
45
  reject(new Error('AI request timeout'));
43
46
  };
44
47
  setTimeout(tick, ms);
45
48
  });
46
49
  }
47
50
 
51
+ function combinedAbortSignal(controller: AbortController): AbortSignal {
52
+ const executionSignal = executionController.getAbortSignal();
53
+ if (!executionSignal) return controller.signal;
54
+ return AbortSignal.any([controller.signal, executionSignal]);
55
+ }
56
+
48
57
  export class Provider {
49
58
  private config: AIConfig;
50
59
  private telemetryEnabled = false;
@@ -69,7 +78,6 @@ export class Provider {
69
78
  },
70
79
  };
71
80
 
72
- static readonly CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
73
81
  lastConversation: Conversation | null = null;
74
82
 
75
83
  constructor(config: AIConfig) {
@@ -137,12 +145,12 @@ export class Provider {
137
145
  return parts.length > 0 ? parts.join('\n\n') : undefined;
138
146
  }
139
147
 
140
- getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined {
148
+ private getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined {
141
149
  const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
142
150
  return agentConfig?.providerOptions;
143
151
  }
144
152
 
145
- getReasoningForAgent(agentName?: string): string | undefined {
153
+ private getReasoningForAgent(agentName?: string): string | undefined {
146
154
  if (!agentName) return undefined;
147
155
  const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
148
156
  return agentConfig?.reasoning;
@@ -171,6 +179,50 @@ export class Provider {
171
179
  if (reasoning) config.reasoning ??= reasoning;
172
180
  }
173
181
 
182
+ private buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any> {
183
+ const telemetry = this.getTelemetry(options);
184
+ const config = this.mergeProviderOptions(
185
+ {
186
+ ...defaults,
187
+ allowSystemInMessages: true,
188
+ ...(this.config.config || {}),
189
+ ...options,
190
+ ...overrides,
191
+ },
192
+ options.agentName
193
+ );
194
+ this.finalizeConfig(config, options, telemetry);
195
+ return config;
196
+ }
197
+
198
+ private recordUsage(agentName: string, modelName: string, usage: any): void {
199
+ if (!usage) return;
200
+ Stats.recordTokens(agentName, modelName, {
201
+ input: usage.inputTokens ?? usage.promptTokens ?? 0,
202
+ output: usage.outputTokens ?? usage.completionTokens ?? 0,
203
+ total: usage.totalTokens ?? 0,
204
+ cached: extractCachedTokens(usage),
205
+ });
206
+ }
207
+
208
+ private async raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T> {
209
+ const cancel = { cancelled: false };
210
+ const controller = new AbortController();
211
+ const combinedSignal = combinedAbortSignal(controller);
212
+ try {
213
+ return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller)]);
214
+ } finally {
215
+ cancel.cancelled = true;
216
+ }
217
+ }
218
+
219
+ private async recoverFromContextLength(error: any, messages: ModelMessage[], options: any, retry: (messages: ModelMessage[], options: any) => Promise<any>): Promise<any> {
220
+ const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
221
+ if (!reduced) throw new ContextLengthError(error.message || error.toString());
222
+ tag('warning').log('Context length exceeded, retrying with reduced messages...');
223
+ return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
224
+ }
225
+
174
226
  private initLangfuse() {
175
227
  const langfuseConfig = this.config.langfuse;
176
228
  const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
@@ -206,20 +258,25 @@ export class Provider {
206
258
 
207
259
  const runTelemetry = Observability.getTelemetry();
208
260
 
209
- if (!options.telemetry) {
261
+ let optionTelemetry = options.telemetry;
262
+ if (options.telemetryFunctionId && !optionTelemetry?.functionId) {
263
+ optionTelemetry = { ...optionTelemetry, functionId: options.telemetryFunctionId };
264
+ }
265
+
266
+ if (!optionTelemetry) {
210
267
  return runTelemetry;
211
268
  }
212
269
 
213
270
  if (!runTelemetry) {
214
- return options.telemetry;
271
+ return optionTelemetry;
215
272
  }
216
273
 
217
274
  return {
218
275
  ...runTelemetry,
219
- ...options.telemetry,
276
+ ...optionTelemetry,
220
277
  metadata: {
221
278
  ...runTelemetry.metadata,
222
- ...options.telemetry.metadata,
279
+ ...optionTelemetry.metadata,
223
280
  },
224
281
  };
225
282
  }
@@ -253,12 +310,8 @@ export class Provider {
253
310
  const toolCalls = response.toolCalls || [];
254
311
  const toolResults = response.toolResults || [];
255
312
 
256
- const toolExecutions = toolCalls.map((call: any, index: number) => ({
257
- toolName: call.toolName || '',
258
- input: call.input,
259
- output: toolResults[index]?.output,
260
- wasSuccessful: toolResults[index]?.output?.success || false,
261
- }));
313
+ const resultsById = new Map(toolResults.map((r: any) => [r.toolCallId, r]));
314
+ const toolExecutions = toolCalls.map((call: any) => toToolExecution(call.toolName || '', call.input, resultsById.get(call.toolCallId)?.output));
262
315
 
263
316
  return { conversation, response, toolExecutions };
264
317
  }
@@ -268,19 +321,7 @@ export class Provider {
268
321
  setActivity(`🤖 Asking ${modelName}`, 'ai');
269
322
  promptLog(`Using model: ${modelName}`);
270
323
 
271
- const telemetry = this.getTelemetry(options);
272
- const config = this.mergeProviderOptions(
273
- {
274
- maxOutputTokens: 16384,
275
- allowSystemInMessages: true,
276
- ...(this.config.config || {}),
277
- ...options,
278
- model,
279
- abortSignal: executionController.getAbortSignal(),
280
- },
281
- options.agentName
282
- );
283
- this.finalizeConfig(config, options, telemetry);
324
+ const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
284
325
 
285
326
  promptLog(messages[messages.length - 1].content);
286
327
  try {
@@ -302,14 +343,7 @@ export class Provider {
302
343
  clearActivity();
303
344
  responseLog(response.text);
304
345
 
305
- if (response.usage) {
306
- Stats.recordTokens(options.agentName || 'unknown', modelName, {
307
- input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
308
- output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
309
- total: response.usage.totalTokens ?? 0,
310
- cached: extractCachedTokens(response.usage),
311
- });
312
- }
346
+ this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
313
347
 
314
348
  return response;
315
349
  } catch (error: any) {
@@ -317,12 +351,7 @@ export class Provider {
317
351
  if (error?.name === 'AbortError') throw error;
318
352
  if (error instanceof ContextLengthError) throw error;
319
353
  if (Provider.isContextLengthError(error)) {
320
- const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
321
- if (reduced) {
322
- tag('warning').log('Context length exceeded, retrying with reduced messages...');
323
- return this.chat(reduced.messages, model, { ...options, _contextRetryLevel: reduced.nextLevel });
324
- }
325
- throw new ContextLengthError(error.message || error.toString());
354
+ return this.recoverFromContextLength(error, messages, options, (m, o) => this.chat(m, model, o));
326
355
  }
327
356
  const message = error.message || error.toString();
328
357
  if (message !== 'No response text from AI') {
@@ -342,47 +371,19 @@ export class Provider {
342
371
  promptLog('Available tools:', toolNames);
343
372
  promptLog(messages[messages.length - 1].content);
344
373
 
345
- const telemetry = this.getTelemetry(options);
346
374
  const maxRoundtrips = options.maxToolRoundtrips ?? 5;
347
375
  const extraStop = options.stopWhen;
348
376
  const stopConditions: any[] = [isStepCount(maxRoundtrips)];
349
377
  if (extraStop) stopConditions.push(extraStop);
350
- const { stopWhen: _ignoredStopWhen, ...optionsWithoutStop } = options;
351
- const config = this.mergeProviderOptions(
352
- {
353
- tools,
354
- maxOutputTokens: 16384,
355
- toolChoice: 'auto',
356
- allowSystemInMessages: true,
357
- ...(this.config.config || {}),
358
- ...optionsWithoutStop,
359
- stopWhen: stopConditions,
360
- model,
361
- abortSignal: executionController.getAbortSignal(),
362
- },
363
- options.agentName
364
- );
365
- this.finalizeConfig(config, options, telemetry);
378
+ const config = this.buildGenerateConfig({ tools, maxOutputTokens: 16384, toolChoice: 'auto' }, { stopWhen: stopConditions, model }, options);
366
379
  try {
367
380
  const response = await withRetry(async () => {
368
- const timeout = config.timeout || 30000;
369
- const cancel = { cancelled: false };
370
- try {
371
- const result = (await Promise.race([
372
- generateText({
373
- messages,
374
- ...config,
375
- }),
376
- rejectAfterIdle(timeout, cancel),
377
- ])) as any;
378
- const hasToolCall = (result.toolCalls?.length || 0) > 0;
379
- if (!result.text && !hasToolCall && result.finishReason === 'length') {
380
- throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
381
- }
382
- return result;
383
- } finally {
384
- cancel.cancelled = true;
381
+ const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
382
+ const hasToolCall = (result.toolCalls?.length || 0) > 0;
383
+ if (!result.text && !hasToolCall && result.finishReason === 'length') {
384
+ throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
385
385
  }
386
+ return result;
386
387
  }, this.getRetryOptions(options));
387
388
 
388
389
  clearActivity();
@@ -397,14 +398,7 @@ export class Provider {
397
398
 
398
399
  responseLog(response.text);
399
400
 
400
- if (response.usage) {
401
- Stats.recordTokens(options.agentName || 'unknown', modelName, {
402
- input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
403
- output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
404
- total: response.usage.totalTokens ?? 0,
405
- cached: extractCachedTokens(response.usage),
406
- });
407
- }
401
+ this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
408
402
 
409
403
  return response;
410
404
  } catch (error: any) {
@@ -415,12 +409,7 @@ export class Provider {
415
409
  if (error?.name === 'AbortError') throw error;
416
410
  if (error instanceof ContextLengthError) throw error;
417
411
  if (Provider.isContextLengthError(error)) {
418
- const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
419
- if (reduced) {
420
- tag('warning').log('Context length exceeded, retrying with reduced messages...');
421
- return this.generateWithTools(reduced.messages, model, tools, { ...options, _contextRetryLevel: reduced.nextLevel });
422
- }
423
- throw new ContextLengthError(error.message || error.toString());
412
+ return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateWithTools(m, model, tools, o));
424
413
  }
425
414
  if (error.constructor?.name === 'AI_APICallError') {
426
415
  responseLog(error.message);
@@ -436,49 +425,18 @@ export class Provider {
436
425
  setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
437
426
  promptLog(`Using model: ${modelName}`);
438
427
 
439
- const telemetry = this.getTelemetry(options);
440
- const config = this.mergeProviderOptions(
441
- {
442
- schema,
443
- allowSystemInMessages: true,
444
- ...(this.config.config || {}),
445
- ...options,
446
- model: modelToUse,
447
- abortSignal: executionController.getAbortSignal(),
448
- },
449
- options.agentName
450
- );
451
- this.finalizeConfig(config, options, telemetry);
428
+ const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
452
429
 
453
430
  try {
454
431
  promptLog(messages[messages.length - 1].content);
455
432
  const response = await withRetry(async () => {
456
- const timeout = config.timeout || 30000;
457
- const cancel = { cancelled: false };
458
- try {
459
- return (await Promise.race([
460
- generateObject({
461
- messages,
462
- ...config,
463
- }),
464
- rejectAfterIdle(timeout, cancel),
465
- ])) as any;
466
- } finally {
467
- cancel.cancelled = true;
468
- }
433
+ return (await this.raceWithIdleTimeout((signal) => generateObject({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
469
434
  }, this.getRetryOptions(options));
470
435
 
471
436
  clearActivity();
472
437
  responseLog(response.object);
473
438
 
474
- if (response.usage) {
475
- Stats.recordTokens(options.agentName || 'unknown', modelName, {
476
- input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
477
- output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
478
- total: response.usage.totalTokens ?? 0,
479
- cached: extractCachedTokens(response.usage),
480
- });
481
- }
439
+ this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
482
440
 
483
441
  return response;
484
442
  } catch (error: any) {
@@ -486,12 +444,7 @@ export class Provider {
486
444
  if (error?.name === 'AbortError') throw error;
487
445
  if (error instanceof ContextLengthError) throw error;
488
446
  if (Provider.isContextLengthError(error)) {
489
- const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
490
- if (reduced) {
491
- tag('warning').log('Context length exceeded, retrying with reduced messages...');
492
- return this.generateObject(reduced.messages, schema, model, { ...options, _contextRetryLevel: reduced.nextLevel });
493
- }
494
- throw new ContextLengthError(error.message || error.toString());
447
+ return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
495
448
  }
496
449
  throw new AiError(error.message || error.toString());
497
450
  }
@@ -499,7 +452,7 @@ export class Provider {
499
452
 
500
453
  static isContextLengthError(error: any): boolean {
501
454
  const msg = (error?.message || error?.toString() || '').toLowerCase();
502
- return Provider.CONTEXT_LENGTH_PATTERNS.some((p) => msg.includes(p));
455
+ return CONTEXT_LENGTH_PATTERNS.some((p) => msg.includes(p));
503
456
  }
504
457
 
505
458
  static trimMessagesForRetry(messages: ModelMessage[]): ModelMessage[] | null {
@@ -670,13 +623,7 @@ export class Provider {
670
623
  clearActivity();
671
624
  responseLog(response.text);
672
625
 
673
- if (response.usage) {
674
- Stats.recordTokens('vision', this.getModelName(this.config.visionModel), {
675
- input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
676
- output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
677
- total: response.usage.totalTokens ?? 0,
678
- });
679
- }
626
+ this.recordUsage('vision', this.getModelName(this.config.visionModel), response.usage);
680
627
 
681
628
  return response;
682
629
  } catch (error: any) {
@@ -1,8 +1,8 @@
1
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import type { ActionResult } from '../action-result.ts';
5
- import { ConfigParser } from '../config.ts';
5
+ import { outputPath } from '../config.ts';
6
6
  import type { StateManager, StateTransition, WebPageState } from '../state-manager.ts';
7
7
  import type { Task } from '../test-plan.ts';
8
8
  import { createDebug, tag } from '../utils/logger.ts';
@@ -12,36 +12,6 @@ import type { Provider } from './provider.ts';
12
12
 
13
13
  const debugLog = createDebug('explorbot:quartermaster');
14
14
 
15
- interface AxeViolation {
16
- id: string;
17
- impact: 'critical' | 'serious' | 'moderate' | 'minor';
18
- description: string;
19
- helpUrl: string;
20
- nodes: Array<{ html: string; target: string[] }>;
21
- }
22
-
23
- interface PageAnalysis {
24
- url: string;
25
- stateHash: string;
26
- timestamp: string;
27
- axeViolations: AxeViolation[];
28
- }
29
-
30
- interface SemanticIssue {
31
- type: 'unclear_intention' | 'confusing_naming' | 'hard_to_interact';
32
- element: string;
33
- issue: string;
34
- suggestion: string;
35
- }
36
-
37
- interface AnalysisReport {
38
- scenario: string;
39
- timestamp: string;
40
- url: string;
41
- axeViolations: AxeViolation[];
42
- semanticIssues: SemanticIssue[];
43
- }
44
-
45
15
  export class Quartermaster {
46
16
  private provider: Provider;
47
17
  private model?: any;
@@ -55,13 +25,12 @@ export class Quartermaster {
55
25
  this.provider = provider;
56
26
  this.model = options?.model;
57
27
 
58
- const configParser = ConfigParser.getInstance();
59
- this.outputDir = join(configParser.getOutputDir(), 'a11y');
28
+ this.outputDir = outputPath('a11y');
60
29
  }
61
30
 
62
31
  start(playwrightHelper: any, stateManager: StateManager): void {
63
32
  this.playwrightHelper = playwrightHelper;
64
- this.ensureDirectory();
33
+ mkdirSync(this.outputDir, { recursive: true });
65
34
 
66
35
  this.unsubscribe = stateManager.onStateChange((event) => {
67
36
  this.onPageChange(event);
@@ -78,12 +47,6 @@ export class Quartermaster {
78
47
  debugLog('Quartermaster stopped');
79
48
  }
80
49
 
81
- private ensureDirectory(): void {
82
- if (!existsSync(this.outputDir)) {
83
- mkdirSync(this.outputDir, { recursive: true });
84
- }
85
- }
86
-
87
50
  private onPageChange(event: StateTransition): void {
88
51
  const state = event.toState;
89
52
  if (!state?.hash) return;
@@ -284,3 +247,33 @@ Focus on what would confuse a real user or caused the agent to make mistakes.`;
284
247
  return content;
285
248
  }
286
249
  }
250
+
251
+ interface AxeViolation {
252
+ id: string;
253
+ impact: 'critical' | 'serious' | 'moderate' | 'minor';
254
+ description: string;
255
+ helpUrl: string;
256
+ nodes: Array<{ html: string; target: string[] }>;
257
+ }
258
+
259
+ interface PageAnalysis {
260
+ url: string;
261
+ stateHash: string;
262
+ timestamp: string;
263
+ axeViolations: AxeViolation[];
264
+ }
265
+
266
+ interface SemanticIssue {
267
+ type: 'unclear_intention' | 'confusing_naming' | 'hard_to_interact';
268
+ element: string;
269
+ issue: string;
270
+ suggestion: string;
271
+ }
272
+
273
+ interface AnalysisReport {
274
+ scenario: string;
275
+ timestamp: string;
276
+ url: string;
277
+ axeViolations: AxeViolation[];
278
+ semanticIssues: SemanticIssue[];
279
+ }
@@ -17,10 +17,10 @@ import type Explorer from '../explorer.ts';
17
17
  import type { KnowledgeTracker } from '../knowledge-tracker.ts';
18
18
  import { Stats } from '../stats.ts';
19
19
  import { Task, Test, TestResult } from '../test-plan.ts';
20
+ import { formatHeadings } from '../utils/context-formatter.ts';
20
21
  import { createDebug, tag } from '../utils/logger.ts';
21
22
  import { loop } from '../utils/loop.ts';
22
23
  import { RulesLoader } from '../utils/rules-loader.ts';
23
- import { loadTestSuites, printTestList } from '../utils/test-files.ts';
24
24
  import type { Agent } from './agent.ts';
25
25
  import { toolExecutionLabel } from './conversation.ts';
26
26
  import type { Navigator } from './navigator.ts';
@@ -38,8 +38,9 @@ export class Rerunner extends TaskAgent implements Agent {
38
38
  private explorer: Explorer;
39
39
  private provider: Provider;
40
40
  private agentTools: any;
41
- private healedSteps: Array<{ test: string; original: string; healed: string }> = [];
41
+ private healedSteps: Array<{ original: string; healed: string }> = [];
42
42
  private traceDir = '';
43
+ private static pluginsWired = false;
43
44
 
44
45
  constructor(explorer: Explorer, provider: Provider, agentTools?: any) {
45
46
  super();
@@ -76,10 +77,6 @@ export class Rerunner extends TaskAgent implements Agent {
76
77
  return this.rerunnerConfig.healMaxIterations ?? 3;
77
78
  }
78
79
 
79
- listTests(testsDir: string): void {
80
- printTestList(loadTestSuites(testsDir));
81
- }
82
-
83
80
  async rerun(filePath: string, options?: { testIndices?: number[] }): Promise<RerunResult> {
84
81
  const absPath = resolve(filePath);
85
82
  if (!existsSync(absPath)) {
@@ -219,7 +216,6 @@ export class Rerunner extends TaskAgent implements Agent {
219
216
 
220
217
  private setupPlugins(): void {
221
218
  const healMod = heal.default || heal;
222
- healMod.connectToEvents();
223
219
 
224
220
  healMod.addRecipe('explorbot-ai-healer', {
225
221
  priority: 10,
@@ -233,21 +229,29 @@ export class Rerunner extends TaskAgent implements Agent {
233
229
  healMod.addRecipe(name, recipe);
234
230
  }
235
231
 
232
+ const outputDir = (global as any).output_dir || 'output';
233
+ this.traceDir = `${outputDir}/rerun-traces`;
234
+
235
+ if (Rerunner.pluginsWired) return;
236
+ Rerunner.pluginsWired = true;
237
+
238
+ healMod.connectToEvents();
239
+
236
240
  let currentTest: any = null;
237
241
  let healTries = 0;
238
242
  let isHealing = false;
239
243
  let caughtError: any = null;
240
- const healLimit = this.healLimit;
241
244
 
242
245
  codeceptjs.event.dispatcher.on('test.before', (test: any) => {
243
246
  currentTest = test;
244
247
  healTries = 0;
245
248
  caughtError = null;
249
+ isHealing = false;
246
250
  });
247
251
 
248
252
  codeceptjs.event.dispatcher.on('step.after', (step: any) => {
249
253
  if (isHealing) return;
250
- if (healTries >= healLimit) return;
254
+ if (healTries >= this.healLimit) return;
251
255
  if (!healMod.hasCorrespondingRecipes(step)) return;
252
256
 
253
257
  codeceptjs.recorder.catchWithoutStop(async (err: any) => {
@@ -286,10 +290,6 @@ export class Rerunner extends TaskAgent implements Agent {
286
290
  factor: 1.5,
287
291
  });
288
292
 
289
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
290
- const outputDir = (global as any).output_dir || 'output';
291
- this.traceDir = `${outputDir}/rerun_${timestamp}`;
292
-
293
293
  const aiTrace = aiTracePlugin.default || aiTracePlugin;
294
294
  aiTrace(this.rerunnerConfig.aiTrace || { output: this.traceDir });
295
295
 
@@ -303,9 +303,9 @@ export class Rerunner extends TaskAgent implements Agent {
303
303
 
304
304
  private teardownHealing(): void {
305
305
  const healMod = heal.default || heal;
306
- healMod.recipes['explorbot-ai-healer'] = undefined;
306
+ Reflect.deleteProperty(healMod.recipes, 'explorbot-ai-healer');
307
307
  for (const name of Object.keys(this.rerunnerConfig.recipes || {})) {
308
- healMod.recipes[name] = undefined;
308
+ Reflect.deleteProperty(healMod.recipes, name);
309
309
  }
310
310
  }
311
311
 
@@ -432,7 +432,7 @@ export class Rerunner extends TaskAgent implements Agent {
432
432
  throw new Error(`Could not heal: ${failedCode}`);
433
433
  }
434
434
 
435
- this.healedSteps.push({ test: '', original: failedCode, healed: healedCommand });
435
+ this.healedSteps.push({ original: failedCode, healed: healedCommand });
436
436
  console.log(chalk.green(` ${figureSet.tick} Healed: ${healedCommand}`));
437
437
  };
438
438
  }
@@ -477,11 +477,7 @@ export class Rerunner extends TaskAgent implements Agent {
477
477
  const state = this.explorer.getStateManager().getCurrentState();
478
478
  const actionResult = state ? ActionResult.fromState(state) : null;
479
479
 
480
- const headings: string[] = [];
481
- if (state?.h1) headings.push(`H1: ${state.h1}`);
482
- if (state?.h2) headings.push(`H2: ${state.h2}`);
483
- if (state?.h3) headings.push(`H3: ${state.h3}`);
484
- if (state?.h4) headings.push(`H4: ${state.h4}`);
480
+ const headings = formatHeadings(state || {});
485
481
 
486
482
  return dedent`
487
483
  A test step failed and needs healing.
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
2
2
  import { join } from 'node:path';
3
3
  import { Worker } from 'node:worker_threads';
4
4
  import { outputPath } from '../../config.ts';
5
+ import { TTLCache } from '../../utils/cache.ts';
5
6
  import { computeHtmlFingerprint } from '../../utils/html-diff.ts';
6
7
  import { debugLog } from './mixin.ts';
7
8
 
@@ -10,8 +11,7 @@ const FINGERPRINT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
10
11
  const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
11
12
  const SIMILARITY_THRESHOLD = 90;
12
13
 
13
- const memoryCache: Record<string, string> = {};
14
- const memoryCacheTimestamps: Record<string, number> = {};
14
+ const memoryCache = new TTLCache<string>(CACHE_TTL_MS);
15
15
 
16
16
  let fingerprintWorker: Worker | null = null;
17
17
 
@@ -28,25 +28,20 @@ function getFingerprintWorker(): Worker {
28
28
  }
29
29
 
30
30
  export function clearResearchCache(): void {
31
- for (const key of Object.keys(memoryCache)) delete memoryCache[key];
32
- for (const key of Object.keys(memoryCacheTimestamps)) delete memoryCacheTimestamps[key];
31
+ memoryCache.clear();
33
32
  }
34
33
 
35
34
  export function getCachedResearch(hash: string): string {
36
35
  if (!hash) return '';
37
- const now = Date.now();
38
- const timestamp = memoryCacheTimestamps[hash];
39
- if (timestamp && now - timestamp <= CACHE_TTL_MS) {
40
- return memoryCache[hash] || '';
41
- }
36
+ const cached = memoryCache.get(hash);
37
+ if (cached !== undefined) return cached;
42
38
  const researchFile = outputPath('research', `${hash}.md`);
43
39
  if (!existsSync(researchFile)) return '';
44
40
  const stats = statSync(researchFile);
45
- if (now - stats.mtimeMs > CACHE_TTL_MS) return '';
46
- const cached = readFileSync(researchFile, 'utf8');
47
- memoryCache[hash] = cached;
48
- memoryCacheTimestamps[hash] = now;
49
- return cached;
41
+ if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) return '';
42
+ const fromDisk = readFileSync(researchFile, 'utf8');
43
+ memoryCache.set(hash, fromDisk);
44
+ return fromDisk;
50
45
  }
51
46
 
52
47
  export function getPreviousResearch(hash: string): string {
@@ -61,8 +56,7 @@ export function saveResearch(hash: string, text: string, combinedHtml?: string):
61
56
  const researchFile = join(researchDir, `${hash}.md`);
62
57
  if (!existsSync(researchDir)) mkdirSync(researchDir, { recursive: true });
63
58
  writeFileSync(researchFile, text);
64
- memoryCache[hash] = text;
65
- memoryCacheTimestamps[hash] = Date.now();
59
+ memoryCache.set(hash, text);
66
60
  debugLog(`Research saved to ${researchFile}`);
67
61
 
68
62
  if (combinedHtml) {