@lincoln504/pi-research 1.2.0 → 1.3.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/dist/cli.mjs +1334 -750
- package/dist/thread-worker.mjs +194 -136
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CHANGELOG.md +114 -16
- package/package.json +1 -1
- package/scripts/cleanup.cjs +64 -2
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cleanup/research-cleanup.ts +12 -4
- package/src/cli.ts +228 -70
- package/src/config.ts +78 -2
- package/src/core/interfaces/health-check-interfaces.ts +3 -39
- package/src/core/interfaces/planning-interfaces.ts +8 -0
- package/src/core/interfaces/service-names.ts +0 -1
- package/src/core/interfaces/state-manager-interfaces.ts +10 -2
- package/src/core/llm/agentic-repair.ts +14 -0
- package/src/core/planning-service.ts +43 -1
- package/src/core/service-initialization.ts +6 -14
- package/src/core/service-interfaces.ts +1 -1
- package/src/healthcheck/index.ts +7 -2
- package/src/infrastructure/browser/browser-client.ts +64 -9
- package/src/infrastructure/browser/browser-server.ts +24 -7
- package/src/infrastructure/browser/browser-task-scheduler.ts +58 -12
- package/src/infrastructure/browser/cleanup-utils.ts +50 -2
- package/src/infrastructure/browser/client-agent.ts +6 -6
- package/src/infrastructure/browser/config.ts +14 -3
- package/src/infrastructure/browser/scheduler-factory.ts +18 -3
- package/src/infrastructure/browser/thread-worker-messaging.ts +54 -2
- package/src/infrastructure/browser/thread-worker-types.ts +3 -1
- package/src/infrastructure/browser/thread-worker.mjs +194 -136
- package/src/infrastructure/browser/worker-pool-manager.ts +7 -2
- package/src/infrastructure/embedding/embedding-factory.ts +35 -6
- package/src/infrastructure/embedding/embedding-server.ts +40 -7
- package/src/infrastructure/file-lock-service.ts +72 -2
- package/src/infrastructure/knowledge-store-service.ts +48 -3
- package/src/infrastructure/process-lifecycle-service.ts +28 -5
- package/src/infrastructure/research-run-semaphore.ts +37 -3
- package/src/infrastructure/state/state-browser-api.ts +5 -2
- package/src/infrastructure/state/state-browser-manager.ts +15 -1
- package/src/infrastructure/state/state-manager-service.ts +4 -4
- package/src/infrastructure/state/state-manager.ts +15 -3
- package/src/knowledge/chunker.ts +88 -1
- package/src/knowledge/embedder.ts +136 -43
- package/src/knowledge/store.ts +66 -12
- package/src/logger.ts +5 -1
- package/src/observers/research-observer-impl.ts +31 -0
- package/src/orchestration/abort-utils.ts +60 -0
- package/src/orchestration/deep-research-orchestrator.ts +12 -2
- package/src/orchestration/headless-observer.ts +8 -0
- package/src/orchestration/research-orchestration-service.ts +63 -11
- package/src/orchestration/research-session-service.ts +37 -11
- package/src/orchestration/researcher-executor.ts +100 -20
- package/src/orchestration/researcher.ts +39 -20
- package/src/orchestration/session-state.ts +11 -0
- package/src/sdk.ts +125 -19
- package/src/security/index.ts +51 -30
- package/src/security/nvd-client.ts +7 -2
- package/src/skill-install/skill-installer.ts +18 -2
- package/src/stackexchange/index.ts +20 -5
- package/src/stackexchange/output/table.ts +7 -2
- package/src/stackexchange/rest-client.ts +35 -1
- package/src/tools/research-tool-definition.ts +8 -2
- package/src/tools/scrape.ts +48 -25
- package/src/tools/search.ts +10 -1
- package/src/tools/security.ts +68 -7
- package/src/tui/research-health.ts +18 -2
- package/src/utils/circuit-breaker.ts +9 -2
- package/src/utils/disk-space-checker.ts +11 -6
- package/src/utils/json-utils.ts +116 -76
- package/src/utils/llm-usage.ts +17 -1
- package/src/utils/log-rotation.ts +42 -10
- package/src/utils/log-utils.ts +40 -0
- package/src/utils/metrics-summary.ts +27 -9
- package/src/utils/stdio-capture.ts +63 -22
- package/src/utils/text-utils.ts +8 -1
- package/src/utils/tool-usage-tracker.ts +9 -2
- package/src/web-research/browser-search.ts +26 -7
- package/src/web-research/scraper-utils.ts +13 -0
- package/src/web-research/search.ts +6 -2
- package/src/web-research/types.ts +4 -2
- package/src/web-research/web-scraper.ts +65 -4
- package/src/core/health-check-service.ts +0 -265
package/dist/thread-worker.mjs
CHANGED
|
@@ -51,13 +51,17 @@ function redactSecrets(message) {
|
|
|
51
51
|
out = out.replace(JWT_PATTERN, "[REDACTED]");
|
|
52
52
|
out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
|
|
53
53
|
out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
|
|
54
|
+
out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
|
|
54
55
|
out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
|
|
55
56
|
out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
|
|
56
57
|
out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
|
|
57
58
|
out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
|
|
58
59
|
return out;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
function stripTerminalEscapes(message) {
|
|
62
|
+
return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
|
|
63
|
+
}
|
|
64
|
+
var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, TERMINAL_ESCAPE_PATTERN;
|
|
61
65
|
var init_log_utils = __esm({
|
|
62
66
|
"src/utils/log-utils.ts"() {
|
|
63
67
|
"use strict";
|
|
@@ -65,6 +69,7 @@ var init_log_utils = __esm({
|
|
|
65
69
|
MAX_LOG_MESSAGE_LENGTH = 1e4;
|
|
66
70
|
ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
67
71
|
URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
|
|
72
|
+
COOKIE_HEADER_PATTERN = /\b(set[_-]?cookie|cookie)\b(["']?\s*[:=]\s*["']?)([^\r\n]+)/gi;
|
|
68
73
|
SENSITIVE_KV_PATTERN = /\b(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)\b(["']?\s*[:=]\s*["']?)([^\s"',&)]+)/gi;
|
|
69
74
|
KNOWN_TOKEN_PATTERN = /\b(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/g;
|
|
70
75
|
JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
@@ -72,6 +77,106 @@ var init_log_utils = __esm({
|
|
|
72
77
|
BEARER_AUTH_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
|
|
73
78
|
PROVIDER_TOKEN_PATTERN = /\b(AIza[0-9A-Za-z_-]{35}|ya29\.[0-9A-Za-z_-]{20,}|[sp]k_(?:live|test)_[0-9A-Za-z]{16,})\b/g;
|
|
74
79
|
LONG_HEX_SECRET_PATTERN = /\b[0-9a-fA-F]{32,}\b/g;
|
|
80
|
+
CONTROL_CHARS_PATTERN = /[\x00-\x1f\x7f]/g;
|
|
81
|
+
TERMINAL_ESCAPE_PATTERN = new RegExp(
|
|
82
|
+
"\\x1b\\[[0-?]*[ -/]*[@-~]?|\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)?|\\x1b[PX^_][^\\x1b]*(?:\\x1b\\\\)?|\\x1b.?",
|
|
83
|
+
"g"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/utils/user-agent.ts
|
|
89
|
+
var USER_AGENTS;
|
|
90
|
+
var init_user_agent = __esm({
|
|
91
|
+
"src/utils/user-agent.ts"() {
|
|
92
|
+
"use strict";
|
|
93
|
+
USER_AGENTS = [
|
|
94
|
+
// Chrome on Windows (updated 2025)
|
|
95
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
96
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
97
|
+
// Chrome on macOS
|
|
98
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
99
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
100
|
+
// Chrome on Linux
|
|
101
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
102
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
103
|
+
// Firefox on Windows (updated 2025)
|
|
104
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
105
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
|
|
106
|
+
// Firefox on macOS
|
|
107
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
108
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// src/web-research/scraper-types.ts
|
|
114
|
+
var MAX_HTML_SIZE, MAX_PDF_SIZE, INTERNAL_NETWORK_PATTERNS, FILTERED_TAGS, BOT_PATTERNS, IMAGE_LINK_PATTERN, MARKDOWN_IMAGE_PATTERN;
|
|
115
|
+
var init_scraper_types = __esm({
|
|
116
|
+
"src/web-research/scraper-types.ts"() {
|
|
117
|
+
"use strict";
|
|
118
|
+
init_user_agent();
|
|
119
|
+
MAX_HTML_SIZE = 25 * 1024 * 1024;
|
|
120
|
+
MAX_PDF_SIZE = 100 * 1024 * 1024;
|
|
121
|
+
INTERNAL_NETWORK_PATTERNS = [
|
|
122
|
+
/^127\./,
|
|
123
|
+
// IPv4 loopback
|
|
124
|
+
/^0\./,
|
|
125
|
+
// IPv4 "this" network
|
|
126
|
+
/^::1$/,
|
|
127
|
+
// IPv6 loopback
|
|
128
|
+
/^fe80::/i,
|
|
129
|
+
// IPv6 link-local
|
|
130
|
+
/^f[cd][0-9a-f]{2}:/i,
|
|
131
|
+
// IPv6 unique local — full fc00::/7 block (fc00::–fdff::)
|
|
132
|
+
/^169\.254\./,
|
|
133
|
+
// IPv4 link-local (and metadata)
|
|
134
|
+
/^10\./,
|
|
135
|
+
// RFC 1918 Class A private
|
|
136
|
+
/^192\.168\./,
|
|
137
|
+
// RFC 1918 Class C private
|
|
138
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
139
|
+
// RFC 1918 Class B private
|
|
140
|
+
/^::ffff:(127\.|0\.|169\.254\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/i
|
|
141
|
+
// IPv4-mapped IPv6
|
|
142
|
+
];
|
|
143
|
+
FILTERED_TAGS = [
|
|
144
|
+
"nav",
|
|
145
|
+
"header",
|
|
146
|
+
"footer",
|
|
147
|
+
"aside",
|
|
148
|
+
"script",
|
|
149
|
+
"style",
|
|
150
|
+
"noscript",
|
|
151
|
+
"form",
|
|
152
|
+
"input",
|
|
153
|
+
"select",
|
|
154
|
+
"textarea",
|
|
155
|
+
"button",
|
|
156
|
+
"object",
|
|
157
|
+
"embed",
|
|
158
|
+
"svg",
|
|
159
|
+
"symbol",
|
|
160
|
+
"use",
|
|
161
|
+
"defs",
|
|
162
|
+
"path",
|
|
163
|
+
"circle",
|
|
164
|
+
"rect",
|
|
165
|
+
"line",
|
|
166
|
+
"polygon",
|
|
167
|
+
"img",
|
|
168
|
+
"iframe"
|
|
169
|
+
];
|
|
170
|
+
BOT_PATTERNS = [
|
|
171
|
+
["_cf_chl_", "Cloudflare challenge"],
|
|
172
|
+
["cf_chl_opt", "Cloudflare challenge opt"],
|
|
173
|
+
["cdn-cgi/challenge-platform", "Cloudflare challenge platform"],
|
|
174
|
+
["ddos-guard", "DDoS-Guard challenge"],
|
|
175
|
+
["Just a moment...", "Cloudflare interstitial"],
|
|
176
|
+
["Checking your browser before accessing", "Cloudflare interstitial"]
|
|
177
|
+
];
|
|
178
|
+
IMAGE_LINK_PATTERN = /\[([^\]]*)\]\((data:image\/[^)]+|[^)\s]+\.(?:svg|png|jpe?g|gif|webp|bmp|ico)(?:\?[^)]*)?)\)/gi;
|
|
179
|
+
MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((?:data:image\/[^)]+|[^)\s]+)\)/gi;
|
|
75
180
|
}
|
|
76
181
|
});
|
|
77
182
|
|
|
@@ -265,6 +370,14 @@ var init_log_rotation = __esm({
|
|
|
265
370
|
try {
|
|
266
371
|
if (fs2.existsSync(logFile)) {
|
|
267
372
|
fs2.unlinkSync(logFile);
|
|
373
|
+
try {
|
|
374
|
+
fs2.closeSync(fs2.openSync(logFile, "a", 384));
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
fs2.chmodSync(logFile, 384);
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
268
381
|
}
|
|
269
382
|
const files = fs2.readdirSync(logDir);
|
|
270
383
|
const baseName = path2.basename(logFile);
|
|
@@ -297,12 +410,16 @@ var init_log_rotation = __esm({
|
|
|
297
410
|
try {
|
|
298
411
|
fs2.renameSync(logFile, archivePath);
|
|
299
412
|
} catch (renameErr) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
413
|
+
this.logger.log("[Logger] Log rotation skipped (rename failed; file likely in use):", renameErr);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
fs2.closeSync(fs2.openSync(logFile, "a", 384));
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
fs2.chmodSync(logFile, 384);
|
|
422
|
+
} catch {
|
|
306
423
|
}
|
|
307
424
|
try {
|
|
308
425
|
const files = fs2.readdirSync(logDir);
|
|
@@ -372,10 +489,13 @@ var init_disk_space_checker = __esm({
|
|
|
372
489
|
const availableBytes = stats.bavail * stats.bsize;
|
|
373
490
|
if (availableBytes < this.MIN_DISK_SPACE_BYTES) {
|
|
374
491
|
this.hasDiskSpace = false;
|
|
375
|
-
|
|
376
|
-
|
|
492
|
+
try {
|
|
493
|
+
process.stderr.write(
|
|
494
|
+
`[pi-research] Insufficient disk space for logging: ${Math.round(availableBytes / 1024 / 1024)}MB available, minimum ${this.MIN_DISK_SPACE_BYTES / 1024 / 1024}MB required
|
|
377
495
|
`
|
|
378
|
-
|
|
496
|
+
);
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
379
499
|
} else {
|
|
380
500
|
this.hasDiskSpace = true;
|
|
381
501
|
}
|
|
@@ -482,7 +602,7 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
|
|
|
482
602
|
if (shouldRedirectStderr(stat)) {
|
|
483
603
|
savedFd2 = fs4.openSync("/dev/fd/2", "a");
|
|
484
604
|
fs4.closeSync(2);
|
|
485
|
-
const newFd = fs4.openSync(logFile, "a");
|
|
605
|
+
const newFd = fs4.openSync(logFile, "a", 384);
|
|
486
606
|
if (newFd === 2) {
|
|
487
607
|
fd2Redirected = true;
|
|
488
608
|
} else {
|
|
@@ -614,42 +734,52 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
|
|
|
614
734
|
}
|
|
615
735
|
} catch (_e) {
|
|
616
736
|
}
|
|
737
|
+
const frame = {
|
|
738
|
+
retired: false,
|
|
739
|
+
restore: () => {
|
|
740
|
+
process.stderr.write = originalStderrWrite;
|
|
741
|
+
process.stdout.write = originalStdoutWrite;
|
|
742
|
+
try {
|
|
743
|
+
fs4.writeSync = originalFsWriteSync;
|
|
744
|
+
} catch {
|
|
745
|
+
}
|
|
746
|
+
console.log = originalConsole.log;
|
|
747
|
+
console.info = originalConsole.info;
|
|
748
|
+
console.warn = originalConsole.warn;
|
|
749
|
+
console.error = originalConsole.error;
|
|
750
|
+
console.debug = originalConsole.debug;
|
|
751
|
+
if (fd2Redirected && savedFd2 >= 0) {
|
|
752
|
+
try {
|
|
753
|
+
fs4.closeSync(2);
|
|
754
|
+
const r = fs4.openSync(`/dev/fd/${savedFd2}`, "a");
|
|
755
|
+
if (r !== 2 && r >= 0) {
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
fs4.closeSync(savedFd2);
|
|
759
|
+
} catch {
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
captureStack.push(frame);
|
|
617
767
|
try {
|
|
618
768
|
return await task();
|
|
619
769
|
} finally {
|
|
620
|
-
process.stderr.write = originalStderrWrite;
|
|
621
|
-
process.stdout.write = originalStdoutWrite;
|
|
622
|
-
try {
|
|
623
|
-
fs4.writeSync = originalFsWriteSync;
|
|
624
|
-
} catch {
|
|
625
|
-
}
|
|
626
|
-
console.log = originalConsole.log;
|
|
627
|
-
console.info = originalConsole.info;
|
|
628
|
-
console.warn = originalConsole.warn;
|
|
629
|
-
console.error = originalConsole.error;
|
|
630
|
-
console.debug = originalConsole.debug;
|
|
631
770
|
if (sessionId) {
|
|
632
771
|
setSessionCapturing(sessionId, false);
|
|
633
772
|
} else {
|
|
634
773
|
unkeyedCaptureDepth = Math.max(0, unkeyedCaptureDepth - 1);
|
|
635
774
|
recomputeGlobalCapturing();
|
|
636
775
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const r = fs4.openSync(`/dev/fd/${savedFd2}`, "a");
|
|
641
|
-
if (r !== 2 && r >= 0) {
|
|
642
|
-
}
|
|
643
|
-
try {
|
|
644
|
-
fs4.closeSync(savedFd2);
|
|
645
|
-
} catch {
|
|
646
|
-
}
|
|
647
|
-
} catch {
|
|
648
|
-
}
|
|
776
|
+
frame.retired = true;
|
|
777
|
+
while (captureStack.length > 0 && captureStack[captureStack.length - 1].retired) {
|
|
778
|
+
captureStack.pop().restore();
|
|
649
779
|
}
|
|
650
780
|
}
|
|
651
781
|
}
|
|
652
|
-
var fs4, sessionCaptureStates, unkeyedCaptureDepth, isAnyLoggerCapturingOutput, NATIVE_LOG_PATTERNS, COMPLEX_TUI_PATTERNS, BOX_DRAWING_CHARS;
|
|
782
|
+
var fs4, sessionCaptureStates, unkeyedCaptureDepth, isAnyLoggerCapturingOutput, captureStack, NATIVE_LOG_PATTERNS, COMPLEX_TUI_PATTERNS, BOX_DRAWING_CHARS;
|
|
653
783
|
var init_stdio_capture = __esm({
|
|
654
784
|
"src/utils/stdio-capture.ts"() {
|
|
655
785
|
"use strict";
|
|
@@ -658,6 +788,7 @@ var init_stdio_capture = __esm({
|
|
|
658
788
|
sessionCaptureStates = /* @__PURE__ */ new Map();
|
|
659
789
|
unkeyedCaptureDepth = 0;
|
|
660
790
|
isAnyLoggerCapturingOutput = false;
|
|
791
|
+
captureStack = [];
|
|
661
792
|
NATIVE_LOG_PATTERNS = [
|
|
662
793
|
"Warning:",
|
|
663
794
|
"Error:",
|
|
@@ -699,7 +830,7 @@ var init_stdio_capture = __esm({
|
|
|
699
830
|
});
|
|
700
831
|
|
|
701
832
|
// src/logger.ts
|
|
702
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync, openSync, closeSync } from "node:fs";
|
|
833
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
703
834
|
import { appendFile as appendFile2 } from "node:fs/promises";
|
|
704
835
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
705
836
|
import * as path3 from "node:path";
|
|
@@ -859,7 +990,7 @@ var init_logger = __esm({
|
|
|
859
990
|
mkdirSync(this.logDir, { recursive: true, mode: 448 });
|
|
860
991
|
}
|
|
861
992
|
try {
|
|
862
|
-
|
|
993
|
+
closeSync2(openSync2(this.logFile, "wx", 384));
|
|
863
994
|
} catch {
|
|
864
995
|
}
|
|
865
996
|
} catch {
|
|
@@ -894,7 +1025,7 @@ var init_logger = __esm({
|
|
|
894
1025
|
if (this.consoleLog) {
|
|
895
1026
|
const color = level === "ERROR" /* ERROR */ ? "\x1B[31m" : level === "WARN" /* WARN */ ? "\x1B[33m" : level === "DEBUG" /* DEBUG */ ? "\x1B[90m" : "\x1B[36m";
|
|
896
1027
|
const reset = "\x1B[0m";
|
|
897
|
-
const msg = message.replace(/[\r\n]/g, "");
|
|
1028
|
+
const msg = stripTerminalEscapes(message.replace(/[\r\n]/g, ""));
|
|
898
1029
|
const rawSid = this.sessionId ?? "";
|
|
899
1030
|
const sid = rawSid.replace(/[\r\n]/g, "");
|
|
900
1031
|
const prefix = sid ? `[${sid}] ` : "";
|
|
@@ -1133,101 +1264,6 @@ var init_metrics = __esm({
|
|
|
1133
1264
|
}
|
|
1134
1265
|
});
|
|
1135
1266
|
|
|
1136
|
-
// src/utils/user-agent.ts
|
|
1137
|
-
var USER_AGENTS;
|
|
1138
|
-
var init_user_agent = __esm({
|
|
1139
|
-
"src/utils/user-agent.ts"() {
|
|
1140
|
-
"use strict";
|
|
1141
|
-
USER_AGENTS = [
|
|
1142
|
-
// Chrome on Windows (updated 2025)
|
|
1143
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
1144
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
1145
|
-
// Chrome on macOS
|
|
1146
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
1147
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
1148
|
-
// Chrome on Linux
|
|
1149
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
1150
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
1151
|
-
// Firefox on Windows (updated 2025)
|
|
1152
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
1153
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
|
|
1154
|
-
// Firefox on macOS
|
|
1155
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
|
|
1156
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
|
|
1157
|
-
];
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
// src/web-research/scraper-types.ts
|
|
1162
|
-
var MAX_HTML_SIZE, MAX_PDF_SIZE, INTERNAL_NETWORK_PATTERNS, FILTERED_TAGS, BOT_PATTERNS, IMAGE_LINK_PATTERN, MARKDOWN_IMAGE_PATTERN;
|
|
1163
|
-
var init_scraper_types = __esm({
|
|
1164
|
-
"src/web-research/scraper-types.ts"() {
|
|
1165
|
-
"use strict";
|
|
1166
|
-
init_user_agent();
|
|
1167
|
-
MAX_HTML_SIZE = 25 * 1024 * 1024;
|
|
1168
|
-
MAX_PDF_SIZE = 100 * 1024 * 1024;
|
|
1169
|
-
INTERNAL_NETWORK_PATTERNS = [
|
|
1170
|
-
/^127\./,
|
|
1171
|
-
// IPv4 loopback
|
|
1172
|
-
/^0\./,
|
|
1173
|
-
// IPv4 "this" network
|
|
1174
|
-
/^::1$/,
|
|
1175
|
-
// IPv6 loopback
|
|
1176
|
-
/^fe80::/i,
|
|
1177
|
-
// IPv6 link-local
|
|
1178
|
-
/^f[cd][0-9a-f]{2}:/i,
|
|
1179
|
-
// IPv6 unique local — full fc00::/7 block (fc00::–fdff::)
|
|
1180
|
-
/^169\.254\./,
|
|
1181
|
-
// IPv4 link-local (and metadata)
|
|
1182
|
-
/^10\./,
|
|
1183
|
-
// RFC 1918 Class A private
|
|
1184
|
-
/^192\.168\./,
|
|
1185
|
-
// RFC 1918 Class C private
|
|
1186
|
-
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
1187
|
-
// RFC 1918 Class B private
|
|
1188
|
-
/^::ffff:(127\.|0\.|169\.254\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/i
|
|
1189
|
-
// IPv4-mapped IPv6
|
|
1190
|
-
];
|
|
1191
|
-
FILTERED_TAGS = [
|
|
1192
|
-
"nav",
|
|
1193
|
-
"header",
|
|
1194
|
-
"footer",
|
|
1195
|
-
"aside",
|
|
1196
|
-
"script",
|
|
1197
|
-
"style",
|
|
1198
|
-
"noscript",
|
|
1199
|
-
"form",
|
|
1200
|
-
"input",
|
|
1201
|
-
"select",
|
|
1202
|
-
"textarea",
|
|
1203
|
-
"button",
|
|
1204
|
-
"object",
|
|
1205
|
-
"embed",
|
|
1206
|
-
"svg",
|
|
1207
|
-
"symbol",
|
|
1208
|
-
"use",
|
|
1209
|
-
"defs",
|
|
1210
|
-
"path",
|
|
1211
|
-
"circle",
|
|
1212
|
-
"rect",
|
|
1213
|
-
"line",
|
|
1214
|
-
"polygon",
|
|
1215
|
-
"img",
|
|
1216
|
-
"iframe"
|
|
1217
|
-
];
|
|
1218
|
-
BOT_PATTERNS = [
|
|
1219
|
-
["_cf_chl_", "Cloudflare challenge"],
|
|
1220
|
-
["cf_chl_opt", "Cloudflare challenge opt"],
|
|
1221
|
-
["cdn-cgi/challenge-platform", "Cloudflare challenge platform"],
|
|
1222
|
-
["ddos-guard", "DDoS-Guard challenge"],
|
|
1223
|
-
["Just a moment...", "Cloudflare interstitial"],
|
|
1224
|
-
["Checking your browser before accessing", "Cloudflare interstitial"]
|
|
1225
|
-
];
|
|
1226
|
-
IMAGE_LINK_PATTERN = /\[([^\]]*)\]\((data:image\/[^)]+|[^)\s]+\.(?:svg|png|jpe?g|gif|webp|bmp|ico)(?:\?[^)]*)?)\)/gi;
|
|
1227
|
-
MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((?:data:image\/[^)]+|[^)\s]+)\)/gi;
|
|
1228
|
-
}
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
1267
|
// src/utils/circuit-breaker.ts
|
|
1232
1268
|
var CircuitBreaker;
|
|
1233
1269
|
var init_circuit_breaker = __esm({
|
|
@@ -1277,7 +1313,7 @@ var init_circuit_breaker = __esm({
|
|
|
1277
1313
|
const result = await action();
|
|
1278
1314
|
const duration = Date.now() - startTime;
|
|
1279
1315
|
metrics.observe("circuit_breaker_call_duration_ms", duration, { breaker: this.options.name, status: "success" });
|
|
1280
|
-
this.onSuccess();
|
|
1316
|
+
this.onSuccess(admittedAsProbe);
|
|
1281
1317
|
return result;
|
|
1282
1318
|
} catch (error) {
|
|
1283
1319
|
const duration = Date.now() - startTime;
|
|
@@ -1295,9 +1331,10 @@ var init_circuit_breaker = __esm({
|
|
|
1295
1331
|
if (admittedAsProbe) this.halfOpenInFlight = Math.max(0, this.halfOpenInFlight - 1);
|
|
1296
1332
|
}
|
|
1297
1333
|
}
|
|
1298
|
-
onSuccess() {
|
|
1334
|
+
onSuccess(admittedAsProbe) {
|
|
1299
1335
|
metrics.increment("circuit_breaker_success_total", 1, { breaker: this.options.name, state: this.state });
|
|
1300
1336
|
if (this.state === "HALF_OPEN") {
|
|
1337
|
+
if (!admittedAsProbe) return;
|
|
1301
1338
|
this.successCount++;
|
|
1302
1339
|
if (this.successCount >= this.options.halfOpenMaxCalls) {
|
|
1303
1340
|
this.transitionTo("CLOSED");
|
|
@@ -1599,12 +1636,14 @@ function isPrivateIpv6(ip) {
|
|
|
1599
1636
|
if (normalized === "::1") return true;
|
|
1600
1637
|
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
|
|
1601
1638
|
if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) return true;
|
|
1639
|
+
if (normalized.startsWith("fec") || normalized.startsWith("fed") || normalized.startsWith("fee") || normalized.startsWith("fef")) return true;
|
|
1602
1640
|
const mapped = extractMappedIpv4(normalized);
|
|
1603
1641
|
if (mapped) {
|
|
1604
1642
|
return isPrivateIp(mapped);
|
|
1605
1643
|
}
|
|
1606
1644
|
if (normalized.startsWith("2002:")) return true;
|
|
1607
1645
|
if (/^2001:0{0,4}:/.test(normalized) || normalized.startsWith("2001::")) return true;
|
|
1646
|
+
if (normalized.startsWith("64:ff9b:")) return true;
|
|
1608
1647
|
if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
|
|
1609
1648
|
return false;
|
|
1610
1649
|
}
|
|
@@ -1857,6 +1896,7 @@ import { platform as platform2 } from "node:os";
|
|
|
1857
1896
|
|
|
1858
1897
|
// src/infrastructure/browser/thread-worker-messaging.ts
|
|
1859
1898
|
init_log_utils();
|
|
1899
|
+
init_scraper_types();
|
|
1860
1900
|
import { appendFileSync as appendFileSync2 } from "node:fs";
|
|
1861
1901
|
var workerId2 = "";
|
|
1862
1902
|
var pageCreationLock = Promise.resolve();
|
|
@@ -2149,17 +2189,31 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2149
2189
|
validateForWorkerSync(ip.includes(":") ? `http://[${ip}]/` : `http://${ip}/`);
|
|
2150
2190
|
}
|
|
2151
2191
|
const contentType = await response?.headerValue("content-type") || "";
|
|
2192
|
+
const status = response?.status?.() ?? 0;
|
|
2152
2193
|
if (contentType.includes("application/pdf")) {
|
|
2153
2194
|
if (!response) throw new Error(`[Worker] No response received for PDF URL: ${url}`);
|
|
2195
|
+
if (status >= 400) throw new Error(`HTTP ${status}`);
|
|
2196
|
+
const declaredLength = parseInt(await response.headerValue("content-length").catch(() => null) || "", 10);
|
|
2197
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_PDF_SIZE) {
|
|
2198
|
+
const sizeMB = Math.round(declaredLength / 1024 / 1024);
|
|
2199
|
+
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
2200
|
+
}
|
|
2154
2201
|
const buffer = await response.body();
|
|
2202
|
+
if (buffer.byteLength > MAX_PDF_SIZE) {
|
|
2203
|
+
const sizeMB = Math.round(buffer.byteLength / 1024 / 1024);
|
|
2204
|
+
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
2205
|
+
}
|
|
2155
2206
|
await Promise.allSettled(pendingAddrChecks);
|
|
2156
2207
|
if (poisonedError) throw poisonedError;
|
|
2157
2208
|
await page.close();
|
|
2158
|
-
return { contentType, buffer, jitter: 0 };
|
|
2209
|
+
return { contentType, bufferB64: buffer.toString("base64"), jitter: 0 };
|
|
2159
2210
|
}
|
|
2160
2211
|
let html = await page.content();
|
|
2161
2212
|
const cfPatterns = ["_cf_chl_", "cdn-cgi/challenge-platform", "cf_chl_opt", "Just a moment...", "Checking your browser before accessing"];
|
|
2162
2213
|
const hasCloudflare = cfPatterns.some((pattern) => html.includes(pattern));
|
|
2214
|
+
if (status >= 400 && !hasCloudflare) {
|
|
2215
|
+
throw new Error(`HTTP ${status}`);
|
|
2216
|
+
}
|
|
2163
2217
|
if (hasCloudflare) {
|
|
2164
2218
|
logToDebugFile2("WARN", `[Worker-${workerId2}] Cloudflare challenge detected for: ${url}`);
|
|
2165
2219
|
try {
|
|
@@ -2184,6 +2238,10 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2184
2238
|
await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch((err) => logToDebugFile2("DEBUG", `[ThreadWorker] Swallowed page close/wait error: ${err.message || String(err)}`));
|
|
2185
2239
|
html = await page.content();
|
|
2186
2240
|
}
|
|
2241
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
2242
|
+
const sizeMB = Math.round(html.length / 1024 / 1024);
|
|
2243
|
+
throw new Error(`Browser HTML too large (${sizeMB}MB, max 25MB)`);
|
|
2244
|
+
}
|
|
2187
2245
|
const jitter = process.env["PI_RESEARCH_MOCK_SCRAPE"] === "true" ? 0 : Math.floor(Math.random() * 1e3) + 500;
|
|
2188
2246
|
if (jitter > 0) await new Promise((r) => setTimeout(r, jitter));
|
|
2189
2247
|
const finalAddr = await finalMainFrameAddr;
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -201,7 +201,7 @@ Services that hold resources implement `dispose()`; the registry disposes them i
|
|
|
201
201
|
reverse dependency order. Resolving through the registry (rather than direct imports)
|
|
202
202
|
enforces lifecycle discipline (init → use → dispose) and lets tests swap in mocks.
|
|
203
203
|
|
|
204
|
-
- Core (`src/core/`): `PlanningService`, `SchedulerService
|
|
204
|
+
- Core (`src/core/`): `PlanningService`, `SchedulerService`
|
|
205
205
|
- Infrastructure (`src/infrastructure/`): `StateManagerService`, `KnowledgeStoreService`, `MetricsService`, `WorkerPoolManager`, `FileLockService`, `GPUResourceService` (plus `WriterQueue`, defined in `src/knowledge/` and registered here)
|
|
206
206
|
- Orchestration (`src/orchestration/`): `ResearchOrchestrationService`, `ResearchSessionService`, `ResearchSynthesisService`
|
|
207
207
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-08-06
|
|
9
|
+
|
|
10
|
+
Bundles three unreleased fix rounds that accumulated on top of 1.2.0 (each
|
|
11
|
+
already pushed to main but never cut), plus a final fresh-eyes review pass
|
|
12
|
+
done specifically to gate this release: an independent reviewer read every
|
|
13
|
+
line of the combined diff against 1.2.0 with no knowledge of why any of it
|
|
14
|
+
was written, and found several real defects the rounds that produced the
|
|
15
|
+
code had missed — two of them the most severe in this release. Every item
|
|
16
|
+
below was re-verified against the current code (not the original bug
|
|
17
|
+
report) before being counted as fixed; the two highest-severity findings
|
|
18
|
+
were personally reproduced (one as an actual OOM crash) before dispatching
|
|
19
|
+
a fix.
|
|
20
|
+
|
|
21
|
+
A minor (not patch) version for the same reason 1.2.0 was: the range
|
|
22
|
+
changes the cancellation exit-code contract again, in the same category
|
|
23
|
+
the last release documented as minor-worthy. A cancelled research run that
|
|
24
|
+
had already collected at least one report used to report plain success
|
|
25
|
+
(`ok: true`, exit `0`) because the orchestrator's fallback-synthesis path
|
|
26
|
+
resolves normally on cancellation rather than throwing — `cmdResearch`'s
|
|
27
|
+
success branch never consulted the cancellation latch, only its error
|
|
28
|
+
branch did. It now exits `128 + signal` (or `130` for a programmatic
|
|
29
|
+
abort) and sets `cancelled: true` in `--json`, matching every other
|
|
30
|
+
cancellation-reporting path fixed in 1.2.0.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
**Found in this release's own fresh-eyes pass (new, on top of the three prior rounds):**
|
|
35
|
+
- **The chunker's own safety invariant was false — `overlap < targetSize` does not, in fact, prevent infinite loops.** Reproduced directly: `new Chunker({targetSize: 5, overlap: 4}).chunk(...)` on text containing a boundary-straddling emoji crashed the worker process with an out-of-memory error rather than merely running slowly. The UTF-16 surrogate-pair backoff (added earlier in this range) can land on the exact fixed point the forward-progress guard already enforces for `end`, so neither `start` nor `end` advances between iterations and the same chunk is pushed forever. A matching, milder gap let a lone high surrogate ship uncorrected into stored/embedded text whenever backing off would have violated that same guard. Both closed: the surrogate fix now grows past the low surrogate instead of silently doing nothing when shrinking would re-collide, and a new forward-progress invariant on `start` — mirroring the existing one on `end` — guarantees real per-iteration progress independent of any heuristic interaction, not just the ones already traced by hand. A brute-force sweep across small targetSize/overlap/emoji-position combinations backs the fix, not just the one reproducing triple.
|
|
36
|
+
- **Uninstall's "is this directory exclusively ours" check was a bare name match, not an ownership check.** `isPiResearchNamespaced()` treated any path segment literally equal to `pi-research` as sufficient proof a `PI_RESEARCH_STATE_DIR` override was safe to delete wholesale — but naming a directory after the tool pointed at it is a completely ordinary thing to do (e.g. organizing several tools' state under `~/agents-state/<tool-name>/`), and nothing stops a user from keeping unrelated content in that same folder. The one regression test guarding this path only ever planted pi-research-owned entries, never foreign content, so it could never have caught this. Now only the exact default `<dirName>/research` layout — something the runtime actually constructs itself and can therefore prove is exclusively its own — authorizes wholesale removal; every other override, however named, only ever loses the specific entries the runtime is known to create there.
|
|
37
|
+
- **A researcher could still run a full billed session after its run was told to stop.** The three fast-stop checkpoints added earlier in this range (top-of-loop, initial-links loop, post-backoff sleep) left one window uncovered: between `createResearcherSession()` resolving and the session being registered for `abortAllSessions()`'s visibility, a few lines later behind an awaited call. A stop tripping in exactly that gap was invisible to the abort scan, and the caller's bounded ~10s wait for self-stopping researchers elapsed before the session's own LLM call and scrape burst finished — the run returned and released its slot while the researcher kept going in the background. A fourth checkpoint immediately after registration closes it.
|
|
38
|
+
- **A "FIX" comment described a circuit-breaker key format that has never existed in production.** `research-cleanup.ts` cleared session breakers assuming they're keyed `${piSessionId}-<8hex>`, but every real caller keys them by the plain `researchId` (generated independently of the pi session) — the function that would produce the assumed format has no production caller at all. The clear was consequently a no-op, masked on the normal run-completion path by a second, correctly-keyed clear elsewhere; the gap only mattered when this was the sole cleanup path (a throw before the orchestrator ever runs). Fixed to clear by the real key; the now-unused prefix-clearing utility this bug motivated, along with its own comment repeating the same wrong premise, is removed rather than left as bait for the same mistake again.
|
|
39
|
+
- **A follower's healthcheck request couldn't be cancelled the way search/scrape already can.** The abort-signal plumbing added earlier in this range for search/scrape was never extended to healthcheck: the interface type carries no `signal` parameter and the one production caller always calls without one — currently harmless, but silently ready to reintroduce "follower cancellation burns a leader pool slot to timeout" the moment any caller starts passing a real signal, with no compiler error to flag it. Wired through, mirroring search/scrape exactly.
|
|
40
|
+
- **Routine follower-cancellation was logged and counted as a server error.** The same abort-signal work introduced rejections shaped like `"Task search aborted while running"`, which the pool-shutdown error classifier doesn't recognize — so every cancelled in-flight browser task (a normal, frequent event whenever a research run is cancelled) logged at ERROR and inflated `browser_search_errors_total`/`browser_scrape_errors_total` before the layer that correctly demotes it ever saw the error. Each of the three call sites now checks the actual signal the rejection is a consequence of, rather than pattern-matching text — deliberately not widened inside the shared classifier, which a different caller also uses to detect a genuine leader handover.
|
|
41
|
+
- **Severity filtering for a security-database search normalized only one of three backends.** `getSeverityParam()` was applied on the NVD leg only; GitHub and OSV read the raw, unvalidated value directly — reproducing, for two of three backends, the exact "unrecognized value silently filters everything to zero" bug an earlier round in this range fixed for NVD. Masked today because the sole production caller does its own separate, stricter validation first. Now normalized once, ahead of all three legs.
|
|
42
|
+
- A flaky CI failure surfaced during this release's own gate run traced to a test relying on a fixed 50ms sleep to assume a lock had been fully acquired before tearing it down — on a loaded runner, teardown could occasionally win the race and land in a different, already-correctly-handled code path than the one the test meant to exercise. Replaced with a real "critical section has started" signal; no production code was at fault.
|
|
43
|
+
|
|
44
|
+
**From the first post-1.2.0 audit round** (orchestration, browser, embedding/state, knowledge, CLI/config, utils, tools, SDK/planning — ~40 defects): aborted researchers restarting as untracked zombies (an `=== 'Aborted'` string match missed the actual sentinel shape); PDF content zeroed over cluster IPC and the resulting extraction error cached as a successful scrape; the stale-profile sweep touching arbitrary shared-tmp entries outside its own namespace; cluster workers leaking the research query into `ps` output; `clearEmbeddingServer` gaining compare-and-delete semantics (a mere follower's clean shutdown could deregister a live leader); `getEmbedder` refusing to join an in-flight init for a different model/device; a code-fence-straddling chunk boundary emitting one overlap-sliver per character; an explicit `--config` file's keys previously applying nowhere when shadowed by the ambient overlay; CLI flags silently swallowing their own values (`--model --json` running against the literal model `"--json"`); nested embedder-init stdio capture losing LIFO ordering on a mid-capture cancel; the scrape context gate billing pre-slice batch counts; StackExchange rate-limit/quota state not surviving across tool calls; and NVD severity filtering not normalizing case or the MODERATE→MEDIUM synonym.
|
|
45
|
+
|
|
46
|
+
**From the second post-1.2.0 audit round** (browser/web-research, orchestration/SDK, knowledge/embedding, CLI/config/install — ~38 defects, including 2 regressions in the first round's own diff caught by that round's fresh-eyes pass): browser scrapes never checking HTTP status, so 4xx/5xx error pages were cached as citable content; the 100MB PDF / 25MB HTML size caps enforced only after the worker had already buffered and IPC'd the full body; search failures always hitting the global circuit breaker instead of the run's own; a follower's cancelled run burning its leader's pool slot to full timeout instead of aborting on socket close; fast-stop not reaching a researcher still acquiring links, sleeping between attempts, or rebuilding its session; the inter-attempt backoff sleep being non-abortable; a poisoned embedding leader staying eligible to re-elect onto GPU in the same process; NaN embedding vectors laundering to 0 across the HTTP hop; the full-text-search skip-guard's version bookkeeping missing rows committed mid-rebuild; a missing/unreadable explicit `--config` file being silently ignored instead of erroring; duplicate SIGINT delivery (process-group plus launcher-forwarded) hitting a consumed one-shot handler and force-exiting mid-cleanup; and the uninstall script's original, pre-this-release ownership gap for `PI_RESEARCH_STATE_DIR` (the one further hardened above).
|
|
47
|
+
|
|
48
|
+
**From the backlog-clearing round** (re-auditing every item deferred since v1.0.7 plus both rounds above): the cancellation-reporting gap described above; `knowledge --json` success payloads not carrying `ok: true` despite claiming parity with the error shape, and `knowledge-config` having no `--json` error handling at all; the SSRF IPv6 gate missing `fec0::/10` and the NAT64 well-known prefix `64:ff9b::/96` (a real bypass on hosts whose egress traverses a NAT64 gateway, reachable via `64:ff9b::169.254.169.254`); steering messages already consumed by a finished run leaking into the next unrelated run's system-prompt injection; `clearBrowserServer` gaining the same compare-and-delete semantics its embedding-leader sibling already had; a second, independent chunker surrogate-pair gap (raw UTF-16 boundaries with no pair awareness); and steering-extended round budgets being invisible to the round-phase prompt logic (`PlanningService` recomputed `maxRounds` from the base complexity table only). `HealthCheckService` — zero runtime consumers, fully superseded by the live `src/healthcheck/registry.ts` system — was deleted.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- **A cancelled-but-partially-successful research run now reports its true status.** See above — this is the same exit-code contract 1.2.0 established, extended to a success-path case that fix didn't reach.
|
|
52
|
+
|
|
8
53
|
## [1.2.0] - 2026-08-05
|
|
9
54
|
|
|
10
55
|
Clears the medium/low findings deferred out of the 1.1.2 audit, then a second
|
|
@@ -256,20 +301,73 @@ and the host-version self-check gained the tested-ceiling warning.
|
|
|
256
301
|
|
|
257
302
|
---
|
|
258
303
|
|
|
259
|
-
## Deferred Findings
|
|
260
|
-
|
|
261
|
-
The
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
304
|
+
## Deferred Findings — cleared 2026-08-06
|
|
305
|
+
|
|
306
|
+
The v1.0.7-era list below was re-audited against the current tree, three weeks
|
|
307
|
+
and many fix rounds after it was written. Every item resolved one of two ways:
|
|
308
|
+
already fixed as a side effect of unrelated work since, or fixed directly in
|
|
309
|
+
this pass with a regression test. None remain open.
|
|
310
|
+
|
|
311
|
+
- **KnowledgeStore.close() timeout behavior** — already safe: writes register
|
|
312
|
+
into the active-write set before the close-in-progress check can be seen,
|
|
313
|
+
and LanceDB table handles keep working after the owning connection closes,
|
|
314
|
+
so a timed-out close no longer strands or corrupts an in-flight write.
|
|
315
|
+
- **Cross-session teardown via shared singletons** — not reachable: pi fully
|
|
316
|
+
serializes a session's teardown before the next one is created, so shared
|
|
317
|
+
singletons are never disposed out from under a still-live session.
|
|
318
|
+
- **SIGINT-adjacent cancellation race** — real gap, fixed: a cancelled run
|
|
319
|
+
that had already produced a report returned through the orchestrator's
|
|
320
|
+
normal (non-throwing) partial-synthesis path, so the CLI's success branch
|
|
321
|
+
never saw the cancellation latch and reported `ok:true`/exit 0 for a run
|
|
322
|
+
the user had stopped. The success path now checks the latch and reports
|
|
323
|
+
the same `cancelled: true` / `128+signal` contract the error path already
|
|
324
|
+
used.
|
|
325
|
+
- **Knowledge-store migration vs. concurrent access** — already safe: a
|
|
326
|
+
cross-process file lock serializes migrations, and every read/write
|
|
327
|
+
re-resolves its table handle, so a migration racing another process's
|
|
328
|
+
handle fails that operation cleanly instead of serving stale data.
|
|
329
|
+
- **Global activity gate reused for per-session decisions** — real gap,
|
|
330
|
+
fixed: steering messages already consumed by a finished research run
|
|
331
|
+
survived in session state and leaked into the next, unrelated run's
|
|
332
|
+
system-prompt injection and final-report attribution. Only genuinely
|
|
333
|
+
still-queued messages now survive a run boundary.
|
|
334
|
+
- **Zombie queued browser tasks after timeout** — already fixed (in the
|
|
335
|
+
first post-1.2.0 audit round): a queued task's client-side timeout now
|
|
336
|
+
actually removes it from the scheduler's queue instead of leaving it to
|
|
337
|
+
be dispatched later.
|
|
338
|
+
- **Non-atomic `clearBrowserServer`** — real gap, fixed: the file write
|
|
339
|
+
itself was already atomic, but the read-decide-clear sequence around it
|
|
340
|
+
was not — a dead-leader restart path could wipe a legitimately new
|
|
341
|
+
leader's registration elected in the same window. `clearBrowserServer`
|
|
342
|
+
now takes an optional expected-owner argument and compares-and-deletes
|
|
343
|
+
inside the lock, mirroring the embedding leader's existing CAS.
|
|
344
|
+
- **Inconsistent `knowledge --json` payload shapes** — real gap, fixed: the
|
|
345
|
+
success payload never carried `ok:true` despite a comment claiming parity
|
|
346
|
+
with the error shape, and `knowledge-config`'s failure path had no
|
|
347
|
+
try/catch at all and fell through to a plain-text-only handler even under
|
|
348
|
+
`--json`. Both now emit a consistent, documented shape.
|
|
349
|
+
- **Backward-clock-step tolerance across multiple rate limiters** — already
|
|
350
|
+
safe: every limiter's timing arithmetic leans toward waiting longer on a
|
|
351
|
+
negative clock delta, never toward a burst or a permanent wedge.
|
|
352
|
+
|
|
353
|
+
Two further items surfaced and closed in the same pass, found via later
|
|
354
|
+
audit rounds' own deferred lists rather than the v1.0.7 one:
|
|
355
|
+
- **SSRF: `isPrivateIpv6` missed `fec0::/10`** (deprecated site-local) and
|
|
356
|
+
**`64:ff9b::/96`** (the NAT64 well-known prefix — actively deployed today,
|
|
357
|
+
and a real bypass vector on a host whose egress traverses a NAT64
|
|
358
|
+
gateway). Both ranges are now blocked.
|
|
359
|
+
- **Chunk boundaries could split a UTF-16 surrogate pair**, corrupting one
|
|
360
|
+
character (renders as U+FFFD) in stored/embedded text at an unlucky
|
|
361
|
+
boundary — the same class of bug already handled in three other places
|
|
362
|
+
in the codebase, now handled here too, on both the truncation and the
|
|
363
|
+
overlap-derived start boundary.
|
|
364
|
+
- **Steering-extended round budgets weren't visible to the round-phase
|
|
365
|
+
prompt.** When mid-run steering extended the round cap beyond the base
|
|
366
|
+
per-complexity value, the planning service still computed guidance
|
|
367
|
+
against the base value, producing an impossible "Round 4 of 3" and,
|
|
368
|
+
more consequentially, prematurely selecting LATE-phase framing during
|
|
369
|
+
rounds steering had explicitly unlocked.
|
|
370
|
+
|
|
371
|
+
One dead-code cleanup: `HealthCheckService` (a superseded, zero-consumer
|
|
372
|
+
predecessor of the live `src/healthcheck/registry.ts` system) was removed.
|
|
275
373
|
|