@mjasnikovs/pi-task 0.38.17 → 0.38.19
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/config/config.d.ts +11 -0
- package/dist/config/config.js +13 -1
- package/dist/shared/child-process.d.ts +19 -0
- package/dist/shared/child-process.js +9 -10
- package/dist/task/child-runner.d.ts +18 -1
- package/dist/task/child-runner.js +18 -3
- package/dist/task/child-status.d.ts +11 -3
- package/dist/task/child-status.js +14 -3
- package/dist/task/context-usage.d.ts +1 -1
- package/dist/task/context-usage.js +1 -1
- package/dist/task/gate-child.js +4 -1
- package/dist/task/orchestrator.js +4 -0
- package/dist/task/stall-detector.d.ts +5 -0
- package/dist/task/stall-detector.js +5 -0
- package/dist/task/widget.js +41 -9
- package/dist/workers/pi-worker-core.d.ts +11 -4
- package/dist/workers/pi-worker-core.js +4 -1
- package/package.json +4 -4
package/dist/config/config.d.ts
CHANGED
|
@@ -215,5 +215,16 @@ export declare const CONFIG_LOADERS: {
|
|
|
215
215
|
* into the config object.
|
|
216
216
|
*/
|
|
217
217
|
export declare function loadConfig(raw: unknown): PiTaskConfig;
|
|
218
|
+
/**
|
|
219
|
+
* Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
|
|
220
|
+
*
|
|
221
|
+
* The override exists because this module loads the real file at import time,
|
|
222
|
+
* which made every test that reads a config value depend on the developer's own
|
|
223
|
+
* `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
|
|
224
|
+
* 12 tests here while CI (no config file at all) stayed green. The test preload
|
|
225
|
+
* points this at a path under the tmp dir that never exists, so tests always see
|
|
226
|
+
* DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
|
|
227
|
+
*/
|
|
228
|
+
export declare const CONFIG_PATH_ENV = "PI_TASK_CONFIG_PATH";
|
|
218
229
|
export declare function getConfig(): PiTaskConfig;
|
|
219
230
|
export declare function saveConfig(config: PiTaskConfig): Promise<void>;
|
package/dist/config/config.js
CHANGED
|
@@ -182,7 +182,19 @@ export function loadConfig(raw) {
|
|
|
182
182
|
}
|
|
183
183
|
return out;
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
|
|
187
|
+
*
|
|
188
|
+
* The override exists because this module loads the real file at import time,
|
|
189
|
+
* which made every test that reads a config value depend on the developer's own
|
|
190
|
+
* `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
|
|
191
|
+
* 12 tests here while CI (no config file at all) stayed green. The test preload
|
|
192
|
+
* points this at a path under the tmp dir that never exists, so tests always see
|
|
193
|
+
* DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
|
|
194
|
+
*/
|
|
195
|
+
export const CONFIG_PATH_ENV = 'PI_TASK_CONFIG_PATH';
|
|
196
|
+
const CONFIG_PATH = process.env[CONFIG_PATH_ENV]?.trim()
|
|
197
|
+
|| path.join(os.homedir(), '.config', 'pi-task', 'config.json');
|
|
186
198
|
const _g = globalThis;
|
|
187
199
|
if (!_g.__piTaskConfig) {
|
|
188
200
|
_g.__piTaskConfig = { config: { ...DEFAULT_CONFIG }, loaded: false };
|
|
@@ -115,6 +115,25 @@ export interface RunChildJsonEventsOptions {
|
|
|
115
115
|
mode: 'json-events';
|
|
116
116
|
onLine?: (line: string) => void;
|
|
117
117
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
118
|
+
/**
|
|
119
|
+
* The child's context window in tokens, supplied BY THE CALLER — pi's event
|
|
120
|
+
* stream does not carry one (GitHub issue #16).
|
|
121
|
+
*
|
|
122
|
+
* Verified against the published tarballs of @earendil-works/pi-coding-agent
|
|
123
|
+
* and @earendil-works/pi-agent-core at 0.80.2 and 0.84.2, and against pi's
|
|
124
|
+
* own docs/json.md: the wire union is session / agent_* / turn_* / message_*
|
|
125
|
+
* / tool_execution_* / queue_update / compaction_* / auto_retry_*, the
|
|
126
|
+
* session header is {type,version,id,timestamp,cwd,parentSession}, and no
|
|
127
|
+
* member of either carries a window or even a model id. `contextUsage`
|
|
128
|
+
* exists ONLY as the in-process `ctx.getContextUsage()` extension API, which
|
|
129
|
+
* a `--mode json` child never speaks back to its parent.
|
|
130
|
+
*
|
|
131
|
+
* The parent therefore has to say. Children are spawned without `-m`
|
|
132
|
+
* (CHILD_BASE_ARGS), so they resolve the same default model the parent runs
|
|
133
|
+
* and the parent session's window is the honest answer. 0 / omitted keeps
|
|
134
|
+
* the old behaviour: report the token count with no window.
|
|
135
|
+
*/
|
|
136
|
+
contextWindow?: number;
|
|
118
137
|
onToolCall?: (call: ToolCall) => LoopHit | null;
|
|
119
138
|
/**
|
|
120
139
|
* Fires when a tool call finishes, carrying its RESULT (mx5 run 10 item 6: the
|
|
@@ -88,15 +88,12 @@ export class JsonEventSink {
|
|
|
88
88
|
handleEvent(evt) {
|
|
89
89
|
const opts = this.opts;
|
|
90
90
|
const t = typeof evt.type === 'string' ? evt.type : '';
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
91
|
+
// `message_end` is the ONLY context readout pi gives a `--mode json`
|
|
92
|
+
// child's parent. There used to be a `context_usage` branch above this
|
|
93
|
+
// one that was preferred over it; no released pi has ever emitted such
|
|
94
|
+
// an event (see `contextWindow` on RunChildJsonEventsOptions), so it was
|
|
95
|
+
// unreachable, and its presence is what made the zero window below look
|
|
96
|
+
// like a harmless fallback rather than the only path. Issue #16.
|
|
100
97
|
if (t === 'message_end' && opts.onContextUsage) {
|
|
101
98
|
const msg = evt.message;
|
|
102
99
|
if (msg?.role === 'assistant') {
|
|
@@ -107,7 +104,9 @@ export class JsonEventSink {
|
|
|
107
104
|
+ Number(usage.cacheWrite ?? 0)
|
|
108
105
|
+ Number(usage.output ?? 0);
|
|
109
106
|
if (tokens > 0) {
|
|
110
|
-
|
|
107
|
+
const cw = Math.max(0, Number(opts.contextWindow ?? 0));
|
|
108
|
+
const percent = cw > 0 ? Math.min(100, (tokens / cw) * 100) : 0;
|
|
109
|
+
opts.onContextUsage({ tokens, contextWindow: cw, percent });
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
}
|
|
@@ -82,13 +82,30 @@ extensions?: readonly string[],
|
|
|
82
82
|
* needs the size of what actually entered the child's context, which the
|
|
83
83
|
* CALL alone does not carry (task/stall-detector.ts).
|
|
84
84
|
*/
|
|
85
|
-
onToolResult?: (text: string, isError: boolean) => void
|
|
85
|
+
onToolResult?: (text: string, isError: boolean) => void,
|
|
86
|
+
/**
|
|
87
|
+
* The child's context window in tokens. Nothing in pi's event stream reports
|
|
88
|
+
* one (issue #16), so the parent hands its own down — children carry no `-m`
|
|
89
|
+
* and resolve the same default model. 0 / omitted = unknown, as before.
|
|
90
|
+
*/
|
|
91
|
+
contextWindow?: number): Promise<PhaseRunResult>;
|
|
86
92
|
interface PhaseDeps {
|
|
87
93
|
cwd: string;
|
|
88
94
|
taskId: string;
|
|
89
95
|
signal: AbortSignal;
|
|
90
96
|
onChildOutput?: (line: string) => void;
|
|
91
97
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
98
|
+
/**
|
|
99
|
+
* The parent session's context window in tokens, handed down to every child.
|
|
100
|
+
*
|
|
101
|
+
* pi's `--mode json` stream reports token counts but no window (issue #16),
|
|
102
|
+
* so without this the gauge shows a bare number and — worse — the
|
|
103
|
+
* StallDetector's CONTEXT CHURN rule, which is gated on a positive window,
|
|
104
|
+
* can never fire. Children are spawned without `-m` (CHILD_BASE_ARGS) and so
|
|
105
|
+
* run the parent's own default model; its window is the honest value.
|
|
106
|
+
* Absent = unknown, and both consumers degrade exactly as they did before.
|
|
107
|
+
*/
|
|
108
|
+
contextWindow?: number;
|
|
92
109
|
/**
|
|
93
110
|
* Record a sub-step duration under the currently running top-level phase.
|
|
94
111
|
* The orchestrator rebinds this between phases so each call lands in the
|
|
@@ -172,7 +172,13 @@ extensions,
|
|
|
172
172
|
* needs the size of what actually entered the child's context, which the
|
|
173
173
|
* CALL alone does not carry (task/stall-detector.ts).
|
|
174
174
|
*/
|
|
175
|
-
onToolResult
|
|
175
|
+
onToolResult,
|
|
176
|
+
/**
|
|
177
|
+
* The child's context window in tokens. Nothing in pi's event stream reports
|
|
178
|
+
* one (issue #16), so the parent hands its own down — children carry no `-m`
|
|
179
|
+
* and resolve the same default model. 0 / omitted = unknown, as before.
|
|
180
|
+
*/
|
|
181
|
+
contextWindow) {
|
|
176
182
|
const invocation = getPiInvocation(childArgs(tools, extensions), prompt);
|
|
177
183
|
let loopHit;
|
|
178
184
|
const result = await runChildUnified(spawnFn ?? spawn, invocation, cwd, signal, {
|
|
@@ -184,6 +190,7 @@ onToolResult) {
|
|
|
184
190
|
streamInactivityMs: getConfig().streamInactivityMs,
|
|
185
191
|
onLine,
|
|
186
192
|
onContextUsage,
|
|
193
|
+
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
187
194
|
onToolResult: onToolResult ? r => onToolResult(r.text, r.isError) : undefined,
|
|
188
195
|
onToolCall: call => {
|
|
189
196
|
if (!onToolCall)
|
|
@@ -316,13 +323,21 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
|
|
|
316
323
|
throw new Error(USER_CANCELLED);
|
|
317
324
|
const detector = new LoopDetector(LOOP_WINDOW, LOOP_THRESHOLD);
|
|
318
325
|
const stall = new StallDetector();
|
|
326
|
+
// Arm the churn rule BEFORE the first tool call. The window used to reach
|
|
327
|
+
// the detector only through a context snapshot, and pi's stream never
|
|
328
|
+
// carries one, so it was always 0 and rule 2 never fired (issue #16). The
|
|
329
|
+
// parent knows the value at spawn time — say it then, not later.
|
|
330
|
+
stall.noteContext(deps.contextWindow ?? 0);
|
|
319
331
|
const clock = phaseTimeout(deps.signal, budgetMs);
|
|
320
332
|
let r;
|
|
321
333
|
try {
|
|
322
334
|
r = await runChild(deps.cwd, tools, prependHint(hint, prompt), clock.signal, deps.onChildOutput, snapshot => {
|
|
335
|
+
// Real window or nothing: noteContext ignores 0, and until
|
|
336
|
+
// deps.contextWindow existed 0 was all it ever saw, which
|
|
337
|
+
// left the churn rule permanently disarmed (issue #16).
|
|
323
338
|
stall.noteContext(snapshot.contextWindow);
|
|
324
339
|
deps.onContextUsage?.(snapshot);
|
|
325
|
-
}, call => detector.record(call) ?? stall.record(call), deps.spawn, deps.childExtensions, (text, isError) => stall.noteResult(text, isError));
|
|
340
|
+
}, call => detector.record(call) ?? stall.record(call), deps.spawn, deps.childExtensions, (text, isError) => stall.noteResult(text, isError), deps.contextWindow);
|
|
326
341
|
}
|
|
327
342
|
finally {
|
|
328
343
|
clock.cleanup();
|
|
@@ -430,7 +445,7 @@ async function appendLoopEvent(cwd, taskId, phase, hit, strike, outcome) {
|
|
|
430
445
|
async function runDegradedFinalAttempt(deps, name, prompt, hit, loopHistory) {
|
|
431
446
|
deps.logDebug?.(`${name}: loop budget exhausted — degrading to a no-tools final attempt`);
|
|
432
447
|
const r = await runChild(deps.cwd, '', // --no-tools: the model cannot read/grep/list, only answer
|
|
433
|
-
prependHint(formatDegradeHint(hit), prompt), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn);
|
|
448
|
+
prependHint(formatDegradeHint(hit), prompt), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn, undefined, undefined, deps.contextWindow);
|
|
434
449
|
if (r.exitCode !== 0 || r.modelError || r.text.trim().length === 0) {
|
|
435
450
|
throw new LoopExhaustedError(name, loopHistory);
|
|
436
451
|
}
|
|
@@ -34,10 +34,15 @@ export declare class ChildStatus {
|
|
|
34
34
|
private readonly _parentContextWindow;
|
|
35
35
|
private readonly _startLoader;
|
|
36
36
|
constructor(deps: ChildStatusDeps);
|
|
37
|
+
/**
|
|
38
|
+
* The parent session's window — the value handed DOWN to each child so its
|
|
39
|
+
* own readout carries one, and the last fallback when a child reports none.
|
|
40
|
+
*/
|
|
41
|
+
get parentContextWindow(): number;
|
|
37
42
|
/** The child's latest stream line. Bind as `onChildOutput` / `onLine`. */
|
|
38
43
|
onLine(line: string): void;
|
|
39
44
|
/**
|
|
40
|
-
* Fold a raw
|
|
45
|
+
* Fold a raw context snapshot into the gauge: the child's own window,
|
|
41
46
|
* else the last known one, else the parent's (`resolveContextUsage`).
|
|
42
47
|
*/
|
|
43
48
|
onContextUsage(snapshot: ContextSnapshot): void;
|
|
@@ -91,5 +96,8 @@ export declare function runPlanningChild(opts: {
|
|
|
91
96
|
prompt: string;
|
|
92
97
|
loader: PlanningChildLoader;
|
|
93
98
|
}): Promise<string>;
|
|
94
|
-
/**
|
|
95
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Wire a `ChildStatus` as a phase child's stream callbacks — plus the window the
|
|
101
|
+
* child must be TOLD, since pi's event stream never reports one (issue #16).
|
|
102
|
+
*/
|
|
103
|
+
export declare function statusCallbacks(status: ChildStatus): Pick<PhaseDeps, 'onChildOutput' | 'onContextUsage' | 'contextWindow'>;
|
|
@@ -30,12 +30,19 @@ export class ChildStatus {
|
|
|
30
30
|
this._parentContextWindow = deps.parentContextWindow;
|
|
31
31
|
this._startLoader = deps.startLoader ?? startAutoLoader;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The parent session's window — the value handed DOWN to each child so its
|
|
35
|
+
* own readout carries one, and the last fallback when a child reports none.
|
|
36
|
+
*/
|
|
37
|
+
get parentContextWindow() {
|
|
38
|
+
return this._parentContextWindow;
|
|
39
|
+
}
|
|
33
40
|
/** The child's latest stream line. Bind as `onChildOutput` / `onLine`. */
|
|
34
41
|
onLine(line) {
|
|
35
42
|
this._lastLine = line;
|
|
36
43
|
}
|
|
37
44
|
/**
|
|
38
|
-
* Fold a raw
|
|
45
|
+
* Fold a raw context snapshot into the gauge: the child's own window,
|
|
39
46
|
* else the last known one, else the parent's (`resolveContextUsage`).
|
|
40
47
|
*/
|
|
41
48
|
onContextUsage(snapshot) {
|
|
@@ -90,10 +97,14 @@ export async function runPlanningChild(opts) {
|
|
|
90
97
|
startedAt
|
|
91
98
|
}), () => runPhaseChild(phaseDeps, name, tools, prompt));
|
|
92
99
|
}
|
|
93
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Wire a `ChildStatus` as a phase child's stream callbacks — plus the window the
|
|
102
|
+
* child must be TOLD, since pi's event stream never reports one (issue #16).
|
|
103
|
+
*/
|
|
94
104
|
export function statusCallbacks(status) {
|
|
95
105
|
return {
|
|
96
106
|
onChildOutput: line => status.onLine(line),
|
|
97
|
-
onContextUsage: snapshot => status.onContextUsage(snapshot)
|
|
107
|
+
onContextUsage: snapshot => status.onContextUsage(snapshot),
|
|
108
|
+
contextWindow: status.parentContextWindow
|
|
98
109
|
};
|
|
99
110
|
}
|
|
@@ -9,7 +9,7 @@ import type { ContextSnapshot } from '../shared/child-process.js';
|
|
|
9
9
|
/** The parent session's context window, or 0 when the model doesn't expose it. */
|
|
10
10
|
export declare function getParentContextWindow(ctx: ExtensionCommandContext): number;
|
|
11
11
|
/**
|
|
12
|
-
* Fold a raw
|
|
12
|
+
* Fold a raw context snapshot into a display snapshot: prefer the child's
|
|
13
13
|
* own contextWindow, else the last known one, else the parent session's; then
|
|
14
14
|
* derive percent against it — falling back to the child's reported percent when
|
|
15
15
|
* no window is known at all.
|
|
@@ -9,7 +9,7 @@ export function getParentContextWindow(ctx) {
|
|
|
9
9
|
return (ctx.model?.contextWindow ?? 0);
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Fold a raw
|
|
12
|
+
* Fold a raw context snapshot into a display snapshot: prefer the child's
|
|
13
13
|
* own contextWindow, else the last known one, else the parent session's; then
|
|
14
14
|
* derive percent against it — falling back to the child's reported percent when
|
|
15
15
|
* no window is known at all.
|
package/dist/task/gate-child.js
CHANGED
|
@@ -119,7 +119,10 @@ export function makeGateChild(deps) {
|
|
|
119
119
|
+ deps.truncateToolResult(text), 'stream')
|
|
120
120
|
}
|
|
121
121
|
: {}),
|
|
122
|
-
onContextUsage: snapshot => deps.status.onContextUsage(snapshot)
|
|
122
|
+
onContextUsage: snapshot => deps.status.onContextUsage(snapshot),
|
|
123
|
+
// The gate child has to be TOLD its window: nothing in pi's
|
|
124
|
+
// event stream reports one (issue #16).
|
|
125
|
+
contextWindow: deps.status.parentContextWindow
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
128
|
finally {
|
|
@@ -122,6 +122,10 @@ export class TaskRunner {
|
|
|
122
122
|
onChildOutput: (line) => {
|
|
123
123
|
this._widgetState.lastLine = line;
|
|
124
124
|
},
|
|
125
|
+
// Handed DOWN so the child's own snapshot carries a window and the
|
|
126
|
+
// StallDetector's churn rule can arm; resolveContextUsage below stays
|
|
127
|
+
// as the fallback for a child that still reports none (issue #16).
|
|
128
|
+
contextWindow: parentContextWindow,
|
|
125
129
|
onContextUsage: snapshot => {
|
|
126
130
|
this._widgetState.contextUsage = resolveContextUsage(snapshot, this._widgetState.contextUsage, parentContextWindow);
|
|
127
131
|
},
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
* rule 1 alone would not have caught it. The bound scales with the model's
|
|
39
39
|
* OWN window, so a 1M-context model gets a 1M-context allowance.
|
|
40
40
|
*
|
|
41
|
+
* The window is supplied by the PARENT at spawn (`PhaseDeps.contextWindow`),
|
|
42
|
+
* not read off the child's event stream: pi emits no context event at all,
|
|
43
|
+
* which is why this rule sat disarmed from the day it was written until
|
|
44
|
+
* GitHub issue #16.
|
|
45
|
+
*
|
|
41
46
|
* Neither rule can fire on a child that is thinking rather than calling tools:
|
|
42
47
|
* that case is bounded by the model's max tokens (server-enforced) and by the
|
|
43
48
|
* stream watchdog if the stream goes silent. Between the three there is no
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
* rule 1 alone would not have caught it. The bound scales with the model's
|
|
39
39
|
* OWN window, so a 1M-context model gets a 1M-context allowance.
|
|
40
40
|
*
|
|
41
|
+
* The window is supplied by the PARENT at spawn (`PhaseDeps.contextWindow`),
|
|
42
|
+
* not read off the child's event stream: pi emits no context event at all,
|
|
43
|
+
* which is why this rule sat disarmed from the day it was written until
|
|
44
|
+
* GitHub issue #16.
|
|
45
|
+
*
|
|
41
46
|
* Neither rule can fire on a child that is thinking rather than calling tools:
|
|
42
47
|
* that case is bounded by the model's max tokens (server-enforced) and by the
|
|
43
48
|
* stream watchdog if the stream goes silent. Between the three there is no
|
package/dist/task/widget.js
CHANGED
|
@@ -115,21 +115,35 @@ export function buildWidgetData(s) {
|
|
|
115
115
|
export function startWidget(ctx, getState) {
|
|
116
116
|
if (!ctx.hasUI)
|
|
117
117
|
return () => { };
|
|
118
|
+
// `ctx.ui` THROWS once the ctx goes stale (/reload, session replacement), so
|
|
119
|
+
// the theme read belongs INSIDE the guard, not one line above it. render()
|
|
120
|
+
// runs from a timer, where an unguarded throw is an uncaughtException that
|
|
121
|
+
// kills the whole pi process — and a swallowed one would throw again on
|
|
122
|
+
// every tick, so a stale ctx latches and stops the timer. Issue #15.
|
|
123
|
+
let stale = false;
|
|
118
124
|
const render = () => {
|
|
125
|
+
if (stale)
|
|
126
|
+
return;
|
|
119
127
|
const s = getState();
|
|
120
|
-
const
|
|
121
|
-
const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire
|
|
128
|
+
const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire; needs no ctx
|
|
122
129
|
try {
|
|
130
|
+
const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
|
|
123
131
|
ctx.ui.setWidget(WIDGET_KEY, lines);
|
|
124
132
|
}
|
|
125
133
|
catch {
|
|
126
|
-
|
|
134
|
+
stale = true;
|
|
135
|
+
clearInterval(timer);
|
|
136
|
+
return;
|
|
127
137
|
}
|
|
128
138
|
setTaskWidget(plain, s ? buildWidgetData(s) : null);
|
|
129
139
|
};
|
|
130
|
-
render
|
|
140
|
+
// The timer is created BEFORE the first render so `timer` is always bound
|
|
141
|
+
// when render's catch reaches for it: a ctx already stale on the very first
|
|
142
|
+
// paint now stops the loop there, instead of arming an interval that wakes
|
|
143
|
+
// up and returns early forever.
|
|
131
144
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
132
145
|
timer.unref?.();
|
|
146
|
+
render();
|
|
133
147
|
return () => {
|
|
134
148
|
clearInterval(timer);
|
|
135
149
|
try {
|
|
@@ -192,21 +206,32 @@ export function buildAutoLoaderData(s) {
|
|
|
192
206
|
export function startAutoLoader(ctx, getState) {
|
|
193
207
|
if (!ctx.hasUI)
|
|
194
208
|
return () => { };
|
|
209
|
+
// Same staleness hazard as startWidget: the theme read must sit inside the
|
|
210
|
+
// guard, and a stale ctx stops the timer rather than throwing every tick.
|
|
211
|
+
let stale = false;
|
|
195
212
|
const render = () => {
|
|
213
|
+
if (stale)
|
|
214
|
+
return;
|
|
196
215
|
const s = getState();
|
|
197
|
-
const
|
|
198
|
-
const plain = s ? buildAutoLoaderLines(s, undefined) : undefined;
|
|
216
|
+
const plain = s ? buildAutoLoaderLines(s, undefined) : undefined; // needs no ctx
|
|
199
217
|
try {
|
|
218
|
+
const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
|
|
200
219
|
ctx.ui.setWidget(AUTO_WIDGET_KEY, lines);
|
|
201
220
|
}
|
|
202
221
|
catch {
|
|
203
|
-
|
|
222
|
+
stale = true;
|
|
223
|
+
clearInterval(timer);
|
|
224
|
+
return;
|
|
204
225
|
}
|
|
205
226
|
setTaskWidget(plain, s ? buildAutoLoaderData(s) : null);
|
|
206
227
|
};
|
|
207
|
-
render
|
|
228
|
+
// The timer is created BEFORE the first render so `timer` is always bound
|
|
229
|
+
// when render's catch reaches for it: a ctx already stale on the very first
|
|
230
|
+
// paint now stops the loop there, instead of arming an interval that wakes
|
|
231
|
+
// up and returns early forever.
|
|
208
232
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
209
233
|
timer.unref?.();
|
|
234
|
+
render();
|
|
210
235
|
return () => {
|
|
211
236
|
clearInterval(timer);
|
|
212
237
|
try {
|
|
@@ -249,7 +274,14 @@ export function buildImplData(s) {
|
|
|
249
274
|
export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
250
275
|
if (!ctx.hasUI)
|
|
251
276
|
return;
|
|
252
|
-
|
|
277
|
+
// Same staleness hazard as the render timers: bail rather than throw.
|
|
278
|
+
let theme;
|
|
279
|
+
try {
|
|
280
|
+
theme = ctx.ui.theme;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
253
285
|
let line;
|
|
254
286
|
let clearMs;
|
|
255
287
|
if (state === 'cancelled') {
|
|
@@ -62,12 +62,19 @@ export interface RunWorkerInput {
|
|
|
62
62
|
toolCallId?: string;
|
|
63
63
|
}) => void;
|
|
64
64
|
/**
|
|
65
|
-
* Called for each
|
|
66
|
-
* stream the phase children parse). Lets a
|
|
67
|
-
* tokens/window progress bar for the worker
|
|
68
|
-
* instead of omitting it.
|
|
65
|
+
* Called for each context snapshot derived from the child's `message_end`
|
|
66
|
+
* events (same `--mode json` stream the phase children parse). Lets a
|
|
67
|
+
* caller's status widget show the tokens/window progress bar for the worker
|
|
68
|
+
* exactly like the phase widget, instead of omitting it.
|
|
69
69
|
*/
|
|
70
70
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
71
|
+
/**
|
|
72
|
+
* The worker child's context window in tokens. pi's event stream carries no
|
|
73
|
+
* window (issue #16), so a caller that wants a progress bar rather than a
|
|
74
|
+
* bare token count has to supply the parent session's — which is the child's
|
|
75
|
+
* too, since workers are spawned without `-m`.
|
|
76
|
+
*/
|
|
77
|
+
contextWindow?: number;
|
|
71
78
|
/**
|
|
72
79
|
* Per-worker wall-clock timeout in ms. Defaults to RESEARCH_WORKER_TIMEOUT_MS.
|
|
73
80
|
* Pass 0 to disable the timeout entirely (run until the child exits on its
|
|
@@ -549,7 +549,10 @@ export async function runWorker(input) {
|
|
|
549
549
|
cmdWatch?.onEnd(r.toolCallId);
|
|
550
550
|
input.onToolResult?.(r);
|
|
551
551
|
},
|
|
552
|
-
onContextUsage: input.onContextUsage
|
|
552
|
+
onContextUsage: input.onContextUsage,
|
|
553
|
+
...(input.contextWindow && input.contextWindow > 0 ?
|
|
554
|
+
{ contextWindow: input.contextWindow }
|
|
555
|
+
: {})
|
|
553
556
|
}, input.spawn);
|
|
554
557
|
}
|
|
555
558
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.19",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"ws": "8.21.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@earendil-works/pi-agent-core": "0.
|
|
36
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
37
|
-
"@earendil-works/pi-tui": "0.
|
|
35
|
+
"@earendil-works/pi-agent-core": "0.84.2",
|
|
36
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
37
|
+
"@earendil-works/pi-tui": "0.84.2",
|
|
38
38
|
"@eslint/js": "10.0.1",
|
|
39
39
|
"@types/bun": "1.3.12",
|
|
40
40
|
"@types/qrcode": "1.5.6",
|