@lincoln504/pi-research 1.6.5 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/agent-skill/pi-research/README.md +5 -4
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli.mjs +616 -153
- package/dist/pdf-extract-worker.mjs +1289 -0
- package/dist/prompts/research-tool-usage.md +2 -1
- package/dist/prompts/researcher.md +1 -1
- package/dist/prompts/system-coordinator.md +2 -2
- package/dist/prompts/system-lead-router.md +1 -1
- package/docs/AGENT-SKILL.md +6 -6
- package/docs/ARCHITECTURE.md +24 -22
- package/docs/CHANGELOG.md +73 -0
- package/docs/CONFIGURATION.md +3 -2
- package/docs/SDK.md +7 -12
- package/package.json +8 -5
- package/scripts/build.cjs +35 -0
- package/scripts/setup.cjs +12 -10
- package/src/cleanup/research-cleanup.ts +11 -5
- package/src/cli.ts +7 -5
- package/src/config.ts +9 -3
- package/src/core/interfaces/knowledge-interfaces.ts +8 -0
- package/src/core/interfaces/orchestration-interfaces.ts +1 -1
- package/src/core/interfaces/research-plan-types.ts +6 -0
- package/src/core/llm/agentic-repair.ts +40 -3
- package/src/core/llm/llm-utils.ts +93 -5
- package/src/core/llm/model-registry-factory.ts +2 -15
- package/src/core/pi-version.ts +1 -1
- package/src/core/planning-service.ts +110 -15
- package/src/core/planning-utils.ts +3 -0
- package/src/core/service-registry.ts +8 -6
- package/src/index.ts +8 -5
- package/src/infrastructure/browser/config.ts +6 -7
- package/src/infrastructure/browser/ensure-browser.ts +14 -7
- package/src/infrastructure/browser/thread-worker-browser.ts +18 -19
- package/src/knowledge/chunker.ts +22 -4
- package/src/knowledge/embedder-init.ts +41 -1
- package/src/knowledge/embedder-utils.ts +7 -1
- package/src/knowledge/store.ts +68 -1
- package/src/knowledge/transformers-loader.ts +58 -1
- package/src/observers/research-observer-impl.ts +4 -1
- package/src/orchestration/deep-research-orchestrator.ts +3 -3
- package/src/orchestration/headless-observer.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +17 -3
- package/src/orchestration/research-orchestration-service.ts +27 -4
- package/src/orchestration/session-state.ts +36 -7
- package/src/prompts/research-tool-usage.md +2 -1
- package/src/prompts/researcher.md +1 -1
- package/src/prompts/system-coordinator.md +2 -2
- package/src/prompts/system-lead-router.md +1 -1
- package/src/sdk.ts +12 -4
- package/src/tools/research-knowledge-search.ts +23 -7
- package/src/tools/research-tool-definition.ts +1 -1
- package/src/tools/scrape.ts +12 -9
- package/src/tools/search.ts +12 -6
- package/src/tools/security.ts +6 -1
- package/src/tools/stackexchange.ts +6 -1
- package/src/utils/cancellation.ts +34 -1
- package/src/utils/llm-usage.ts +8 -0
- package/src/utils/log-utils.ts +1 -1
- package/src/web-research/browser-search.ts +6 -5
- package/src/web-research/pdf-extract-worker.mjs +1289 -0
- package/src/web-research/pdf-extract-worker.ts +42 -0
- package/src/web-research/pdf-extraction.ts +392 -0
- package/src/web-research/web-scraper.ts +3 -111
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
// src/web-research/pdf-extract-worker.ts
|
|
2
|
+
import { parentPort } from "node:worker_threads";
|
|
3
|
+
|
|
4
|
+
// src/web-research/pdf-extraction.ts
|
|
5
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
// src/logger.ts
|
|
9
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
10
|
+
import { appendFile } from "node:fs/promises";
|
|
11
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
12
|
+
import * as path3 from "node:path";
|
|
13
|
+
|
|
14
|
+
// src/utils/error-tracker.ts
|
|
15
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
|
+
var trackerStorage = new AsyncLocalStorage();
|
|
17
|
+
var ErrorTracker = class {
|
|
18
|
+
patterns = /* @__PURE__ */ new Map();
|
|
19
|
+
MAX_CONTEXTS_PER_PATTERN = 10;
|
|
20
|
+
// Bound total memory. The global singleton is never clear()'d, so out-of-run errors (logger,
|
|
21
|
+
// scrapers, healthcheck) would otherwise grow these maps without limit over a long-lived host.
|
|
22
|
+
MAX_PATTERNS = 500;
|
|
23
|
+
MAX_DOMAINS_PER_PATTERN = 100;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize an error message to extract a stable signature.
|
|
26
|
+
* Removes UUIDs, numbers (except HTTP status codes), URLs, and normalizes whitespace.
|
|
27
|
+
*
|
|
28
|
+
* @param message - The error message to normalize
|
|
29
|
+
* @returns A normalized signature for grouping similar errors
|
|
30
|
+
*/
|
|
31
|
+
extractSignature(message) {
|
|
32
|
+
return message.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<UUID>").replace(/\b(?![12]\d{2}\b|[3-5]\d{2}\b)\d+\b/g, "<NUM>").replace(/https?:\/\/[^\s]+/g, "<URL>").replace(/\s+/g, " ").trim();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Track an error with optional context.
|
|
36
|
+
* Groups similar errors by normalized signature and maintains a rolling buffer of contexts.
|
|
37
|
+
*
|
|
38
|
+
* @param error - The error to track (Error object, string message, or unknown)
|
|
39
|
+
* @param context - Optional context information about where the error occurred
|
|
40
|
+
*/
|
|
41
|
+
trackError(error, context = {}) {
|
|
42
|
+
let message;
|
|
43
|
+
if (error instanceof Error) {
|
|
44
|
+
message = error.message;
|
|
45
|
+
} else if (typeof error === "string") {
|
|
46
|
+
message = error;
|
|
47
|
+
} else {
|
|
48
|
+
message = String(error);
|
|
49
|
+
}
|
|
50
|
+
const signature = this.extractSignature(message);
|
|
51
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
52
|
+
let pattern = this.patterns.get(signature);
|
|
53
|
+
if (!pattern) {
|
|
54
|
+
if (this.patterns.size >= this.MAX_PATTERNS) {
|
|
55
|
+
this.evictLeastFrequentPattern();
|
|
56
|
+
}
|
|
57
|
+
pattern = {
|
|
58
|
+
signature,
|
|
59
|
+
message,
|
|
60
|
+
count: 0,
|
|
61
|
+
firstSeen: now,
|
|
62
|
+
lastSeen: now,
|
|
63
|
+
contexts: [],
|
|
64
|
+
domainCounts: /* @__PURE__ */ new Map()
|
|
65
|
+
};
|
|
66
|
+
this.patterns.set(signature, pattern);
|
|
67
|
+
}
|
|
68
|
+
pattern.count++;
|
|
69
|
+
pattern.lastSeen = now;
|
|
70
|
+
if (context.domain) {
|
|
71
|
+
const existing = pattern.domainCounts.get(context.domain);
|
|
72
|
+
if (existing !== void 0) {
|
|
73
|
+
pattern.domainCounts.set(context.domain, existing + 1);
|
|
74
|
+
} else if (pattern.domainCounts.size < this.MAX_DOMAINS_PER_PATTERN) {
|
|
75
|
+
pattern.domainCounts.set(context.domain, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
pattern.contexts.push(context);
|
|
79
|
+
if (pattern.contexts.length > this.MAX_CONTEXTS_PER_PATTERN) {
|
|
80
|
+
pattern.contexts.shift();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Generate a complete error report with sorted patterns and domain/type breakdowns.
|
|
85
|
+
*
|
|
86
|
+
* @returns An ErrorReport with aggregated error information
|
|
87
|
+
*/
|
|
88
|
+
getReport() {
|
|
89
|
+
const patterns = Array.from(this.patterns.values());
|
|
90
|
+
let totalErrors = 0;
|
|
91
|
+
for (const pattern of patterns) {
|
|
92
|
+
totalErrors += pattern.count;
|
|
93
|
+
}
|
|
94
|
+
patterns.sort((a, b) => b.count - a.count);
|
|
95
|
+
const byDomain = this.getErrorsByDomain();
|
|
96
|
+
const byType = this.getErrorsByType();
|
|
97
|
+
return {
|
|
98
|
+
totalErrors,
|
|
99
|
+
uniquePatterns: patterns.length,
|
|
100
|
+
patterns,
|
|
101
|
+
byDomain,
|
|
102
|
+
byType
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Group errors by domain from context information.
|
|
107
|
+
*
|
|
108
|
+
* @returns A Map of domain names to error counts
|
|
109
|
+
*/
|
|
110
|
+
getErrorsByDomain() {
|
|
111
|
+
const totalDomainCounts = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const pattern of this.patterns.values()) {
|
|
113
|
+
for (const [domain, count] of pattern.domainCounts.entries()) {
|
|
114
|
+
totalDomainCounts.set(domain, (totalDomainCounts.get(domain) ?? 0) + count);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return totalDomainCounts;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Group errors by type (first part of signature before colon or first few words).
|
|
121
|
+
*
|
|
122
|
+
* @returns A Map of error types to error counts
|
|
123
|
+
*/
|
|
124
|
+
getErrorsByType() {
|
|
125
|
+
const typeCounts = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const pattern of this.patterns.values()) {
|
|
127
|
+
const parts = pattern.signature.split(":");
|
|
128
|
+
let type = parts[0]?.trim() ?? "Unknown";
|
|
129
|
+
if (type === pattern.signature) {
|
|
130
|
+
const words = pattern.signature.split(" ");
|
|
131
|
+
const firstWords = words.slice(0, 3).filter((w) => w.length > 0);
|
|
132
|
+
type = firstWords.length > 0 ? firstWords.join(" ") : "Unknown";
|
|
133
|
+
}
|
|
134
|
+
typeCounts.set(type, (typeCounts.get(type) ?? 0) + pattern.count);
|
|
135
|
+
}
|
|
136
|
+
return typeCounts;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Evict the least-frequent pattern to keep the map bounded (see MAX_PATTERNS). O(n) but only
|
|
140
|
+
* runs when a new signature is added while already at the cap.
|
|
141
|
+
*/
|
|
142
|
+
evictLeastFrequentPattern() {
|
|
143
|
+
let victimKey;
|
|
144
|
+
let victimCount = Infinity;
|
|
145
|
+
for (const [key, p] of this.patterns) {
|
|
146
|
+
if (p.count < victimCount) {
|
|
147
|
+
victimCount = p.count;
|
|
148
|
+
victimKey = key;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (victimKey !== void 0) {
|
|
152
|
+
this.patterns.delete(victimKey);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Clear all tracked errors (called between research runs).
|
|
157
|
+
*/
|
|
158
|
+
clear() {
|
|
159
|
+
this.patterns.clear();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var globalInstance = new ErrorTracker();
|
|
163
|
+
var errorTracker = new Proxy(globalInstance, {
|
|
164
|
+
get(target, prop, receiver) {
|
|
165
|
+
const currentTracker = trackerStorage.getStore() ?? target;
|
|
166
|
+
const value = Reflect.get(currentTracker, prop, receiver);
|
|
167
|
+
if (typeof value === "function") {
|
|
168
|
+
return value.bind(currentTracker);
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// src/utils/log-utils.ts
|
|
175
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
176
|
+
import * as os from "node:os";
|
|
177
|
+
import * as path from "node:path";
|
|
178
|
+
var logContextStorage = new AsyncLocalStorage2();
|
|
179
|
+
function buildDefaultDebugLogPath() {
|
|
180
|
+
const override = process.env["PI_RESEARCH_LOG_PATH"];
|
|
181
|
+
if (override) return override;
|
|
182
|
+
return path.join(os.tmpdir(), "pi-research.log");
|
|
183
|
+
}
|
|
184
|
+
function isVerboseFromEnv() {
|
|
185
|
+
return process.env["PI_RESEARCH_DEBUG"] === "true";
|
|
186
|
+
}
|
|
187
|
+
function getLogContext() {
|
|
188
|
+
return logContextStorage.getStore() ?? {};
|
|
189
|
+
}
|
|
190
|
+
function safeJsonStringify(value) {
|
|
191
|
+
try {
|
|
192
|
+
return JSON.stringify(value, null, 0);
|
|
193
|
+
} catch {
|
|
194
|
+
return "[unserializable]";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function formatArg(arg) {
|
|
198
|
+
if (arg instanceof Error) {
|
|
199
|
+
return arg.stack ?? arg.message;
|
|
200
|
+
}
|
|
201
|
+
if (typeof arg === "object" && arg !== null) {
|
|
202
|
+
return safeJsonStringify(arg);
|
|
203
|
+
}
|
|
204
|
+
return String(arg);
|
|
205
|
+
}
|
|
206
|
+
var MAX_LOG_MESSAGE_LENGTH = 1e4;
|
|
207
|
+
var ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
208
|
+
var URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]{0,31}:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
|
|
209
|
+
var B_LEFT = "(?<![A-Za-z0-9])";
|
|
210
|
+
var B_RIGHT = "(?![A-Za-z0-9])";
|
|
211
|
+
var COOKIE_HEADER_PATTERN = new RegExp(
|
|
212
|
+
`${B_LEFT}(set[_-]?cookie|cookie)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\r\\n]+)`,
|
|
213
|
+
"gi"
|
|
214
|
+
);
|
|
215
|
+
var SENSITIVE_KV_PATTERN = new RegExp(
|
|
216
|
+
`${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
|
|
217
|
+
"gi"
|
|
218
|
+
);
|
|
219
|
+
var KNOWN_TOKEN_PATTERN = new RegExp(
|
|
220
|
+
`${B_LEFT}(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})${B_RIGHT}`,
|
|
221
|
+
"g"
|
|
222
|
+
);
|
|
223
|
+
var JWT_PATTERN = new RegExp(
|
|
224
|
+
`${B_LEFT}eyJ[A-Za-z0-9_-]{1,1024}\\.[A-Za-z0-9_-]{1,8192}\\.[A-Za-z0-9_-]{1,4096}`,
|
|
225
|
+
"g"
|
|
226
|
+
);
|
|
227
|
+
var BASIC_AUTH_PATTERN = new RegExp(`${B_LEFT}Basic\\s+[A-Za-z0-9+/]{16,}={0,2}`, "gi");
|
|
228
|
+
var BEARER_AUTH_PATTERN = new RegExp(`${B_LEFT}Bearer\\s+[A-Za-z0-9._~+/-]+=*`, "gi");
|
|
229
|
+
var PROVIDER_TOKEN_PATTERN = new RegExp(
|
|
230
|
+
`${B_LEFT}(AIza[0-9A-Za-z_-]{35}|ya29\\.[0-9A-Za-z_-]{20,}|[sp]k_(?:live|test)_[0-9A-Za-z]{16,})${B_RIGHT}`,
|
|
231
|
+
"g"
|
|
232
|
+
);
|
|
233
|
+
var LONG_HEX_SECRET_PATTERN = new RegExp(`${B_LEFT}[0-9a-fA-F]{32,}${B_RIGHT}`, "g");
|
|
234
|
+
var CONTROL_CHARS_PATTERN = /[\x00-\x1f\x7f]/g;
|
|
235
|
+
var REDACT_SCAN_LENGTH = MAX_LOG_MESSAGE_LENGTH * 4;
|
|
236
|
+
var TOKEN_CHAR_PATTERN = /[A-Za-z0-9_\-./+=]/;
|
|
237
|
+
var REDACT_SCAN_EXTENSION_MAX = 2048;
|
|
238
|
+
function redactSecrets(message) {
|
|
239
|
+
const rawLength = message.length;
|
|
240
|
+
let scanEnd = REDACT_SCAN_LENGTH;
|
|
241
|
+
if (rawLength > scanEnd) {
|
|
242
|
+
const extensionLimit = Math.min(rawLength, scanEnd + REDACT_SCAN_EXTENSION_MAX);
|
|
243
|
+
while (scanEnd < extensionLimit && TOKEN_CHAR_PATTERN.test(message.charAt(scanEnd))) {
|
|
244
|
+
scanEnd += 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
let out = rawLength > scanEnd ? message.slice(0, scanEnd) : message;
|
|
248
|
+
out = out.replace(ANSI_PATTERN, "");
|
|
249
|
+
out = out.replace(URL_CREDENTIALS_PATTERN, "$1[REDACTED]@");
|
|
250
|
+
out = out.replace(JWT_PATTERN, "[REDACTED]");
|
|
251
|
+
out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
|
|
252
|
+
out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
|
|
253
|
+
out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
|
|
254
|
+
out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
|
|
255
|
+
out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
|
|
256
|
+
out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
|
|
257
|
+
out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
|
|
258
|
+
let keep = MAX_LOG_MESSAGE_LENGTH;
|
|
259
|
+
if (out.length > keep) {
|
|
260
|
+
const lastKeptUnit = out.charCodeAt(keep - 1);
|
|
261
|
+
if (lastKeptUnit >= 55296 && lastKeptUnit <= 56319) keep -= 1;
|
|
262
|
+
}
|
|
263
|
+
if (out.length > MAX_LOG_MESSAGE_LENGTH) {
|
|
264
|
+
out = `${out.slice(0, keep)}\u2026[truncated ${Math.max(rawLength, out.length) - keep} chars]`;
|
|
265
|
+
} else if (rawLength > scanEnd) {
|
|
266
|
+
out = `${out}\u2026[truncated ${rawLength - scanEnd} chars]`;
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
var TERMINAL_ESCAPE_PATTERN = new RegExp(
|
|
271
|
+
"\\x1b\\[[0-?]*[ -/]*[@-~]?|\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)?|\\x1b[PX^_][^\\x1b]*(?:\\x1b\\\\)?|\\x1b.?",
|
|
272
|
+
"g"
|
|
273
|
+
);
|
|
274
|
+
function stripTerminalEscapes(message) {
|
|
275
|
+
return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/utils/log-rotation.ts
|
|
279
|
+
import * as fs from "node:fs";
|
|
280
|
+
import * as path2 from "node:path";
|
|
281
|
+
var APPEND_CREATE_NOFOLLOW_FLAGS = fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | (typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0);
|
|
282
|
+
function touchOwnerOnly(filePath, mode) {
|
|
283
|
+
let fd;
|
|
284
|
+
try {
|
|
285
|
+
fd = fs.openSync(filePath, APPEND_CREATE_NOFOLLOW_FLAGS, mode);
|
|
286
|
+
fs.fchmodSync(fd, mode);
|
|
287
|
+
} catch {
|
|
288
|
+
} finally {
|
|
289
|
+
if (fd !== void 0) {
|
|
290
|
+
try {
|
|
291
|
+
fs.closeSync(fd);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
var LogRotation = class {
|
|
298
|
+
MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
299
|
+
// 10MB max file size
|
|
300
|
+
MAX_LOG_FILES = 10;
|
|
301
|
+
// Keep last 10 archived logs
|
|
302
|
+
lastRotationCheck = 0;
|
|
303
|
+
logger;
|
|
304
|
+
constructor(logger2) {
|
|
305
|
+
this.logger = logger2;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Clear all research logs including archives.
|
|
309
|
+
*/
|
|
310
|
+
clearLogs(logFile, logDir) {
|
|
311
|
+
try {
|
|
312
|
+
if (fs.existsSync(logFile)) {
|
|
313
|
+
fs.unlinkSync(logFile);
|
|
314
|
+
touchOwnerOnly(logFile, 384);
|
|
315
|
+
}
|
|
316
|
+
const files = fs.readdirSync(logDir);
|
|
317
|
+
const baseName = path2.basename(logFile);
|
|
318
|
+
const archives = files.filter((f) => f.startsWith(baseName) && f !== baseName);
|
|
319
|
+
for (const archive of archives) {
|
|
320
|
+
try {
|
|
321
|
+
fs.unlinkSync(path2.join(logDir, archive));
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
this.logger.log("[Logger] All logs and archives cleared.");
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.logger.log("[Logger] Failed to clear logs:", err);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Rotate log files when they exceed MAX_LOG_SIZE.
|
|
332
|
+
* Archives are created with ISO timestamp suffix.
|
|
333
|
+
* Old archives beyond MAX_LOG_FILES are cleaned up.
|
|
334
|
+
*/
|
|
335
|
+
rotateLogFile(logFile, logDir) {
|
|
336
|
+
try {
|
|
337
|
+
const stats = fs.statSync(logFile);
|
|
338
|
+
const fileSize = stats.size;
|
|
339
|
+
if (fileSize <= this.MAX_LOG_SIZE) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
343
|
+
const archivePath = `${logFile}.${timestamp}`;
|
|
344
|
+
try {
|
|
345
|
+
fs.renameSync(logFile, archivePath);
|
|
346
|
+
} catch (renameErr) {
|
|
347
|
+
this.logger.log("[Logger] Log rotation skipped (rename failed; file likely in use):", renameErr);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
touchOwnerOnly(logFile, 384);
|
|
351
|
+
try {
|
|
352
|
+
const files = fs.readdirSync(logDir);
|
|
353
|
+
const logFiles = files.filter((f) => f.startsWith(path2.basename(logFile)) && f !== path2.basename(logFile)).sort();
|
|
354
|
+
const toDelete = logFiles.slice(0, -this.MAX_LOG_FILES);
|
|
355
|
+
for (const file of toDelete) {
|
|
356
|
+
try {
|
|
357
|
+
fs.unlinkSync(path2.join(logDir, file));
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
this.logger.log("[Logger] Rotated log file to:", archivePath);
|
|
364
|
+
} catch (_error) {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Check if rotation is needed and perform if necessary.
|
|
369
|
+
*
|
|
370
|
+
* Throttled to one filesystem stat per 60s. We deliberately do NOT offer a
|
|
371
|
+
* `force` bypass: a previous version forced a check on every WARN/ERROR, which
|
|
372
|
+
* turned into a synchronous fs.statSync per line (thousands per heavy run) and
|
|
373
|
+
* also reset the throttle so the timer path never fired. The 60s timer
|
|
374
|
+
* preserves all log data on rotation regardless of when it lands — a file may
|
|
375
|
+
* transiently exceed MAX_LOG_SIZE by at most one check interval, which is fine.
|
|
376
|
+
* @returns true if a rotation check was performed
|
|
377
|
+
*/
|
|
378
|
+
checkAndRotate(logFile, logDir) {
|
|
379
|
+
const now = Date.now();
|
|
380
|
+
if (now - this.lastRotationCheck > 6e4) {
|
|
381
|
+
this.rotateLogFile(logFile, logDir);
|
|
382
|
+
this.lastRotationCheck = now;
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/utils/disk-space-checker.ts
|
|
390
|
+
import * as fs2 from "node:fs";
|
|
391
|
+
var DiskSpaceChecker = class {
|
|
392
|
+
MIN_DISK_SPACE_BYTES = 1048576;
|
|
393
|
+
// 1MB minimum
|
|
394
|
+
DISK_SPACE_CHECK_INTERVAL_MS = 6e4;
|
|
395
|
+
// Check every 60 seconds
|
|
396
|
+
lastDiskSpaceCheck = 0;
|
|
397
|
+
hasDiskSpace = true;
|
|
398
|
+
/**
|
|
399
|
+
* Check if there is sufficient disk space for logging.
|
|
400
|
+
* Checks are throttled to avoid excessive filesystem operations.
|
|
401
|
+
*/
|
|
402
|
+
checkDiskSpace(logDir) {
|
|
403
|
+
const now = Date.now();
|
|
404
|
+
if (now - this.lastDiskSpaceCheck < this.DISK_SPACE_CHECK_INTERVAL_MS) {
|
|
405
|
+
return this.hasDiskSpace;
|
|
406
|
+
}
|
|
407
|
+
this.lastDiskSpaceCheck = now;
|
|
408
|
+
try {
|
|
409
|
+
const stats = fs2.statfsSync(logDir);
|
|
410
|
+
const availableBytes = stats.bavail * stats.bsize;
|
|
411
|
+
if (availableBytes < this.MIN_DISK_SPACE_BYTES) {
|
|
412
|
+
this.hasDiskSpace = false;
|
|
413
|
+
try {
|
|
414
|
+
process.stderr.write(
|
|
415
|
+
`[pi-research] Insufficient disk space for logging: ${Math.round(availableBytes / 1024 / 1024)}MB available, minimum ${this.MIN_DISK_SPACE_BYTES / 1024 / 1024}MB required
|
|
416
|
+
`
|
|
417
|
+
);
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
this.hasDiskSpace = true;
|
|
422
|
+
}
|
|
423
|
+
} catch (_error) {
|
|
424
|
+
this.hasDiskSpace = true;
|
|
425
|
+
}
|
|
426
|
+
return this.hasDiskSpace;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/utils/stdio-capture.ts
|
|
431
|
+
import { createRequire } from "node:module";
|
|
432
|
+
import { TextDecoder } from "node:util";
|
|
433
|
+
var fs3 = createRequire(import.meta.url)("node:fs");
|
|
434
|
+
var sessionCaptureStates = /* @__PURE__ */ new Map();
|
|
435
|
+
var unkeyedCaptureDepth = 0;
|
|
436
|
+
var isAnyLoggerCapturingOutput = false;
|
|
437
|
+
var captureStack = [];
|
|
438
|
+
function recomputeGlobalCapturing() {
|
|
439
|
+
isAnyLoggerCapturingOutput = unkeyedCaptureDepth > 0 || Array.from(sessionCaptureStates.values()).some((v) => v);
|
|
440
|
+
}
|
|
441
|
+
function isSessionCapturing(sessionId) {
|
|
442
|
+
if (sessionId) {
|
|
443
|
+
return sessionCaptureStates.get(sessionId) ?? false;
|
|
444
|
+
}
|
|
445
|
+
return isAnyLoggerCapturingOutput;
|
|
446
|
+
}
|
|
447
|
+
function setSessionCapturing(sessionId, capturing) {
|
|
448
|
+
sessionCaptureStates.set(sessionId, capturing);
|
|
449
|
+
if (capturing) {
|
|
450
|
+
isAnyLoggerCapturingOutput = true;
|
|
451
|
+
} else {
|
|
452
|
+
recomputeGlobalCapturing();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
var NATIVE_LOG_PATTERNS = [
|
|
456
|
+
"Warning:",
|
|
457
|
+
"Error:",
|
|
458
|
+
"Dawn",
|
|
459
|
+
"ONNX",
|
|
460
|
+
"ort",
|
|
461
|
+
"maxDynamicStorageBuffersPerPipelineLayout",
|
|
462
|
+
"maxComputeWorkgroupStorageSize",
|
|
463
|
+
"allocation limit",
|
|
464
|
+
"artificially",
|
|
465
|
+
"reduced from",
|
|
466
|
+
"dynamic offset allocation limit"
|
|
467
|
+
];
|
|
468
|
+
var COMPLEX_TUI_PATTERNS = [
|
|
469
|
+
"\x1B[H",
|
|
470
|
+
// Home
|
|
471
|
+
"\x1B[2J",
|
|
472
|
+
// Clear screen
|
|
473
|
+
"\x1B[J",
|
|
474
|
+
// Clear to end
|
|
475
|
+
"\x1B[K",
|
|
476
|
+
// Clear line
|
|
477
|
+
"\x1B[?25",
|
|
478
|
+
// Cursor visibility
|
|
479
|
+
"\x1B[?1000",
|
|
480
|
+
// Mouse tracking
|
|
481
|
+
"\x1B[?1002",
|
|
482
|
+
"\x1B[?1003",
|
|
483
|
+
"\x1B[?1006",
|
|
484
|
+
"\x1B[?2004",
|
|
485
|
+
// Bracketed paste
|
|
486
|
+
"\x1B[<u",
|
|
487
|
+
// Kitty protocol
|
|
488
|
+
"\x1B[>4;0m"
|
|
489
|
+
// xterm modifyOtherKeys
|
|
490
|
+
];
|
|
491
|
+
var BOX_DRAWING_CHARS = /[─│┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬╴╵╶╷╭╮╯╰╱╲╳]/;
|
|
492
|
+
function isComplexTui(message) {
|
|
493
|
+
if (!message.includes("\x1B[")) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
for (const pattern of COMPLEX_TUI_PATTERNS) {
|
|
497
|
+
if (message.includes(pattern)) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (/\u001b\[\d+;\d+H/.test(message) || /\u001b\[\d+[ABCD]/.test(message)) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
function isNativeLog(message) {
|
|
507
|
+
return NATIVE_LOG_PATTERNS.some((pattern) => message.includes(pattern));
|
|
508
|
+
}
|
|
509
|
+
function isBoxDrawing(message) {
|
|
510
|
+
return BOX_DRAWING_CHARS.test(message);
|
|
511
|
+
}
|
|
512
|
+
function shouldRedirectStderr(stat) {
|
|
513
|
+
return !stat.isFIFO() && !stat.isSocket();
|
|
514
|
+
}
|
|
515
|
+
function createConsolePatch(level, logFile, hasSufficientDiskSpace) {
|
|
516
|
+
return (...args) => {
|
|
517
|
+
if (!hasSufficientDiskSpace()) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
521
|
+
const entry = {
|
|
522
|
+
timestamp,
|
|
523
|
+
level: `CONSOLE_${level.toUpperCase()}`,
|
|
524
|
+
...getLogContext(),
|
|
525
|
+
// Redact secrets + bound size, matching Logger.emit(); captured console
|
|
526
|
+
// output can echo auth headers / tokens from dependencies.
|
|
527
|
+
message: redactSecrets(args.map(formatArg).join(" "))
|
|
528
|
+
};
|
|
529
|
+
try {
|
|
530
|
+
fs3.appendFileSync(logFile, `${safeJsonStringify(entry)}
|
|
531
|
+
`);
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
|
|
537
|
+
const sessionCapturing = sessionId ? isSessionCapturing(sessionId) : false;
|
|
538
|
+
if (!logFile || sessionCapturing || !sessionId && isAnyLoggerCapturingOutput) {
|
|
539
|
+
return await task();
|
|
540
|
+
}
|
|
541
|
+
if (sessionId) {
|
|
542
|
+
setSessionCapturing(sessionId, true);
|
|
543
|
+
} else {
|
|
544
|
+
unkeyedCaptureDepth++;
|
|
545
|
+
isAnyLoggerCapturingOutput = true;
|
|
546
|
+
}
|
|
547
|
+
const stderrDecoder = new TextDecoder();
|
|
548
|
+
const stdoutDecoder = new TextDecoder();
|
|
549
|
+
const fd1SyncDecoder = new TextDecoder();
|
|
550
|
+
const fd2SyncDecoder = new TextDecoder();
|
|
551
|
+
const originalStderrWrite = process.stderr.write;
|
|
552
|
+
const originalStdoutWrite = process.stdout.write;
|
|
553
|
+
const originalFsWriteSync = fs3.writeSync;
|
|
554
|
+
const originalConsole = {
|
|
555
|
+
log: console.log,
|
|
556
|
+
info: console.info,
|
|
557
|
+
warn: console.warn,
|
|
558
|
+
error: console.error,
|
|
559
|
+
debug: console.debug
|
|
560
|
+
};
|
|
561
|
+
let savedFd2 = -1;
|
|
562
|
+
let fd2Redirected = false;
|
|
563
|
+
if (process.platform !== "win32") {
|
|
564
|
+
try {
|
|
565
|
+
const stat = fs3.fstatSync(2);
|
|
566
|
+
if (shouldRedirectStderr(stat)) {
|
|
567
|
+
savedFd2 = fs3.openSync("/dev/fd/2", "a");
|
|
568
|
+
fs3.closeSync(2);
|
|
569
|
+
const newFd = fs3.openSync(logFile, "a", 384);
|
|
570
|
+
if (newFd === 2) {
|
|
571
|
+
fd2Redirected = true;
|
|
572
|
+
} else {
|
|
573
|
+
if (newFd >= 0) fs3.closeSync(newFd);
|
|
574
|
+
try {
|
|
575
|
+
const r = fs3.openSync(`/dev/fd/${savedFd2}`, "a");
|
|
576
|
+
if (r !== 2 && r >= 0) fs3.closeSync(r);
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
fs3.closeSync(savedFd2);
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
savedFd2 = -1;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
console.log = createConsolePatch("log", logFile, hasSufficientDiskSpace);
|
|
590
|
+
console.info = createConsolePatch("info", logFile, hasSufficientDiskSpace);
|
|
591
|
+
console.warn = createConsolePatch("warn", logFile, hasSufficientDiskSpace);
|
|
592
|
+
console.error = createConsolePatch("error", logFile, hasSufficientDiskSpace);
|
|
593
|
+
console.debug = createConsolePatch("debug", logFile, hasSufficientDiskSpace);
|
|
594
|
+
process.stderr.write = (chunk, encodingOrCb, callback) => {
|
|
595
|
+
const cb = typeof encodingOrCb === "function" ? encodingOrCb : callback;
|
|
596
|
+
const message = typeof chunk === "string" ? chunk : stderrDecoder.decode(chunk, { stream: true });
|
|
597
|
+
if (message.length === 0) {
|
|
598
|
+
if (typeof cb === "function") cb();
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
if (!hasSufficientDiskSpace()) {
|
|
602
|
+
if (typeof cb === "function") cb();
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
606
|
+
const entry = {
|
|
607
|
+
timestamp,
|
|
608
|
+
level: "STDERR",
|
|
609
|
+
...getLogContext(),
|
|
610
|
+
message: redactSecrets(message.trim())
|
|
611
|
+
};
|
|
612
|
+
try {
|
|
613
|
+
fs3.appendFileSync(logFile, `${safeJsonStringify(entry)}
|
|
614
|
+
`);
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
if (typeof cb === "function") cb();
|
|
618
|
+
return true;
|
|
619
|
+
};
|
|
620
|
+
process.stdout.write = (chunk, encodingOrCb, callback) => {
|
|
621
|
+
const message = typeof chunk === "string" ? chunk : stdoutDecoder.decode(chunk, { stream: true });
|
|
622
|
+
const isNative = isNativeLog(message);
|
|
623
|
+
const complexTui = isComplexTui(message);
|
|
624
|
+
const boxDrawing = isBoxDrawing(message);
|
|
625
|
+
const hasAnsi = message.includes("\x1B[");
|
|
626
|
+
if (isNative && !boxDrawing) {
|
|
627
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
628
|
+
const entry = {
|
|
629
|
+
timestamp,
|
|
630
|
+
level: "STDOUT_NATIVE",
|
|
631
|
+
...getLogContext(),
|
|
632
|
+
message: redactSecrets(message.trim())
|
|
633
|
+
};
|
|
634
|
+
try {
|
|
635
|
+
fs3.appendFileSync(logFile, `${safeJsonStringify(entry)}
|
|
636
|
+
`);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
const cb = typeof encodingOrCb === "function" ? encodingOrCb : callback;
|
|
640
|
+
if (typeof cb === "function") cb();
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
if (complexTui || boxDrawing) {
|
|
644
|
+
return originalStdoutWrite.call(process.stdout, chunk, encodingOrCb, callback);
|
|
645
|
+
}
|
|
646
|
+
if (!hasAnsi && message.trim().length > 0) {
|
|
647
|
+
if (!hasSufficientDiskSpace()) {
|
|
648
|
+
const cb2 = typeof encodingOrCb === "function" ? encodingOrCb : callback;
|
|
649
|
+
if (typeof cb2 === "function") cb2();
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
653
|
+
const entry = {
|
|
654
|
+
timestamp,
|
|
655
|
+
level: "STDOUT_PLAIN",
|
|
656
|
+
...getLogContext(),
|
|
657
|
+
message: redactSecrets(message.trim())
|
|
658
|
+
};
|
|
659
|
+
try {
|
|
660
|
+
fs3.appendFileSync(logFile, `${safeJsonStringify(entry)}
|
|
661
|
+
`);
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
const cb = typeof encodingOrCb === "function" ? encodingOrCb : callback;
|
|
665
|
+
if (typeof cb === "function") cb();
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
return originalStdoutWrite.call(process.stdout, chunk, encodingOrCb, callback);
|
|
669
|
+
};
|
|
670
|
+
try {
|
|
671
|
+
const descriptor = Object.getOwnPropertyDescriptor(fs3, "writeSync");
|
|
672
|
+
if (!descriptor || (descriptor.writable || descriptor.set)) {
|
|
673
|
+
fs3.writeSync = (fd, chunk, ...args) => {
|
|
674
|
+
if (fd === 1 || fd === 2) {
|
|
675
|
+
const fdDecoder = fd === 1 ? fd1SyncDecoder : fd2SyncDecoder;
|
|
676
|
+
const message = typeof chunk === "string" ? chunk : fdDecoder.decode(chunk, { stream: true });
|
|
677
|
+
if (fd === 2 && message.length === 0) {
|
|
678
|
+
return typeof chunk === "string" ? Buffer.from(chunk).length : chunk.length;
|
|
679
|
+
}
|
|
680
|
+
const isNative = isNativeLog(message);
|
|
681
|
+
const boxDrawing = isBoxDrawing(message);
|
|
682
|
+
const complexTui = isComplexTui(message);
|
|
683
|
+
const hasAnsi = message.includes("\x1B[");
|
|
684
|
+
const shouldDivert = fd === 2 || isNative && !boxDrawing && !complexTui || !hasAnsi && !boxDrawing && message.trim().length > 0;
|
|
685
|
+
if (shouldDivert) {
|
|
686
|
+
if (!hasSufficientDiskSpace()) {
|
|
687
|
+
return typeof chunk === "string" ? Buffer.from(chunk).length : chunk.length;
|
|
688
|
+
}
|
|
689
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
690
|
+
const entry = {
|
|
691
|
+
timestamp,
|
|
692
|
+
level: fd === 1 ? "FS_WRITE_SYNC_STDOUT" : "FS_WRITE_SYNC_STDERR",
|
|
693
|
+
...getLogContext(),
|
|
694
|
+
message: redactSecrets(message.trim())
|
|
695
|
+
};
|
|
696
|
+
try {
|
|
697
|
+
fs3.appendFileSync(logFile, `${safeJsonStringify(entry)}
|
|
698
|
+
`);
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
return typeof chunk === "string" ? Buffer.from(chunk).length : chunk.length;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return originalFsWriteSync.apply(fs3, [fd, chunk, ...args]);
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
} catch (_e) {
|
|
708
|
+
}
|
|
709
|
+
const frame = {
|
|
710
|
+
retired: false,
|
|
711
|
+
restore: () => {
|
|
712
|
+
process.stderr.write = originalStderrWrite;
|
|
713
|
+
process.stdout.write = originalStdoutWrite;
|
|
714
|
+
try {
|
|
715
|
+
fs3.writeSync = originalFsWriteSync;
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
console.log = originalConsole.log;
|
|
719
|
+
console.info = originalConsole.info;
|
|
720
|
+
console.warn = originalConsole.warn;
|
|
721
|
+
console.error = originalConsole.error;
|
|
722
|
+
console.debug = originalConsole.debug;
|
|
723
|
+
if (fd2Redirected && savedFd2 >= 0) {
|
|
724
|
+
try {
|
|
725
|
+
fs3.closeSync(2);
|
|
726
|
+
const r = fs3.openSync(`/dev/fd/${savedFd2}`, "a");
|
|
727
|
+
if (r !== 2 && r >= 0) {
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
fs3.closeSync(savedFd2);
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
captureStack.push(frame);
|
|
739
|
+
try {
|
|
740
|
+
return await task();
|
|
741
|
+
} finally {
|
|
742
|
+
if (sessionId) {
|
|
743
|
+
setSessionCapturing(sessionId, false);
|
|
744
|
+
} else {
|
|
745
|
+
unkeyedCaptureDepth = Math.max(0, unkeyedCaptureDepth - 1);
|
|
746
|
+
recomputeGlobalCapturing();
|
|
747
|
+
}
|
|
748
|
+
frame.retired = true;
|
|
749
|
+
while (captureStack.length > 0 && captureStack[captureStack.length - 1].retired) {
|
|
750
|
+
captureStack.pop().restore();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/logger.ts
|
|
756
|
+
var LogWriter = class _LogWriter {
|
|
757
|
+
constructor(filePath) {
|
|
758
|
+
this.filePath = filePath;
|
|
759
|
+
}
|
|
760
|
+
filePath;
|
|
761
|
+
buffer = [];
|
|
762
|
+
bufferBytes = 0;
|
|
763
|
+
flushScheduled = false;
|
|
764
|
+
flushing = false;
|
|
765
|
+
// Flush eagerly once buffered output gets large, to bound memory and latency
|
|
766
|
+
// during sustained bursts instead of waiting for the scheduled tick.
|
|
767
|
+
static MAX_BUFFER_BYTES = 256 * 1024;
|
|
768
|
+
/** Queue a line for asynchronous, non-blocking write (INFO/DEBUG path). */
|
|
769
|
+
enqueue(line) {
|
|
770
|
+
this.buffer.push(line);
|
|
771
|
+
this.bufferBytes += line.length;
|
|
772
|
+
if (this.bufferBytes >= _LogWriter.MAX_BUFFER_BYTES) {
|
|
773
|
+
void this.flush();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
this.scheduleFlush();
|
|
777
|
+
}
|
|
778
|
+
scheduleFlush() {
|
|
779
|
+
if (this.flushScheduled || this.flushing) return;
|
|
780
|
+
this.flushScheduled = true;
|
|
781
|
+
setTimeout(() => {
|
|
782
|
+
void this.flush();
|
|
783
|
+
}, 0);
|
|
784
|
+
}
|
|
785
|
+
/** Asynchronously write buffered lines. Never blocks the event loop. */
|
|
786
|
+
async flush() {
|
|
787
|
+
this.flushScheduled = false;
|
|
788
|
+
if (this.flushing || this.buffer.length === 0) return;
|
|
789
|
+
this.flushing = true;
|
|
790
|
+
try {
|
|
791
|
+
while (this.buffer.length > 0) {
|
|
792
|
+
const batch = this.buffer.join("");
|
|
793
|
+
this.buffer.length = 0;
|
|
794
|
+
this.bufferBytes = 0;
|
|
795
|
+
try {
|
|
796
|
+
await appendFile(this.filePath, batch);
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
} finally {
|
|
801
|
+
this.flushing = false;
|
|
802
|
+
if (this.buffer.length > 0) this.scheduleFlush();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Drain buffered lines, then write `line`, all synchronously and in order.
|
|
807
|
+
* Used for WARN/ERROR so the critical line (and all lines queued in the buffer
|
|
808
|
+
* before it) are on disk before this call returns.
|
|
809
|
+
*
|
|
810
|
+
* Ordering caveat: if a large INFO/DEBUG burst already cleared the buffer into
|
|
811
|
+
* an in-flight async appendFile, drainSync() will find the buffer empty and
|
|
812
|
+
* the WARN line will be appended before that in-flight batch completes. In
|
|
813
|
+
* practice this means WARN/ERROR lines may appear slightly before the last
|
|
814
|
+
* flush of the preceding INFO/DEBUG burst. Acceptable for a diagnostic log.
|
|
815
|
+
*/
|
|
816
|
+
writeSync(line) {
|
|
817
|
+
this.drainSync();
|
|
818
|
+
try {
|
|
819
|
+
appendFileSync(this.filePath, line);
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/** Synchronously flush all buffered lines (graceful exit / tests / pre-sync-write). */
|
|
824
|
+
drainSync() {
|
|
825
|
+
if (this.buffer.length === 0) return;
|
|
826
|
+
const batch = this.buffer.join("");
|
|
827
|
+
this.buffer.length = 0;
|
|
828
|
+
this.bufferBytes = 0;
|
|
829
|
+
try {
|
|
830
|
+
appendFileSync(this.filePath, batch);
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
var logWriters = /* @__PURE__ */ new Map();
|
|
836
|
+
var exitDrainRegistered = false;
|
|
837
|
+
function getLogWriter(filePath) {
|
|
838
|
+
let writer = logWriters.get(filePath);
|
|
839
|
+
if (!writer) {
|
|
840
|
+
writer = new LogWriter(filePath);
|
|
841
|
+
logWriters.set(filePath, writer);
|
|
842
|
+
}
|
|
843
|
+
if (!exitDrainRegistered) {
|
|
844
|
+
exitDrainRegistered = true;
|
|
845
|
+
process.on("exit", () => {
|
|
846
|
+
for (const w of logWriters.values()) {
|
|
847
|
+
try {
|
|
848
|
+
w.drainSync();
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return writer;
|
|
855
|
+
}
|
|
856
|
+
var LOGGER_BRAND = /* @__PURE__ */ Symbol.for("pi-research.Logger");
|
|
857
|
+
var Logger = class {
|
|
858
|
+
verbose;
|
|
859
|
+
consoleLog;
|
|
860
|
+
logFile;
|
|
861
|
+
logDir;
|
|
862
|
+
sessionId;
|
|
863
|
+
[LOGGER_BRAND] = true;
|
|
864
|
+
rotation;
|
|
865
|
+
diskSpaceChecker;
|
|
866
|
+
constructor(options = {}) {
|
|
867
|
+
this.verbose = options.verbose ?? isVerboseFromEnv();
|
|
868
|
+
this.consoleLog = options.consoleLog ?? process.env["PI_RESEARCH_CONSOLE_LOG"] === "true";
|
|
869
|
+
this.logFile = options.logFilePath ?? buildDefaultDebugLogPath();
|
|
870
|
+
this.logDir = path3.dirname(this.logFile);
|
|
871
|
+
this.sessionId = options.researchRunId;
|
|
872
|
+
this.rotation = new LogRotation(this);
|
|
873
|
+
this.diskSpaceChecker = new DiskSpaceChecker();
|
|
874
|
+
try {
|
|
875
|
+
if (!existsSync2(this.logDir)) {
|
|
876
|
+
mkdirSync(this.logDir, { recursive: true, mode: 448 });
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
closeSync2(openSync2(this.logFile, "wx", 384));
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
emit(level, ...args) {
|
|
886
|
+
if (!this.verbose && (level === "INFO" /* INFO */ || level === "DEBUG" /* DEBUG */)) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (!this.diskSpaceChecker.checkDiskSpace(this.logDir)) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
this.rotation.checkAndRotate(this.logFile, this.logDir);
|
|
893
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
894
|
+
const firstError = args.find((arg) => arg instanceof Error);
|
|
895
|
+
const message = redactSecrets(args.map(formatArg).join(" "));
|
|
896
|
+
const entry = {
|
|
897
|
+
timestamp,
|
|
898
|
+
level,
|
|
899
|
+
...getLogContext(),
|
|
900
|
+
// Every process writes to the SAME log file — up to three concurrent research
|
|
901
|
+
// runs plus the elected browser-pool leader — and until this field existed
|
|
902
|
+
// there was nothing to tell their lines apart. Reconstructing a lock
|
|
903
|
+
// contention from the log then meant inferring process boundaries from
|
|
904
|
+
// timing, which is guesswork: two runs a second apart interleave into what
|
|
905
|
+
// reads like one process contradicting itself. Twelve bytes a line buys
|
|
906
|
+
// attributable forensics.
|
|
907
|
+
//
|
|
908
|
+
// After the context spread, not before: which process wrote a line is not
|
|
909
|
+
// something a scoped context gets to claim otherwise.
|
|
910
|
+
pid: process.pid,
|
|
911
|
+
message,
|
|
912
|
+
...firstError ? { errorMessage: redactSecrets(firstError.message), errorStack: redactSecrets(firstError.stack ?? "") } : {}
|
|
913
|
+
};
|
|
914
|
+
const line = `${safeJsonStringify(entry)}
|
|
915
|
+
`;
|
|
916
|
+
const writer = getLogWriter(this.logFile);
|
|
917
|
+
if (level === "ERROR" /* ERROR */ || level === "WARN" /* WARN */) {
|
|
918
|
+
writer.writeSync(line);
|
|
919
|
+
} else {
|
|
920
|
+
writer.enqueue(line);
|
|
921
|
+
}
|
|
922
|
+
if (this.consoleLog) {
|
|
923
|
+
const color = level === "ERROR" /* ERROR */ ? "\x1B[31m" : level === "WARN" /* WARN */ ? "\x1B[33m" : level === "DEBUG" /* DEBUG */ ? "\x1B[90m" : "\x1B[36m";
|
|
924
|
+
const reset = "\x1B[0m";
|
|
925
|
+
const msg = stripTerminalEscapes(message.replace(/[\r\n]/g, ""));
|
|
926
|
+
const rawSid = this.sessionId ?? "";
|
|
927
|
+
const sid = rawSid.replace(/[\r\n]/g, "");
|
|
928
|
+
const prefix = sid ? `[${sid}] ` : "";
|
|
929
|
+
console.log(`${color}${timestamp} ${level} ${prefix}${reset}${msg}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async runCapturingStderr(task) {
|
|
933
|
+
return captureStdio(
|
|
934
|
+
this.logFile,
|
|
935
|
+
() => this.diskSpaceChecker.checkDiskSpace(this.logDir),
|
|
936
|
+
task,
|
|
937
|
+
this.sessionId
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
log(...args) {
|
|
941
|
+
this.emit("INFO" /* INFO */, ...args);
|
|
942
|
+
}
|
|
943
|
+
info(...args) {
|
|
944
|
+
this.emit("INFO" /* INFO */, ...args);
|
|
945
|
+
}
|
|
946
|
+
error(...args) {
|
|
947
|
+
const errArg = args.find((arg) => arg instanceof Error);
|
|
948
|
+
if (errArg) {
|
|
949
|
+
const context = getLogContext();
|
|
950
|
+
errorTracker.trackError(errArg, context);
|
|
951
|
+
}
|
|
952
|
+
this.emit("ERROR" /* ERROR */, ...args);
|
|
953
|
+
}
|
|
954
|
+
warn(...args) {
|
|
955
|
+
this.emit("WARN" /* WARN */, ...args);
|
|
956
|
+
}
|
|
957
|
+
debug(...args) {
|
|
958
|
+
this.emit("DEBUG" /* DEBUG */, ...args);
|
|
959
|
+
}
|
|
960
|
+
clear() {
|
|
961
|
+
this.flushSync();
|
|
962
|
+
this.rotation.clearLogs(this.logFile, this.logDir);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Synchronously flush this logger's buffered INFO/DEBUG output to disk.
|
|
966
|
+
* Use before reading the log file directly (tests) or during shutdown.
|
|
967
|
+
*/
|
|
968
|
+
flushSync() {
|
|
969
|
+
getLogWriter(this.logFile).drainSync();
|
|
970
|
+
}
|
|
971
|
+
getLogFilePath() {
|
|
972
|
+
return this.logFile;
|
|
973
|
+
}
|
|
974
|
+
isVerbose() {
|
|
975
|
+
return this.verbose;
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
var loggerContext = new AsyncLocalStorage3();
|
|
979
|
+
var sessionLoggers = /* @__PURE__ */ new Map();
|
|
980
|
+
var _globalLogger = null;
|
|
981
|
+
function getLogger(sessionId) {
|
|
982
|
+
const contextLogger = loggerContext.getStore();
|
|
983
|
+
if (contextLogger) return contextLogger;
|
|
984
|
+
if (sessionId) {
|
|
985
|
+
let lg = sessionLoggers.get(sessionId);
|
|
986
|
+
if (!lg) {
|
|
987
|
+
lg = new Logger({
|
|
988
|
+
verbose: isVerboseFromEnv(),
|
|
989
|
+
researchRunId: sessionId,
|
|
990
|
+
consoleLog: process.env["PI_RESEARCH_CONSOLE_LOG"] === "true"
|
|
991
|
+
});
|
|
992
|
+
sessionLoggers.set(sessionId, lg);
|
|
993
|
+
}
|
|
994
|
+
return lg;
|
|
995
|
+
}
|
|
996
|
+
if (!_globalLogger) {
|
|
997
|
+
_globalLogger = new Logger({
|
|
998
|
+
verbose: isVerboseFromEnv(),
|
|
999
|
+
consoleLog: process.env["PI_RESEARCH_CONSOLE_LOG"] === "true"
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
return _globalLogger;
|
|
1003
|
+
}
|
|
1004
|
+
var logger = {
|
|
1005
|
+
log: (...args) => getLogger().log(...args),
|
|
1006
|
+
info: (...args) => getLogger().info(...args),
|
|
1007
|
+
error: (...args) => getLogger().error(...args),
|
|
1008
|
+
warn: (...args) => getLogger().warn(...args),
|
|
1009
|
+
debug: (...args) => getLogger().debug(...args),
|
|
1010
|
+
clear: () => getLogger().clear(),
|
|
1011
|
+
flushSync: () => getLogger().flushSync(),
|
|
1012
|
+
runCapturingStderr: (task) => getLogger().runCapturingStderr(task)
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// src/utils/metrics.ts
|
|
1016
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "node:async_hooks";
|
|
1017
|
+
var MetricsRegistry = class {
|
|
1018
|
+
counters = /* @__PURE__ */ new Map();
|
|
1019
|
+
gauges = /* @__PURE__ */ new Map();
|
|
1020
|
+
histograms = /* @__PURE__ */ new Map();
|
|
1021
|
+
serializeLabels(labels) {
|
|
1022
|
+
if (!labels) return "";
|
|
1023
|
+
const keys = Object.keys(labels).sort();
|
|
1024
|
+
return keys.map((k) => `${k}="${labels[k]}"`).join(",");
|
|
1025
|
+
}
|
|
1026
|
+
getKey(name, labels) {
|
|
1027
|
+
const labelStr = this.serializeLabels(labels);
|
|
1028
|
+
return labelStr ? `${name}{${labelStr}}` : name;
|
|
1029
|
+
}
|
|
1030
|
+
increment(name, value = 1, labels) {
|
|
1031
|
+
const key = this.getKey(name, labels);
|
|
1032
|
+
this.counters.set(key, (this.counters.get(key) ?? 0) + value);
|
|
1033
|
+
if (name.startsWith("llm_")) {
|
|
1034
|
+
logger.debug(`[Metrics] Incremented ${name} by ${value}${labels ? ` (labels: ${JSON.stringify(labels)})` : ""}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
setGauge(name, value, labels) {
|
|
1038
|
+
this.gauges.set(this.getKey(name, labels), value);
|
|
1039
|
+
}
|
|
1040
|
+
observe(name, value, labels) {
|
|
1041
|
+
if (!Number.isFinite(value)) return;
|
|
1042
|
+
const key = this.getKey(name, labels);
|
|
1043
|
+
let entry = this.histograms.get(key);
|
|
1044
|
+
if (!entry) {
|
|
1045
|
+
entry = { values: [], pointer: 0, limit: 1e4, dirty: true, cached: null };
|
|
1046
|
+
this.histograms.set(key, entry);
|
|
1047
|
+
}
|
|
1048
|
+
entry.dirty = true;
|
|
1049
|
+
if (entry.values.length < entry.limit) {
|
|
1050
|
+
entry.values.push(value);
|
|
1051
|
+
} else {
|
|
1052
|
+
entry.values[entry.pointer] = value;
|
|
1053
|
+
entry.pointer = (entry.pointer + 1) % entry.limit;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async measure(name, action, labels) {
|
|
1057
|
+
const start = process.hrtime.bigint();
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await action();
|
|
1060
|
+
this.observe(name, Number(process.hrtime.bigint() - start) / 1e6, labels);
|
|
1061
|
+
return result;
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
this.observe(name, Number(process.hrtime.bigint() - start) / 1e6, { ...labels, error: "true" });
|
|
1064
|
+
this.increment(`${name}_errors_total`, 1, labels);
|
|
1065
|
+
throw error;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
percentile(sorted, pct) {
|
|
1069
|
+
const len = sorted.length;
|
|
1070
|
+
if (len === 0) return 0;
|
|
1071
|
+
if (len === 1) return sorted[0] ?? 0;
|
|
1072
|
+
if (pct <= 0) return sorted[0] ?? 0;
|
|
1073
|
+
if (pct >= 100) return sorted[len - 1] ?? 0;
|
|
1074
|
+
const rank = pct / 100 * (len - 1);
|
|
1075
|
+
const lo = Math.floor(rank);
|
|
1076
|
+
const hi = Math.ceil(rank);
|
|
1077
|
+
const w = rank - lo;
|
|
1078
|
+
return (sorted[lo] ?? 0) + ((sorted[hi] ?? 0) - (sorted[lo] ?? 0)) * w;
|
|
1079
|
+
}
|
|
1080
|
+
getSnapshot() {
|
|
1081
|
+
const snap = { counters: {}, gauges: {}, histograms: {} };
|
|
1082
|
+
for (const [k, v] of this.counters) snap.counters[k] = v;
|
|
1083
|
+
for (const [k, v] of this.gauges) snap.gauges[k] = v;
|
|
1084
|
+
for (const [k, entry] of this.histograms) {
|
|
1085
|
+
if (entry.values.length === 0) continue;
|
|
1086
|
+
if (!entry.dirty && entry.cached) {
|
|
1087
|
+
snap.histograms[k] = entry.cached;
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const vals = entry.values;
|
|
1091
|
+
const sorted = vals.slice().sort((a, b) => a - b);
|
|
1092
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
1093
|
+
const histogram = {
|
|
1094
|
+
count: sorted.length,
|
|
1095
|
+
min: sorted[0] ?? 0,
|
|
1096
|
+
max: sorted[sorted.length - 1] ?? 0,
|
|
1097
|
+
avg: sum / sorted.length,
|
|
1098
|
+
p50: this.percentile(sorted, 50),
|
|
1099
|
+
p90: this.percentile(sorted, 90),
|
|
1100
|
+
p95: this.percentile(sorted, 95),
|
|
1101
|
+
p99: this.percentile(sorted, 99)
|
|
1102
|
+
};
|
|
1103
|
+
entry.cached = histogram;
|
|
1104
|
+
entry.dirty = false;
|
|
1105
|
+
snap.histograms[k] = histogram;
|
|
1106
|
+
}
|
|
1107
|
+
return snap;
|
|
1108
|
+
}
|
|
1109
|
+
clear() {
|
|
1110
|
+
this.counters.clear();
|
|
1111
|
+
this.gauges.clear();
|
|
1112
|
+
this.histograms.clear();
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
var runRegistryStorage = new AsyncLocalStorage4();
|
|
1116
|
+
var MAX_RUN_HISTORY = 10;
|
|
1117
|
+
var SessionMetrics = class {
|
|
1118
|
+
_session = new MetricsRegistry();
|
|
1119
|
+
_sessionStartedAt = Date.now();
|
|
1120
|
+
_runHistory = [];
|
|
1121
|
+
getActive() {
|
|
1122
|
+
return runRegistryStorage.getStore() ?? this._session;
|
|
1123
|
+
}
|
|
1124
|
+
// ── Emit API (routes to active registry) ──────────────────────────────
|
|
1125
|
+
increment(name, value = 1, labels) {
|
|
1126
|
+
this.getActive().increment(name, value, labels);
|
|
1127
|
+
}
|
|
1128
|
+
setGauge(name, value, labels) {
|
|
1129
|
+
this.getActive().setGauge(name, value, labels);
|
|
1130
|
+
}
|
|
1131
|
+
observe(name, value, labels) {
|
|
1132
|
+
this.getActive().observe(name, value, labels);
|
|
1133
|
+
}
|
|
1134
|
+
async measure(name, action, labels) {
|
|
1135
|
+
return this.getActive().measure(name, action, labels);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Emit API that ALWAYS targets the session registry, ignoring any active run
|
|
1139
|
+
* context.
|
|
1140
|
+
*
|
|
1141
|
+
* For process-lifetime callbacks that happen to be *registered* during a run.
|
|
1142
|
+
* AsyncLocalStorage resolves at emit time from the async resource that fired,
|
|
1143
|
+
* and for a listener installed on a long-lived emitter that resource is the
|
|
1144
|
+
* emitter itself — created, for every lazily-initialized subsystem here, inside
|
|
1145
|
+
* whichever run first needed it. So a worker-pool error handler installed during
|
|
1146
|
+
* run 1 keeps writing into run 1's registry for the rest of the process: those
|
|
1147
|
+
* counts disappear from every later run's summary (the registry was discarded
|
|
1148
|
+
* when the run ended) and the dead registry stays reachable through the closure.
|
|
1149
|
+
*
|
|
1150
|
+
* Use this only where the emission is genuinely process-scoped. Anything that
|
|
1151
|
+
* describes work a run asked for belongs in the run registry, which is what the
|
|
1152
|
+
* plain emit methods above give you.
|
|
1153
|
+
*/
|
|
1154
|
+
session = {
|
|
1155
|
+
increment: (name, value = 1, labels) => this._session.increment(name, value, labels),
|
|
1156
|
+
setGauge: (name, value, labels) => this._session.setGauge(name, value, labels),
|
|
1157
|
+
observe: (name, value, labels) => this._session.observe(name, value, labels)
|
|
1158
|
+
};
|
|
1159
|
+
// ── Session-level read API ─────────────────────────────────────────────
|
|
1160
|
+
/** Snapshot of the session registry (infrastructure / cross-run metrics). */
|
|
1161
|
+
getSessionSnapshot() {
|
|
1162
|
+
return this._session.getSnapshot();
|
|
1163
|
+
}
|
|
1164
|
+
/** Millisecond timestamp when this session started or was last reset. */
|
|
1165
|
+
getSessionStartedAt() {
|
|
1166
|
+
return this._sessionStartedAt;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Ordered run summaries, oldest first. Capped at MAX_RUN_HISTORY entries.
|
|
1170
|
+
* Returns a shallow copy so callers cannot mutate the internal list.
|
|
1171
|
+
*/
|
|
1172
|
+
getRunHistory() {
|
|
1173
|
+
return [...this._runHistory];
|
|
1174
|
+
}
|
|
1175
|
+
// ── Run lifecycle API ──────────────────────────────────────────────────
|
|
1176
|
+
/**
|
|
1177
|
+
* Append a completed run's snapshot to the history.
|
|
1178
|
+
* Call this AFTER runWithRunRegistry returns so the write lands in session
|
|
1179
|
+
* scope. Automatically evicts the oldest entry when the cap is reached.
|
|
1180
|
+
*/
|
|
1181
|
+
recordRunSummary(summary) {
|
|
1182
|
+
this._runHistory.push(summary);
|
|
1183
|
+
if (this._runHistory.length > MAX_RUN_HISTORY) {
|
|
1184
|
+
this._runHistory.shift();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// ── Reset API ──────────────────────────────────────────────────────────
|
|
1188
|
+
/**
|
|
1189
|
+
* Clear the session registry, run history, and session start timestamp.
|
|
1190
|
+
* In-flight run registries are NOT affected.
|
|
1191
|
+
*/
|
|
1192
|
+
clearSession() {
|
|
1193
|
+
this._session.clear();
|
|
1194
|
+
this._runHistory = [];
|
|
1195
|
+
this._sessionStartedAt = Date.now();
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
var metrics = new SessionMetrics();
|
|
1199
|
+
|
|
1200
|
+
// src/web-research/scraper-types.ts
|
|
1201
|
+
var MAX_HTML_SIZE = 25 * 1024 * 1024;
|
|
1202
|
+
var MAX_PDF_SIZE = 100 * 1024 * 1024;
|
|
1203
|
+
|
|
1204
|
+
// src/web-research/pdf-extraction.ts
|
|
1205
|
+
var MODULE_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
1206
|
+
var PdfExtractError = class extends Error {
|
|
1207
|
+
kind;
|
|
1208
|
+
constructor(kind, message, options) {
|
|
1209
|
+
super(message, options);
|
|
1210
|
+
this.name = "PdfExtractError";
|
|
1211
|
+
this.kind = kind;
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
var pdfModulePromise = null;
|
|
1215
|
+
function loadPdfModule() {
|
|
1216
|
+
if (pdfModulePromise === null) {
|
|
1217
|
+
pdfModulePromise = import("pdf-oxide-wasm");
|
|
1218
|
+
}
|
|
1219
|
+
return pdfModulePromise;
|
|
1220
|
+
}
|
|
1221
|
+
async function parsePdfCore(bytes) {
|
|
1222
|
+
if (bytes.length > MAX_PDF_SIZE) {
|
|
1223
|
+
const sizeMB = Math.round(bytes.length / 1024 / 1024);
|
|
1224
|
+
throw new PdfExtractError("size_exceeded", `PDF too large (${sizeMB}MB, max 100MB)`);
|
|
1225
|
+
}
|
|
1226
|
+
let WasmPdfDocument;
|
|
1227
|
+
try {
|
|
1228
|
+
({ WasmPdfDocument } = await loadPdfModule());
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1231
|
+
throw new PdfExtractError(
|
|
1232
|
+
"native_module_unavailable",
|
|
1233
|
+
`PDF extraction unavailable (pdf-oxide-wasm failed to load: ${msg})`,
|
|
1234
|
+
{ cause: e }
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
const doc = new WasmPdfDocument(bytes);
|
|
1239
|
+
try {
|
|
1240
|
+
const pageCount = doc.pageCount();
|
|
1241
|
+
let markdown = `# PDF Document
|
|
1242
|
+
|
|
1243
|
+
**Pages:** ${pageCount}
|
|
1244
|
+
|
|
1245
|
+
`;
|
|
1246
|
+
try {
|
|
1247
|
+
markdown += doc.toMarkdownAll();
|
|
1248
|
+
} catch {
|
|
1249
|
+
for (let i = 0; i < pageCount; i++) {
|
|
1250
|
+
markdown += `## Page ${i + 1}
|
|
1251
|
+
|
|
1252
|
+
${doc.toMarkdown(i)}
|
|
1253
|
+
|
|
1254
|
+
`;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return { markdown, pageCount };
|
|
1258
|
+
} finally {
|
|
1259
|
+
doc.free();
|
|
1260
|
+
}
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1263
|
+
throw new PdfExtractError("extraction_failed", `Could not extract content from PDF (${msg})`, { cause: e });
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/web-research/pdf-extract-worker.ts
|
|
1268
|
+
if (parentPort !== null) {
|
|
1269
|
+
const port = parentPort;
|
|
1270
|
+
port.on("message", (msg) => {
|
|
1271
|
+
void (async () => {
|
|
1272
|
+
try {
|
|
1273
|
+
const result = await parsePdfCore(msg.bytes);
|
|
1274
|
+
port.postMessage({ id: msg.id, ok: true, markdown: result.markdown, pageCount: result.pageCount });
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
if (e instanceof PdfExtractError) {
|
|
1277
|
+
port.postMessage({ id: msg.id, ok: false, kind: e.kind, message: e.message });
|
|
1278
|
+
} else {
|
|
1279
|
+
port.postMessage({
|
|
1280
|
+
id: msg.id,
|
|
1281
|
+
ok: false,
|
|
1282
|
+
kind: "extraction_failed",
|
|
1283
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
})();
|
|
1288
|
+
});
|
|
1289
|
+
}
|