@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 +1 -2
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli.mjs +2 -3
- package/docs/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/infrastructure/file-lock-service.ts +18 -10
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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
|
-
//
|
|
698
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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' });
|