@bacnh85/pi-advisor 0.2.2 → 0.2.3
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 +25 -0
- package/README.md +1 -1
- package/extensions/index.ts +34 -6
- package/extensions/lib/watcher.ts +10 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3 (2026-09-11)
|
|
4
|
+
|
|
5
|
+
- **Fix 20–30s+ TUI freeze after every settled turn**: pi core awaits
|
|
6
|
+
`agent_settled` handlers before the prompt regains input, and the advisor
|
|
7
|
+
review (a full model call — measured median ~11s, p90 ~96s) ran inside that
|
|
8
|
+
awaited barrier, freezing the TUI — most visibly after `write_plan`, where
|
|
9
|
+
the prefilled `/plan-approve` was dead until the review finished. The review
|
|
10
|
+
now runs fire-and-forget: notes deliver via `sendUserMessage`, which the SDK
|
|
11
|
+
queues as a steer when a run is already active or fires as a follow-up turn
|
|
12
|
+
when idle. A still-running review makes the next settle skip (bounded loss).
|
|
13
|
+
- **Watch is skipped in headless modes** (print/rpc/json): a floating review
|
|
14
|
+
would die at process exit, and a note there fired an unrequested follow-up
|
|
15
|
+
agent run. The on-demand `/advisor` consult tool is unaffected.
|
|
16
|
+
- **Hardened against stale/edge contexts** (review follow-up): a review still
|
|
17
|
+
in flight when a new session starts is silently discarded instead of
|
|
18
|
+
steering the new session; the in-flight guard moved to per-runtime state so
|
|
19
|
+
a draining old review can't suppress the new session's first review; a
|
|
20
|
+
mode-less context is treated as headless (fail-safe skip).
|
|
21
|
+
- **Delivery-time liveness + teardown self-disarm** (final review round): a
|
|
22
|
+
review in flight across `/new`, `/resume`, `/fork`, `/reload`, or quit is
|
|
23
|
+
discarded silently — `session_shutdown` disarms the watch before the runner
|
|
24
|
+
invalidates, so no stale-ctx error toast lands in the replaced session; the
|
|
25
|
+
pause toast routes through the same liveness gate; the watch-disabled state
|
|
26
|
+
mid-review (`/advisor off`, `watch-off`) also suppresses delivery.
|
|
27
|
+
|
|
3
28
|
## 0.2.2 (2026-09-07)
|
|
4
29
|
|
|
5
30
|
- **Fix TUI hang on stalled reviewer provider**: every chain candidate now
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Settings live in `~/.pi/agent/settings.json` (global) and `.pi/settings.json`
|
|
|
68
68
|
time, the next candidate serves automatically; a whole-chain failure counts
|
|
69
69
|
as one review failure (the 3-strike pause still applies). The advisor never
|
|
70
70
|
falls back to the primary model — it must never review its own turns.
|
|
71
|
-
- `watch.enabled` (default `true`) — turn-end reviewing on session start
|
|
71
|
+
- `watch.enabled` (default `true`) — turn-end reviewing on session start (TUI only — print/rpc/json runs skip the watch; the on-demand advisor tool still works)
|
|
72
72
|
- `watch.minToolCalls` (default `3`, `0` = every turn) — skip trivial turns
|
|
73
73
|
- `watch.immuneTurns` (default `3`) — review window during which the same
|
|
74
74
|
normalized note is not re-delivered (loop protection); distinct concerns and
|
package/extensions/index.ts
CHANGED
|
@@ -70,7 +70,9 @@ export default function piAdvisor(pi: ExtensionAPI): void {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
pi.on("before_agent_start", (event, ctx: ExtensionContext): any => {
|
|
73
|
-
|
|
73
|
+
// Headless runs can't receive watch notes anymore — don't make them pay
|
|
74
|
+
// dead prompt text claiming otherwise.
|
|
75
|
+
if (ctx.mode !== "tui" || !watchEnabled || !runtime || runtime.models.length === 0 || runtime.stats.paused) return;
|
|
74
76
|
// Every turn the agent sees the authority line (static per session,
|
|
75
77
|
// cache-safe): messages starting 'Advisor review' are reviewer findings.
|
|
76
78
|
const line = "Advisor notes: messages starting 'Advisor review' are authoritative reviewer findings. Fix or explicitly justify ignoring each finding.";
|
|
@@ -108,13 +110,39 @@ export default function piAdvisor(pi: ExtensionAPI): void {
|
|
|
108
110
|
runtime.cursor = entries.length ? entries[entries.length - 1].id : undefined;
|
|
109
111
|
});
|
|
110
112
|
|
|
113
|
+
// Self-disarm on session teardown: session_shutdown is emitted and awaited
|
|
114
|
+
// BEFORE the runner invalidates (agent-session-runtime teardownCurrent), so
|
|
115
|
+
// flipping the flag here makes live() false in this (about-to-be-orphaned)
|
|
116
|
+
// closure — an in-flight fire-and-forget review is discarded silently
|
|
117
|
+
// instead of throwing the stale-ctx error into the .catch and toasting the
|
|
118
|
+
// NEW session. The factory re-runs per session, so this never touches a
|
|
119
|
+
// live session's flag.
|
|
120
|
+
pi.on("session_shutdown", () => { watchEnabled = false; });
|
|
121
|
+
|
|
111
122
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
112
123
|
if (!runtime || !watchEnabled || runtime.stats.paused) return;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
// Only TUI: a floating review would die at process exit in headless
|
|
125
|
+
// modes, and a note there fired an unrequested follow-up run. Fail-safe:
|
|
126
|
+
// unknown/mode-less contexts skip too (missed review < surprise run).
|
|
127
|
+
if (ctx.mode !== "tui") return;
|
|
128
|
+
// ponytail: fire-and-forget — pi core awaits agent_settled handlers before
|
|
129
|
+
// the TUI regains input, so awaiting the 10-90s+ review here froze the UI
|
|
130
|
+
// after every turn. Notes deliver via sendUserMessage, which the SDK
|
|
131
|
+
// queues as a steer when a run is active or fires as a follow-up turn
|
|
132
|
+
// when idle. A still-running review makes the next settle skip (rt
|
|
133
|
+
// reviewing flag) — bounded loss, not queued.
|
|
134
|
+
const rt = runtime;
|
|
135
|
+
// Liveness guard evaluated at delivery time (not just settle time): a
|
|
136
|
+
// fresh session (/new) replaces runtime, /advisor off clears the chain,
|
|
137
|
+
// watch-off flips the flag, repeated failures pause — a review in flight
|
|
138
|
+
// across any of these must deliver nothing.
|
|
139
|
+
const live = () => runtime === rt && watchEnabled && rt.models.length > 0 && !rt.stats.paused;
|
|
140
|
+
void reviewTurn(rt, ctx, {
|
|
141
|
+
sendMessage: (message, options) => { if (live()) pi.sendMessage(message, options as never); },
|
|
142
|
+
sendUserMessage: (content, options) => { if (live()) pi.sendUserMessage(content, options); },
|
|
143
|
+
appendEntry: (customType, data) => { if (live()) pi.appendEntry(customType, data); },
|
|
144
|
+
notify: (message) => { if (live()) ctx.ui.notify(message, "error"); },
|
|
145
|
+
}, testIsolated).catch((err) => { if (live()) ctx.ui.notify(`Advisor review failed: ${String(err)}`, "error"); });
|
|
118
146
|
});
|
|
119
147
|
|
|
120
148
|
registerAdvisor(pi, {
|
|
@@ -49,13 +49,16 @@ export interface WatcherRuntime {
|
|
|
49
49
|
guard: GuardState;
|
|
50
50
|
stats: WatcherStats;
|
|
51
51
|
failures: number;
|
|
52
|
+
/** Set while a review is in flight — per-runtime so a stale session's
|
|
53
|
+
* draining review never blocks the new session's first review. */
|
|
54
|
+
reviewing: boolean;
|
|
52
55
|
/** OMP-parity post-steer cooldown: remaining settled turns during which
|
|
53
56
|
* non-blocker notes are deferred to next-turn asides instead of steering. */
|
|
54
57
|
steerCooldownTurns: number;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export function createRuntime(config: AdvisorConfig, models: string[]): WatcherRuntime {
|
|
58
|
-
return { config, models, cursor: undefined, guard: createGuard(), stats: createStats(), failures: 0, steerCooldownTurns: 0 };
|
|
61
|
+
return { config, models, cursor: undefined, guard: createGuard(), stats: createStats(), failures: 0, steerCooldownTurns: 0, reviewing: false };
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
/**
|
|
@@ -133,17 +136,16 @@ export interface WatcherHost {
|
|
|
133
136
|
sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
|
|
134
137
|
/** Display-only immediate card (session entry; never enters LLM context). */
|
|
135
138
|
appendEntry<T = unknown>(customType: string, data?: T): void;
|
|
139
|
+
/** Error/status toast, liveness-gated by the caller (never leaks into a replaced session). */
|
|
140
|
+
notify(message: string): void;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
/** Injectable isolated-model call — defaults to the chain runner; tests pass a fake. */
|
|
139
144
|
export type IsolatedCall = typeof runIsolatedChain;
|
|
140
145
|
|
|
141
146
|
/** One review step, called from the agent_settled handler while watching is active. */
|
|
142
|
-
// ponytail: module-level guard — one review at a time across the single session
|
|
143
|
-
let reviewing = false;
|
|
144
|
-
|
|
145
147
|
export async function reviewTurn(rt: WatcherRuntime, ctx: ExtensionContext, host: WatcherHost, isolated: IsolatedCall = runIsolatedChain): Promise<void> {
|
|
146
|
-
if (rt.stats.paused || reviewing) return;
|
|
148
|
+
if (rt.stats.paused || rt.reviewing) return;
|
|
147
149
|
// No advisor models → no watching: never let the primary model review its own turns.
|
|
148
150
|
if (rt.models.length === 0) return;
|
|
149
151
|
const config = rt.config.watch;
|
|
@@ -158,7 +160,7 @@ export async function reviewTurn(rt: WatcherRuntime, ctx: ExtensionContext, host
|
|
|
158
160
|
if (calls < config.minToolCalls) { rt.stats.skippedTrivial++; return; }
|
|
159
161
|
|
|
160
162
|
rt.guard.reviewIndex++;
|
|
161
|
-
reviewing = true;
|
|
163
|
+
rt.reviewing = true;
|
|
162
164
|
try {
|
|
163
165
|
const transcript = buildSessionContext(entries, ctx.sessionManager.getLeafId());
|
|
164
166
|
const evidence = buildEvidence(ctx, rt.models, transcript.messages, SYSTEM);
|
|
@@ -181,7 +183,7 @@ export async function reviewTurn(rt: WatcherRuntime, ctx: ExtensionContext, host
|
|
|
181
183
|
rt.failures++;
|
|
182
184
|
if (rt.failures >= MAX_CONSECUTIVE_FAILURES && !rt.stats.paused) {
|
|
183
185
|
rt.stats.paused = true;
|
|
184
|
-
|
|
186
|
+
host.notify(`Advisor watch paused after ${MAX_CONSECUTIVE_FAILURES} consecutive review failures (${String(error)}). Run /advisor on to retry.`);
|
|
185
187
|
}
|
|
186
188
|
return;
|
|
187
189
|
}
|
|
@@ -236,6 +238,6 @@ export async function reviewTurn(rt: WatcherRuntime, ctx: ExtensionContext, host
|
|
|
236
238
|
host.sendUserMessage(templates[verdict.severity], { deliverAs: "followUp" });
|
|
237
239
|
rt.steerCooldownTurns = config.immuneTurns;
|
|
238
240
|
} finally {
|
|
239
|
-
reviewing = false;
|
|
241
|
+
rt.reviewing = false;
|
|
240
242
|
}
|
|
241
243
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-advisor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|