@lincoln504/pi-research 1.6.2 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,11 +63,10 @@ Three depth levels (normal, deep, ultra) set the team size and number of rounds.
63
63
  - Keeping a persistent and searchable knowledge store of findings, scoped globally or per project.
64
64
  - Building agent systems that find and read web content, or populating a dataset of web sources.
65
65
 
66
- ### Why pi-research
66
+ ### Why
67
67
 
68
68
  - **No quota and no monthly fee.** Most AI search providers cap free searches and then charge, with results served from their index on their servers. pi-research searches and scrapes locally through DuckDuckGo in a stealth browser.
69
69
  - **The index is local.** Every finding can be saved to a local [LanceDB](https://lancedb.com) knowledge store that seeds future runs, so repeat questions get faster and cheaper answers.
70
- - **Cited reports rather than raw dumps.** Each run returns one sourced Markdown report instead of raw web content pasted into the conversation. Context efficient and easy to verify.
71
70
  - **Read-only by design.** The research agent cannot run shell commands or write, edit, or delete anything — the right shape for an agent whose whole job is reading untrusted web pages. Prompt injection picked up mid-run has nothing to act on.
72
71
  - **Search a little or a lot.** Depth levels range from a quick pass to a large-scale investigation.
73
72
 
@@ -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.6.2", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.6.3", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
6
6
  allowed-tools: Bash(node:*)
7
7
  ---
8
8
 
package/dist/cli.mjs CHANGED
@@ -6831,7 +6831,7 @@ var init_file_lock_service = __esm({
6831
6831
  const errnoError = error;
6832
6832
  if (errnoError.code === "EEXIST") {
6833
6833
  contentionCount++;
6834
- let reclaimEligibleAt = null;
6834
+ let awaitingReclaim = false;
6835
6835
  try {
6836
6836
  const rawContent = await fs7.readFile(this.lockFilePath, "utf-8");
6837
6837
  const parsed = this._parseLockContent(rawContent);
@@ -6843,7 +6843,7 @@ var init_file_lock_service = __esm({
6843
6843
  }
6844
6844
  lastSeenUuid = lockUuid;
6845
6845
  lastSeenMtime = stats.mtimeMs;
6846
- reclaimEligibleAt = (parsed?.pid ?? null) === null ? stats.mtimeMs + this._unidentifiedStaleThreshold() : null;
6846
+ awaitingReclaim = this.lockTimeout > 0 && (parsed?.pid ?? null) === null && lockAge <= this._unidentifiedStaleThreshold();
6847
6847
  if (lockUuid === this.lockUuid) {
6848
6848
  try {
6849
6849
  await fs7.unlink(this.lockFilePath);
@@ -6880,7 +6880,6 @@ var init_file_lock_service = __esm({
6880
6880
  }
6881
6881
  const stalledFor = Date.now() - lastProgressAt;
6882
6882
  const waitedFor = Date.now() - startTime;
6883
- const awaitingReclaim = this.lockTimeout > 0 && reclaimEligibleAt !== null && Date.now() < reclaimEligibleAt;
6884
6883
  if (stalledFor >= this.lockTimeout && !awaitingReclaim || waitedFor >= this.acquireCeilingMs) {
6885
6884
  metrics.observe("state_lock_acquire_duration_ms", waitedFor, { status: "timeout" });
6886
6885
  metrics.increment("state_lock_acquire_total", 1, { status: "timeout" });
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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.3] - 2026-08-25
9
+
10
+ A unit test that had already been granted CI-jitter headroom twice went red a third
11
+ time — and this time the timing signature (rejection 10008ms into a 10000ms window)
12
+ identified a real race in the lock acquire loop rather than a slow runner.
13
+
14
+ ### Fixed
15
+
16
+ - **A waiter gave up on a torn lock at the exact moment its reclaim window opened.** The acquire loop's reclaim decision measures the lock's age at `fs.stat` time, but the stall-check exemption that keeps a waiter alive until that reclaim ("never give up on a lock we are about to be allowed to take") compared a fresh `Date.now()` against the window. For the few milliseconds between the stat and the stall check the two clocks could disagree, and a retry tick landing astride the window boundary satisfied neither: not yet reclaimable by its stat, no longer awaited by the clock. Since a torn, pid-less lock never shows progress, the long-expired stall bound then fired, and a wait that had correctly outlasted it was abandoned right as the recovery became permitted — in production, a knowledge-store init failing at the moment it could have recovered. Both judgments now derive from the same observation, so every tick that can read the lock either reclaims it or keeps waiting; the try-take exclusion and the absolute ceiling are unchanged (`src/infrastructure/file-lock-service.ts`).
17
+ - **The test's forensic comment blamed scheduler jitter for all three CI failures.** True for the first two (which failed at the stall bound); wrong for this one (which failed at the window boundary). The record now distinguishes the two mechanisms (`test/unit/infrastructure/file-lock-progress-aware-wait.test.ts`).
18
+ - **A browser-config test deleted the whole suite's shared log directory as its own cleanup.** `unit-env.ts` routes every unit worker's log into `<tmpdir>/pi-research-test/`; the profile-containment test nested its scratch path inside that directory and removed the shared parent recursively — destroying the live log out from under every parallel worker, and racing the lazy logger re-creating the file mid-removal, which is an `ENOTEMPTY` crash of the cleanup itself (seen on macos-latest CI on this release's first tag). The test now creates its own `mkdtemp` directory and removes only that (`test/unit/infrastructure/browser-config.test.ts`).
19
+
20
+ ### Verified
21
+
22
+ - 2938 unit tests over 233 files, lint, type-check, and the full CI matrix green on the fix commit — including the ubuntu-latest job that caught the failure.
23
+
8
24
  ## [1.6.2] - 2026-08-24
9
25
 
10
26
  An agent-skill run reported that every search had failed and concluded it had hit an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Web research for your agents with smart and safe tooling + knowledge store",
5
5
  "type": "module",
6
6
  "private": false,
@@ -694,12 +694,13 @@ export class FileLockService implements IService {
694
694
  const errnoError = error as NodeJS.ErrnoException;
695
695
  if (errnoError.code === 'EEXIST') {
696
696
  contentionCount++;
697
- // When the contended lock becomes eligible for reclaim, if this tick can
698
- // know it. Scoped per observation: a tick that could not read the lock
697
+ // Whether THIS tick judged the contended lock not-yet-eligible for the
698
+ // pid-less reclaim — i.e. we are waiting for a recovery we will be allowed
699
+ // to perform. Scoped per observation: a tick that could not read the lock
699
700
  // learned nothing, and an eligibility carried over from an earlier one is
700
701
  // no longer evidence of anything. See the stall check below for why the
701
702
  // stall bound must not fire before this moment.
702
- let reclaimEligibleAt: number | null = null;
703
+ let awaitingReclaim = false;
703
704
  // Read lock content to verify ownership and liveness before considering stale
704
705
  try {
705
706
  const rawContent = await fs.readFile(this.lockFilePath, 'utf-8');
@@ -720,17 +721,26 @@ export class FileLockService implements IService {
720
721
  // A lock with no readable pid is judged purely on age (see
721
722
  // _shouldReclaim), and its mtime never moves again — so it can never
722
723
  // show progress and the stall bound is guaranteed to expire first.
723
- // Record when it becomes reclaimable so the wait below can outlast the
724
- // stall bound rather than abandoning the recovery at 20s.
724
+ // Mark this tick as awaiting that reclaim so the wait below can outlast
725
+ // the stall bound rather than abandoning the recovery at 20s.
726
+ //
727
+ // Judged from the SAME lockAge observation _shouldReclaim will use, not
728
+ // from a fresh Date.now() at the stall check: the two clocks disagreed
729
+ // for the few ms between the stat and that check, and a tick landing
730
+ // astride the window boundary judged the lock not-yet-reclaimable here
731
+ // yet no-longer-awaited there — giving up at the exact moment the
732
+ // window opened (ubuntu CI, 2026-08-24: rejected 10008ms into a
733
+ // 10000ms window). The complement of _shouldReclaim's strict > means
734
+ // one of the two branches now holds on every readable tick.
725
735
  //
726
736
  // Deliberately NOT extended to a live owner: there the heartbeat is the
727
737
  // designed progress signal, its absence past lockTimeout is real evidence
728
738
  // of a wedge, and reporting that to the caller promptly beats waiting out
729
739
  // a two-to-four-minute live-owner threshold on a lock we may well never
730
740
  // be handed.
731
- reclaimEligibleAt = (parsed?.pid ?? null) === null
732
- ? stats.mtimeMs + this._unidentifiedStaleThreshold()
733
- : null;
741
+ awaitingReclaim = this.lockTimeout > 0
742
+ && (parsed?.pid ?? null) === null
743
+ && lockAge <= this._unidentifiedStaleThreshold();
734
744
 
735
745
  if (lockUuid === this.lockUuid) {
736
746
  // This is our own lock — a previous attempt was interrupted. Reclaim.
@@ -802,8 +812,6 @@ export class FileLockService implements IService {
802
812
  // on the spot rather than wait for anything, and the run semaphore's slot
803
813
  // files carry a TEN-MINUTE unidentified window, so honouring eligibility
804
814
  // there would turn "is this slot free right now" into a ten-minute block.
805
- const awaitingReclaim =
806
- this.lockTimeout > 0 && reclaimEligibleAt !== null && Date.now() < reclaimEligibleAt;
807
815
  if ((stalledFor >= this.lockTimeout && !awaitingReclaim) || waitedFor >= this.acquireCeilingMs) {
808
816
  metrics.observe('state_lock_acquire_duration_ms', waitedFor, { status: 'timeout' });
809
817
  metrics.increment('state_lock_acquire_total', 1, { status: 'timeout' });