@lincoln504/pi-research 1.0.8 → 1.0.9

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 CHANGED
@@ -2485,7 +2485,7 @@ function createConfig(env, processEnv) {
2485
2485
  SEARCH_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_SEARCH_TIMEOUT_MS", DEFAULTS.SEARCH_TIMEOUT_MS, 5e3, 12e4),
2486
2486
  TUI_REFRESH_DEBOUNCE_MS: parseEnvNumber(e, "PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS", DEFAULTS.TUI_REFRESH_DEBOUNCE_MS, 0, 1e3),
2487
2487
  BROWSER_TASK_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS", DEFAULTS.BROWSER_TASK_TIMEOUT_MS, 2e3, 12e4),
2488
- LLM_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_LLM_TIMEOUT_MS", DEFAULTS.LLM_TIMEOUT_MS, 6e4, 6e5),
2488
+ LLM_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_LLM_TIMEOUT_MS", DEFAULTS.LLM_TIMEOUT_MS, 6e4, 18e5),
2489
2489
  LLM_THINKING_LEVEL: parseEnvEnum(e, "PI_RESEARCH_LLM_THINKING_LEVEL", ["off", "minimal", "low", "medium", "high"], DEFAULTS.LLM_THINKING_LEVEL),
2490
2490
  PLANNING_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_PLANNING_MAX_TOKENS", DEFAULTS.PLANNING_MAX_TOKENS, 1024, 131072),
2491
2491
  SYNTHESIS_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_SYNTHESIS_MAX_TOKENS", DEFAULTS.SYNTHESIS_MAX_TOKENS, 1024, 131072),
@@ -2641,9 +2641,13 @@ var init_config = __esm({
2641
2641
  * never killed before it has used its full nav budget plus time waiting in the queue.
2642
2642
  * (default: 10000) */
2643
2643
  BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
2644
- /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-600s).
2645
- * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var. */
2646
- LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 6e5, default: 3e5 }),
2644
+ /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
2645
+ * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
2646
+ * Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
2647
+ * large-context synthesis/plan call can legitimately run >10min, and the old
2648
+ * 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
2649
+ * (e.g. 900000), causing those calls to time out mid-generation. */
2650
+ LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
2647
2651
  /** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
2648
2652
  * evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
2649
2653
  * sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
@@ -3201,7 +3205,8 @@ var init_agentic_repair = __esm({
3201
3205
  });
3202
3206
 
3203
3207
  // src/core/llm/model-registry-factory.ts
3204
- import { AuthStorage, ModelRegistry, getAgentDir } from "@earendil-works/pi-coding-agent";
3208
+ import { ModelRegistry, ModelRuntime, getAgentDir } from "@earendil-works/pi-coding-agent";
3209
+ import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
3205
3210
  import * as path6 from "node:path";
3206
3211
  import * as fs5 from "node:fs";
3207
3212
  function safeGetAll(registry) {
@@ -3243,27 +3248,27 @@ function buildHeaders(provider) {
3243
3248
  if (provider === "openrouter") return { "HTTP-Referer": "https://pi.ai", "X-Title": "pi-research" };
3244
3249
  return {};
3245
3250
  }
3246
- function buildModelRegistry(apiKey, provider) {
3251
+ async function buildModelRegistry(apiKey, provider) {
3247
3252
  const agentDir = getAgentDir();
3248
3253
  const modelsJsonPath = path6.join(agentDir, "models.json");
3249
3254
  const authPath = path6.join(agentDir, "auth.json");
3255
+ const hasModels = fs5.existsSync(modelsJsonPath);
3256
+ const hasAuth = fs5.existsSync(authPath);
3257
+ const runtime = await ModelRuntime.create({
3258
+ authPath: hasAuth ? authPath : void 0,
3259
+ credentials: hasAuth ? void 0 : new InMemoryCredentialStore(),
3260
+ modelsPath: hasModels ? modelsJsonPath : null,
3261
+ allowModelNetwork: false
3262
+ });
3250
3263
  if (apiKey && provider) {
3251
- const authStorage = AuthStorage.inMemory({
3252
- [provider]: { type: "api_key", key: apiKey }
3253
- });
3254
- return ModelRegistry.create(
3255
- authStorage,
3256
- fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
3257
- );
3258
- }
3259
- if (fs5.existsSync(authPath) || fs5.existsSync(modelsJsonPath)) {
3260
- const authStorage = AuthStorage.create(authPath);
3261
- return ModelRegistry.create(
3262
- authStorage,
3263
- fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
3264
- );
3264
+ await runtime.setRuntimeApiKey(provider, apiKey, { allowNetwork: false });
3265
3265
  }
3266
- return ModelRegistry.inMemory(AuthStorage.inMemory());
3266
+ const registry = new ModelRegistry(runtime);
3267
+ registryRuntimes.set(registry, runtime);
3268
+ return registry;
3269
+ }
3270
+ function getRuntimeForRegistry(registry) {
3271
+ return registry instanceof Object ? registryRuntimes.get(registry) : void 0;
3267
3272
  }
3268
3273
  function constructMinimalModel(provider, modelId, _apiKey) {
3269
3274
  return {
@@ -3355,10 +3360,12 @@ function pickPreferredAvailable(available, providerOrder) {
3355
3360
  }
3356
3361
  return available[0];
3357
3362
  }
3363
+ var registryRuntimes;
3358
3364
  var init_model_registry_factory = __esm({
3359
3365
  "src/core/llm/model-registry-factory.ts"() {
3360
3366
  "use strict";
3361
3367
  init_logger();
3368
+ registryRuntimes = /* @__PURE__ */ new WeakMap();
3362
3369
  }
3363
3370
  });
3364
3371
 
@@ -14770,7 +14777,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
14770
14777
  metrics.increment("browser_search_orchestrations_total", 1);
14771
14778
  logger.log(`[Search] Orchestrating ${queries.length} queries across ${maxWorkers} worker processes...`);
14772
14779
  if (onProgress) onProgress(0);
14773
- const QUERY_TIMEOUT_MS = config?.SEARCH_TIMEOUT_MS ?? 45e3;
14780
+ const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45e3) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 1e4);
14774
14781
  const filteredQueries = queries.filter((q) => q.trim());
14775
14782
  let timeoutCount = 0;
14776
14783
  let errorCount = 0;
@@ -14815,7 +14822,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
14815
14822
  metrics.observe("browser_search_query_duration_ms", queryDuration, { status });
14816
14823
  metrics.increment("browser_search_queries_total", 1, { status });
