@lincoln504/pi-research 1.0.5 → 1.0.8
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 +210 -40
- package/dist/prompts/research-tool-usage.md +1 -0
- package/dist/thread-worker.mjs +2 -0
- package/docs/ARCHITECTURE.md +23 -7
- package/docs/CHANGELOG.md +108 -0
- package/docs/CONFIGURATION.md +1 -0
- package/docs/SDK.md +4 -1
- package/package.json +6 -5
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +53 -4
- package/src/config.ts +4 -0
- package/src/index.ts +2 -0
- package/src/infrastructure/browser/task-execution-service.ts +15 -3
- package/src/infrastructure/browser/thread-worker.mjs +2 -0
- package/src/orchestration/headless-observer.ts +27 -9
- package/src/orchestration/research-orchestration-service.ts +6 -6
- package/src/orchestration/research-synthesis-service.ts +31 -20
- package/src/orchestration/researcher-executor.ts +51 -13
- package/src/orchestration/session-state.ts +27 -4
- package/src/prompts/research-tool-usage.md +1 -0
- package/src/sdk.ts +57 -4
- package/src/youtube/index.ts +5 -4
package/dist/cli.mjs
CHANGED
|
@@ -2295,6 +2295,7 @@ function saveConfig(config, scope = "local", cwd = process.cwd(), changedKeys) {
|
|
|
2295
2295
|
PI_RESEARCH_MAX_RESEARCHERS: String(config.MAX_CONCURRENT_RESEARCHERS),
|
|
2296
2296
|
PI_RESEARCH_MAX_RETRIES: String(config.RESEARCHER_MAX_RETRIES),
|
|
2297
2297
|
PI_RESEARCH_RETRY_DELAY_MS: String(config.RESEARCHER_MAX_RETRY_DELAY_MS),
|
|
2298
|
+
PI_RESEARCH_MAX_FAILED_RESEARCHERS: String(config.MAX_FAILED_RESEARCHERS ?? DEFAULTS.MAX_FAILED_RESEARCHERS),
|
|
2298
2299
|
PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS: String(config.HEALTH_CHECK_TIMEOUT_MS ?? DEFAULTS.HEALTH_CHECK_TIMEOUT_MS),
|
|
2299
2300
|
PI_RESEARCH_SEARCH_TIMEOUT_MS: String(config.SEARCH_TIMEOUT_MS),
|
|
2300
2301
|
PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS: String(config.TUI_REFRESH_DEBOUNCE_MS),
|
|
@@ -2457,6 +2458,7 @@ function createConfig(env, processEnv) {
|
|
|
2457
2458
|
MAX_CONCURRENT_RESEARCHERS: parseEnvNumber(e, "PI_RESEARCH_MAX_RESEARCHERS", DEFAULTS.MAX_CONCURRENT_RESEARCHERS, 1, 5, true),
|
|
2458
2459
|
RESEARCHER_MAX_RETRIES: parseEnvNumber(e, "PI_RESEARCH_MAX_RETRIES", DEFAULTS.RESEARCHER_MAX_RETRIES, 0, 5, true),
|
|
2459
2460
|
RESEARCHER_MAX_RETRY_DELAY_MS: parseEnvNumber(e, "PI_RESEARCH_RETRY_DELAY_MS", DEFAULTS.RESEARCHER_MAX_RETRY_DELAY_MS, 100, 1e4),
|
|
2461
|
+
MAX_FAILED_RESEARCHERS: parseEnvNumber(e, "PI_RESEARCH_MAX_FAILED_RESEARCHERS", DEFAULTS.MAX_FAILED_RESEARCHERS, 1, 10, true),
|
|
2460
2462
|
DEFAULT_RESEARCH_DEPTH: parseEnvNumber(e, "PI_RESEARCH_DEFAULT_RESEARCH_DEPTH", DEFAULTS.DEFAULT_RESEARCH_DEPTH, 1, 3, true),
|
|
2461
2463
|
MAX_SCRAPE_BATCHES: parseEnvNumber(e, "PI_RESEARCH_MAX_SCRAPE_BATCHES", DEFAULTS.MAX_SCRAPE_BATCHES, 0, 99, true),
|
|
2462
2464
|
MAX_GATHERING_CALLS: parseEnvNumber(e, "PI_RESEARCH_MAX_GATHERING_CALLS", DEFAULTS.MAX_GATHERING_CALLS, 1, 100, true),
|
|
@@ -2580,6 +2582,8 @@ var init_config = __esm({
|
|
|
2580
2582
|
RESEARCHER_MAX_RETRIES: Type.Number({ minimum: 0, maximum: 5, default: 2 }),
|
|
2581
2583
|
/** Base delay between retries in milliseconds (default: 2000, range: 100-10000) */
|
|
2582
2584
|
RESEARCHER_MAX_RETRY_DELAY_MS: Type.Number({ minimum: 100, maximum: 1e4, default: 2e3 }),
|
|
2585
|
+
/** Unique researcher failures that abort the entire run (default: 2, range: 1-10) */
|
|
2586
|
+
MAX_FAILED_RESEARCHERS: Type.Number({ minimum: 1, maximum: 10, default: 2 }),
|
|
2583
2587
|
/** Target depth for recursive research (default: 1, range: 1-3) */
|
|
2584
2588
|
DEFAULT_RESEARCH_DEPTH: Type.Number({ minimum: 1, maximum: 3, default: 1 }),
|
|
2585
2589
|
/** Number of batches to allow for a single scrape tool call (default: 2, 0=unlimited) */
|
|
@@ -14650,7 +14654,7 @@ async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retrie
|
|
|
14650
14654
|
if (now - lastRestartTime > RESTART_COOLDOWN_MS) {
|
|
14651
14655
|
lastRestartTime = now;
|
|
14652
14656
|
logger.warn(`[BrowserManager] Forcing scheduler restart and retrying...`);
|
|
14653
|
-
await forceSchedulerRestart2(
|
|
14657
|
+
await forceSchedulerRestart2(false, container);
|
|
14654
14658
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
14655
14659
|
} else {
|
|
14656
14660
|
logger.warn(`[BrowserManager] Scheduler restart recently triggered, waiting for pool idle...`);
|
|
@@ -14685,7 +14689,7 @@ async function runBrowserHealthCheck(config, retries = 1, signal, container = ge
|
|
|
14685
14689
|
if (now - lastRestartTime > RESTART_COOLDOWN_MS) {
|
|
14686
14690
|
lastRestartTime = now;
|
|
14687
14691
|
logger.warn(`[BrowserManager] Forcing scheduler restart and retrying...`);
|
|
14688
|
-
await forceSchedulerRestart2(
|
|
14692
|
+
await forceSchedulerRestart2(false, container);
|
|
14689
14693
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
14690
14694
|
} else {
|
|
14691
14695
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
@@ -14724,7 +14728,7 @@ async function runWorkerSearch(query, config, signal, retries = 1, sessionId, co
|
|
|
14724
14728
|
if (now - lastRestartTime > RESTART_COOLDOWN_MS) {
|
|
14725
14729
|
lastRestartTime = now;
|
|
14726
14730
|
logger.warn(`[BrowserManager] Forcing scheduler restart and retrying...`);
|
|
14727
|
-
await forceSchedulerRestart2(
|
|
14731
|
+
await forceSchedulerRestart2(false, container);
|
|
14728
14732
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
14729
14733
|
} else {
|
|
14730
14734
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
@@ -19445,10 +19449,8 @@ function formatResults(results, rejected) {
|
|
|
19445
19449
|
if (typeof r.charCount === "number") meta.push(`${r.charCount.toLocaleString()} chars`);
|
|
19446
19450
|
if (meta.length) md += `**${meta.join(" \xB7 ")}**
|
|
19447
19451
|
`;
|
|
19448
|
-
|
|
19449
|
-
md += `**Cite as:** ${r.url} \u2014 '${r.title}'${r.author ? ` by ${r.author}` : ""}
|
|
19452
|
+
md += `**Cite as:** ${r.url}${r.title ? ` \u2014 '${r.title}'${r.author ? ` by ${r.author}` : ""}` : ""}
|
|
19450
19453
|
`;
|
|
19451
|
-
}
|
|
19452
19454
|
md += `
|
|
19453
19455
|
${r.text}
|
|
19454
19456
|
|
|
@@ -20108,10 +20110,10 @@ function getFailedResearchers(piSessionId, researchId) {
|
|
|
20108
20110
|
const failures = state.failures.get(researchId) || [];
|
|
20109
20111
|
return [...new Set(failures)];
|
|
20110
20112
|
}
|
|
20111
|
-
var
|
|
20112
|
-
function shouldStopResearch(piSessionId, researchId) {
|
|
20113
|
+
var DEFAULT_MAX_FAILED_RESEARCHERS = 2;
|
|
20114
|
+
function shouldStopResearch(piSessionId, researchId, maxFailedResearchers = DEFAULT_MAX_FAILED_RESEARCHERS) {
|
|
20113
20115
|
const sid = normalizeSessionId(piSessionId);
|
|
20114
|
-
return getFailedResearchers(sid, researchId).length >=
|
|
20116
|
+
return getFailedResearchers(sid, researchId).length >= maxFailedResearchers;
|
|
20115
20117
|
}
|
|
20116
20118
|
function getResearchStopMessage(piSessionId, researchId) {
|
|
20117
20119
|
const sid = normalizeSessionId(piSessionId);
|
|
@@ -20137,6 +20139,12 @@ function getResearchStopMessage(piSessionId, researchId) {
|
|
|
20137
20139
|
"Partial results may be available below."
|
|
20138
20140
|
].join("\n");
|
|
20139
20141
|
}
|
|
20142
|
+
var RESEARCH_STOPPED_ERROR_CODE = "RESEARCH_STOPPED";
|
|
20143
|
+
function createResearchStopError(piSessionId, researchId) {
|
|
20144
|
+
const err = new Error(getResearchStopMessage(piSessionId, researchId));
|
|
20145
|
+
err.code = RESEARCH_STOPPED_ERROR_CODE;
|
|
20146
|
+
return err;
|
|
20147
|
+
}
|
|
20140
20148
|
|
|
20141
20149
|
// src/orchestration/researcher-executor.ts
|
|
20142
20150
|
async function runResearcher(options) {
|
|
@@ -20171,18 +20179,42 @@ ${currentPlan.allQueries.map((q) => `- ${q}`).join("\n")}
|
|
|
20171
20179
|
Previous summary: ${e.description}`).join("\n");
|
|
20172
20180
|
}
|
|
20173
20181
|
const researcherPromptTemplate = loadPrompt("researcher");
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20178
|
-
|
|
20179
|
-
|
|
20182
|
+
let effectiveInitialLinks = initialLinks;
|
|
20183
|
+
if (effectiveInitialLinks.length === 0 && historicalUrls.length === 0) {
|
|
20184
|
+
const retryQueries = researcherConfig.queries?.length ? researcherConfig.queries : [researcherConfig.goal];
|
|
20185
|
+
const maxLinkAttempts = config.RESEARCHER_MAX_RETRIES + 1;
|
|
20186
|
+
for (let linkAttempt = 1; linkAttempt <= maxLinkAttempts && effectiveInitialLinks.length === 0; linkAttempt++) {
|
|
20187
|
+
if (linkAttempt > 1) {
|
|
20188
|
+
if (signal?.aborted || container?.isDisposing) break;
|
|
20189
|
+
const delay2 = Math.min(1e3 * Math.pow(2, linkAttempt - 2), config.RESEARCHER_MAX_RETRY_DELAY_MS);
|
|
20190
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} has no initial search results; retry ${linkAttempt - 1}/${config.RESEARCHER_MAX_RETRIES} after ${delay2}ms`);
|
|
20191
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
20192
|
+
}
|
|
20193
|
+
if (signal?.aborted || container?.isDisposing) break;
|
|
20194
|
+
try {
|
|
20195
|
+
const queryResults = await search(retryQueries, config, signal, void 0, container);
|
|
20196
|
+
const urls = /* @__PURE__ */ new Set();
|
|
20197
|
+
for (const qr of queryResults) {
|
|
20198
|
+
for (const r of qr.results || []) urls.add(r.url);
|
|
20199
|
+
}
|
|
20200
|
+
effectiveInitialLinks = [...urls];
|
|
20201
|
+
} catch (err) {
|
|
20202
|
+
logger.debug(`[ResearcherExecutor] Researcher ${id} link-retry search burst failed:`, err);
|
|
20203
|
+
}
|
|
20204
|
+
}
|
|
20205
|
+
if (effectiveInitialLinks.length === 0) {
|
|
20206
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} has no initial search results or historical links after ${maxLinkAttempts} attempt(s); skipping.`);
|
|
20207
|
+
recordResearcherFailure(sessionId, researchId, id, "no initial search results or historical links (search produced nothing usable)");
|
|
20208
|
+
metrics.increment("researcher_skipped_total", 1, { mode: "deep", complexity: String(complexity), reason: "no_initial_links" });
|
|
20209
|
+
observer?.onResearcherFailure?.(id, "No initial search results or historical links available");
|
|
20210
|
+
return;
|
|
20211
|
+
}
|
|
20180
20212
|
}
|
|
20181
20213
|
let evidenceSection = "";
|
|
20182
|
-
if (
|
|
20214
|
+
if (effectiveInitialLinks.length > 0) {
|
|
20183
20215
|
evidenceSection = `## Evidence Provided
|
|
20184
20216
|
Initial search results provided the following URLs to investigate:
|
|
20185
|
-
${
|
|
20217
|
+
${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
|
|
20186
20218
|
}
|
|
20187
20219
|
const maxAttempts = config.RESEARCHER_MAX_RETRIES + 1;
|
|
20188
20220
|
let lastError;
|
|
@@ -20428,11 +20460,11 @@ init_constants();
|
|
|
20428
20460
|
// src/orchestration/headless-observer.ts
|
|
20429
20461
|
init_logger();
|
|
20430
20462
|
function makeSafeObserver(observer) {
|
|
20431
|
-
return new Proxy(
|
|
20432
|
-
get(
|
|
20463
|
+
return new Proxy({}, {
|
|
20464
|
+
get(_target, prop) {
|
|
20433
20465
|
let value;
|
|
20434
20466
|
try {
|
|
20435
|
-
value = Reflect.get(
|
|
20467
|
+
value = Reflect.get(observer, prop, observer);
|
|
20436
20468
|
} catch (err) {
|
|
20437
20469
|
logger.warn(`[observer] Ignored error reading observer.${String(prop)}:`, err);
|
|
20438
20470
|
return void 0;
|
|
@@ -20440,7 +20472,7 @@ function makeSafeObserver(observer) {
|
|
|
20440
20472
|
if (typeof value !== "function") return value;
|
|
20441
20473
|
return (...args) => {
|
|
20442
20474
|
try {
|
|
20443
|
-
const out = value.apply(
|
|
20475
|
+
const out = value.apply(observer, args);
|
|
20444
20476
|
if (out && typeof out.then === "function") {
|
|
20445
20477
|
return out.catch((err) => {
|
|
20446
20478
|
logger.warn(`[observer] Ignored rejection from observer.${String(prop)}:`, err);
|
|
@@ -20452,6 +20484,17 @@ function makeSafeObserver(observer) {
|
|
|
20452
20484
|
return void 0;
|
|
20453
20485
|
}
|
|
20454
20486
|
};
|
|
20487
|
+
},
|
|
20488
|
+
has(_target, prop) {
|
|
20489
|
+
try {
|
|
20490
|
+
return Reflect.has(observer, prop);
|
|
20491
|
+
} catch {
|
|
20492
|
+
return false;
|
|
20493
|
+
}
|
|
20494
|
+
},
|
|
20495
|
+
// Keep `instanceof` transparent (legal on an extensible target).
|
|
20496
|
+
getPrototypeOf() {
|
|
20497
|
+
return Reflect.getPrototypeOf(observer);
|
|
20455
20498
|
}
|
|
20456
20499
|
});
|
|
20457
20500
|
}
|
|
@@ -21403,12 +21446,13 @@ var ResearchOrchestrationService = class {
|
|
|
21403
21446
|
const researchers = plan.researchers || [];
|
|
21404
21447
|
const active = /* @__PURE__ */ new Set();
|
|
21405
21448
|
const maxConcurrent = orchestratorOptions.config?.MAX_CONCURRENT_RESEARCHERS ?? DEFAULTS.MAX_CONCURRENT_RESEARCHERS;
|
|
21449
|
+
const maxFailedResearchers = orchestratorOptions.config?.MAX_FAILED_RESEARCHERS ?? DEFAULTS.MAX_FAILED_RESEARCHERS;
|
|
21406
21450
|
for (const configItem of researchers) {
|
|
21407
21451
|
if (signal?.aborted) break;
|
|
21408
|
-
if (shouldStopResearch(sessionId, researchId)) {
|
|
21452
|
+
if (shouldStopResearch(sessionId, researchId, maxFailedResearchers)) {
|
|
21409
21453
|
const sessionService = await getService(ServiceNames.RESEARCH_SESSION_SERVICE, ctx, container);
|
|
21410
21454
|
await sessionService.abortAllSessions(researchId);
|
|
21411
|
-
throw
|
|
21455
|
+
throw createResearchStopError(sessionId, researchId);
|
|
21412
21456
|
}
|
|
21413
21457
|
if (active.size >= maxConcurrent) {
|
|
21414
21458
|
if (signal?.aborted) break;
|
|
@@ -21477,11 +21521,10 @@ var ResearchOrchestrationService = class {
|
|
|
21477
21521
|
});
|
|
21478
21522
|
if (signal?.aborted) break;
|
|
21479
21523
|
}
|
|
21480
|
-
if (shouldStopResearch(sessionId, researchId)) {
|
|
21524
|
+
if (shouldStopResearch(sessionId, researchId, maxFailedResearchers)) {
|
|
21481
21525
|
const sessionService = await getService(ServiceNames.RESEARCH_SESSION_SERVICE, ctx, container);
|
|
21482
21526
|
await sessionService.abortAllSessions(researchId);
|
|
21483
|
-
|
|
21484
|
-
throw new Error(stopMessage);
|
|
21527
|
+
throw createResearchStopError(sessionId, researchId);
|
|
21485
21528
|
}
|
|
21486
21529
|
}
|
|
21487
21530
|
await Promise.all(active);
|
|
@@ -21878,18 +21921,26 @@ Research performed using ${modelId}`;
|
|
|
21878
21921
|
ensureCitedLinks(sessionId, synthesis) {
|
|
21879
21922
|
const reports = this.getSessionReports(sessionId);
|
|
21880
21923
|
let globalCitations = reports.size > 0 ? normalizeCitations(reports).globalCitations : [];
|
|
21881
|
-
if (
|
|
21924
|
+
if (reports.size > 0) {
|
|
21882
21925
|
const scraped = getScrapedLinks(sessionId);
|
|
21883
21926
|
if (scraped.length > 0) {
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
|
|
21892
|
-
|
|
21927
|
+
const known = new Set(globalCitations.map((c) => normalizeUrl(c.url)));
|
|
21928
|
+
const missing = scraped.filter((url) => !known.has(normalizeUrl(url)));
|
|
21929
|
+
if (missing.length > 0) {
|
|
21930
|
+
const startId = globalCitations.reduce((max, c) => Math.max(max, c.id), 0) + 1;
|
|
21931
|
+
globalCitations = [
|
|
21932
|
+
...globalCitations,
|
|
21933
|
+
...missing.map((url, i) => ({
|
|
21934
|
+
id: startId + i,
|
|
21935
|
+
url,
|
|
21936
|
+
description: "",
|
|
21937
|
+
source: isTranscribedLink(sessionId, url) ? "YouTube Transcript" : "Scrape"
|
|
21938
|
+
}))
|
|
21939
|
+
];
|
|
21940
|
+
logger.warn(
|
|
21941
|
+
`[ResearchSynthesisService] Folded in ${missing.length} source(s) from scrape/transcript provenance not covered by parsed citations.`
|
|
21942
|
+
);
|
|
21943
|
+
}
|
|
21893
21944
|
}
|
|
21894
21945
|
}
|
|
21895
21946
|
if (globalCitations.length > 0) {
|
|
@@ -22103,6 +22154,79 @@ init_metrics();
|
|
|
22103
22154
|
|
|
22104
22155
|
// src/utils/metrics-summary.ts
|
|
22105
22156
|
init_text_utils();
|
|
22157
|
+
function sumCounter(counters, baseName) {
|
|
22158
|
+
let total = 0;
|
|
22159
|
+
for (const [key, value] of Object.entries(counters)) {
|
|
22160
|
+
if (key === baseName || key.startsWith(baseName + "{")) {
|
|
22161
|
+
total += value;
|
|
22162
|
+
}
|
|
22163
|
+
}
|
|
22164
|
+
return total;
|
|
22165
|
+
}
|
|
22166
|
+
function getLabeledCounter(counters, baseName, labels) {
|
|
22167
|
+
const labelParts = Object.entries(labels).map(([k, v]) => `${k}="${v}"`);
|
|
22168
|
+
const suffix = `{${labelParts.join(",")}}`;
|
|
22169
|
+
for (const [key, value] of Object.entries(counters)) {
|
|
22170
|
+
if (key === `${baseName}${suffix}`) {
|
|
22171
|
+
return value;
|
|
22172
|
+
}
|
|
22173
|
+
}
|
|
22174
|
+
return 0;
|
|
22175
|
+
}
|
|
22176
|
+
function extractRunStats(snapshot) {
|
|
22177
|
+
const counters = snapshot.counters || {};
|
|
22178
|
+
const histograms = snapshot.histograms || {};
|
|
22179
|
+
const researchersLaunched = sumCounter(counters, "researchers_launched_total");
|
|
22180
|
+
const browserSearchQueries = sumCounter(counters, "browser_search_queries_total");
|
|
22181
|
+
const searchQueries = sumCounter(counters, "search_queries_total") + browserSearchQueries;
|
|
22182
|
+
const urlsDiscovered = sumCounter(counters, "browser_search_results_total") + sumCounter(counters, "browser_search_unique_urls_total");
|
|
22183
|
+
const fetchSuccess = getLabeledCounter(counters, "scrape_results_total", { outcome: "fetch_success" });
|
|
22184
|
+
const browserSuccess = getLabeledCounter(counters, "scrape_results_total", { outcome: "browser_success" });
|
|
22185
|
+
const totalFailure = getLabeledCounter(counters, "scrape_results_total", { outcome: "total_failure" });
|
|
22186
|
+
const browserFallbacks = sumCounter(counters, "scrape_layer_fallbacks_total");
|
|
22187
|
+
const urlsAnalyzed = fetchSuccess + browserSuccess;
|
|
22188
|
+
const urlsFailed = totalFailure;
|
|
22189
|
+
const toolUsage = {
|
|
22190
|
+
searches: sumCounter(counters, "tool_search_calls_total"),
|
|
22191
|
+
scrapes: sumCounter(counters, "tool_scrape_calls_total"),
|
|
22192
|
+
securitySearches: sumCounter(counters, "tool_security_search_calls_total"),
|
|
22193
|
+
stackexchangeQueries: sumCounter(counters, "stackexchange_requests_total"),
|
|
22194
|
+
knowledgeLookups: sumCounter(counters, "research_knowledge_search_total")
|
|
22195
|
+
};
|
|
22196
|
+
const errors = sumCounter(counters, "researcher_errors_total") + sumCounter(counters, "llm_api_errors_total");
|
|
22197
|
+
const tokens = sumCounter(counters, "llm_tokens_total");
|
|
22198
|
+
const cost = sumCounter(counters, "llm_cost_total");
|
|
22199
|
+
const durationMs = histograms['research_session_duration_ms{mode="deep",complexity="1",status="success"}']?.max || histograms['research_session_duration_ms{mode="deep",complexity="2",status="success"}']?.max || histograms['research_session_duration_ms{mode="deep",complexity="3",status="success"}']?.max || histograms['research_session_duration_ms{mode="quick",complexity="0",status="success"}']?.max || 0;
|
|
22200
|
+
const roundsCompleted = sumCounter(counters, "evaluator_runs_total");
|
|
22201
|
+
let complexity = 1;
|
|
22202
|
+
for (const key of Object.keys(counters)) {
|
|
22203
|
+
if (key.startsWith("research_sessions_total{")) {
|
|
22204
|
+
const match = key.match(/complexity="(\d+)"/);
|
|
22205
|
+
if (match?.[1]) complexity = parseInt(match[1], 10);
|
|
22206
|
+
break;
|
|
22207
|
+
}
|
|
22208
|
+
}
|
|
22209
|
+
if (researchersLaunched === 0 && searchQueries === 0 && urlsAnalyzed === 0 && tokens === 0) {
|
|
22210
|
+
return null;
|
|
22211
|
+
}
|
|
22212
|
+
return {
|
|
22213
|
+
durationMs,
|
|
22214
|
+
complexity,
|
|
22215
|
+
researchersLaunched,
|
|
22216
|
+
roundsCompleted,
|
|
22217
|
+
searchQueries,
|
|
22218
|
+
urlsDiscovered,
|
|
22219
|
+
fetchSuccess,
|
|
22220
|
+
browserSuccess,
|
|
22221
|
+
browserFallbacks,
|
|
22222
|
+
urlsAnalyzed,
|
|
22223
|
+
urlsFailed,
|
|
22224
|
+
errors,
|
|
22225
|
+
tokens,
|
|
22226
|
+
cost,
|
|
22227
|
+
toolUsage
|
|
22228
|
+
};
|
|
22229
|
+
}
|
|
22106
22230
|
|
|
22107
22231
|
// src/sdk.ts
|
|
22108
22232
|
init_error_tracker();
|
|
@@ -22124,6 +22248,7 @@ var _isRunning = false;
|
|
|
22124
22248
|
var _lastResearchId = null;
|
|
22125
22249
|
var _lastRunSummary = null;
|
|
22126
22250
|
var _lastErrorReport = null;
|
|
22251
|
+
var _lastResearcherOutcome = null;
|
|
22127
22252
|
var _onSignal = null;
|
|
22128
22253
|
var _sigintHandler = null;
|
|
22129
22254
|
var _sigtermHandler = null;
|
|
@@ -22310,6 +22435,7 @@ async function runDeepResearch(query, options = {}, signal) {
|
|
|
22310
22435
|
metrics.recordRunSummary(_lastRunSummary);
|
|
22311
22436
|
_lastErrorReport = runTracker.getReport();
|
|
22312
22437
|
logRunErrorSummary(_lastErrorReport, depthLabel, "success");
|
|
22438
|
+
_lastResearcherOutcome = await captureResearcherOutcome(sessionId, researchId, _lastRunSummary.snapshot);
|
|
22313
22439
|
return result;
|
|
22314
22440
|
} catch (err) {
|
|
22315
22441
|
const completedAt = Date.now();
|
|
@@ -22325,6 +22451,7 @@ async function runDeepResearch(query, options = {}, signal) {
|
|
|
22325
22451
|
metrics.recordRunSummary(_lastRunSummary);
|
|
22326
22452
|
_lastErrorReport = runTracker.getReport();
|
|
22327
22453
|
logRunErrorSummary(_lastErrorReport, depthLabel, "error");
|
|
22454
|
+
_lastResearcherOutcome = await captureResearcherOutcome(sessionId, researchId, _lastRunSummary.snapshot);
|
|
22328
22455
|
throw err;
|
|
22329
22456
|
} finally {
|
|
22330
22457
|
try {
|
|
@@ -22334,11 +22461,35 @@ async function runDeepResearch(query, options = {}, signal) {
|
|
|
22334
22461
|
_isRunning = false;
|
|
22335
22462
|
}
|
|
22336
22463
|
}
|
|
22464
|
+
function getLastErrorReport() {
|
|
22465
|
+
return _lastErrorReport;
|
|
22466
|
+
}
|
|
22467
|
+
function getLastResearcherOutcome() {
|
|
22468
|
+
return _lastResearcherOutcome;
|
|
22469
|
+
}
|
|
22337
22470
|
function logRunErrorSummary(report, depthLabel, status) {
|
|
22338
22471
|
if (!report || report.totalErrors === 0) return;
|
|
22339
22472
|
const top = report.patterns.slice(0, 3).map((p) => `"${String(p.message ?? p.signature ?? "error").slice(0, 48)}" \xD7${p.count}`).join(", ");
|
|
22340
22473
|
logger.warn(`[SDK] run ${status} with ${report.totalErrors} tracked error(s) across ${report.uniquePatterns} pattern(s) [${depthLabel}]${top ? ` \u2014 top: ${top}` : ""}`);
|
|
22341
22474
|
}
|
|
22475
|
+
async function captureResearcherOutcome(sessionId, researchId, snapshot) {
|
|
22476
|
+
try {
|
|
22477
|
+
const planningService = await getService(ServiceNames.PLANNING, void 0, globalContainer);
|
|
22478
|
+
const planned = planningService.getTotalResearchersPlanned(researchId);
|
|
22479
|
+
const launched = extractRunStats(snapshot)?.researchersLaunched ?? 0;
|
|
22480
|
+
const failed = getFailedResearchers(sessionId, researchId);
|
|
22481
|
+
return {
|
|
22482
|
+
planned,
|
|
22483
|
+
launched,
|
|
22484
|
+
succeeded: Math.max(0, launched - failed.length),
|
|
22485
|
+
failed: failed.length,
|
|
22486
|
+
failureReasons: getResearcherFailureReasons(sessionId, researchId)
|
|
22487
|
+
};
|
|
22488
|
+
} catch (err) {
|
|
22489
|
+
logger.debug("[SDK] Failed to capture researcher outcome (non-fatal):", err);
|
|
22490
|
+
return null;
|
|
22491
|
+
}
|
|
22492
|
+
}
|
|
22342
22493
|
async function searchKnowledge(queries, signal) {
|
|
22343
22494
|
if (!isInitialized || !globalContainer) {
|
|
22344
22495
|
throw new Error("SDK not initialized. Call initResearchSDK() first.");
|
|
@@ -22422,6 +22573,7 @@ async function _doShutdown() {
|
|
|
22422
22573
|
_lastResearchId = null;
|
|
22423
22574
|
_lastRunSummary = null;
|
|
22424
22575
|
_lastErrorReport = null;
|
|
22576
|
+
_lastResearcherOutcome = null;
|
|
22425
22577
|
resetConfig();
|
|
22426
22578
|
_removeSignalHandlers();
|
|
22427
22579
|
if (errors.length > 0) {
|
|
@@ -22927,6 +23079,8 @@ Error: ${vmsg}
|
|
|
22927
23079
|
`);
|
|
22928
23080
|
const observer = makeProgressObserver();
|
|
22929
23081
|
let exit = EXIT.OK;
|
|
23082
|
+
const abortController = new AbortController();
|
|
23083
|
+
activeResearchAbortController = abortController;
|
|
22930
23084
|
try {
|
|
22931
23085
|
await initResearchSDK({
|
|
22932
23086
|
model: runModel,
|
|
@@ -22943,7 +23097,7 @@ Error: ${vmsg}
|
|
|
22943
23097
|
observer,
|
|
22944
23098
|
...args.excludeTools ? { excludeTools: args.excludeTools } : {},
|
|
22945
23099
|
...args.initialLinks ? { initialLinks: args.initialLinks } : {}
|
|
22946
|
-
});
|
|
23100
|
+
}, abortController.signal);
|
|
22947
23101
|
let reportPath = null;
|
|
22948
23102
|
const exportCfg = getConfig(process.cwd(), "cli");
|
|
22949
23103
|
if (exportCfg.RESEARCH_REPORT_EXPORT_ENABLED) {
|
|
@@ -22955,13 +23109,25 @@ Error: ${vmsg}
|
|
|
22955
23109
|
}
|
|
22956
23110
|
}
|
|
22957
23111
|
if (args.json) {
|
|
22958
|
-
|
|
23112
|
+
const outcome = getLastResearcherOutcome();
|
|
23113
|
+
const errorReport = getLastErrorReport();
|
|
23114
|
+
toStdout(pretty({
|
|
23115
|
+
ok: true,
|
|
23116
|
+
depth,
|
|
23117
|
+
report,
|
|
23118
|
+
...reportPath ? { reportPath } : {},
|
|
23119
|
+
...outcome ? { researchers: { planned: outcome.planned, launched: outcome.launched, succeeded: outcome.succeeded, failed: outcome.failed } } : {},
|
|
23120
|
+
...errorReport && errorReport.totalErrors > 0 ? {
|
|
23121
|
+
trackedErrors: { total: errorReport.totalErrors, uniquePatterns: errorReport.uniquePatterns }
|
|
23122
|
+
} : {}
|
|
23123
|
+
}));
|
|
22959
23124
|
} else {
|
|
22960
23125
|
toStdout(report.endsWith("\n") ? report : report + "\n");
|
|
22961
23126
|
}
|
|
22962
23127
|
} catch (err) {
|
|
22963
23128
|
exit = reportError(err, "research", args.json);
|
|
22964
23129
|
} finally {
|
|
23130
|
+
if (activeResearchAbortController === abortController) activeResearchAbortController = null;
|
|
22965
23131
|
await safeShutdown();
|
|
22966
23132
|
}
|
|
22967
23133
|
return exit;
|
|
@@ -23238,7 +23404,8 @@ function describeUninstall(r) {
|
|
|
23238
23404
|
function reportError(err, what, json) {
|
|
23239
23405
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
23240
23406
|
const lower = msg2.toLowerCase();
|
|
23241
|
-
const
|
|
23407
|
+
const isTaggedResearchStop = err?.code === RESEARCH_STOPPED_ERROR_CODE;
|
|
23408
|
+
const isConfigError = !isTaggedResearchStop && (lower.includes("no llm model available") || lower.includes("not found in pi") || lower.includes("no api key") || lower.includes("provider must be specified") || lower.includes("invalid model string") || lower.includes("authentication") || lower.includes("unauthorized") || lower.includes("api key"));
|
|
23242
23409
|
const isRateLimit = lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests");
|
|
23243
23410
|
const exitCode = isConfigError && !isRateLimit ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23244
23411
|
if (json) {
|
|
@@ -23546,6 +23713,7 @@ var flushAndExit = (code) => {
|
|
|
23546
23713
|
});
|
|
23547
23714
|
});
|
|
23548
23715
|
};
|
|
23716
|
+
var activeResearchAbortController = null;
|
|
23549
23717
|
async function main(argv) {
|
|
23550
23718
|
let _signalCleanupDone = false;
|
|
23551
23719
|
const onSignal = (sig) => {
|
|
@@ -23554,6 +23722,7 @@ async function main(argv) {
|
|
|
23554
23722
|
toStderr(`
|
|
23555
23723
|
[pi-research] ${sig} \u2014 cleaning up\u2026
|
|
23556
23724
|
`);
|
|
23725
|
+
activeResearchAbortController?.abort();
|
|
23557
23726
|
void safeShutdown().finally(() => flushAndExit(EXIT.SOFTWARE));
|
|
23558
23727
|
};
|
|
23559
23728
|
process.once("SIGINT", () => onSignal("SIGINT"));
|
|
@@ -23623,5 +23792,6 @@ export {
|
|
|
23623
23792
|
EXIT,
|
|
23624
23793
|
UsageError,
|
|
23625
23794
|
bridgeConfigEnv,
|
|
23626
|
-
parseArgs
|
|
23795
|
+
parseArgs,
|
|
23796
|
+
reportError
|
|
23627
23797
|
};
|
|
@@ -85,6 +85,7 @@ The coordinator will plan as many researchers as needed (up to the max). You do
|
|
|
85
85
|
|
|
86
86
|
**Multiple Topics:**
|
|
87
87
|
- If you need to research multiple unrelated topics of **truly distinct scope**, you may emit multiple `research` calls **simultaneously in a single turn**. Each call gets its own isolated research run with its own coordinator and researchers.
|
|
88
|
+
- **Maximum 4 simultaneous calls** — Do NOT make more than 4 `research` calls in a single turn, even if you have more unrelated topics. Exceeding 4 simultaneous research runs can overload the system and degrade performance. If you have more than 4 truly distinct topics, they can likely be consolidated into fewer calls (group related subtopics, use broader queries, or batch across multiple turns).
|
|
88
89
|
- Example: `research("bananas")` and `research("quantum computing")` in the same turn — these have zero overlap.
|
|
89
90
|
- **Group related topics**: If topics share ANY scope or could inform each other, combine them into a single `research` call. The internal coordinator will decompose them into parallel researcher agents. Do NOT split related sub-topics into separate calls.
|
|
90
91
|
- Example: `research("Python async performance and Rust async performance comparison")` — NOT separate calls for Python and Rust, since the comparison IS the research goal.
|
package/dist/thread-worker.mjs
CHANGED
|
@@ -2219,6 +2219,8 @@ var ConfigSchema = Type.Object({
|
|
|
2219
2219
|
RESEARCHER_MAX_RETRIES: Type.Number({ minimum: 0, maximum: 5, default: 2 }),
|
|
2220
2220
|
/** Base delay between retries in milliseconds (default: 2000, range: 100-10000) */
|
|
2221
2221
|
RESEARCHER_MAX_RETRY_DELAY_MS: Type.Number({ minimum: 100, maximum: 1e4, default: 2e3 }),
|
|
2222
|
+
/** Unique researcher failures that abort the entire run (default: 2, range: 1-10) */
|
|
2223
|
+
MAX_FAILED_RESEARCHERS: Type.Number({ minimum: 1, maximum: 10, default: 2 }),
|
|
2222
2224
|
/** Target depth for recursive research (default: 1, range: 1-3) */
|
|
2223
2225
|
DEFAULT_RESEARCH_DEPTH: Type.Number({ minimum: 1, maximum: 3, default: 1 }),
|
|
2224
2226
|
/** Number of batches to allow for a single scrape tool call (default: 2, 0=unlimited) */
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -25,8 +25,8 @@ pi CLI
|
|
|
25
25
|
in-session, the calling agent picks the depth (1–3) from the user's wording and the
|
|
26
26
|
task's complexity, guided by the tool's usage prompt (`src/prompts/research-tool-usage.md`).
|
|
27
27
|
CLI and SDK callers pass the depth explicitly.
|
|
28
|
-
2. Depth 0 takes the quick path; depth 1–3 takes the deep path (below). The
|
|
29
|
-
|
|
28
|
+
2. Depth 0 takes the quick path; depth 1–3 takes the deep path (below). The pi extension's
|
|
29
|
+
tool and the TUI are restricted to levels 1–3; the CLI, SDK, and agent skill can pass 0.
|
|
30
30
|
3. On the deep path the coordinator plans the research tracks and runs one initial
|
|
31
31
|
search burst, then hands each researcher a set of result URLs to start from.
|
|
32
32
|
4. Researchers scrape and read those pages through the scrape tooling and return cited
|
|
@@ -84,10 +84,23 @@ Two conventions apply:
|
|
|
84
84
|
report. A mid-round evaluation that cannot be parsed continues the existing agenda
|
|
85
85
|
rather than finalizing early, so a parse failure never truncates a run.
|
|
86
86
|
|
|
87
|
-
###
|
|
87
|
+
### Tool inventory
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
This is the canonical list of every tool the system exposes, on both surfaces.
|
|
90
|
+
|
|
91
|
+
**Host-facing tools** — registered with the pi session (`src/index.ts`) for the calling
|
|
92
|
+
agent to invoke:
|
|
93
|
+
|
|
94
|
+
| Tool | Purpose |
|
|
95
|
+
|------|---------|
|
|
96
|
+
| `research` | Run a full multi-source research session and return the cited Markdown report |
|
|
97
|
+
| `research_knowledge_search` | Instant local search of the knowledge store — checked before live research; always registered, reports why when the store is disabled |
|
|
98
|
+
| `health` | Verify system status (browser pool, knowledge store, GPU lock); optional liveness probe |
|
|
99
|
+
|
|
100
|
+
**Researcher-agent tools** — the fixed set each researcher sub-agent works with
|
|
101
|
+
(`src/tools/index.ts`). `search`, `security_search`, `stackexchange`, and
|
|
102
|
+
`youtube_transcript` share a budget of 12 gathering calls per phase
|
|
103
|
+
(`MAX_GATHERING_CALLS`); `scrape` and local `grep` have their own budgets:
|
|
91
104
|
|
|
92
105
|
| Tool | Quick | Deep | Backend |
|
|
93
106
|
|------|-------|------|---------|
|
|
@@ -102,7 +115,9 @@ Each researcher has a fixed tool set and a shared budget of 12 gathering calls p
|
|
|
102
115
|
In deep research `search` is excluded — the coordinator runs the search burst and hands
|
|
103
116
|
out URLs directly. In quick research `grep` is excluded — the single session is not
|
|
104
117
|
expected to traverse a local codebase. Researchers cannot write files, run shell
|
|
105
|
-
commands, or reach the network outside these tools.
|
|
118
|
+
commands, or reach the network outside these tools. Individual researcher tools can be
|
|
119
|
+
disabled per run with `PI_RESEARCH_DISABLED_TOOLS`
|
|
120
|
+
(see [CONFIGURATION.md](CONFIGURATION.md)).
|
|
106
121
|
|
|
107
122
|
### Browser infrastructure
|
|
108
123
|
|
|
@@ -154,7 +169,8 @@ WriterQueue (async, non-blocking)
|
|
|
154
169
|
Key files: `src/knowledge/store.ts` (LanceDB operations), `embedder.ts` (model load +
|
|
155
170
|
batched inference), `writer-queue.ts` (async writes + content-hash dedup), `chunker.ts`
|
|
156
171
|
(chunking), `webgpu-viability.ts` (out-of-process GPU probe + cached verdict),
|
|
157
|
-
`migration.ts` (
|
|
172
|
+
`migration.ts` (migration strategy types — the drop / backup / re-embed logic
|
|
173
|
+
itself lives in `store.ts`).
|
|
158
174
|
|
|
159
175
|
The store needs native ONNX-runtime and LanceDB bindings. On platforms with no prebuilt
|
|
160
176
|
binary — notably Intel macOS (`darwin-x64`) — it is absent: the health check reports it
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.8] - 2024-07-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **CHANGELOG** — Added comprehensive CHANGELOG.md documenting all versions from 0.1.13 through 1.0.7
|
|
12
|
+
- **System prompt constraint** — Added "Maximum 4 simultaneous calls" constraint to research-tool-usage prompt to prevent system overload
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Topic consolidation guidance** — Emphasized that topics can likely be consolidated to fit the 4-call maximum, with strategies for grouping related subtopics
|
|
16
|
+
- **Package structure** — CHANGELOG.md now included in published package (docs/CHANGELOG.md)
|
|
17
|
+
|
|
18
|
+
## [1.0.7] - 2024-07-14
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Browser-pool leader-election race** - Fixed a critical bug where a single client's transient socket error could force-clear shared leader state, causing a live, healthy leader to self-destruct via the leadership-miss watchdog. The fix reserves the "unreachable, force clear" path for cases where liveness probes actually confirm the registered leader PID or port is gone.
|
|
22
|
+
- **Researcher retry-exemption** - Fixed the "no initial search results or historical links" failure path that bypassed the retry loop entirely. This now routes through the same `RESEARCHER_MAX_RETRIES`/`RESEARCHER_MAX_RETRY_DELAY_MS` retry loop as other failures.
|
|
23
|
+
- **Exit-code misclassification** - Fixed false-positive classification of "Research stopped" errors as config errors (exit 78) via substring matching on the boilerplate "API key" advice text. Error classification now properly distinguishes infrastructure failures from credential problems.
|
|
24
|
+
- **Unsurfaced quality signals** - Added per-run error tracking and per-researcher grounding signals to the log output for better debugging of report quality variance.
|
|
25
|
+
- **SIGINT/SIGTERM cancellation wiring** - Improved cancellation handling for graceful shutdown on interrupt signals.
|
|
26
|
+
- **Multi-researcher citation-provenance folding** - Fixed citation provenance tracking when multiple researchers contribute to a report.
|
|
27
|
+
- **Untitled YouTube video citation line** - Fixed citation formatting for YouTube videos without titles.
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
- Several hardening improvements from the adversarial-review audit:
|
|
31
|
+
- Migration abort safety gates
|
|
32
|
+
- Hollow-run detection gates
|
|
33
|
+
- Streaming caps for large responses
|
|
34
|
+
- Socket drain on Content-Length pre-screen throws
|
|
35
|
+
- Observer isolation improvements
|
|
36
|
+
|
|
37
|
+
## [1.0.6] - 2024-07-01
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **Observer isolation** - Fixed `makeSafeObserver` proxying the observer directly, which could cause state leakage.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
- Corrected depth-0 availability documentation
|
|
44
|
+
- Fixed migration.ts attribution in architecture docs
|
|
45
|
+
|
|
46
|
+
## [1.0.5] - 2024-06-28
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- **Content-Length pre-screen throws** - Added proper body draining to prevent resource leaks
|
|
50
|
+
- **TTL refresh for transcribed links** - Fixed TTL handling for YouTube transcripts
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- **YouTube transcript provenance** - Fixed transcript provenance tracking in final report citations
|
|
54
|
+
|
|
55
|
+
## [1.0.4] - 2024-06-25
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- **Credential detection** - Detect pi keys by pi's own semantics, not file presence; registry no longer gated on auth.json
|
|
59
|
+
- **Configuration clarity** - Updated docs to reflect that pi has configuration files, not a named 'auth' feature
|
|
60
|
+
|
|
61
|
+
## [1.0.3] - 2024-06-20
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
- **Model resolution** - Require a configured model on the standalone CLI/skill; align SDK session model with RESEARCH_MODEL
|
|
65
|
+
- **Configuration requirement** - Added proper error messages when model is not configured
|
|
66
|
+
|
|
67
|
+
### Documentation
|
|
68
|
+
- Updated installation and configuration documentation
|
|
69
|
+
|
|
70
|
+
## [1.0.2] - 2024-06-15
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
- **Timeout robustness** - Improved timeout handling for long-running operations
|
|
74
|
+
- De-emphasized the markdown-conversion fallback in favor of native HTML-to-markdown conversion
|
|
75
|
+
|
|
76
|
+
## [1.0.1] - 2024-06-10
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
- Initial stable release after rebuild against current pi APIs
|
|
80
|
+
- Stealth-browser stack integration
|
|
81
|
+
|
|
82
|
+
## [0.1.13] - 2024-04-15
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
- Initial research functionality
|
|
86
|
+
- Knowledge store integration
|
|
87
|
+
- Multi-agent orchestration
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Deferred Findings (from v1.0.7 audit)
|
|
92
|
+
|
|
93
|
+
The following issues were identified during the v1.0.7 audit but intentionally deferred to future releases:
|
|
94
|
+
|
|
95
|
+
1. **KnowledgeStore.close() timeout behavior** - The close() method proceeds even when the pending-write wait times out. A correct fix requires threading cancellation tokens through write paths or documenting the 10s timeout as intended behavior.
|
|
96
|
+
|
|
97
|
+
2. **Cross-session teardown via shared singletons** - Session shutdown handlers run process-wide cleanup for reload/new/fork events. The correct behavior depends on pi's session model semantics and needs further investigation.
|
|
98
|
+
|
|
99
|
+
3. **Lower-severity hardening items** - Various low-confidence or low-impact findings noted for future investigation:
|
|
100
|
+
- SIGINT-adjacent cancellation race
|
|
101
|
+
- Knowledge-store migration vs. concurrent access
|
|
102
|
+
- Global activity gate reused for per-session decisions
|
|
103
|
+
- Zombie queued browser tasks after timeout
|
|
104
|
+
- Non-atomic `clearBrowserServer`
|
|
105
|
+
- Inconsistent `knowledge --json` payload shapes
|
|
106
|
+
- Backward-clock-step tolerance across multiple rate limiters
|
|
107
|
+
|
|
108
|
+
See `DEFERRED-FINDINGS-2026-07-14.md` for full details.
|