@bacnh85/pi-advisor 0.2.1 → 0.2.2
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/CHANGELOG.md +12 -0
- package/extensions/lib/isolated-model.ts +41 -4
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 (2026-09-07)
|
|
4
|
+
|
|
5
|
+
- **Fix TUI hang on stalled reviewer provider**: every chain candidate now
|
|
6
|
+
runs under a 90s **idle** deadline — a candidate is treated as dead only
|
|
7
|
+
after 90s with no stream events, so a hung provider (accepts the
|
|
8
|
+
connection, never streams) advances the chain to the next model, while
|
|
9
|
+
healthy slow streams (reasoning models, large transcripts) are never
|
|
10
|
+
killed: any event resets the deadline. Previously a hung provider stalled
|
|
11
|
+
the awaited `agent_settled` review forever, blocking the prompt so
|
|
12
|
+
slash commands (e.g. `/plan-approve` after write_plan) could not run.
|
|
13
|
+
Caller abort (`/advisor` consult tool) still aborts without fall-through.
|
|
14
|
+
|
|
3
15
|
## 0.2.1 (2026-09-05)
|
|
4
16
|
|
|
5
17
|
- Widen Pi SDK peer range to `>=0.85.0 <0.86.0` and bump devDep to `^0.85.0` for Pi 0.85.0 compatibility (no breaking changes; peer cap widening only).
|
|
@@ -18,6 +18,8 @@ export async function runIsolated(
|
|
|
18
18
|
onDelta?: (delta: string) => void,
|
|
19
19
|
signal?: AbortSignal,
|
|
20
20
|
reasoning?: string,
|
|
21
|
+
/** Progress hook — every stream event (incl. non-text deltas) resets the caller's idle deadline. */
|
|
22
|
+
onEvent?: () => void,
|
|
21
23
|
): Promise<string> {
|
|
22
24
|
const parsed = modelId ? parseModel(modelId) : undefined;
|
|
23
25
|
if (modelId && !parsed) throw new Error(`Invalid model: ${modelId}`);
|
|
@@ -32,16 +34,46 @@ export async function runIsolated(
|
|
|
32
34
|
const response = provider?.streamSimple
|
|
33
35
|
? provider.streamSimple(model, context, streamOptions)
|
|
34
36
|
: streamSimple(model, context, streamOptions);
|
|
35
|
-
for await (const event of response)
|
|
37
|
+
for await (const event of response) {
|
|
38
|
+
onEvent?.();
|
|
39
|
+
if (event.type === "text_delta") onDelta?.(event.delta);
|
|
40
|
+
}
|
|
36
41
|
const result = await response.result();
|
|
37
42
|
if (result.stopReason !== "stop") throw new Error(result.errorMessage ?? `Model stopped: ${result.stopReason}`);
|
|
38
43
|
return text(result);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
/** Idle deadline per candidate: a candidate is aborted only after timeoutMs
|
|
47
|
+
* with NO stream events — healthy slow streams (reasoning models, large
|
|
48
|
+
* transcripts) keep resetting it, so progress is never killed. Caller abort
|
|
49
|
+
* propagates immediately. streamSimple honors the signal (same mechanism as
|
|
50
|
+
* tool-call abort). */
|
|
51
|
+
const CANDIDATE_TIMEOUT_MS = 90_000;
|
|
52
|
+
|
|
53
|
+
function idleSignal(signal: AbortSignal | undefined, timeoutMs: number): { signal: AbortSignal; touch(): void; dispose(): void } {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
+
const arm = () => {
|
|
57
|
+
if (timer) clearTimeout(timer);
|
|
58
|
+
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
|
+
};
|
|
60
|
+
const onAbort = () => controller.abort();
|
|
61
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
arm();
|
|
63
|
+
return {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
touch: arm,
|
|
66
|
+
dispose() {
|
|
67
|
+
if (timer) clearTimeout(timer);
|
|
68
|
+
signal?.removeEventListener("abort", onAbort);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
/**
|
|
42
74
|
* Try each model in priority order: unresolvable candidates are skipped;
|
|
43
|
-
* any call error (rate limit, quota, unavailable, network) advances
|
|
44
|
-
* next candidate — for a best-effort reviewer any dead candidate should
|
|
75
|
+
* any call error (rate limit, quota, unavailable, network, timeout) advances
|
|
76
|
+
* to the next candidate — for a best-effort reviewer any dead candidate should
|
|
45
77
|
* yield to the next. All exhausted → the last error is rethrown.
|
|
46
78
|
* No parent-model fallback: the advisor must never use the primary model.
|
|
47
79
|
*/
|
|
@@ -56,16 +88,21 @@ export async function runIsolatedChain(
|
|
|
56
88
|
onDelta?: ChainOnDelta,
|
|
57
89
|
signal?: AbortSignal,
|
|
58
90
|
reasoning?: string,
|
|
91
|
+
// ponytail: test seam — production callers use the 90s default
|
|
92
|
+
timeoutMs: number = CANDIDATE_TIMEOUT_MS,
|
|
59
93
|
): Promise<{ text: string; model: string }> {
|
|
60
94
|
let lastError: unknown;
|
|
61
95
|
for (const [attempt, modelId] of models.entries()) {
|
|
62
96
|
if (signal?.aborted) throw new Error("Advisor call aborted");
|
|
97
|
+
const idle = idleSignal(signal, timeoutMs);
|
|
63
98
|
try {
|
|
64
|
-
return { text: await runIsolated(ctx, modelId, context, (delta) => onDelta?.(delta, attempt), signal, reasoning), model: modelId };
|
|
99
|
+
return { text: await runIsolated(ctx, modelId, context, (delta) => onDelta?.(delta, attempt), idle.signal, reasoning, idle.touch), model: modelId };
|
|
65
100
|
} catch (error) {
|
|
66
101
|
// An abort is a caller decision, not a dead model — do not fall through.
|
|
67
102
|
if (signal?.aborted) throw error;
|
|
68
103
|
lastError = error;
|
|
104
|
+
} finally {
|
|
105
|
+
idle.dispose();
|
|
69
106
|
}
|
|
70
107
|
}
|
|
71
108
|
throw lastError instanceof Error ? lastError : new Error(`All advisor models failed: ${models.join(", ") || "none configured"}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-advisor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Pi extension for an automatic advisor: a second model that reviews each settled turn and injects severity-routed notes, plus an on-demand consult tool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@earendil-works/pi-ai": "^0.85.0",
|
|
51
51
|
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
52
|
-
"@earendil-works/pi-server": "^0.85.0",
|
|
53
52
|
"@earendil-works/pi-tui": "^0.85.0",
|
|
54
53
|
"@types/mocha": "^10.0.10",
|
|
55
54
|
"@types/node": "^20.19.43",
|