14817
14824
  if (isTimeout) {
14818
- logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms (likely blocked or slow startup): "${query}"`);
14825
+ logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms \u2014 likely starved waiting for a free browser worker under concurrent load, or a slow/blocked search provider: "${query}"`);
14819
14826
  } else {
14820
14827
  const msg2 = error instanceof Error ? error.message : String(error);
14821
14828
  if (msg2 !== "Aborted") {
@@ -19635,40 +19642,9 @@ function createResearchTools(options) {
19635
19642
  }
19636
19643
 
19637
19644
  // src/utils/make-resource-loader.ts
19645
+ import { createExtensionRuntime } from "@earendil-works/pi-coding-agent";
19638
19646
  function makeResourceLoader(systemPromptText) {
19639
- const mockRuntime = {
19640
- flagValues: /* @__PURE__ */ new Map(),
19641
- pendingProviderRegistrations: [],
19642
- registerProvider: () => {
19643
- },
19644
- unregisterProvider: () => {
19645
- },
19646
- sendMessage: async () => {
19647
- },
19648
- sendUserMessage: async () => {
19649
- },
19650
- appendEntry: async () => {
19651
- },
19652
- setSessionName: () => void 0,
19653
- getSessionName: () => void 0,
19654
- setLabel: async () => {
19655
- },
19656
- getActiveTools: () => [],
19657
- getAllTools: () => [],
19658
- setActiveTools: () => {
19659
- },
19660
- refreshTools: () => {
19661
- },
19662
- getCommands: () => [],
19663
- setModel: async () => true,
19664
- getThinkingLevel: () => "off",
19665
- setThinkingLevel: () => {
19666
- },
19667
- assertActive: () => {
19668
- },
19669
- invalidate: () => {
19670
- }
19671
- };
19647
+ const mockRuntime = createExtensionRuntime();
19672
19648
  return {
19673
19649
  getExtensions: () => ({ extensions: [], errors: [], runtime: mockRuntime }),
19674
19650
  getSkills: () => ({ skills: [], diagnostics: [] }),
@@ -19822,6 +19798,7 @@ function createDefaultToolLimits(config) {
19822
19798
  // src/orchestration/researcher.ts
19823
19799
  init_logger();
19824
19800
  init_research_model_resolver();
19801
+ init_model_registry_factory();
19825
19802
  async function createResearcherSession(options) {
19826
19803
  const {
19827
19804
  cwd,
@@ -19889,7 +19866,17 @@ async function createResearcherSession(options) {
19889
19866
  // Each researcher gets its own isolated session
19890
19867
  settingsManager: researcherSettings,
19891
19868
  model: modelToUse,
19892
- modelRegistry,
19869
+ // pi 0.80.8 replaced the SDK's `modelRegistry`/`authStorage` session options
19870
+ // with the async `modelRuntime`. Pass the runtime backing OUR registry when
19871
+ // this run built one (standalone CLI/SDK): it carries any explicit API key
19872
+ // seeded via setRuntimeApiKey, which the session's own default runtime
19873
+ // (auto-built from ~/.pi/agent/{auth,models}.json) would never see — without
19874
+ // this, standalone PI_RESEARCH_API_KEY users fail auth on every researcher
19875
+ // LLM call despite passing pre-flight. For registries we did NOT build
19876
+ // (the pi host's ExtensionContext.modelRegistry), this resolves to
19877
+ // undefined and the host auto-builds from its own agent dir — correct
19878
+ // in-host, where auth lives in the host's config.
19879
+ modelRuntime: getRuntimeForRegistry(modelRegistry),
19893
19880
  resourceLoader: makeResourceLoader(systemPrompt),
19894
19881
  // Thinking/reasoning is disabled for researcher agents: they do retrieval and
19895
19882
  // synthesis from scraped pages, not open-ended reasoning. Keeping it off reduces
@@ -22332,7 +22319,7 @@ async function _doInit(options = {}) {
22332
22319
  if (globalApiKey && !parsedProvider) {
22333
22320
  throw new Error("Provider must be specified when using an explicit API key (set provider option or PI_RESEARCH_PROVIDER).");
22334
22321
  }
22335
- globalRegistry = buildModelRegistry(globalApiKey, parsedProvider);
22322
+ globalRegistry = await buildModelRegistry(globalApiKey, parsedProvider);
22336
22323
  try {
22337
22324
  if (effectiveModel && typeof effectiveModel === "object" && effectiveModel.id) {
22338
22325
  globalModel = effectiveModel;
@@ -22940,16 +22927,16 @@ function fileExists(p) {
22940
22927
  return false;
22941
22928
  }
22942
22929
  }
22943
- function piKeysAvailable(piAuthPresent, provider) {
22930
+ async function piKeysAvailable(piAuthPresent, provider) {
22944
22931
  try {
22945
- const available = safeGetAvailable(buildModelRegistry(void 0, void 0));
22932
+ const available = safeGetAvailable(await buildModelRegistry(void 0, void 0));
22946
22933
  if (!provider) return available.length > 0;
22947
22934
  return available.some((m) => m.provider === provider);
22948
22935
  } catch {
22949
22936
  return piAuthPresent;
22950
22937
  }
22951
22938
  }
22952
- function detectCredentials(explicitModel) {
22939
+ async function detectCredentials(explicitModel) {
22953
22940
  const paths = resolvedConfigPaths();
22954
22941
  const piAuthPresent = fileExists(paths.piAuth);
22955
22942
  const piModelsPresent = fileExists(paths.piModels);
@@ -22960,7 +22947,7 @@ function detectCredentials(explicitModel) {
22960
22947
  const model = explicitModel ?? modelEnv ?? modelConfig;
22961
22948
  const modelFrom = explicitModel ? "--model" : modelEnv ? "PI_RESEARCH_MODEL" : modelConfig ? "config.env" : "unset";
22962
22949
  const provider = providerEnv ?? (model && model.includes("/") ? model.slice(0, model.indexOf("/")) : void 0);
22963
- const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
22950
+ const piKeysUsable = await piKeysAvailable(piAuthPresent, provider);
22964
22951
  if (apiKey) {
22965
22952
  if (!provider) {
22966
22953
  return {
@@ -23052,7 +23039,7 @@ function configBlock(det, extraNote) {
23052
23039
  return lines.join("\n");
23053
23040
  }
23054
23041
  async function cmdResearch(args) {
23055
- const det = detectCredentials(args.model);
23042
+ const det = await detectCredentials(args.model);
23056
23043
  if (det.problem) {
23057
23044
  if (args.json) toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
23058
23045
  else toStderr(`
@@ -23125,7 +23112,7 @@ Error: ${vmsg}
23125
23112
  toStdout(report.endsWith("\n") ? report : report + "\n");
23126
23113
  }
23127
23114
  } catch (err) {
23128
- exit = reportError(err, "research", args.json);
23115
+ exit = await reportError(err, "research", args.json);
23129
23116
  } finally {
23130
23117
  if (activeResearchAbortController === abortController) activeResearchAbortController = null;
23131
23118
  await safeShutdown();
@@ -23133,7 +23120,7 @@ Error: ${vmsg}
23133
23120
  return exit;
23134
23121
  }
23135
23122
  async function cmdKnowledge(queries, json) {
23136
- const det = detectCredentials();
23123
+ const det = await detectCredentials();
23137
23124
  if (det.problem) {
23138
23125
  if (json) {
23139
23126
  toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
@@ -23187,7 +23174,7 @@ Configure in:
23187
23174
  return exit;
23188
23175
  }
23189
23176
  async function cmdStatus(json) {
23190
- const det = detectCredentials();
23177
+ const det = await detectCredentials();
23191
23178
  const paths = resolvedConfigPaths();
23192
23179
  const cfg = getConfig(process.cwd(), "cli");
23193
23180
  const summary = {
@@ -23401,7 +23388,7 @@ function describeUninstall(r) {
23401
23388
  return r.status;
23402
23389
  }
23403
23390
  }
23404
- function reportError(err, what, json) {
23391
+ async function reportError(err, what, json) {
23405
23392
  const msg2 = err instanceof Error ? err.message : String(err);
23406
23393
  const lower = msg2.toLowerCase();
23407
23394
  const isTaggedResearchStop = err?.code === RESEARCH_STOPPED_ERROR_CODE;
@@ -23427,7 +23414,7 @@ Error: pi-research ${what} halted: provider rate limit reached. Wait a moment an
23427
23414
  toStderr(`
23428
23415
  Error: pi-research ${what} failed: ${msg2}
23429
23416
 
23430
- ${configBlock(detectCredentials())}
23417
+ ${configBlock(await detectCredentials())}
23431
23418
  `);
23432
23419
  return exitCode;
23433
23420
  }
@@ -2278,9 +2278,13 @@ var ConfigSchema = Type.Object({
2278
2278
  * never killed before it has used its full nav budget plus time waiting in the queue.
2279
2279
  * (default: 10000) */
2280
2280
  BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
2281
- /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-600s).
2282
- * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var. */
2283
- LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 6e5, default: 3e5 }),
2281
+ /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
2282
+ * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
2283
+ * Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
2284
+ * large-context synthesis/plan call can legitimately run >10min, and the old
2285
+ * 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
2286
+ * (e.g. 900000), causing those calls to time out mid-generation. */
2287
+ LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
2284
2288
  /** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
2285
2289
  * evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
2286
2290
  * sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
package/docs/CHANGELOG.md CHANGED
@@ -5,7 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.0.8] - 2024-07-17
8
+ ## [1.0.9] - 2026-07-28
9
+
10
+ ### Fixed
11
+ - **pi ≥ 0.81.0 researcher crash** — every researcher session died with `pendingNativeProviderRegistrations is not iterable` under pi 0.81.0+ ("no source material was collected"). The hand-maintained `ExtensionRuntime` mock in the resource loader was frozen at a pre-0.81 contract; it is replaced with pi's own `createExtensionRuntime()`, which always satisfies the running host's contract and cannot drift again.
12
+ - **pi 0.80.8+ ModelRuntime migration** — `buildModelRegistry` migrated off the removed `AuthStorage`/`ModelRegistry.create()` APIs to `ModelRuntime.create()` + the synchronous `ModelRegistry` facade. The `@earendil-works/*` dependency floor and the in-host version self-check are both raised to 0.80.8 to match.
13
+ - **Explicit-API-key auth now reaches researcher LLM calls** — researcher sessions are created with the `modelRuntime` backing our own registry (pi 0.80.8 removed the old `modelRegistry` session option). Without this, standalone `PI_RESEARCH_API_KEY` users with no `~/.pi/agent/auth.json` passed pre-flight and then failed auth on every researcher call.
14
+ - **Explicit-API-key pre-flight hang** — seeding a runtime API key triggered a model-catalog refresh whose default options open live network connections with no timeout; in network-restricted environments the CLI pre-flight hung indefinitely. The refresh is now pinned offline (`allowNetwork: false`).
15
+ - **No filesystem side effects on read-only commands** — the migrated runtime's default file-backed credential store created `~/.pi/agent/auth.json` at construction, so every CLI invocation (including `status`) silently created a pi config dir on machines without pi and hard-failed where `$HOME` is read-only. An in-memory credential store is used when no `auth.json` exists; env-var provider keys are unaffected.
16
+ - **Search-timeout cascade under concurrent load** — the per-query hard cap in browser search started at enqueue with only the nav budget, so a query stuck in a saturated worker queue burned its whole budget waiting and aborted with zero work ("Query timed out … likely blocked or slow startup"). The cap now includes the queue-wait margin, matching the scheduler's own ceiling.
17
+ - **`PI_RESEARCH_LLM_TIMEOUT_MS` ceiling raised 10 → 30 min** — an explicit higher value was silently clamped to 600000, timing out large-context synthesis calls mid-generation.
18
+ - **CHANGELOG dates corrected** — release dates in this file wrongly said 2024; corrected to the actual git release dates (2026).
19
+
20
+ ### Security
21
+ - **protobufjs override 7.6.4 → 7.6.5** — clears GHSA-j3f2-48v5-ccww (DoS) and its whole moderate advisory chain through `@google/genai`/pi-ai.
22
+ - **adm-zip forced to 0.6.0 via override** — clears GHSA-xcpc-8h2w-3j85 (crafted-ZIP 4 GB allocation) in the camoufox-js subtree; extraction APIs camoufox uses verified compatible.
23
+ - **Audit gate hardened with a reviewed exception list** — CI and the release publish gate now run `scripts/audit-gate.cjs`: any moderate+ advisory in the shipped tree fails the build unless explicitly allowlisted in `config/tooling/audit-exceptions.json` with an upstream-blocked justification, a clears-when condition, and a review date. Current exceptions: `brace-expansion` (frozen inside pi-coding-agent's npm-shrinkwrap; fix released upstream) and `sharp` (unused image pipeline of the embeddings-only transformers dependency).
24
+
25
+ ### Changed
26
+ - **Dependencies**: pi ecosystem lockfile refreshed to 0.82.1 (matching the current pi host); `pdf-oxide-wasm` 0.3.77; `undici` ^8.9; devDependencies refreshed (eslint 10.8, typescript-eslint 8.65, vitest 4.1.10, dependency-cruiser 17.4.3). Deliberate pins unchanged (typebox 1.1.38, playwright-core 1.60.0, camoufox-js 0.10.x, lancedb 0.29, apache-arrow 21.1.0, transformers 4.2, impit 0.13).
27
+ - **Docs**: `PI_RESEARCH_LLM_TIMEOUT_MS` range updated in CONFIGURATION.md.
28
+
29
+ ## [1.0.8] - 2026-07-17
9
30
 
10
31
  ### Added
11
32
  - **CHANGELOG** — Added comprehensive CHANGELOG.md documenting all versions from 0.1.13 through 1.0.7
@@ -15,7 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
36
  - **Topic consolidation guidance** — Emphasized that topics can likely be consolidated to fit the 4-call maximum, with strategies for grouping related subtopics
16
37
  - **Package structure** — CHANGELOG.md now included in published package (docs/CHANGELOG.md)
17
38
 
18
- ## [1.0.7] - 2024-07-14
39
+ ## [1.0.7] - 2026-07-14
19
40
 
20
41
  ### Fixed
21
42
  - **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.
@@ -34,7 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
55
  - Socket drain on Content-Length pre-screen throws
35
56
  - Observer isolation improvements
36
57
 
37
- ## [1.0.6] - 2024-07-01
58
+ ## [1.0.6] - 2026-07-04
38
59
 
39
60
  ### Fixed
40
61
  - **Observer isolation** - Fixed `makeSafeObserver` proxying the observer directly, which could cause state leakage.
@@ -43,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
43
64
  - Corrected depth-0 availability documentation
44
65
  - Fixed migration.ts attribution in architecture docs
45
66
 
46
- ## [1.0.5] - 2024-06-28
67
+ ## [1.0.5] - 2026-07-04
47
68
 
48
69
  ### Fixed
49
70
  - **Content-Length pre-screen throws** - Added proper body draining to prevent resource leaks
@@ -52,13 +73,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
52
73
  ### Fixed
53
74
  - **YouTube transcript provenance** - Fixed transcript provenance tracking in final report citations
54
75
 
55
- ## [1.0.4] - 2024-06-25
76
+ ## [1.0.4] - 2026-07-04
56
77
 
57
78
  ### Fixed
58
79
  - **Credential detection** - Detect pi keys by pi's own semantics, not file presence; registry no longer gated on auth.json
59
80
  - **Configuration clarity** - Updated docs to reflect that pi has configuration files, not a named 'auth' feature
60
81
 
61
- ## [1.0.3] - 2024-06-20
82
+ ## [1.0.3] - 2026-07-04
62
83
 
63
84
  ### Fixed
64
85
  - **Model resolution** - Require a configured model on the standalone CLI/skill; align SDK session model with RESEARCH_MODEL
@@ -67,19 +88,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
67
88
  ### Documentation
68
89
  - Updated installation and configuration documentation
69
90
 
70
- ## [1.0.2] - 2024-06-15
91
+ ## [1.0.2] - 2026-07-04
71
92
 
72
93
  ### Fixed
73
94
  - **Timeout robustness** - Improved timeout handling for long-running operations
74
95
  - De-emphasized the markdown-conversion fallback in favor of native HTML-to-markdown conversion
75
96
 
76
- ## [1.0.1] - 2024-06-10
97
+ ## [1.0.1] - 2026-07-04
77
98
 
78
99
  ### Fixed
79
100
  - Initial stable release after rebuild against current pi APIs
80
101
  - Stealth-browser stack integration
81
102
 
82
- ## [0.1.13] - 2024-04-15
103
+ ## [0.1.13] - 2026-04-15
83
104
 
84
105
  ### Added
85
106
  - Initial research functionality
@@ -97,7 +97,7 @@ Timeouts
97
97
 
98
98
  | Variable | Default | Range | Description |
99
99
  |----------|---------|-------|-------------|
100
- | `PI_RESEARCH_LLM_TIMEOUT_MS` | `300000` | 60000–600000 | Coordinator / evaluator / repair / knowledge LLM call timeout. |
100
+ | `PI_RESEARCH_LLM_TIMEOUT_MS` | `300000` | 60000–1800000 | Coordinator / evaluator / repair / knowledge LLM call timeout. |
101
101
  | `PI_RESEARCH_SCRAPE_TIMEOUT_MS` | `15000` | 5000–120000 | Per-page scrape (page-load) timeout. |
102
102
  | `PI_RESEARCH_SEARCH_TIMEOUT_MS` | `45000` | 5000–120000 | Browser search page timeout. |
103
103
  | `PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS` | `10000` | 2000–120000 | Queue-wait / overhead margin added to each browser op's own timeout (a search task ceiling is `SEARCH_TIMEOUT_MS` + this; a scrape is `SCRAPE_TIMEOUT_MS` + this). |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "A set of web research tools (search, scrape, and others) and research agent system that uses them safely and efficiently.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -110,9 +110,9 @@
110
110
  "image": "https://raw.githubusercontent.com/Lincoln504/pi-research/main/docs/media/hero.png"
111
111
  },
112
112
  "dependencies": {
113
- "@earendil-works/pi-ai": ">=0.80.7 <1",
114
- "@earendil-works/pi-coding-agent": ">=0.80.7 <1",
115
- "@earendil-works/pi-tui": ">=0.80.7 <1",
113
+ "@earendil-works/pi-ai": ">=0.80.8 <1",
114
+ "@earendil-works/pi-coding-agent": ">=0.80.8 <1",
115
+ "@earendil-works/pi-tui": ">=0.80.8 <1",
116
116
  "@huggingface/transformers": "4.2.0",
117
117
  "@kreuzberg/html-to-markdown-node": "^3.5.5",
118
118
  "@lancedb/lancedb": "0.29.0",
@@ -123,11 +123,11 @@
123
123
  "impit": "0.13.0",
124
124
  "jsdom": "^29.1.1",
125
125
  "node-html-markdown": "^2.0.0",
126
- "pdf-oxide-wasm": "^0.3.70",
126
+ "pdf-oxide-wasm": "^0.3.75",
127
127
  "playwright-core": "1.60.0",
128
128
  "poolifier": "^5.3.2",
129
129
  "typebox": "1.1.38",
130
- "undici": "^8.6.0",
130
+ "undici": "^8.9.0",
131
131
  "youtubei.js": "^17.2.0"
132
132
  },
133
133
  "optionalDependencies": {
@@ -140,20 +140,21 @@
140
140
  "onnxruntime-web": "1.24.3",
141
141
  "node-domexception": "npm:@nolyfill/domexception@^1.0.28",
142
142
  "ua-parser-js": "^1.0.41",
143
- "protobufjs": "7.6.4",
144
- "vite": "^8.0.16"
143
+ "protobufjs": "7.6.5",
144
+ "vite": "^8.0.16",
145
+ "adm-zip": "^0.6.0"
145
146
  },
146
147
  "devDependencies": {
147
148
  "@eslint/js": "^10.0.1",
148
149
  "@types/jsdom": "^28.0.3",
149
- "@typescript-eslint/eslint-plugin": "^8.60.0",
150
- "@typescript-eslint/parser": "^8.60.0",
151
- "@vitest/coverage-v8": "^4.1.7",
152
- "dependency-cruiser": "^17.4.2",
150
+ "@typescript-eslint/eslint-plugin": "^8.65.0",
151
+ "@typescript-eslint/parser": "^8.65.0",
152
+ "@vitest/coverage-v8": "^4.1.10",
153
+ "dependency-cruiser": "^17.4.3",
153
154
  "depngn": "^0.6.0",
154
- "eslint": "^10.4.0",
155
- "npm-check-updates": "^22.2.1",
155
+ "eslint": "^10.8.0",
156
+ "npm-check-updates": "^22.2.9",
156
157
  "typescript": ">=6.0.3 <6.1.0",
157
- "vitest": "^4.1.7"
158
+ "vitest": "^4.1.10"
158
159
  }
159
160
  }
@@ -2,7 +2,7 @@
2
2
  name: pi-research
3
3
  description: "Multi-agent web research for any coding agent. Use for anything requiring current information from the internet — news, trends, product/service/company facts, statistics, people, places, events, APIs, library behavior, or 'what is X / how does X work' questions. Also use to search a local knowledge store of previously researched findings before doing live research. Powered by the pi-research engine (search, stealth scraping, security databases, Stack Exchange). Not for reading local files, running project commands, or analyzing this repository's own source."
4
4
  license: MIT
5
- metadata: { "author": "Lincoln Deen", "version": "1.0.8", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.0.9", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
6
6
  allowed-tools: Bash(node:*)
7
7
  ---
8
8
 
package/src/cli.ts CHANGED
@@ -240,9 +240,9 @@ function fileExists(p: string): boolean {
240
240
  * OPENAI_API_KEY. File presence alone is neither necessary (keys can live in
241
241
  * models.json / env with no auth.json) nor sufficient (auth.json can be `{}`).
242
242
  */
243
- function piKeysAvailable(piAuthPresent: boolean, provider?: string): boolean {
243
+ async function piKeysAvailable(piAuthPresent: boolean, provider?: string): Promise<boolean> {
244
244
  try {
245
- const available = safeGetAvailable(buildModelRegistry(undefined, undefined));
245
+ const available = safeGetAvailable(await buildModelRegistry(undefined, undefined));
246
246
  if (!provider) return available.length > 0;
247
247
  // Provider known (from the configured provider/model-id): keys must exist
248
248
  // for THAT provider — a key for some other provider would pass pre-flight
@@ -282,7 +282,7 @@ export interface CredentialDetection {
282
282
  * blocks a valid `--model` run). An explicit PI_RESEARCH_PROVIDER still wins:
283
283
  * it names the provider the configured key belongs to.
284
284
  */
285
- function detectCredentials(explicitModel?: string): CredentialDetection {
285
+ async function detectCredentials(explicitModel?: string): Promise<CredentialDetection> {
286
286
  const paths = resolvedConfigPaths();
287
287
  const piAuthPresent = fileExists(paths.piAuth);
288
288
  const piModelsPresent = fileExists(paths.piModels);
@@ -299,7 +299,7 @@ function detectCredentials(explicitModel?: string): CredentialDetection {
299
299
  providerEnv ?? (model && model.includes('/') ? model.slice(0, model.indexOf('/')) : undefined);
300
300
  // Provider-aware when the configured model names one: a key for a DIFFERENT
301
301
  // provider must not greenlight this run.
302
- const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
302
+ const piKeysUsable = await piKeysAvailable(piAuthPresent, provider);
303
303
 
304
304
  // --- Explicit API-key path -------------------------------------------------
305
305
  if (apiKey) {
@@ -440,7 +440,7 @@ interface ResearchArgs {
440
440
  async function cmdResearch(args: ResearchArgs): Promise<number> {
441
441
  // Pre-flight with the per-run --model flag folded in: it satisfies the
442
442
  // model-required check and steers the provider-aware key detection.
443
- const det = detectCredentials(args.model);
443
+ const det = await detectCredentials(args.model);
444
444
  if (det.problem) {
445
445
  if (args.json) toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
446
446
  else toStderr(`\nError: ${det.problem}\n\n${configBlock(det)}\n`);
@@ -531,7 +531,7 @@ async function cmdResearch(args: ResearchArgs): Promise<number> {
531
531
  toStdout(report.endsWith('\n') ? report : report + '\n');
532
532
  }
533
533
  } catch (err) {
534
- exit = reportError(err, 'research', args.json);
534
+ exit = await reportError(err, 'research', args.json);
535
535
  } finally {
536
536
  if (activeResearchAbortController === abortController) activeResearchAbortController = null;
537
537
  await safeShutdown();
@@ -545,7 +545,7 @@ async function cmdResearch(args: ResearchArgs): Promise<number> {
545
545
  * research.
546
546
  */
547
547
  async function cmdKnowledge(queries: string[], json?: boolean): Promise<number> {
548
- const det = detectCredentials();
548
+ const det = await detectCredentials();
549
549
  if (det.problem) {
550
550
  if (json) { toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG })); return EXIT.CONFIG; }
551
551
  toStderr(`\nError: ${det.problem}\n\n${configBlock(det)}\n`);
@@ -595,7 +595,7 @@ async function cmdKnowledge(queries: string[], json?: boolean): Promise<number>
595
595
  * run, without initializing the SDK. Machine-readable with --json.
596
596
  */
597
597
  async function cmdStatus(json?: boolean): Promise<number> {
598
- const det = detectCredentials();
598
+ const det = await detectCredentials();
599
599
  const paths = resolvedConfigPaths();
600
600
  const cfg = getConfig(process.cwd(), 'cli');
601
601
  const summary = {
@@ -815,7 +815,7 @@ function describeUninstall(r: { status: string; path: string; message?: string }
815
815
  // ---------------------------------------------------------------------------
816
816
 
817
817
  /** Distinguish setup errors from runtime errors and print a clean, located message. */
818
- export function reportError(err: unknown, what: string, json?: boolean): number {
818
+ export async function reportError(err: unknown, what: string, json?: boolean): Promise<number> {
819
819
  const msg = err instanceof Error ? err.message : String(err);
820
820
  const lower = msg.toLowerCase();
821
821
 
@@ -865,7 +865,7 @@ export function reportError(err: unknown, what: string, json?: boolean): number
865
865
  return exitCode;
866
866
  }
867
867
  if (isConfigError) {
868
- toStderr(`\nError: pi-research ${what} failed: ${msg}\n\n${configBlock(detectCredentials())}\n`);
868
+ toStderr(`\nError: pi-research ${what} failed: ${msg}\n\n${configBlock(await detectCredentials())}\n`);
869
869
  return exitCode;
870
870
  }
871
871
  toStderr(`\nError: pi-research ${what} failed: ${msg}\n`);
package/src/config.ts CHANGED
@@ -87,9 +87,13 @@ export const ConfigSchema = Type.Object({
87
87
  * never killed before it has used its full nav budget plus time waiting in the queue.
88
88
  * (default: 10000) */
89
89
  BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2000, maximum: 120000, default: 10000 }),
90
- /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-600s).
91
- * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var. */
92
- LLM_TIMEOUT_MS: Type.Number({ minimum: 60000, maximum: 600000, default: 300000 }),
90
+ /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
91
+ * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
92
+ * Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
93
+ * large-context synthesis/plan call can legitimately run >10min, and the old
94
+ * 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
95
+ * (e.g. 900000), causing those calls to time out mid-generation. */
96
+ LLM_TIMEOUT_MS: Type.Number({ minimum: 60000, maximum: 1800000, default: 300000 }),
93
97
  /** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
94
98
  * evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
95
99
  * sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
@@ -945,7 +949,7 @@ export function createConfig(env: Record<string, string | undefined>, processEnv
945
949
  SEARCH_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_SEARCH_TIMEOUT_MS', DEFAULTS.SEARCH_TIMEOUT_MS, 5000, 120000),
946
950
  TUI_REFRESH_DEBOUNCE_MS: parseEnvNumber(e, 'PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS', DEFAULTS.TUI_REFRESH_DEBOUNCE_MS, 0, 1000),
947
951
  BROWSER_TASK_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS', DEFAULTS.BROWSER_TASK_TIMEOUT_MS, 2000, 120000),
948
- LLM_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_LLM_TIMEOUT_MS', DEFAULTS.LLM_TIMEOUT_MS, 60000, 600000),
952
+ LLM_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_LLM_TIMEOUT_MS', DEFAULTS.LLM_TIMEOUT_MS, 60000, 1800000),
949
953
  LLM_THINKING_LEVEL: parseEnvEnum(e, 'PI_RESEARCH_LLM_THINKING_LEVEL', ['off', 'minimal', 'low', 'medium', 'high'] as const, DEFAULTS.LLM_THINKING_LEVEL),
950
954
  PLANNING_MAX_TOKENS: parseEnvNumber(e, 'PI_RESEARCH_PLANNING_MAX_TOKENS', DEFAULTS.PLANNING_MAX_TOKENS, 1024, 131072),
951
955
  SYNTHESIS_MAX_TOKENS: parseEnvNumber(e, 'PI_RESEARCH_SYNTHESIS_MAX_TOKENS', DEFAULTS.SYNTHESIS_MAX_TOKENS, 1024, 131072),
@@ -8,7 +8,8 @@
8
8
  * Previously this logic was duplicated (#29) — any bug fix now updates both paths.
9
9
  */
10
10
 
11
- import { AuthStorage, ModelRegistry, getAgentDir } from '@earendil-works/pi-coding-agent';
11
+ import { ModelRegistry, ModelRuntime, getAgentDir } from '@earendil-works/pi-coding-agent';
12
+ import { InMemoryCredentialStore } from '@earendil-works/pi-ai';
12
13
  import type { Model } from '@earendil-works/pi-ai';
13
14
  import * as path from 'node:path';
14
15
  import * as fs from 'node:fs';
@@ -103,42 +104,93 @@ function buildHeaders(provider?: string): Record<string, string> {
103
104
  /**
104
105
  * Create a ModelRegistry with the given credentials.
105
106
  *
107
+ * pi 0.80.8 replaced `AuthStorage`/`ModelRegistry.create()` with the async
108
+ * `ModelRuntime`: `ModelRegistry` is now a SYNCHRONOUS compatibility facade that
109
+ * wraps a `ModelRuntime` and still exposes `getAll`/`getAvailable`/`find`/
110
+ * `getApiKeyAndHeaders` — so every consumer of the registry (resolveModel,
111
+ * safeGet*, the research-model resolver) stays synchronous. Only this factory
112
+ * became async (constructing the ModelRuntime is inherently async) and its two
113
+ * callers await it. Mirrors how pi itself builds a registry
114
+ * (`sdk.js`: `await ModelRuntime.create({ authPath, modelsPath })`).
115
+ *
106
116
  * @param apiKey - Optional explicit API key
107
117
  * @param provider - Optional provider name to key the API key under
108
118
  * @returns A configured ModelRegistry
109
119
  */
110
- export function buildModelRegistry(apiKey?: string, provider?: string): ModelRegistry {
120
+ export async function buildModelRegistry(apiKey?: string, provider?: string): Promise<ModelRegistry> {
111
121
  const agentDir = getAgentDir();
112
122
  const modelsJsonPath = path.join(agentDir, 'models.json');
113
123
  const authPath = path.join(agentDir, 'auth.json');
124
+ const hasModels = fs.existsSync(modelsJsonPath);
125
+ const hasAuth = fs.existsSync(authPath);
126
+
127
+ // Build the runtime from the user's pi config (models.json + auth.json) when
128
+ // EITHER file is present, otherwise a bare runtime that reads env-var keys and
129
+ // built-in providers. allowModelNetwork:false preserves the prior behavior of
130
+ // NOT doing a network model-catalog refresh at construction (the old
131
+ // ModelRegistry.create only read the local files). Env provider keys
132
+ // (e.g. OPENAI_API_KEY) are resolved via pi-ai's provider auth CONTEXT, not the
133
+ // credential store, so they are honored in every branch below; embedded
134
+ // models.json apiKeys likewise — auth.json presence must not gate models.json.
135
+ //
136
+ // When auth.json does NOT exist, an explicit in-memory credential store is
137
+ // passed instead of letting ModelRuntime default to the file at authPath:
138
+ // AuthStorage's file backend CREATES ~/.pi/agent/ and an empty auth.json as a
139
+ // construction side effect (mkdir+write). Without this, every CLI invocation —
140
+ // including read-only ones like `status` — would silently create a pi config
141
+ // dir on machines that never installed pi, and would hard-fail in read-only-
142
+ // $HOME environments (containers/CI) where the old in-memory path needed no
143
+ // filesystem write access at all.
144
+ const runtime = await ModelRuntime.create({
145
+ authPath: hasAuth ? authPath : undefined,
146
+ credentials: hasAuth ? undefined : new InMemoryCredentialStore(),
147
+ modelsPath: hasModels ? modelsJsonPath : null,
148
+ allowModelNetwork: false,
149
+ });
114
150
 
151
+ // Explicit key: seed it on the runtime after construction. NOTE: unlike the old
152
+ // AuthStorage.inMemory({ [provider]: ... }) seeding, this MERGES — the explicit
153
+ // key wins for its own provider (runtime overrides are checked first), but any
154
+ // providers already configured in auth.json remain visible in getAvailable().
155
+ // That broadening is acceptable because explicit-key flows always pair the key
156
+ // with an explicit model (enforced in cli.ts pre-flight), so resolution never
157
+ // falls back to "first available".
115
158
  if (apiKey && provider) {
116
- // Explicit key: seed InMemory storage under the correct provider name.
117
- const authStorage = AuthStorage.inMemory({
118
- [provider]: { type: 'api_key', key: apiKey },
119
- });
120
- return ModelRegistry.create(
121
- authStorage,
122
- fs.existsSync(modelsJsonPath) ? modelsJsonPath : undefined,
123
- );
159
+ // allowNetwork:false is REQUIRED here, not just consistent with create():
160
+ // setRuntimeApiKey triggers a model-catalog refresh for the newly configured
161
+ // provider, and its default options open live catalog connections — verified
162
+ // to hang indefinitely (no timeout at this layer) in network-restricted
163
+ // environments, which would wedge every explicit-API-key pre-flight.
164
+ await runtime.setRuntimeApiKey(provider, apiKey, { allowNetwork: false });
124
165
  }
125
166
 
126
- // No explicit key: use the user's pi configuration when EITHER file exists.
127
- // ModelRegistry.create() reads embedded apiKeys from models.json providers
128
- // directly (via providerRequestConfigs), so custom providers with embedded
129
- // keys work with no auth.json at all — auth.json presence must not gate
130
- // models.json (AuthStorage.create tolerates a missing file). Env provider
131
- // keys (e.g. OPENAI_API_KEY) are honored by AuthStorage in every branch.
132
- if (fs.existsSync(authPath) || fs.existsSync(modelsJsonPath)) {
133
- const authStorage = AuthStorage.create(authPath);
134
- return ModelRegistry.create(
135
- authStorage,
136
- fs.existsSync(modelsJsonPath) ? modelsJsonPath : undefined,
137
- );
138
- }
167
+ // The sync facade all our resolvers consume. getAvailable()/getApiKeyAndHeaders()
168
+ // read a cached snapshot populated by create(), so the sync reads below work
169
+ // without further awaits.
170
+ const registry = new ModelRegistry(runtime);
171
+ registryRuntimes.set(registry, runtime);
172
+ return registry;
173
+ }
139
174
 
140
- // Last resort: in-memory with nothing — will fail at LLM call time with actionable error
141
- return ModelRegistry.inMemory(AuthStorage.inMemory());
175
+ /**
176
+ * The ModelRuntime backing each registry this factory built. Researcher sessions
177
+ * must be created with `modelRuntime` (pi 0.80.8+ replaced the old
178
+ * `modelRegistry` session option): without it, createAgentSession() builds its
179
+ * OWN runtime from the default ~/.pi/agent paths, which never sees an explicit
180
+ * API key seeded via setRuntimeApiKey above — standalone PI_RESEARCH_API_KEY
181
+ * users with no (or a different-provider) auth.json would pass pre-flight and
182
+ * then fail auth on every researcher LLM call.
183
+ */
184
+ const registryRuntimes = new WeakMap<ModelRegistry, ModelRuntime>();
185
+
186
+ /**
187
+ * Return the ModelRuntime behind a registry built by {@link buildModelRegistry},
188
+ * or undefined for registries built elsewhere (e.g. the pi host's own
189
+ * ExtensionContext.modelRegistry — for those, createAgentSession()'s default
190
+ * runtime construction from the host's agent dir is exactly right).
191
+ */
192
+ export function getRuntimeForRegistry(registry: unknown): ModelRuntime | undefined {
193
+ return registry instanceof Object ? registryRuntimes.get(registry as ModelRegistry) : undefined;
142
194
  }
143
195
 
144
196
  /**
package/src/index.ts CHANGED
@@ -90,10 +90,14 @@ function extractResultText(result: AgentToolResult<unknown>): string {
90
90
  * Pi Research Extension
91
91
  */
92
92
  export default async function (pi: ExtensionAPI) {
93
- // Runtime version check — must match the @earendil-works/* dependency minimum (>=0.80.2).
94
- // 0.80.2 is the floor because the AuthStorage credential discriminator pi-research
95
- // passes (`type: 'api_key'`, model-registry-factory.ts) only became correct in 0.80.2
96
- // (it was `api-key` before); the pi-ai `/compat` entrypoint also exists from 0.80.0.
93
+ // Runtime version check — must match the @earendil-works/* dependency minimum (>=0.80.8).
94
+ // 0.80.8 is the floor because pi 0.80.8 introduced ModelRuntime and removed
95
+ // AuthStorage/ModelRegistry.create(); buildModelRegistry (model-registry-factory.ts)
96
+ // unconditionally calls ModelRuntime.create(), and createAgentSession() is invoked
97
+ // without the removed `modelRegistry` option (researcher.ts), relying on the
98
+ // 0.80.8+ host auto-building its ModelRuntime. This in-host check is the real
99
+ // enforcement point: when running as an extension the host's version is NOT
100
+ // constrained by npm's resolution of our own package.json range.
97
101
  const versionParts = PI_VERSION.split('.').map(Number);
98
102
  const major = versionParts[0] ?? 0;
99
103
  const minor = versionParts[1] ?? 0;
@@ -104,7 +108,7 @@ export default async function (pi: ExtensionAPI) {
104
108
  `Please ensure pi-coding-agent is installed correctly.`,
105
109
  );
106
110
  }
107
- const minMajor = 0, minMinor = 80, minPatch = 2;
111
+ const minMajor = 0, minMinor = 80, minPatch = 8;
108
112
  const tooOld = major < minMajor
109
113
  || (major === minMajor && minor < minMinor)
110
114
  || (major === minMajor && minor === minMinor && patch < minPatch);
@@ -2278,9 +2278,13 @@ var ConfigSchema = Type.Object({
2278
2278
  * never killed before it has used its full nav budget plus time waiting in the queue.
2279
2279
  * (default: 10000) */
2280
2280
  BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
2281
- /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-600s).
2282
- * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var. */
2283
- LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 6e5, default: 3e5 }),
2281
+ /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
2282
+ * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
2283
+ * Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
2284
+ * large-context synthesis/plan call can legitimately run >10min, and the old
2285
+ * 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
2286
+ * (e.g. 900000), causing those calls to time out mid-generation. */
2287
+ LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
2284
2288
  /** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
2285
2289
  * evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
2286
2290
  * sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
@@ -22,6 +22,7 @@ import type { Config } from '../config.ts';
22
22
  import { logger } from '../logger.ts';
23
23
 
24
24
  import { resolveResearchModel } from '../core/llm/research-model-resolver.ts';
25
+ import { getRuntimeForRegistry } from '../core/llm/model-registry-factory.ts';
25
26
 
26
27
  export interface CreateResearcherSessionOptions {
27
28
  cwd: string;
@@ -137,7 +138,17 @@ export async function createResearcherSession(options: CreateResearcherSessionOp
137
138
  sessionManager: SessionManager.inMemory(), // Each researcher gets its own isolated session
138
139
  settingsManager: researcherSettings,
139
140
  model: modelToUse,
140
- modelRegistry,
141
+ // pi 0.80.8 replaced the SDK's `modelRegistry`/`authStorage` session options
142
+ // with the async `modelRuntime`. Pass the runtime backing OUR registry when
143
+ // this run built one (standalone CLI/SDK): it carries any explicit API key
144
+ // seeded via setRuntimeApiKey, which the session's own default runtime
145
+ // (auto-built from ~/.pi/agent/{auth,models}.json) would never see — without
146
+ // this, standalone PI_RESEARCH_API_KEY users fail auth on every researcher
147
+ // LLM call despite passing pre-flight. For registries we did NOT build
148
+ // (the pi host's ExtensionContext.modelRegistry), this resolves to
149
+ // undefined and the host auto-builds from its own agent dir — correct
150
+ // in-host, where auth lives in the host's config.
151
+ modelRuntime: getRuntimeForRegistry(modelRegistry),
141
152
  resourceLoader: makeResourceLoader(systemPrompt),
142
153
  // Thinking/reasoning is disabled for researcher agents: they do retrieval and
143
154
  // synthesis from scraped pages, not open-ended reasoning. Keeping it off reduces
package/src/sdk.ts CHANGED
@@ -286,7 +286,7 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
286
286
  }
287
287
 
288
288
  // Build and cache the registry (one instance for the lifetime of this init cycle).
289
- globalRegistry = sharedBuildModelRegistry(globalApiKey, parsedProvider);
289
+ globalRegistry = await sharedBuildModelRegistry(globalApiKey, parsedProvider);
290
290
 
291
291
  try {
292
292
  // Resolve the model. Precedence: explicit option (a Model object is used
@@ -5,31 +5,21 @@
5
5
  * Both files use identical ResourceLoader implementations.
6
6
  */
7
7
 
8
- import type { ResourceLoader, ExtensionRuntime } from '@earendil-works/pi-coding-agent';
8
+ import { createExtensionRuntime, type ResourceLoader } from '@earendil-works/pi-coding-agent';
9
9
 
10
10
  export function makeResourceLoader(systemPromptText: string): ResourceLoader {
11
- const mockRuntime: ExtensionRuntime = {
12
- flagValues: new Map(),
13
- pendingProviderRegistrations: [],
14
- registerProvider: () => {},
15
- unregisterProvider: () => {},
16
- sendMessage: async () => {},
17
- sendUserMessage: async () => {},
18
- appendEntry: async () => {},
19
- setSessionName: () => undefined,
20
- getSessionName: () => undefined,
21
- setLabel: async () => {},
22
- getActiveTools: () => [],
23
- getAllTools: () => [],
24
- setActiveTools: () => {},
25
- refreshTools: () => {},
26
- getCommands: () => [],
27
- setModel: async () => true,
28
- getThinkingLevel: () => 'off',
29
- setThinkingLevel: () => {},
30
- assertActive: () => {},
31
- invalidate: () => {},
32
- };
11
+ // Use pi's own createExtensionRuntime() instead of hand-maintaining a mock.
12
+ //
13
+ // A literal mock object drifted out of sync with the ExtensionRuntime contract:
14
+ // pi 0.81.0 ("Full provider extensions") added `pendingNativeProviderRegistrations`
15
+ // + `registerNativeProvider`, which this mock omitted. Under pi >= 0.81.0 the
16
+ // host's runner.bindCore() / createAgentSessionServices() iterate that field —
17
+ // `for (... of runtime.pendingNativeProviderRegistrations)` — and an `undefined`
18
+ // field threw `this.runtime.pendingNativeProviderRegistrations is not iterable`,
19
+ // aborting EVERY researcher session ("Research produced no report — no source
20
+ // material was collected"). createExtensionRuntime() always returns a runtime
21
+ // that satisfies the current host's full contract, so this can't drift again.
22
+ const mockRuntime = createExtensionRuntime();
33
23
 
34
24
  return {
35
25
  getExtensions: () => ({ extensions: [], errors: [], runtime: mockRuntime }),
@@ -61,10 +61,22 @@ export async function performSearch(
61
61
  if (onProgress) onProgress(0);
62
62
 
63
63
  // Hard cap per query so a single Cloudflare block or hung browser worker
64
- // cannot stall the entire Promise.all burst. Derived from the configured
65
- // search timeout (default 45s) so raising SEARCH_TIMEOUT_MS actually takes
66
- // effect here instead of being silently overridden by a lower literal.
67
- const QUERY_TIMEOUT_MS = config?.SEARCH_TIMEOUT_MS ?? 45_000;
64
+ // cannot stall the entire Promise.all burst.
65
+ //
66
+ // The budget is the worker's nav budget (SEARCH_TIMEOUT_MS) PLUS the
67
+ // queue-wait/overhead margin (BROWSER_TASK_TIMEOUT_MS), mirroring the
68
+ // scheduler's own task-timeout ceiling in BrowserTaskScheduler.runSearch
69
+ // (= SEARCH_TIMEOUT_MS + BROWSER_TASK_TIMEOUT_MS). This clock starts at
70
+ // ENQUEUE (before runWorkerSearch queues onto the shared browser pool), so
71
+ // a budget of only SEARCH_TIMEOUT_MS would let a query that sat in the
72
+ // saturated worker queue burn its ENTIRE nav budget waiting for a slot and
73
+ // abort with zero actual search work — exactly the "Query timed out after
74
+ // 45000ms (likely blocked or slow startup)" cascade observed under
75
+ // concurrent load. Including the margin lets a queued query still use its
76
+ // full nav budget once a worker picks it up, and keeps this layer's hard
77
+ // cap coherent with the scheduler's so it never preempts the worker's own
78
+ // (post-dequeue) deadline.
79
+ const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45_000) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 10_000);
68
80
 
69
81
  const filteredQueries = queries.filter(q => q.trim());
70
82
  let timeoutCount = 0;
@@ -133,7 +145,7 @@ export async function performSearch(
133
145
  metrics.increment('browser_search_queries_total', 1, { status });
134
146
 
135
147
  if (isTimeout) {
136
- logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms (likely blocked or slow startup): "${query}"`);
148
+ logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms — likely starved waiting for a free browser worker under concurrent load, or a slow/blocked search provider: "${query}"`);
137
149
  } else {
138
150
  const msg = error instanceof Error ? error.message : String(error);
139
151
  if (msg !== 'Aborted') {