@lincoln504/pi-research 1.4.1 → 1.5.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/README.md +1 -1
- package/dist/cli.mjs +449 -187
- package/dist/prompts/research-knowledge-relevance-triage.md +2 -0
- package/dist/prompts/research-knowledge-search-extractor.md +2 -0
- package/dist/prompts/researcher.md +2 -14
- package/dist/thread-worker.mjs +91 -59
- package/docs/AGENT-SKILL.md +4 -3
- package/docs/CHANGELOG.md +120 -0
- package/docs/CONFIGURATION.md +2 -2
- package/docs/SDK.md +1 -1
- package/package.json +1 -1
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +44 -3
- package/src/config.ts +36 -13
- package/src/constants.ts +23 -4
- package/src/core/llm/research-model-resolver.ts +38 -0
- package/src/core/planning-service.ts +67 -9
- package/src/index.ts +3 -1
- package/src/infrastructure/browser/browser-task-scheduler.ts +16 -9
- package/src/infrastructure/browser/config.ts +40 -2
- package/src/infrastructure/browser/thread-worker-browser.ts +69 -48
- package/src/infrastructure/browser/thread-worker-messaging.ts +65 -4
- package/src/infrastructure/browser/thread-worker.mjs +91 -59
- package/src/infrastructure/browser/thread-worker.ts +13 -5
- package/src/infrastructure/embedding/embedding-server.ts +54 -0
- package/src/knowledge/embedder-init.ts +3 -2
- package/src/knowledge/embedder.ts +11 -3
- package/src/knowledge/index.ts +36 -10
- package/src/knowledge/store.ts +85 -5
- package/src/observers/research-observer-impl.ts +14 -6
- package/src/orchestration/deep-research-orchestrator.ts +3 -1
- package/src/orchestration/quick-research-orchestrator.ts +29 -10
- package/src/orchestration/research-synthesis-service.ts +32 -0
- package/src/orchestration/researcher-executor.ts +31 -16
- package/src/orchestration/researcher.ts +2 -2
- package/src/orchestration/session-state.ts +35 -4
- package/src/prompts/research-knowledge-relevance-triage.md +2 -0
- package/src/prompts/research-knowledge-search-extractor.md +2 -0
- package/src/prompts/researcher.md +2 -14
- package/src/sdk.ts +16 -0
- package/src/skill-install/skill-installer.ts +5 -1
- package/src/stackexchange/index.ts +3 -0
- package/src/stackexchange/rest-client.ts +7 -12
- package/src/tools/index.ts +2 -1
- package/src/tools/research-knowledge-search.ts +37 -4
- package/src/tools/research-tool-definition.ts +54 -12
- package/src/tools/scrape.ts +8 -1
- package/src/tools/security.ts +13 -10
- package/src/utils/coverage-digest.ts +1 -1
- package/src/utils/host-config.ts +12 -1
- package/src/utils/log-utils.ts +15 -2
- package/src/utils/metrics-summary.ts +21 -1
- package/src/utils/tool-usage-tracker.ts +6 -3
- package/src/web-research/browser-search.ts +11 -4
- package/src/web-research/web-scraper.ts +36 -16
- package/src/youtube/index.ts +11 -1
- package/src/youtube/potoken.ts +8 -1
- package/src/youtube/transcript-client.ts +56 -5
package/dist/cli.mjs
CHANGED
|
@@ -241,6 +241,8 @@ function redactSecrets(message) {
|
|
|
241
241
|
}
|
|
242
242
|
if (out.length > MAX_LOG_MESSAGE_LENGTH) {
|
|
243
243
|
out = `${out.slice(0, keep)}\u2026[truncated ${Math.max(rawLength, out.length) - keep} chars]`;
|
|
244
|
+
} else if (rawLength > scanEnd) {
|
|
245
|
+
out = `${out}\u2026[truncated ${rawLength - scanEnd} chars]`;
|
|
244
246
|
}
|
|
245
247
|
return out;
|
|
246
248
|
}
|
|
@@ -2279,7 +2281,9 @@ import { createRequire as createRequire2 } from "node:module";
|
|
|
2279
2281
|
function getConfigDirName() {
|
|
2280
2282
|
if (_configDirName !== void 0) return _configDirName;
|
|
2281
2283
|
const override = process.env["PI_RESEARCH_CONFIG_DIR_NAME"];
|
|
2282
|
-
if (override)
|
|
2284
|
+
if (override && !override.includes("/") && !override.includes("\\") && override !== "." && override !== "..") {
|
|
2285
|
+
return _configDirName = override;
|
|
2286
|
+
}
|
|
2283
2287
|
try {
|
|
2284
2288
|
const require2 = createRequire2(import.meta.url);
|
|
2285
2289
|
const host = require2("@earendil-works/pi-coding-agent");
|
|
@@ -2639,10 +2643,10 @@ function loadEnvFiles(cwd, iface) {
|
|
|
2639
2643
|
try {
|
|
2640
2644
|
logger.warn(`[config] WARNING: .pi-research.env files are deprecated. Settings will be auto-migrated to the centralized registry. Remove ${legacyPath} after migration.`);
|
|
2641
2645
|
legacyEnv = parseDotEnv(fs5.readFileSync(legacyPath, "utf-8"));
|
|
2642
|
-
Object.assign(merged, legacyEnv);
|
|
2643
2646
|
const legacyLocal = Object.fromEntries(
|
|
2644
2647
|
Object.entries(legacyEnv).filter(([k]) => LOCAL_SCOPE_KEYS.has(k))
|
|
2645
2648
|
);
|
|
2649
|
+
Object.assign(merged, legacyLocal);
|
|
2646
2650
|
const existingLocal = Object.fromEntries(
|
|
2647
2651
|
Object.entries(findRegistryEntry(registry, normalizedCwd) ?? {}).filter(([k]) => LOCAL_SCOPE_KEYS.has(k))
|
|
2648
2652
|
);
|
|
@@ -2672,7 +2676,10 @@ function loadEnvFiles(cwd, iface) {
|
|
|
2672
2676
|
}
|
|
2673
2677
|
}
|
|
2674
2678
|
} else if (Object.keys(merged).length === 0 && !fs5.existsSync(legacyPath) && !fs5.existsSync(globalPath)) {
|
|
2675
|
-
|
|
2679
|
+
if (!warnedMissingConfigWorkspaces.has(normalizedCwd)) {
|
|
2680
|
+
warnedMissingConfigWorkspaces.add(normalizedCwd);
|
|
2681
|
+
logger.warn(`[config] No configuration found for workspace: ${cwd}. Using code defaults. Run /research-config to configure.`);
|
|
2682
|
+
}
|
|
2676
2683
|
}
|
|
2677
2684
|
warnLegacyEnvVarsOnce(merged);
|
|
2678
2685
|
return merged;
|
|
@@ -2961,7 +2968,7 @@ function parseEnvEnum(env, key, allowed, def) {
|
|
|
2961
2968
|
logger.warn(`[config] Invalid ${key}="${raw}" (expected one of: ${allowed.join(", ")}). Using default "${def}".`);
|
|
2962
2969
|
return def;
|
|
2963
2970
|
}
|
|
2964
|
-
var ConfigSchema, DEFAULTS, LOCAL_SCOPE_KEYS, USER_MIGRATION_KEYS, RECLAIM_FILE_SWEEP_AGE_MS, LEGACY_ENV_VARS, warnedLegacyEnvVars, configCache;
|
|
2971
|
+
var ConfigSchema, DEFAULTS, LOCAL_SCOPE_KEYS, USER_MIGRATION_KEYS, RECLAIM_FILE_SWEEP_AGE_MS, LEGACY_ENV_VARS, warnedLegacyEnvVars, warnedMissingConfigWorkspaces, configCache;
|
|
2965
2972
|
var init_config = __esm({
|
|
2966
2973
|
"src/config.ts"() {
|
|
2967
2974
|
"use strict";
|
|
@@ -3031,10 +3038,16 @@ var init_config = __esm({
|
|
|
3031
3038
|
SEARCH_TIMEOUT_MS: Type.Number({ minimum: 5e3, maximum: 12e4, default: 45e3 }),
|
|
3032
3039
|
/** TUI refresh debounce in milliseconds (default: 100ms) */
|
|
3033
3040
|
TUI_REFRESH_DEBOUNCE_MS: Type.Number({ minimum: 0, maximum: 1e3, default: 100 }),
|
|
3034
|
-
/**
|
|
3035
|
-
*
|
|
3036
|
-
* SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this
|
|
3037
|
-
*
|
|
3041
|
+
/** Overhead margin (ms) added on top of each browser operation's own nav timeout to
|
|
3042
|
+
* form its scheduler task-timeout ceiling: a search task times out after
|
|
3043
|
+
* SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this — covering
|
|
3044
|
+
* per-task work beyond raw navigation (page setup, result parsing). Queue wait is
|
|
3045
|
+
* NOT part of this margin: the deadline is armed on dispatch, not on enqueue, so
|
|
3046
|
+
* time spent waiting for a free worker never counts against it (see
|
|
3047
|
+
* browser-task-scheduler.ts). Cold browser-launch/context-creation time is also
|
|
3048
|
+
* separate from this margin — COLD_START_ALLOWANCE_MS
|
|
3049
|
+
* (infrastructure/browser/config.ts) covers that unconditionally on top of it,
|
|
3050
|
+
* since it isn't something a user should need to tune.
|
|
3038
3051
|
* (default: 10000) */
|
|
3039
3052
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
|
|
3040
3053
|
/** Timeout for coordinator/research-lead/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
@@ -3174,6 +3187,7 @@ var init_config = __esm({
|
|
|
3174
3187
|
PROXY_URL: "no longer read \u2014 the SearXNG proxy path was removed in v1.0.0 and searches/scrapes connect directly"
|
|
3175
3188
|
};
|
|
3176
3189
|
warnedLegacyEnvVars = false;
|
|
3190
|
+
warnedMissingConfigWorkspaces = /* @__PURE__ */ new Set();
|
|
3177
3191
|
configCache = /* @__PURE__ */ new Map();
|
|
3178
3192
|
}
|
|
3179
3193
|
});
|
|
@@ -3186,10 +3200,11 @@ function getMaxGatheringCalls(config) {
|
|
|
3186
3200
|
return MAX_GATHERING_CALLS;
|
|
3187
3201
|
}
|
|
3188
3202
|
}
|
|
3189
|
-
function getMaxScrapeBatches(config) {
|
|
3203
|
+
function getMaxScrapeBatches(config, cachingActive = false) {
|
|
3190
3204
|
try {
|
|
3191
3205
|
const batches = (config || getConfig()).MAX_SCRAPE_BATCHES;
|
|
3192
|
-
|
|
3206
|
+
if (batches === 0 || batches > 99) return 999999;
|
|
3207
|
+
return cachingActive ? Math.min(batches + 1, 99) : batches;
|
|
3193
3208
|
} catch {
|
|
3194
3209
|
return 2;
|
|
3195
3210
|
}
|
|
@@ -4330,12 +4345,12 @@ async function isModelCached(model) {
|
|
|
4330
4345
|
const cacheDir = env.cacheDir;
|
|
4331
4346
|
if (!cacheDir) return false;
|
|
4332
4347
|
const { access: access3, readdir: readdir6, readFile: readFile7, stat: stat6, rm: rm3 } = await import("node:fs/promises");
|
|
4333
|
-
const
|
|
4334
|
-
const modelDir =
|
|
4335
|
-
const onnxDir =
|
|
4336
|
-
await access3(
|
|
4348
|
+
const path20 = await import("node:path");
|
|
4349
|
+
const modelDir = path20.default.join(cacheDir, model);
|
|
4350
|
+
const onnxDir = path20.default.join(modelDir, "onnx");
|
|
4351
|
+
await access3(path20.default.join(onnxDir, "model.onnx"));
|
|
4337
4352
|
try {
|
|
4338
|
-
const cfgRaw = await readFile7(
|
|
4353
|
+
const cfgRaw = await readFile7(path20.default.join(modelDir, "config.json"), "utf-8");
|
|
4339
4354
|
JSON.parse(cfgRaw);
|
|
4340
4355
|
} catch {
|
|
4341
4356
|
return false;
|
|
@@ -4343,7 +4358,7 @@ async function isModelCached(model) {
|
|
|
4343
4358
|
const tokenizerFiles = ["tokenizer.json", "tokenizer.model", "vocab.txt"];
|
|
4344
4359
|
let hasTokenizer = false;
|
|
4345
4360
|
for (const f of tokenizerFiles) {
|
|
4346
|
-
const st = await stat6(
|
|
4361
|
+
const st = await stat6(path20.default.join(modelDir, f)).catch(() => null);
|
|
4347
4362
|
if (st && st.size > 0) {
|
|
4348
4363
|
hasTokenizer = true;
|
|
4349
4364
|
break;
|
|
@@ -4353,7 +4368,7 @@ async function isModelCached(model) {
|
|
|
4353
4368
|
const entries = await readdir6(onnxDir).catch(() => []);
|
|
4354
4369
|
const hasExternalWeights = entries.includes("model.onnx_data");
|
|
4355
4370
|
if (hasExternalWeights) {
|
|
4356
|
-
const dataStat = await stat6(
|
|
4371
|
+
const dataStat = await stat6(path20.default.join(onnxDir, "model.onnx_data")).catch(() => null);
|
|
4357
4372
|
if (!dataStat || dataStat.size === 0) return false;
|
|
4358
4373
|
}
|
|
4359
4374
|
const partialArtifacts = entries.filter(
|
|
@@ -4362,7 +4377,7 @@ async function isModelCached(model) {
|
|
|
4362
4377
|
if (partialArtifacts.length > 0) {
|
|
4363
4378
|
if (hasExternalWeights) {
|
|
4364
4379
|
for (const f of partialArtifacts) {
|
|
4365
|
-
await rm3(
|
|
4380
|
+
await rm3(path20.default.join(onnxDir, f), { force: true }).catch(() => {
|
|
4366
4381
|
});
|
|
4367
4382
|
}
|
|
4368
4383
|
} else {
|
|
@@ -4384,10 +4399,10 @@ async function purgeModelCache(model) {
|
|
|
4384
4399
|
const cacheDir = env.cacheDir;
|
|
4385
4400
|
if (!cacheDir) return;
|
|
4386
4401
|
const { rm: rm3 } = await import("node:fs/promises");
|
|
4387
|
-
const
|
|
4388
|
-
const resolvedCache =
|
|
4389
|
-
const modelDir =
|
|
4390
|
-
if (!model ||
|
|
4402
|
+
const path20 = await import("node:path");
|
|
4403
|
+
const resolvedCache = path20.default.resolve(cacheDir);
|
|
4404
|
+
const modelDir = path20.default.resolve(resolvedCache, model);
|
|
4405
|
+
if (!model || path20.default.isAbsolute(model) || modelDir === resolvedCache || !modelDir.startsWith(resolvedCache + path20.default.sep)) {
|
|
4391
4406
|
logger.warn(`[embedder] Refusing to purge unsafe model-cache path for model id ${JSON.stringify(model)}`);
|
|
4392
4407
|
return;
|
|
4393
4408
|
}
|
|
@@ -4470,7 +4485,7 @@ async function handleWebGPULoadError(loadErr, pipeline, stateManager, gpuLockHel
|
|
|
4470
4485
|
return { success: false, error };
|
|
4471
4486
|
}
|
|
4472
4487
|
}
|
|
4473
|
-
async function handleWebGPUWarmupError(warmupErr, pipeline, stateManager, gpuLockHeld, model, initializationTimeoutMs, useCache) {
|
|
4488
|
+
async function handleWebGPUWarmupError(warmupErr, pipeline, stateManager, gpuLockHeld, model, initializationTimeoutMs, useCache, poolingMode = "mean") {
|
|
4474
4489
|
markWebGpuFallback();
|
|
4475
4490
|
logger.warn(
|
|
4476
4491
|
"[embedder] WebGPU error during warmup \u2014 falling back to CPU. The underlying cause (commonly VRAM exhaustion) is in the native Dawn/ONNX output logged just above this line."
|
|
@@ -4487,7 +4502,7 @@ async function handleWebGPUWarmupError(warmupErr, pipeline, stateManager, gpuLoc
|
|
|
4487
4502
|
try {
|
|
4488
4503
|
const cpuPipeline = await loadModelOnCPU(model, initializationTimeoutMs, useCache);
|
|
4489
4504
|
logger.info("[embedder] CPU pipeline loaded successfully");
|
|
4490
|
-
const { dummy, success, error: cpuWarmupErr } = await warmupPipeline(cpuPipeline,
|
|
4505
|
+
const { dummy, success, error: cpuWarmupErr } = await warmupPipeline(cpuPipeline, poolingMode, useCache);
|
|
4491
4506
|
if (!success || cpuWarmupErr) {
|
|
4492
4507
|
logger.error("[embedder] CPU fallback pipeline warmup failed:", cpuWarmupErr);
|
|
4493
4508
|
if (cpuPipeline) {
|
|
@@ -4906,7 +4921,8 @@ var init_embedder = __esm({
|
|
|
4906
4921
|
this.gpuLockHeld,
|
|
4907
4922
|
this.model,
|
|
4908
4923
|
this.initializationTimeoutMs,
|
|
4909
|
-
this.useCache
|
|
4924
|
+
this.useCache,
|
|
4925
|
+
this.poolingMode
|
|
4910
4926
|
);
|
|
4911
4927
|
if (!result.success) {
|
|
4912
4928
|
throw result.error;
|
|
@@ -4968,9 +4984,12 @@ var init_embedder = __esm({
|
|
|
4968
4984
|
truncateText(text) {
|
|
4969
4985
|
const maxChars = this.maxTokens * this.charsPerToken;
|
|
4970
4986
|
if (text.length > maxChars) {
|
|
4987
|
+
let keep = maxChars;
|
|
4988
|
+
const lastKeptUnit = text.charCodeAt(keep - 1);
|
|
4989
|
+
if (lastKeptUnit >= 55296 && lastKeptUnit <= 56319) keep -= 1;
|
|
4971
4990
|
metrics.increment("embedder_truncations_total", 1, { model: this.model });
|
|
4972
|
-
logger.debug(`[embedder] Truncated input for embedding: ${text.length} -> ${
|
|
4973
|
-
return text.slice(0,
|
|
4991
|
+
logger.debug(`[embedder] Truncated input for embedding: ${text.length} -> ${keep} chars (model=${this.model})`);
|
|
4992
|
+
return text.slice(0, keep);
|
|
4974
4993
|
}
|
|
4975
4994
|
return text;
|
|
4976
4995
|
}
|
|
@@ -5432,6 +5451,11 @@ var init_embedding_server = __esm({
|
|
|
5432
5451
|
hardDeadlineMs;
|
|
5433
5452
|
onPoison;
|
|
5434
5453
|
poisoned = null;
|
|
5454
|
+
// The currently-executing task's real work promise (not the soft-timeout race) —
|
|
5455
|
+
// set only while a native call is actually in flight. Lets a caller that needs
|
|
5456
|
+
// to dispose the underlying pipeline (EmbeddingServer.shutdown) wait for true
|
|
5457
|
+
// idleness instead of racing disposal against a still-running inference.
|
|
5458
|
+
currentWork = null;
|
|
5435
5459
|
constructor(maxDepth = 200, timeoutMs = 12e4, hardDeadlineMs = 6e5, onPoison) {
|
|
5436
5460
|
this.maxDepth = maxDepth;
|
|
5437
5461
|
this.timeoutMs = timeoutMs;
|
|
@@ -5457,6 +5481,7 @@ var init_embedding_server = __esm({
|
|
|
5457
5481
|
let hardTimer;
|
|
5458
5482
|
try {
|
|
5459
5483
|
work = fn();
|
|
5484
|
+
this.currentWork = work;
|
|
5460
5485
|
hardTimer = setTimeout(() => {
|
|
5461
5486
|
if (settled) return;
|
|
5462
5487
|
this.poison(new Error(
|
|
@@ -5477,6 +5502,7 @@ var init_embedding_server = __esm({
|
|
|
5477
5502
|
} finally {
|
|
5478
5503
|
settled = true;
|
|
5479
5504
|
if (hardTimer !== void 0) clearTimeout(hardTimer);
|
|
5505
|
+
if (this.currentWork === work) this.currentWork = null;
|
|
5480
5506
|
}
|
|
5481
5507
|
}
|
|
5482
5508
|
};
|
|
@@ -5484,6 +5510,40 @@ var init_embedding_server = __esm({
|
|
|
5484
5510
|
if (!this.running) void this.pump();
|
|
5485
5511
|
});
|
|
5486
5512
|
}
|
|
5513
|
+
/**
|
|
5514
|
+
* Resolve once no native call is in flight AND no queued task is still due
|
|
5515
|
+
* to run. Used by EmbeddingServer.shutdown() to wait out a still-running
|
|
5516
|
+
* inference before disposing the pipeline — without this, disposal could
|
|
5517
|
+
* run concurrently with a legitimately slow (not hung) embed/embedMany call
|
|
5518
|
+
* on the same re-entrant-locked GPU context, the exact native-segfault
|
|
5519
|
+
* class this queue exists to prevent. No timeout: a promise that truly
|
|
5520
|
+
* never settles is already an accepted, pre-existing leak-to-process-exit
|
|
5521
|
+
* scenario elsewhere in this class (see the poison path's
|
|
5522
|
+
* disposeEmbedder=false handling) — better to leak than risk concurrent
|
|
5523
|
+
* native access.
|
|
5524
|
+
*
|
|
5525
|
+
* Checking only `currentWork` is NOT enough with a second task already
|
|
5526
|
+
* queued: a task's `run()` clears `currentWork` to null in its `finally`
|
|
5527
|
+
* (once its native call has settled), and `pump()` only claims it again for
|
|
5528
|
+
* the next task on a LATER microtask tick, once `run()` for the current
|
|
5529
|
+
* task has fully unwound and `await entry.run()` resumes. If a caller's own
|
|
5530
|
+
* check happens to land in that gap, `currentWork` reads as null even
|
|
5531
|
+
* though a second native call is about to start (or a queue in the process
|
|
5532
|
+
* of unwinding to its next task hasn't reached it yet) — exactly the window
|
|
5533
|
+
* disposal must not run in. `running` stays true for the queue's entire
|
|
5534
|
+
* drain, across every task in `tasks`, not just the current one, so it has
|
|
5535
|
+
* no equivalent gap and closes this one.
|
|
5536
|
+
*/
|
|
5537
|
+
async waitForIdle() {
|
|
5538
|
+
while (this.currentWork || this.running) {
|
|
5539
|
+
if (this.currentWork) {
|
|
5540
|
+
await this.currentWork.catch(() => {
|
|
5541
|
+
});
|
|
5542
|
+
} else {
|
|
5543
|
+
await Promise.resolve();
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5487
5547
|
poison(err) {
|
|
5488
5548
|
if (this.poisoned) return;
|
|
5489
5549
|
this.poisoned = err;
|
|
@@ -5665,6 +5725,7 @@ var init_embedding_server = __esm({
|
|
|
5665
5725
|
this.server = null;
|
|
5666
5726
|
}
|
|
5667
5727
|
if (disposeEmbedder) {
|
|
5728
|
+
await this.queue.waitForIdle();
|
|
5668
5729
|
await this.embedder.dispose().catch((err) => {
|
|
5669
5730
|
if (this.isShuttingDown) {
|
|
5670
5731
|
logger.debug("[EmbeddingServer] Error disposing embedder during shutdown (expected):", err);
|
|
@@ -5895,7 +5956,7 @@ var init_embedding_client = __esm({
|
|
|
5895
5956
|
}
|
|
5896
5957
|
}
|
|
5897
5958
|
// ---- Internal HTTP helpers ----
|
|
5898
|
-
getRequest(
|
|
5959
|
+
getRequest(path20, timeoutMs = 12e4) {
|
|
5899
5960
|
return new Promise((resolve9, reject) => {
|
|
5900
5961
|
let resolved = false;
|
|
5901
5962
|
let req;
|
|
@@ -5903,7 +5964,7 @@ var init_embedding_client = __esm({
|
|
|
5903
5964
|
if (!resolved) {
|
|
5904
5965
|
resolved = true;
|
|
5905
5966
|
req?.destroy();
|
|
5906
|
-
reject(new Error(`[EmbeddingClient] GET ${
|
|
5967
|
+
reject(new Error(`[EmbeddingClient] GET ${path20} timed out after ${timeoutMs}ms`));
|
|
5907
5968
|
}
|
|
5908
5969
|
}, timeoutMs);
|
|
5909
5970
|
if (timer.unref) timer.unref();
|
|
@@ -5911,7 +5972,7 @@ var init_embedding_client = __esm({
|
|
|
5911
5972
|
{
|
|
5912
5973
|
hostname: "127.0.0.1",
|
|
5913
5974
|
port: this.port,
|
|
5914
|
-
path:
|
|
5975
|
+
path: path20,
|
|
5915
5976
|
method: "GET",
|
|
5916
5977
|
headers: this.authHeaders()
|
|
5917
5978
|
},
|
|
@@ -5927,19 +5988,19 @@ var init_embedding_client = __esm({
|
|
|
5927
5988
|
try {
|
|
5928
5989
|
const parsed = JSON.parse(collected.toString());
|
|
5929
5990
|
if (res.statusCode !== 200) {
|
|
5930
|
-
reject(new Error(`[EmbeddingClient] GET ${
|
|
5991
|
+
reject(new Error(`[EmbeddingClient] GET ${path20} returned HTTP ${res.statusCode}`));
|
|
5931
5992
|
} else {
|
|
5932
5993
|
resolve9(parsed);
|
|
5933
5994
|
}
|
|
5934
5995
|
} catch (_e) {
|
|
5935
|
-
reject(new Error(`[EmbeddingClient] Failed to parse GET ${
|
|
5996
|
+
reject(new Error(`[EmbeddingClient] Failed to parse GET ${path20} response: ${collected.toString()}`));
|
|
5936
5997
|
}
|
|
5937
5998
|
});
|
|
5938
5999
|
res.on("error", (err) => {
|
|
5939
6000
|
if (resolved) return;
|
|
5940
6001
|
resolved = true;
|
|
5941
6002
|
clearTimeout(timer);
|
|
5942
|
-
reject(new Error(`[EmbeddingClient] GET ${
|
|
6003
|
+
reject(new Error(`[EmbeddingClient] GET ${path20} response error: ${err.message}`));
|
|
5943
6004
|
});
|
|
5944
6005
|
}
|
|
5945
6006
|
);
|
|
@@ -5947,12 +6008,12 @@ var init_embedding_client = __esm({
|
|
|
5947
6008
|
if (resolved) return;
|
|
5948
6009
|
resolved = true;
|
|
5949
6010
|
clearTimeout(timer);
|
|
5950
|
-
reject(new Error(`[EmbeddingClient] GET ${
|
|
6011
|
+
reject(new Error(`[EmbeddingClient] GET ${path20} request error: ${err.message}`));
|
|
5951
6012
|
});
|
|
5952
6013
|
req.end();
|
|
5953
6014
|
});
|
|
5954
6015
|
}
|
|
5955
|
-
request(
|
|
6016
|
+
request(path20, data) {
|
|
5956
6017
|
return new Promise((resolve9, reject) => {
|
|
5957
6018
|
const timeoutMs = 12e4;
|
|
5958
6019
|
let resolved = false;
|
|
@@ -5961,7 +6022,7 @@ var init_embedding_client = __esm({
|
|
|
5961
6022
|
if (!resolved) {
|
|
5962
6023
|
resolved = true;
|
|
5963
6024
|
req?.destroy();
|
|
5964
|
-
reject(new Error(`[EmbeddingClient] POST ${
|
|
6025
|
+
reject(new Error(`[EmbeddingClient] POST ${path20} timed out after ${timeoutMs}ms`));
|
|
5965
6026
|
}
|
|
5966
6027
|
}, timeoutMs);
|
|
5967
6028
|
if (timer.unref) timer.unref();
|
|
@@ -5970,7 +6031,7 @@ var init_embedding_client = __esm({
|
|
|
5970
6031
|
{
|
|
5971
6032
|
hostname: "127.0.0.1",
|
|
5972
6033
|
port: this.port,
|
|
5973
|
-
path:
|
|
6034
|
+
path: path20,
|
|
5974
6035
|
method: "POST",
|
|
5975
6036
|
headers: {
|
|
5976
6037
|
"Content-Type": "application/json",
|
|
@@ -5996,14 +6057,14 @@ var init_embedding_client = __esm({
|
|
|
5996
6057
|
resolve9(parsed);
|
|
5997
6058
|
}
|
|
5998
6059
|
} catch (_e) {
|
|
5999
|
-
reject(new Error(`[EmbeddingClient] Failed to parse POST ${
|
|
6060
|
+
reject(new Error(`[EmbeddingClient] Failed to parse POST ${path20} response: ${collected.toString()}`));
|
|
6000
6061
|
}
|
|
6001
6062
|
});
|
|
6002
6063
|
res.on("error", (err) => {
|
|
6003
6064
|
if (resolved) return;
|
|
6004
6065
|
resolved = true;
|
|
6005
6066
|
clearTimeout(timer);
|
|
6006
|
-
reject(new Error(`[EmbeddingClient] POST ${
|
|
6067
|
+
reject(new Error(`[EmbeddingClient] POST ${path20} response error: ${err.message}`));
|
|
6007
6068
|
});
|
|
6008
6069
|
}
|
|
6009
6070
|
);
|
|
@@ -6011,7 +6072,7 @@ var init_embedding_client = __esm({
|
|
|
6011
6072
|
if (resolved) return;
|
|
6012
6073
|
resolved = true;
|
|
6013
6074
|
clearTimeout(timer);
|
|
6014
|
-
reject(new Error(`[EmbeddingClient] POST ${
|
|
6075
|
+
reject(new Error(`[EmbeddingClient] POST ${path20} request error: ${err.message}`));
|
|
6015
6076
|
});
|
|
6016
6077
|
req.write(body);
|
|
6017
6078
|
req.end();
|
|
@@ -7640,8 +7701,36 @@ var init_store = __esm({
|
|
|
7640
7701
|
const canonicalDir = path15.join(this.options.dbDir, `${canonicalName}.lance`);
|
|
7641
7702
|
await fsPromises.rename(tempDir, canonicalDir);
|
|
7642
7703
|
this.tableName = canonicalName;
|
|
7643
|
-
await this.saveManifest()
|
|
7644
|
-
|
|
7704
|
+
if (await this.saveManifest()) {
|
|
7705
|
+
this.table = await this.db.openTable(this.tableName);
|
|
7706
|
+
} else {
|
|
7707
|
+
logger.error("[store] Failed to persist canonical table name after rename \u2014 reverting to keep manifest and disk in sync.");
|
|
7708
|
+
try {
|
|
7709
|
+
await fsPromises.rename(canonicalDir, tempDir);
|
|
7710
|
+
this.tableName = tempTableName;
|
|
7711
|
+
this.table = await this.db.openTable(this.tableName);
|
|
7712
|
+
logger.info(`[store] Migrated data remains safe and accessible as ${tempTableName}; canonical rename deferred to a future successful manifest save.`);
|
|
7713
|
+
} catch (revertErr) {
|
|
7714
|
+
logger.error(`[store] Revert rename also failed; data remains on disk at ${canonicalDir}: ${revertErr}`);
|
|
7715
|
+
this.tableName = canonicalName;
|
|
7716
|
+
let reopened = false;
|
|
7717
|
+
try {
|
|
7718
|
+
this.table = await this.db.openTable(this.tableName);
|
|
7719
|
+
reopened = true;
|
|
7720
|
+
} catch (openErr) {
|
|
7721
|
+
logger.error(`[store] CRITICAL: could not reopen the table at '${canonicalName}' after a failed rename revert (${openErr}). Data should still be intact on disk at ${canonicalDir}, but this process has no working table handle for it. Attempting the manifest repair anyway so a restart can find the data; verify ${canonicalDir} exists if problems persist.`);
|
|
7722
|
+
}
|
|
7723
|
+
try {
|
|
7724
|
+
if (await this.saveManifest()) {
|
|
7725
|
+
logger.warn(`[store] Recovered: manifest now points at '${canonicalName}', matching the data's actual on-disk location${reopened ? "" : " (this process still has no working table handle \u2014 restart to serve it)"}.`);
|
|
7726
|
+
} else {
|
|
7727
|
+
logger.error(`[store] CRITICAL: could not persist a manifest matching disk after a failed rename revert. Data is intact on disk at ${canonicalDir} (table '${canonicalName}'), but the persisted manifest is stale and points at '${tempTableName}', which no longer exists. If this process exits before a future successful manifest save, the next start will not find the data automatically \u2014 manually repair the manifest to point at '${canonicalName}' before restarting.`);
|
|
7728
|
+
}
|
|
7729
|
+
} catch (manifestErr) {
|
|
7730
|
+
logger.error(`[store] CRITICAL: manifest repair threw after a failed rename revert (${manifestErr}). Data is intact on disk at ${canonicalDir} (table '${canonicalName}'), but the persisted manifest is stale and points at '${tempTableName}'. Manually repair the manifest to point at '${canonicalName}' before restarting.`);
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7645
7734
|
} catch (renameErr) {
|
|
7646
7735
|
logger.warn(`[store] Directory rename failed, persisting temp table name in manifest: ${renameErr}`);
|
|
7647
7736
|
this.tableName = tempTableName;
|
|
@@ -8693,6 +8782,7 @@ var init_chunker = __esm({
|
|
|
8693
8782
|
|
|
8694
8783
|
// src/knowledge/index.ts
|
|
8695
8784
|
import * as fs14 from "node:fs";
|
|
8785
|
+
import * as path16 from "node:path";
|
|
8696
8786
|
function getMigrationStrategy(config) {
|
|
8697
8787
|
const strategy = config.MIGRATION_STRATEGY;
|
|
8698
8788
|
if (!strategy) return void 0;
|
|
@@ -8754,15 +8844,25 @@ async function createKnowledgeStoreComponents(embedderFactory, reconnectFactory,
|
|
|
8754
8844
|
}
|
|
8755
8845
|
async function forceDeleteKnowledgeStore(config, workspace) {
|
|
8756
8846
|
const dbDir = getDbDir(config, workspace);
|
|
8757
|
-
if (fs14.existsSync(dbDir))
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
throw err;
|
|
8847
|
+
if (!fs14.existsSync(dbDir)) return;
|
|
8848
|
+
let tableName = "knowledge";
|
|
8849
|
+
const manifestPath = path16.join(dbDir, "store-manifest.json");
|
|
8850
|
+
try {
|
|
8851
|
+
if (fs14.existsSync(manifestPath)) {
|
|
8852
|
+
const manifest = JSON.parse(fs14.readFileSync(manifestPath, "utf-8"));
|
|
8853
|
+
if (manifest.activeTableName) tableName = manifest.activeTableName;
|
|
8765
8854
|
}
|
|
8855
|
+
} catch (err) {
|
|
8856
|
+
logger.warn(`[knowledge] Failed to read manifest before forced delete, defaulting to table '${tableName}':`, err);
|
|
8857
|
+
}
|
|
8858
|
+
const tableDir = path16.join(dbDir, `${tableName}.lance`);
|
|
8859
|
+
if (!fs14.existsSync(tableDir)) return;
|
|
8860
|
+
try {
|
|
8861
|
+
fs14.rmSync(tableDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
8862
|
+
logger.info(`[knowledge] Corrupted table directory ${tableDir} forcefully deleted; will be recreated on next init.`);
|
|
8863
|
+
} catch (err) {
|
|
8864
|
+
logger.error(`[knowledge] Failed to forcefully delete table directory ${tableDir}:`, err);
|
|
8865
|
+
throw err;
|
|
8766
8866
|
}
|
|
8767
8867
|
}
|
|
8768
8868
|
var MAX_INIT_RETRIES, BASE_RETRY_DELAY_MS, MAX_RETRY_DELAY_MS;
|
|
@@ -8787,7 +8887,7 @@ var knowledge_store_service_exports = {};
|
|
|
8787
8887
|
__export(knowledge_store_service_exports, {
|
|
8788
8888
|
KnowledgeStoreService: () => KnowledgeStoreService
|
|
8789
8889
|
});
|
|
8790
|
-
import * as
|
|
8890
|
+
import * as path17 from "node:path";
|
|
8791
8891
|
var KnowledgeStoreService;
|
|
8792
8892
|
var init_knowledge_store_service = __esm({
|
|
8793
8893
|
"src/infrastructure/knowledge-store-service.ts"() {
|
|
@@ -8881,7 +8981,7 @@ var init_knowledge_store_service = __esm({
|
|
|
8881
8981
|
};
|
|
8882
8982
|
const container = tryGetServiceContainerFromCtx(ctx);
|
|
8883
8983
|
const pathConfig = await getService(ServiceNames.STATE_PATH_CONFIGURATION, void 0, container);
|
|
8884
|
-
const lockPath =
|
|
8984
|
+
const lockPath = path17.join(pathConfig.getLockDirPath(), "knowledge-store-init.lock");
|
|
8885
8985
|
if (!this._initLock) {
|
|
8886
8986
|
const processLifecycle = await getService(ServiceNames.PROCESS_LIFECYCLE, void 0, container);
|
|
8887
8987
|
this._initLock = new FileLockService({
|
|
@@ -9235,6 +9335,15 @@ function resolveResearchModel(options) {
|
|
|
9235
9335
|
}
|
|
9236
9336
|
throw new Error(buildNoModelAvailableMessage());
|
|
9237
9337
|
}
|
|
9338
|
+
function isPromptCachingActiveForModel(model) {
|
|
9339
|
+
if (!model) return false;
|
|
9340
|
+
if (model.api === "anthropic-messages") return true;
|
|
9341
|
+
if (model.api === "openai-completions") {
|
|
9342
|
+
const compat = model.compat;
|
|
9343
|
+
return compat?.cacheControlFormat === "anthropic";
|
|
9344
|
+
}
|
|
9345
|
+
return false;
|
|
9346
|
+
}
|
|
9238
9347
|
var init_research_model_resolver = __esm({
|
|
9239
9348
|
"src/core/llm/research-model-resolver.ts"() {
|
|
9240
9349
|
"use strict";
|
|
@@ -9372,6 +9481,7 @@ __export(research_knowledge_search_exports, {
|
|
|
9372
9481
|
isTransientSynthesisError: () => isTransientSynthesisError,
|
|
9373
9482
|
rebuildDocuments: () => rebuildDocuments,
|
|
9374
9483
|
runBackgroundExtraction: () => runBackgroundExtraction,
|
|
9484
|
+
splitPromptAtUserTurn: () => splitPromptAtUserTurn,
|
|
9375
9485
|
triageRelevantUrls: () => triageRelevantUrls
|
|
9376
9486
|
});
|
|
9377
9487
|
import { Type as Type10 } from "typebox";
|
|
@@ -9515,13 +9625,28 @@ function isTransientSynthesisError(message) {
|
|
|
9515
9625
|
// connection surfaces as `TypeError: terminated` (cause "other side closed" / UND_ERR_SOCKET).
|
|
9516
9626
|
m.includes("terminated") || m.includes("other side closed") || m.includes("socket hang up") || m.includes("und_err");
|
|
9517
9627
|
}
|
|
9628
|
+
function splitPromptAtUserTurn(template) {
|
|
9629
|
+
const open5 = template.indexOf("<!-- USER_TURN");
|
|
9630
|
+
if (open5 < 0) return { system: template, user: null };
|
|
9631
|
+
const close = template.indexOf("-->", open5);
|
|
9632
|
+
if (close < 0) return { system: template, user: null };
|
|
9633
|
+
return {
|
|
9634
|
+
system: template.slice(0, open5).trimEnd(),
|
|
9635
|
+
user: template.slice(close + "-->".length).trimStart()
|
|
9636
|
+
};
|
|
9637
|
+
}
|
|
9518
9638
|
async function runBackgroundExtraction(model, auth, conversationHistory, queries, referenceDocuments, llmTimeout, maxTokens, thinkingLevel, signal, sessionId) {
|
|
9519
9639
|
const promptTemplate = loadPrompt("research-knowledge-search-extractor");
|
|
9520
9640
|
if (!promptTemplate) {
|
|
9521
9641
|
throw new Error("research-knowledge-search prompt template not found");
|
|
9522
9642
|
}
|
|
9523
|
-
const
|
|
9524
|
-
const
|
|
9643
|
+
const { system, user } = splitPromptAtUserTurn(promptTemplate);
|
|
9644
|
+
const fill = (t) => t.replace("{{queries}}", () => formatQueriesBlock(queries)).replace("{{conversation_history}}", () => conversationHistory).replace("{{reference_documents}}", () => referenceDocuments);
|
|
9645
|
+
const finalInstruction = "Analyze the reference documents above and extract the answer using the required JSON format.";
|
|
9646
|
+
const systemPrompt = user === null ? fill(system) : system;
|
|
9647
|
+
const userMessage = user === null ? finalInstruction : `${fill(user)}
|
|
9648
|
+
|
|
9649
|
+
${finalInstruction}`;
|
|
9525
9650
|
let responseText;
|
|
9526
9651
|
let attempt = 0;
|
|
9527
9652
|
for (; ; ) {
|
|
@@ -9609,8 +9734,13 @@ async function triageRelevantUrls(model, auth, conversationHistory, queries, can
|
|
|
9609
9734
|
return `[${i}] ${c.url}
|
|
9610
9735
|
${desc}`;
|
|
9611
9736
|
}).join("\n\n");
|
|
9612
|
-
const
|
|
9613
|
-
const
|
|
9737
|
+
const { system, user } = splitPromptAtUserTurn(promptTemplate);
|
|
9738
|
+
const fill = (t) => t.replace("{{queries}}", () => formatQueriesBlock(queries)).replace("{{conversation_history}}", () => conversationHistory).replace("{{candidates}}", () => candidateBlock);
|
|
9739
|
+
const finalInstruction = "Return the JSON object listing the indices of the relevant candidates.";
|
|
9740
|
+
const systemPrompt = user === null ? fill(system) : system;
|
|
9741
|
+
const userMessage = user === null ? finalInstruction : `${fill(user)}
|
|
9742
|
+
|
|
9743
|
+
${finalInstruction}`;
|
|
9614
9744
|
let responseText;
|
|
9615
9745
|
let attempt = 0;
|
|
9616
9746
|
for (; ; ) {
|
|
@@ -9942,11 +10072,11 @@ __export(skill_installer_exports, {
|
|
|
9942
10072
|
import * as crypto10 from "node:crypto";
|
|
9943
10073
|
import * as fs16 from "node:fs";
|
|
9944
10074
|
import * as os6 from "node:os";
|
|
9945
|
-
import * as
|
|
10075
|
+
import * as path18 from "node:path";
|
|
9946
10076
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
9947
10077
|
function readSkillVersion(sourceDir) {
|
|
9948
10078
|
try {
|
|
9949
|
-
const md = fs16.readFileSync(
|
|
10079
|
+
const md = fs16.readFileSync(path18.join(sourceDir, "SKILL.md"), "utf-8");
|
|
9950
10080
|
return /"version"\s*:\s*"([^"]+)"/.exec(md)?.[1] ?? null;
|
|
9951
10081
|
} catch {
|
|
9952
10082
|
return null;
|
|
@@ -9961,7 +10091,7 @@ function resolveSkillSourceDir(explicit) {
|
|
|
9961
10091
|
if (process.env["PI_RESEARCH_SKILL_DIR"]) candidates.push(process.env["PI_RESEARCH_SKILL_DIR"]);
|
|
9962
10092
|
const fromHere = (() => {
|
|
9963
10093
|
try {
|
|
9964
|
-
return
|
|
10094
|
+
return path18.dirname(fileURLToPath4(import.meta.url));
|
|
9965
10095
|
} catch {
|
|
9966
10096
|
return void 0;
|
|
9967
10097
|
}
|
|
@@ -9970,22 +10100,22 @@ function resolveSkillSourceDir(explicit) {
|
|
|
9970
10100
|
if (!start) continue;
|
|
9971
10101
|
let dir = start;
|
|
9972
10102
|
for (let i = 0; i < 8; i++) {
|
|
9973
|
-
candidates.push(
|
|
9974
|
-
const parent =
|
|
10103
|
+
candidates.push(path18.join(dir, "skills", SKILL_NAME));
|
|
10104
|
+
const parent = path18.dirname(dir);
|
|
9975
10105
|
if (parent === dir) break;
|
|
9976
10106
|
dir = parent;
|
|
9977
10107
|
}
|
|
9978
10108
|
}
|
|
9979
10109
|
for (const c of candidates) {
|
|
9980
10110
|
try {
|
|
9981
|
-
if (fs16.existsSync(
|
|
10111
|
+
if (fs16.existsSync(path18.join(c, "SKILL.md"))) return path18.resolve(c);
|
|
9982
10112
|
} catch {
|
|
9983
10113
|
}
|
|
9984
10114
|
}
|
|
9985
10115
|
throw new Error("Could not locate the bundled research skill (skills/pi-research/SKILL.md). Set PI_RESEARCH_SKILL_DIR to its path.");
|
|
9986
10116
|
}
|
|
9987
10117
|
function getManifestPath(opts) {
|
|
9988
|
-
return
|
|
10118
|
+
return path18.join(homeDir(opts), ".pi", "research", "installed-skills.json");
|
|
9989
10119
|
}
|
|
9990
10120
|
function readManifest(opts) {
|
|
9991
10121
|
const p = getManifestPath(opts);
|
|
@@ -10002,7 +10132,7 @@ function readManifest(opts) {
|
|
|
10002
10132
|
}
|
|
10003
10133
|
function writeManifest(manifest, opts) {
|
|
10004
10134
|
const p = getManifestPath(opts);
|
|
10005
|
-
fs16.mkdirSync(
|
|
10135
|
+
fs16.mkdirSync(path18.dirname(p), { recursive: true });
|
|
10006
10136
|
const tmp = `${p}.tmp.${process.pid}.${Date.now()}.${manifestTmpSeq++}`;
|
|
10007
10137
|
fs16.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
10008
10138
|
try {
|
|
@@ -10024,8 +10154,8 @@ function isOwnedSymlink(targetPath, expectedSource) {
|
|
|
10024
10154
|
try {
|
|
10025
10155
|
const st = fs16.lstatSync(targetPath);
|
|
10026
10156
|
if (!st.isSymbolicLink()) return false;
|
|
10027
|
-
const dest =
|
|
10028
|
-
if (expectedSource &&
|
|
10157
|
+
const dest = path18.resolve(path18.dirname(targetPath), fs16.readlinkSync(targetPath));
|
|
10158
|
+
if (expectedSource && path18.resolve(expectedSource) === dest) return true;
|
|
10029
10159
|
return /[/\\]pi-research[/\\]skills[/\\]pi-research$/.test(dest);
|
|
10030
10160
|
} catch {
|
|
10031
10161
|
return false;
|
|
@@ -10033,7 +10163,7 @@ function isOwnedSymlink(targetPath, expectedSource) {
|
|
|
10033
10163
|
}
|
|
10034
10164
|
function isOwnedCopy(targetPath) {
|
|
10035
10165
|
try {
|
|
10036
|
-
const md = fs16.readFileSync(
|
|
10166
|
+
const md = fs16.readFileSync(path18.join(targetPath, "SKILL.md"), "utf-8");
|
|
10037
10167
|
return md.includes(PACKAGE_NAME);
|
|
10038
10168
|
} catch {
|
|
10039
10169
|
return false;
|
|
@@ -10042,11 +10172,11 @@ function isOwnedCopy(targetPath) {
|
|
|
10042
10172
|
function detectHarnesses(opts) {
|
|
10043
10173
|
const home = homeDir(opts);
|
|
10044
10174
|
return HARNESSES.map((h) => {
|
|
10045
|
-
const absSkillsDir =
|
|
10046
|
-
const absSkillPath =
|
|
10175
|
+
const absSkillsDir = path18.join(home, h.skillsDir);
|
|
10176
|
+
const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
|
|
10047
10177
|
let present = false;
|
|
10048
10178
|
try {
|
|
10049
|
-
present = fs16.existsSync(
|
|
10179
|
+
present = fs16.existsSync(path18.join(home, h.baseDir));
|
|
10050
10180
|
} catch {
|
|
10051
10181
|
}
|
|
10052
10182
|
let installed = "none";
|
|
@@ -10087,15 +10217,15 @@ function installSkill(toolIds, opts = {}) {
|
|
|
10087
10217
|
results.push({ tool: id, path: "", type: "symlink", status: "error", message: `unknown target "${id}"` });
|
|
10088
10218
|
continue;
|
|
10089
10219
|
}
|
|
10090
|
-
const absSkillsDir =
|
|
10091
|
-
const absSkillPath =
|
|
10220
|
+
const absSkillsDir = path18.join(home, def.skillsDir);
|
|
10221
|
+
const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
|
|
10092
10222
|
const type = opts.copy ? "copy" : "symlink";
|
|
10093
10223
|
if (isOwnedSymlink(absSkillPath, source) || isOwnedCopy(absSkillPath)) {
|
|
10094
10224
|
try {
|
|
10095
10225
|
const lst = fs16.lstatSync(absSkillPath);
|
|
10096
10226
|
if (lst.isSymbolicLink()) {
|
|
10097
|
-
const dest =
|
|
10098
|
-
if (dest !==
|
|
10227
|
+
const dest = path18.resolve(path18.dirname(absSkillPath), fs16.readlinkSync(absSkillPath));
|
|
10228
|
+
if (dest !== path18.resolve(source)) {
|
|
10099
10229
|
if (opts.dryRun) {
|
|
10100
10230
|
results.push({ tool: id, path: absSkillPath, type: "symlink", status: "planned", message: "would re-point stale symlink to current source" });
|
|
10101
10231
|
continue;
|
|
@@ -10171,7 +10301,7 @@ function uninstallSkill(toolIds, opts = {}) {
|
|
|
10171
10301
|
}
|
|
10172
10302
|
}
|
|
10173
10303
|
for (const { tool, path: skillPath } of targets.values()) {
|
|
10174
|
-
if (toolIds
|
|
10304
|
+
if (toolIds !== void 0 && !toolIds.includes(tool)) {
|
|
10175
10305
|
const e = manifest.entries.find((x) => x.path === skillPath);
|
|
10176
10306
|
if (e) keep.push(e);
|
|
10177
10307
|
continue;
|
|
@@ -10232,19 +10362,19 @@ function reconcileSkillInstalls(opts = {}) {
|
|
|
10232
10362
|
if (e.type !== "symlink") {
|
|
10233
10363
|
const stale2 = currentVersion !== null && e.version !== currentVersion;
|
|
10234
10364
|
if (stale2 && source !== null && fs16.existsSync(e.path) && isOwnedCopy(e.path)) {
|
|
10235
|
-
const dir2 =
|
|
10236
|
-
const base2 =
|
|
10365
|
+
const dir2 = path18.dirname(e.path);
|
|
10366
|
+
const base2 = path18.basename(e.path);
|
|
10237
10367
|
try {
|
|
10238
10368
|
for (const name of fs16.readdirSync(dir2)) {
|
|
10239
10369
|
if (name.startsWith(`.${base2}.new-`) || name.startsWith(`.${base2}.old-`)) {
|
|
10240
|
-
fs16.rmSync(
|
|
10370
|
+
fs16.rmSync(path18.join(dir2, name), { recursive: true, force: true });
|
|
10241
10371
|
}
|
|
10242
10372
|
}
|
|
10243
10373
|
} catch {
|
|
10244
10374
|
}
|
|
10245
10375
|
const suffix = crypto10.randomBytes(4).toString("hex");
|
|
10246
|
-
const fresh =
|
|
10247
|
-
const retired =
|
|
10376
|
+
const fresh = path18.join(dir2, `.${base2}.new-${suffix}`);
|
|
10377
|
+
const retired = path18.join(dir2, `.${base2}.old-${suffix}`);
|
|
10248
10378
|
try {
|
|
10249
10379
|
copyDir(source, fresh);
|
|
10250
10380
|
} catch {
|
|
@@ -10290,12 +10420,12 @@ function reconcileSkillInstalls(opts = {}) {
|
|
|
10290
10420
|
}
|
|
10291
10421
|
continue;
|
|
10292
10422
|
}
|
|
10293
|
-
const dir =
|
|
10294
|
-
const base =
|
|
10423
|
+
const dir = path18.dirname(e.path);
|
|
10424
|
+
const base = path18.basename(e.path);
|
|
10295
10425
|
try {
|
|
10296
10426
|
for (const name of fs16.readdirSync(dir)) {
|
|
10297
10427
|
if (name.startsWith(`.${base}.new-`)) {
|
|
10298
|
-
fs16.rmSync(
|
|
10428
|
+
fs16.rmSync(path18.join(dir, name), { recursive: true, force: true });
|
|
10299
10429
|
}
|
|
10300
10430
|
}
|
|
10301
10431
|
} catch {
|
|
@@ -10311,10 +10441,10 @@ function reconcileSkillInstalls(opts = {}) {
|
|
|
10311
10441
|
const targetLive = fs16.existsSync(e.path);
|
|
10312
10442
|
let dest = null;
|
|
10313
10443
|
try {
|
|
10314
|
-
dest =
|
|
10444
|
+
dest = path18.resolve(path18.dirname(e.path), fs16.readlinkSync(e.path));
|
|
10315
10445
|
} catch {
|
|
10316
10446
|
}
|
|
10317
|
-
const stale = source !== null && dest !== null && dest !==
|
|
10447
|
+
const stale = source !== null && dest !== null && dest !== path18.resolve(source);
|
|
10318
10448
|
if (!targetLive || stale) {
|
|
10319
10449
|
if (source !== null) {
|
|
10320
10450
|
try {
|
|
@@ -10329,7 +10459,7 @@ function reconcileSkillInstalls(opts = {}) {
|
|
|
10329
10459
|
result.repointed.push(e.path);
|
|
10330
10460
|
} catch {
|
|
10331
10461
|
const suffix = crypto10.randomBytes(4).toString("hex");
|
|
10332
|
-
const fresh =
|
|
10462
|
+
const fresh = path18.join(dir, `.${base}.new-${suffix}`);
|
|
10333
10463
|
try {
|
|
10334
10464
|
copyDir(source, fresh);
|
|
10335
10465
|
fs16.renameSync(fresh, e.path);
|
|
@@ -10371,7 +10501,7 @@ function copyDir(src, dest) {
|
|
|
10371
10501
|
if (st.isDirectory()) {
|
|
10372
10502
|
fs16.mkdirSync(dest, { recursive: true });
|
|
10373
10503
|
for (const name of fs16.readdirSync(src)) {
|
|
10374
|
-
copyDir(
|
|
10504
|
+
copyDir(path18.join(src, name), path18.join(dest, name));
|
|
10375
10505
|
}
|
|
10376
10506
|
} else {
|
|
10377
10507
|
fs16.copyFileSync(src, dest);
|
|
@@ -10382,12 +10512,12 @@ var init_skill_installer = __esm({
|
|
|
10382
10512
|
"src/skill-install/skill-installer.ts"() {
|
|
10383
10513
|
"use strict";
|
|
10384
10514
|
HARNESSES = [
|
|
10385
|
-
{ id: "claude", label: "Claude", baseDir: ".claude", skillsDir:
|
|
10386
|
-
{ id: "openclaw", label: "OpenClaw", baseDir: ".openclaw", skillsDir:
|
|
10387
|
-
{ id: "pi", label: "pi", baseDir: ".pi", skillsDir:
|
|
10388
|
-
{ id: "cursor", label: "Cursor", baseDir: ".cursor", skillsDir:
|
|
10389
|
-
{ id: "codex", label: "OpenAI Codex CLI", baseDir: ".codex", skillsDir:
|
|
10390
|
-
{ id: "agents", label: "Cross-tool (~/.agents/skills)", baseDir: ".agents", skillsDir:
|
|
10515
|
+
{ id: "claude", label: "Claude", baseDir: ".claude", skillsDir: path18.join(".claude", "skills"), confidence: "confirmed" },
|
|
10516
|
+
{ id: "openclaw", label: "OpenClaw", baseDir: ".openclaw", skillsDir: path18.join(".openclaw", "skills"), confidence: "confirmed", note: "OpenClaw reads ~/.openclaw/skills as a managed skill root and accepts symlinked skill folders (docs.openclaw.ai/tools/skills)." },
|
|
10517
|
+
{ id: "pi", label: "pi", baseDir: ".pi", skillsDir: path18.join(".pi", "skills"), confidence: "confirmed" },
|
|
10518
|
+
{ id: "cursor", label: "Cursor", baseDir: ".cursor", skillsDir: path18.join(".cursor", "skills"), confidence: "partial", note: "Cursor project-level skills are documented; the global ~/.cursor/skills path is community-reported." },
|
|
10519
|
+
{ id: "codex", label: "OpenAI Codex CLI", baseDir: ".codex", skillsDir: path18.join(".codex", "skills"), confidence: "unverified", note: "Codex skills support is emerging; path not confirmed by official docs." },
|
|
10520
|
+
{ id: "agents", label: "Cross-tool (~/.agents/skills)", baseDir: ".agents", skillsDir: path18.join(".agents", "skills"), confidence: "unverified", note: "Proposed Universal Agents convention; not yet a ratified standard." }
|
|
10391
10521
|
];
|
|
10392
10522
|
SKILL_NAME = "pi-research";
|
|
10393
10523
|
PACKAGE_NAME = "@lincoln504/pi-research";
|
|
@@ -10400,7 +10530,7 @@ var init_skill_installer = __esm({
|
|
|
10400
10530
|
// src/cli.ts
|
|
10401
10531
|
import * as fs17 from "node:fs";
|
|
10402
10532
|
import * as os7 from "node:os";
|
|
10403
|
-
import * as
|
|
10533
|
+
import * as path19 from "node:path";
|
|
10404
10534
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
10405
10535
|
|
|
10406
10536
|
// src/core/service-initialization.ts
|
|
@@ -10814,7 +10944,7 @@ function delimiterRegex(word) {
|
|
|
10814
10944
|
return new RegExp(`(^|\\n)([ \\t]*(?:#{1,6}[ \\t]*|>[ \\t]*|\\*\\*[ \\t]*)?)${word}\\b`, "i");
|
|
10815
10945
|
}
|
|
10816
10946
|
function splitCoverageDigest(report) {
|
|
10817
|
-
if (!report) return { digest: "", body: report
|
|
10947
|
+
if (!report) return { digest: "", body: report };
|
|
10818
10948
|
const headerMatch = delimiterRegex(COVERAGE_DIGEST_HEADER).exec(report);
|
|
10819
10949
|
if (!headerMatch) return { digest: "", body: report };
|
|
10820
10950
|
const lineStart = headerMatch.index + headerMatch[1].length;
|
|
@@ -11193,7 +11323,12 @@ function isDegradableLlmError(error, signal) {
|
|
|
11193
11323
|
function salvageReportText(raw) {
|
|
11194
11324
|
const trimmed = raw.trim();
|
|
11195
11325
|
if (trimmed.length <= 50) return "";
|
|
11196
|
-
|
|
11326
|
+
let sniff = trimmed;
|
|
11327
|
+
const fence = sniff.match(/^```[a-zA-Z0-9_-]*[ \t]*\r?\n/);
|
|
11328
|
+
if (fence) {
|
|
11329
|
+
sniff = sniff.slice(fence[0].length).replace(/\r?\n```\s*$/, "").trim();
|
|
11330
|
+
}
|
|
11331
|
+
if (sniff.startsWith("{") && /"(?:action|content|researchers)"\s*:/.test(sniff)) {
|
|
11197
11332
|
return "";
|
|
11198
11333
|
}
|
|
11199
11334
|
return raw;
|
|
@@ -11540,14 +11675,24 @@ ${previousQueries.map((q) => `- ${q}`).join("\n")}
|
|
|
11540
11675
|
complexity,
|
|
11541
11676
|
observer
|
|
11542
11677
|
};
|
|
11543
|
-
const responseText = isRouter ? await
|
|
11544
|
-
...
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11678
|
+
const responseText = isRouter ? await (() => {
|
|
11679
|
+
const routerMessage = buildRouterMessage(...this.buildRouterEvidence(options, round), runContext);
|
|
11680
|
+
const window = model.contextWindow ?? DEFAULT_MODEL_CONTEXT_WINDOW;
|
|
11681
|
+
const estimatedTokens = routerMessage.length / CHARS_PER_TOKEN + ROUTER_MAX_TOKENS + SYNTHESIS_OVERHEAD_TOKENS;
|
|
11682
|
+
if (estimatedTokens > window) {
|
|
11683
|
+
logger.warn(
|
|
11684
|
+
`[PlanningService] Router request for ${model.id} is an estimated ${Math.round(estimatedTokens)} tokens against a ${window}-token context window (evidence ${routerMessage.length} chars, output ceiling ${ROUTER_MAX_TOKENS} tokens). Unlike the synthesizer, the router has no reduce path for an over-budget round \u2014 sending it anyway, the provider may reject this request.`
|
|
11685
|
+
);
|
|
11686
|
+
}
|
|
11687
|
+
return this.callLead({
|
|
11688
|
+
...callOptions,
|
|
11689
|
+
systemPrompt: populate(""),
|
|
11690
|
+
userMessage: routerMessage,
|
|
11691
|
+
maxTokens: ROUTER_MAX_TOKENS,
|
|
11692
|
+
component: "router",
|
|
11693
|
+
label: "router-updatePlanForRound"
|
|
11694
|
+
});
|
|
11695
|
+
})() : await this.synthesizeCorpus({ ...callOptions, reports, runContext, populate });
|
|
11551
11696
|
let plan = null;
|
|
11552
11697
|
try {
|
|
11553
11698
|
plan = this.parseJsonPlan(responseText);
|
|
@@ -11795,6 +11940,13 @@ ${previousQueries.map((q) => `- ${q}`).join("\n")}
|
|
|
11795
11940
|
`[PlanningService] Synthesis corpus is still ${finalSize} chars after ${MAX_SYNTHESIS_REDUCE_PASSES} reduce pass(es), over the ${budgetChars}-char budget for ${args.model.id}. Sending it anyway \u2014 the provider may reject this request.`
|
|
11796
11941
|
);
|
|
11797
11942
|
}
|
|
11943
|
+
const window = args.model.contextWindow ?? DEFAULT_MODEL_CONTEXT_WINDOW;
|
|
11944
|
+
const estimatedTotalTokens = (finalSize + globalSourceList.length + runContext.length) / CHARS_PER_TOKEN + config.SYNTHESIS_MAX_TOKENS + SYNTHESIS_OVERHEAD_TOKENS;
|
|
11945
|
+
if (estimatedTotalTokens > window) {
|
|
11946
|
+
logger.warn(
|
|
11947
|
+
`[PlanningService] Synthesis request for ${args.model.id} is an estimated ${Math.round(estimatedTotalTokens)} tokens against a ${window}-token context window (corpus ${finalSize} chars, source list ${globalSourceList.length} chars, run context ${runContext.length} chars, output ceiling ${config.SYNTHESIS_MAX_TOKENS} tokens) \u2014 the source list and/or run context are large enough on their own to overflow the window even though the reduced corpus fits its budget. Sending it anyway \u2014 the provider may reject this request.`
|
|
11948
|
+
);
|
|
11949
|
+
}
|
|
11798
11950
|
return this.callLead({
|
|
11799
11951
|
...args,
|
|
11800
11952
|
systemPrompt: populate(reduced ? MERGE_SYNTHESIS_SECTION : ""),
|
|
@@ -12175,10 +12327,13 @@ function isFullMockMode() {
|
|
|
12175
12327
|
}
|
|
12176
12328
|
var HEALTHCHECK_MIN_BUDGET_MS = 45e3 + 6e4;
|
|
12177
12329
|
var HEALTHCHECK_NAV_ATTEMPTS = 3;
|
|
12330
|
+
var BROWSER_LAUNCH_TIMEOUT_MS = 9e4;
|
|
12331
|
+
var CONTEXT_CREATION_TIMEOUT_MS = 3e4;
|
|
12332
|
+
var COLD_START_ALLOWANCE_MS = BROWSER_LAUNCH_TIMEOUT_MS + CONTEXT_CREATION_TIMEOUT_MS;
|
|
12178
12333
|
function getHealthCheckBudgetMs(config) {
|
|
12179
12334
|
const c = config || getConfig();
|
|
12180
12335
|
const ms = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
12181
|
-
const longestSlotHold = Math.max(ms(c.SEARCH_TIMEOUT_MS, 45e3), ms(c.SCRAPE_TIMEOUT_MS, 15e3)) + ms(c.BROWSER_TASK_TIMEOUT_MS, 1e4);
|
|
12336
|
+
const longestSlotHold = Math.max(ms(c.SEARCH_TIMEOUT_MS, 45e3), ms(c.SCRAPE_TIMEOUT_MS, 15e3)) + ms(c.BROWSER_TASK_TIMEOUT_MS, 1e4) + COLD_START_ALLOWANCE_MS;
|
|
12182
12337
|
const probeWork = HEALTHCHECK_NAV_ATTEMPTS * Math.max(1e4, ms(c.HEALTH_CHECK_TIMEOUT_MS, 1e4));
|
|
12183
12338
|
return Math.max(HEALTHCHECK_MIN_BUDGET_MS, longestSlotHold + probeWork);
|
|
12184
12339
|
}
|
|
@@ -12234,9 +12389,9 @@ var BrowserClient = class {
|
|
|
12234
12389
|
get targetPort() {
|
|
12235
12390
|
return this.port;
|
|
12236
12391
|
}
|
|
12237
|
-
async request(
|
|
12392
|
+
async request(path20, data, signal, config) {
|
|
12238
12393
|
const start = Date.now();
|
|
12239
|
-
const operation =
|
|
12394
|
+
const operation = path20.includes("/search") ? "search" : path20.includes("/scrape") ? "browser-task" : path20.includes("/healthcheck") ? "healthcheck" : "network";
|
|
12240
12395
|
return new Promise((resolve9, reject) => {
|
|
12241
12396
|
const agent = getClientAgent();
|
|
12242
12397
|
const timeoutMs = resolveClientRequestTimeoutMs(operation, config);
|
|
@@ -12261,7 +12416,7 @@ var BrowserClient = class {
|
|
|
12261
12416
|
resolved = true;
|
|
12262
12417
|
abortCleanup?.();
|
|
12263
12418
|
controller.abort();
|
|
12264
|
-
const error = new Error(`[BrowserClient] Request to ${
|
|
12419
|
+
const error = new Error(`[BrowserClient] Request to ${path20} timed out after ${timeoutMs}ms (Shared queue may be deep)`);
|
|
12265
12420
|
errorTracker.trackError(error, {
|
|
12266
12421
|
component: "browser-manager",
|
|
12267
12422
|
operation,
|
|
@@ -12274,7 +12429,7 @@ var BrowserClient = class {
|
|
|
12274
12429
|
const req = http4.request({
|
|
12275
12430
|
hostname: "127.0.0.1",
|
|
12276
12431
|
port: this.port,
|
|
12277
|
-
path:
|
|
12432
|
+
path: path20,
|
|
12278
12433
|
method: "POST",
|
|
12279
12434
|
agent,
|
|
12280
12435
|
signal: controller.signal,
|
|
@@ -12298,7 +12453,7 @@ var BrowserClient = class {
|
|
|
12298
12453
|
const error = new Error(
|
|
12299
12454
|
`Browser pool leader is draining (${parsed.reason || "leadership-lost"}) \u2014 re-elect and retry`
|
|
12300
12455
|
);
|
|
12301
|
-
logger.debug(`[BrowserClient] Leader draining on ${
|
|
12456
|
+
logger.debug(`[BrowserClient] Leader draining on ${path20}; re-election required.`);
|
|
12302
12457
|
errorTracker.trackError(error, {
|
|
12303
12458
|
component: "browser-manager",
|
|
12304
12459
|
operation,
|
|
@@ -12314,7 +12469,7 @@ var BrowserClient = class {
|
|
|
12314
12469
|
});
|
|
12315
12470
|
reject(error);
|
|
12316
12471
|
} else {
|
|
12317
|
-
logger.debug(`[BrowserClient] Request ${
|
|
12472
|
+
logger.debug(`[BrowserClient] Request ${path20} completed in ${duration}ms`);
|
|
12318
12473
|
resolve9(parsed);
|
|
12319
12474
|
}
|
|
12320
12475
|
} catch (_e) {
|
|
@@ -12334,7 +12489,7 @@ var BrowserClient = class {
|
|
|
12334
12489
|
resolved = true;
|
|
12335
12490
|
clearTimeout(timer);
|
|
12336
12491
|
abortCleanup?.();
|
|
12337
|
-
const error = new Error(`[BrowserClient] Response stream error on ${
|
|
12492
|
+
const error = new Error(`[BrowserClient] Response stream error on ${path20}: ${err.message}`);
|
|
12338
12493
|
errorTracker.trackError(error, {
|
|
12339
12494
|
component: "browser-manager",
|
|
12340
12495
|
operation,
|
|
@@ -12353,20 +12508,20 @@ var BrowserClient = class {
|
|
|
12353
12508
|
let errorMsg;
|
|
12354
12509
|
let errorType;
|
|
12355
12510
|
if (nodeErr.code === "ECONNRESET" || nodeErr.code === "EPIPE") {
|
|
12356
|
-
errorMsg = `Browser pool socket ${
|
|
12511
|
+
errorMsg = `Browser pool socket ${path20} closed (pool likely busy or restarting) - ${err.message}`;
|
|
12357
12512
|
errorType = "connection_reset";
|
|
12358
12513
|
} else if (nodeErr.code === "ECONNREFUSED") {
|
|
12359
|
-
errorMsg = `Browser pool ${
|
|
12514
|
+
errorMsg = `Browser pool ${path20} unreachable (server restarting or torn down) - ${err.message}`;
|
|
12360
12515
|
errorType = "connection_refused";
|
|
12361
12516
|
} else if (nodeErr.code === "ETIMEDOUT") {
|
|
12362
|
-
errorMsg = `Browser pool ${
|
|
12517
|
+
errorMsg = `Browser pool ${path20} timed out (slow browser response) - ${err.message}`;
|
|
12363
12518
|
errorType = "timeout";
|
|
12364
12519
|
} else {
|
|
12365
|
-
errorMsg = `Browser pool ${
|
|
12520
|
+
errorMsg = `Browser pool ${path20} error: ${err.message}`;
|
|
12366
12521
|
errorType = "unknown";
|
|
12367
12522
|
}
|
|
12368
12523
|
const error = new Error(errorMsg);
|
|
12369
|
-
logger.error(`[BrowserClient] Request to http://127.0.0.1:${this.port}${
|
|
12524
|
+
logger.error(`[BrowserClient] Request to http://127.0.0.1:${this.port}${path20} failed:`, errorMsg);
|
|
12370
12525
|
errorTracker.trackError(error, {
|
|
12371
12526
|
component: "browser-manager",
|
|
12372
12527
|
operation,
|
|
@@ -13246,7 +13401,7 @@ var BrowserTaskScheduler = class {
|
|
|
13246
13401
|
const pool = await (await this.getWorkerPoolManager()).ensurePool(config);
|
|
13247
13402
|
const startTime = Date.now();
|
|
13248
13403
|
const cfg = config || getConfig();
|
|
13249
|
-
const timeoutMs = cfg.SEARCH_TIMEOUT_MS + cfg.BROWSER_TASK_TIMEOUT_MS;
|
|
13404
|
+
const timeoutMs = cfg.SEARCH_TIMEOUT_MS + cfg.BROWSER_TASK_TIMEOUT_MS + COLD_START_ALLOWANCE_MS;
|
|
13250
13405
|
let timeoutId;
|
|
13251
13406
|
let dispatchedAt = 0;
|
|
13252
13407
|
let startDeadline = () => {
|
|
@@ -13335,7 +13490,7 @@ var BrowserTaskScheduler = class {
|
|
|
13335
13490
|
const startTime = Date.now();
|
|
13336
13491
|
const cfg = config || getConfig();
|
|
13337
13492
|
const isMocking = process.env["PI_RESEARCH_MOCK_SCRAPE"] === "true";
|
|
13338
|
-
const timeoutMs = cfg.SCRAPE_TIMEOUT_MS + (isMocking ? 5e3 : cfg.BROWSER_TASK_TIMEOUT_MS);
|
|
13493
|
+
const timeoutMs = cfg.SCRAPE_TIMEOUT_MS + (isMocking ? 5e3 : cfg.BROWSER_TASK_TIMEOUT_MS + COLD_START_ALLOWANCE_MS);
|
|
13339
13494
|
let timeoutId;
|
|
13340
13495
|
let dispatchedAt = 0;
|
|
13341
13496
|
let startDeadline = () => {
|
|
@@ -15393,8 +15548,8 @@ var StateValidator = class {
|
|
|
15393
15548
|
if (!Value4.Check(SingletonStateSchema, coerced)) {
|
|
15394
15549
|
const errors = [...Value4.Errors(SingletonStateSchema, coerced)];
|
|
15395
15550
|
const errorMsg = errors.map((e) => {
|
|
15396
|
-
const
|
|
15397
|
-
return `${
|
|
15551
|
+
const path20 = String(e.path || "root");
|
|
15552
|
+
return `${path20}: ${e.message} (${JSON.stringify(e.value)})`;
|
|
15398
15553
|
}).join(", ");
|
|
15399
15554
|
throw new Error(`Invalid state: ${errorMsg}`);
|
|
15400
15555
|
}
|
|
@@ -17123,7 +17278,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
|
|
|
17123
17278
|
metrics.increment("browser_search_orchestrations_total", 1);
|
|
17124
17279
|
logger.log(`[Search] Orchestrating ${queries.length} queries across ${maxWorkers} worker processes...`);
|
|
17125
17280
|
if (onProgress) onProgress(0);
|
|
17126
|
-
const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45e3) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 1e4);
|
|
17281
|
+
const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45e3) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 1e4) + COLD_START_ALLOWANCE_MS;
|
|
17127
17282
|
const filteredQueries = [...new Set(queries.filter((q) => q.trim()))];
|
|
17128
17283
|
let timeoutCount = 0;
|
|
17129
17284
|
let errorCount = 0;
|
|
@@ -18158,28 +18313,31 @@ async function extractPdfToMarkdown(bytes) {
|
|
|
18158
18313
|
}
|
|
18159
18314
|
try {
|
|
18160
18315
|
const doc = new WasmPdfDocument(bytes);
|
|
18161
|
-
|
|
18162
|
-
|
|
18316
|
+
try {
|
|
18317
|
+
const pageCount = doc.pageCount();
|
|
18318
|
+
let markdown = `# PDF Document
|
|
18163
18319
|
|
|
18164
18320
|
**Pages:** ${pageCount}
|
|
18165
18321
|
|
|
18166
18322
|
`;
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18323
|
+
try {
|
|
18324
|
+
markdown += doc.toMarkdownAll();
|
|
18325
|
+
} catch {
|
|
18326
|
+
for (let i = 0; i < pageCount; i++) {
|
|
18327
|
+
markdown += `## Page ${i + 1}
|
|
18172
18328
|
|
|
18173
18329
|
${doc.toMarkdown(i)}
|
|
18174
18330
|
|
|
18175
18331
|
`;
|
|
18332
|
+
}
|
|
18176
18333
|
}
|
|
18334
|
+
const pdfExtractionDuration = Date.now() - pdfExtractionStart;
|
|
18335
|
+
metrics.observe("scrape_pdf_conversion_ms", pdfExtractionDuration);
|
|
18336
|
+
metrics.increment("scrape_pdf_conversions_total", 1, { status: "success", pages: String(pageCount) });
|
|
18337
|
+
return markdown;
|
|
18338
|
+
} finally {
|
|
18339
|
+
doc.free();
|
|
18177
18340
|
}
|
|
18178
|
-
doc.free();
|
|
18179
|
-
const pdfExtractionDuration = Date.now() - pdfExtractionStart;
|
|
18180
|
-
metrics.observe("scrape_pdf_conversion_ms", pdfExtractionDuration);
|
|
18181
|
-
metrics.increment("scrape_pdf_conversions_total", 1, { status: "success", pages: String(pageCount) });
|
|
18182
|
-
return markdown;
|
|
18183
18341
|
} catch (e) {
|
|
18184
18342
|
const msg2 = e instanceof Error ? e.message : String(e);
|
|
18185
18343
|
logger.debug(`[Scrapers] PDF extraction failed: ${msg2}`);
|
|
@@ -18279,7 +18437,7 @@ async function scrapeWithFetch(url, signal) {
|
|
|
18279
18437
|
const contentLength = response.headers.get("content-length");
|
|
18280
18438
|
if (contentLength) {
|
|
18281
18439
|
const size = parseInt(contentLength, 10);
|
|
18282
|
-
if (contentType.includes("application/pdf") ||
|
|
18440
|
+
if (contentType.includes("application/pdf") || currentUrl.toLowerCase().endsWith(".pdf")) {
|
|
18283
18441
|
if (size > MAX_PDF_SIZE) {
|
|
18284
18442
|
const sizeMB = Math.round(size / 1024 / 1024);
|
|
18285
18443
|
logger.warn(`[Scrapers] PDF too large (Content-Length: ${sizeMB}MB, max 100MB), skipping`);
|
|
@@ -18301,7 +18459,7 @@ async function scrapeWithFetch(url, signal) {
|
|
|
18301
18459
|
throw new Error(`HTML response too large (${sizeMB}MB, max 25MB)`);
|
|
18302
18460
|
}
|
|
18303
18461
|
}
|
|
18304
|
-
if (contentType.includes("application/pdf") ||
|
|
18462
|
+
if (contentType.includes("application/pdf") || currentUrl.toLowerCase().endsWith(".pdf")) {
|
|
18305
18463
|
let pdfBytes;
|
|
18306
18464
|
try {
|
|
18307
18465
|
pdfBytes = await readBodyCapped(response, MAX_PDF_SIZE);
|
|
@@ -18541,7 +18699,7 @@ init_metrics();
|
|
|
18541
18699
|
init_cancellation();
|
|
18542
18700
|
function createScrapeTool(options) {
|
|
18543
18701
|
const config = options.config || getConfig(options.ctx.cwd);
|
|
18544
|
-
const maxScrapeBatches = getMaxScrapeBatches(config);
|
|
18702
|
+
const maxScrapeBatches = options.tracker?.getToolLimit("scrape") ?? getMaxScrapeBatches(config);
|
|
18545
18703
|
const container = tryGetServiceContainerFromCtx(options.ctx);
|
|
18546
18704
|
const fallbackState = {
|
|
18547
18705
|
version: 1,
|
|
@@ -20426,14 +20584,6 @@ function createSecuritySearchTool(options) {
|
|
|
20426
20584
|
details: { error: "invalid_parameters" }
|
|
20427
20585
|
};
|
|
20428
20586
|
}
|
|
20429
|
-
const allowed = options.tracker.recordCall("security_search");
|
|
20430
|
-
if (!allowed) {
|
|
20431
|
-
metrics.increment("tool_security_search_calls_total", 1, { status: "rate_limited" });
|
|
20432
|
-
return {
|
|
20433
|
-
content: [{ type: "text", text: options.tracker.getLimitMessage("security_search") }],
|
|
20434
|
-
details: { blocked: true, reason: "limit_reached" }
|
|
20435
|
-
};
|
|
20436
|
-
}
|
|
20437
20587
|
const p = params;
|
|
20438
20588
|
const terms = p.terms;
|
|
20439
20589
|
if (terms.length === 0) {
|
|
@@ -20475,6 +20625,14 @@ function createSecuritySearchTool(options) {
|
|
|
20475
20625
|
}
|
|
20476
20626
|
}
|
|
20477
20627
|
const maxResults = p.maxResults ?? 20;
|
|
20628
|
+
const allowed = options.tracker.recordCall("security_search");
|
|
20629
|
+
if (!allowed) {
|
|
20630
|
+
metrics.increment("tool_security_search_calls_total", 1, { status: "rate_limited" });
|
|
20631
|
+
return {
|
|
20632
|
+
content: [{ type: "text", text: options.tracker.getLimitMessage("security_search") }],
|
|
20633
|
+
details: { blocked: true, reason: "limit_reached" }
|
|
20634
|
+
};
|
|
20635
|
+
}
|
|
20478
20636
|
metrics.increment("tool_security_search_terms_total", terms.length);
|
|
20479
20637
|
metrics.increment("tool_security_search_databases_total", databases.length);
|
|
20480
20638
|
let results;
|
|
@@ -20882,20 +21040,19 @@ var StackExchangeClient = class {
|
|
|
20882
21040
|
}
|
|
20883
21041
|
});
|
|
20884
21042
|
}
|
|
21043
|
+
// Pure accessor — no metrics side effects. `stackexchangeCommand()` calls this
|
|
21044
|
+
// repeatedly per invocation (pre-request gate checks, success footer, error
|
|
21045
|
+
// footer) purely to read current state for display; incrementing counters here
|
|
21046
|
+
// used to record 2-3x the real number of quota-low/exhausted events per call.
|
|
21047
|
+
// The counters are incremented once, at the single gate check that actually
|
|
21048
|
+
// observes each event (see stackexchange/index.ts).
|
|
20885
21049
|
getQuotaInfo() {
|
|
20886
|
-
|
|
21050
|
+
return {
|
|
20887
21051
|
remaining: this.quotaRemaining,
|
|
20888
21052
|
max: this.quotaMax,
|
|
20889
21053
|
requestCount: this.requestCount,
|
|
20890
21054
|
lastBackoff: this.lastBackoff
|
|
20891
21055
|
};
|
|
20892
|
-
if (this.isQuotaExhausted()) {
|
|
20893
|
-
metrics.increment("stackexchange_quota_exhausted_total", 1);
|
|
20894
|
-
}
|
|
20895
|
-
if (this.isQuotaLow()) {
|
|
20896
|
-
metrics.increment("stackexchange_quota_low_total", 1);
|
|
20897
|
-
}
|
|
20898
|
-
return info;
|
|
20899
21056
|
}
|
|
20900
21057
|
isQuotaExhausted() {
|
|
20901
21058
|
return this.quotaExhaustedAt !== null && Date.now() - this.quotaExhaustedAt < QUOTA_PROBE_COOLDOWN_MS;
|
|
@@ -21174,6 +21331,7 @@ function formatSitesCompact(sites) {
|
|
|
21174
21331
|
|
|
21175
21332
|
// src/stackexchange/index.ts
|
|
21176
21333
|
init_logger();
|
|
21334
|
+
init_metrics();
|
|
21177
21335
|
function notify(ctx, message, type) {
|
|
21178
21336
|
ctx.ui?.notify?.(message, type);
|
|
21179
21337
|
}
|
|
@@ -21200,6 +21358,7 @@ async function stackexchangeCommand(options) {
|
|
|
21200
21358
|
const client = getClient(config);
|
|
21201
21359
|
try {
|
|
21202
21360
|
if (client.isQuotaExhausted()) {
|
|
21361
|
+
metrics.increment("stackexchange_quota_exhausted_total", 1);
|
|
21203
21362
|
const quota2 = client.getQuotaInfo();
|
|
21204
21363
|
notify(
|
|
21205
21364
|
ctx,
|
|
@@ -21215,6 +21374,7 @@ async function stackexchangeCommand(options) {
|
|
|
21215
21374
|
};
|
|
21216
21375
|
}
|
|
21217
21376
|
if (client.isQuotaLow()) {
|
|
21377
|
+
metrics.increment("stackexchange_quota_low_total", 1);
|
|
21218
21378
|
const quota2 = client.getQuotaInfo();
|
|
21219
21379
|
notify(
|
|
21220
21380
|
ctx,
|
|
@@ -21663,6 +21823,7 @@ init_http_body();
|
|
|
21663
21823
|
init_logger();
|
|
21664
21824
|
init_safe_unref();
|
|
21665
21825
|
init_metrics();
|
|
21826
|
+
init_http_body();
|
|
21666
21827
|
var DEFAULT_REQUEST_KEY = "O43z0dpjhgX20SCx4KAo";
|
|
21667
21828
|
var BRIDGED_GLOBALS = ["window", "document", "location", "navigator", "origin"];
|
|
21668
21829
|
function describeError(error) {
|
|
@@ -21762,7 +21923,10 @@ async function doMint(identifiers, opts) {
|
|
|
21762
21923
|
body: JSON.stringify([requestKey, botguardResponse]),
|
|
21763
21924
|
signal: opts.signal
|
|
21764
21925
|
});
|
|
21765
|
-
|
|
21926
|
+
if (!integrityResponse.ok) {
|
|
21927
|
+
throw new Error(`HTTP ${integrityResponse.status} from attestation server`);
|
|
21928
|
+
}
|
|
21929
|
+
const integrityJson = await readJsonCapped(integrityResponse);
|
|
21766
21930
|
const integrityToken = integrityJson?.[0];
|
|
21767
21931
|
if (!integrityToken) {
|
|
21768
21932
|
throw new Error("Integrity token rejected by attestation server (likely a non-residential IP)");
|
|
@@ -21817,7 +21981,16 @@ async function fetchVideoTranscripts(videoIds, opts = {}) {
|
|
|
21817
21981
|
if (log?.setLevel && log.Level) log.setLevel(log.Level.NONE);
|
|
21818
21982
|
} catch {
|
|
21819
21983
|
}
|
|
21820
|
-
const
|
|
21984
|
+
const ssrfFetcher = await getSsrfSafeFetcher();
|
|
21985
|
+
const fetchImpl = ssrfFetcher ? (input, init) => {
|
|
21986
|
+
if (typeof input === "string" || input instanceof URL) {
|
|
21987
|
+
return ssrfFetcher.fetch(String(input), {
|
|
21988
|
+
...init ?? {},
|
|
21989
|
+
dispatcher: ssrfFetcher.dispatcher
|
|
21990
|
+
});
|
|
21991
|
+
}
|
|
21992
|
+
return fetch(input, init);
|
|
21993
|
+
} : (input, init) => fetch(input, init);
|
|
21821
21994
|
let visitorData;
|
|
21822
21995
|
try {
|
|
21823
21996
|
const seed = await breaker.execute(() => withTimeout2(
|
|
@@ -21900,10 +22073,27 @@ async function fetchOne(session, videoId, contentToken, lang, timeoutMs, signal,
|
|
|
21900
22073
|
const sep3 = track.base_url.includes("?") ? "&" : "?";
|
|
21901
22074
|
const url = `${track.base_url}${sep3}fmt=json3&c=WEB&pot=${encodeURIComponent(contentToken)}`;
|
|
21902
22075
|
const fetchSignal = createTimeoutSignal(timeoutMs, signal);
|
|
21903
|
-
const
|
|
21904
|
-
|
|
21905
|
-
|
|
21906
|
-
|
|
22076
|
+
const MAX_REDIRECTS = 5;
|
|
22077
|
+
let currentUrl = url;
|
|
22078
|
+
let response;
|
|
22079
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
22080
|
+
response = await fetchImpl(currentUrl, {
|
|
22081
|
+
signal: fetchSignal,
|
|
22082
|
+
redirect: "manual",
|
|
22083
|
+
headers: { "User-Agent": "Mozilla/5.0", "Accept-Language": lang }
|
|
22084
|
+
});
|
|
22085
|
+
if (response.status >= 300 && response.status < 400) {
|
|
22086
|
+
const location = response.headers.get("location");
|
|
22087
|
+
if (!location) break;
|
|
22088
|
+
const resolved = new URL(location, currentUrl).href;
|
|
22089
|
+
assertTrustedTimedtextUrl(resolved);
|
|
22090
|
+
void response.body?.cancel()?.catch(() => {
|
|
22091
|
+
});
|
|
22092
|
+
currentUrl = resolved;
|
|
22093
|
+
continue;
|
|
22094
|
+
}
|
|
22095
|
+
break;
|
|
22096
|
+
}
|
|
21907
22097
|
if (!response.ok) {
|
|
21908
22098
|
void response.body?.cancel()?.catch(() => {
|
|
21909
22099
|
});
|
|
@@ -22008,6 +22198,8 @@ var PermanentError = class extends Error {
|
|
|
22008
22198
|
|
|
22009
22199
|
// src/youtube/index.ts
|
|
22010
22200
|
init_metrics();
|
|
22201
|
+
init_text_utils();
|
|
22202
|
+
init_constants();
|
|
22011
22203
|
function buildTranscriptDocument(r) {
|
|
22012
22204
|
const header = [];
|
|
22013
22205
|
if (r.title) header.push(`# ${r.title}`);
|
|
@@ -22085,7 +22277,7 @@ function formatResults(results, rejected) {
|
|
|
22085
22277
|
md += `**Cite as:** ${r.url}${r.title ? ` \u2014 '${r.title}'${r.author ? ` by ${r.author}` : ""}` : ""}
|
|
22086
22278
|
`;
|
|
22087
22279
|
md += `
|
|
22088
|
-
${r.text}
|
|
22280
|
+
${truncateWithMarker(r.text ?? "", MAX_SCRAPE_CONTENT_CHARS_PER_DOC)}
|
|
22089
22281
|
|
|
22090
22282
|
---
|
|
22091
22283
|
|
|
@@ -22424,10 +22616,10 @@ var ToolUsageTracker = class {
|
|
|
22424
22616
|
this.usage.clear();
|
|
22425
22617
|
}
|
|
22426
22618
|
};
|
|
22427
|
-
function createDefaultToolLimits(config) {
|
|
22619
|
+
function createDefaultToolLimits(config, cachingActive = false) {
|
|
22428
22620
|
return {
|
|
22429
22621
|
gathering: getMaxGatheringCalls(config),
|
|
22430
|
-
scrape: getMaxScrapeBatches(config),
|
|
22622
|
+
scrape: getMaxScrapeBatches(config, cachingActive),
|
|
22431
22623
|
search: 1,
|
|
22432
22624
|
youtube_transcript: 1,
|
|
22433
22625
|
grep: MAX_GREP_CALLS,
|
|
@@ -22491,7 +22683,7 @@ async function createResearcherSession(options) {
|
|
|
22491
22683
|
});
|
|
22492
22684
|
logger.info(`[Researcher] Using model for researcher ${researcherId}: ${modelToUse.provider}/${modelToUse.id}`);
|
|
22493
22685
|
try {
|
|
22494
|
-
const tracker = new ToolUsageTracker(createDefaultToolLimits(config));
|
|
22686
|
+
const tracker = new ToolUsageTracker(createDefaultToolLimits(config, isPromptCachingActiveForModel(modelToUse)));
|
|
22495
22687
|
const globalLinks = updateGlobalLinks || (() => {
|
|
22496
22688
|
});
|
|
22497
22689
|
const customTools = createResearchTools({
|
|
@@ -22719,7 +22911,7 @@ function endResearchSession(piSessionId, researchId) {
|
|
|
22719
22911
|
state.order.splice(index, 1);
|
|
22720
22912
|
notifyOrderChange(sid);
|
|
22721
22913
|
}
|
|
22722
|
-
if (state.order.length === 0 && state.panels.size === 0) {
|
|
22914
|
+
if (state.order.length === 0 && state.panels.size === 0 && state.aborts.size === 0) {
|
|
22723
22915
|
state.steeringMessages = state.steeringMessages.filter((m) => m.status === "queued");
|
|
22724
22916
|
const hasRemainingSteering = state.steeringMessages.length > 0;
|
|
22725
22917
|
if (!hasRemainingSteering) {
|
|
@@ -22731,7 +22923,6 @@ function endResearchSession(piSessionId, researchId) {
|
|
|
22731
22923
|
} else {
|
|
22732
22924
|
state.failures.clear();
|
|
22733
22925
|
state.failureReasons.clear();
|
|
22734
|
-
state.aborts.clear();
|
|
22735
22926
|
}
|
|
22736
22927
|
}
|
|
22737
22928
|
}
|
|
@@ -22895,6 +23086,7 @@ ${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
|
|
|
22895
23086
|
let lastError;
|
|
22896
23087
|
const researcherExecutionStartMs = Date.now();
|
|
22897
23088
|
const deliveredSteeringIds = /* @__PURE__ */ new Set();
|
|
23089
|
+
const researcherSystemPrompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{extra_tool_guidelines}}", "").replace("{{digest_section}}", () => RESEARCHER_DIGEST_SECTION).trim();
|
|
22898
23090
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
22899
23091
|
if (signal?.aborted) throw new Error("Aborted");
|
|
22900
23092
|
if (stopRequested()) {
|
|
@@ -22928,14 +23120,25 @@ ${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
|
|
|
22928
23120
|
const allSteering = getSteeringMessages(sessionId);
|
|
22929
23121
|
let steeringSection = "";
|
|
22930
23122
|
if (allSteering.length > 0) {
|
|
22931
|
-
steeringSection = "
|
|
23123
|
+
steeringSection = "### ADDITIONAL USER GUIDANCE (Mandatory directional rules for your research)\n" + allSteering.map((m) => {
|
|
22932
23124
|
deliveredSteeringIds.add(m.id);
|
|
22933
23125
|
return `- ${m.text}`;
|
|
22934
23126
|
}).join("\n");
|
|
22935
23127
|
}
|
|
22936
|
-
const
|
|
23128
|
+
const userMessage = [
|
|
23129
|
+
`Topic: ${researcherConfig.name}
|
|
23130
|
+
Goal: ${researcherConfig.goal}`,
|
|
23131
|
+
storeSection.trim(),
|
|
23132
|
+
evidenceSection.trim(),
|
|
23133
|
+
previousQueriesSection.trim(),
|
|
23134
|
+
steeringSection,
|
|
23135
|
+
"Perform your research and submit your full report now."
|
|
23136
|
+
].filter(Boolean).join("\n\n");
|
|
22937
23137
|
logger.debug(`[ResearcherExecutor] Researcher ${id} attempt ${attempt} System Prompt:
|
|
22938
|
-
${
|
|
23138
|
+
${researcherSystemPrompt}
|
|
23139
|
+
|
|
23140
|
+
User Message:
|
|
23141
|
+
${userMessage}`);
|
|
22939
23142
|
const workerExclude = ["search"];
|
|
22940
23143
|
const mergedExclude = [.../* @__PURE__ */ new Set([...workerExclude, ...options.excludeTools || []])];
|
|
22941
23144
|
const scrapeEnabled = !mergedExclude.includes("scrape") && process.env["PI_RESEARCH_MOCK_SCRAPE"] !== "true";
|
|
@@ -22945,7 +23148,7 @@ ${prompt}`);
|
|
|
22945
23148
|
cwd: ctx.cwd,
|
|
22946
23149
|
ctxModel: model,
|
|
22947
23150
|
modelRegistry: ctx.modelRegistry,
|
|
22948
|
-
systemPrompt:
|
|
23151
|
+
systemPrompt: researcherSystemPrompt,
|
|
22949
23152
|
extensionCtx: ctx,
|
|
22950
23153
|
excludeTools: mergedExclude,
|
|
22951
23154
|
researcherId: id,
|
|
@@ -23060,10 +23263,7 @@ ${prompt}`);
|
|
|
23060
23263
|
});
|
|
23061
23264
|
let abortCleanup;
|
|
23062
23265
|
try {
|
|
23063
|
-
const promptPromise = session.prompt(
|
|
23064
|
-
Goal: ${researcherConfig.goal}
|
|
23065
|
-
|
|
23066
|
-
Perform your research and submit your full report now.`);
|
|
23266
|
+
const promptPromise = session.prompt(userMessage);
|
|
23067
23267
|
promptPromise.catch((err) => logger.debug(`[ResearcherExecutor] Background session prompt rejection: ${err.message}`));
|
|
23068
23268
|
await Promise.race([
|
|
23069
23269
|
promptPromise,
|
|
@@ -23167,6 +23367,7 @@ init_logger();
|
|
|
23167
23367
|
init_config();
|
|
23168
23368
|
init_text_utils();
|
|
23169
23369
|
init_constants();
|
|
23370
|
+
init_research_model_resolver();
|
|
23170
23371
|
|
|
23171
23372
|
// src/orchestration/headless-observer.ts
|
|
23172
23373
|
init_logger();
|
|
@@ -23362,7 +23563,10 @@ var QuickResearchOrchestrator = class {
|
|
|
23362
23563
|
}
|
|
23363
23564
|
}
|
|
23364
23565
|
const researcherPromptTemplate = loadPrompt("researcher");
|
|
23365
|
-
const
|
|
23566
|
+
const cachingActiveForBudget = isPromptCachingActiveForModel(
|
|
23567
|
+
resolveResearchModel({ modelRegistry: ctx.modelRegistry, config: this.config, hostModel: model, cwd: ctx.cwd })
|
|
23568
|
+
);
|
|
23569
|
+
const maxScrapeBatches = getMaxScrapeBatches(this.config, cachingActiveForBudget);
|
|
23366
23570
|
const maxScrapeBatchesDisplay = maxScrapeBatches > 99 ? "unlimited" : maxScrapeBatches.toString();
|
|
23367
23571
|
const quickEvidenceSection = `## Search
|
|
23368
23572
|
You have access to the \`search\` tool. You get EXACTLY ONE search call \u2014 make it count.
|
|
@@ -23385,9 +23589,19 @@ Prioritize primary sources and authoritative data.`;
|
|
|
23385
23589
|
evidenceLines.push("## Initial Links\nInvestigate these provided URLs first:\n" + this.options.initialLinks.map((l) => `- ${l}`).join("\n"));
|
|
23386
23590
|
}
|
|
23387
23591
|
evidenceLines.push(quickEvidenceSection);
|
|
23388
|
-
const prompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{
|
|
23592
|
+
const prompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{extra_tool_guidelines}}", "- `search`: Perform broad web searches (Round 1 only).").replace("{{digest_section}}", "").trim();
|
|
23593
|
+
const userMessage = [
|
|
23594
|
+
`Goal: ${query}`,
|
|
23595
|
+
storeSection.trim(),
|
|
23596
|
+
evidenceLines.join("\n\n").trim(),
|
|
23597
|
+
steeringSection.trim(),
|
|
23598
|
+
"Perform your research and submit your full report now."
|
|
23599
|
+
].filter(Boolean).join("\n\n");
|
|
23389
23600
|
logger.debug(`[QuickOrchestrator] System Prompt:
|
|
23390
|
-
${prompt}
|
|
23601
|
+
${prompt}
|
|
23602
|
+
|
|
23603
|
+
User Message:
|
|
23604
|
+
${userMessage}`);
|
|
23391
23605
|
let lastSeenSearchCount = 0;
|
|
23392
23606
|
const { session, resolvedModel } = await createResearcherSession({
|
|
23393
23607
|
cwd: ctx.cwd,
|
|
@@ -23481,7 +23695,7 @@ ${prompt}`);
|
|
|
23481
23695
|
});
|
|
23482
23696
|
let abortCleanup;
|
|
23483
23697
|
try {
|
|
23484
|
-
const promptPromise = session.prompt(
|
|
23698
|
+
const promptPromise = session.prompt(userMessage);
|
|
23485
23699
|
promptPromise.catch((err) => logger.debug(`[QuickOrchestrator] Background session prompt rejection: ${err.message}`));
|
|
23486
23700
|
await Promise.race([
|
|
23487
23701
|
promptPromise,
|
|
@@ -23898,7 +24112,6 @@ var DeepResearchOrchestrator = class {
|
|
|
23898
24112
|
if (!routerChoseSynthesis) {
|
|
23899
24113
|
if (this.currentRound > lastAnnouncedRound) {
|
|
23900
24114
|
observer?.onRoundStart?.(this.currentRound);
|
|
23901
|
-
lastAnnouncedRound = this.currentRound;
|
|
23902
24115
|
}
|
|
23903
24116
|
observer?.onEvaluationStart?.(this.currentRound);
|
|
23904
24117
|
observer?.onEvaluationProgress?.("evaluating");
|
|
@@ -24632,6 +24845,10 @@ init_service_registry();
|
|
|
24632
24845
|
init_service_names();
|
|
24633
24846
|
var NO_SOURCES_NOTE = "CITED LINKS\n_No web sources were successfully retrieved for this report \u2014 searches or page fetches may have been blocked (for example by site bot-protection) or returned no usable results. Treat the findings above as unverified._";
|
|
24634
24847
|
var UNVERIFIED_LINKS_NOTE = "_None of the links above could be verified: no page fetch succeeded this session and no citation could be matched to retrieved content. They may be inaccurate or invented \u2014 check each one before relying on it._";
|
|
24848
|
+
var WEAK_GROUNDING_MIN_SOURCES = 3;
|
|
24849
|
+
function weakGroundingNote(count) {
|
|
24850
|
+
return `_Grounding notice: only ${count} source${count === 1 ? "" : "s"} could be verified for this report. Claims not attributable to the cited source` + (count === 1 ? "" : "s") + " above should be treated as unverified \u2014 cross-check anything load-bearing before relying on it._";
|
|
24851
|
+
}
|
|
24635
24852
|
function redactUnverifiedProseUrls(text, trusted) {
|
|
24636
24853
|
if (trusted.size === 0) return text;
|
|
24637
24854
|
let removed = 0;
|
|
@@ -24874,6 +25091,14 @@ Research performed using ${modelId}`;
|
|
|
24874
25091
|
body = rewriteCitationMarkers(body, localToGlobal, (n) => droppedNumbers.has(n));
|
|
24875
25092
|
}
|
|
24876
25093
|
body = redactUnverifiedProseUrls(body, trustedUrls);
|
|
25094
|
+
if (globalCitations.length < WEAK_GROUNDING_MIN_SOURCES) {
|
|
25095
|
+
logger.warn(`[ResearchSynthesisService] Report is weakly grounded: only ${globalCitations.length} verified source(s) \u2014 appending grounding notice.`);
|
|
25096
|
+
return `${body}
|
|
25097
|
+
|
|
25098
|
+
${weakGroundingNote(globalCitations.length)}
|
|
25099
|
+
|
|
25100
|
+
${verifiedLinksSection}`;
|
|
25101
|
+
}
|
|
24877
25102
|
return `${body}
|
|
24878
25103
|
|
|
24879
25104
|
${verifiedLinksSection}`;
|
|
@@ -24882,6 +25107,14 @@ ${verifiedLinksSection}`;
|
|
|
24882
25107
|
const validIds = new Set(globalCitations.map((c) => c.id));
|
|
24883
25108
|
let prose = rewriteCitationMarkers(synthesis.trim(), /* @__PURE__ */ new Map(), (n) => !validIds.has(n));
|
|
24884
25109
|
prose = redactUnverifiedProseUrls(prose, trustedUrls);
|
|
25110
|
+
if (globalCitations.length < WEAK_GROUNDING_MIN_SOURCES) {
|
|
25111
|
+
logger.warn(`[ResearchSynthesisService] Report is weakly grounded: only ${globalCitations.length} verified source(s) \u2014 appending grounding notice.`);
|
|
25112
|
+
return `${prose}
|
|
25113
|
+
|
|
25114
|
+
${weakGroundingNote(globalCitations.length)}
|
|
25115
|
+
|
|
25116
|
+
${verifiedLinksSection}`;
|
|
25117
|
+
}
|
|
24885
25118
|
return `${prose}
|
|
24886
25119
|
|
|
24887
25120
|
${verifiedLinksSection}`;
|
|
@@ -25116,6 +25349,8 @@ function extractRunStats(snapshot) {
|
|
|
25116
25349
|
};
|
|
25117
25350
|
const errors = sumCounter(counters, "researcher_errors_total") + sumCounter(counters, "llm_api_errors_total");
|
|
25118
25351
|
const tokens = sumCounter(counters, "llm_tokens_total");
|
|
25352
|
+
const cacheReadTokens = sumCounter(counters, "llm_cache_read_tokens_total");
|
|
25353
|
+
const cacheWriteTokens = sumCounter(counters, "llm_cache_write_tokens_total");
|
|
25119
25354
|
const cost = sumCounter(counters, "llm_cost_total");
|
|
25120
25355
|
let durationMs = 0;
|
|
25121
25356
|
for (const key of Object.keys(histograms)) {
|
|
@@ -25150,6 +25385,8 @@ function extractRunStats(snapshot) {
|
|
|
25150
25385
|
urlsFailed,
|
|
25151
25386
|
errors,
|
|
25152
25387
|
tokens,
|
|
25388
|
+
cacheReadTokens,
|
|
25389
|
+
cacheWriteTokens,
|
|
25153
25390
|
cost,
|
|
25154
25391
|
toolUsage
|
|
25155
25392
|
};
|
|
@@ -25181,6 +25418,7 @@ var _onSignal = null;
|
|
|
25181
25418
|
var _sigintHandler = null;
|
|
25182
25419
|
var _sigtermHandler = null;
|
|
25183
25420
|
var _sighupHandler = null;
|
|
25421
|
+
var _sigquitHandler = null;
|
|
25184
25422
|
var _sigbreakHandler = null;
|
|
25185
25423
|
var _shuttingDown = false;
|
|
25186
25424
|
var _shutdownPromise = null;
|
|
@@ -25201,9 +25439,11 @@ function _registerSignalHandlers() {
|
|
|
25201
25439
|
_sigintHandler = () => _onSignal?.("SIGINT");
|
|
25202
25440
|
_sigtermHandler = () => _onSignal?.("SIGTERM");
|
|
25203
25441
|
_sighupHandler = () => _onSignal?.("SIGHUP");
|
|
25442
|
+
_sigquitHandler = () => _onSignal?.("SIGQUIT");
|
|
25204
25443
|
process.on("SIGINT", _sigintHandler);
|
|
25205
25444
|
process.on("SIGTERM", _sigtermHandler);
|
|
25206
25445
|
process.on("SIGHUP", _sighupHandler);
|
|
25446
|
+
process.on("SIGQUIT", _sigquitHandler);
|
|
25207
25447
|
if (process.platform === "win32") {
|
|
25208
25448
|
_sigbreakHandler = () => _onSignal?.("SIGBREAK");
|
|
25209
25449
|
process.on("SIGBREAK", _sigbreakHandler);
|
|
@@ -25222,6 +25462,10 @@ function _removeSignalHandlers() {
|
|
|
25222
25462
|
process.removeListener("SIGHUP", _sighupHandler);
|
|
25223
25463
|
_sighupHandler = null;
|
|
25224
25464
|
}
|
|
25465
|
+
if (_sigquitHandler) {
|
|
25466
|
+
process.removeListener("SIGQUIT", _sigquitHandler);
|
|
25467
|
+
_sigquitHandler = null;
|
|
25468
|
+
}
|
|
25225
25469
|
if (_sigbreakHandler) {
|
|
25226
25470
|
process.removeListener("SIGBREAK", _sigbreakHandler);
|
|
25227
25471
|
_sigbreakHandler = null;
|
|
@@ -25303,6 +25547,10 @@ async function _doInit(options = {}) {
|
|
|
25303
25547
|
}
|
|
25304
25548
|
}
|
|
25305
25549
|
async function initResearchSDK(options = {}) {
|
|
25550
|
+
if (_shutdownPromise) {
|
|
25551
|
+
await _shutdownPromise.catch(() => {
|
|
25552
|
+
});
|
|
25553
|
+
}
|
|
25306
25554
|
if (isInitialized) {
|
|
25307
25555
|
logger.warn("[SDK] Research SDK already initialized. Call shutdownResearchSDK() first if you want to re-initialize with new options.");
|
|
25308
25556
|
return;
|
|
@@ -25653,7 +25901,7 @@ init_logger();
|
|
|
25653
25901
|
init_constants();
|
|
25654
25902
|
import { promises as fs15 } from "node:fs";
|
|
25655
25903
|
import { homedir as homedir4, tmpdir as tmpdir4, platform as platform3 } from "node:os";
|
|
25656
|
-
import { join as
|
|
25904
|
+
import { join as join20, resolve as resolve5, normalize, sep as sep2 } from "node:path";
|
|
25657
25905
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
25658
25906
|
var PREFERRED_SUBDIRS = [
|
|
25659
25907
|
"research",
|
|
@@ -25724,7 +25972,7 @@ async function findPreferredSubdir(cwd) {
|
|
|
25724
25972
|
for (const target of PREFERRED_SUBDIRS) {
|
|
25725
25973
|
const match = entries.find((e) => e.toLowerCase() === target);
|
|
25726
25974
|
if (match === void 0) continue;
|
|
25727
|
-
const candidate =
|
|
25975
|
+
const candidate = join20(cwd, match);
|
|
25728
25976
|
try {
|
|
25729
25977
|
const stat6 = await fs15.stat(candidate);
|
|
25730
25978
|
if (stat6.isDirectory()) return candidate;
|
|
@@ -25769,7 +26017,7 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
|
|
|
25769
26017
|
for (let attempt = 0; attempt < MAX_EXPORT_RETRIES; attempt++) {
|
|
25770
26018
|
const hash = generateHash();
|
|
25771
26019
|
const filename = `${baseFilename}-${hash}.md`;
|
|
25772
|
-
const filepath =
|
|
26020
|
+
const filepath = join20(targetDir, filename);
|
|
25773
26021
|
try {
|
|
25774
26022
|
await fs15.writeFile(filepath, result, { flag: "wx" });
|
|
25775
26023
|
logger.log(`[export] Research report saved to: ${filepath}`);
|
|
@@ -25904,10 +26152,17 @@ function makeProgressObserver() {
|
|
|
25904
26152
|
}
|
|
25905
26153
|
break;
|
|
25906
26154
|
}
|
|
25907
|
-
case "complete":
|
|
26155
|
+
case "complete": {
|
|
25908
26156
|
toStderr(` \u2022 complete (${data?.result?.length ?? 0} chars)
|
|
25909
26157
|
`);
|
|
26158
|
+
const reportText = typeof data?.result === "string" ? data.result : "";
|
|
26159
|
+
if (reportText.includes("No web sources were successfully retrieved") || reportText.includes("None of the links above could be verified")) {
|
|
26160
|
+
toStderr(" \u2022 WARNING: no web sources could be retrieved \u2014 the report is unverified\n");
|
|
26161
|
+
} else if (reportText.includes("Grounding notice: only")) {
|
|
26162
|
+
toStderr(" \u2022 WARNING: weakly grounded report (fewer than 3 verified sources) \u2014 treat unattributed claims as unverified\n");
|
|
26163
|
+
}
|
|
25910
26164
|
break;
|
|
26165
|
+
}
|
|
25911
26166
|
case "error":
|
|
25912
26167
|
toStderr(` \u2022 error: ${data?.message}
|
|
25913
26168
|
`);
|
|
@@ -25966,8 +26221,8 @@ function resolvedConfigPaths() {
|
|
|
25966
26221
|
configDir,
|
|
25967
26222
|
cliIfaceEnv: getInterfaceEnvFilePath("cli"),
|
|
25968
26223
|
piIfaceEnv: getInterfaceEnvFilePath("pi"),
|
|
25969
|
-
piAuth:
|
|
25970
|
-
piModels:
|
|
26224
|
+
piAuth: path19.join(agentDir, "auth.json"),
|
|
26225
|
+
piModels: path19.join(agentDir, "models.json"),
|
|
25971
26226
|
piState: getStateDir()
|
|
25972
26227
|
};
|
|
25973
26228
|
}
|
|
@@ -26223,7 +26478,7 @@ Configure in:
|
|
|
26223
26478
|
config: getConfig(process.cwd(), "cli"),
|
|
26224
26479
|
ignoreGlobalConfig: true
|
|
26225
26480
|
});
|
|
26226
|
-
toStderr(`[pi-research] searching knowledge store (${queries.length}
|
|
26481
|
+
toStderr(`[pi-research] searching knowledge store (${queries.length} ${queries.length === 1 ? "query" : "queries"})\u2026
|
|
26227
26482
|
`);
|
|
26228
26483
|
result = await searchKnowledge(queries, abortController.signal);
|
|
26229
26484
|
} catch (err) {
|
|
@@ -26881,9 +27136,15 @@ async function main(argv) {
|
|
|
26881
27136
|
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
26882
27137
|
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
26883
27138
|
process.on("SIGHUP", () => onSignal("SIGHUP"));
|
|
27139
|
+
process.on("SIGQUIT", () => onSignal("SIGQUIT"));
|
|
26884
27140
|
if (process.platform === "win32") {
|
|
26885
27141
|
process.on("SIGBREAK", () => onSignal("SIGBREAK"));
|
|
26886
27142
|
}
|
|
27143
|
+
if (process.env["PI_RESEARCH_TEST_HANG_FOR_SIGNAL"] === "1") {
|
|
27144
|
+
process.stdout.write("PI_RESEARCH_TEST_READY\n");
|
|
27145
|
+
await new Promise(() => setInterval(() => {
|
|
27146
|
+
}, 1e3));
|
|
27147
|
+
}
|
|
26887
27148
|
let parsed;
|
|
26888
27149
|
try {
|
|
26889
27150
|
parsed = parseArgs(argv);
|
|
@@ -26940,8 +27201,8 @@ Error: ${err.message}
|
|
|
26940
27201
|
var _isMain = (() => {
|
|
26941
27202
|
try {
|
|
26942
27203
|
if (!process.argv[1]) return false;
|
|
26943
|
-
const modulePath = fs17.realpathSync(
|
|
26944
|
-
const invokedPath = fs17.realpathSync(
|
|
27204
|
+
const modulePath = fs17.realpathSync(path19.resolve(fileURLToPath5(import.meta.url)));
|
|
27205
|
+
const invokedPath = fs17.realpathSync(path19.resolve(process.argv[1]));
|
|
26945
27206
|
return modulePath === invokedPath;
|
|
26946
27207
|
} catch {
|
|
26947
27208
|
return false;
|
|
@@ -26968,6 +27229,7 @@ export {
|
|
|
26968
27229
|
cmdKnowledgeConfig,
|
|
26969
27230
|
cmdResearch,
|
|
26970
27231
|
exitCodeForSignal,
|
|
27232
|
+
makeProgressObserver,
|
|
26971
27233
|
parseArgs,
|
|
26972
27234
|
reportError
|
|
26973
27235
|
};
|