pi-goal-list-loop-audit 0.35.65 → 0.35.67
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 +27 -0
- package/extensions/goal-loop-auditor-process.ts +13 -2
- package/extensions/goal-loop-forever.ts +3 -2
- package/extensions/goal-loop-repetition.ts +12 -0
- package/extensions/loops/goal-activation.ts +50 -3
- package/extensions/main-model-recovery.ts +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.67 — in-band provider-result recovery (2026-08-26)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
Repeated successful tool transports that carry a strong 503/429/network
|
|
7
|
+
provider pane no longer enter loop stuck or plateau accounting as ordinary
|
|
8
|
+
work. After the same tool/result fingerprint repeats, the turn is routed
|
|
9
|
+
through the existing provider-recovery envelope; one-off status text in a
|
|
10
|
+
searched document remains ordinary output.
|
|
11
|
+
|
|
12
|
+
### Tests
|
|
13
|
+
Coverage spans provider-marker classification, repeated-pane detection,
|
|
14
|
+
loop-turn exemption, durable recovery parking, and the unchanged real-error
|
|
15
|
+
and repetition paths. The full release gate remains green.
|
|
16
|
+
|
|
17
|
+
## 0.35.66 — compiled-host auditor launcher (2026-08-26)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
Detached completion audits no longer launch the worker with a compiled Pi
|
|
21
|
+
executable. The process layer preserves explicit runtime overrides, keeps
|
|
22
|
+
Node/Node.js/Bun/Deno `process.execPath` values, and falls back to `node` for
|
|
23
|
+
compiled hosts that would otherwise parse worker flags such as `--job-dir`.
|
|
24
|
+
|
|
25
|
+
### Tests
|
|
26
|
+
Runtime-resolution coverage includes Unix, Windows, Node.js aliases, Bun,
|
|
27
|
+
Deno, compiled Pi paths, and bare executable names. The existing explicit
|
|
28
|
+
runtime-override transport regression remains covered.
|
|
29
|
+
|
|
3
30
|
## 0.35.65 — status surfaces and worker liveness (2026-08-26)
|
|
4
31
|
|
|
5
32
|
### Added
|
|
@@ -573,7 +573,7 @@ interface AuditorProgressFile {
|
|
|
573
573
|
export type AuditorInfrastructureClass = "no-verdict" | "timeout" | "transport" | "provider";
|
|
574
574
|
|
|
575
575
|
export interface AuditorProcessRuntime {
|
|
576
|
-
/** Override the worker launcher command (normally process.execPath). */
|
|
576
|
+
/** Override the worker launcher command (normally resolved from process.execPath, with a JS-runtime fallback for compiled hosts). */
|
|
577
577
|
command?: string;
|
|
578
578
|
/** Override the worker module (normally scripts/goal-auditor-worker.mjs). */
|
|
579
579
|
workerPath?: string;
|
|
@@ -761,6 +761,17 @@ function stampToken<T extends GoalAuditorResult>(result: T, capturedToken: GoalR
|
|
|
761
761
|
return { ...result, goalRevision: capturedToken };
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
+
const JAVASCRIPT_RUNTIME_BASENAMES = new Set(["node", "nodejs", "bun", "deno"]);
|
|
765
|
+
|
|
766
|
+
/** Resolve the runtime that can execute the extension-less worker module.
|
|
767
|
+
* In a normal Node/Bun/Deno host, process.execPath is already a JavaScript
|
|
768
|
+
* runtime. In a compiled Pi host it is the Pi executable, which would parse
|
|
769
|
+
* worker flags such as --job-dir itself and exit before the worker starts. */
|
|
770
|
+
export function resolveWorkerCommand(execPath: string): string {
|
|
771
|
+
const base = path.basename(execPath.replace(/\\/g, "/")).replace(/\.exe$/i, "").toLowerCase();
|
|
772
|
+
return JAVASCRIPT_RUNTIME_BASENAMES.has(base) ? execPath : "node";
|
|
773
|
+
}
|
|
774
|
+
|
|
764
775
|
/**
|
|
765
776
|
* Run one completion audit in a detached, extension-less child process.
|
|
766
777
|
* Infrastructure failures never become semantic disapprovals and never fall
|
|
@@ -881,7 +892,7 @@ export async function runDetachedGoalCompletionAuditor(args: {
|
|
|
881
892
|
args.onProgress?.(asProgress(initialProgress, startedAt));
|
|
882
893
|
|
|
883
894
|
const workerPath = runtime.workerPath ?? defaultWorkerPath();
|
|
884
|
-
const command = runtime.command ?? process.execPath;
|
|
895
|
+
const command = runtime.command ?? resolveWorkerCommand(process.execPath);
|
|
885
896
|
const spawn = runtime.spawn ?? nodeSpawn;
|
|
886
897
|
const env = { ...process.env, ...(runtime.env ?? {}) };
|
|
887
898
|
if (runtime.piBinary) env.GLLA_PI_BINARY = runtime.piBinary;
|
|
@@ -113,8 +113,9 @@ export interface LoopState {
|
|
|
113
113
|
recentPrints?: string[];
|
|
114
114
|
/** v0.24.0: last few iteration texts (near-duplicate check + banned openings). */
|
|
115
115
|
recentTexts?: string[];
|
|
116
|
-
/** v0.24.0: rolling tool-result fingerprints {tool, hash, isError}
|
|
117
|
-
|
|
116
|
+
/** v0.24.0: rolling tool-result fingerprints {tool, hash, isError};
|
|
117
|
+
* providerFailure marks a repeated in-band provider/network pane. */
|
|
118
|
+
recentToolResults?: { tool: string; hash: string; isError: boolean; providerFailure?: boolean }[];
|
|
118
119
|
/** v0.24.0: tool calls seen since the last completed iteration. */
|
|
119
120
|
toolsThisTurn?: number;
|
|
120
121
|
/** 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 {
|
|
@@ -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;
|
|
@@ -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.67",
|
|
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",
|