@lincoln504/pi-research 1.6.5 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +14 -9
  2. package/agent-skill/pi-research/README.md +5 -4
  3. package/agent-skill/pi-research/SKILL.md +1 -1
  4. package/dist/cli.mjs +616 -153
  5. package/dist/pdf-extract-worker.mjs +1289 -0
  6. package/dist/prompts/research-tool-usage.md +2 -1
  7. package/dist/prompts/researcher.md +1 -1
  8. package/dist/prompts/system-coordinator.md +2 -2
  9. package/dist/prompts/system-lead-router.md +1 -1
  10. package/docs/AGENT-SKILL.md +6 -6
  11. package/docs/ARCHITECTURE.md +24 -22
  12. package/docs/CHANGELOG.md +73 -0
  13. package/docs/CONFIGURATION.md +3 -2
  14. package/docs/SDK.md +7 -12
  15. package/package.json +8 -5
  16. package/scripts/build.cjs +35 -0
  17. package/scripts/setup.cjs +12 -10
  18. package/src/cleanup/research-cleanup.ts +11 -5
  19. package/src/cli.ts +7 -5
  20. package/src/config.ts +9 -3
  21. package/src/core/interfaces/knowledge-interfaces.ts +8 -0
  22. package/src/core/interfaces/orchestration-interfaces.ts +1 -1
  23. package/src/core/interfaces/research-plan-types.ts +6 -0
  24. package/src/core/llm/agentic-repair.ts +40 -3
  25. package/src/core/llm/llm-utils.ts +93 -5
  26. package/src/core/llm/model-registry-factory.ts +2 -15
  27. package/src/core/pi-version.ts +1 -1
  28. package/src/core/planning-service.ts +110 -15
  29. package/src/core/planning-utils.ts +3 -0
  30. package/src/core/service-registry.ts +8 -6
  31. package/src/index.ts +8 -5
  32. package/src/infrastructure/browser/config.ts +6 -7
  33. package/src/infrastructure/browser/ensure-browser.ts +14 -7
  34. package/src/infrastructure/browser/thread-worker-browser.ts +18 -19
  35. package/src/knowledge/chunker.ts +22 -4
  36. package/src/knowledge/embedder-init.ts +41 -1
  37. package/src/knowledge/embedder-utils.ts +7 -1
  38. package/src/knowledge/store.ts +68 -1
  39. package/src/knowledge/transformers-loader.ts +58 -1
  40. package/src/observers/research-observer-impl.ts +4 -1
  41. package/src/orchestration/deep-research-orchestrator.ts +3 -3
  42. package/src/orchestration/headless-observer.ts +33 -0
  43. package/src/orchestration/quick-research-orchestrator.ts +17 -3
  44. package/src/orchestration/research-orchestration-service.ts +27 -4
  45. package/src/orchestration/session-state.ts +36 -7
  46. package/src/prompts/research-tool-usage.md +2 -1
  47. package/src/prompts/researcher.md +1 -1
  48. package/src/prompts/system-coordinator.md +2 -2
  49. package/src/prompts/system-lead-router.md +1 -1
  50. package/src/sdk.ts +12 -4
  51. package/src/tools/research-knowledge-search.ts +23 -7
  52. package/src/tools/research-tool-definition.ts +1 -1
  53. package/src/tools/scrape.ts +12 -9
  54. package/src/tools/search.ts +12 -6
  55. package/src/tools/security.ts +6 -1
  56. package/src/tools/stackexchange.ts +6 -1
  57. package/src/utils/cancellation.ts +34 -1
  58. package/src/utils/llm-usage.ts +8 -0
  59. package/src/utils/log-utils.ts +1 -1
  60. package/src/web-research/browser-search.ts +6 -5
  61. package/src/web-research/pdf-extract-worker.mjs +1289 -0
  62. package/src/web-research/pdf-extract-worker.ts +42 -0
  63. package/src/web-research/pdf-extraction.ts +392 -0
  64. package/src/web-research/web-scraper.ts +3 -111
@@ -85,7 +85,8 @@ 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
+ - **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). Treat the machine-wide run cap (next bullet) as the REAL ceiling: it may be lower than 4, and it always wins.
89
+ - **Simultaneous calls are also bounded by the machine-wide run cap (default 3, `PI_RESEARCH_MAX_CONCURRENT_RUNS`).** A call beyond the cap WAITS for a free slot and fails with a capacity error if the wait expires — so do not count on more than 3 running at once, and never rely on the 4-call maximum being available.
89
90
  - Example: `research("bananas")` and `research("quantum computing")` in the same turn — these have zero overlap.
90
91
  - **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.
91
92
  - Example: `research("Python async performance and Rust async performance comparison")` — NOT separate calls for Python and Rust, since the comparison IS the research goal.
@@ -50,7 +50,7 @@ Write your report immediately after scraping is complete, or as soon as a tool r
50
50
  - `youtube_transcript`: Read the captions/transcript of YouTube videos from your source list — it is the `scrape` for video. ONE call only, batching up to a few of the most relevant YouTube links. Cite these as `Source: YouTube Transcript`.
51
51
  - `stackexchange`: Use ONLY for Stack Overflow or Stack Exchange URLs.
52
52
  - `security_search`: Query NVD, CVE, OSV, CISA databases.
53
- - `read` / `grep`: Read files and search text in the local codebase. Use ONLY if local codebase context is explicitly required.
53
+ - `read`: Read files in the local codebase. Use ONLY if local codebase context is explicitly required.
54
54
  {{extra_tool_guidelines}}
55
55
 
56
56
  - **Specialized Tooling Directive**: Do NOT waste time with auxiliary tools (`stackexchange`, `security_search`) unless they are **specifically necessary** for your assigned goal. They are not for exploratory steps. Your primary workflow is to **scrape** authoritative web sources and **report** findings.
@@ -43,7 +43,7 @@ Before splitting the query into researchers, pin down its scope — exactly what
43
43
 
44
44
  ## Output Format
45
45
 
46
- Return ONLY a JSON block containing your full team plan. Use this general schema:
46
+ Respond with your full team plan by CALLING the `submit_plan` tool when it is offered in the conversation — its arguments ARE the plan (primary, schema-exact channel). Only if you cannot call tools, return a JSON block instead. Use this general schema:
47
47
 
48
48
  ```json
49
49
  {
@@ -72,4 +72,4 @@ Return ONLY a JSON block containing your full team plan. Use this general schema
72
72
  - **Researcher IDs**: Use Round.Index format (e.g. **1.1**, **1.2**).
73
73
  - **Citations**: Each researcher's queries should be designed to uncover sources that can be comprehensively cited. Prioritize queries that lead to authoritative references, data, and documented facts.
74
74
  - **Title**: Set `"title"` to 1-2 words that best name the core topic (e.g. `"iPhone 15"`, `"React Performance"`, `"PostgreSQL Replication"`). Use proper nouns and brand names where applicable. Do NOT use generic words like "research", "overview", or "options".
75
- - **Format**: Valid JSON only. No markdown, no explanation outside the JSON block.
75
+ - **Format**: Call `submit_plan` with the plan as its arguments when the tool is offered; otherwise output valid JSON only. No markdown, no explanation outside the JSON block.
@@ -119,4 +119,4 @@ When delegating, ensure:
119
119
  - **Researcher IDs**: Use Round.Index format — the current round number from the RUN CONTEXT block, then the researcher index. In round 3 that is **3.1**, **3.2**, and so on.
120
120
  - **Query Budget**: Use the complexity-specific budget ({{query_budget}} per researcher). Fill each researcher's query budget completely.
121
121
  - **Team Size**: Scale researcher count to match the gaps. Use up to {{max_team_size}} researchers when delegating, but a single well-targeted researcher is often sufficient for focused gaps. Don't pad the team when fewer researchers will cover the remaining gaps efficiently.
122
- - **Format**: ONLY return valid JSON in a code block. Never return report prose.
122
+ - **Format**: Call `submit_routing_decision` with the updated plan as its arguments when the tool is offered; otherwise return ONLY valid JSON in a code block. Never return report prose.
@@ -17,14 +17,14 @@ For standalone use without pi, install the engine globally, then link the skill
17
17
  every coding agent detected on this machine:
18
18
 
19
19
  ```bash
