@lincoln504/pi-research 0.1.12 → 1.0.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.
- package/LICENSE +1 -1
- package/README.md +64 -115
- package/dist/cli.mjs +23211 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1044 -0
- package/src/config.ts +1036 -147
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +264 -7
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
98
|
-
let timeoutId: NodeJS.Timeout | undefined;
|
|
131
|
+
logger.debug(`[withTimeout] Starting ${timeoutMs}ms timeout guard (external signal: ${signal?.aborted ?? 'no signal'})`);
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
(timeoutId as TimeoutHandle).unref?.();
|
|
158
|
+
combinedSignal.addEventListener('abort', abortHandler, { once: true });
|
|
116
159
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|