@bacnh85/pi-advisor 0.2.0 → 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 CHANGED
@@ -1,5 +1,21 @@
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
+
15
+ ## 0.2.1 (2026-09-05)
16
+
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
+
3
19
  ## 0.2.0 (2026-08-31)
4
20
 
5
21
  ### Added
@@ -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) if (event.type === "text_delta") onDelta?.(event.delta);
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 to the
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.0",
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",
@@ -41,15 +41,15 @@
41
41
  "typecheck": "tsc --noEmit"
42
42
  },
43
43
  "peerDependencies": {
44
- "@earendil-works/pi-ai": ">=0.80.8 <0.85.0",
45
- "@earendil-works/pi-coding-agent": ">=0.84.3 <0.85.0",
46
- "@earendil-works/pi-tui": ">=0.80.8 <0.85.0",
44
+ "@earendil-works/pi-ai": ">=0.80.8 <0.86.0",
45
+ "@earendil-works/pi-coding-agent": ">=0.84.3 <0.86.0",
46
+ "@earendil-works/pi-tui": ">=0.80.8 <0.86.0",
47
47
  "typebox": "*"
48
48
  },
49
49
  "devDependencies": {
50
- "@earendil-works/pi-ai": "^0.84.2",
51
- "@earendil-works/pi-coding-agent": "^0.84.3",
52
- "@earendil-works/pi-tui": "^0.84.3",
50
+ "@earendil-works/pi-ai": "^0.85.0",
51
+ "@earendil-works/pi-coding-agent": "^0.85.0",
52
+ "@earendil-works/pi-tui": "^0.85.0",
53
53
  "@types/mocha": "^10.0.10",
54
54
  "@types/node": "^20.19.43",
55
55
  "chai": "^4.5.0",