@mjasnikovs/pi-task 0.38.30 → 0.38.32
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 +2 -1
- package/dist/config/config.d.ts +16 -2
- package/dist/config/config.js +7 -2
- package/dist/config/group-args.d.ts +52 -0
- package/dist/config/group-args.js +110 -0
- package/dist/config/group-models.d.ts +88 -0
- package/dist/config/group-models.js +117 -0
- package/dist/config/groups.d.ts +76 -0
- package/dist/config/groups.js +110 -0
- package/dist/config/option-picker.d.ts +42 -0
- package/dist/config/option-picker.js +73 -0
- package/dist/config/reasoning.d.ts +22 -63
- package/dist/config/reasoning.js +37 -108
- package/dist/config/register.d.ts +98 -12
- package/dist/config/register.js +228 -23
- package/dist/index.js +4 -0
- package/dist/remote/push.js +1 -7
- package/dist/shared/command-watchdog.d.ts +63 -0
- package/dist/shared/command-watchdog.js +87 -0
- package/dist/shared/data-home.d.ts +8 -0
- package/dist/shared/data-home.js +14 -0
- package/dist/shared/model-endpoint.d.ts +53 -0
- package/dist/shared/model-endpoint.js +98 -2
- package/dist/shared/reasoning-capability.d.ts +25 -5
- package/dist/shared/reasoning-capability.js +18 -9
- package/dist/task/auto-orchestrator.js +14 -3
- package/dist/task/child-runner.d.ts +92 -15
- package/dist/task/child-runner.js +303 -66
- package/dist/task/context-usage.d.ts +46 -0
- package/dist/task/context-usage.js +41 -0
- package/dist/task/failure-classifier.js +24 -1
- package/dist/task/gate-child.d.ts +15 -4
- package/dist/task/gate-child.js +2 -2
- package/dist/task/gate-deps.js +7 -2
- package/dist/task/implementation-guards.d.ts +26 -0
- package/dist/task/implementation-guards.js +177 -0
- package/dist/task/implementation-hold.d.ts +118 -0
- package/dist/task/implementation-hold.js +165 -0
- package/dist/task/implementation-turn.d.ts +5 -0
- package/dist/task/implementation-turn.js +12 -1
- package/dist/task/loop-detector.d.ts +18 -0
- package/dist/task/loop-detector.js +22 -2
- package/dist/task/model-hold-stash.d.ts +43 -0
- package/dist/task/model-hold-stash.js +70 -0
- package/dist/task/orchestrator.d.ts +18 -5
- package/dist/task/orchestrator.js +63 -6
- package/dist/task/phases.js +18 -5
- package/dist/task/research-worker.d.ts +2 -2
- package/dist/task/research-worker.js +1 -1
- package/dist/workers/docs-core.js +2 -2
- package/dist/workers/docs-lookup.d.ts +4 -3
- package/dist/workers/docs-lookup.js +1 -1
- package/dist/workers/fetch-core.js +2 -2
- package/dist/workers/focused-extractor.d.ts +6 -4
- package/dist/workers/focused-extractor.js +17 -5
- package/dist/workers/index.js +2 -0
- package/dist/workers/model-warning.d.ts +69 -0
- package/dist/workers/model-warning.js +113 -0
- package/dist/workers/pi-worker-core.d.ts +9 -38
- package/dist/workers/pi-worker-core.js +8 -86
- package/dist/workers/pi-worker-docs.js +2 -2
- package/dist/workers/pi-worker.js +4 -4
- package/dist/workers/reasoning-warning.d.ts +17 -9
- package/dist/workers/reasoning-warning.js +69 -22
- package/dist/workers/single-read-guard.d.ts +6 -6
- package/dist/workers/single-read-guard.js +8 -8
- package/dist/workers/worker-profiles.d.ts +11 -3
- package/dist/workers/worker-profiles.js +33 -1
- package/package.json +1 -1
- package/dist/config/reasoning-args.d.ts +0 -23
- package/dist/config/reasoning-args.js +0 -28
- package/dist/task/implementation-thinking.d.ts +0 -56
- package/dist/task/implementation-thinking.js +0 -32
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Either pattern returns a LoopHit so the caller can kill the child and re-spawn
|
|
14
14
|
* with a hint. No I/O. No imports from index.ts. Trivially unit-testable.
|
|
15
|
+
*
|
|
16
|
+
* The three tuning constants live here rather than in child-runner.ts because
|
|
17
|
+
* worker-profiles.ts reads them at module top level to build DEFAULT_LOOP_DETECTOR.
|
|
18
|
+
* From child-runner.ts that is a cycle — child-runner → worker-profiles →
|
|
19
|
+
* child-runner — and the failure is a TDZ ReferenceError on import order, which
|
|
20
|
+
* no compile step catches. This module imports nothing, so it cannot close one.
|
|
15
21
|
*/
|
|
22
|
+
/** Recent tool calls the exact-repeat rule looks back over. */
|
|
23
|
+
export declare const LOOP_WINDOW = 20;
|
|
24
|
+
/** Repeats of one key within the window that trip the detector. */
|
|
25
|
+
export declare const LOOP_THRESHOLD = 5;
|
|
26
|
+
/** Re-spawns allowed after a loop kill — 3 attempts total with the initial one. */
|
|
27
|
+
export declare const MAX_LOOP_RESTARTS = 2;
|
|
16
28
|
export interface ToolCall {
|
|
17
29
|
name: string;
|
|
18
30
|
args: unknown;
|
|
@@ -27,6 +39,12 @@ export interface LoopHit {
|
|
|
27
39
|
* Arrays preserve their order (positional). undefined / primitives passthrough.
|
|
28
40
|
*/
|
|
29
41
|
export declare function stableStringify(value: unknown): string;
|
|
42
|
+
/**
|
|
43
|
+
* The exact-match identity of a tool call. Exported because implementation-guards.ts
|
|
44
|
+
* keys its own per-call strike count on it, and a second hand-written copy of this
|
|
45
|
+
* expression is free to drift away from the one `record` uses.
|
|
46
|
+
*/
|
|
47
|
+
export declare function loopKey(call: ToolCall): string;
|
|
30
48
|
/**
|
|
31
49
|
* The primary file path a tool call targets, mirroring summarizeToolArgs' field
|
|
32
50
|
* precedence. Returns null when the call names no path (e.g. bash, or a grep
|
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Either pattern returns a LoopHit so the caller can kill the child and re-spawn
|
|
14
14
|
* with a hint. No I/O. No imports from index.ts. Trivially unit-testable.
|
|
15
|
+
*
|
|
16
|
+
* The three tuning constants live here rather than in child-runner.ts because
|
|
17
|
+
* worker-profiles.ts reads them at module top level to build DEFAULT_LOOP_DETECTOR.
|
|
18
|
+
* From child-runner.ts that is a cycle — child-runner → worker-profiles →
|
|
19
|
+
* child-runner — and the failure is a TDZ ReferenceError on import order, which
|
|
20
|
+
* no compile step catches. This module imports nothing, so it cannot close one.
|
|
15
21
|
*/
|
|
22
|
+
/** Recent tool calls the exact-repeat rule looks back over. */
|
|
23
|
+
export const LOOP_WINDOW = 20;
|
|
24
|
+
/** Repeats of one key within the window that trip the detector. */
|
|
25
|
+
export const LOOP_THRESHOLD = 5;
|
|
26
|
+
/** Re-spawns allowed after a loop kill — 3 attempts total with the initial one. */
|
|
27
|
+
export const MAX_LOOP_RESTARTS = 2;
|
|
16
28
|
/**
|
|
17
29
|
* JSON.stringify with sorted object keys so {a:1,b:2} and {b:2,a:1} hash equal.
|
|
18
30
|
* Arrays preserve their order (positional). undefined / primitives passthrough.
|
|
@@ -28,6 +40,14 @@ export function stableStringify(value) {
|
|
|
28
40
|
return sorted;
|
|
29
41
|
});
|
|
30
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The exact-match identity of a tool call. Exported because implementation-guards.ts
|
|
45
|
+
* keys its own per-call strike count on it, and a second hand-written copy of this
|
|
46
|
+
* expression is free to drift away from the one `record` uses.
|
|
47
|
+
*/
|
|
48
|
+
export function loopKey(call) {
|
|
49
|
+
return `${call.name}\x00${stableStringify(call.args)}`;
|
|
50
|
+
}
|
|
31
51
|
/**
|
|
32
52
|
* The primary file path a tool call targets, mirroring summarizeToolArgs' field
|
|
33
53
|
* precedence. Returns null when the call names no path (e.g. bash, or a grep
|
|
@@ -78,7 +98,7 @@ export class LoopDetector {
|
|
|
78
98
|
threshold;
|
|
79
99
|
pathThreshold;
|
|
80
100
|
buf = [];
|
|
81
|
-
constructor(window =
|
|
101
|
+
constructor(window = LOOP_WINDOW, threshold = LOOP_THRESHOLD,
|
|
82
102
|
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
83
103
|
pathThreshold = threshold) {
|
|
84
104
|
this.window = window;
|
|
@@ -87,7 +107,7 @@ export class LoopDetector {
|
|
|
87
107
|
}
|
|
88
108
|
/** Record a tool call. Returns LoopHit if either threshold is breached, else null. */
|
|
89
109
|
record(call) {
|
|
90
|
-
const key =
|
|
110
|
+
const key = loopKey(call);
|
|
91
111
|
const offset = readOffset(call.args);
|
|
92
112
|
this.buf.push({ key, path: primaryPath(call.args), offset, end: readEnd(call.args, offset) });
|
|
93
113
|
if (this.buf.length > this.window)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The crash half of the implementation hold.
|
|
3
|
+
*
|
|
4
|
+
* `release()` runs in a `finally`, and a `finally` does not run for SIGKILL, a
|
|
5
|
+
* segfault, or the power going out. An implementation turn is measured in hours.
|
|
6
|
+
* Without this, one of those leaves pi's GLOBAL `defaultModel` pointing at the
|
|
7
|
+
* implementation model — and because children carry no `-m` and resolve that
|
|
8
|
+
* same default, every child of every future run, in every project, quietly
|
|
9
|
+
* follows it until the user notices.
|
|
10
|
+
*
|
|
11
|
+
* So acquire leaves a note on disk and release removes it. A later session
|
|
12
|
+
* finds the note and puts the model back.
|
|
13
|
+
*/
|
|
14
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
15
|
+
/**
|
|
16
|
+
* Both halves of the switch, as `provider/id` strings.
|
|
17
|
+
*
|
|
18
|
+
* `applied` is what makes the restore safe to run in a second, unrelated
|
|
19
|
+
* session: it is the value we are allowed to overwrite, and nothing else.
|
|
20
|
+
*/
|
|
21
|
+
export interface StashRecord {
|
|
22
|
+
before: string;
|
|
23
|
+
applied: string;
|
|
24
|
+
}
|
|
25
|
+
/** The seam, so the hold's tests never touch a real home directory. */
|
|
26
|
+
export interface HoldStash {
|
|
27
|
+
read(): StashRecord | undefined;
|
|
28
|
+
write(record: StashRecord): void;
|
|
29
|
+
clear(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function readHoldStash(): StashRecord | undefined;
|
|
32
|
+
export declare function writeHoldStash(record: StashRecord): void;
|
|
33
|
+
export declare function clearHoldStash(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Register the crash restore. Runs once per `session_start`, before any task.
|
|
36
|
+
*
|
|
37
|
+
* It reaches for `pi.setModel` rather than writing `settings.json` itself:
|
|
38
|
+
* pi-task has no sanctioned way to write that file, and inventing one would fork
|
|
39
|
+
* the format. Everything it can go wrong on — no registry, a model that has
|
|
40
|
+
* since vanished, a rejected auth check — ends the same way, with the note
|
|
41
|
+
* dropped, because a restore that cannot happen must not re-fire forever.
|
|
42
|
+
*/
|
|
43
|
+
export declare function registerModelHoldRestore(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { splitSpec } from '../config/group-models.js';
|
|
4
|
+
import { defaultModelRef } from '../shared/model-endpoint.js';
|
|
5
|
+
import { stateFile } from '../shared/data-home.js';
|
|
6
|
+
import { restoreHeldModel } from './implementation-hold.js';
|
|
7
|
+
const stashPath = () => stateFile('model-hold.json');
|
|
8
|
+
export function readHoldStash() {
|
|
9
|
+
try {
|
|
10
|
+
const j = JSON.parse(fs.readFileSync(stashPath(), 'utf8'));
|
|
11
|
+
if (typeof j.before !== 'string' || typeof j.applied !== 'string')
|
|
12
|
+
return undefined;
|
|
13
|
+
if (j.before === '' || j.applied === '')
|
|
14
|
+
return undefined;
|
|
15
|
+
return { before: j.before, applied: j.applied };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function writeHoldStash(record) {
|
|
22
|
+
try {
|
|
23
|
+
const p = stashPath();
|
|
24
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
25
|
+
fs.writeFileSync(p, JSON.stringify(record));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A stash we cannot write costs us the crash restore, not the turn.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function clearHoldStash() {
|
|
32
|
+
try {
|
|
33
|
+
fs.rmSync(stashPath(), { force: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* already gone */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register the crash restore. Runs once per `session_start`, before any task.
|
|
41
|
+
*
|
|
42
|
+
* It reaches for `pi.setModel` rather than writing `settings.json` itself:
|
|
43
|
+
* pi-task has no sanctioned way to write that file, and inventing one would fork
|
|
44
|
+
* the format. Everything it can go wrong on — no registry, a model that has
|
|
45
|
+
* since vanished, a rejected auth check — ends the same way, with the note
|
|
46
|
+
* dropped, because a restore that cannot happen must not re-fire forever.
|
|
47
|
+
*/
|
|
48
|
+
export function registerModelHoldRestore(pi) {
|
|
49
|
+
pi.on('session_start', (_event, ctx) => {
|
|
50
|
+
const model = {
|
|
51
|
+
current: () => {
|
|
52
|
+
const m = ctx.model;
|
|
53
|
+
return m ? { spec: `${m.provider}/${m.id}`, handle: m } : undefined;
|
|
54
|
+
},
|
|
55
|
+
resolve: spec => {
|
|
56
|
+
const parts = splitSpec(spec);
|
|
57
|
+
return parts ? ctx.modelRegistry.find(parts.provider, parts.id) : undefined;
|
|
58
|
+
},
|
|
59
|
+
apply: handle => pi.setModel(handle)
|
|
60
|
+
};
|
|
61
|
+
// The saved default, read from the file the crash left wrong — not from
|
|
62
|
+
// `ctx.model`, which a `--model` flag or a resumed session can make say
|
|
63
|
+
// something else entirely.
|
|
64
|
+
const saved = () => {
|
|
65
|
+
const ref = defaultModelRef();
|
|
66
|
+
return ref && `${ref.provider}/${ref.id}`;
|
|
67
|
+
};
|
|
68
|
+
void restoreHeldModel(model, saved).catch(() => { });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -21,9 +21,18 @@ import { type WidgetState } from './widget.js';
|
|
|
21
21
|
import { type RunTaskFn } from './gate-deps.js';
|
|
22
22
|
import { type GateDeps } from './task-gates.js';
|
|
23
23
|
import { type PhaseSeams } from './child-runner.js';
|
|
24
|
-
import { type
|
|
24
|
+
import { type ImplementationControls } from './implementation-hold.js';
|
|
25
25
|
import { type RunEnd } from './run-end.js';
|
|
26
26
|
import { type SuperviseOptions } from './implementation-turn.js';
|
|
27
|
+
/**
|
|
28
|
+
* pi's own `Model`, named WITHOUT importing `@earendil-works/pi-ai` — which is
|
|
29
|
+
* neither a dependency, a devDependency nor a peerDependency of this package
|
|
30
|
+
* (see shared/model-endpoint.ts's header). The context already carries the type,
|
|
31
|
+
* so deriving it costs nothing and adds no edge to the dependency graph.
|
|
32
|
+
*/
|
|
33
|
+
type PiModel = NonNullable<ExtensionCommandContext['model']>;
|
|
34
|
+
/** Both halves of the implementation hold, over the live session. */
|
|
35
|
+
export declare function piImplementationControls(ctx: ExtensionCommandContext): ImplementationControls<PiModel>;
|
|
27
36
|
/**
|
|
28
37
|
* Everything one TaskRunner needs, as one object. The runner is the shared core
|
|
29
38
|
* under `runSingleTask` (and so under /task-auto's per-task loop), so this is the
|
|
@@ -163,11 +172,14 @@ export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId'
|
|
|
163
172
|
*/
|
|
164
173
|
notifyFinish?: boolean;
|
|
165
174
|
/**
|
|
166
|
-
* How the implementation turn's thinking level
|
|
167
|
-
* to the live pi session; injectable so the
|
|
168
|
-
* with no real session to restore.
|
|
175
|
+
* How the implementation turn's MODEL and thinking level are read and
|
|
176
|
+
* written. Defaults to the live pi session; injectable so the
|
|
177
|
+
* hold-and-restore is assertable with no real session to restore.
|
|
178
|
+
*
|
|
179
|
+
* One object rather than two, because the two must be acquired and released
|
|
180
|
+
* in one order and a caller handed two seams could supply half of one.
|
|
169
181
|
*/
|
|
170
|
-
|
|
182
|
+
implementationControls?: ImplementationControls<never>;
|
|
171
183
|
}
|
|
172
184
|
export interface RunSingleTaskResult {
|
|
173
185
|
taskId: string;
|
|
@@ -223,3 +235,4 @@ export declare function runGatedTask(ctx: ExtensionCommandContext, cwd: string,
|
|
|
223
235
|
deps?: GateDeps;
|
|
224
236
|
}): Promise<void>;
|
|
225
237
|
export declare function registerTask(pi: ExtensionAPI): void;
|
|
238
|
+
export {};
|
|
@@ -26,6 +26,7 @@ import { readTextFile } from '../shared/fs-text.js';
|
|
|
26
26
|
import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
27
27
|
import { startWidget } from './widget.js';
|
|
28
28
|
import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.js';
|
|
29
|
+
import { armImplementationGuard, disarmImplementationGuard } from './implementation-guards.js';
|
|
29
30
|
import { publishViewer, publishNotify, registerBridgeCommand, getBridge } from '../remote/bridge.js';
|
|
30
31
|
import { pushNotify } from '../remote/push.js';
|
|
31
32
|
import { getConfig } from '../config/config.js';
|
|
@@ -37,7 +38,8 @@ import { findDeliveryPhantoms, formatApiOverrideBanner } from '../workers/phanto
|
|
|
37
38
|
import { titleForDisplay } from './parsers.js';
|
|
38
39
|
import { USER_CANCELLED } from './child-runner.js';
|
|
39
40
|
import { cancelCheckpoint } from './cancel-points.js';
|
|
40
|
-
import {
|
|
41
|
+
import { splitSpec } from '../config/group-models.js';
|
|
42
|
+
import { holdImplementation } from './implementation-hold.js';
|
|
41
43
|
import { rearmCancelListener } from './cancel-input.js';
|
|
42
44
|
import { takeHeldInput } from './mid-run-input.js';
|
|
43
45
|
import { withRun, announceTerminal } from './run-bracket.js';
|
|
@@ -73,7 +75,38 @@ function piThinkingControl() {
|
|
|
73
75
|
const api = piApi;
|
|
74
76
|
if (!api)
|
|
75
77
|
return { get: () => 'off', set: () => { } };
|
|
76
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
get: () => api.getThinkingLevel(),
|
|
80
|
+
set: (level) => api.setThinkingLevel(level)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The live session's model, as a {@link ModelControl} over pi's own `Model`.
|
|
85
|
+
*
|
|
86
|
+
* `current()` reads `ctx.model`, which is a live GETTER on the extension context
|
|
87
|
+
* (pi's `core/extensions/runner.js`), so a read after a set is the new value.
|
|
88
|
+
* `resolve` goes through `find(provider, id)` — EXACT, deliberately stricter
|
|
89
|
+
* than pi's own CLI, which also substring-matches. We store a canonical
|
|
90
|
+
* `provider/id`, so exact is the only match that should ever count, and being
|
|
91
|
+
* stricter here can only cost us a hold we then decline to take.
|
|
92
|
+
*/
|
|
93
|
+
function piModelControl(ctx) {
|
|
94
|
+
const api = piApi;
|
|
95
|
+
return {
|
|
96
|
+
current: () => {
|
|
97
|
+
const m = ctx.model;
|
|
98
|
+
return m ? { spec: `${m.provider}/${m.id}`, handle: m } : undefined;
|
|
99
|
+
},
|
|
100
|
+
resolve: spec => {
|
|
101
|
+
const parts = splitSpec(spec);
|
|
102
|
+
return parts ? ctx.modelRegistry.find(parts.provider, parts.id) : undefined;
|
|
103
|
+
},
|
|
104
|
+
apply: async (handle) => (api ? api.setModel(handle) : false)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Both halves of the implementation hold, over the live session. */
|
|
108
|
+
export function piImplementationControls(ctx) {
|
|
109
|
+
return { thinking: piThinkingControl(), model: piModelControl(ctx) };
|
|
77
110
|
}
|
|
78
111
|
// ─── TaskRunner class ────────────────────────────────────────────────────────
|
|
79
112
|
/** Encapsulates the full lifecycle of a single pi-task run. */
|
|
@@ -364,12 +397,24 @@ export class TaskRunner {
|
|
|
364
397
|
};
|
|
365
398
|
if (this._sendSpec) {
|
|
366
399
|
armImplWidget(meta, { oneShot: !this._implAwaited });
|
|
400
|
+
// Same lifetime as the widget, and for the same reason: an awaited run
|
|
401
|
+
// spans resume and steer turns, a fire-and-forget one does not.
|
|
402
|
+
armImplementationGuard({ oneShot: !this._implAwaited });
|
|
403
|
+
let delivered = false;
|
|
367
404
|
try {
|
|
368
405
|
await this._sendSpec(spec);
|
|
406
|
+
delivered = true;
|
|
369
407
|
}
|
|
370
408
|
finally {
|
|
371
|
-
|
|
409
|
+
// Arming precedes delivery because the turn can begin inside it. So a
|
|
410
|
+
// delivery that THREW leaves a guard watching a turn that will never
|
|
411
|
+
// run: pi's `prompt` rejects on a compaction already in progress, a
|
|
412
|
+
// missing model, or a failed auth. The next unrelated turn would
|
|
413
|
+
// inherit it, and this guard can end a turn outright.
|
|
414
|
+
if (this._implAwaited || !delivered) {
|
|
372
415
|
disarmImplWidget();
|
|
416
|
+
disarmImplementationGuard();
|
|
417
|
+
}
|
|
373
418
|
}
|
|
374
419
|
return;
|
|
375
420
|
}
|
|
@@ -377,12 +422,24 @@ export class TaskRunner {
|
|
|
377
422
|
throw new Error('extension not initialised (no ExtensionAPI captured)');
|
|
378
423
|
}
|
|
379
424
|
armImplWidget(meta, { oneShot: true });
|
|
425
|
+
armImplementationGuard({ oneShot: true });
|
|
426
|
+
// Same reason as the awaited path's `delivered` flag: this send can throw
|
|
427
|
+
// SYNCHRONOUSLY — the loader gates every ExtensionAPI action behind
|
|
428
|
+
// `assertActive()` — and a guard left armed over a turn that never starts
|
|
429
|
+
// is inherited by the next one, which it can terminate.
|
|
380
430
|
// Always name a delivery mode. pi's `prompt()` consults `streamingBehavior`
|
|
381
431
|
// only inside `if (this.isStreaming)`, so naming one is inert on an idle
|
|
382
432
|
// session and queues on a busy one — correct in both cases, where an
|
|
383
433
|
// isIdle() check is a check-then-act race that loses to any turn starting
|
|
384
434
|
// in between.
|
|
385
|
-
|
|
435
|
+
try {
|
|
436
|
+
piApi.sendUserMessage(spec, { deliverAs: 'followUp' });
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
disarmImplWidget();
|
|
440
|
+
disarmImplementationGuard();
|
|
441
|
+
throw e;
|
|
442
|
+
}
|
|
386
443
|
}
|
|
387
444
|
/**
|
|
388
445
|
* The spec as the implementer should receive it (Layer B). Layer A strips phantom
|
|
@@ -462,7 +519,7 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
462
519
|
// The autofix re-runner (gateRunTask) re-enters runSingleTask
|
|
463
520
|
// and so re-enters this closure, which is why the hold lives
|
|
464
521
|
// here rather than at either call site.
|
|
465
|
-
const release =
|
|
522
|
+
const release = await holdImplementation(opts.implementationControls ?? piImplementationControls(newCtx));
|
|
466
523
|
try {
|
|
467
524
|
// Queue-or-run: naming a delivery mode means pi's
|
|
468
525
|
// "Agent is already processing" throw is unreachable here.
|
|
@@ -482,7 +539,7 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
482
539
|
}
|
|
483
540
|
}
|
|
484
541
|
finally {
|
|
485
|
-
release();
|
|
542
|
+
await release();
|
|
486
543
|
}
|
|
487
544
|
},
|
|
488
545
|
seams: opts.seams,
|
package/dist/task/phases.js
CHANGED
|
@@ -38,7 +38,7 @@ import { readContracts, buildContractsBlock, buildContractsVerifyBlock } from '.
|
|
|
38
38
|
import { readRequirements, buildRequirementsBlock, buildOwnedRequirementsBlock, readOwnedRequirements, writeOwnedRequirements, ownedForTitle, appendOwnedConstraints } from './requirements.js';
|
|
39
39
|
import { detachUnsatisfiableRequirements, claimPendingRequirements, unclaimedPendingRequirements, formatReassignActions } from './owned-freeze-reassign.js';
|
|
40
40
|
import { trackedSourceOracle } from './owned-freeze-conflict.js';
|
|
41
|
-
import {
|
|
41
|
+
import { groupArgsForChild, runPhaseChild, runWithEmphasisRetry, prependHint, USER_CANCELLED, CommandTimeoutError, isFatalChildCause } from './child-runner.js';
|
|
42
42
|
import { runResearchWorker, researchWorkerCacheHeading } from './research-worker.js';
|
|
43
43
|
import { SessionUI } from '../remote/bridge.js';
|
|
44
44
|
import { isYoloMode, yoloPickAutoAnswer } from './yolo.js';
|
|
@@ -301,7 +301,16 @@ export async function phaseVerifyTooling(deps, research) {
|
|
|
301
301
|
try {
|
|
302
302
|
verifyOutput = await runPhaseChild(deps, 'verify-tooling', 'read,bash', VERIFY_TOOLING_PROMPT(toolingList));
|
|
303
303
|
}
|
|
304
|
-
catch {
|
|
304
|
+
catch (e) {
|
|
305
|
+
if (isFatalChildCause(e))
|
|
306
|
+
throw e;
|
|
307
|
+
// The fallback ships the tooling list UNVERIFIED, which is the right
|
|
308
|
+
// degrade for a child that merely failed. A hung command is different: it
|
|
309
|
+
// cost the ceiling on every strike and says the SPEC named something
|
|
310
|
+
// unbounded, so it is the one cause worth a trail line rather than silence.
|
|
311
|
+
if (e instanceof CommandTimeoutError) {
|
|
312
|
+
deps.logDebug?.(`verify-tooling: ${e.message} — shipping the list unverified`);
|
|
313
|
+
}
|
|
305
314
|
return replaceToolingWithVerified(research, commands);
|
|
306
315
|
}
|
|
307
316
|
const parsed = parseVerifyToolingOutput(verifyOutput);
|
|
@@ -645,7 +654,7 @@ export async function phaseResearch(deps, refined) {
|
|
|
645
654
|
// questions, so one shared `research` cell is the tempting shape —
|
|
646
655
|
// but the four cells do not ship identical, so sharing one would
|
|
647
656
|
// silently change three of them. See config/reasoning.ts.
|
|
648
|
-
|
|
657
|
+
groupArgsFor: groupArgsForChild,
|
|
649
658
|
logDebug: deps.logDebug,
|
|
650
659
|
onChildOutput: deps.onChildOutput,
|
|
651
660
|
record: recordWorker,
|
|
@@ -761,7 +770,9 @@ export async function phaseAutoAnswer(deps, refined, research, question) {
|
|
|
761
770
|
if (autoAnswerHasTag(text2))
|
|
762
771
|
reasked = parseAutoAnswer(text2);
|
|
763
772
|
}
|
|
764
|
-
catch {
|
|
773
|
+
catch (e) {
|
|
774
|
+
if (isFatalChildCause(e))
|
|
775
|
+
throw e;
|
|
765
776
|
reasked = null;
|
|
766
777
|
}
|
|
767
778
|
if (reasked === null
|
|
@@ -1024,7 +1035,9 @@ extraDefects) {
|
|
|
1024
1035
|
// needs no file access.
|
|
1025
1036
|
verdict = await runPhaseChild(deps, 'critique-triage', '', CRITIQUE_TRIAGE_PROMPT(spec, refined, qa, contractsBlock));
|
|
1026
1037
|
}
|
|
1027
|
-
catch {
|
|
1038
|
+
catch (e) {
|
|
1039
|
+
if (isFatalChildCause(e))
|
|
1040
|
+
throw e;
|
|
1028
1041
|
verdict = null;
|
|
1029
1042
|
}
|
|
1030
1043
|
deps.recordSubStep?.('triage', Date.now() - tTriage);
|
|
@@ -24,7 +24,7 @@ import type { DebugLine } from './debug-log.js';
|
|
|
24
24
|
/**
|
|
25
25
|
* One research worker's row. `section` is the heading its output is assembled
|
|
26
26
|
* and cached under; `label` is its child NAME — what the loader and the debug
|
|
27
|
-
* trail print, and the key into `
|
|
27
|
+
* trail print, and the key into `GROUP_BY_CHILD` (config/groups.ts).
|
|
28
28
|
*/
|
|
29
29
|
export interface ResearchWorkerSpec {
|
|
30
30
|
section: string;
|
|
@@ -72,7 +72,7 @@ export interface ResearchWorkerRun {
|
|
|
72
72
|
signal: AbortSignal;
|
|
73
73
|
spawn?: SpawnFn;
|
|
74
74
|
/** The `--thinking` fragment for a named child. */
|
|
75
|
-
|
|
75
|
+
groupArgsFor: (label: string) => string[];
|
|
76
76
|
logDebug?: (msg: string, kind?: DebugLine) => void;
|
|
77
77
|
onChildOutput?: (line: string) => void;
|
|
78
78
|
/** Record one finished worker's timing splits. */
|
|
@@ -241,7 +241,7 @@ export async function runResearchWorker(spec, run, prior = []) {
|
|
|
241
241
|
// config/reasoning.ts is where each one's level lives, so this
|
|
242
242
|
// line decides what THIS worker runs at for a default-mode
|
|
243
243
|
// user.
|
|
244
|
-
|
|
244
|
+
groupArgs: run.groupArgsFor(spec.label),
|
|
245
245
|
...(spec.tools ? { tools: spec.tools } : {}),
|
|
246
246
|
...(spec.extensions ? { extensions: spec.extensions } : {}),
|
|
247
247
|
// The three lever spreads that would otherwise sit here are the
|
|
@@ -11,7 +11,7 @@ import { runChild } from '../shared/child-process.js';
|
|
|
11
11
|
import { docsLookup } from './docs-lookup.js';
|
|
12
12
|
import { buildExtractionPrompt } from './abstention.js';
|
|
13
13
|
import {} from '../shared/child-output.js';
|
|
14
|
-
import {
|
|
14
|
+
import { groupChildArgs } from '../config/group-args.js';
|
|
15
15
|
const DEFAULT_LIMIT = PACKAGE_RETRIEVE_LIMIT;
|
|
16
16
|
const DEFAULT_BUDGET = RETRIEVE_CONTENT_BUDGET;
|
|
17
17
|
const NO_CACHE_HEAD = 25_000;
|
|
@@ -550,7 +550,7 @@ export async function docsFocused(input) {
|
|
|
550
550
|
spawn,
|
|
551
551
|
// The `extraction` group's level. Resolved at the call site so neither
|
|
552
552
|
// the lookup nor the extractor reads ambient config.
|
|
553
|
-
|
|
553
|
+
groupArgs: groupChildArgs('extraction')
|
|
554
554
|
});
|
|
555
555
|
const extraction = r.extraction;
|
|
556
556
|
const base = {
|
|
@@ -36,10 +36,11 @@ export interface DocsLookupInput {
|
|
|
36
36
|
signal?: AbortSignal;
|
|
37
37
|
spawn?: SpawnFn;
|
|
38
38
|
/**
|
|
39
|
-
* The `extraction` group's
|
|
40
|
-
* this module
|
|
39
|
+
* The `extraction` group's argv fragment — its model and its thinking level.
|
|
40
|
+
* Resolved by the CALLER so this module, like the extractor it wraps, never
|
|
41
|
+
* reads ambient config.
|
|
41
42
|
*/
|
|
42
|
-
|
|
43
|
+
groupArgs: readonly string[];
|
|
43
44
|
}
|
|
44
45
|
export type DocsLookup = {
|
|
45
46
|
kind: 'answer';
|
|
@@ -2,7 +2,7 @@ import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
|
|
|
2
2
|
import { runFocusedExtraction } from './focused-extractor.js';
|
|
3
3
|
import { abstentionSentence } from './abstention.js';
|
|
4
4
|
import {} from '../shared/child-output.js';
|
|
5
|
-
import {
|
|
5
|
+
import { groupChildArgs } from '../config/group-args.js';
|
|
6
6
|
const CONTENT_BUDGET = 30_000;
|
|
7
7
|
const HEAD_CHARS = 25_000;
|
|
8
8
|
const TAIL_CHARS = 5_000;
|
|
@@ -79,7 +79,7 @@ export async function fetchFocused(input) {
|
|
|
79
79
|
spawn: input.spawn,
|
|
80
80
|
// The `extraction` group's level. Resolved at the call site so the
|
|
81
81
|
// extractor itself never reads ambient config.
|
|
82
|
-
|
|
82
|
+
groupArgs: groupChildArgs('extraction'),
|
|
83
83
|
abortedMessage: 'Fetch aborted.'
|
|
84
84
|
});
|
|
85
85
|
const base = {
|
|
@@ -3,12 +3,13 @@ import { type ExcerptVerification } from '../shared/child-output.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* The argv every focused extraction child runs with: the shared child base — any whitelisted
|
|
5
5
|
* extensions, then `--print --no-skills --no-extensions --no-prompt-templates
|
|
6
|
-
* --no-context-files --no-session` — then the caller's
|
|
6
|
+
* --no-context-files --no-session` — then the caller's group fragment (its model and its
|
|
7
|
+
* thinking level), then `--no-tools`.
|
|
7
8
|
*
|
|
8
9
|
* `--no-tools` is the contract, not a default: the child is given all the content it may use
|
|
9
10
|
* inside its prompt, so a tool call could only reach for something unsourced.
|
|
10
11
|
*/
|
|
11
|
-
export declare const focusedChildArgs: (
|
|
12
|
+
export declare const focusedChildArgs: (groupArgs?: readonly string[]) => string[];
|
|
12
13
|
export interface FocusedRequest {
|
|
13
14
|
/** The fully assembled prompt, including the content block. Delivered on stdin. */
|
|
14
15
|
prompt: string;
|
|
@@ -42,7 +43,7 @@ export interface FocusedRequest {
|
|
|
42
43
|
* `focusedChildArgs` read ambient config, and a function that reads config
|
|
43
44
|
* internally cannot be tested without the developer's own machine state.
|
|
44
45
|
*/
|
|
45
|
-
|
|
46
|
+
groupArgs?: readonly string[];
|
|
46
47
|
}
|
|
47
48
|
/** What every outcome carries, success or failure — the raw child evidence. */
|
|
48
49
|
interface FocusedChildEvidence {
|
|
@@ -78,7 +79,8 @@ export type FocusedResult = FocusedFailure | FocusedAnswer;
|
|
|
78
79
|
* `<answer>`/`<excerpt>` and verify the excerpt against `verifyAgainst`.
|
|
79
80
|
*
|
|
80
81
|
* Never retries — a re-ask of a deterministic extraction over unchanged content is a second
|
|
81
|
-
* bill for the same answer. Exactly one child is spawned per call
|
|
82
|
+
* bill for the same answer. Exactly one child is spawned per call, on every path including
|
|
83
|
+
* the empty-output failure below.
|
|
82
84
|
*/
|
|
83
85
|
export declare function runFocusedExtraction(req: FocusedRequest): Promise<FocusedResult>;
|
|
84
86
|
export {};
|
|
@@ -29,14 +29,15 @@ import { formatChildFailure } from './shared.js';
|
|
|
29
29
|
/**
|
|
30
30
|
* The argv every focused extraction child runs with: the shared child base — any whitelisted
|
|
31
31
|
* extensions, then `--print --no-skills --no-extensions --no-prompt-templates
|
|
32
|
-
* --no-context-files --no-session` — then the caller's
|
|
32
|
+
* --no-context-files --no-session` — then the caller's group fragment (its model and its
|
|
33
|
+
* thinking level), then `--no-tools`.
|
|
33
34
|
*
|
|
34
35
|
* `--no-tools` is the contract, not a default: the child is given all the content it may use
|
|
35
36
|
* inside its prompt, so a tool call could only reach for something unsourced.
|
|
36
37
|
*/
|
|
37
|
-
export const focusedChildArgs = (
|
|
38
|
+
export const focusedChildArgs = (groupArgs = []) => [
|
|
38
39
|
...childBaseArgs(),
|
|
39
|
-
...
|
|
40
|
+
...groupArgs,
|
|
40
41
|
'--no-tools'
|
|
41
42
|
];
|
|
42
43
|
/**
|
|
@@ -44,11 +45,12 @@ export const focusedChildArgs = (thinking = []) => [
|
|
|
44
45
|
* `<answer>`/`<excerpt>` and verify the excerpt against `verifyAgainst`.
|
|
45
46
|
*
|
|
46
47
|
* Never retries — a re-ask of a deterministic extraction over unchanged content is a second
|
|
47
|
-
* bill for the same answer. Exactly one child is spawned per call
|
|
48
|
+
* bill for the same answer. Exactly one child is spawned per call, on every path including
|
|
49
|
+
* the empty-output failure below.
|
|
48
50
|
*/
|
|
49
51
|
export async function runFocusedExtraction(req) {
|
|
50
52
|
const spawn = req.spawn ?? defaultSpawn;
|
|
51
|
-
const invocation = getPiInvocation(focusedChildArgs(req.
|
|
53
|
+
const invocation = getPiInvocation(focusedChildArgs(req.groupArgs), req.prompt);
|
|
52
54
|
const child = await runChild(spawn, invocation, req.cwd, req.signal);
|
|
53
55
|
const evidence = {
|
|
54
56
|
exitCode: child.exitCode,
|
|
@@ -59,6 +61,16 @@ export async function runFocusedExtraction(req) {
|
|
|
59
61
|
const failure = formatChildFailure(child, req.abortedMessage);
|
|
60
62
|
if (failure !== null)
|
|
61
63
|
return { ok: false, failure, ...evidence };
|
|
64
|
+
// A dropped provider exits 0 with empty text, and TEXT mode never populates
|
|
65
|
+
// `modelError`. Returned as an empty answer it is MEMOISED: both caches keep
|
|
66
|
+
// it and re-serve a dead socket as a real answer for the rest of the run.
|
|
67
|
+
if (child.stdout.trim().length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
failure: 'Worker exited 0 without writing anything — no answer to parse',
|
|
71
|
+
...evidence
|
|
72
|
+
};
|
|
73
|
+
}
|
|
62
74
|
const parsed = parseChildOutput(child.stdout);
|
|
63
75
|
const excerptCheck = parsed.excerpt ? verifyExcerpt(parsed.excerpt, req.verifyAgainst) : undefined;
|
|
64
76
|
return {
|
package/dist/workers/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { registerPiWorkerFetch } from './pi-worker-fetch.js';
|
|
|
4
4
|
import { registerPiWorkerDocs } from './pi-worker-docs.js';
|
|
5
5
|
import { registerBraveKeyWarning } from './brave-warning.js';
|
|
6
6
|
import { registerReasoningWarning } from './reasoning-warning.js';
|
|
7
|
+
import { registerModelWarning } from './model-warning.js';
|
|
7
8
|
export function registerWorkers(pi) {
|
|
8
9
|
registerPiWorker(pi);
|
|
9
10
|
registerPiWorkerSearch(pi);
|
|
@@ -11,4 +12,5 @@ export function registerWorkers(pi) {
|
|
|
11
12
|
registerPiWorkerDocs(pi);
|
|
12
13
|
registerBraveKeyWarning(pi);
|
|
13
14
|
registerReasoningWarning(pi);
|
|
15
|
+
registerModelWarning(pi);
|
|
14
16
|
}
|