@mjasnikovs/pi-task 0.38.15 → 0.38.16
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/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +16 -13
- package/dist/task/auto-orchestrator.js +38 -36
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +63 -1
- package/dist/task/boot-probe.js +98 -2
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +2 -7
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +49 -88
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +66 -44
- package/dist/task/phases.d.ts +58 -34
- package/dist/task/phases.js +140 -113
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/task-gates.d.ts +8 -0
- package/dist/task/task-gates.js +23 -4
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +12 -17
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http-request — the one bounded HTTP request in this codebase.
|
|
3
|
+
*
|
|
4
|
+
* Five modules used to hand-roll the same ~12 lines: an internal
|
|
5
|
+
* `AbortController`, a `setTimeout` that aborts it, a `userAborted` flag set
|
|
6
|
+
* from the caller's signal, and a `finally` that clears the timer and removes
|
|
7
|
+
* the listener. Five copies of a rule is five chances to drift, and it HAD
|
|
8
|
+
* drifted — `npm-version.ts` never grew the `userAborted` flag, so a user cancel
|
|
9
|
+
* came back as `null`, indistinguishable from a registry that is down.
|
|
10
|
+
*
|
|
11
|
+
* What is shared is the BOUNDING, not the interpretation. Each caller still owns
|
|
12
|
+
* its own status-code policy (DDG treats 429/403 as rate-limiting, Brave splits
|
|
13
|
+
* auth from rate-limit, npm treats every non-OK as "no answer") and its own error
|
|
14
|
+
* type, because those genuinely differ. What they cannot differ on is whether the
|
|
15
|
+
* request was cancelled by the user, killed by the clock, or refused by the
|
|
16
|
+
* network — so that verdict is made once, here.
|
|
17
|
+
*
|
|
18
|
+
* The handler runs INSIDE the timeout. `fetch` resolves as soon as the headers
|
|
19
|
+
* arrive, so a seam that returned the `Response` and cleared its own timer would
|
|
20
|
+
* leave the body read unbounded — a hung stream would hang forever. Passing a
|
|
21
|
+
* handler keeps the clock over the whole operation, which is what every copy of
|
|
22
|
+
* the ritual already did.
|
|
23
|
+
*/
|
|
24
|
+
/** Injectable fetch — the narrow signature keeps test fakes free of Bun's extras. */
|
|
25
|
+
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
26
|
+
/**
|
|
27
|
+
* Why the request never produced a response. Deliberately only two kinds: a
|
|
28
|
+
* status code is not a failure of the REQUEST, and what a given status means is
|
|
29
|
+
* the caller's policy.
|
|
30
|
+
*/
|
|
31
|
+
export declare class HttpRequestError extends Error {
|
|
32
|
+
readonly kind: 'aborted' | 'network';
|
|
33
|
+
/** The underlying cause, already rendered — callers put it in their own message. */
|
|
34
|
+
readonly detail: string;
|
|
35
|
+
readonly cause?: unknown | undefined;
|
|
36
|
+
constructor(kind: 'aborted' | 'network',
|
|
37
|
+
/** The underlying cause, already rendered — callers put it in their own message. */
|
|
38
|
+
detail: string, cause?: unknown | undefined);
|
|
39
|
+
}
|
|
40
|
+
export interface HttpRequestOpts {
|
|
41
|
+
method?: string;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
body?: string;
|
|
44
|
+
redirect?: 'follow' | 'error' | 'manual';
|
|
45
|
+
/** Wall clock over the request AND the handler. Required — no silent default. */
|
|
46
|
+
timeoutMs: number;
|
|
47
|
+
/** The caller's cancel. Its firing is what makes `userAborted()` true. */
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
fetchImpl?: FetchLike;
|
|
50
|
+
}
|
|
51
|
+
/** What a handler can ask about, and do to, the request it is reading. */
|
|
52
|
+
export interface HttpRequestControl {
|
|
53
|
+
/** The request's own signal, so a handler can pass it further down. */
|
|
54
|
+
readonly signal: AbortSignal;
|
|
55
|
+
/**
|
|
56
|
+
* Abort the in-flight request from inside the handler — a size cap hit, an
|
|
57
|
+
* early stop. Distinct from both a user cancel and the timeout, so a handler
|
|
58
|
+
* that calls this can tell its own abort apart from the other two.
|
|
59
|
+
*/
|
|
60
|
+
abort(): void;
|
|
61
|
+
/** The CALLER cancelled. Not the timeout, not `abort()`. */
|
|
62
|
+
userAborted(): boolean;
|
|
63
|
+
/** The wall clock fired. */
|
|
64
|
+
timedOut(): boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run one bounded HTTP request and hand the response to `handle`.
|
|
68
|
+
*
|
|
69
|
+
* Throws {@link HttpRequestError} when the request itself failed. Anything
|
|
70
|
+
* `handle` throws propagates untouched — that is the caller's own policy talking.
|
|
71
|
+
*/
|
|
72
|
+
export declare function httpRequest<T>(url: string, opts: HttpRequestOpts, handle: (response: Response, ctl: HttpRequestControl) => Promise<T>): Promise<T>;
|
|
73
|
+
/** A readable one-liner for an unknown thrown value. Four byte-identical copies. */
|
|
74
|
+
export declare function describeError(err: unknown): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http-request — the one bounded HTTP request in this codebase.
|
|
3
|
+
*
|
|
4
|
+
* Five modules used to hand-roll the same ~12 lines: an internal
|
|
5
|
+
* `AbortController`, a `setTimeout` that aborts it, a `userAborted` flag set
|
|
6
|
+
* from the caller's signal, and a `finally` that clears the timer and removes
|
|
7
|
+
* the listener. Five copies of a rule is five chances to drift, and it HAD
|
|
8
|
+
* drifted — `npm-version.ts` never grew the `userAborted` flag, so a user cancel
|
|
9
|
+
* came back as `null`, indistinguishable from a registry that is down.
|
|
10
|
+
*
|
|
11
|
+
* What is shared is the BOUNDING, not the interpretation. Each caller still owns
|
|
12
|
+
* its own status-code policy (DDG treats 429/403 as rate-limiting, Brave splits
|
|
13
|
+
* auth from rate-limit, npm treats every non-OK as "no answer") and its own error
|
|
14
|
+
* type, because those genuinely differ. What they cannot differ on is whether the
|
|
15
|
+
* request was cancelled by the user, killed by the clock, or refused by the
|
|
16
|
+
* network — so that verdict is made once, here.
|
|
17
|
+
*
|
|
18
|
+
* The handler runs INSIDE the timeout. `fetch` resolves as soon as the headers
|
|
19
|
+
* arrive, so a seam that returned the `Response` and cleared its own timer would
|
|
20
|
+
* leave the body read unbounded — a hung stream would hang forever. Passing a
|
|
21
|
+
* handler keeps the clock over the whole operation, which is what every copy of
|
|
22
|
+
* the ritual already did.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Why the request never produced a response. Deliberately only two kinds: a
|
|
26
|
+
* status code is not a failure of the REQUEST, and what a given status means is
|
|
27
|
+
* the caller's policy.
|
|
28
|
+
*/
|
|
29
|
+
export class HttpRequestError extends Error {
|
|
30
|
+
kind;
|
|
31
|
+
detail;
|
|
32
|
+
cause;
|
|
33
|
+
constructor(kind,
|
|
34
|
+
/** The underlying cause, already rendered — callers put it in their own message. */
|
|
35
|
+
detail, cause) {
|
|
36
|
+
super(detail);
|
|
37
|
+
this.kind = kind;
|
|
38
|
+
this.detail = detail;
|
|
39
|
+
this.cause = cause;
|
|
40
|
+
this.name = 'HttpRequestError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run one bounded HTTP request and hand the response to `handle`.
|
|
45
|
+
*
|
|
46
|
+
* Throws {@link HttpRequestError} when the request itself failed. Anything
|
|
47
|
+
* `handle` throws propagates untouched — that is the caller's own policy talking.
|
|
48
|
+
*/
|
|
49
|
+
export async function httpRequest(url, opts, handle) {
|
|
50
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
let userAborted = false;
|
|
53
|
+
let timedOut = false;
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
timedOut = true;
|
|
56
|
+
controller.abort();
|
|
57
|
+
}, opts.timeoutMs);
|
|
58
|
+
const onUserAbort = () => {
|
|
59
|
+
userAborted = true;
|
|
60
|
+
controller.abort();
|
|
61
|
+
};
|
|
62
|
+
if (opts.signal) {
|
|
63
|
+
if (opts.signal.aborted)
|
|
64
|
+
onUserAbort();
|
|
65
|
+
else
|
|
66
|
+
opts.signal.addEventListener('abort', onUserAbort, { once: true });
|
|
67
|
+
}
|
|
68
|
+
const ctl = {
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
abort: () => controller.abort(),
|
|
71
|
+
userAborted: () => userAborted,
|
|
72
|
+
timedOut: () => timedOut
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = await fetchImpl(url, {
|
|
78
|
+
...(opts.method === undefined ? {} : { method: opts.method }),
|
|
79
|
+
...(opts.headers === undefined ? {} : { headers: opts.headers }),
|
|
80
|
+
...(opts.body === undefined ? {} : { body: opts.body }),
|
|
81
|
+
...(opts.redirect === undefined ? {} : { redirect: opts.redirect }),
|
|
82
|
+
signal: controller.signal
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (userAborted)
|
|
87
|
+
throw new HttpRequestError('aborted', 'Request aborted.', err);
|
|
88
|
+
throw new HttpRequestError('network', describeError(err), err);
|
|
89
|
+
}
|
|
90
|
+
return await handle(response, ctl);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
if (opts.signal)
|
|
95
|
+
opts.signal.removeEventListener('abort', onUserAbort);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** A readable one-liner for an unknown thrown value. Four byte-identical copies. */
|
|
99
|
+
export function describeError(err) {
|
|
100
|
+
if (err instanceof Error)
|
|
101
|
+
return err.message;
|
|
102
|
+
return String(err);
|
|
103
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* enough to anchor the worker in current reality: the dist-tag 'latest', a
|
|
11
11
|
* short list of recent versions, and the publish date of latest.
|
|
12
12
|
*/
|
|
13
|
+
import { httpRequest, HttpRequestError } from './http-request.js';
|
|
13
14
|
const REGISTRY_BASE = 'https://registry.npmjs.org';
|
|
14
15
|
const DEFAULT_TIMEOUT_MS = 3000;
|
|
15
16
|
const RECENT_VERSIONS_LIMIT = 10;
|
|
@@ -24,50 +25,44 @@ export async function npmVersionLookup(pkg, opts = {}) {
|
|
|
24
25
|
return null;
|
|
25
26
|
const base = opts.registry ?? REGISTRY_BASE;
|
|
26
27
|
const url = `${base}/${encodePackageName(pkg)}`;
|
|
27
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
28
|
-
const internalController = new AbortController();
|
|
29
|
-
const timeoutHandle = setTimeout(() => internalController.abort(), timeoutMs);
|
|
30
|
-
const onUserAbort = () => internalController.abort();
|
|
31
|
-
if (opts.signal) {
|
|
32
|
-
if (opts.signal.aborted)
|
|
33
|
-
onUserAbort();
|
|
34
|
-
else
|
|
35
|
-
opts.signal.addEventListener('abort', onUserAbort, { once: true });
|
|
36
|
-
}
|
|
37
28
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const publishedAt = typeof publishedAtRaw === 'string' ? publishedAtRaw : undefined;
|
|
65
|
-
return { pkg, latest, recent, publishedAt };
|
|
29
|
+
return await httpRequest(url, {
|
|
30
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
31
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
32
|
+
method: 'GET',
|
|
33
|
+
headers: { accept: 'application/vnd.npm.install-v1+json, application/json' }
|
|
34
|
+
}, async (response) => {
|
|
35
|
+
// npm's own status policy: a version banner is a nicety, so every
|
|
36
|
+
// non-OK answer is simply "no version to report".
|
|
37
|
+
if (!response.ok)
|
|
38
|
+
return null;
|
|
39
|
+
let body;
|
|
40
|
+
try {
|
|
41
|
+
body = (await response.json());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const latest = body['dist-tags']?.latest;
|
|
47
|
+
if (typeof latest !== 'string' || latest.length === 0)
|
|
48
|
+
return null;
|
|
49
|
+
const allVersions = Object.keys(body.versions ?? {});
|
|
50
|
+
const recent = allVersions.slice(-RECENT_VERSIONS_LIMIT).reverse();
|
|
51
|
+
const publishedAtRaw = body.time?.[latest];
|
|
52
|
+
const publishedAt = typeof publishedAtRaw === 'string' ? publishedAtRaw : undefined;
|
|
53
|
+
return { pkg, latest, recent, publishedAt };
|
|
54
|
+
});
|
|
66
55
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
catch (err) {
|
|
57
|
+
// A user cancel is re-thrown, not swallowed. This module was the copy of the
|
|
58
|
+
// request ritual that never grew a `userAborted` flag, so a cancelled lookup
|
|
59
|
+
// returned `null` — indistinguishable from a registry that is down, and the
|
|
60
|
+
// caller went on assembling a block for a run the user had already stopped.
|
|
61
|
+
if (err instanceof HttpRequestError && err.kind === 'aborted')
|
|
62
|
+
throw err;
|
|
63
|
+
if (err instanceof HttpRequestError)
|
|
64
|
+
return null;
|
|
65
|
+
throw err;
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
68
|
/** Format an NpmVersionInfo as a short Markdown block for EXTERNAL CONTEXT. */
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { type ContextSnapshot, type SpawnFn } from '../shared/child-process.js';
|
|
2
2
|
import { type LoopHit } from '../task/loop-detector.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Tool calls that can GROUND an APIS claim — i.e. return content a signature or
|
|
5
|
+
* command could be cited from. `pi-worker-docs` (the primary), `read` and `grep`
|
|
6
|
+
* (project source), and the web escalations `pi-worker-search`/`pi-worker-fetch`.
|
|
7
|
+
*
|
|
8
|
+
* `ls` and `find` are deliberately EXCLUDED: they return file/directory NAMES,
|
|
9
|
+
* and APIS owns symbols by name only, never paths (RESEARCH_APIS_PROMPT). Bare
|
|
10
|
+
* enumeration cannot verify a signature, so a worker that fabricates its section
|
|
11
|
+
* from memory does not launder itself grounded by calling `ls` once. That
|
|
12
|
+
* exclusion is the anti-gaming property of any gate built on this count: "one
|
|
13
|
+
* trivial `ls` then fabricate the rest" leaves groundingRetrievalCount at 0.
|
|
14
|
+
*/
|
|
15
|
+
export { isGroundingRetrieval } from './worker-channels.js';
|
|
5
16
|
/**
|
|
6
17
|
* Does this partial output carry ANSWER CONTENT, or is it the model clearing its
|
|
7
18
|
* throat?
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
2
2
|
import { runChildDefault } from '../shared/child-process.js';
|
|
3
3
|
import { CommandWatchdog, commandTimeoutHint, realTimerDeps } from '../shared/command-watchdog.js';
|
|
4
|
+
import { isGroundingRetrieval as isGrounding, workerChannel } from './worker-channels.js';
|
|
4
5
|
import { childBaseArgs } from '../shared/child-extensions.js';
|
|
5
6
|
import { LoopDetector } from '../task/loop-detector.js';
|
|
6
7
|
import { LOOP_WINDOW, LOOP_THRESHOLD, MAX_LOOP_RESTARTS, formatLoopHint, isConnectionError, connectionRetryBackoffMs } from '../task/child-runner.js';
|
|
@@ -27,17 +28,10 @@ const DEFAULT_TOOLS = 'read,grep,find,ls';
|
|
|
27
28
|
* exclusion is the anti-gaming property of any gate built on this count: "one
|
|
28
29
|
* trivial `ls` then fabricate the rest" leaves groundingRetrievalCount at 0.
|
|
29
30
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'pi-worker-search',
|
|
35
|
-
'pi-worker-fetch'
|
|
36
|
-
]);
|
|
37
|
-
/** True when a tool call retrieves content an APIS entry could be grounded in. */
|
|
38
|
-
export function isGroundingRetrieval(toolName) {
|
|
39
|
-
return GROUNDING_RETRIEVAL_TOOLS.has(toolName);
|
|
40
|
-
}
|
|
31
|
+
// The grounding set is derived from WORKER_CHANNELS (worker-channels.ts), not
|
|
32
|
+
// hand-kept — this was a second copy of the four tool names. Re-exported because
|
|
33
|
+
// several call sites and tests import it from here.
|
|
34
|
+
export { isGroundingRetrieval } from './worker-channels.js';
|
|
41
35
|
/**
|
|
42
36
|
* Hard wall-clock bound on a single research worker run (one spawn). The
|
|
43
37
|
* exact-match LoopDetector only catches *identical* repeated tool calls; a model
|
|
@@ -272,7 +266,7 @@ export function commandCeilingForAttempt(baseMs, priorHangs) {
|
|
|
272
266
|
const RESTART_RULES = [
|
|
273
267
|
{
|
|
274
268
|
// A loop-kill gets the same restart-with-hint treatment every other phase
|
|
275
|
-
// already gets (
|
|
269
|
+
// already gets (runPhaseChild) — name the offending call so the
|
|
276
270
|
// re-spawn avoids it. Bounded by the shared restart budget.
|
|
277
271
|
reason: 'loop',
|
|
278
272
|
detect: s => s.loopHit && s.restartBudgetSpent < MAX_LOOP_RESTARTS ?
|
|
@@ -329,7 +323,7 @@ const RESTART_RULES = [
|
|
|
329
323
|
},
|
|
330
324
|
{
|
|
331
325
|
// A connection-class model error is restartable on the same budget, exactly
|
|
332
|
-
// as
|
|
326
|
+
// as runPhaseChild already treats it — a research worker had no such
|
|
333
327
|
// retry, so one dropped fetch failed the whole task at research while the
|
|
334
328
|
// identical blip in refine/compose was absorbed.
|
|
335
329
|
//
|
|
@@ -429,7 +423,7 @@ export async function runWorker(input) {
|
|
|
429
423
|
const timeoutMs = input.timeoutMs ?? RESEARCH_WORKER_TIMEOUT_MS;
|
|
430
424
|
let hint = null;
|
|
431
425
|
// Loop-kill and timeout share one restart budget, mirroring
|
|
432
|
-
//
|
|
426
|
+
// runPhaseChild: a runaway worker gets re-spawned with a corrective
|
|
433
427
|
// hint up to MAX_LOOP_RESTARTS times before we give up. Leaked tool calls
|
|
434
428
|
// keep their own MAX_LEAK_RETRIES budget below — a different failure mode.
|
|
435
429
|
let restartBudgetSpent = 0;
|
|
@@ -522,12 +516,13 @@ export async function runWorker(input) {
|
|
|
522
516
|
// A tool call is the worker working. Inert unless the
|
|
523
517
|
// caller opted into a progress-based deadline.
|
|
524
518
|
timeout.progress();
|
|
519
|
+
// The generic child runner used to name ONE tool and ONE of
|
|
520
|
+
// its parameters here. It asks the tool's own row now.
|
|
525
521
|
if (input.fanoutTimeout
|
|
526
|
-
&& call.name ===
|
|
527
|
-
&& call.args?.module === '.') {
|
|
522
|
+
&& workerChannel(call.name)?.isProjectSourceLookup?.(call.args ?? {}) === true) {
|
|
528
523
|
timeout.extend(input.fanoutTimeout.perLookupMs, input.fanoutTimeout.ceilingMs);
|
|
529
524
|
}
|
|
530
|
-
if (
|
|
525
|
+
if (isGrounding(call.name))
|
|
531
526
|
groundingRetrievalCount++;
|
|
532
527
|
if (!loopDetector)
|
|
533
528
|
return null;
|
|
@@ -67,7 +67,7 @@ export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWor
|
|
|
67
67
|
* exists to prevent: four regexes matching three phrasings, documented at length in
|
|
68
68
|
* abstention.ts, which cost a real bug.
|
|
69
69
|
*/
|
|
70
|
-
export declare function docsCacheable(d: Pick<DocsDetails, '
|
|
70
|
+
export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified'>, text: string): boolean;
|
|
71
71
|
/** The docs cache key: a package's answer is per (module, question). A project-source
|
|
72
72
|
* `.` lookup is never cached — the working tree mutates as tasks implement. */
|
|
73
73
|
export declare function docsCacheKey(params: {
|
|
@@ -7,7 +7,7 @@ import { formatResultText } from '../shared/child-output.js';
|
|
|
7
7
|
import { docsRaw, packageHeader, buildPrompt, buildVersionBanner } from './docs-core.js';
|
|
8
8
|
import { formatNpmVersionSection } from './npm-version.js';
|
|
9
9
|
import { runFocusedExtraction } from './focused-extractor.js';
|
|
10
|
-
import { makeWorkerTool } from './shared.js';
|
|
10
|
+
import { childFailureReason, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
11
11
|
import { isTypeOnlyAnswer } from '../task/type-only-answer.js';
|
|
12
12
|
import { logDocsAnswer } from './typeonly-log.js';
|
|
13
13
|
import { normalizeQuery } from './research-cache.js';
|
|
@@ -61,18 +61,18 @@ function pinDetails(pin) {
|
|
|
61
61
|
* The paths differ only in `prefix`: the npm path leads every result, failures included, with
|
|
62
62
|
* its version banner and npm-version header; the project path has neither.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* It says UNAVAILABLE, so the cache cannot take it. It used to say so by writing a
|
|
65
|
+
* non-zero `childExitCode` and letting `docsCacheable` re-derive the verdict — a
|
|
66
|
+
* derivation that failed on the case it mattered most for: a signal-killed child
|
|
67
|
+
* reports exit code 0, so `"Docs lookup aborted."` was cached for the whole run.
|
|
68
|
+
* `aborted` was written here and read by nothing, which is what let that hide.
|
|
66
69
|
*/
|
|
67
70
|
function docsFailureResult(extraction, baseDetails, prefix) {
|
|
68
|
-
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
childExitCode: extraction.exitCode
|
|
74
|
-
}
|
|
75
|
-
};
|
|
71
|
+
return workerUnavailable(prefix + extraction.failure, {
|
|
72
|
+
...baseDetails,
|
|
73
|
+
...(extraction.aborted ? { aborted: true } : {}),
|
|
74
|
+
childExitCode: extraction.exitCode
|
|
75
|
+
}, childFailureReason({ exitCode: extraction.exitCode, aborted: extraction.aborted }));
|
|
76
76
|
}
|
|
77
77
|
export function registerPiWorkerDocs(pi, internals = {}) {
|
|
78
78
|
// CAP arm of nexttask 5B — OFF unless PI_TASK_PROJECT_DOCS_BUDGET is set, and
|
|
@@ -145,10 +145,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
145
145
|
if (budget !== null && ++projectLookups > budget) {
|
|
146
146
|
// Refused BEFORE any work: the point of the cap is the child
|
|
147
147
|
// spawn and the model pass this branch would otherwise run.
|
|
148
|
-
return {
|
|
149
|
-
text: projectDocsBudgetExhausted(budget),
|
|
150
|
-
details: { budgetSpent: true }
|
|
151
|
-
};
|
|
148
|
+
return workerUnavailable(projectDocsBudgetExhausted(budget), { budgetSpent: true }, 'budget-spent');
|
|
152
149
|
}
|
|
153
150
|
const openCache = internals.openCache ?? defaultOpenCache;
|
|
154
151
|
let cache;
|
|
@@ -160,24 +157,19 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
160
157
|
cacheError = err instanceof Error ? err.message : String(err);
|
|
161
158
|
}
|
|
162
159
|
if (!cache) {
|
|
163
|
-
return {
|
|
164
|
-
text: `Project docs unavailable: cache open failed (${cacheError}).`,
|
|
165
|
-
details: {}
|
|
166
|
-
};
|
|
160
|
+
return workerUnavailable(`Project docs unavailable: cache open failed (${cacheError}).`, {}, 'cache-open-failed');
|
|
167
161
|
}
|
|
168
162
|
const retrieveChunks = internals.retrieveChunks ?? defaultRetrieveChunks;
|
|
169
163
|
const projectResult = projectDocsRaw(cache, ctx.cwd, params.query, retrieveChunks);
|
|
170
164
|
if (projectResult.kind === 'error') {
|
|
171
|
-
return
|
|
165
|
+
return workerUnavailable(`Project docs error: ${projectResult.message}`, {}, 'project-docs-error');
|
|
172
166
|
}
|
|
173
167
|
if (projectResult.kind === 'no_chunks') {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
};
|
|
168
|
+
// The project IS indexed and has nothing — a real answer.
|
|
169
|
+
return workerAnswer(`Project "${projectResult.projectName}" has no .ts/.tsx files indexed.`, {
|
|
170
|
+
hitCache: projectResult.hitCache,
|
|
171
|
+
indexedFiles: projectResult.filesIngested
|
|
172
|
+
});
|
|
181
173
|
}
|
|
182
174
|
const { projectName, chunks, hitCache, filesIngested, indexingMs } = projectResult;
|
|
183
175
|
const baseDetails = {
|
|
@@ -214,14 +206,10 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
214
206
|
excerptCheck: extraction.excerptCheck,
|
|
215
207
|
toolText: text
|
|
216
208
|
});
|
|
217
|
-
return {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
childExitCode: 0,
|
|
222
|
-
excerptVerified: verified
|
|
223
|
-
}
|
|
224
|
-
};
|
|
209
|
+
return workerAnswer(text, {
|
|
210
|
+
...baseDetails,
|
|
211
|
+
excerptVerified: verified
|
|
212
|
+
});
|
|
225
213
|
}
|
|
226
214
|
// ── npm package lookup (existing path) ──────────────────────────
|
|
227
215
|
const rawResult = await docsRaw({
|
|
@@ -253,31 +241,27 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
253
241
|
autoInstalled: rawResult.autoInstalled,
|
|
254
242
|
...npmDetails
|
|
255
243
|
};
|
|
256
|
-
return
|
|
244
|
+
return workerUnavailable(npmHeader + rawResult.message, details, 'docs-error');
|
|
257
245
|
}
|
|
258
246
|
if (rawResult.kind === 'not_installed') {
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
+ `Package "${rawResult.pkg}" is not installed and auto-install failed.`,
|
|
262
|
-
details: { resolveError: 'not_installed', ...npmDetails }
|
|
263
|
-
};
|
|
247
|
+
return workerUnavailable(npmHeader
|
|
248
|
+
+ `Package "${rawResult.pkg}" is not installed and auto-install failed.`, { resolveError: 'not_installed', ...npmDetails }, 'not-installed');
|
|
264
249
|
}
|
|
265
250
|
if (rawResult.kind === 'no_chunks') {
|
|
266
251
|
const banner = buildVersionBanner(rawResult.autoInstallPin, rawResult.pkg.name, rawResult.pkg.version, ctx.cwd);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
};
|
|
252
|
+
// The package resolved and genuinely ships nothing to read — an
|
|
253
|
+
// answer, and a stable one for this run.
|
|
254
|
+
return workerAnswer(banner
|
|
255
|
+
+ npmHeader
|
|
256
|
+
+ `Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no .d.ts files or README. Use pi-worker to read source directly.`, {
|
|
257
|
+
version: rawResult.pkg.version,
|
|
258
|
+
hitCache: rawResult.hitCache,
|
|
259
|
+
indexedFiles: rawResult.indexedFiles ?? 0,
|
|
260
|
+
cacheError: rawResult.cacheError,
|
|
261
|
+
autoInstalled: rawResult.autoInstalled,
|
|
262
|
+
...pinDetails(rawResult.autoInstallPin),
|
|
263
|
+
...npmDetails
|
|
264
|
+
});
|
|
281
265
|
}
|
|
282
266
|
// kind === 'ok'
|
|
283
267
|
const { pkg, chunks, hitCache, indexingMs, cacheError, autoInstalled } = rawResult;
|
|
@@ -352,15 +336,11 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
352
336
|
excerptCheck: extraction.excerptCheck,
|
|
353
337
|
toolText: text
|
|
354
338
|
});
|
|
355
|
-
return {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
excerptVerified: verified,
|
|
361
|
-
...(typeOnly.typeOnly ? { typeOnly: true } : {})
|
|
362
|
-
}
|
|
363
|
-
};
|
|
339
|
+
return workerAnswer(text, {
|
|
340
|
+
...baseDetails,
|
|
341
|
+
excerptVerified: verified,
|
|
342
|
+
...(typeOnly.typeOnly ? { typeOnly: true } : {})
|
|
343
|
+
});
|
|
364
344
|
},
|
|
365
345
|
renderCall(args, theme) {
|
|
366
346
|
const query = args.query.replace(/\s+/g, ' ').trim();
|
|
@@ -410,10 +390,11 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
410
390
|
* abstention.ts, which cost a real bug.
|
|
411
391
|
*/
|
|
412
392
|
export function docsCacheable(d, text) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
393
|
+
// Answer QUALITY only. Whether there IS an answer is `WorkerOutcome.kind`, and
|
|
394
|
+
// `makeWorkerTool` has already refused an `unavailable` before reaching here —
|
|
395
|
+
// this used to open with `childExitCode === 0`, which a signal-killed child
|
|
396
|
+
// satisfies, so an aborted lookup was memoised for the run.
|
|
397
|
+
return d.typeOnly !== true && d.excerptVerified !== false && !isAbstention(text);
|
|
417
398
|
}
|
|
418
399
|
/** The docs cache key: a package's answer is per (module, question). A project-source
|
|
419
400
|
* `.` lookup is never cached — the working tree mutates as tasks implement. */
|
|
@@ -28,7 +28,7 @@ export declare function registerPiWorkerFetch(pi: ExtensionAPI, internals?: PiWo
|
|
|
28
28
|
* `docsCacheable`: pi-worker-fetch.test.ts carried a hand-retyped copy driving four
|
|
29
29
|
* tests, which a change to the shipped rule would leave green.
|
|
30
30
|
*/
|
|
31
|
-
export declare function fetchCacheable(
|
|
31
|
+
export declare function fetchCacheable(_d: Pick<FetchDetails, never>, text: string): boolean;
|
|
32
32
|
/** The fetch cache key. URL verbatim (path case can matter), question normalised —
|
|
33
33
|
* same page, different question is a different answer. */
|
|
34
34
|
export declare function fetchCacheKey(params: {
|