@lincoln504/pi-research 0.1.13 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -138
  3. package/dist/cli.mjs +23375 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +130 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +211 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +122 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +66 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1195 -0
  30. package/src/config.ts +1032 -155
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +258 -11
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -1,3 +1,4 @@
1
+ import { safeUnref } from '../utils/safe-unref.ts';
1
2
  /**
2
3
  * API Retry and Timeout Utilities
3
4
  *
@@ -8,6 +9,7 @@
8
9
  */
9
10
 
10
11
  import { logger } from '../logger.ts';
12
+ import { metrics } from '../utils/metrics.ts';
11
13
 
12
14
  // ============================================================================
13
15
  // Type Definitions
@@ -30,6 +32,34 @@ export interface RetryOptions {
30
32
  readonly maxDelay: number;
31
33
  readonly label?: string;
32
34
  readonly isTransientError?: (error: unknown) => boolean;
35
+ // When provided, aborting it short-circuits the inter-attempt backoff wait and
36
+ // stops further retries, so a caller cancel isn't stuck behind a long sleep.
37
+ readonly signal?: AbortSignal;
38
+ }
39
+
40
+ /**
41
+ * Sleep for `ms`, resolving early (rejecting with an AbortError) if `signal`
42
+ * aborts. The timer is unref'd so it never keeps the event loop alive.
43
+ */
44
+ export function abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {
45
+ return new Promise<void>((resolve, reject) => {
46
+ if (signal?.aborted) {
47
+ reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }));
48
+ return;
49
+ }
50
+ const timeoutId = setTimeout(() => {
51
+ if (signal && onAbort) signal.removeEventListener('abort', onAbort);
52
+ resolve();
53
+ }, ms);
54
+ (timeoutId as TimeoutHandle).unref?.();
55
+ const onAbort = signal
56
+ ? () => {
57
+ clearTimeout(timeoutId as unknown as ReturnType<typeof setTimeout>);
58
+ reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }));
59
+ }
60
+ : undefined;
61
+ if (signal && onAbort) signal.addEventListener('abort', onAbort, { once: true });
62
+ });
33
63
  }
34
64
 
35
65
  // ============================================================================
@@ -45,25 +75,30 @@ export function createTimeoutSignal(timeoutMs: number, signal?: AbortSignal): Ab
45
75
  if (typeof AbortSignal.timeout === 'function') {
46
76
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
47
77
  if (signal && typeof AbortSignal.any === 'function') {
48
- return AbortSignal.any([signal, timeoutSignal]);
78
+ try {
79
+ return AbortSignal.any([signal, timeoutSignal]);
80
+ } catch (e) {
81
+ // Fallback if AbortSignal.any fails or is not quite right
82
+ logger.warn(`[createTimeoutSignal] AbortSignal.any failed, falling back to manual combination: ${e}`);
83
+ }
49
84
  }
50
- return timeoutSignal;
85
+ if (!signal) return timeoutSignal;
51
86
  }
52
87
 
53
- // Fallback for older Node.js versions
88
+ // Fallback for older Node.js versions or if signal combination is needed manually
54
89
  const controller = new AbortController();
90
+ const combinedController = signal ? new AbortController() : controller;
91
+
55
92
  const timeoutId = setTimeout(() => {
56
- controller.abort();
93
+ combinedController.abort('timeout');
57
94
  }, timeoutMs);
58
95
 
59
96
  // Unref so it doesn't keep the process alive
60
97
  if (typeof timeoutId.unref === 'function') {
61
- timeoutId.unref();
98
+ safeUnref(timeoutId);
62
99
  }
63
100
 
64
101
  if (signal) {
65
- // Combine signals manually for older versions
66
- const combinedController = new AbortController();
67
102
  const abortHandler = () => {
68
103
  clearTimeout(timeoutId);
69
104
  combinedController.abort();
@@ -71,14 +106,19 @@ export function createTimeoutSignal(timeoutMs: number, signal?: AbortSignal): Ab
71
106
 
72
107
  signal.addEventListener('abort', abortHandler, { once: true });
73
108
 
74
- return combinedController.signal;
109
+ // Also clean up signal listener if timeout fires first
110
+ combinedController.signal.addEventListener('abort', () => {
111
+ clearTimeout(timeoutId);
112
+ signal.removeEventListener('abort', abortHandler);
113
+ }, { once: true });
75
114
  }
76
115
 
77
- return controller.signal;
116
+ return combinedController.signal;
78
117
  }