20
- npm install -g @lincoln504/pi-research --allow-scripts=better-sqlite3 # the engine (puts `pi-research` on PATH)
20
+ npm install -g @lincoln504/pi-research # the engine (puts `pi-research` on PATH)
21
21
  pi-research skill install # link the skill into every detected agent
22
22
  ```
23
23
 
24
- On npm 12 or newer the flag is needed: npm blocks dependency install scripts by
25
- default, and without the one that builds the stealth browser's SQLite module every
26
- search fails with a missing-module error. It is harmless on older npm (see the
27
- [README](../README.md#install)).
24
+ On npm ≥11.19 (and npm 12) dependency install scripts are skipped by default — and
25
+ none are needed: better-sqlite3 13 ships prebuilt bindings in its own tarball, and the
26
+ stealth browser self-provisions on its first use (the first scrape takes a few
27
+ minutes). No approval is required (see the [README](../README.md#install)).
28
28
 
29
29
  `skill install` targets only agents already set up under `$HOME`, never overwrites a
30
30
  different skill in the slot, and records what it created so `pi-research skill uninstall`
@@ -130,7 +130,7 @@ Manual. Symlink the directory into any agent's skills folder yourself:
130
130
  (`PI_RESEARCH_MODEL`) + API key. See [Configuration](CONFIGURATION.md).
131
131
 
132
132
  ```bash
133
- npm install -g @lincoln504/pi-research --allow-scripts=better-sqlite3
133
+ npm install -g @lincoln504/pi-research
134
134
  node "<skill_dir>/scripts/run.mjs" status # verify the engine is detected
