pi-goal-list-loop-audit 0.35.66 → 0.35.68
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 +28 -0
- package/extensions/goal-loop-forever.ts +20 -2
- package/extensions/goal-loop-repetition.ts +12 -0
- package/extensions/goal-loop.ts +34 -2
- package/extensions/loops/goal-activation.ts +50 -3
- package/extensions/loops/goal-tools.ts +11 -6
- package/extensions/main-model-recovery.ts +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.68 — bound-stop recovery (2026-08-26)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
Explicit `/loop resume` now recovers time- and token-bound stops as fresh
|
|
7
|
+
supervised windows without discarding iteration, history, or best-value
|
|
8
|
+
state. Recoverable stopped loops can accept a confirmed
|
|
9
|
+
`propose_loop_refine` change while remaining stopped until explicitly
|
|
10
|
+
resumed. Clean max-iteration and finished loops remain terminal; automatic
|
|
11
|
+
startup does not silently reset an explicit budget.
|
|
12
|
+
|
|
13
|
+
### Tests
|
|
14
|
+
Coverage verifies fresh time windows, token-budget resets, preserved loop
|
|
15
|
+
history, stopped-loop refinement, and the unchanged max-iteration guard.
|
|
16
|
+
|
|
17
|
+
## 0.35.67 — in-band provider-result recovery (2026-08-26)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
Repeated successful tool transports that carry a strong 503/429/network
|
|
21
|
+
provider pane no longer enter loop stuck or plateau accounting as ordinary
|
|
22
|
+
work. After the same tool/result fingerprint repeats, the turn is routed
|
|
23
|
+
through the existing provider-recovery envelope; one-off status text in a
|
|
24
|
+
searched document remains ordinary output.
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
Coverage spans provider-marker classification, repeated-pane detection,
|
|
28
|
+
loop-turn exemption, durable recovery parking, and the unchanged real-error
|
|
29
|
+
and repetition paths. The full release gate remains green.
|
|
30
|
+
|
|
3
31
|
## 0.35.66 — compiled-host auditor launcher (2026-08-26)
|
|
4
32
|
|
|
5
33
|
### Fixed
|
|
@@ -54,6 +54,23 @@ export function isLifecycleHeldLoopReason(reason?: string): boolean {
|
|
|
54
54
|
|| !!reason?.startsWith("send-retry storm:");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** A stopped loop can be respecified without discarding its history when the
|
|
58
|
+
* stop is a recoverable work failure or an explicit time/token window. Max
|
|
59
|
+
* iterations and clean/user stops remain terminal until a fresh `/loop start`.
|
|
60
|
+
*/
|
|
61
|
+
export function isRefinableStoppedLoopReason(reason?: string): boolean {
|
|
62
|
+
return !!reason && (
|
|
63
|
+
reason.startsWith("time bound reached")
|
|
64
|
+
|| reason.startsWith("token budget exhausted")
|
|
65
|
+
|| reason.startsWith("stuck —")
|
|
66
|
+
|| reason.startsWith("plateau —")
|
|
67
|
+
|| reason.startsWith("metric never moved —")
|
|
68
|
+
|| reason.startsWith("measure command broken —")
|
|
69
|
+
|| reason.startsWith("provider errors —")
|
|
70
|
+
|| reason.startsWith("stalled:")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
export interface LoopState {
|
|
58
75
|
target: string;
|
|
59
76
|
/** v0.23.0: optional — a metricless "spec loop" (measure=none) has no
|
|
@@ -113,8 +130,9 @@ export interface LoopState {
|
|
|
113
130
|
recentPrints?: string[];
|
|
114
131
|
/** v0.24.0: last few iteration texts (near-duplicate check + banned openings). */
|
|
115
132
|
recentTexts?: string[];
|
|
116
|
-
/** v0.24.0: rolling tool-result fingerprints {tool, hash, isError}
|
|
117
|
-
|
|
133
|
+
/** v0.24.0: rolling tool-result fingerprints {tool, hash, isError};
|
|
134
|
+
* providerFailure marks a repeated in-band provider/network pane. */
|
|
135
|
+
recentToolResults?: { tool: string; hash: string; isError: boolean; providerFailure?: boolean }[];
|
|
118
136
|
/** v0.24.0: tool calls seen since the last completed iteration. */
|
|
119
137
|
toolsThisTurn?: number;
|
|
120
138
|
/** v0.24.0: consecutive iterations with zero tool calls. */
|
|
@@ -129,6 +129,18 @@ export interface ToolResultPrint {
|
|
|
129
129
|
tool: string;
|
|
130
130
|
hash: string;
|
|
131
131
|
isError: boolean;
|
|
132
|
+
/** Successful tool transport can still contain a repeated provider failure. */
|
|
133
|
+
providerFailure?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The repeated in-band provider pane is an outage signal, not loop work. */
|
|
137
|
+
export function repeatedInBandProviderFailure(results: ToolResultPrint[], repeat = REPETITION.toolResultRepeat): boolean {
|
|
138
|
+
if (repeat <= 0) return false;
|
|
139
|
+
const recent = results.slice(-repeat);
|
|
140
|
+
if (recent.length !== repeat || !recent[0]?.providerFailure) return false;
|
|
141
|
+
return recent.every((result) => result.providerFailure === true
|
|
142
|
+
&& result.tool === recent[0]!.tool
|
|
143
|
+
&& result.hash === recent[0]!.hash);
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
export interface LoopStuckInput {
|
package/extensions/goal-loop.ts
CHANGED
|
@@ -939,6 +939,8 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
939
939
|
!!r?.startsWith("plateau —") ||
|
|
940
940
|
!!r?.startsWith("stalled:") ||
|
|
941
941
|
!!r?.startsWith("stuck —") ||
|
|
942
|
+
!!r?.startsWith("time bound reached") ||
|
|
943
|
+
!!r?.startsWith("token budget exhausted") ||
|
|
942
944
|
// v0.35.54 (collect-pass HIGH finding): the v0.35.31 "metric never
|
|
943
945
|
// moved" stop message promises "/loop resume retries or /loop stop",
|
|
944
946
|
// but this predicate never matched that prefix — the promised command
|
|
@@ -983,8 +985,33 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
983
985
|
// An explicit resume re-arms the counters: fresh stall window,
|
|
984
986
|
// cleared dead-turn/stuck streaks, reprieves restored — the user
|
|
985
987
|
// saying "push again" wins over the ladder's memory (v0.29.19).
|
|
986
|
-
|
|
988
|
+
// Time and token bounds are per supervised run window: resuming a
|
|
989
|
+
// bound-stopped loop preserves its iteration/history/best but starts a
|
|
990
|
+
// fresh elapsed-time window or token budget instead of stopping again
|
|
991
|
+
// on the same bound.
|
|
992
|
+
const resetTimeWindow = stored.stopReason?.startsWith("time bound reached") ?? false;
|
|
993
|
+
const resetTokenBudget = stored.stopReason?.startsWith("token budget exhausted") ?? false;
|
|
994
|
+
const resumedAt = nowIso();
|
|
995
|
+
state.loop = {
|
|
996
|
+
...stored,
|
|
997
|
+
active: true,
|
|
998
|
+
stopReason: undefined,
|
|
999
|
+
consecutiveErrors: 0,
|
|
1000
|
+
consecutiveStuck: 0,
|
|
1001
|
+
lastStuckReason: undefined,
|
|
1002
|
+
stallCount: 0,
|
|
1003
|
+
auditPlateauReprieves: 0,
|
|
1004
|
+
...(resetTimeWindow ? { startedAt: resumedAt } : {}),
|
|
1005
|
+
...(resetTokenBudget ? { tokensUsed: 0 } : {}),
|
|
1006
|
+
};
|
|
987
1007
|
persistState(ctx);
|
|
1008
|
+
if (resetTimeWindow || resetTokenBudget) {
|
|
1009
|
+
appendLedger(ctx.cwd, "loop_bound_window_reset", {
|
|
1010
|
+
timeWindow: resetTimeWindow,
|
|
1011
|
+
tokenBudget: resetTokenBudget,
|
|
1012
|
+
iteration: stored.iteration,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
988
1015
|
// v0.35.23 (note.md Next #2): an explicit resume is exactly the
|
|
989
1016
|
// decision a load hold waits for — release it or the tick below
|
|
990
1017
|
// would be frozen.
|
|
@@ -995,8 +1022,13 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
995
1022
|
releaseContinuationDispatchStandDown();
|
|
996
1023
|
releaseAuditorSurface();
|
|
997
1024
|
scheduleLoopTick(ctx);
|
|
1025
|
+
const boundResetNote = resetTimeWindow
|
|
1026
|
+
? " · fresh time window"
|
|
1027
|
+
: resetTokenBudget
|
|
1028
|
+
? " · fresh token budget"
|
|
1029
|
+
: "";
|
|
998
1030
|
ctx.ui.notify(
|
|
999
|
-
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"} — ${displaySlice(stored.target, 60)}`,
|
|
1031
|
+
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"}${boundResetNote} — ${displaySlice(stored.target, 60)}`,
|
|
1000
1032
|
"info",
|
|
1001
1033
|
);
|
|
1002
1034
|
return;
|
|
@@ -179,6 +179,7 @@ import {
|
|
|
179
179
|
} from "../length-continue.js";
|
|
180
180
|
import { isSubagentProviderFailure } from "../quota-retry.js";
|
|
181
181
|
import {
|
|
182
|
+
classifyInBandProviderFailure,
|
|
182
183
|
classifyMainModelFailure,
|
|
183
184
|
isMainModelFallbackFailure,
|
|
184
185
|
requiresMainModelRecovery,
|
|
@@ -229,6 +230,7 @@ import {
|
|
|
229
230
|
import {
|
|
230
231
|
REPETITION,
|
|
231
232
|
isActuallyStuck,
|
|
233
|
+
repeatedInBandProviderFailure,
|
|
232
234
|
loopInterventionDirective,
|
|
233
235
|
continueVariant,
|
|
234
236
|
textFingerprint,
|
|
@@ -456,6 +458,13 @@ const ZOMBIE_PAUSE_REASON = "automatic zero-stream abort — no provider activit
|
|
|
456
458
|
// the park standing for manual resume — an honest degradation.
|
|
457
459
|
let zombieRetryStreak: ZombieRetryStreak = { key: "", count: 0, lastAbortStreamAt: 0 };
|
|
458
460
|
let zombieRetryTimer: NodeJS.Timeout | null = null;
|
|
461
|
+
// An in-band provider pane is observed during tool_result and consumed at the
|
|
462
|
+
// matching agent_end. Keep the raw text only in memory; the durable ledger
|
|
463
|
+
// records the bounded classification, never the provider payload.
|
|
464
|
+
let inBandProviderFailureRaw: string | null = null;
|
|
465
|
+
function clearInBandProviderFailure(): void {
|
|
466
|
+
inBandProviderFailureRaw = null;
|
|
467
|
+
}
|
|
459
468
|
|
|
460
469
|
/** Arm the one-shot automatic re-dispatch after a successful zombie abort.
|
|
461
470
|
* Returns true when a retry was scheduled (the caller adjusts its user-facing
|
|
@@ -951,11 +960,29 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
951
960
|
const loop = state.loop!;
|
|
952
961
|
const out = event?.output ?? event?.result ?? event?.details ?? "";
|
|
953
962
|
const text = typeof out === "string" ? out : JSON.stringify(out) ?? "";
|
|
963
|
+
const tool = String(event?.toolName ?? "?");
|
|
964
|
+
const inBandFailure = classifyInBandProviderFailure(text);
|
|
954
965
|
loop.recentToolResults = pushRepetitionCapped(
|
|
955
966
|
loop.recentToolResults ?? [],
|
|
956
|
-
{
|
|
967
|
+
{
|
|
968
|
+
tool,
|
|
969
|
+
hash: textFingerprint(text),
|
|
970
|
+
isError: Boolean(event?.isError ?? event?.error) || !!inBandFailure,
|
|
971
|
+
...(inBandFailure ? { providerFailure: true } : {}),
|
|
972
|
+
},
|
|
957
973
|
REPETITION.toolWindow,
|
|
958
974
|
);
|
|
975
|
+
// Successful transport is not proof of successful work: only a stable
|
|
976
|
+
// repeated provider pane becomes a loop-level recovery signal. One-off
|
|
977
|
+
// 503/429 text in a searched document remains ordinary tool output.
|
|
978
|
+
if (inBandFailure && repeatedInBandProviderFailure(loop.recentToolResults)) {
|
|
979
|
+
inBandProviderFailureRaw = text.slice(0, 800);
|
|
980
|
+
appendLedger(eventCtx.cwd, "loop_in_band_provider_failure", {
|
|
981
|
+
tool,
|
|
982
|
+
kind: inBandFailure.kind,
|
|
983
|
+
repeats: REPETITION.toolResultRepeat,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
959
986
|
// v0.25.1: file-write progress signal for the multi-signal stuck
|
|
960
987
|
// gate — a loop that is WRITING files is shipping, not stuck.
|
|
961
988
|
if (isLoopWriteTool(String(event?.toolName ?? ""))) {
|
|
@@ -1034,6 +1061,7 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1034
1061
|
});
|
|
1035
1062
|
}
|
|
1036
1063
|
appendLedger(ctx.cwd, "session_shutdown", { reason: shutdownReason });
|
|
1064
|
+
clearInBandProviderFailure();
|
|
1037
1065
|
markSessionOwnerShutdown(ctx.cwd, shutdownReason);
|
|
1038
1066
|
writeSessionHandoff(ctx, shutdownReason);
|
|
1039
1067
|
sessionReplacementUntil = Date.now() + SESSION_REBIND_GRACE_MS;
|
|
@@ -1136,6 +1164,7 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1136
1164
|
const startReason = typeof event?.reason === "string" ? event.reason : "unknown";
|
|
1137
1165
|
initialSessionLoadPending = isBlankInitialStartup(ctx, startReason);
|
|
1138
1166
|
rememberCtx(ctx);
|
|
1167
|
+
clearInBandProviderFailure();
|
|
1139
1168
|
startHeartbeat();
|
|
1140
1169
|
startUITicker();
|
|
1141
1170
|
// v0.30.0: rebind bookkeeping — claim ownership, close any replacement
|
|
@@ -1744,10 +1773,10 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1744
1773
|
// v0.27.3: enrich lastA with text + priorText for the smarter nudge
|
|
1745
1774
|
// accounting below.
|
|
1746
1775
|
const assistants = (event.messages as any[]).filter((m: any) => m.role === "assistant");
|
|
1747
|
-
|
|
1776
|
+
let rawLastA = assistants.length ? assistants[assistants.length - 1] : null;
|
|
1748
1777
|
const rawPriorA = assistants.length >= 2 ? assistants[assistants.length - 2] : null;
|
|
1749
1778
|
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") : "";
|
|
1750
|
-
|
|
1779
|
+
let lastA = rawLastA ? { stopReason: rawLastA.stopReason, text: extractText(rawLastA), priorText: extractText(rawPriorA) } : null;
|
|
1751
1780
|
// v0.34.19: pi-ai clamps max_tokens to the remaining context before the
|
|
1752
1781
|
// provider call. At ~99% context that clamp can be 1 token, which the
|
|
1753
1782
|
// provider reports as stopReason "length" — but this is NOT an overlong
|
|
@@ -1835,6 +1864,23 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1835
1864
|
// never pass the same failure into main-goal recovery.
|
|
1836
1865
|
return;
|
|
1837
1866
|
}
|
|
1867
|
+
// A repeated provider pane arrived through a successful tool transport,
|
|
1868
|
+
// so pi reports an ordinary end_turn. Convert it into the same bounded
|
|
1869
|
+
// recovery envelope as a provider error before loop measurement/stuck
|
|
1870
|
+
// accounting can consume the dead turn.
|
|
1871
|
+
if (inBandProviderFailureRaw && isLoopActive()) {
|
|
1872
|
+
const raw = inBandProviderFailureRaw;
|
|
1873
|
+
clearInBandProviderFailure();
|
|
1874
|
+
const loop = state.loop!;
|
|
1875
|
+
// The normal loop error branch owns the consecutive-error counter and
|
|
1876
|
+
// its bounded recovery cap. Clearing the fingerprints here prevents the
|
|
1877
|
+
// same pane from being reclassified before that branch runs.
|
|
1878
|
+
loop.recentToolResults = [];
|
|
1879
|
+
rawLastA = { ...(rawLastA ?? {}), stopReason: "error", errorMessage: raw, content: [] };
|
|
1880
|
+
lastA = { stopReason: "error", text: raw, priorText: lastA?.priorText ?? "" };
|
|
1881
|
+
} else if (inBandProviderFailureRaw) {
|
|
1882
|
+
clearInBandProviderFailure();
|
|
1883
|
+
}
|
|
1838
1884
|
if (await handleMainModelAgentEnd(ctx, rawLastA, lastA)) return;
|
|
1839
1885
|
// v0.25.2: per-goal turn telemetry (/glla stats).
|
|
1840
1886
|
if (state.goal && state.goal.status === "active") {
|
|
@@ -2292,6 +2338,7 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
2292
2338
|
});
|
|
2293
2339
|
pi.on("agent_start", (_event: any, ctx: ExtensionContext) => {
|
|
2294
2340
|
rememberCtx(ctx);
|
|
2341
|
+
clearInBandProviderFailure();
|
|
2295
2342
|
if (tryAbsorbHostSuccessor(ctx, "agent_start")) {
|
|
2296
2343
|
ensureAgentToolsReady(ctx, true);
|
|
2297
2344
|
return;
|
|
@@ -318,6 +318,7 @@ import {
|
|
|
318
318
|
listAuditFanoutItemText,
|
|
319
319
|
type LoopTickOutcome,
|
|
320
320
|
HELD_ON_RESTORE,
|
|
321
|
+
isRefinableStoppedLoopReason,
|
|
321
322
|
type LoopState,
|
|
322
323
|
} from "../goal-loop-forever.js";
|
|
323
324
|
import {
|
|
@@ -1928,7 +1929,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1928
1929
|
pi.registerTool(defineTool({
|
|
1929
1930
|
name: "propose_loop_refine",
|
|
1930
1931
|
label: "Propose loop spec refinement",
|
|
1931
|
-
description: "While a loop is
|
|
1932
|
+
description: "While a loop is active or safely stopped by a recoverable bound/failure, propose refining its spec — sharpen the target and/or change the measure command — when the current spec no longer captures 'better'. The user confirms; on a measure change the orchestrator test-runs the new command and re-baselines. Never edit the measure command or its inputs directly — that is gaming the metric.",
|
|
1932
1933
|
parameters: Type.Object({
|
|
1933
1934
|
target: Type.Optional(Type.String({ description: "The sharpened target text (omit to keep the current target)" })),
|
|
1934
1935
|
measureCmd: Type.Optional(Type.String({ description: "The new measure command printing ONE number (omit to keep the current metric)" })),
|
|
@@ -1943,9 +1944,11 @@ function registerAgentTools(pi: any): void {
|
|
|
1943
1944
|
const liveCtx = currentToolContext(execCtx);
|
|
1944
1945
|
if (!liveCtx) return staleToolResult();
|
|
1945
1946
|
const loop = state.loop;
|
|
1946
|
-
|
|
1947
|
-
|
|
1947
|
+
const stoppedRefinable = !!loop && !loop.active && isRefinableStoppedLoopReason(loop.stopReason);
|
|
1948
|
+
if (!loop || (!loop.active && !stoppedRefinable)) {
|
|
1949
|
+
return { content: [{ type: "text", text: "No refinable loop is available. propose_loop_refine applies while a loop is running or after a recoverable bound/failure stop; clean max-iteration and user-finished loops require /loop start." }], details: {} };
|
|
1948
1950
|
}
|
|
1951
|
+
const wasActive = loop.active;
|
|
1949
1952
|
const newTarget = p.target?.trim() || loop.target;
|
|
1950
1953
|
const newMeasure = p.measureCmd?.trim() || loop.measureCmd || "";
|
|
1951
1954
|
// v0.23.0: a metricless loop can't be refined into a measured one
|
|
@@ -1989,7 +1992,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1989
1992
|
confirmed = (await confirmDraft(
|
|
1990
1993
|
liveCtx,
|
|
1991
1994
|
"Confirm loop spec refinement",
|
|
1992
|
-
`Rationale: ${sanitizeDisplayText(p.rationale)}\n\nTarget:\n old: ${displaySlice(loop.target, 120)}\n new: ${displaySlice(newTarget, 120)}\n\nMeasure:\n old: ${sanitizeDisplayText(loop.measureCmd ?? "none")}\n new: ${sanitizeDisplayText(newMeasure)}${newMeasure !== loop.measureCmd ? `\n test-run: ${sanitizeDisplayText(testOutput).slice(0, 120)} → ${newBaseline}` : ""}${specChange ? `\n\nSpec file (${sanitizeDisplayText(loop.specFile ?? "")}:\n ${p.specText?.trim() ? `REPLACE with ${p.specText!.trim().length} chars` : ""}${p.specText?.trim() && p.specAppend?.trim() ? " + " : ""}${p.specAppend?.trim() ? `APPEND: ${sanitizeDisplayText(p.specAppend!.trim()).slice(0, 120)}` : ""}` : ""}\n\nThe loop keeps running against the refined spec (iteration ${loop.iteration} so far). Apply?`,
|
|
1995
|
+
`Rationale: ${sanitizeDisplayText(p.rationale)}\n\nTarget:\n old: ${displaySlice(loop.target, 120)}\n new: ${displaySlice(newTarget, 120)}\n\nMeasure:\n old: ${sanitizeDisplayText(loop.measureCmd ?? "none")}\n new: ${sanitizeDisplayText(newMeasure)}${newMeasure !== loop.measureCmd ? `\n test-run: ${sanitizeDisplayText(testOutput).slice(0, 120)} → ${newBaseline}` : ""}${specChange ? `\n\nSpec file (${sanitizeDisplayText(loop.specFile ?? "")}:\n ${p.specText?.trim() ? `REPLACE with ${p.specText!.trim().length} chars` : ""}${p.specText?.trim() && p.specAppend?.trim() ? " + " : ""}${p.specAppend?.trim() ? `APPEND: ${sanitizeDisplayText(p.specAppend!.trim()).slice(0, 120)}` : ""}` : ""}\n\nThe loop ${wasActive ? "keeps running" : "stays stopped until /loop resume"} against the refined spec (iteration ${loop.iteration} so far). Apply?`,
|
|
1993
1996
|
)) === "yes";
|
|
1994
1997
|
} catch {
|
|
1995
1998
|
confirmed = false;
|
|
@@ -2026,8 +2029,10 @@ function registerAgentTools(pi: any): void {
|
|
|
2026
2029
|
}
|
|
2027
2030
|
persistState(liveCtx);
|
|
2028
2031
|
appendLedger(liveCtx.cwd, "loop_refined", { iteration: loop.iteration, newTarget, newMeasureCmd: newMeasure, newBaseline, specChanged: specChange || undefined });
|
|
2029
|
-
liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}${specChange ? " Spec file updated." : ""}`, "info");
|
|
2030
|
-
return { content: [{ type: "text", text:
|
|
2032
|
+
liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}${specChange ? " Spec file updated." : ""}${wasActive ? "" : " Run /loop resume to continue with the preserved history."}`, "info");
|
|
2033
|
+
return { content: [{ type: "text", text: wasActive
|
|
2034
|
+
? "Refinement confirmed and applied. Continue improving against the NEW spec — one small change per turn."
|
|
2035
|
+
: "Refinement confirmed and applied to the stopped loop. Run /loop resume to continue with the preserved history." }], details: {} };
|
|
2031
2036
|
},
|
|
2032
2037
|
}));
|
|
2033
2038
|
|
|
@@ -159,6 +159,20 @@ export function classifyMainModelFailure(error: string | undefined, opts?: { isC
|
|
|
159
159
|
return { kind: "unknown", raw };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** A successful tool invocation can still carry a provider/network failure
|
|
163
|
+
* in its output. Only strong pane-shaped markers are eligible here; the loop
|
|
164
|
+
* caller additionally requires the same tool/result fingerprint to repeat
|
|
165
|
+
* before turning this into model recovery, so a one-off `503` in a searched
|
|
166
|
+
* document is not enough to park a loop. */
|
|
167
|
+
const IN_BAND_PROVIDER_FAILURE_PATTERN = /\b(?:http\s*)?(?:429|5\d\d)\b|rate[_ -]?limit|too many requests|network[_ -]?error|upstream(?:\s+(?:error|failure|unavailable))?|service unavailable|fetch failed|econn(?:reset|refused)|gateway(?:\s+(?:error|timeout))?/i;
|
|
168
|
+
|
|
169
|
+
export function classifyInBandProviderFailure(output: string | undefined): MainModelFailure | undefined {
|
|
170
|
+
const raw = typeof output === "string" ? output.trim() : "";
|
|
171
|
+
if (!raw || !IN_BAND_PROVIDER_FAILURE_PATTERN.test(raw)) return undefined;
|
|
172
|
+
const failure = classifyMainModelFailure(raw);
|
|
173
|
+
return failure.kind === "non-recoverable" ? undefined : failure;
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
/** v0.34.116: detect when a length-context failure happened AFTER the
|
|
163
177
|
* session_compact already failed. The classifier maps this to
|
|
164
178
|
* `context-overflow` (rollback path: rotate to a larger-context ref). The
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.68",
|
|
4
4
|
"description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "dracon",
|