79
118
 
80
119
  /**
81
- * Wraps a promise with a timeout and abort signal support
120
+ * Wraps a promise with a timeout and abort signal support.
121
+ * Ensures cleanup of event listeners on resolution/rejection.
82
122
  */
83
123
  export function withTimeout<T>(
84
124
  promise: Promise<T>,
@@ -86,53 +126,53 @@ export function withTimeout<T>(
86
126
  label: string,
87
127
  signal?: AbortSignal
88
128
  ): Promise<T> {
89
- const timeoutController = new AbortController();
90
- const timeoutSignal = timeoutController.signal;
91
- const combinedSignal = signal
92
- ? createTimeoutSignal(timeoutMs, signal)
93
- : timeoutSignal;
94
-
95
- logger.log(`[withTimeout] Starting ${label} with timeout ${timeoutMs}ms. External signal aborted: ${signal?.aborted ?? 'no signal'}`);
129
+ const combinedSignal = createTimeoutSignal(timeoutMs, signal);
96
130
 
97
- let raceWon = false;
98
- let timeoutId: NodeJS.Timeout | undefined;
131
+ logger.debug(`[withTimeout] Starting ${timeoutMs}ms timeout guard (external signal: ${signal?.aborted ?? 'no signal'})`);
99
132
 
100
- const wrappedPromise = promise.then((val) => {
101
- raceWon = true;
102
- if (timeoutId) clearTimeout(timeoutId);
103
- return val;
104
- });
133
+ return new Promise<T>((resolve, reject) => {
134
+ // Early abort check: if signal is already aborted, reject immediately. Already
135
+ // aborted before we start means an external (user) cancellation, not a timeout —
136
+ // a clean stop, so log at debug, not error.
137
+ if (combinedSignal.aborted) {
138
+ logger.debug(`[withTimeout] ${label} already aborted at start (external cancellation)`);
139
+ return reject(new Error(`${label} cancelled or timed out`));
140
+ }
105
141
 
106
- const rejectPromise = new Promise<T>((_, reject) => {
107
- // Timeout handler
108
- timeoutId = setTimeout(() => {
109
- logger.error(`[withTimeout] ${label} TIMEOUT after ${timeoutMs}ms`);
110
- timeoutController.abort();
111
- reject(new Error(`${label} timeout after ${timeoutMs}ms`));
112
- }, timeoutMs);
142
+ let settled = false;
143
+ const abortHandler = () => {
144
+ if (!settled) {
145
+ settled = true;
146
+ // Distinguish a user/external cancellation (clean — debug) from a genuine
147
+ // timeout (worth a warning). The reject text keeps both words so downstream
148
+ // transient-classification on "timed out" is unaffected.
149
+ if (signal?.aborted) {
150
+ logger.debug(`[withTimeout] ${label} cancelled (external signal)`);
151
+ } else {
152
+ logger.warn(`[withTimeout] ${label} timed out after ${timeoutMs}ms`);
153
+ }
154
+ reject(new Error(`${label} cancelled or timed out`));
155
+ }
156
+ };
113
157
 
114
- // Unref so it doesn't keep the process alive
115
- (timeoutId as TimeoutHandle).unref?.();
158
+ combinedSignal.addEventListener('abort', abortHandler, { once: true });
116
159
 
117
- if (combinedSignal.aborted) {
118
- logger.error(`[withTimeout] ${label} ALREADY ABORTED at start`);
119
- if (timeoutId) clearTimeout(timeoutId);
120
- reject(new Error(`${label} cancelled`));
121
- } else {
122
- combinedSignal.addEventListener('abort', () => {
123
- if (!raceWon) {
124
- logger.error(`[withTimeout] ${label} ABORT SIGNAL FIRED`);
125
- if (timeoutId) clearTimeout(timeoutId);
126
- reject(new Error(`${label} cancelled`));
160
+ promise.then(
161
+ (val) => {
162
+ if (!settled) {
163
+ settled = true;
164
+ combinedSignal.removeEventListener('abort', abortHandler);
165
+ resolve(val);
127
166
  }
128
- }, { once: true });
129
- }
130
- });
131
-
132
- return Promise.race([wrappedPromise, rejectPromise]).finally(() => {
133
- // Clean up on completion or error
134
- if (timeoutId) clearTimeout(timeoutId);
135
- timeoutController.abort();
167
+ },
168
+ (err) => {
169
+ if (!settled) {
170
+ settled = true;
171
+ combinedSignal.removeEventListener('abort', abortHandler);
172
+ reject(err);
173
+ }
174
+ }
175
+ );
136
176
  });
137
177
  }
138
178
 
@@ -141,34 +181,42 @@ export function withTimeout<T>(
141
181
  // ============================================================================
142
182
 
143
183
  /**
144
- * Default transient error detector
184
+ * True if a single error MESSAGE carries a transient signal. Split out so the same
185
+ * matching is applied both to the top-level error and to each link of a `cause`
186
+ * chain (undici nests the real socket reason under `error.cause`).
145
187
  */
146
- export function isTransientError(error: unknown): boolean {
147
- if (!(error instanceof Error)) {
148
- return false;
149
- }
150
-
151
- const message = error.message.toLowerCase();
188
+ function messageIsTransient(message: string): boolean {
189
+ const m = message.toLowerCase();
152
190
 
153
- // Network errors
154
- if (message.includes('econnrefused') || message.includes('enotfound') || message.includes('timeout') || message.includes('etimedout')) {
191
+ // Network / socket-layer transport failures, including undici's mid-stream aborts:
192
+ // a dropped streaming response surfaces as `TypeError: terminated` (cause
193
+ // "other side closed" / UND_ERR_SOCKET / ECONNRESET). "timed out" is NOT included
194
+ // here on purpose — the generic classifier keeps "timeout"/"etimedout"; the LLM
195
+ // wrapper's "cancelled or timed out" wording is handled by the planning-service
196
+ // classifier, so a security/youtube caller's abort string isn't reinterpreted.
197
+ if (
198
+ m.includes('econnrefused') || m.includes('enotfound') || m.includes('timeout') ||
199
+ m.includes('etimedout') || m.includes('econnreset') || m.includes('fetch failed') ||
200
+ m.includes('terminated') || m.includes('other side closed') ||
201
+ m.includes('socket hang up') || m.includes('und_err')
202
+ ) {
155
203
  return true;
156
204
  }
157
205
 
158
- // Rate limiting
159
- if (message.includes('429') || message.includes('rate') || message.includes('quota')) {
206
+ // Rate limiting. Anchor on word boundaries so we don't treat "generate",
207
+ // "moderate" (contain "rate") or "429" embedded in a larger number as
208
+ // transient — matching the StackExchange client's boundary-anchored guard.
209
+ if (/\b429\b/.test(m) || /\brate\b/.test(m) || m.includes('quota')) {
160
210
  return true;
161
211
  }
162
212
 
163
- // Temporary service unavailability or server errors
213
+ // Temporary service unavailability, provider overload, or 5xx server errors.
214
+ // \b5\d\d\b matches a standalone 5xx code without matching a substring of e.g. "50000".
164
215
  if (
165
- message.includes('503') ||
166
- message.includes('500') ||
167
- message.includes('502') ||
168
- message.includes('504') ||
169
- message.includes('temporarily') ||
170
- message.includes('unavailable') ||
171
- message.includes('http 5')
216
+ /\b5\d\d\b/.test(m) ||
217
+ m.includes('temporarily') ||
218
+ m.includes('unavailable') ||
219
+ m.includes('overloaded')
172
220
  ) {
173
221
  return true;
174
222
  }
@@ -176,6 +224,34 @@ export function isTransientError(error: unknown): boolean {
176
224
  return false;
177
225
  }
178
226
 
227
+ /**
228
+ * Default transient error detector. Inspects the error message AND walks the
229
+ * `cause` chain (bounded) plus any Node/undici error `code`, so a wrapped transport
230
+ * failure (undici nests the socket reason under `cause`) is still recognized on the
231
+ * direct-fetch paths where that chain survives.
232
+ */
233
+ export function isTransientError(error: unknown): boolean {
234
+ // Reading .message/.cause/.code are plain property accesses on normal Error/undici objects,
235
+ // but guard against a pathological throwing getter so classification can never crash the
236
+ // retry loop — fail safe toward "not transient" (i.e. fail fast) if inspection throws.
237
+ try {
238
+ let current: unknown = error;
239
+ for (let depth = 0; current instanceof Error && depth < 5; depth++) {
240
+ if (messageIsTransient(current.message)) {
241
+ return true;
242
+ }
243
+ const code = (current as { code?: unknown }).code;
244
+ if (typeof code === 'string' && (/^und_err/i.test(code) || code === 'ECONNRESET' || code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'ENOTFOUND')) {
245
+ return true;
246
+ }
247
+ current = (current as { cause?: unknown }).cause;
248
+ }
249
+ } catch {
250
+ return false;
251
+ }
252
+ return false;
253
+ }
254
+
179
255
  /**
180
256
  * Retry helper with exponential backoff and jitter for transient errors.
181
257
  *
@@ -188,7 +264,7 @@ export async function retryWithBackoff<T>(
188
264
  fn: () => Promise<T>,
189
265
  options: Partial<RetryOptions> = {},
190
266
  ): Promise<T> {
191
- const opts: Required<RetryOptions> = {
267
+ const opts: Required<Omit<RetryOptions, 'signal'>> & { signal?: AbortSignal } = {
192
268
  maxRetries: 3,
193
269
  initialDelay: 1000,
194
270
  maxDelay: 10000,
@@ -197,16 +273,30 @@ export async function retryWithBackoff<T>(
197
273
  ...options,
198
274
  };
199
275
 
276
+ const startTime = Date.now();
200
277
  let lastError: Error | null = null;
201
278
  let attempt = 0;
279
+ metrics.increment('retry_attempts_total', 1, { label: opts.label });
202
280
 
203
281
  while (attempt <= opts.maxRetries) {
204
282
  try {
205
- return await fn();
283
+ const result = await fn();
284
+ const duration = Date.now() - startTime;
285
+ metrics.observe('retry_duration_ms', duration, { label: opts.label, status: 'success' });
286
+ if (attempt > 0) {
287
+ metrics.increment('retry_successful_after_retries_total', 1, { attempt: String(attempt), label: opts.label });
288
+ }
289
+ return result;
206
290
  } catch (error: unknown) {
207
291
  lastError = error instanceof Error ? error : new Error(String(error));
208
292
 
209
293
  if (!opts.isTransientError(lastError) || attempt === opts.maxRetries) {
294
+ const duration = Date.now() - startTime;
295
+ metrics.observe('retry_duration_ms', duration, { label: opts.label, status: 'exhausted' });
296
+ metrics.increment('retry_exhausted_total', 1, { label: opts.label });
297
+ if (!opts.isTransientError(lastError)) {
298
+ metrics.increment('retry_non_transient_errors_total', 1, { label: opts.label });
299
+ }
210
300
  throw lastError;
211
301
  }
212
302
 
@@ -215,13 +305,19 @@ export async function retryWithBackoff<T>(
215
305
  const jitter = Math.random() * 0.5 * baseDelay; // ±50% jitter
216
306
  const delay = Math.min(baseDelay + jitter, opts.maxDelay);
217
307
 
308
+ metrics.increment('retry_backoff_total', 1, { attempt: String(attempt), label: opts.label });
309
+ metrics.observe('retry_backoff_delay_ms', delay, { attempt: String(attempt), label: opts.label });
218
310
  logger.warn(`[Retry] ${opts.label} failed (attempt ${attempt + 1}/${opts.maxRetries + 1}), retrying in ${Math.round(delay)}ms: ${lastError.message}`);
219
311
 
220
- // Wait before retrying
221
- await new Promise<void>(resolve => {
222
- const timeoutId = setTimeout(resolve, delay);
223
- (timeoutId as TimeoutHandle).unref?.();
224
- });
312
+ // Wait before retrying — abortable so a caller cancel doesn't sit through
313
+ // the full backoff. If the wait is aborted, surface the original transient
314
+ // error (not the synthetic AbortError) so callers see the real cause.
315
+ try {
316
+ await abortableDelay(delay, opts.signal);
317
+ } catch {
318
+ metrics.increment('retry_aborted_total', 1, { label: opts.label });
319
+ throw lastError;
320
+ }
225
321
 
226
322
  attempt++;
227
323
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Scraper Types
3
+ */
4
+
5
+ import { USER_AGENTS } from '../utils/user-agent.ts';
6
+
7
+ export type FetchType = (
8
+ _input: string | Request,
9
+ _init?: RequestInit,
10
+ ) => Promise<Response>;
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
13
+ declare const fetch: FetchType;
14
+
15
+ // Size limits to prevent OOM attacks
16
+ export const MAX_HTML_SIZE = 25 * 1024 * 1024; // 25MB
17
+ export const MAX_PDF_SIZE = 100 * 1024 * 1024; // 100MB
18
+
19
+ // SSRF protection patterns
20
+ export const INTERNAL_NETWORK_PATTERNS: ReadonlyArray<RegExp> = [
21
+ /^127\./, // IPv4 loopback
22
+ /^0\./, // IPv4 "this" network
23
+ /^::1$/, // IPv6 loopback
24
+ /^fe80::/i, // IPv6 link-local
25
+ /^f[cd][0-9a-f]{2}:/i, // IPv6 unique local — full fc00::/7 block (fc00::–fdff::)
26
+ /^169\.254\./, // IPv4 link-local (and metadata)
27
+ /^10\./, // RFC 1918 Class A private
28
+ /^192\.168\./, // RFC 1918 Class C private
29
+ /^172\.(1[6-9]|2[0-9]|3[01])\./, // RFC 1918 Class B private
30
+ /^::ffff:(127\.|0\.|169\.254\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/i, // IPv4-mapped IPv6
31
+ ];
32
+
33
+ // USER_AGENTS re-exported from utils/user-agent.ts for convenience
34
+ export { USER_AGENTS };
35
+
36
+ export const FILTERED_TAGS = [
37
+ 'nav', 'header', 'footer', 'aside',
38
+ 'script', 'style', 'noscript',
39
+ 'form', 'input', 'select', 'textarea', 'button',
40
+ 'object', 'embed',
41
+ 'svg', 'symbol', 'use', 'defs', 'path', 'circle', 'rect', 'line', 'polygon',
42
+ 'img', 'iframe',
43
+ ] as const;
44
+
45
+ /** Bot detection patterns used by scrapers and browser workers */
46
+ export const BOT_PATTERNS: ReadonlyArray<[string, string]> = [
47
+ ['_cf_chl_', 'Cloudflare challenge'],
48
+ ['cf_chl_opt', 'Cloudflare challenge opt'],
49
+ ['cdn-cgi/challenge-platform', 'Cloudflare challenge platform'],
50
+ ['ddos-guard', 'DDoS-Guard challenge'],
51
+ ['Just a moment...', 'Cloudflare interstitial'],
52
+ ['Checking your browser before accessing', 'Cloudflare interstitial'],
53
+ ];
54
+
55
+ export const IMAGE_LINK_PATTERN = /\[([^\]]*)\]\((data:image\/[^)]+|[^)\s]+\.(?:svg|png|jpe?g|gif|webp|bmp|ico)(?:\?[^)]*)?)\)/gi;
56
+ export const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((?:data:image\/[^)]+|[^)\s]+)\)/gi;
57
+
58
+ export type { ScrapeLayerResult } from './types.ts';
59
+
60
+ export interface NativeHtmlToMarkdownModule {
61
+ convert(
62
+ html: string,
63
+ options?: {
64
+ headingStyle?: unknown;
65
+ codeBlockStyle?: unknown;
66
+ wrap?: boolean;
67
+ } | null,
68
+ visitor?: object | null,
69
+ ): { content?: string };
70
+ HeadingStyle: { Atx: unknown };
71
+ CodeBlockStyle: { Backticks: unknown };
72
+ }