gsd-pi 2.3.8 → 2.3.9
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/README.md +5 -2
- package/dist/cli.js +32 -2
- package/dist/logo.d.ts +16 -0
- package/dist/logo.js +25 -0
- package/dist/onboarding.d.ts +43 -0
- package/dist/onboarding.js +425 -0
- package/dist/wizard.js +8 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +38 -9
- package/src/resources/GSD-WORKFLOW.md +2 -2
- package/src/resources/extensions/google-search/index.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +353 -144
- package/src/resources/extensions/gsd/files.ts +9 -7
- package/src/resources/extensions/gsd/index.ts +3 -1
- package/src/resources/extensions/gsd/metrics.ts +7 -5
- package/src/resources/extensions/gsd/migrate/command.ts +4 -1
- package/src/resources/extensions/gsd/migrate/validator.ts +5 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/parsers.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +23 -6
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +116 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +22 -1
- package/src/resources/extensions/gsd/workspace-index.ts +2 -2
- package/src/resources/extensions/gsd/worktree-command.ts +147 -41
- package/src/resources/extensions/gsd/worktree.ts +105 -8
- package/src/resources/extensions/mcporter/index.ts +21 -2
- package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
- package/src/resources/extensions/search-the-web/http.ts +1 -1
- package/src/resources/extensions/search-the-web/index.ts +9 -3
- package/src/resources/extensions/search-the-web/provider.ts +118 -0
- package/src/resources/extensions/search-the-web/tavily.ts +116 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +265 -108
- package/src/resources/extensions/search-the-web/tool-search.ts +161 -88
- package/src/resources/extensions/subagent/index.ts +1 -1
|
@@ -61,7 +61,9 @@ import {
|
|
|
61
61
|
autoCommitCurrentBranch,
|
|
62
62
|
ensureSliceBranch,
|
|
63
63
|
getCurrentBranch,
|
|
64
|
+
getMainBranch,
|
|
64
65
|
getSliceBranchName,
|
|
66
|
+
parseSliceBranch,
|
|
65
67
|
switchToMain,
|
|
66
68
|
mergeSliceToMain,
|
|
67
69
|
} from "./worktree.ts";
|
|
@@ -69,6 +71,39 @@ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
|
69
71
|
import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
|
|
70
72
|
import { showNextAction } from "../shared/next-action-ui.js";
|
|
71
73
|
|
|
74
|
+
// ─── Disk-backed completed-unit helpers ───────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** Path to the persisted completed-unit keys file. */
|
|
77
|
+
function completedKeysPath(base: string): string {
|
|
78
|
+
return join(base, ".gsd", "completed-units.json");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Write a completed unit key to disk (read-modify-write append to set). */
|
|
82
|
+
function persistCompletedKey(base: string, key: string): void {
|
|
83
|
+
const file = completedKeysPath(base);
|
|
84
|
+
let keys: string[] = [];
|
|
85
|
+
try {
|
|
86
|
+
if (existsSync(file)) {
|
|
87
|
+
keys = JSON.parse(readFileSync(file, "utf-8"));
|
|
88
|
+
}
|
|
89
|
+
} catch { /* corrupt file — start fresh */ }
|
|
90
|
+
if (!keys.includes(key)) {
|
|
91
|
+
keys.push(key);
|
|
92
|
+
writeFileSync(file, JSON.stringify(keys), "utf-8");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Load all completed unit keys from disk into the in-memory set. */
|
|
97
|
+
function loadPersistedKeys(base: string, target: Set<string>): void {
|
|
98
|
+
const file = completedKeysPath(base);
|
|
99
|
+
try {
|
|
100
|
+
if (existsSync(file)) {
|
|
101
|
+
const keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
|
|
102
|
+
for (const k of keys) target.add(k);
|
|
103
|
+
}
|
|
104
|
+
} catch { /* non-fatal */ }
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
73
108
|
|
|
74
109
|
let active = false;
|
|
@@ -78,10 +113,15 @@ let verbose = false;
|
|
|
78
113
|
let cmdCtx: ExtensionCommandContext | null = null;
|
|
79
114
|
let basePath = "";
|
|
80
115
|
|
|
81
|
-
/** Track
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
116
|
+
/** Track total dispatches per unit to detect stuck loops (catches A→B→A→B patterns) */
|
|
117
|
+
const unitDispatchCount = new Map<string, number>();
|
|
118
|
+
const MAX_UNIT_DISPATCHES = 3;
|
|
119
|
+
|
|
120
|
+
/** Tracks recovery attempt count per unit for backoff and diagnostics. */
|
|
121
|
+
const unitRecoveryCount = new Map<string, number>();
|
|
122
|
+
|
|
123
|
+
/** Persisted completed-unit keys — survives restarts. Loaded from .gsd/completed-units.json. */
|
|
124
|
+
const completedKeySet = new Set<string>();
|
|
85
125
|
|
|
86
126
|
/** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */
|
|
87
127
|
let pendingCrashRecovery: string | null = null;
|
|
@@ -102,6 +142,26 @@ let unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
|
102
142
|
let wrapupWarningHandle: ReturnType<typeof setTimeout> | null = null;
|
|
103
143
|
let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
|
|
104
144
|
|
|
145
|
+
/** Format token counts for compact display */
|
|
146
|
+
function formatWidgetTokens(count: number): string {
|
|
147
|
+
if (count < 1000) return count.toString();
|
|
148
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
149
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
150
|
+
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
|
151
|
+
return `${Math.round(count / 1000000)}M`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Footer factory that renders zero lines — hides the built-in footer entirely.
|
|
156
|
+
* All footer info (pwd, branch, tokens, cost, model) is shown inside the
|
|
157
|
+
* progress widget instead, so there's no gap or redundancy.
|
|
158
|
+
*/
|
|
159
|
+
const hideFooter = () => ({
|
|
160
|
+
render(_width: number): string[] { return []; },
|
|
161
|
+
invalidate() {},
|
|
162
|
+
dispose() {},
|
|
163
|
+
});
|
|
164
|
+
|
|
105
165
|
/** Dashboard data for the overlay */
|
|
106
166
|
export interface AutoDashboardData {
|
|
107
167
|
active: boolean;
|
|
@@ -185,13 +245,15 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
185
245
|
active = false;
|
|
186
246
|
paused = false;
|
|
187
247
|
stepMode = false;
|
|
188
|
-
|
|
248
|
+
unitDispatchCount.clear();
|
|
249
|
+
unitRecoveryCount.clear();
|
|
189
250
|
currentUnit = null;
|
|
190
251
|
currentMilestoneId = null;
|
|
191
252
|
cachedSliceProgress = null;
|
|
192
253
|
pendingCrashRecovery = null;
|
|
193
254
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
194
255
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
256
|
+
ctx?.ui.setFooter(undefined);
|
|
195
257
|
|
|
196
258
|
// Restore the user's original model
|
|
197
259
|
if (pi && ctx && originalModelId) {
|
|
@@ -214,11 +276,12 @@ export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Pro
|
|
|
214
276
|
if (basePath) clearLock(basePath);
|
|
215
277
|
active = false;
|
|
216
278
|
paused = true;
|
|
217
|
-
// Preserve:
|
|
279
|
+
// Preserve: unitDispatchCount, currentUnit, basePath, verbose, cmdCtx,
|
|
218
280
|
// completedUnits, autoStartTime, currentMilestoneId, originalModelId
|
|
219
281
|
// — all needed for resume and dashboard display
|
|
220
282
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
221
283
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
284
|
+
ctx?.ui.setFooter(undefined);
|
|
222
285
|
const resumeCmd = stepMode ? "/gsd next" : "/gsd auto";
|
|
223
286
|
ctx?.ui.notify(
|
|
224
287
|
`${stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
|
|
@@ -226,6 +289,33 @@ export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Pro
|
|
|
226
289
|
);
|
|
227
290
|
}
|
|
228
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Self-heal: scan runtime records in .gsd/ and clear any where the expected
|
|
294
|
+
* artifact already exists on disk. This repairs incomplete closeouts from
|
|
295
|
+
* prior crashes — preventing spurious re-dispatch of already-completed units.
|
|
296
|
+
*/
|
|
297
|
+
async function selfHealRuntimeRecords(base: string, ctx: ExtensionContext): Promise<void> {
|
|
298
|
+
try {
|
|
299
|
+
const { listUnitRuntimeRecords } = await import("./unit-runtime.js");
|
|
300
|
+
const records = listUnitRuntimeRecords(base);
|
|
301
|
+
let healed = 0;
|
|
302
|
+
for (const record of records) {
|
|
303
|
+
const { unitType, unitId } = record;
|
|
304
|
+
const artifactPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
305
|
+
if (artifactPath && existsSync(artifactPath)) {
|
|
306
|
+
// Artifact exists — unit completed but closeout didn't finish.
|
|
307
|
+
clearUnitRuntimeRecord(base, unitType, unitId);
|
|
308
|
+
healed++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (healed > 0) {
|
|
312
|
+
ctx.ui.notify(`Self-heal: cleared ${healed} stale runtime record(s) with completed artifacts.`, "info");
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
// Non-fatal — self-heal should never block auto-mode start
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
229
319
|
export async function startAuto(
|
|
230
320
|
ctx: ExtensionCommandContext,
|
|
231
321
|
pi: ExtensionAPI,
|
|
@@ -245,9 +335,11 @@ export async function startAuto(
|
|
|
245
335
|
stepMode = requestedStepMode;
|
|
246
336
|
cmdCtx = ctx;
|
|
247
337
|
basePath = base;
|
|
338
|
+
unitDispatchCount.clear();
|
|
248
339
|
// Re-initialize metrics in case ledger was lost during pause
|
|
249
340
|
if (!getLedger()) initMetrics(base);
|
|
250
341
|
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
342
|
+
ctx.ui.setFooter(hideFooter);
|
|
251
343
|
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
252
344
|
// Rebuild disk state before resuming — user interaction during pause may have changed files
|
|
253
345
|
try { await rebuildState(base); } catch { /* non-fatal */ }
|
|
@@ -257,6 +349,8 @@ export async function startAuto(
|
|
|
257
349
|
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
|
|
258
350
|
}
|
|
259
351
|
} catch { /* non-fatal */ }
|
|
352
|
+
// Self-heal: clear stale runtime records where artifacts already exist
|
|
353
|
+
await selfHealRuntimeRecords(base, ctx);
|
|
260
354
|
await dispatchNextUnit(ctx, pi);
|
|
261
355
|
return;
|
|
262
356
|
}
|
|
@@ -335,8 +429,10 @@ export async function startAuto(
|
|
|
335
429
|
verbose = verboseMode;
|
|
336
430
|
cmdCtx = ctx;
|
|
337
431
|
basePath = base;
|
|
338
|
-
|
|
339
|
-
|
|
432
|
+
unitDispatchCount.clear();
|
|
433
|
+
unitRecoveryCount.clear();
|
|
434
|
+
completedKeySet.clear();
|
|
435
|
+
loadPersistedKeys(base, completedKeySet);
|
|
340
436
|
autoStartTime = Date.now();
|
|
341
437
|
completedUnits = [];
|
|
342
438
|
currentUnit = null;
|
|
@@ -352,6 +448,7 @@ export async function startAuto(
|
|
|
352
448
|
}
|
|
353
449
|
|
|
354
450
|
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
451
|
+
ctx.ui.setFooter(hideFooter);
|
|
355
452
|
const modeLabel = stepMode ? "Step-mode" : "Auto-mode";
|
|
356
453
|
const pendingCount = state.registry.filter(m => m.status !== 'complete').length;
|
|
357
454
|
const scopeMsg = pendingCount > 1
|
|
@@ -359,6 +456,9 @@ export async function startAuto(
|
|
|
359
456
|
: "Will loop until milestone complete.";
|
|
360
457
|
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
361
458
|
|
|
459
|
+
// Self-heal: clear stale runtime records where artifacts already exist
|
|
460
|
+
await selfHealRuntimeRecords(base, ctx);
|
|
461
|
+
|
|
362
462
|
// Dispatch the first unit
|
|
363
463
|
await dispatchNextUnit(ctx, pi);
|
|
364
464
|
}
|
|
@@ -594,7 +694,18 @@ function updateProgressWidget(
|
|
|
594
694
|
const slice = state.activeSlice;
|
|
595
695
|
const task = state.activeTask;
|
|
596
696
|
const next = peekNext(unitType, state);
|
|
597
|
-
|
|
697
|
+
|
|
698
|
+
// Cache git branch at widget creation time (not per render)
|
|
699
|
+
let cachedBranch: string | null = null;
|
|
700
|
+
try { cachedBranch = getCurrentBranch(basePath); } catch { /* not in git repo */ }
|
|
701
|
+
|
|
702
|
+
// Cache pwd with ~ substitution
|
|
703
|
+
let widgetPwd = process.cwd();
|
|
704
|
+
const widgetHome = process.env.HOME || process.env.USERPROFILE;
|
|
705
|
+
if (widgetHome && widgetPwd.startsWith(widgetHome)) {
|
|
706
|
+
widgetPwd = `~${widgetPwd.slice(widgetHome.length)}`;
|
|
707
|
+
}
|
|
708
|
+
if (cachedBranch) widgetPwd = `${widgetPwd} (${cachedBranch})`;
|
|
598
709
|
|
|
599
710
|
ctx.ui.setWidget("gsd-progress", (tui, theme) => {
|
|
600
711
|
let pulseBright = true;
|
|
@@ -677,8 +788,63 @@ function updateProgressWidget(
|
|
|
677
788
|
));
|
|
678
789
|
}
|
|
679
790
|
|
|
791
|
+
// ── Footer info (pwd, tokens, cost, context, model) ──────────────
|
|
792
|
+
lines.push("");
|
|
793
|
+
lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
|
|
794
|
+
|
|
795
|
+
// Token stats from current unit session + cumulative cost from metrics
|
|
796
|
+
{
|
|
797
|
+
let totalInput = 0, totalOutput = 0;
|
|
798
|
+
let totalCacheRead = 0, totalCacheWrite = 0;
|
|
799
|
+
if (cmdCtx) {
|
|
800
|
+
for (const entry of cmdCtx.sessionManager.getEntries()) {
|
|
801
|
+
if (entry.type === "message" && (entry as any).message?.role === "assistant") {
|
|
802
|
+
const u = (entry as any).message.usage;
|
|
803
|
+
if (u) {
|
|
804
|
+
totalInput += u.input || 0;
|
|
805
|
+
totalOutput += u.output || 0;
|
|
806
|
+
totalCacheRead += u.cacheRead || 0;
|
|
807
|
+
totalCacheWrite += u.cacheWrite || 0;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const mLedger = getLedger();
|
|
813
|
+
const autoTotals = mLedger ? getProjectTotals(mLedger.units) : null;
|
|
814
|
+
const cumulativeCost = autoTotals?.cost ?? 0;
|
|
815
|
+
|
|
816
|
+
const cxUsage = cmdCtx?.getContextUsage?.();
|
|
817
|
+
const cxWindow = cxUsage?.contextWindow ?? cmdCtx?.model?.contextWindow ?? 0;
|
|
818
|
+
const cxPctVal = cxUsage?.percent ?? 0;
|
|
819
|
+
const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?";
|
|
820
|
+
|
|
821
|
+
const sp: string[] = [];
|
|
822
|
+
if (totalInput) sp.push(`↑${formatWidgetTokens(totalInput)}`);
|
|
823
|
+
if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`);
|
|
824
|
+
if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
|
|
825
|
+
if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
|
|
826
|
+
if (cumulativeCost) sp.push(`$${cumulativeCost.toFixed(3)}`);
|
|
827
|
+
|
|
828
|
+
const cxDisplay = cxPct === "?"
|
|
829
|
+
? `?/${formatWidgetTokens(cxWindow)}`
|
|
830
|
+
: `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
|
|
831
|
+
if (cxPctVal > 90) {
|
|
832
|
+
sp.push(theme.fg("error", cxDisplay));
|
|
833
|
+
} else if (cxPctVal > 70) {
|
|
834
|
+
sp.push(theme.fg("warning", cxDisplay));
|
|
835
|
+
} else {
|
|
836
|
+
sp.push(cxDisplay);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
|
|
840
|
+
.join(theme.fg("dim", " "));
|
|
841
|
+
|
|
842
|
+
const modelId = cmdCtx?.model?.id ?? "";
|
|
843
|
+
const sRight = modelId ? theme.fg("dim", modelId) : "";
|
|
844
|
+
lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
|
|
845
|
+
}
|
|
846
|
+
|
|
680
847
|
const hintParts: string[] = [];
|
|
681
|
-
if (preferredModel) hintParts.push(preferredModel);
|
|
682
848
|
hintParts.push("esc pause");
|
|
683
849
|
hintParts.push("Ctrl+Alt+G dashboard");
|
|
684
850
|
lines.push(...ui.hints(hintParts));
|
|
@@ -786,8 +952,8 @@ async function dispatchNextUnit(
|
|
|
786
952
|
"info",
|
|
787
953
|
);
|
|
788
954
|
// Reset stuck detection for new milestone
|
|
789
|
-
|
|
790
|
-
|
|
955
|
+
unitDispatchCount.clear();
|
|
956
|
+
unitRecoveryCount.clear();
|
|
791
957
|
}
|
|
792
958
|
if (mid) currentMilestoneId = mid;
|
|
793
959
|
|
|
@@ -811,10 +977,10 @@ async function dispatchNextUnit(
|
|
|
811
977
|
// - complete-milestone runs on a slice branch (last slice bypass)
|
|
812
978
|
{
|
|
813
979
|
const currentBranch = getCurrentBranch(basePath);
|
|
814
|
-
const
|
|
815
|
-
if (
|
|
816
|
-
const branchMid =
|
|
817
|
-
const branchSid =
|
|
980
|
+
const parsedBranch = parseSliceBranch(currentBranch);
|
|
981
|
+
if (parsedBranch) {
|
|
982
|
+
const branchMid = parsedBranch.milestoneId;
|
|
983
|
+
const branchSid = parsedBranch.sliceId;
|
|
818
984
|
// Check if this slice is marked done in the roadmap
|
|
819
985
|
const roadmapFile = resolveMilestoneFile(basePath, branchMid, "ROADMAP");
|
|
820
986
|
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
@@ -828,8 +994,9 @@ async function dispatchNextUnit(
|
|
|
828
994
|
const mergeResult = mergeSliceToMain(
|
|
829
995
|
basePath, branchMid, branchSid, sliceTitleForMerge,
|
|
830
996
|
);
|
|
997
|
+
const targetBranch = getMainBranch(basePath);
|
|
831
998
|
ctx.ui.notify(
|
|
832
|
-
`Merged ${mergeResult.branch} →
|
|
999
|
+
`Merged ${mergeResult.branch} → ${targetBranch}.`,
|
|
833
1000
|
"info",
|
|
834
1001
|
);
|
|
835
1002
|
// Re-derive state from main so downstream logic sees merged state
|
|
@@ -863,6 +1030,12 @@ async function dispatchNextUnit(
|
|
|
863
1030
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
864
1031
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
865
1032
|
}
|
|
1033
|
+
// Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
|
|
1034
|
+
try {
|
|
1035
|
+
const file = completedKeysPath(basePath);
|
|
1036
|
+
if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
|
|
1037
|
+
completedKeySet.clear();
|
|
1038
|
+
} catch { /* non-fatal */ }
|
|
866
1039
|
await stopAuto(ctx, pi);
|
|
867
1040
|
return;
|
|
868
1041
|
}
|
|
@@ -902,144 +1075,157 @@ async function dispatchNextUnit(
|
|
|
902
1075
|
// can perform the UAT manually. On next resume, result file will exist → skip.
|
|
903
1076
|
let pauseAfterUatDispatch = false;
|
|
904
1077
|
|
|
905
|
-
// ──
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
if (needsRunUat) {
|
|
910
|
-
const { sliceId, uatType } = needsRunUat;
|
|
911
|
-
unitType = "run-uat";
|
|
912
|
-
unitId = `${mid}/${sliceId}`;
|
|
913
|
-
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
|
914
|
-
const uatContent = await loadFile(uatFile);
|
|
915
|
-
prompt = await buildRunUatPrompt(
|
|
916
|
-
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
|
917
|
-
);
|
|
918
|
-
// For non-artifact-driven UAT types, pause after the prompt is dispatched.
|
|
919
|
-
// The agent receives the prompt, writes S0x-UAT-RESULT.md surfacing the UAT,
|
|
920
|
-
// then auto-mode pauses for human execution. On resume, result file exists → skip.
|
|
921
|
-
if (uatType !== "artifact-driven") {
|
|
922
|
-
pauseAfterUatDispatch = true;
|
|
923
|
-
}
|
|
924
|
-
} else if (needsReassess) {
|
|
925
|
-
unitType = "reassess-roadmap";
|
|
926
|
-
unitId = `${mid}/${needsReassess.sliceId}`;
|
|
927
|
-
prompt = await buildReassessRoadmapPrompt(mid, midTitle!, needsReassess.sliceId, basePath);
|
|
928
|
-
} else if (state.phase === "pre-planning") {
|
|
929
|
-
// Need roadmap — check if context exists
|
|
930
|
-
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
|
|
931
|
-
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
932
|
-
|
|
933
|
-
if (!hasContext) {
|
|
934
|
-
await stopAuto(ctx, pi);
|
|
935
|
-
ctx.ui.notify("No context or roadmap yet. Run /gsd to discuss first.", "warning");
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Research before roadmap if no research exists
|
|
940
|
-
const researchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
|
|
941
|
-
const hasResearch = !!(researchFile && await loadFile(researchFile));
|
|
942
|
-
|
|
943
|
-
if (!hasResearch) {
|
|
944
|
-
unitType = "research-milestone";
|
|
945
|
-
unitId = mid;
|
|
946
|
-
prompt = await buildResearchMilestonePrompt(mid, midTitle!, basePath);
|
|
947
|
-
} else {
|
|
948
|
-
unitType = "plan-milestone";
|
|
949
|
-
unitId = mid;
|
|
950
|
-
prompt = await buildPlanMilestonePrompt(mid, midTitle!, basePath);
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
} else if (state.phase === "planning") {
|
|
954
|
-
// Slice needs planning — but research first if no research exists
|
|
955
|
-
const sid = state.activeSlice!.id;
|
|
956
|
-
const sTitle = state.activeSlice!.title;
|
|
957
|
-
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
958
|
-
const hasResearch = !!(researchFile && await loadFile(researchFile));
|
|
959
|
-
|
|
960
|
-
if (!hasResearch) {
|
|
961
|
-
unitType = "research-slice";
|
|
962
|
-
unitId = `${mid}/${sid}`;
|
|
963
|
-
prompt = await buildResearchSlicePrompt(mid, midTitle!, sid, sTitle, basePath);
|
|
964
|
-
} else {
|
|
965
|
-
unitType = "plan-slice";
|
|
966
|
-
unitId = `${mid}/${sid}`;
|
|
967
|
-
prompt = await buildPlanSlicePrompt(mid, midTitle!, sid, sTitle, basePath);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
} else if (state.phase === "replanning-slice") {
|
|
971
|
-
// Blocker discovered — replan the slice before continuing
|
|
972
|
-
const sid = state.activeSlice!.id;
|
|
973
|
-
const sTitle = state.activeSlice!.title;
|
|
974
|
-
unitType = "replan-slice";
|
|
975
|
-
unitId = `${mid}/${sid}`;
|
|
976
|
-
prompt = await buildReplanSlicePrompt(mid, midTitle!, sid, sTitle, basePath);
|
|
977
|
-
|
|
978
|
-
} else if (state.phase === "executing" && state.activeTask) {
|
|
979
|
-
// Execute next task
|
|
980
|
-
const sid = state.activeSlice!.id;
|
|
981
|
-
const sTitle = state.activeSlice!.title;
|
|
982
|
-
const tid = state.activeTask.id;
|
|
983
|
-
const tTitle = state.activeTask.title;
|
|
984
|
-
unitType = "execute-task";
|
|
985
|
-
unitId = `${mid}/${sid}/${tid}`;
|
|
986
|
-
prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, basePath);
|
|
987
|
-
|
|
988
|
-
} else if (state.phase === "summarizing") {
|
|
989
|
-
// All tasks done — complete the slice
|
|
1078
|
+
// ── Phase-first dispatch: complete-slice MUST run before reassessment ──
|
|
1079
|
+
// If the current phase is "summarizing", complete-slice is responsible for
|
|
1080
|
+
// mergeSliceToMain. Reassessment must wait until the merge is done.
|
|
1081
|
+
if (state.phase === "summarizing") {
|
|
990
1082
|
const sid = state.activeSlice!.id;
|
|
991
1083
|
const sTitle = state.activeSlice!.title;
|
|
992
1084
|
unitType = "complete-slice";
|
|
993
1085
|
unitId = `${mid}/${sid}`;
|
|
994
1086
|
prompt = await buildCompleteSlicePrompt(mid, midTitle!, sid, sTitle, basePath);
|
|
1087
|
+
} else {
|
|
1088
|
+
// ── Adaptive Replanning: check if last completed slice needs reassessment ──
|
|
1089
|
+
// Computed here (after summarizing guard) so complete-slice always runs first.
|
|
1090
|
+
const needsReassess = await checkNeedsReassessment(basePath, mid, state);
|
|
1091
|
+
if (needsRunUat) {
|
|
1092
|
+
const { sliceId, uatType } = needsRunUat;
|
|
1093
|
+
unitType = "run-uat";
|
|
1094
|
+
unitId = `${mid}/${sliceId}`;
|
|
1095
|
+
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
|
1096
|
+
const uatContent = await loadFile(uatFile);
|
|
1097
|
+
prompt = await buildRunUatPrompt(
|
|
1098
|
+
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
|
1099
|
+
);
|
|
1100
|
+
// For non-artifact-driven UAT types, pause after the prompt is dispatched.
|
|
1101
|
+
// The agent receives the prompt, writes S0x-UAT-RESULT.md surfacing the UAT,
|
|
1102
|
+
// then auto-mode pauses for human execution. On resume, result file exists → skip.
|
|
1103
|
+
if (uatType !== "artifact-driven") {
|
|
1104
|
+
pauseAfterUatDispatch = true;
|
|
1105
|
+
}
|
|
1106
|
+
} else if (needsReassess) {
|
|
1107
|
+
unitType = "reassess-roadmap";
|
|
1108
|
+
unitId = `${mid}/${needsReassess.sliceId}`;
|
|
1109
|
+
prompt = await buildReassessRoadmapPrompt(mid, midTitle!, needsReassess.sliceId, basePath);
|
|
1110
|
+
} else if (state.phase === "pre-planning") {
|
|
1111
|
+
// Need roadmap — check if context exists
|
|
1112
|
+
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
|
|
1113
|
+
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
1114
|
+
|
|
1115
|
+
if (!hasContext) {
|
|
1116
|
+
await stopAuto(ctx, pi);
|
|
1117
|
+
ctx.ui.notify("No context or roadmap yet. Run /gsd to discuss first.", "warning");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
995
1120
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
unitId = mid;
|
|
1000
|
-
prompt = await buildCompleteMilestonePrompt(mid, midTitle!, basePath);
|
|
1121
|
+
// Research before roadmap if no research exists
|
|
1122
|
+
const researchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
|
|
1123
|
+
const hasResearch = !!(researchFile && await loadFile(researchFile));
|
|
1001
1124
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1125
|
+
if (!hasResearch) {
|
|
1126
|
+
unitType = "research-milestone";
|
|
1127
|
+
unitId = mid;
|
|
1128
|
+
prompt = await buildResearchMilestonePrompt(mid, midTitle!, basePath);
|
|
1129
|
+
} else {
|
|
1130
|
+
unitType = "plan-milestone";
|
|
1131
|
+
unitId = mid;
|
|
1132
|
+
prompt = await buildPlanMilestonePrompt(mid, midTitle!, basePath);
|
|
1133
|
+
}
|
|
1012
1134
|
|
|
1013
|
-
|
|
1135
|
+
} else if (state.phase === "planning") {
|
|
1136
|
+
// Slice needs planning — but research first if no research exists
|
|
1137
|
+
const sid = state.activeSlice!.id;
|
|
1138
|
+
const sTitle = state.activeSlice!.title;
|
|
1139
|
+
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
1140
|
+
const hasResearch = !!(researchFile && await loadFile(researchFile));
|
|
1141
|
+
|
|
1142
|
+
if (!hasResearch) {
|
|
1143
|
+
unitType = "research-slice";
|
|
1144
|
+
unitId = `${mid}/${sid}`;
|
|
1145
|
+
prompt = await buildResearchSlicePrompt(mid, midTitle!, sid, sTitle, basePath);
|
|
1146
|
+
} else {
|
|
1147
|
+
unitType = "plan-slice";
|
|
1148
|
+
unitId = `${mid}/${sid}`;
|
|
1149
|
+
prompt = await buildPlanSlicePrompt(mid, midTitle!, sid, sTitle, basePath);
|
|
1150
|
+
}
|
|
1014
1151
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1152
|
+
} else if (state.phase === "replanning-slice") {
|
|
1153
|
+
// Blocker discovered — replan the slice before continuing
|
|
1154
|
+
const sid = state.activeSlice!.id;
|
|
1155
|
+
const sTitle = state.activeSlice!.title;
|
|
1156
|
+
unitType = "replan-slice";
|
|
1157
|
+
unitId = `${mid}/${sid}`;
|
|
1158
|
+
prompt = await buildReplanSlicePrompt(mid, midTitle!, sid, sTitle, basePath);
|
|
1159
|
+
|
|
1160
|
+
} else if (state.phase === "executing" && state.activeTask) {
|
|
1161
|
+
// Execute next task
|
|
1162
|
+
const sid = state.activeSlice!.id;
|
|
1163
|
+
const sTitle = state.activeSlice!.title;
|
|
1164
|
+
const tid = state.activeTask.id;
|
|
1165
|
+
const tTitle = state.activeTask.title;
|
|
1166
|
+
unitType = "execute-task";
|
|
1167
|
+
unitId = `${mid}/${sid}/${tid}`;
|
|
1168
|
+
prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, basePath);
|
|
1169
|
+
|
|
1170
|
+
} else if (state.phase === "completing-milestone") {
|
|
1171
|
+
// All slices done — complete the milestone
|
|
1172
|
+
unitType = "complete-milestone";
|
|
1173
|
+
unitId = mid;
|
|
1174
|
+
prompt = await buildCompleteMilestonePrompt(mid, midTitle!, basePath);
|
|
1020
1175
|
|
|
1021
|
-
|
|
1176
|
+
} else {
|
|
1022
1177
|
if (currentUnit) {
|
|
1023
1178
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1024
1179
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1180
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1025
1181
|
}
|
|
1026
|
-
saveActivityLog(ctx, basePath, lastUnit.type, lastUnit.id);
|
|
1027
|
-
|
|
1028
|
-
// Diagnostic: what file was expected?
|
|
1029
|
-
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
1030
1182
|
await stopAuto(ctx, pi);
|
|
1031
|
-
ctx.ui.notify(
|
|
1032
|
-
`Stuck: ${unitType} ${unitId} fired ${retryCount + 1} times. Expected artifact not found.${expected ? `\n Expected: ${expected}` : ""}\n Check .gsd/ and activity logs.`,
|
|
1033
|
-
"error",
|
|
1034
|
-
);
|
|
1183
|
+
ctx.ui.notify(`Unexpected phase: ${state.phase}. Stopping auto-mode.`, "warning");
|
|
1035
1184
|
return;
|
|
1036
1185
|
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
await emitObservabilityWarnings(ctx, unitType, unitId);
|
|
1189
|
+
|
|
1190
|
+
// Idempotency: skip units already completed in a prior session.
|
|
1191
|
+
const idempotencyKey = `${unitType}/${unitId}`;
|
|
1192
|
+
if (completedKeySet.has(idempotencyKey)) {
|
|
1193
|
+
ctx.ui.notify(
|
|
1194
|
+
`Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
|
|
1195
|
+
"info",
|
|
1196
|
+
);
|
|
1197
|
+
// Yield to the event loop before re-dispatching to avoid tight recursion
|
|
1198
|
+
// when many units are already completed (e.g., after crash recovery).
|
|
1199
|
+
await new Promise(r => setImmediate(r));
|
|
1200
|
+
await dispatchNextUnit(ctx, pi);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Stuck detection — tracks total dispatches per unit (not just consecutive repeats).
|
|
1205
|
+
// Pattern A→B→A→B would reset retryCount every time; this map catches it.
|
|
1206
|
+
const dispatchKey = `${unitType}/${unitId}`;
|
|
1207
|
+
const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
|
|
1208
|
+
if (prevCount >= MAX_UNIT_DISPATCHES) {
|
|
1209
|
+
if (currentUnit) {
|
|
1210
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1211
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1212
|
+
}
|
|
1213
|
+
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1214
|
+
|
|
1215
|
+
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
1216
|
+
await stopAuto(ctx, pi);
|
|
1217
|
+
ctx.ui.notify(
|
|
1218
|
+
`Loop detected: ${unitType} ${unitId} dispatched ${prevCount + 1} times total. Expected artifact not found.${expected ? `\n Expected: ${expected}` : ""}\n Check branch state and .gsd/ artifacts.`,
|
|
1219
|
+
"error",
|
|
1220
|
+
);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
unitDispatchCount.set(dispatchKey, prevCount + 1);
|
|
1224
|
+
if (prevCount > 0) {
|
|
1037
1225
|
ctx.ui.notify(
|
|
1038
|
-
`${unitType} ${unitId} didn't produce expected artifact. Retrying (${
|
|
1226
|
+
`${unitType} ${unitId} didn't produce expected artifact. Retrying (${prevCount + 1}/${MAX_UNIT_DISPATCHES}).`,
|
|
1039
1227
|
"warning",
|
|
1040
1228
|
);
|
|
1041
|
-
} else {
|
|
1042
|
-
retryCount = 0;
|
|
1043
1229
|
}
|
|
1044
1230
|
// Snapshot metrics + activity log for the PREVIOUS unit before we reassign.
|
|
1045
1231
|
// The session still holds the previous unit's data (newSession hasn't fired yet).
|
|
@@ -1048,6 +1234,11 @@ async function dispatchNextUnit(
|
|
|
1048
1234
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1049
1235
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1050
1236
|
|
|
1237
|
+
// Persist completion to disk BEFORE updating memory — so a crash here is recoverable.
|
|
1238
|
+
const closeoutKey = `${currentUnit.type}/${currentUnit.id}`;
|
|
1239
|
+
persistCompletedKey(basePath, closeoutKey);
|
|
1240
|
+
completedKeySet.add(closeoutKey);
|
|
1241
|
+
|
|
1051
1242
|
completedUnits.push({
|
|
1052
1243
|
type: currentUnit.type,
|
|
1053
1244
|
id: currentUnit.id,
|
|
@@ -1055,9 +1246,9 @@ async function dispatchNextUnit(
|
|
|
1055
1246
|
finishedAt: Date.now(),
|
|
1056
1247
|
});
|
|
1057
1248
|
clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
|
|
1249
|
+
unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
1250
|
+
unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
1058
1251
|
}
|
|
1059
|
-
|
|
1060
|
-
lastUnit = { type: unitType, id: unitId };
|
|
1061
1252
|
currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1062
1253
|
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
1063
1254
|
phase: "dispatched",
|
|
@@ -1101,7 +1292,7 @@ async function dispatchNextUnit(
|
|
|
1101
1292
|
if (pendingCrashRecovery) {
|
|
1102
1293
|
finalPrompt = `${pendingCrashRecovery}\n\n---\n\n${finalPrompt}`;
|
|
1103
1294
|
pendingCrashRecovery = null;
|
|
1104
|
-
} else if (
|
|
1295
|
+
} else if ((unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
1105
1296
|
const diagnostic = getDeepDiagnostic(basePath);
|
|
1106
1297
|
if (diagnostic) {
|
|
1107
1298
|
finalPrompt = `**RETRY — your previous attempt did not produce the required artifact.**\n\nDiagnostic from previous attempt:\n${diagnostic}\n\nFix whatever went wrong and make sure you write the required file this time.\n\n---\n\n${finalPrompt}`;
|
|
@@ -2079,6 +2270,20 @@ async function recoverTimedOutUnit(
|
|
|
2079
2270
|
const recoveryAttempts = runtime?.recoveryAttempts ?? 0;
|
|
2080
2271
|
const maxRecoveryAttempts = reason === "idle" ? 2 : 1;
|
|
2081
2272
|
|
|
2273
|
+
const recoveryKey = `${unitType}/${unitId}`;
|
|
2274
|
+
const attemptNumber = (unitRecoveryCount.get(recoveryKey) ?? 0) + 1;
|
|
2275
|
+
unitRecoveryCount.set(recoveryKey, attemptNumber);
|
|
2276
|
+
|
|
2277
|
+
if (attemptNumber > 1) {
|
|
2278
|
+
// Exponential backoff: 2^(n-1) seconds, capped at 30s
|
|
2279
|
+
const backoffMs = Math.min(1000 * Math.pow(2, attemptNumber - 2), 30000);
|
|
2280
|
+
ctx.ui.notify(
|
|
2281
|
+
`Recovery attempt ${attemptNumber} for ${unitType} ${unitId}. Waiting ${backoffMs / 1000}s before retry.`,
|
|
2282
|
+
"info",
|
|
2283
|
+
);
|
|
2284
|
+
await new Promise(r => setTimeout(r, backoffMs));
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2082
2287
|
if (unitType === "execute-task") {
|
|
2083
2288
|
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
2084
2289
|
if (!status) return "paused";
|
|
@@ -2094,9 +2299,10 @@ async function recoverTimedOutUnit(
|
|
|
2094
2299
|
recovery: status,
|
|
2095
2300
|
});
|
|
2096
2301
|
ctx.ui.notify(
|
|
2097
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode
|
|
2302
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode. (attempt ${attemptNumber})`,
|
|
2098
2303
|
"info",
|
|
2099
2304
|
);
|
|
2305
|
+
unitRecoveryCount.delete(recoveryKey);
|
|
2100
2306
|
await dispatchNextUnit(ctx, pi);
|
|
2101
2307
|
return "recovered";
|
|
2102
2308
|
}
|
|
@@ -2143,7 +2349,7 @@ async function recoverTimedOutUnit(
|
|
|
2143
2349
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
2144
2350
|
);
|
|
2145
2351
|
ctx.ui.notify(
|
|
2146
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to finish durable output (attempt ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
2352
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to finish durable output (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
2147
2353
|
"warning",
|
|
2148
2354
|
);
|
|
2149
2355
|
return "recovered";
|
|
@@ -2164,9 +2370,10 @@ async function recoverTimedOutUnit(
|
|
|
2164
2370
|
lastRecoveryReason: reason,
|
|
2165
2371
|
});
|
|
2166
2372
|
ctx.ui.notify(
|
|
2167
|
-
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts (${diagnostic}). Blocker artifacts written. Advancing pipeline
|
|
2373
|
+
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts (${diagnostic}). Blocker artifacts written. Advancing pipeline. (attempt ${attemptNumber})`,
|
|
2168
2374
|
"warning",
|
|
2169
2375
|
);
|
|
2376
|
+
unitRecoveryCount.delete(recoveryKey);
|
|
2170
2377
|
await dispatchNextUnit(ctx, pi);
|
|
2171
2378
|
return "recovered";
|
|
2172
2379
|
}
|
|
@@ -2197,9 +2404,10 @@ async function recoverTimedOutUnit(
|
|
|
2197
2404
|
lastRecoveryReason: reason,
|
|
2198
2405
|
});
|
|
2199
2406
|
ctx.ui.notify(
|
|
2200
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} artifact already exists on disk. Advancing
|
|
2407
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} artifact already exists on disk. Advancing. (attempt ${attemptNumber})`,
|
|
2201
2408
|
"info",
|
|
2202
2409
|
);
|
|
2410
|
+
unitRecoveryCount.delete(recoveryKey);
|
|
2203
2411
|
await dispatchNextUnit(ctx, pi);
|
|
2204
2412
|
return "recovered";
|
|
2205
2413
|
}
|
|
@@ -2245,7 +2453,7 @@ async function recoverTimedOutUnit(
|
|
|
2245
2453
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
2246
2454
|
);
|
|
2247
2455
|
ctx.ui.notify(
|
|
2248
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
2456
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
2249
2457
|
"warning",
|
|
2250
2458
|
);
|
|
2251
2459
|
return "recovered";
|
|
@@ -2265,9 +2473,10 @@ async function recoverTimedOutUnit(
|
|
|
2265
2473
|
lastRecoveryReason: reason,
|
|
2266
2474
|
});
|
|
2267
2475
|
ctx.ui.notify(
|
|
2268
|
-
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts. Blocker placeholder written to ${placeholder}. Advancing pipeline
|
|
2476
|
+
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts. Blocker placeholder written to ${placeholder}. Advancing pipeline. (attempt ${attemptNumber})`,
|
|
2269
2477
|
"warning",
|
|
2270
2478
|
);
|
|
2479
|
+
unitRecoveryCount.delete(recoveryKey);
|
|
2271
2480
|
await dispatchNextUnit(ctx, pi);
|
|
2272
2481
|
return "recovered";
|
|
2273
2482
|
}
|