pi-goal-list-loop-audit 0.27.1 → 0.27.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.
|
@@ -180,3 +180,77 @@ export function shouldHeartbeatRefire(input: HeartbeatInput): boolean {
|
|
|
180
180
|
export function accountTurnForNudges(toolCalls: number, currentNudges: number): number {
|
|
181
181
|
return toolCalls > 0 ? 0 : currentNudges + 1;
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* v0.27.3: the pure nudge detector and richer accounting. A supervising turn
|
|
186
|
+
* is a nudge (no real progress) iff it has NO tool calls AND its text is
|
|
187
|
+
* either short (< DEFAULT_STALL_SHORT_WORDS words) OR highly similar to
|
|
188
|
+
* the prior assistant turn (3-gram Jaccard > DEFAULT_STALL_SIM_THRESHOLD).
|
|
189
|
+
* Substantive analytical replies (≥ 15 words, novel) reset the counter
|
|
190
|
+
* even with no tool calls — the polis-session incident ("3 consecutive
|
|
191
|
+
* turns with no tool calls" tripped the brake on real investigation work,
|
|
192
|
+
* screenshot 2026-07-27) showed the simple tool-only check is too coarse.
|
|
193
|
+
*
|
|
194
|
+
* Word-count rather than char-count: "Working…" (1 word) is a nudge;
|
|
195
|
+
* "state-pump-dom.ts has zero references to hud." (8 words, one sentence)
|
|
196
|
+
* is not. A paragraph with at least one real sentence is > 15 words.
|
|
197
|
+
*
|
|
198
|
+
* Pure: no side effects, no state. Safe to unit-test with crafted inputs.
|
|
199
|
+
*/
|
|
200
|
+
export const DEFAULT_STALL_SHORT_WORDS = 15;
|
|
201
|
+
export const DEFAULT_STALL_SIM_THRESHOLD = 0.6;
|
|
202
|
+
|
|
203
|
+
export function trigramSimilarity(a: string, b: string): number {
|
|
204
|
+
if (!a && !b) return 1;
|
|
205
|
+
if (!a || !b) return 0;
|
|
206
|
+
const grams = (s: string) => {
|
|
207
|
+
const g = new Map<string, number>();
|
|
208
|
+
const t = s.toLowerCase();
|
|
209
|
+
for (let i = 0; i <= t.length - 3; i++) {
|
|
210
|
+
const k = t.slice(i, i + 3);
|
|
211
|
+
g.set(k, (g.get(k) ?? 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
return g;
|
|
214
|
+
};
|
|
215
|
+
const ga = grams(a);
|
|
216
|
+
const gb = grams(b);
|
|
217
|
+
let inter = 0;
|
|
218
|
+
let uni = 0;
|
|
219
|
+
const keys = new Set([...ga.keys(), ...gb.keys()]);
|
|
220
|
+
for (const k of keys) {
|
|
221
|
+
const va = ga.get(k) ?? 0;
|
|
222
|
+
const vb = gb.get(k) ?? 0;
|
|
223
|
+
inter += Math.min(va, vb);
|
|
224
|
+
uni += Math.max(va, vb);
|
|
225
|
+
}
|
|
226
|
+
return uni === 0 ? 0 : inter / uni;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function isNudgeTurn(opts: {
|
|
230
|
+
toolCalls: number;
|
|
231
|
+
text: string;
|
|
232
|
+
priorText: string;
|
|
233
|
+
shortWords?: number;
|
|
234
|
+
simThreshold?: number;
|
|
235
|
+
}): boolean {
|
|
236
|
+
if (opts.toolCalls > 0) return false;
|
|
237
|
+
const shortThr = opts.shortWords ?? DEFAULT_STALL_SHORT_WORDS;
|
|
238
|
+
const simThr = opts.simThreshold ?? DEFAULT_STALL_SIM_THRESHOLD;
|
|
239
|
+
const wordCount = (opts.text.trim().match(/\S+/g) ?? []).length;
|
|
240
|
+
if (wordCount < shortThr) return true;
|
|
241
|
+
if (!opts.priorText) return false; // first turn in a streak — no similarity to compare to
|
|
242
|
+
return trigramSimilarity(opts.text, opts.priorText) > simThr;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function accountTurnForNudgesRich(
|
|
246
|
+
opts: {
|
|
247
|
+
toolCalls: number;
|
|
248
|
+
text: string;
|
|
249
|
+
priorText: string;
|
|
250
|
+
shortWords?: number;
|
|
251
|
+
simThreshold?: number;
|
|
252
|
+
},
|
|
253
|
+
currentNudges: number,
|
|
254
|
+
): number {
|
|
255
|
+
return isNudgeTurn(opts) ? currentNudges + 1 : 0;
|
|
256
|
+
}
|
|
@@ -52,6 +52,13 @@ export interface Settings {
|
|
|
52
52
|
/** v0.26.1: consecutive heartbeat refires without a real turn before
|
|
53
53
|
* the goal pauses / loop stops (default 5; 0 = never escalate). */
|
|
54
54
|
stallEscalationRefires?: number;
|
|
55
|
+
/** v0.27.3: a turn with no tool calls AND fewer words than this is a
|
|
56
|
+
* nudge. Default 15 words. Higher = stricter (more pauses). */
|
|
57
|
+
stallShortWords?: number;
|
|
58
|
+
/** v0.27.3: a turn with no tool calls whose text trigram-similarity to
|
|
59
|
+
* the prior assistant turn exceeds this is a nudge. Default 0.6. Higher
|
|
60
|
+
* = stricter (more pauses). */
|
|
61
|
+
stallSimilarityThreshold?: number;
|
|
55
62
|
/** on → propose_* drafts activate WITHOUT the Confirm dialog and the
|
|
56
63
|
* interview floor is skipped — the seed carries the intent (unattended
|
|
57
64
|
* rigs). Default off: nothing activates before the user confirms. */
|
|
@@ -131,6 +138,8 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
|
|
|
131
138
|
"quotaRetryMinutes",
|
|
132
139
|
"stuckMaxInterventions",
|
|
133
140
|
"stallEscalationRefires",
|
|
141
|
+
"stallShortWords",
|
|
142
|
+
"stallSimilarityThreshold",
|
|
134
143
|
];
|
|
135
144
|
|
|
136
145
|
/** Where each effective setting comes from (for the /glla display). */
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// pi-goal-list-loop-audit — v0.27.2
|
|
2
|
+
// extensions/length-continue.ts
|
|
3
|
+
//
|
|
4
|
+
// Folded-in auto-continue for output-token truncation (was the standalone
|
|
5
|
+
// pi-length-continue 0.1.0, now deprecated). When ONE assistant response
|
|
6
|
+
// exceeds the model's provider-side per-response output cap, pi ends the
|
|
7
|
+
// turn with stopReason "length" and idles — a dead stop on unattended rigs.
|
|
8
|
+
// The tracker decides when to re-trigger; goal.ts's agent_end handler wires
|
|
9
|
+
// it BEFORE all turn bookkeeping: a truncated turn is not a completed turn
|
|
10
|
+
// (no telemetry), not a stall (no no-tool nudge), and must not run the
|
|
11
|
+
// loop measure or the normal goal continuation on half a response.
|
|
12
|
+
//
|
|
13
|
+
// Guards (same as the standalone):
|
|
14
|
+
// - consecutive cap: after MAX back-to-back truncations, give up (once)
|
|
15
|
+
// instead of burning quota in a truncation ping-pong. Any normally
|
|
16
|
+
// finished turn resets the counter.
|
|
17
|
+
// - the caller skips when messages are already pending (a queued message
|
|
18
|
+
// triggers a turn anyway) and routes stale-handle errors to
|
|
19
|
+
// goStaleTerminal (pi#7154).
|
|
20
|
+
|
|
21
|
+
export const LENGTH_CONTINUE_MAX = 3;
|
|
22
|
+
|
|
23
|
+
export const LENGTH_CONTINUE_TEXT = [
|
|
24
|
+
"Your previous response was cut off at the model's per-response output token limit.",
|
|
25
|
+
"Continue EXACTLY where you stopped — finish the current artifact, then keep going.",
|
|
26
|
+
"Keep each individual response shorter from here: split large file writes into multiple smaller write/edit calls across turns instead of one giant response.",
|
|
27
|
+
].join(" ");
|
|
28
|
+
|
|
29
|
+
export interface LengthContinueTick {
|
|
30
|
+
/** Send the continue message this round. */
|
|
31
|
+
fire: boolean;
|
|
32
|
+
/** The cap was just exceeded — notify the give-up exactly once. */
|
|
33
|
+
giveUpNow: boolean;
|
|
34
|
+
/** Current consecutive truncation streak (after this tick). */
|
|
35
|
+
consecutive: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function makeLengthContinueTracker(max: number = LENGTH_CONTINUE_MAX) {
|
|
39
|
+
let consecutive = 0;
|
|
40
|
+
let gaveUp = false;
|
|
41
|
+
return {
|
|
42
|
+
tick(stopped: boolean): LengthContinueTick {
|
|
43
|
+
if (!stopped) {
|
|
44
|
+
consecutive = 0;
|
|
45
|
+
gaveUp = false;
|
|
46
|
+
return { fire: false, giveUpNow: false, consecutive: 0 };
|
|
47
|
+
}
|
|
48
|
+
consecutive++;
|
|
49
|
+
if (consecutive > max) {
|
|
50
|
+
const giveUpNow = !gaveUp;
|
|
51
|
+
gaveUp = true;
|
|
52
|
+
return { fire: false, giveUpNow, consecutive };
|
|
53
|
+
}
|
|
54
|
+
return { fire: true, giveUpNow: false, consecutive };
|
|
55
|
+
},
|
|
56
|
+
get consecutive(): number {
|
|
57
|
+
return consecutive;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Session-level singleton — one tracker per extension runtime. The factory
|
|
63
|
+
// calls resetLengthContinue() so an extension reload starts clean.
|
|
64
|
+
let tracker = makeLengthContinueTracker();
|
|
65
|
+
|
|
66
|
+
export function tickLengthContinue(stopped: boolean): LengthContinueTick {
|
|
67
|
+
return tracker.tick(stopped);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resetLengthContinue(): void {
|
|
71
|
+
tracker = makeLengthContinueTracker();
|
|
72
|
+
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -86,6 +86,12 @@ import {
|
|
|
86
86
|
writeGoalMd,
|
|
87
87
|
missingGllaTools,
|
|
88
88
|
} from "../goal-loop-core.js";
|
|
89
|
+
import {
|
|
90
|
+
LENGTH_CONTINUE_MAX,
|
|
91
|
+
LENGTH_CONTINUE_TEXT,
|
|
92
|
+
resetLengthContinue,
|
|
93
|
+
tickLengthContinue,
|
|
94
|
+
} from "../length-continue.js";
|
|
89
95
|
import {
|
|
90
96
|
isQuotaError,
|
|
91
97
|
isSubagentQuotaResult,
|
|
@@ -150,8 +156,10 @@ import {
|
|
|
150
156
|
type LoopState,
|
|
151
157
|
} from "../goal-loop-forever.js";
|
|
152
158
|
import {
|
|
153
|
-
|
|
159
|
+
accountTurnForNudgesRich,
|
|
154
160
|
BACKOFF_IDLE_RETRY_MS,
|
|
161
|
+
DEFAULT_STALL_SIM_THRESHOLD,
|
|
162
|
+
DEFAULT_STALL_SHORT_WORDS,
|
|
155
163
|
HEARTBEAT_INTERVAL_MS,
|
|
156
164
|
HEARTBEAT_MAX_NUDGES,
|
|
157
165
|
HEARTBEAT_STALL_MS,
|
|
@@ -527,6 +535,25 @@ function sendContinuation(goalId: string): void {
|
|
|
527
535
|
}
|
|
528
536
|
}
|
|
529
537
|
|
|
538
|
+
// v0.27.2: send the truncation-continue nudge. Same guards as
|
|
539
|
+
// sendContinuation (stale api = terminal), independent of goal state —
|
|
540
|
+
// plain sessions truncate too.
|
|
541
|
+
function sendLengthContinue(ctx: ExtensionContext, consecutive: number): void {
|
|
542
|
+
if (!extensionApi || extensionApiStale) return;
|
|
543
|
+
try {
|
|
544
|
+
extensionApi.sendMessage({
|
|
545
|
+
customType: GOAL_EVENT_ENTRY,
|
|
546
|
+
content: LENGTH_CONTINUE_TEXT,
|
|
547
|
+
display: true,
|
|
548
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
549
|
+
appendLedger(ctx.cwd, "length_continue_sent", { consecutive });
|
|
550
|
+
ctx.ui.notify(`Response hit the output-token cap — auto-continuing (${consecutive}/${LENGTH_CONTINUE_MAX})`, "warning");
|
|
551
|
+
} catch (err) {
|
|
552
|
+
appendLedger(ctx.cwd, "length_continue_send_failed", { consecutive, error: err instanceof Error ? err.message : String(err) });
|
|
553
|
+
if (isStaleApiError(err)) goStaleTerminal(ctx, "sendLengthContinue");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
530
557
|
function continuationPrompt(goal: Goal): string {
|
|
531
558
|
// Read the .md file as the template, then substitute {{tokens}}.
|
|
532
559
|
// For v0.1.0 we inline-substitute so we don't need fs at runtime.
|
|
@@ -2875,6 +2902,8 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
|
|
|
2875
2902
|
`Wedge alert minutes — ${show("wedgeAlertMinutes", `(${WEDGE_ALERT_DEFAULT_MINUTES})`)} — hung-command alert while the session is busy (0 = off)`,
|
|
2876
2903
|
`Stuck max interventions — ${show("stuckMaxInterventions", "(5)")} — consecutive stuck interventions before a loop stops`,
|
|
2877
2904
|
`Stall escalation refires — ${show("stallEscalationRefires", "(5)")} — heartbeat refires with no turn before the goal pauses / loop stops (0 = never)`,
|
|
2905
|
+
`Stall short words — ${show("stallShortWords", `(${DEFAULT_STALL_SHORT_WORDS})`)} — turns with no tools AND fewer words than this count as a nudge`,
|
|
2906
|
+
`Stall similarity threshold — ${show("stallSimilarityThreshold", `(${DEFAULT_STALL_SIM_THRESHOLD})`)} — no-tool turns whose text is > this similar to the prior turn count as a nudge (0–1)`,
|
|
2878
2907
|
"── Subagents ──",
|
|
2879
2908
|
`Subagent model strategy — ${show("subagentModelStrategy", "(inherit-parent)")} — inherit-parent shares your session model+quota; agent-default pins haiku for Explore`,
|
|
2880
2909
|
`Subagent Explore pin — ${settings.subagentModelOverrides?.Explore ?? "(follows strategy)"} — provider/model pin; always wins over strategy`,
|
|
@@ -3214,6 +3243,8 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3214
3243
|
fmt("stuckMaxInterventions", "stuckMaxInterventions"),
|
|
3215
3244
|
fmt("stallEscalationRefires", "stallEscalation"),
|
|
3216
3245
|
fmt("wedgeAlertMinutes", "wedgeAlert"),
|
|
3246
|
+
fmt("stallShortWords", "stallShortWords"),
|
|
3247
|
+
fmt("stallSimilarityThreshold", "stallSimilarityThreshold"),
|
|
3217
3248
|
// v0.25.6: effective per-type subagent model resolution.
|
|
3218
3249
|
...["Explore", "Plan", "general-purpose"].map(
|
|
3219
3250
|
(t) => `subagent ${t}: ${resolveEffectiveSubagentModel(t, loadSettings(ctx.cwd), (ctx.model as any)?.id ? `${(ctx.model as any).provider}/${(ctx.model as any).id}` : undefined)}`,
|
|
@@ -3366,6 +3397,32 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3366
3397
|
ctx.ui.notify(`stuckmax must be a positive integer, got: ${value}`, "warning");
|
|
3367
3398
|
}
|
|
3368
3399
|
}
|
|
3400
|
+
} else if (key === "stallshortwords" || key === "stallshort") {
|
|
3401
|
+
if (["unset", "default"].includes(value)) {
|
|
3402
|
+
patch.stallShortWords = undefined;
|
|
3403
|
+
changed = true;
|
|
3404
|
+
} else {
|
|
3405
|
+
const n = Number.parseInt(value, 10);
|
|
3406
|
+
if (Number.isInteger(n) && n >= 1) {
|
|
3407
|
+
patch.stallShortWords = n;
|
|
3408
|
+
changed = true;
|
|
3409
|
+
} else {
|
|
3410
|
+
ctx.ui.notify(`stallshortwords must be a positive integer, got: ${value}`, "warning");
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
} else if (key === "stallsim" || key === "stallsimilaritythreshold") {
|
|
3414
|
+
if (["unset", "default"].includes(value)) {
|
|
3415
|
+
patch.stallSimilarityThreshold = undefined;
|
|
3416
|
+
changed = true;
|
|
3417
|
+
} else {
|
|
3418
|
+
const n = Number.parseFloat(value);
|
|
3419
|
+
if (Number.isFinite(n) && n >= 0 && n <= 1) {
|
|
3420
|
+
patch.stallSimilarityThreshold = n;
|
|
3421
|
+
changed = true;
|
|
3422
|
+
} else {
|
|
3423
|
+
ctx.ui.notify(`stallsimilaritythreshold must be between 0 and 1, got: ${value}`, "warning");
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3369
3426
|
} else if (key === "thinking" || key === "auditorthinkinglevel") {
|
|
3370
3427
|
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(value)) {
|
|
3371
3428
|
patch.auditorThinkingLevel = value as Settings["auditorThinkingLevel"];
|
|
@@ -3463,6 +3520,7 @@ function warnOnCommandCollision(ctx: ExtensionContext): void {
|
|
|
3463
3520
|
export default function (pi: ExtensionAPI): void {
|
|
3464
3521
|
extensionApi = pi;
|
|
3465
3522
|
extensionApiStale = false; // a fresh factory run means a fresh runtime (reload path)
|
|
3523
|
+
resetLengthContinue(); // v0.27.2: fresh runtime, fresh truncation streak
|
|
3466
3524
|
startHeartbeat();
|
|
3467
3525
|
startUITicker();
|
|
3468
3526
|
// Four top-level commands, that's all (v0.8.0 consolidation):
|
|
@@ -3778,6 +3836,29 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3778
3836
|
// continuation loop.
|
|
3779
3837
|
if (isForeignCtx(ctx)) return;
|
|
3780
3838
|
noteActivity(true);
|
|
3839
|
+
// v0.27.2: folded-in length-continue (standalone pi-length-continue is
|
|
3840
|
+
// deprecated). A response cut by the per-response output cap is NOT a
|
|
3841
|
+
// completed turn (no telemetry), NOT a stall (no no-tool nudge), and
|
|
3842
|
+
// must not run the loop measure or the normal goal continuation on half
|
|
3843
|
+
// a response — re-trigger immediately with split-smaller guidance and
|
|
3844
|
+
// skip ALL turn bookkeeping; the NEXT agent_end processes the run.
|
|
3845
|
+
// Works with no goal active (plain sessions truncate too).
|
|
3846
|
+
// v0.27.3: enrich lastA with text + priorText for the smarter nudge
|
|
3847
|
+
// accounting below.
|
|
3848
|
+
const assistants = (event.messages as any[]).filter((m: any) => m.role === "assistant");
|
|
3849
|
+
const rawLastA = assistants.length ? assistants[assistants.length - 1] : null;
|
|
3850
|
+
const rawPriorA = assistants.length >= 2 ? assistants[assistants.length - 2] : null;
|
|
3851
|
+
const extractText = (m: any): string => (m && Array.isArray(m.content)) ? m.content.filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n") : "";
|
|
3852
|
+
const lastA = rawLastA ? { stopReason: rawLastA.stopReason, text: extractText(rawLastA), priorText: extractText(rawPriorA) } : null;
|
|
3853
|
+
const lc = tickLengthContinue(lastA?.stopReason === "length");
|
|
3854
|
+
if (lc.giveUpNow) {
|
|
3855
|
+
ctx.ui.notify(`glla: response hit the output-token cap ${LENGTH_CONTINUE_MAX}× in a row — stepping aside. Ask the model to split the work into smaller pieces.`, "warning");
|
|
3856
|
+
notifyExternal(ctx, "Response truncated 3× in a row — giving up auto-continue.");
|
|
3857
|
+
}
|
|
3858
|
+
if (lastA?.stopReason === "length") {
|
|
3859
|
+
if (lc.fire && !ctx.hasPendingMessages()) sendLengthContinue(ctx, lc.consecutive);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3781
3862
|
// v0.25.2: per-goal turn telemetry (/glla stats).
|
|
3782
3863
|
if (state.goal && state.goal.status === "active") {
|
|
3783
3864
|
const t = state.goal.telemetry ?? { turns: 0, fileWrites: 0, bashCalls: 0 };
|
|
@@ -3789,27 +3870,34 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3789
3870
|
registeredCtx = ctx;
|
|
3790
3871
|
}
|
|
3791
3872
|
ensureAgentToolsActive(pi, ctx);
|
|
3792
|
-
//
|
|
3793
|
-
//
|
|
3873
|
+
// v0.27.3: nudge accounting — substantive analytical turns (long, novel
|
|
3874
|
+
// text) reset the counter even with no tool calls. Polis-session
|
|
3875
|
+
// incident showed the tool-only check fired on real investigation work.
|
|
3794
3876
|
if (isSupervising()) {
|
|
3795
|
-
|
|
3877
|
+
const s = loadSettings(ctx.cwd);
|
|
3878
|
+
const shortWordsThr = s.stallShortWords ?? DEFAULT_STALL_SHORT_WORDS;
|
|
3879
|
+
const simThr = s.stallSimilarityThreshold ?? DEFAULT_STALL_SIM_THRESHOLD;
|
|
3880
|
+
heartbeatNudges = accountTurnForNudgesRich(
|
|
3881
|
+
{ toolCalls: toolCallsThisTurn, text: lastA?.text ?? "", priorText: lastA?.priorText ?? "", shortWords: shortWordsThr, simThreshold: simThr },
|
|
3882
|
+
heartbeatNudges,
|
|
3883
|
+
);
|
|
3796
3884
|
if (heartbeatNudges >= HEARTBEAT_MAX_NUDGES) {
|
|
3797
3885
|
heartbeatNudges = 0;
|
|
3798
3886
|
if (isLoopActive()) {
|
|
3799
3887
|
clearLoopTimer();
|
|
3800
|
-
state.loop = { ...state.loop!, active: false, stopReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive turns
|
|
3888
|
+
state.loop = { ...state.loop!, active: false, stopReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive unproductive turns (no tools, short or repetitive)` };
|
|
3801
3889
|
persistState(ctx);
|
|
3802
|
-
ctx.ui.notify(`Loop stopped: stalled (${HEARTBEAT_MAX_NUDGES} turns
|
|
3890
|
+
ctx.ui.notify(`Loop stopped: stalled (${HEARTBEAT_MAX_NUDGES} unproductive turns). /loop start to begin a new one.`, "warning");
|
|
3803
3891
|
notifyExternal(ctx, "Loop stopped: stalled (no tool calls).");
|
|
3804
3892
|
return;
|
|
3805
3893
|
}
|
|
3806
3894
|
if (state.goal) {
|
|
3807
3895
|
updateGoal({
|
|
3808
3896
|
status: "paused",
|
|
3809
|
-
pauseReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive turns
|
|
3897
|
+
pauseReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive unproductive turns (no tools, short or repetitive)`,
|
|
3810
3898
|
pauseSuggestedAction: "Inspect the goal — /goal resume to retry, /goal tweak to narrow it, /goal cancel to abort.",
|
|
3811
3899
|
}, ctx);
|
|
3812
|
-
ctx.ui.notify(`Goal paused: stalled (${HEARTBEAT_MAX_NUDGES} turns
|
|
3900
|
+
ctx.ui.notify(`Goal paused: stalled (${HEARTBEAT_MAX_NUDGES} unproductive turns).`, "warning");
|
|
3813
3901
|
notifyExternal(ctx, "Goal paused: stalled (no tool calls).");
|
|
3814
3902
|
return;
|
|
3815
3903
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.3",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|