pi-goal-list-loop-audit 0.28.0 → 0.28.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.
|
@@ -156,6 +156,12 @@ export interface Goal {
|
|
|
156
156
|
stopReason?: string;
|
|
157
157
|
pauseReason?: string;
|
|
158
158
|
pauseSuggestedAction?: string;
|
|
159
|
+
/** v0.28.1 (S1/S2): stale-handle interrupt marker. Set INSTEAD of pausing
|
|
160
|
+
* when pi invalidates the extension handle mid-goal — the goal stays
|
|
161
|
+
* active so a fresh session auto-resumes it via the restore gate. Cleared
|
|
162
|
+
* on that auto-resume. */
|
|
163
|
+
interruptedAt?: string;
|
|
164
|
+
interruptedReason?: string;
|
|
159
165
|
/** v0.25.0 (contract item 22): auditor objections extracted as TODOs when
|
|
160
166
|
* aggressiveMode keeps the goal active past the disapproval cap. Rendered
|
|
161
167
|
* into every continuation prompt until the next audit clears them. */
|
|
@@ -137,6 +137,11 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
|
|
|
137
137
|
return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}`;
|
|
138
138
|
}
|
|
139
139
|
if (g.status === "active") {
|
|
140
|
+
// v0.28.1 (S1/S2): a stale-handle interrupt keeps the goal ACTIVE (the
|
|
141
|
+
// next fresh session auto-resumes it) — say so instead of looking healthy.
|
|
142
|
+
if (g.interruptedAt) {
|
|
143
|
+
return `glla: ${g.policy} ${paint(theme, "error", "⚠ interrupted — stale handle · auto-resumes on pi restart")}`;
|
|
144
|
+
}
|
|
140
145
|
// v0.24.7: list policy gets its own wording — a queue item is not a goal.
|
|
141
146
|
// Before: "glla: list ● 3m 19s · list 29" (policy label AND queue counter
|
|
142
147
|
// both said "list"). After: "glla: list ● 3m 19s · 29 queued". Goal
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -198,24 +198,57 @@ let extensionApi: ExtensionAPI | null = null;
|
|
|
198
198
|
// failure shape. Detect the stale signature once and go terminally loud.
|
|
199
199
|
let extensionApiStale = false;
|
|
200
200
|
|
|
201
|
-
/** v0.26.7: a stale api is terminal for this process —
|
|
202
|
-
*
|
|
201
|
+
/** v0.26.7: a stale api is terminal for this process — go loudly with
|
|
202
|
+
* restart guidance instead of retrying sends that can never land.
|
|
203
|
+
* v0.28.1 (S1/S2): goals STAY ACTIVE with an interrupt marker instead of
|
|
204
|
+
* pausing — the restore gate only auto-resumes ACTIVE goals, so pausing
|
|
205
|
+
* here stranded goals until manual /goal resume (hegemon/sraaal shape).
|
|
206
|
+
* sendContinuation's extensionApiStale guard already stops further sends
|
|
207
|
+
* in this doomed process; the next fresh session auto-resumes. */
|
|
203
208
|
function goStaleTerminal(ctx: ExtensionContext, where: string): void {
|
|
204
209
|
if (extensionApiStale) return; // already terminal — don't re-spam
|
|
205
210
|
extensionApiStale = true;
|
|
206
211
|
appendLedger(ctx.cwd, "extension_api_stale", { where, kind: isLoopActive() ? "loop" : "goal" });
|
|
207
|
-
const guidance = "pi invalidated this session's extension handle (session replacement — compaction triggers it in pi 0.82.x). Sends can never land in this process. Restart pi (or reload extensions)
|
|
212
|
+
const guidance = "pi invalidated this session's extension handle (session replacement — compaction triggers it in pi 0.82.x). Sends can never land in this process. Restart pi (or reload extensions) — an active goal auto-resumes on the fresh session; loops need /loop start.";
|
|
208
213
|
if (isLoopActive()) {
|
|
209
214
|
clearLoopTimer();
|
|
210
215
|
state.loop = { ...state.loop!, active: false, stopReason: `extension api stale: ${guidance}` };
|
|
211
216
|
persistState(ctx);
|
|
212
217
|
} else if (state.goal && state.goal.status === "active") {
|
|
213
|
-
updateGoal({
|
|
218
|
+
updateGoal({ interruptedAt: nowIso(), interruptedReason: `extension api stale (${where})` }, ctx);
|
|
214
219
|
}
|
|
215
220
|
ctx.ui.notify(`glla: ${guidance}`, "warning");
|
|
216
221
|
notifyExternal(ctx, `glla: extension api stale — restart pi. (${where})`);
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
/** v0.28.1 (S3): side-effect-free staleness probe — getSessionName()
|
|
225
|
+
* routes through pi's assertActive() and throws the stale signature iff
|
|
226
|
+
* pi invalidated this factory handle (session replacement). A positive
|
|
227
|
+
* result is cached in extensionApiStale. */
|
|
228
|
+
function probeExtensionApiStale(): boolean {
|
|
229
|
+
if (extensionApiStale) return true;
|
|
230
|
+
if (!extensionApi) return false;
|
|
231
|
+
try {
|
|
232
|
+
extensionApi.getSessionName();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (isStaleApiError(err)) extensionApiStale = true;
|
|
235
|
+
}
|
|
236
|
+
return extensionApiStale;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** v0.28.1 (S3): command-entry staleness probe + honest warning. Returns
|
|
240
|
+
* true when the handle is stale — callers must skip send-dependent paths
|
|
241
|
+
* and must NOT claim work started (S3's "created — starting now" lie). */
|
|
242
|
+
function warnIfStaleAtEntry(ctx: ExtensionContext, what: string): boolean {
|
|
243
|
+
if (!probeExtensionApiStale()) return false;
|
|
244
|
+
appendLedger(ctx.cwd, "extension_api_stale", { where: `entry probe (${what})` });
|
|
245
|
+
ctx.ui.notify(
|
|
246
|
+
`glla: this session's extension handle is stale (pi session replacement) — ${what} can't send continuations in this process. State is safe in .pi-glla/ — restart pi and the active goal auto-resumes.`,
|
|
247
|
+
"warning",
|
|
248
|
+
);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
// The most recent ExtensionContext seen from any event or command handler.
|
|
220
253
|
// pi replaces sessions (newSession/fork/reload) and stale ctx throws on use,
|
|
221
254
|
// so timers must never capture a ctx — they read lastCtx at fire time.
|
|
@@ -842,8 +875,18 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
842
875
|
draftingUserReplies = 0;
|
|
843
876
|
draftingBlockedProposals = 0;
|
|
844
877
|
draftingSeedInFlight = true; // our injected prompt also arrives as a user message — don't count it
|
|
845
|
-
} catch {
|
|
878
|
+
} catch (err) {
|
|
846
879
|
draftingTarget = null;
|
|
880
|
+
// v0.28.1 (E6): the seed send used to fail SILENTLY — the user pressed
|
|
881
|
+
// Enter on /goal and nothing happened. Now: loud, and stale handles get
|
|
882
|
+
// the honest restart guidance.
|
|
883
|
+
if (isStaleApiError(err)) {
|
|
884
|
+
extensionApiStale = true;
|
|
885
|
+
appendLedger(ctx.cwd, "extension_api_stale", { where: "startDrafting seed" });
|
|
886
|
+
ctx.ui.notify("glla: can't start the drafting interview — this session's extension handle is stale (pi session replacement). Restart pi and re-run the command.", "warning");
|
|
887
|
+
} else {
|
|
888
|
+
ctx.ui.notify(`glla: couldn't start the drafting interview (${err instanceof Error ? err.message : String(err)}) — try again.`, "warning");
|
|
889
|
+
}
|
|
847
890
|
}
|
|
848
891
|
}
|
|
849
892
|
|
|
@@ -880,6 +923,10 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
880
923
|
// =================================================================
|
|
881
924
|
|
|
882
925
|
async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): Promise<void> {
|
|
926
|
+
// v0.28.1 (S3): probe at the creation entry — no "created — starting now"
|
|
927
|
+
// lie in a doomed process. (The draft path's seed send has its own loud
|
|
928
|
+
// stale handling — E6.)
|
|
929
|
+
const staleEntry = warnIfStaleAtEntry(ctx, "/goal");
|
|
883
930
|
let raw = args.trim();
|
|
884
931
|
// Users naturally quote the objective ("/goal \"do X\""); strip one layer of
|
|
885
932
|
// surrounding matching quotes so they don't leak into the goal text.
|
|
@@ -909,6 +956,13 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
|
|
|
909
956
|
iterationCounter = 0;
|
|
910
957
|
consecutiveErrorIterations = 0;
|
|
911
958
|
consecutiveNoToolIterations = 0;
|
|
959
|
+
if (staleEntry) {
|
|
960
|
+
// v0.28.1 (S3): the goal is persisted — mark the interrupt so the next
|
|
961
|
+
// fresh session auto-resumes, and tell the truth instead of "starting now".
|
|
962
|
+
updateGoal({ interruptedAt: nowIso(), interruptedReason: "created in a stale session" }, ctx);
|
|
963
|
+
ctx.ui.notify(`Goal ${goal.id} created and safe in .pi-glla/ — this stale process can't send continuations. Restart pi and it auto-resumes.`, "warning");
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
912
966
|
ctx.ui.notify(`Goal ${goal.id} created — starting now. Auditor will verify on completion.`, "info");
|
|
913
967
|
scheduleContinuation(ctx, true);
|
|
914
968
|
}
|
|
@@ -949,6 +1003,12 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
|
949
1003
|
|
|
950
1004
|
async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
951
1005
|
if (!state.goal || state.goal.status !== "paused") return;
|
|
1006
|
+
// v0.28.1 (S1/S3): resuming in a stale session used to flip status to
|
|
1007
|
+
// active, claim "Resumed goal", then re-pause on the stale send failure
|
|
1008
|
+
// (or zombie — S1). Now: persist the resume (the next fresh session
|
|
1009
|
+
// auto-resumes ACTIVE goals), mark the interrupt, tell the truth, and
|
|
1010
|
+
// skip the send that can never land.
|
|
1011
|
+
const staleEntry = warnIfStaleAtEntry(ctx, "/goal resume");
|
|
952
1012
|
// v0.12.0: refresh the token cap from CURRENT settings on resume — goals
|
|
953
1013
|
// snapshot the cap at creation, so a goal paused under an old default
|
|
954
1014
|
// (e.g. 10M) would re-pause instantly even after the default changed.
|
|
@@ -956,7 +1016,8 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
956
1016
|
const usage = state.goal.usage
|
|
957
1017
|
? { tokensUsed: state.goal.usage.tokensUsed, tokensLimit: freshLimit }
|
|
958
1018
|
: undefined;
|
|
959
|
-
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(usage ? { usage } : {}) }, ctx);
|
|
1019
|
+
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
|
|
1020
|
+
if (staleEntry) return;
|
|
960
1021
|
// v0.22.5: say what was resumed — with a non-empty list this also resumes
|
|
961
1022
|
// the queue (the active goal IS the list's head item).
|
|
962
1023
|
// v0.22.7: name WHAT was resumed — list items resume through /list.
|
|
@@ -1111,6 +1172,8 @@ async function bulkAddFromFile(ctx: ExtensionContext, abs: string): Promise<void
|
|
|
1111
1172
|
}
|
|
1112
1173
|
|
|
1113
1174
|
async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
1175
|
+
// v0.28.1 (S3): honest staleness warning; read-only subcommands still work.
|
|
1176
|
+
warnIfStaleAtEntry(ctx, "/list");
|
|
1114
1177
|
const parts = args.trim().split(/\s+/);
|
|
1115
1178
|
const sub = (parts[0] ?? "").toLowerCase();
|
|
1116
1179
|
const rest = args.trim().slice(sub.length).trim();
|
|
@@ -2407,6 +2470,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2407
2470
|
};
|
|
2408
2471
|
}
|
|
2409
2472
|
const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
|
|
2473
|
+
// v0.28.1 (S3): honest staleness warning before any Confirm attempt.
|
|
2474
|
+
warnIfStaleAtEntry(liveCtx, "goal drafting");
|
|
2410
2475
|
// Multi-item list draft: one Confirm for the whole batch.
|
|
2411
2476
|
if (p.items && p.items.length > 0) {
|
|
2412
2477
|
// v0.23.7: show ALL items in full — the user approves the whole
|
|
@@ -2424,7 +2489,14 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2424
2489
|
"Confirm list batch",
|
|
2425
2490
|
`${p.items.length} items:\n${preview}${batchActivates ? "\n\n(List is empty — confirming ACTIVATES item 1 immediately as the active goal.)" : ""}`,
|
|
2426
2491
|
);
|
|
2427
|
-
} catch {
|
|
2492
|
+
} catch (err) {
|
|
2493
|
+
// v0.28.1 (T1): a stale confirm is NOT a rejection — nothing was
|
|
2494
|
+
// refused; the dialog simply can't render in a doomed process.
|
|
2495
|
+
if (isStaleApiError(err)) {
|
|
2496
|
+
extensionApiStale = true;
|
|
2497
|
+
appendLedger(liveCtx.cwd, "extension_api_stale", { where: "batch confirm" });
|
|
2498
|
+
return { content: [{ type: "text", text: "The Confirm dialog could not render: pi invalidated this session's extension handle (session replacement). This is NOT a rejection — do NOT refine or re-propose. Tell the user to restart pi, then re-run the drafting flow." }], details: {} };
|
|
2499
|
+
}
|
|
2428
2500
|
batchConfirmed = false;
|
|
2429
2501
|
}
|
|
2430
2502
|
}
|
|
@@ -2465,7 +2537,13 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2465
2537
|
} else {
|
|
2466
2538
|
try {
|
|
2467
2539
|
confirmed = await liveCtx.ui.confirm(isListDraft ? "Confirm list item" : "Confirm goal", `${p.objective.trim()}${contractBlock}${activationNote}`);
|
|
2468
|
-
} catch {
|
|
2540
|
+
} catch (err) {
|
|
2541
|
+
// v0.28.1 (T1): a stale confirm is NOT "Draft rejected by the user".
|
|
2542
|
+
if (isStaleApiError(err)) {
|
|
2543
|
+
extensionApiStale = true;
|
|
2544
|
+
appendLedger(liveCtx.cwd, "extension_api_stale", { where: "draft confirm" });
|
|
2545
|
+
return { content: [{ type: "text", text: "The Confirm dialog could not render: pi invalidated this session's extension handle (session replacement). This is NOT a rejection — do NOT refine or re-propose. Tell the user to restart pi, then re-run the drafting flow." }], details: {} };
|
|
2546
|
+
}
|
|
2469
2547
|
confirmed = false;
|
|
2470
2548
|
}
|
|
2471
2549
|
}
|
|
@@ -4032,8 +4110,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4032
4110
|
}
|
|
4033
4111
|
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
4034
4112
|
if (autoResume) {
|
|
4113
|
+
// v0.28.1 (S2): clear the stale-handle interrupt marker — this IS
|
|
4114
|
+
// the auto-resume the marker promised.
|
|
4115
|
+
const wasInterrupted = !!state.goal.interruptedAt;
|
|
4116
|
+
if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
|
|
4035
4117
|
ctx.ui.notify(
|
|
4036
|
-
`Resuming ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
4118
|
+
`Resuming ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}${wasInterrupted ? " — auto-resumed after the stale-handle interrupt" : ""}`,
|
|
4037
4119
|
"info",
|
|
4038
4120
|
);
|
|
4039
4121
|
scheduleContinuation(ctx, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 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 \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|
package/schemas/goal.schema.json
CHANGED
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"stopReason": { "type": "string" },
|
|
47
47
|
"pauseReason": { "type": "string" },
|
|
48
48
|
"pauseSuggestedAction": { "type": "string" },
|
|
49
|
+
"interruptedAt": { "type": "string" },
|
|
50
|
+
"interruptedReason": { "type": "string" },
|
|
49
51
|
"activePath": { "type": "string" },
|
|
50
52
|
"archivedPath": { "type": "string" },
|
|
51
53
|
"usage": {
|