135
135
  ```
136
136
 
@@ -309,27 +309,27 @@ Worker pool over direct browser — browser processes are isolated in workers so
309
309
  in one cannot affect the orchestrator or other sessions.
310
310
 
311
311
  Pinned browser stack — `playwright-core` and `impit` are pinned to exact versions and
312
- `camoufox-js` to its `0.10.x` line; the three are coupled and upgraded together, because
313
- each floating range broke fresh consumer installs that our lockfile masked.
312
+ `camoufox-js` is pinned to its `0.12.0` line; the three are coupled and upgraded together,
313
+ because each floating range broke fresh consumer installs that our lockfile masked.
314
314
  playwright-core stays at `1.60.0` (1.61+ rejects camoufox's Juggler and fails every
315
- launch — corroborated upstream, since camoufox-js `0.12.0` declares
315
+ launch — corroborated upstream: camoufox-js `0.12.0` declares
316
316
  `peerDependencies: { "playwright-core": "<1.61.0" }`, the same bound this pin holds by
317
- hand). `impit` stays at `0.13.0`: camoufox-js 0.10.2 requires `^0.13.0`, and within that
318
- range only 0.13.0 lacks the `only-allow pnpm` preinstall guard that breaks
319
- `npm install -g` (an upstream mistake in 0.13.1/0.14.0, dropped again in 0.14.1). An
320
- exact pin is required because npm `overrides` do not propagate to consumers. Rationale in
321
- full: `src/infrastructure/browser/thread-worker-browser.ts`.
322
-
323
- The two original blockers have both lapsed — camoufox restored Windows binaries in
324
- `v152.0.4-beta.26` (2026-07-16) and impit's guard is long gone — but camoufox-js `0.12.0`
325
- is held back by a third, worse one: it requires `better-sqlite3 ^13.0.1`, and
326
- better-sqlite3 13 **dropped prebuilt binaries**. 12.x installs via
327
- `prebuild-install || node-gyp rebuild`; 13.x ships no install script at all, so npm
328
- auto-runs `node-gyp rebuild` against its `binding.gyp` and every install demands a full
329
- C++ toolchain. This was measured, not inferred: the upgrade was made, and `npm ci` failed
330
- outright on the Windows CI runner (`could not find a version of Visual Studio 2017 or
331
- newer`) — i.e. it would break installation for most Windows users, which is exactly what
332
- this pin exists to prevent. Check better-sqlite3 before camoufox next time.
317
+ hand). `impit` is exact at `0.14.4` (refreshed from `0.13.0` on 2026-08-30 together with
318
+ the camoufox bump) — exact because npm `overrides` do not propagate to consumers, so an
319
+ exact pin is the only way to force a version downstream; impit's `only-allow pnpm`
320
+ preinstall-guard incident (0.13.1/0.14.0, dropped in 0.14.1) is why floating ranges are
321
+ not trusted here. Rationale in full: `src/infrastructure/browser/thread-worker-browser.ts`.
322
+
323
+ The 0.10.x→0.12.0 camoufox bump had been held back through two refresh cycles by three
324
+ blockers, all since resolved: camoufox restored Windows binaries in `v152.0.4-beta.26`
325
+ (2026-07-16); impit's pnpm guard existed only in 0.13.1/0.14.0; and camoufox-js 0.12's
326
+ better-sqlite3 13 upgrade, which initially looked like it demanded a C++ toolchain on
327
+ every install. Measured on 13.0.3: `prebuilds/` for all eight platform/arch pairs ship
328
+ INSIDE its tarball and load at runtime via node-gyp-build — no install script, nothing
329
+ for a consumer to approve. What actually broke was tooling, not the binding: npm ≤11's
330
+ injected `node-gyp rebuild` needlessly recompiles a binding.gyp with no install script
331
+ (and its node-gyp 11.2 cannot detect the VS2026 CI runner image), which is why CI runs
332
+ npm 12. Any future bump checks better-sqlite3 first, not camoufox.
333
333
 
334
334
  The browser BINARY, by contrast, is not pinned and cannot be. `camoufox-js fetch` takes
335
335
  no version argument: it walks the `daijro/camoufox` GitHub releases newest-first and takes
@@ -339,8 +339,7 @@ which camoufox-js version is installed — the npm pins do not freeze it, and a
339
339
  camoufox release could break launches for fresh installs with no change on our side.
340
340
  Windows assets were in fact missing from `v146-hardware` through `v152.0.2-alpha` and
341
341
  returned in `v152.0.4-beta.26` (2026-07-16). Current newest is `v152.0.4-beta.28`
342
- (Firefox 152); it launches and drives cleanly under playwright-core `1.60.0` **with
343
- camoufox-js 0.10.2** (whose `MIN_VERSION` is `beta.19`, so it accepts it), verified
342
+ (Firefox 152); it launches and drives cleanly under playwright-core `1.60.0`, verified
344
343
  directly, as does the older `v135.0.1-beta.24` an existing cache may still hold. The
345
344
  practical consequence is that browser freshness is independent of the npm pin: a stale
346
345
  `camoufox-js` does not mean a stale Firefox. Re-verify a real launch when bumping this
@@ -366,7 +365,8 @@ caret-safe. Note also that every `@lancedb/lancedb` release through 0.37 declare
366
365
  changes which pairing needs re-validating.
367
366
 
368
367
  Pinned validation library — `typebox` is pinned to the exact version the pi host packages
369
- depend on (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent` 0.84.2 pin `1.3.7`). Every
368
+ depend on (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent` pin `1.3.7` across
369
+ the 0.84.x line). Every
370
370
  tool's parameter schema is built with TypeBox here and handed across the boundary to pi's
371
371
  tool system, so the two must agree on `Value.Check`/`Convert` semantics. A floating `^1.1.38`
372
372
  range let a fresh consumer install resolve pi-research to a newer TypeBox than pi's, shipping
@@ -427,6 +427,8 @@ npm run test:unit # unit tests, no browser required
427
427
  npm run test:integration # requires camoufox (Xvfb only for the opt-in virtual-display tests)
428
428
  npm run type-check # TypeScript strict mode (src)
429
429
  npm run type-check:tests # TypeScript strict mode (tests)
430
+ npm run type-check:native # SAME checks on the TS7 native compiler (pinned; ~9x faster)
431
+ npm run type-check:native:tests # TS7 native check, test project
430
432
  npm run lint # ESLint
431
433
  npm run deps:check # architectural rule enforcement
432
434
  npm run build:worker # bundle the browser worker (required before integration tests / publish)
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,79 @@ 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.6.7] - 2026-08-31
9
+
10
+ Two threads from the issue tracker collide here: #10 ("Installation via NPM fails")
11
+ turned out to be an install-time sharp build failure we could — and now did — design
12
+ away, while the skew guard shipped across 1.5.x–1.6.6 rested on a premise about pi's
13
+ module loading that is not how pi works.
14
+
15
+ ### Fixed
16
+
17
+ - **`pi install` no longer fails outright on machines where sharp's native image chain cannot install (#10).** The failure chain, reproduced and pinned: `@huggingface/transformers` (the local-embeddings backend) declared sharp `^0.34.5` as a regular dependency; when npm skipped sharp's platform binary (`@img/sharp-*` optionals — the npm/cli#4828 pruning family on existing trees for npm 10.3.0–11.2.x, or any interrupted install, which the re-resolve then will NOT heal because skipped optionals stay inert), sharp's install script fell back to a source build that itself is broken (node-addon-api is only a devDependency), and the whole install died with `npm error code 1` — the extension never arrived. `@huggingface/transformers` is now an **optional dependency** (the same pattern `@lancedb/lancedb` already uses for it): npm drops a failing optional subtree with a warning and the install succeeds; research, scraping, and the CLI all work without embeddings. The absence is a first-class state at runtime too: `getTransformers()` reclassifies the missing-module rejection as a typed `TRANSFORMERS_UNAVAILABLE` error carrying remediation (`src/knowledge/transformers-loader.ts`), `isNativeStackUnavailableError()` recognizes it (`src/knowledge/embedder-utils.ts`), and the knowledge store therefore memoizes DISABLED on first touch instead of re-running the full init + 5-attempt backoff storm (~15s) on every store use. A transformers-INTERNAL failure (its own onnxruntime import, a corrupt install) still routes through the pre-existing onnxruntime/lancedb arms — a broken install of a PRESENT package is not "the optional was skipped". Retry storms, probe behavior, and startup were verified: the WebGPU probe subprocess already treats a missing module as "no WebGPU" and is unreachable before the env init anyway.
18
+
19
+ ### Removed
20
+
21
+ - **The pi-ai/pi-coding-agent "skew guard" is gone** (`src/core/pi-ai-skew.ts`; shipped in 1.6.5 as fail-fast, reworked in 1.6.6 to walk node_modules ancestors). The premise does not survive contact with pi's loader: pi resolves EVERY `@earendil-works/*` and `typebox` specifier in extension code to the HOST's own copies via jiti `virtualModules`/`alias` (verified in the 0.84.2 and 0.84.4 loader source — coverage includes the exact four specifiers this package imports), so the on-disk copies the guard compared are never loaded in-host. Under the host the guard measured dependencies nobody executes; its FATAL 'stale' refusal would have blocked a healthy extension after every host upgrade the moment the managed tree's inert copies lagged — and the 1.6.6 ancestor-walk made that state reachable on the primary `pi install npm:` layout (pi's own install code uses `--legacy-peer-deps` precisely because it expects extensions to declare these packages as peers, not dependencies). Host-version compatibility stays where it belongs: `checkPiCompatibility()` against the running host's VERSION — refuse below the 0.84.0 floor, warn once above the tested line — which is now the only startup check. The standalone CLI/SDK never ran the guard (it lived solely in the extension entry), so removal changes nothing there. What actually caused the original 2026-08-30 named-export crash remains unproven — in-host specifier mixing should have been impossible under 0.84.2's alias table — and is recorded here honestly rather than preserved as a false safety net.
22
+
23
+ ### Verified
24
+
25
+ - 2993 unit tests over 236 files (11 new: the transformers-absent typed error and its classification — ESM and CJS not-found forms, transformers-internal failures passing through untouched, unrelated-package precision, and the DISABLED-memoization handoff to `isNativeStackUnavailableError`), plus lint, all four type-checks (TS6 and TS7 native, src and tests), dependency-cruiser, the audit gate, the full integration suite (57 passed / 8 skipped parallel, 103 serial), and the tarball manifest gate (235 files — one fewer with the deleted guard module). CLI boot verified end-to-end with the entire transformers/sharp/@img subtree physically absent from the install tree.
26
+
27
+ ## [1.6.6] - 2026-08-31
28
+
29
+ Issue #9 reported deep-research runs stalling for minutes inside planning salvage
30
+ and then silently degrading to a single researcher. Three of its four claims were
31
+ real; the fourth (zero prompt-cache reads) turned out to be the provider's route
32
+ reporting no cached tokens — with a metric doc that wrongly implied otherwise.
33
+
34
+ ### Fixed
35
+
36
+ - **The skew guard could not see the copies of the PRIMARY install layout, and false-warned on every healthy startup there.** `pi install npm:` installs into one flat managed npm tree (`~/.pi/agent/npm/`, measured): the extension sits at `node_modules/@lincoln504/pi-research` with NO nested `node_modules`, and its `@earendil-works/*` dependencies hoist to the managed root. The guard's nested-only lookup therefore missed both copies on every `pi install` — classifying a healthy install as 'incomplete' ("cannot resolve pi-ai") on every startup — and its remedy (`cd <package> && npm install`) would have created a rogue divergent nested install inside the managed tree. Worse, the guard's protection never actually covered that layout: when the host advances and the managed tree's hoisted copies go stale (the 2026-08-30 crash shape), the lookup still found nothing and stayed at the soft warn. Copy resolution now walks ancestors exactly as Node's bare-specifier resolution binds them — nested copies win when present (git installs clone + `npm install` inside the checkout; the dev repo has its own), otherwise the hoisting tree is found and classified — so a healthy flat install is silent 'ok', and a stale managed tree is the FATAL refusal with a remedy that `cd`s into the tree that OWNS the stale copy (`~/.pi/agent/npm`), not the package dir (`src/core/pi-ai-skew.ts`). Five fixture suites pin every layout: flat-managed, nested, half-hoisted incident shape, truly-absent, and stale-flat remedy targeting.
37
+ - **The release record claimed a 1.6.6 release that never happened, and the docs had drifted against it three ways.** Nothing since v1.6.5 is published (npm's latest is 1.6.5; no v1.6.6 tag or release run exists), yet this file carried both a dated '[1.6.6] - 2026-08-26' entry asserting a shipped release and a separate '[1.6.7] - Unreleased' entry — while package.json and SKILL.md sit at 1.6.6, the version the release gates enforce against the next tag. The two halves are now one '[1.6.6] - Unreleased' section, and the 'pre-1.6.7' cross-references in docs/CONFIGURATION.md and .env.example point at [1.6.6]. ARCHITECTURE.md still taught the npm 12 script-approval flow (`--allow-scripts=better-sqlite3` / `approve-scripts`+`rebuild`) as what made the camoufox 0.12 refresh shippable and said better-sqlite3 13 "dropped prebuilt binaries" — both superseded by the measured 13.0.3 truth (prebuilds/ for all eight platform/arch pairs ship in-tarball; nothing needs approving) that the same day's commits put into the README/SDK/skill docs; the paragraph now records that mechanism, and its typebox anchor covers the whole 0.84.x line (0.84.4 pins the same 1.3.7). CONFIGURATION.md's `PLAYWRIGHT_BROWSERS_PATH` row still carried the pre-0.12 camoufox caveat ("moves only where pi-research looks, not where the download lands") two lines under the corrected `CAMOUFOX_INSTALL_DIR` row — on 0.12.0+ the alias is exported AS `CAMOUFOX_INSTALL_DIR` and relocates the download too — and .env.example gains the `CAMOUFOX_INSTALL_DIR` entry its "exhaustive env-var reference" section was missing. The README states the versioning policy outright: pi-research typically follows the latest pi version and is not actively backwards compatible. Also pinned here: the cancellation classifier's "This operation was aborted" alternation, recorded as test-pinned since the re-audit but covered by no test until now (`test/unit/utils/cancellation.test.ts`).
38
+ - **Every `npm run test:unit` run leaked its per-worker throwaway HOME into /tmp forever** — 4,990 `pi-research-unit-home-*` directories had accumulated on one dev machine. The unit-env setup now removes its own directory when the worker exits and opportunistically reclaims leaks older than 24h, without touching dirs owned by concurrent runs.
39
+ - **The npm 12 install guidance was wrong for the current dependency set, and the CI Windows legs failed on the same discovery.** Measured on npm 12.0.2 (sandboxed installs of the real tarball under a supported Node): better-sqlite3 13.0.3 bundles `prebuilds/` for all eight platform/arch pairs inside its tarball and loads them at runtime via node-gyp-build — with every install script skipped, the package installs cleanly and works, and the browser self-provisions on first use. The approval dance taught by the README/SDK/AGENT-SKILL npm-12 notes (and by the status/search failure remedies) described better-sqlite3 12's download-at-install model and was replaced with the measured truth on every surface; the thread-worker pin rationale now records the real 13.x mechanism. Separately, GitHub's windows-latest image moved to Visual Studio 2026 (version 18), which npm ≤11's node-gyp 11.2 cannot detect (`unknown version "undefined"`; GYP_MSVS_VERSION does not help — the crash precedes that lookup), so `npm ci` failed at better-sqlite3's rebuild on every Windows leg; CI now runs npm 12 (node-gyp 13, VS2026-aware) on every job that installs dependencies, and provisions the integration browsers through an explicit setup step because npm ≥11.19 no longer runs the postinstall that used to do it.
40
+ - **A four-lane re-audit of this release's own diff found seven residual defects; all are fixed here.** The worst: the pi-ai skew guard's incomplete branch (one nested SDK copy missing) warned instead of refusing to start when the copy that IS present lags the host — exactly the half-hoisted layout that caused the 2026-08-30 crash (`src/core/pi-ai-skew.ts`). Next: `raceWithSignal`'s already-aborted fast path left the eagerly-started op unhandled, so a probe rejecting after abort fired an unhandledRejection (`src/utils/cancellation.ts`); `getResearcherFailureReasons` still created-on-read, leaking a session state for ended/never-created sessions (`src/orchestration/session-state.ts`); `isHeadlessObserverBag` covered 8 of 24 observer methods, so a full observer with `onProgress` plus one unlisted typed method was demoted to a bag and lost every typed callback (`src/orchestration/headless-observer.ts`); the store's alternation detector flagged a deliberate second model change (A→B→C) as thrash — it now tracks two migrations and fires only on a RETURN (A→B→A) (`src/knowledge/store.ts`); the scrape tool's schema advertised `maxItems: 20` with only 6 fetched per call — now stated in the schema itself (`src/tools/scrape.ts`); and the cancellation classifier missed the canonical DOM form "This operation was aborted" (`src/utils/cancellation.ts`). Ten new tests pin all of it.
41
+ - **The browser-fetch bounds were still sized for the ~100MB camoufox 0.10 binary.** The 0.12 refresh's browser measures ~500MB, and camoufox-js deletes the install directory BEFORE downloading with no resume — so a fetch killed by the old 15-minute bound (or by any mid-download failure on a slow link) leaves NOTHING, which is exactly how this release's dev machine lost its working browser. Postinstall and the runtime ensure path now allow 45 minutes (a working link down to ~180 KB/s), and the stale-lock/peer-wait windows scale with the fetch so a live slow fetcher is never treated as crashed or duplicated; the "~100MB" messaging is corrected everywhere (`scripts/setup.cjs`, `src/infrastructure/browser/ensure-browser.ts`).
42
+
43
+ ### Fixed
44
+
45
+ - **A stale extension `node_modules` crashed every LLM call with a misleading "Provider error".** When the host pi advanced past the `@earendil-works/*` copies this extension resolves (host 0.84.4 vs extension pi-ai 0.84.2), mixed-version module graphs killed researchers and knowledge triage/extraction at provider load with ESM named-export errors (`clampThinkingBudgetToAnswerRoom`, first exported in pi-ai 0.84.3). The lockfile now resolves the SDK trio at 0.84.4, and a new load-time guard (`checkPiAiSkew`) walks the host tree and the extension's resolved copies and REFUSES TO START with the exact remediation instead of five silent researcher deaths (`src/core/pi-ai-skew.ts`).
46
+ - **The packaged CLI crashed on the first PDF.** `resolveWorkerPath()` used bare `__dirname`, which is undefined in the esbuild `format: 'esm'` bundle (`dist/cli.mjs`) and in jiti-loaded ESM: the ReferenceError fired BEFORE any try/catch, so every PDF extraction failed in built installs (dev worked — tsx/vitest shim `__dirname`; the unit default of `PI_RESEARCH_PDF_WORKER=off` also short-circuited before the crash). Worker path resolution now derives from `fileURLToPath(import.meta.url)` exactly like the browser worker pool, pinned by a source-invariant regression test, and the built bundle was verified end-to-end with a real PDF round-trip (`src/web-research/pdf-extraction.ts`).
47
+ - **A wedged PDF worker could be terminated twice, siblings hung out to their own timeouts, and a stale exit event could kill a fresh worker.** The 30s timeout terminated the module-global worker and nulled it, so sibling requests never got the fail-fast `failAllPending` (each hung its full 30s), a `postMessage` sync-throw left its timer armed to terminate an unrelated future parse, and an old worker's late `exit` event (arriving after a respawn) matched the global and failed the NEW worker's requests. The timeout now terminates the worker instance captured in its own `PendingRequest`, fails all remaining pending requests unconditionally, clears timer+entry on sync postMessage throw, and crash/exit handlers act only when the dying worker is the current one (`src/web-research/pdf-extraction.ts`).
48
+ - **A large PDF blocked the main process's event loop for seconds.** pdf-oxide-wasm's parse is irreducibly synchronous, so it now runs in a dedicated worker_threads worker (zero-copy transfer, 30s terminate-and-retry-in-process) with byte-identical error strings, metrics, and classification; a missing bundle or `PI_RESEARCH_PDF_WORKER=off` keeps today's in-process behavior (`src/web-research/pdf-extraction.ts`).
49
+
50
+ ### Fixed
51
+
52
+ - **A poisoned embedding-model cache retried into the same wall instead of self-healing.** transformers.js exports embedding models as a small graph stub referencing external weights; an interrupted download leaves a cache that passes every `isModelCached` check (the weights file was only validated when present) and then fails to load with a local-file-missing error that nothing classified — so no purge, no re-download, and knowledge-store init burned its whole retry budget on every open. The cache check now requires referenced external weights to be present, and the load failure is classified as recoverable (purge + one remote re-download).
53
+ - **A full-surface audit (six parallel read-only lanes over core, knowledge, orchestration, web-research, surface, and release engineering) landed twelve remediation commits:** the PDF worker's ESM `__dirname` crash in built installs (every PDF failed) plus its timeout/teardown races; the structured-output empty-response error string silently bypassing the retry→degrade ladder (a run-aborting regression caught by the audit's cross-suite seam analysis); abort-unaware health checks that could hang a cancel for the full 105s probe; create-on-read leaks and hybrid-observer demotion in session-state; the SKILL.md version drift that would have failed the release gates; five stale post-refresh pin rationales; honest tool schemas (search maxItems, run-cap prompt, phantom grep tool); model-alternation detection for the shared knowledge store; chunker O(n²) fence rescans; and run-level metrics for capacity-refused/cancelled runs.
54
+
55
+ ### Changed
56
+
57
+ - **Planner, round-2 router, and knowledge synthesis now offer schema-constrained submit tools** (pi-ai `constrainedSampling`, `strict: 'prefer'`): schema-exact JSON where the provider supports it, plain tool calls everywhere else, and text answers still flow through the existing parse/repair pipeline unchanged (`src/core/llm/llm-utils.ts`).
58
+ - Runtime deps refreshed: camoufox-js 0.12, impit 0.14.4; apache-arrow stays lockstep 21.1.0 (lancedb 0.37.1 cannot read 21.2.0 IPC) and playwright-core stays 1.60.0 (camoufox 0.12 peer-caps <1.61.0).
59
+ - CI typechecks with the TypeScript 7 native compiler alongside TS6 (~9x faster, 3.7s vs ~35s here); TS6 remains the typescript-eslint compiler until its TS 7.1 API support lands.
60
+
61
+ ### Verified
62
+
63
+ - 3000 unit tests over 236 files (16 new: the half-hoisted skew fatal, the raceWithSignal post-abort rejection swallow, peek-vs-create read discipline, the full observer-marker surface, return-only alternation thrash, capacity_refused/cancelled run metrics, the DOM-abort classifier form, and the five skew-guard install-layout walks), plus lint, all four type-checks (TS6 and TS7 native, src and tests), dependency-cruiser, the full integration suite, and the tarball manifest gate.
64
+ - The resumable browser install was exercised end-to-end on the machine that lost its binary: the ~500MB release asset fetched with byte-range resume across connection drops, installed through camoufox-js's own extract/version logic (`camoufox-js fetch` then reports "up to date"), and `pi-research status` reports `ready` with the browser detected.
65
+
66
+ ### Fixed
67
+
68
+ - **Agentic JSON salvage could hang for two full LLM timeouts before the caller's fallback ran.** Each of `repairJsonWithLlm`'s two attempts was granted the entire `PI_RESEARCH_LLM_TIMEOUT_MS` budget (5 min default), and a timed-out attempt was still followed by a second — with a strictly larger prompt (validation errors are appended) and the same full budget, a near-certain repeat of the timeout observed in the wild as salvage hanging until manual abort. The whole salvage pass now shares ONE LLM-timeout budget: each attempt gets the remaining time, an attempt that cannot finish inside what remains (floor 30s — a repair call must re-upload the malformed payload and re-emit the full JSON) is skipped with a log line instead of launched into a guaranteed timeout, and a timed-out attempt is never retried, because unlike a validation failure the retry cannot plausibly succeed — it only delays the caller's fallback (`src/core/llm/agentic-repair.ts`).
69
+ - **A malformed plan JSON was diagnosed with its best evidence discarded.** Both planning call sites caught the parse error and logged a generic "JSON malformed" warning — dropping the thrown parse/validation detail — and nothing anywhere recorded the response's `stopReason`, so a plan truncated at the output-token cap (`stopReason 'length'`, the likeliest cause with a verbose model) was indistinguishable in the log from schema-incompatible output, which is exactly the ambiguity issue #9 hit ("no clear truncation/max-tokens reason captured"). The parse error, `stopReason`, and a tail preview of the response (truncation leaves the JSON dangling at the END — a head preview cannot show it) are now logged at both sites, and every planning/lead call warns when it hit its output-token cap (`src/core/planning-service.ts`).
70
+ - **A failed planning pass silently degraded a deep query to one researcher.** `buildFallbackCoordinatorPlan` returns a structurally ordinary plan, so when the model's plan JSON was unusable the TUI showed the fallback as a normal 'ready' coordinator and SDK consumers saw an ordinary `planning_success` — no surface said the user's deep-research request had been quietly downgraded to a single researcher with boilerplate queries. Fallback plans now carry `fallback: true` (never requested from the model; optional in the schema so persisted plans still validate), the TUI coordinator line reads 'fallback — planning failed', and the marker rides the plan object into every `onPlanningSuccess` consumer (`src/core/interfaces/research-plan-types.ts`, `src/core/planning-utils.ts`, `src/observers/research-observer-impl.ts`).
71
+ - **Unit tests read the developer's real `~/.pi/research/config.env`.** `getConfig()` layers that file under process env, so unit-test behavior silently varied by machine: on a dev box carrying `PI_RESEARCH_LLM_TIMEOUT_MS=900000`, `getLlmTimeoutMs()` answered 900000 in tests while CI (no config.env) saw the 300000 default — found when a budget-sensitive salvage test passed on CI and failed locally. The unit setup now redirects `HOME`/`USERPROFILE` onto a throwaway directory (os.homedir() reads HOME on POSIX, USERPROFILE on Windows, and the unit matrix runs all three OSes), isolating config.env, the state dir, and knowledge-db discovery the same way the log path was already isolated (`test/setup/unit-env.ts`).
72
+
73
+ ### Changed
74
+
75
+ - **The cache-metrics doc no longer implies "counter absent" distinguishes an uncaching provider.** For the openai-completions family (including OpenRouter), pi-ai's usage normalization always emits `cacheRead`/`cacheWrite`, defaulting missing upstream fields to 0 — so a route that does no prompt caching shows `llm_cache_read_tokens_total` stuck at 0, not omitted, and the 0-vs-absent distinction only holds for APIs that omit the fields outright. Issue #9's "no prompt-cache benefit during planning" is this: the model's OpenRouter route reports no cached tokens, while the run's prompts were already cache-shaped (round-invariant system prompts, run-varying content appended at the tail, session-affinity id forwarded). The doc now says how to read a stuck-at-zero counter (`src/utils/llm-usage.ts`).
76
+
77
+ ### Verified
78
+
79
+ - 2943 unit tests over 233 files (5 new: salvage-timeout non-retry, below-floor skip, validation-retry regression guard, fallback marker, schema round-trip of the marker), lint, and both type-checks green — with the suite running under the isolated HOME, i.e. against CI-equivalent config for the first time.
80
+
8
81
  ## [1.6.5] - 2026-08-26
9
82
 
10
83
  1.6.4 verified its commands and then recommended the wrong one first: the user-level
@@ -152,12 +152,13 @@ Diagnostics & platform
152
152
  | `PI_RESEARCH_STATE_DIR` | `~/.pi/research/state` | Override the state directory (active sessions, browser status, project registry). |
153
153
  | `PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS` | `100` | TUI refresh debounce (0–1000 ms). |
154
154
  | `PI_RESEARCH_SKIP_HEALTHCHECK` | _(unset)_ | Set `1`/`true` to skip the pre-flight browser/embedding health check and rely on per-task timeouts. **Depth 0 (quick) only** — depth 1-3 runs have no such pre-flight check, so this has no effect on them. |
155
+ | `PI_RESEARCH_PDF_WORKER` | _(unset)_ | Set `off` to force PDF parsing onto the main thread (pre-1.6.6 behavior), bypassing the worker-threads offload. Kill-switch for worker or bundling issues; the default runs the parse in a worker when its bundle is present. |
155
156
  | `PI_RESEARCH_USE_XVFB` | _(unset)_ | Linux only. Bare-TTY runs are true-headless and need no X server; set `true` to opt into a virtual framebuffer (`sudo apt install xvfb`). |
156
157
  | `PI_RESEARCH_SKILL_DIR` | _(auto)_ | Override the bundled research-skill source directory used by the skill installer. |
157
158
  | `PI_RESEARCH_PURGE_BROWSERS` | _(unset)_ | Read by the bundled `scripts/cleanup.cjs`: set `1` to also delete the shared camoufox browser cache (kept by default because other installs may use it). Note npm ≥7 does not run `preuninstall`, so that script does not fire on `npm uninstall` — see [AGENT-SKILL.md](AGENT-SKILL.md). |
158
159
  | `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` | _(unset)_ | Set `1` during `npm install` to skip the camoufox browser download (fetched lazily on first use instead; standard Playwright convention). |
159
- | `CAMOUFOX_INSTALL_DIR` | _(user cache)_ | camoufox-js's own variable, and the only one that can relocate the browser. Sets where pi-research looks the binary up, and is exported to the postinstall fetch and to browser workers. **Not effective on the currently pinned camoufox-js (0.10.x), which hardcodes its cache directory and honours no variable** — so relocating the browser is unsupported until that pin can advance (see ARCHITECTURE.md). |
160
- | `PLAYWRIGHT_BROWSERS_PATH` | _(user cache)_ | Alias for the above, accepted for compatibility and exported onward as `CAMOUFOX_INSTALL_DIR`. Same caveat: on the pinned camoufox-js it moves only where pi-research looks, not where the download lands, so setting it reports the browser missing from the custom path after a successful install. |
160
+ | `CAMOUFOX_INSTALL_DIR` | _(user cache)_ | camoufox-js's own variable, and the only one that can relocate the browser. Sets where pi-research looks the binary up, and is exported to the postinstall fetch and to browser workers. Effective on the pinned camoufox-js 0.12.0+, which honours it again; on older pins (<0.12, which hardcoded their cache directory) it only relocated lookup — see ARCHITECTURE.md. |
161
+ | `PLAYWRIGHT_BROWSERS_PATH` | _(user cache)_ | Alias for the above, accepted for compatibility and exported onward as `CAMOUFOX_INSTALL_DIR` — on camoufox-js 0.12.0+ it relocates the download itself, exactly like `CAMOUFOX_INSTALL_DIR`. |
161
162
  | `XDG_CACHE_HOME` | `~/.cache` | Standard XDG variable. When set, every `~/.cache/pi-research/...` path below is rooted at `$XDG_CACHE_HOME/pi-research/...` instead. |
162
163
  | `PI_RESEARCH_BIN` (alias `PI_RESEARCH_PATH`) | _(auto)_ | Agent-skill launcher only: explicit path to the pi-research engine binary when auto-resolution (PATH → local install → npx) should be bypassed. See [agent-skill/pi-research/references/configuration.md](../agent-skill/pi-research/references/configuration.md). |
163
164
  | `PLAYWRIGHT_INSTALL_DEPS` | _(unset)_ | Linux only. Set `true` during `npm install` to also install system libraries via `npx playwright install-deps` (same as `npm run install:system-deps`). |
package/docs/SDK.md CHANGED
@@ -13,18 +13,13 @@ run the `pi` extension, which keeps its own private copy your scripts can't impo
13
13
  npm install @lincoln504/pi-research
14
14
  ```
15
15
 
16
- On npm 12 or newer, npm skips dependency install scripts by default and a project-scoped
17
- install rejects the `--allow-scripts` flag — record the one script search depends on in
18
- your project instead, then run it:
19
-
20
- ```bash
21
- npm approve-scripts better-sqlite3 # pins the approval in your allowScripts field
22
- npm rebuild better-sqlite3
23
- ```
24
-
25
- Without that script every search fails with a missing-module error, and rerunning
26
- `npm install` does not run it — `npm rebuild` does. The same pair repairs an install
27
- that already ran blocked.
16
+ On npm ≥11.19 (and npm 12), dependency install scripts are skipped by default. Nothing
17
+ here needs them: better-sqlite3 13 carries prebuilt bindings for every supported
18
+ platform and loads them at runtime, and the stealth browser self-provisions on its
19
+ first use. (The `npm approve-scripts better-sqlite3` + `npm rebuild` pair documented by
20
+ earlier releases repaired better-sqlite3 12, which downloaded its binding from an
21
+ install script — 13 ships the binding instead, and on npm 12.0.2 an approval does not
22
+ make a skipped script run anyway.)
28
23
 
29
24
  Then pick the model: pass `model` to `initResearchSDK`, or set `PI_RESEARCH_MODEL`
30
25
  (env or `~/.pi/research/config.env`). The SDK never follows the model selected
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "Web research for your agents with smart and safe tooling + knowledge store",
5
5
  "type": "module",
6
6
  "private": false,
@@ -18,6 +18,7 @@
18
18
  "src/",
19
19
  "dist/cli.mjs",
20
20
  "dist/thread-worker.mjs",
21
+ "dist/pdf-extract-worker.mjs",
21
22
  "dist/webgpu-probe.mjs",
22
23
  "dist/prompts/",
23
24
  "agent-skill/",
@@ -79,6 +80,8 @@
79
80
  "lint:fix": "npx eslint --config config/tooling/eslint.config.js src/ --fix",
80
81
  "type-check": "tsc --noEmit",
81
82
  "type-check:tests": "tsc --noEmit --project config/tooling/tsconfig.test.json",
83
+ "type-check:native": "npx -y -p typescript@7.0.2 tsc --noEmit -p tsconfig.json",
84
+ "type-check:native:tests": "npx -y -p typescript@7.0.2 tsc --noEmit --project config/tooling/tsconfig.test.json",
82
85
  "build:worker": "node scripts/build.cjs worker",
83
86
  "test": "npm run test:unit",
84
87
  "test:unit": "vitest run --config config/tooling/vitest.config.unit.ts",
@@ -114,14 +117,13 @@
114
117
  "@earendil-works/pi-ai": ">=0.84.0 <1",
115
118
  "@earendil-works/pi-coding-agent": ">=0.84.0 <1",
116
119
  "@earendil-works/pi-tui": ">=0.84.0 <1",
117
- "@huggingface/transformers": "4.2.0",
118
- "@kreuzberg/html-to-markdown-node": "^3.5.5",
120
+ "@kreuzberg/html-to-markdown-node": "^3.7.2",
119
121
  "@lancedb/lancedb": "0.37.1",
120
122
  "apache-arrow": "21.1.0",
121
123
  "bgutils-js": "^4.0.3",
122
- "camoufox-js": "^0.10.2",
124
+ "camoufox-js": "^0.12.0",
123
125
  "esbuild": "^0.28.1",
124
- "impit": "0.13.0",
126
+ "impit": "0.14.4",
125
127
  "jsdom": "^29.1.1",
126
128
  "node-html-markdown": "^2.0.0",
127
129
  "pdf-oxide-wasm": "^0.3.75",
@@ -132,6 +134,7 @@
132
134
  "youtubei.js": "^18.0.0"
133
135
  },
134
136
  "optionalDependencies": {
137
+ "@huggingface/transformers": "4.2.0",
135
138
  "webgpu": "^0.4.0"
136
139
  },
137
140
  "overrides": {
package/scripts/build.cjs CHANGED
@@ -71,6 +71,18 @@ async function ensureWorkerBuilt() {
71
71
  }
72
72
  }
73
73
 
74
+ // Build the PDF extraction worker bundle. Same lifecycle rules as the browser
75
+ // thread-worker: a pre-compiled .mjs next to its source, `packages: 'external'`
76
+ // so it resolves pdf-oxide-wasm from the installed node_modules, and never
77
+ // committed to git (regenerated by prepare).
78
+ function buildPdfWorker() {
79
+ return esbuild.build({
80
+ ...COMMON,
81
+ entryPoints: [p('src', 'web-research', 'pdf-extract-worker.ts')],
82
+ outfile: p('src', 'web-research', 'pdf-extract-worker.mjs'),
83
+ });
84
+ }
85
+
74
86
  // Copy the built worker bundle into dist/. dist/thread-worker.mjs is resolved by
75
87
  // worker-pool-manager.ts at runtime, so the bundled cli — which spins up the
76
88
  // browser pool — needs it next to its bundle.
@@ -99,10 +111,32 @@ function copyPrompts() {
99
111
  }
100
112
  }
101
113
 
114
+ async function ensurePdfWorkerBuilt() {
115
+ const src = p('src', 'web-research', 'pdf-extract-worker.ts');
116
+ const out = p('src', 'web-research', 'pdf-extract-worker.mjs');
117
+ const stale =
118
+ !fs.existsSync(out) || fs.statSync(out).mtimeMs < fs.statSync(src).mtimeMs;
119
+ if (stale) {
120
+ await buildPdfWorker();
121
+ }
122
+ }
123
+
124
+ // Copy the PDF worker bundle into dist/ next to the cli bundle; pdf-extraction.ts
125
+ // resolves it relative to its own compiled location at runtime.
126
+ function copyPdfWorker() {
127
+ fs.mkdirSync(p('dist'), { recursive: true });
128
+ fs.copyFileSync(
129
+ p('src', 'web-research', 'pdf-extract-worker.mjs'),
130
+ p('dist', 'pdf-extract-worker.mjs'),
131
+ );
132
+ }
133
+
102
134
  const TARGETS = {
103
135
  worker: () => buildWorker(),
136
+ pdfworker: () => buildPdfWorker(),
104
137
  cli: async () => {
105
138
  await ensureWorkerBuilt();
139
+ await ensurePdfWorkerBuilt();
106
140
  await esbuild.build({
107
141
  ...COMMON,
108
142
  entryPoints: [p('src', 'cli.ts')],
@@ -110,6 +144,7 @@ const TARGETS = {
110
144
  banner: { js: SHEBANG },
111
145
  });
112
146
  copyThreadWorker();
147
+ copyPdfWorker();
113
148
  copyWebGpuProbe();
114
149
  copyPrompts();
115
150
  },
package/scripts/setup.cjs CHANGED
@@ -53,12 +53,11 @@ function resolveCamoufoxBin() {
53
53
  * agree or install and lookup land in different directories.
54
54
  *
55
55
  * CAMOUFOX_INSTALL_DIR first: it is camoufox-js's own variable and the only one
56
- * that can relocate the install (camoufox-js <0.12.0 hardcodes
57
- * userCacheDir("camoufox") and honours nothing). PLAYWRIGHT_BROWSERS_PATH is
58
- * kept as the documented alias and is exported BELOW as CAMOUFOX_INSTALL_DIR.
59
- * NOTE: inert on the pinned camoufox-js 0.10.x, which reads neither — this is
60
- * forward-compatible plumbing, not a live fix. See src/infrastructure/browser/
61
- * config.ts and thread-worker-browser.ts.
56
+ * that can relocate the install (camoufox-js <0.12.0 hardcoded
57
+ * userCacheDir("camoufox") and honoured nothing — inert there; effective on the
58
+ * 0.12.0+ refresh). PLAYWRIGHT_BROWSERS_PATH is kept as the documented alias
59
+ * and is exported BELOW as CAMOUFOX_INSTALL_DIR. See
60
+ * src/infrastructure/browser/config.ts and thread-worker-browser.ts.
62
61
  */
63
62
  function customCamoufoxDir() {
64
63
  return process.env.CAMOUFOX_INSTALL_DIR || process.env.PLAYWRIGHT_BROWSERS_PATH || null;
@@ -126,10 +125,13 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
126
125
  // shell metacharacters would execute them at install time. Windows still
127
126
  // needs a shell to run the `.cmd` shim, and there the path is quoted; the
128
127
  // runtime twin in ensure-browser.ts uses the same shape.
129
- // Bound the ~100MB download so a stalled/interrupted network fails fast
130
- // instead of hanging `npm install` forever (the catch below exits 0, and the
131
- // browser is re-fetchable manually). 15 min is ample even on slow links.
132
- const spawnOpts = { stdio: 'inherit', env, timeout: 15 * 60 * 1000 };
128
+ // Bound the ~500MB camoufox-js 0.12 browser download (was ~100MB on the
129
+ // 0.10 pin — the old 15-minute bound could kill a download that would
130
+ // have succeeded on a slow link) so a stalled/interrupted network fails
131
+ // eventually instead of hanging `npm install` forever (the catch below
132
+ // exits 0, and the browser is re-fetchable manually). 45 min covers a
133
+ // working link down to ~180 KB/s; a dead link aborts on its own sooner.
134
+ const spawnOpts = { stdio: 'inherit', env, timeout: 45 * 60 * 1000 };
133
135
  const res = bin
134
136
  ? (isWindows
135
137
  ? spawnSync(`"${bin}"`, ['fetch'], { ...spawnOpts, shell: true })
@@ -92,11 +92,17 @@ export function createCleanupFunction(
92
92
  let hasUI = false;
93
93
  try { hasUI = ctx.hasUI; } catch { /* ctx stale (session closed mid-run) → treat as no UI */ }
94
94
  if (hasUI) {
95
- ctx.ui.setWidget(masterWidgetId, undefined);
96
- const tuiUI = ctx.ui as { setWorkingVisible?: (visible: boolean) => void };
97
- if (typeof tuiUI?.setWorkingVisible === 'function') {
98
- tuiUI.setWorkingVisible(true);
99
- }
95
+ // Audit L3: the ctx can go stale BETWEEN the hasUI probe and these calls
96
+ // (session replacement races the probe) — the same throw this block exists
97
+ // to suppress would then reject the whole cleanup promise. Guard the calls
98
+ // identically to the probe.
99
+ try {
100
+ ctx.ui.setWidget(masterWidgetId, undefined);
101
+ const tuiUI = ctx.ui as { setWorkingVisible?: (visible: boolean) => void };
102
+ if (typeof tuiUI?.setWorkingVisible === 'function') {
103
+ tuiUI.setWorkingVisible(true);
104
+ }
105
+ } catch { /* ctx went stale after the probe → UI is gone; nothing to clean */ }
100
106
  }
101
107
  } else {
102
108
  refreshAllSessions(piSessionId);
package/src/cli.ts CHANGED
@@ -828,13 +828,15 @@ async function cmdStatus(json?: boolean): Promise<number> {
828
828
  installed: browserInstalled,
829
829
  nativeDepsOk: nativeDeps.ok,
830
830
  nativeDepsError: nativeDeps.ok ? null : nativeDeps.error,
831
- // --allow-scripts is a package-name LIST, not a boolean: the bare flag is
832
- // accepted and silently allows nothing (measured on npm 12.0.2). It is
833
- // also global/one-off only — a project-scoped install rejects it with
834
- // EALLOWSCRIPTS, where `npm approve-scripts` is the supported route.
831
+ // better-sqlite3 13 ships prebuilds/ for all eight platform/arch pairs and
832
+ // loads them at runtime via node-gyp-build — no install script involved.
833
+ // On modern npm (≥11.19) the injected `node-gyp rebuild` that a
834
+ // binding.gyp-without-install-script triggers is skipped, so a missing
835
+ // binding here means an old/broken install, not an approval problem:
836
+ // point at the upgrade that fixes it.
835
837
  nativeDepsFix: nativeDeps.ok
836
838
  ? null
837
- : 'Reinstall allowing the dependency install script: npm install -g @lincoln504/pi-research --allow-scripts=better-sqlite3 (npm 12 blocks dependency install scripts by default). For a project-scoped install: npm approve-scripts better-sqlite3, then npm rebuild better-sqlite3.',
839
+ : 'Upgrade npm and reinstall: npm install -g npm@12, then reinstall @lincoln504/pi-research. better-sqlite3 13 ships prebuilt bindings (no script needed); older npm needlessly recompiles it and fails on machines without a C++ toolchain.',
838
840
  // Same remedy the health check prints, so both surfaces name one command.
839
841
  fix: browserInstalled ? null : 'npx camoufox-js fetch',
840
842
  note: browserInstalled
package/src/config.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Configuration Module
3
3
  *
4
- * Source of truth: .env file in the extension directory.
5
- * The /research-config TUI is a friendly editor for that file.
6
- * process.env values override the file (useful for CI / one-off overrides).
4
+ * Resolution order (lowest → highest precedence):
5
+ * built-in DEFAULTS
6
+ * < ~/.pi/research/config.env (user file; the /research-config TUI edits this)
7
+ * < ~/.pi/research/{iface}.env (per-interface overlay: cli.env, pi.env, sdk.env)
8
+ * < legacy extension-dir .env (deprecated; migrated on load)
9
+ * < project registry (~/.pi/research/projects.json, per-directory)
10
+ * < process.env (CI / one-off overrides)
11
+ * See loadEnvFiles()/createConfig() below for the implementation and the
12
+ * scope-demotion rules for project-registered keys read from ambient files.
7
13
  */
8
14
 
9
15
  import * as fs from 'node:fs';
@@ -168,6 +168,14 @@ export interface IKnowledgeStoreService extends IService {
168
168
  */
169
169
  export interface IWriterQueue extends IService {
170
170
  drain(): Promise<void>;
171
+ /**
172
+ * Fire-and-forget enqueue of a derived, re-derivable row (a link description
173
+ * or synthesis). RAM-ONLY by design: the queue is not journaled, so anything
174
+ * enqueued but not yet addDocuments-committed is LOST on crash or kill, and
175
+ * dispose() bounds its final drain (dropping the remainder on timeout). Never
176
+ * enqueue primary user data that cannot be re-produced by re-running the
177
+ * scrape/synthesis that produced it.
178
+ */
171
179
  enqueue(item: IngestionItem): void;
172
180
  }
173
181
 
@@ -84,5 +84,5 @@ export interface IResearchOrchestration extends IService {
84
84
  runSearchBurst(queries: string[], config: Config, signal?: AbortSignal, onProgress?: (links: number) => void, ctx?: any): Promise<QueryResultWithError[]>;
85
85
  storeLinkDescriptions(sessionId: string, round: number, researchId: string, config: Config, ctx?: any): Promise<void>;
86
86
  // Advisory only — logs infrastructure health; research never aborts on the result.
87
- checkHealth(round: number, researchId?: string, ctx?: any): Promise<void>;
87
+ checkHealth(round: number, researchId?: string, ctx?: any, signal?: AbortSignal): Promise<void>;
88
88
  }
@@ -44,6 +44,12 @@ export const ResearchPlanSchema = Type.Object({
44
44
  allQueries: Type.Optional(Type.Array(Type.String())),
45
45
  content: Type.Optional(Type.String()),
46
46
  title: Type.Optional(Type.String()),
47
+ // Set ONLY by buildFallbackCoordinatorPlan (never requested from the model): marks a
48
+ // plan the engine synthesized after the model's planning output was unusable, so a
49
+ // single-researcher fallback is distinguishable from a plan the model actually chose.
50
+ // Without it a deep query silently degrades to one researcher with no user-visible
51
+ // notice — the TUI/SDK see an ordinary plan (issue #9).
52
+ fallback: Type.Optional(Type.Boolean()),
47
53
  });
48
54
 
49
55
  /**