gsd-pi 2.80.0-dev.2be7caf18 → 2.80.0-dev.42bd34838
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 +1 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +29 -15
- package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
- package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
- package/dist/resources/extensions/gsd/auto.js +61 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
- package/dist/resources/extensions/gsd/context-budget.js +37 -2
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
- package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
- package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
- package/dist/resources/extensions/gsd/git-service.js +36 -4
- package/dist/resources/extensions/gsd/gsd-db.js +46 -13
- package/dist/resources/extensions/gsd/guided-flow.js +19 -4
- package/dist/resources/extensions/gsd/memory-store.js +69 -12
- package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
- package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +3 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +9 -2
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +5 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +5 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +35 -20
- package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
- package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
- package/src/resources/extensions/gsd/auto.ts +69 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
- package/src/resources/extensions/gsd/context-budget.ts +44 -2
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
- package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
- package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
- package/src/resources/extensions/gsd/git-service.ts +46 -8
- package/src/resources/extensions/gsd/gsd-db.ts +50 -13
- package/src/resources/extensions/gsd/guided-flow.ts +32 -4
- package/src/resources/extensions/gsd/memory-store.ts +77 -12
- package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
- package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
- /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → tht0ltP8EIjsohPOA_WZf}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → tht0ltP8EIjsohPOA_WZf}/_ssgManifest.js +0 -0
|
@@ -50,12 +50,12 @@ import {
|
|
|
50
50
|
} from "../workflow-logger.js";
|
|
51
51
|
import { gsdRoot } from "../paths.js";
|
|
52
52
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
53
|
-
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
|
|
53
|
+
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
|
|
54
54
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
55
55
|
import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
|
|
56
56
|
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
|
|
57
57
|
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
|
|
58
|
-
import { isDbAvailable, getMilestoneSlices
|
|
58
|
+
import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
|
|
59
59
|
import type { MinimalModelRegistry } from "../context-budget.js";
|
|
60
60
|
import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
|
|
61
61
|
import { resolveUokFlags } from "../uok/flags.js";
|
|
@@ -76,12 +76,6 @@ function isSamePathLocal(a: string, b: string): boolean {
|
|
|
76
76
|
return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function refreshPlanSliceRecoveryDbIfNeeded(unitType: string): boolean {
|
|
80
|
-
if (unitType !== "plan-slice") return true;
|
|
81
|
-
if (!isDbAvailable()) return true;
|
|
82
|
-
return refreshOpenDatabaseFromDisk();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
79
|
// ─── Session timeout auto-resume state ────────────────────────────────────────
|
|
86
80
|
|
|
87
81
|
let consecutiveSessionTimeouts = 0;
|
|
@@ -1100,17 +1094,24 @@ export async function runDispatch(
|
|
|
1100
1094
|
level: 1,
|
|
1101
1095
|
action: "artifact-found",
|
|
1102
1096
|
});
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
"info",
|
|
1106
|
-
);
|
|
1107
|
-
if (!refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
|
|
1097
|
+
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
1098
|
+
if (!recoveryDb.ok) {
|
|
1108
1099
|
ctx.ui.notify(
|
|
1109
|
-
|
|
1100
|
+
recoveryDb.fatal
|
|
1101
|
+
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
1102
|
+
: `${recoveryDb.message} Keeping stuck state for retry.`,
|
|
1110
1103
|
"warning",
|
|
1111
1104
|
);
|
|
1105
|
+
if (recoveryDb.fatal) {
|
|
1106
|
+
await deps.pauseAuto(ctx, pi);
|
|
1107
|
+
return { action: "break", reason: recoveryDb.reason };
|
|
1108
|
+
}
|
|
1112
1109
|
return { action: "continue" };
|
|
1113
1110
|
}
|
|
1111
|
+
ctx.ui.notify(
|
|
1112
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1113
|
+
"info",
|
|
1114
|
+
);
|
|
1114
1115
|
deps.invalidateAllCaches();
|
|
1115
1116
|
loopState.recentUnits.length = 0;
|
|
1116
1117
|
loopState.stuckRecoveryAttempts = 0;
|
|
@@ -1135,19 +1136,26 @@ export async function runDispatch(
|
|
|
1135
1136
|
level: 2,
|
|
1136
1137
|
action: "artifact-found",
|
|
1137
1138
|
});
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1139
|
+
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
1140
|
+
if (recoveryDb.ok) {
|
|
1141
|
+
ctx.ui.notify(
|
|
1142
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`,
|
|
1143
|
+
"info",
|
|
1144
|
+
);
|
|
1143
1145
|
loopState.recentUnits.length = 0;
|
|
1144
1146
|
loopState.stuckRecoveryAttempts = 0;
|
|
1145
1147
|
return { action: "continue" };
|
|
1146
1148
|
}
|
|
1147
1149
|
ctx.ui.notify(
|
|
1148
|
-
|
|
1150
|
+
recoveryDb.fatal
|
|
1151
|
+
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
1152
|
+
: `${recoveryDb.message} Stopping for manual recovery.`,
|
|
1149
1153
|
"warning",
|
|
1150
1154
|
);
|
|
1155
|
+
if (recoveryDb.fatal) {
|
|
1156
|
+
await deps.pauseAuto(ctx, pi);
|
|
1157
|
+
return { action: "break", reason: recoveryDb.reason };
|
|
1158
|
+
}
|
|
1151
1159
|
}
|
|
1152
1160
|
debugLog("autoLoop", {
|
|
1153
1161
|
phase: "stuck-detected",
|
|
@@ -2265,6 +2273,13 @@ export async function runFinalize(
|
|
|
2265
2273
|
|
|
2266
2274
|
// Both pre and post verification completed without timeout — reset counter
|
|
2267
2275
|
loopState.consecutiveFinalizeTimeouts = 0;
|
|
2276
|
+
if (preUnitSnapshot) {
|
|
2277
|
+
writeUnitRuntimeRecord(s.basePath, preUnitSnapshot.type, preUnitSnapshot.id, preUnitSnapshot.startedAt, {
|
|
2278
|
+
phase: "finalized",
|
|
2279
|
+
lastProgressAt: Date.now(),
|
|
2280
|
+
lastProgressKind: "finalize-success",
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2268
2283
|
s.currentUnit = null;
|
|
2269
2284
|
clearCurrentPhase();
|
|
2270
2285
|
|
|
@@ -22,6 +22,7 @@ import { bumpTurnGeneration } from "./turn-epoch.js";
|
|
|
22
22
|
|
|
23
23
|
let _currentResolve: ((result: UnitResult) => void) | null = null;
|
|
24
24
|
let _sessionSwitchInFlight = false;
|
|
25
|
+
let _pendingSwitchCancellation: { errorContext?: ErrorContext } | null = null;
|
|
25
26
|
|
|
26
27
|
// ─── Setters (needed for cross-module mutation) ─────────────────────────────
|
|
27
28
|
|
|
@@ -37,6 +38,12 @@ export function _clearCurrentResolve(): void {
|
|
|
37
38
|
_currentResolve = null;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export function _consumePendingSwitchCancellation(): { errorContext?: ErrorContext } | null {
|
|
42
|
+
const pending = _pendingSwitchCancellation;
|
|
43
|
+
_pendingSwitchCancellation = null;
|
|
44
|
+
return pending;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
// ─── resolveAgentEnd ─────────────────────────────────────────────────────────
|
|
41
48
|
|
|
42
49
|
/**
|
|
@@ -96,7 +103,7 @@ export function bumpAndResolveSynthetic(reason: string): void {
|
|
|
96
103
|
* blocks to ensure the autoLoop is never stuck awaiting a promise that
|
|
97
104
|
* will never resolve. Safe to call when no resolver is pending (no-op).
|
|
98
105
|
*/
|
|
99
|
-
export function resolveAgentEndCancelled(errorContext?: ErrorContext):
|
|
106
|
+
export function resolveAgentEndCancelled(errorContext?: ErrorContext): boolean {
|
|
100
107
|
if (_currentResolve) {
|
|
101
108
|
// Cancellation supersedes the in-flight turn the same way timeout
|
|
102
109
|
// recovery does — bump the turn epoch so any lingering writes from the
|
|
@@ -107,8 +114,22 @@ export function resolveAgentEndCancelled(errorContext?: ErrorContext): void {
|
|
|
107
114
|
debugLog("resolveAgentEndCancelled", { status: "resolving-cancelled" });
|
|
108
115
|
const r = _currentResolve;
|
|
109
116
|
_currentResolve = null;
|
|
117
|
+
_pendingSwitchCancellation = null;
|
|
110
118
|
r({ status: "cancelled", ...(errorContext ? { errorContext } : {}) });
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (_sessionSwitchInFlight) {
|
|
123
|
+
bumpTurnGeneration(
|
|
124
|
+
`cancelled-during-switch:${errorContext?.category ?? "unknown"}`,
|
|
125
|
+
);
|
|
126
|
+
_pendingSwitchCancellation = errorContext ? { errorContext } : {};
|
|
127
|
+
debugLog("resolveAgentEndCancelled", { status: "queued-during-switch" });
|
|
128
|
+
return false;
|
|
111
129
|
}
|
|
130
|
+
|
|
131
|
+
debugLog("resolveAgentEndCancelled", { status: "no-pending-resolve" });
|
|
132
|
+
return false;
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
// ─── resetPendingResolve (test helper) ───────────────────────────────────────
|
|
@@ -120,6 +141,7 @@ export function resolveAgentEndCancelled(errorContext?: ErrorContext): void {
|
|
|
120
141
|
export function _resetPendingResolve(): void {
|
|
121
142
|
_currentResolve = null;
|
|
122
143
|
_sessionSwitchInFlight = false;
|
|
144
|
+
_pendingSwitchCancellation = null;
|
|
123
145
|
}
|
|
124
146
|
|
|
125
147
|
export function _hasPendingResolveForTest(): boolean {
|
|
@@ -11,7 +11,12 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
|
|
|
11
11
|
import type { AutoSession } from "./session.js";
|
|
12
12
|
import { NEW_SESSION_TIMEOUT_MS } from "./session.js";
|
|
13
13
|
import type { UnitResult } from "./types.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
_clearCurrentResolve,
|
|
16
|
+
_consumePendingSwitchCancellation,
|
|
17
|
+
_setCurrentResolve,
|
|
18
|
+
_setSessionSwitchInFlight,
|
|
19
|
+
} from "./resolve.js";
|
|
15
20
|
import {
|
|
16
21
|
getCurrentTurnGeneration,
|
|
17
22
|
runWithTurnGeneration,
|
|
@@ -97,6 +102,7 @@ export async function runUnit(
|
|
|
97
102
|
sessionResult = await Promise.race([sessionPromise, timeoutPromise]);
|
|
98
103
|
} catch (sessionErr) {
|
|
99
104
|
if (sessionTimeoutHandle) clearTimeout(sessionTimeoutHandle);
|
|
105
|
+
_consumePendingSwitchCancellation();
|
|
100
106
|
const msg =
|
|
101
107
|
sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
|
|
102
108
|
debugLog("runUnit", {
|
|
@@ -110,17 +116,20 @@ export async function runUnit(
|
|
|
110
116
|
if (sessionTimeoutHandle) clearTimeout(sessionTimeoutHandle);
|
|
111
117
|
|
|
112
118
|
if (sessionResult.cancelled) {
|
|
119
|
+
_consumePendingSwitchCancellation();
|
|
113
120
|
debugLog("runUnit-session-timeout", { unitType, unitId });
|
|
114
121
|
return { status: "cancelled", errorContext: { message: "Session creation timed out", category: "timeout", isTransient: true } };
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
if (!s.active) {
|
|
125
|
+
_consumePendingSwitchCancellation();
|
|
118
126
|
return { status: "cancelled" };
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
if (s.currentUnitModel && typeof pi.setModel === "function") {
|
|
122
130
|
const restored = await pi.setModel(s.currentUnitModel, { persist: false });
|
|
123
131
|
if (!restored) {
|
|
132
|
+
_consumePendingSwitchCancellation();
|
|
124
133
|
const message =
|
|
125
134
|
`Failed to restore configured model ${s.currentUnitModel.provider}/${s.currentUnitModel.id} after session creation`;
|
|
126
135
|
ctx.ui.notify(
|
|
@@ -145,6 +154,14 @@ export async function runUnit(
|
|
|
145
154
|
const unitPromise = new Promise<UnitResult>((resolve) => {
|
|
146
155
|
_setCurrentResolve(resolve);
|
|
147
156
|
});
|
|
157
|
+
const pendingSwitchCancellation = _consumePendingSwitchCancellation();
|
|
158
|
+
if (pendingSwitchCancellation) {
|
|
159
|
+
_clearCurrentResolve();
|
|
160
|
+
return {
|
|
161
|
+
status: "cancelled",
|
|
162
|
+
...(pendingSwitchCancellation.errorContext ? { errorContext: pendingSwitchCancellation.errorContext } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
148
165
|
|
|
149
166
|
// ── Provider request-readiness pre-check (#4555) ──
|
|
150
167
|
// Verify the provider can accept requests before dispatching. If the token
|
|
@@ -67,6 +67,60 @@ export {
|
|
|
67
67
|
|
|
68
68
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
69
69
|
|
|
70
|
+
export type ArtifactRecoveryDbRefreshResult =
|
|
71
|
+
| { ok: true }
|
|
72
|
+
| { ok: false; fatal: boolean; message: string; reason: string };
|
|
73
|
+
|
|
74
|
+
export function refreshRecoveryDbForArtifact(
|
|
75
|
+
unitType: string,
|
|
76
|
+
unitId: string,
|
|
77
|
+
): ArtifactRecoveryDbRefreshResult {
|
|
78
|
+
if (unitType !== "plan-slice" && unitType !== "execute-task") return { ok: true };
|
|
79
|
+
if (!isDbAvailable()) return { ok: true };
|
|
80
|
+
|
|
81
|
+
if (!refreshOpenDatabaseFromDisk()) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
fatal: unitType === "execute-task",
|
|
85
|
+
reason: `${unitType}-db-refresh-failed`,
|
|
86
|
+
message: `Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (unitType !== "execute-task") return { ok: true };
|
|
91
|
+
|
|
92
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
93
|
+
if (!mid || !sid || !tid) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
fatal: true,
|
|
97
|
+
reason: "execute-task-invalid-unit-id",
|
|
98
|
+
message: `Stuck recovery found execute-task ${unitId} artifacts, but the unit id could not be parsed for DB verification.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const task = getTask(mid, sid, tid);
|
|
103
|
+
if (!task) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
fatal: true,
|
|
107
|
+
reason: "execute-task-artifact-db-missing",
|
|
108
|
+
message: `Stuck recovery found execute-task ${unitId} artifacts, but no matching DB task row exists after refresh.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!isClosedStatus(task.status)) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
fatal: true,
|
|
116
|
+
reason: "execute-task-artifact-db-mismatch",
|
|
117
|
+
message: `Stuck recovery found execute-task ${unitId} artifacts, but the DB task status is still '${task.status}' after refresh.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
70
124
|
function hasCapturedWorkflowPrefs(base: string): boolean {
|
|
71
125
|
const prefsPath = resolveExpectedArtifactPath("workflow-preferences", "WORKFLOW-PREFS", base);
|
|
72
126
|
if (!prefsPath || !existsSync(prefsPath)) return false;
|
|
@@ -32,6 +32,7 @@ let _currentSigtermHandler: (() => void) | null = null;
|
|
|
32
32
|
export function registerSigtermHandler(
|
|
33
33
|
currentBasePath: string,
|
|
34
34
|
previousHandler: (() => void) | null,
|
|
35
|
+
onSignalCleanup?: () => void,
|
|
35
36
|
): () => void {
|
|
36
37
|
// Remove the explicitly-passed previous handler
|
|
37
38
|
if (previousHandler) {
|
|
@@ -43,6 +44,12 @@ export function registerSigtermHandler(
|
|
|
43
44
|
for (const sig of CLEANUP_SIGNALS) process.off(sig, _currentSigtermHandler);
|
|
44
45
|
}
|
|
45
46
|
const handler = () => {
|
|
47
|
+
try {
|
|
48
|
+
onSignalCleanup?.();
|
|
49
|
+
} catch {
|
|
50
|
+
void 0;
|
|
51
|
+
// Signal cleanup is best-effort; lock cleanup and process exit still run.
|
|
52
|
+
}
|
|
46
53
|
clearLock(currentBasePath);
|
|
47
54
|
releaseSessionLock(currentBasePath);
|
|
48
55
|
process.exit(0);
|
|
@@ -71,14 +71,14 @@ export async function recoverTimedOutUnit(
|
|
|
71
71
|
recovery: status,
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
const durableComplete = status.summaryExists && status.taskChecked && status.nextActionAdvanced;
|
|
74
|
+
const durableComplete = status.dbComplete || (status.summaryExists && status.taskChecked && status.nextActionAdvanced);
|
|
75
75
|
if (durableComplete) {
|
|
76
76
|
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
|
|
77
77
|
phase: "finalized",
|
|
78
78
|
recovery: status,
|
|
79
79
|
});
|
|
80
80
|
ctx.ui.notify(
|
|
81
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed
|
|
81
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed. Continuing auto-mode. (attempt ${attemptNumber})`,
|
|
82
82
|
"info",
|
|
83
83
|
);
|
|
84
84
|
unitRecoveryCount.delete(recoveryKey);
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
isLockProcessAlive,
|
|
60
60
|
formatCrashInfo,
|
|
61
61
|
emitCrashRecoveredUnitEnd,
|
|
62
|
+
emitOpenUnitEndForUnit,
|
|
62
63
|
} from "./crash-recovery.js";
|
|
63
64
|
import {
|
|
64
65
|
acquireSessionLock,
|
|
@@ -200,6 +201,8 @@ import {
|
|
|
200
201
|
detectWorkingTreeActivity,
|
|
201
202
|
} from "./auto-supervisor.js";
|
|
202
203
|
import { isDbAvailable, getMilestone } from "./gsd-db.js";
|
|
204
|
+
import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
|
|
205
|
+
import { writeUnitRuntimeRecord } from "./unit-runtime.js";
|
|
203
206
|
import { countPendingCaptures } from "./captures.js";
|
|
204
207
|
import { CMUX_CHANNELS, type CmuxLogLevel } from "../shared/cmux-events.js";
|
|
205
208
|
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
|
|
@@ -242,6 +245,20 @@ import {
|
|
|
242
245
|
type WorktreeResolverDeps,
|
|
243
246
|
} from "./worktree-resolver.js";
|
|
244
247
|
import { reorderForCaching } from "./prompt-ordering.js";
|
|
248
|
+
import { initTokenCounter } from "./token-counter.js";
|
|
249
|
+
|
|
250
|
+
// Warm the tiktoken encoder at extension startup so context-budget computations
|
|
251
|
+
// can use accurate token counts via countTokensSync without paying the load
|
|
252
|
+
// cost mid-prompt-build. Fire-and-forget — failure falls back to the
|
|
253
|
+
// provider-aware char-ratio estimator already used by getCharsPerToken().
|
|
254
|
+
// Catch rejections explicitly: an unhandled rejection at module-import time
|
|
255
|
+
// can destabilize startup before the engine logger is configured.
|
|
256
|
+
void initTokenCounter().catch((err) => {
|
|
257
|
+
logWarning(
|
|
258
|
+
"engine",
|
|
259
|
+
`token counter warm-up failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
260
|
+
);
|
|
261
|
+
});
|
|
245
262
|
|
|
246
263
|
// ─── Session State ─────────────────────────────────────────────────────────
|
|
247
264
|
|
|
@@ -505,9 +522,54 @@ export {
|
|
|
505
522
|
getBudgetEnforcementAction,
|
|
506
523
|
} from "./auto-budget.js";
|
|
507
524
|
|
|
525
|
+
function closeOutSignalInterruptedUnit(currentBasePath: string): void {
|
|
526
|
+
const currentUnit = s.currentUnit;
|
|
527
|
+
if (!currentUnit) return;
|
|
528
|
+
|
|
529
|
+
const reason = "Auto-mode process received a termination signal";
|
|
530
|
+
const errorContext: ErrorContext = {
|
|
531
|
+
message: reason,
|
|
532
|
+
category: "aborted",
|
|
533
|
+
isTransient: false,
|
|
534
|
+
};
|
|
535
|
+
const basePath = s.basePath || currentBasePath;
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
emitOpenUnitEndForUnit(basePath, currentUnit.type, currentUnit.id, "cancelled", errorContext);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
logWarning("engine", `signal unit-end cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
|
|
545
|
+
phase: "crashed",
|
|
546
|
+
lastProgressAt: Date.now(),
|
|
547
|
+
lastProgressKind: "signal",
|
|
548
|
+
});
|
|
549
|
+
} catch (err) {
|
|
550
|
+
logWarning("engine", `signal runtime cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
if (s.workerId) markLatestActiveForWorkerCanceled(s.workerId, "signal-exit");
|
|
555
|
+
} catch (err) {
|
|
556
|
+
logWarning("engine", `signal dispatch cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
resolveAgentEndCancelled(errorContext);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
logWarning("engine", `signal resolve cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
508
566
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
509
567
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
510
|
-
s.sigtermHandler = _registerSigtermHandler(
|
|
568
|
+
s.sigtermHandler = _registerSigtermHandler(
|
|
569
|
+
currentBasePath,
|
|
570
|
+
s.sigtermHandler,
|
|
571
|
+
() => closeOutSignalInterruptedUnit(currentBasePath),
|
|
572
|
+
);
|
|
511
573
|
}
|
|
512
574
|
|
|
513
575
|
/** Wrapper: deregister SIGTERM handler and clear reference. */
|
|
@@ -972,6 +1034,8 @@ export async function stopAuto(
|
|
|
972
1034
|
if (s.workerId) {
|
|
973
1035
|
markWorkerStopping(s.workerId);
|
|
974
1036
|
}
|
|
1037
|
+
s.workerId = null;
|
|
1038
|
+
s.milestoneLeaseToken = null;
|
|
975
1039
|
} catch (e) {
|
|
976
1040
|
debugLog("stop-cleanup-coordination", { error: e instanceof Error ? e.message : String(e) });
|
|
977
1041
|
}
|
|
@@ -1975,6 +2039,10 @@ export async function startAuto(
|
|
|
1975
2039
|
: new URL("../../../resource-loader.js", import.meta.url).href;
|
|
1976
2040
|
const { initResources } = await import(resourceLoaderPath);
|
|
1977
2041
|
initResources(agentDir);
|
|
2042
|
+
// initResources() uses synchronous fs APIs, so the prompt-template cache
|
|
2043
|
+
// can be primed immediately — no need for the legacy 1s setTimeout deferral.
|
|
2044
|
+
const { primeCache } = await import("./prompt-loader.js");
|
|
2045
|
+
primeCache();
|
|
1978
2046
|
// Open the project DB before rebuild/derive so resume uses DB-backed
|
|
1979
2047
|
// state instead of falling back to stale markdown parsing (#2940).
|
|
1980
2048
|
await openProjectDbIfPresent(s.basePath);
|
|
@@ -153,9 +153,29 @@ export async function handleAgentEnd(
|
|
|
153
153
|
if (maybeHandleEmptyIntentTurn(event, isAutoActive())) return;
|
|
154
154
|
|
|
155
155
|
if (!isAutoActive()) return;
|
|
156
|
-
if (isSessionSwitchInFlight()) return;
|
|
157
156
|
|
|
158
157
|
const lastMsg = event.messages[event.messages.length - 1];
|
|
158
|
+
if (isSessionSwitchInFlight()) {
|
|
159
|
+
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
160
|
+
const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
|
161
|
+
if (isUserInitiatedAbortMessage(rawErrorMsg)) {
|
|
162
|
+
resolveAgentEndCancelled({
|
|
163
|
+
message: rawErrorMsg,
|
|
164
|
+
category: "aborted",
|
|
165
|
+
isTransient: false,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} else if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
169
|
+
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
170
|
+
const hasEmptyContent = Array.isArray(content) && content.length === 0;
|
|
171
|
+
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
172
|
+
if (!hasEmptyContent || hasErrorMessage) {
|
|
173
|
+
resolveAgentEndCancelled(_buildAbortedPauseContext(lastMsg as { errorMessage?: unknown }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
160
180
|
// Empty content with aborted stopReason is a non-fatal agent stop (the LLM
|
|
161
181
|
// chose to end without producing output). Only pause on genuine fatal aborts
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
* @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type TokenProvider,
|
|
13
|
+
getCharsPerToken,
|
|
14
|
+
isAccurateCountingAvailable,
|
|
15
|
+
countTokensSync,
|
|
16
|
+
} from "./token-counter.js";
|
|
12
17
|
|
|
13
18
|
// ─── Budget ratio constants ──────────────────────────────────────────────────
|
|
14
19
|
// Percentages of total context window allocated to each budget category.
|
|
@@ -32,6 +37,24 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
|
32
37
|
/** Conservative effective context for Claude Code subscription routing (#4676) */
|
|
33
38
|
const CLAUDE_CODE_EFFECTIVE_CONTEXT_WINDOW = 200_000;
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Cached empirical chars-per-token from a tiktoken probe, keyed by provider.
|
|
42
|
+
* countTokensSync's fallback path is provider-aware, so we cache per-provider
|
|
43
|
+
* to preserve that distinction once the encoder warms. The cl100k_base encoder
|
|
44
|
+
* itself gives a stable ratio for ASCII English so a single probe per provider
|
|
45
|
+
* key is sufficient. Empty map means "not yet probed" or "encoder unavailable".
|
|
46
|
+
*/
|
|
47
|
+
const _empiricalCharsPerTokenByProvider = new Map<string, number>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Test hook — clears the empirical chars-per-token cache so test cases that
|
|
51
|
+
* assert against the static char-ratio fallback aren't polluted by a prior
|
|
52
|
+
* tiktoken-warmed run in the same process. Production code must not call this.
|
|
53
|
+
*/
|
|
54
|
+
export function _resetEmpiricalCacheForTest(): void {
|
|
55
|
+
_empiricalCharsPerTokenByProvider.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
/** Percentage of context consumed before suggesting a continue-here checkpoint */
|
|
36
59
|
const CONTINUE_THRESHOLD_PERCENT = 70;
|
|
37
60
|
|
|
@@ -101,7 +124,26 @@ export interface MinimalPreferences {
|
|
|
101
124
|
export function computeBudgets(contextWindow: number, provider?: TokenProvider): BudgetAllocation {
|
|
102
125
|
const effectiveWindow = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
|
|
103
126
|
const charsPerToken = provider ? getCharsPerToken(provider) : CHARS_PER_TOKEN;
|
|
104
|
-
|
|
127
|
+
|
|
128
|
+
// Prefer the tiktoken encoder for total-char estimation when it has been
|
|
129
|
+
// warmed (initTokenCounter resolved). The cl100k_base ratio is stable for
|
|
130
|
+
// ASCII English, so probe once per provider and cache — computeBudgets is
|
|
131
|
+
// called multiple times per prompt build and the probe encode is otherwise
|
|
132
|
+
// wasted work.
|
|
133
|
+
let totalChars: number;
|
|
134
|
+
if (isAccurateCountingAvailable()) {
|
|
135
|
+
const providerKey = provider ?? "__default__";
|
|
136
|
+
let empirical = _empiricalCharsPerTokenByProvider.get(providerKey);
|
|
137
|
+
if (empirical === undefined) {
|
|
138
|
+
const probe = "the quick brown fox jumps over the lazy dog ".repeat(64);
|
|
139
|
+
const probeTokens = countTokensSync(probe, provider);
|
|
140
|
+
empirical = probeTokens > 0 ? probe.length / probeTokens : charsPerToken;
|
|
141
|
+
_empiricalCharsPerTokenByProvider.set(providerKey, empirical);
|
|
142
|
+
}
|
|
143
|
+
totalChars = effectiveWindow * empirical;
|
|
144
|
+
} else {
|
|
145
|
+
totalChars = effectiveWindow * charsPerToken;
|
|
146
|
+
}
|
|
105
147
|
|
|
106
148
|
return {
|
|
107
149
|
summaryBudgetChars: Math.floor(totalChars * SUMMARY_RATIO),
|
|
@@ -359,6 +359,47 @@ export function markCanceled(dispatchId: number, reason: string): void {
|
|
|
359
359
|
).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
|
|
364
|
+
* a worker when the process is exiting before the normal loop can settle it.
|
|
365
|
+
*/
|
|
366
|
+
export function markLatestActiveForWorkerCanceled(workerId: string, reason: string): boolean {
|
|
367
|
+
if (!isDbAvailable()) return false;
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
const db = _getAdapter()!;
|
|
370
|
+
const result = transaction(() => {
|
|
371
|
+
return db.prepare(
|
|
372
|
+
`UPDATE unit_dispatches
|
|
373
|
+
SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
|
|
374
|
+
WHERE id = (
|
|
375
|
+
SELECT id FROM unit_dispatches
|
|
376
|
+
WHERE worker_id = :worker_id
|
|
377
|
+
AND status IN ('pending','claimed','running')
|
|
378
|
+
ORDER BY id DESC
|
|
379
|
+
LIMIT 1
|
|
380
|
+
)`,
|
|
381
|
+
).run({
|
|
382
|
+
":ended_at": now,
|
|
383
|
+
":reason": reason,
|
|
384
|
+
":worker_id": workerId,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
const changes =
|
|
388
|
+
typeof (result as { changes?: unknown }).changes === "number"
|
|
389
|
+
? (result as { changes: number }).changes
|
|
390
|
+
: 0;
|
|
391
|
+
if (changes <= 0) return false;
|
|
392
|
+
insertAuditEvent({
|
|
393
|
+
eventId: randomUUID(),
|
|
394
|
+
traceId: workerId,
|
|
395
|
+
category: "orchestration",
|
|
396
|
+
type: "dispatch-canceled",
|
|
397
|
+
ts: now,
|
|
398
|
+
payload: { workerId, reason },
|
|
399
|
+
});
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
362
403
|
/**
|
|
363
404
|
* Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
|
|
364
405
|
* callers to compute attempt_n and by detect-stuck.ts (B3) to consult
|
|
@@ -57,7 +57,8 @@ export function createBaseSchemaObjects(db: DbAdapter, hooks: BaseSchemaHooks):
|
|
|
57
57
|
slice_id TEXT DEFAULT NULL,
|
|
58
58
|
task_id TEXT DEFAULT NULL,
|
|
59
59
|
full_content TEXT NOT NULL DEFAULT '',
|
|
60
|
-
imported_at TEXT NOT NULL DEFAULT ''
|
|
60
|
+
imported_at TEXT NOT NULL DEFAULT '',
|
|
61
|
+
content_hash TEXT DEFAULT NULL
|
|
61
62
|
)
|
|
62
63
|
`);
|
|
63
64
|
|
|
@@ -76,7 +77,8 @@ export function createBaseSchemaObjects(db: DbAdapter, hooks: BaseSchemaHooks):
|
|
|
76
77
|
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
77
78
|
scope TEXT NOT NULL DEFAULT 'project',
|
|
78
79
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
79
|
-
structured_fields TEXT DEFAULT NULL
|
|
80
|
+
structured_fields TEXT DEFAULT NULL,
|
|
81
|
+
last_hit_at TEXT DEFAULT NULL
|
|
80
82
|
)
|
|
81
83
|
`);
|
|
82
84
|
|
|
@@ -416,6 +416,14 @@ export function applyMigrationV26MilestoneCommitAttributions(db: DbAdapter): voi
|
|
|
416
416
|
db.exec("CREATE INDEX IF NOT EXISTS idx_milestone_commit_attr_milestone ON milestone_commit_attributions(milestone_id)");
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
export function applyMigrationV27ArtifactHash(db: DbAdapter): void {
|
|
420
|
+
ensureColumn(db, "artifacts", "content_hash", "ALTER TABLE artifacts ADD COLUMN content_hash TEXT DEFAULT NULL");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function applyMigrationV28MemoryLastHitAt(db: DbAdapter): void {
|
|
424
|
+
ensureColumn(db, "memories", "last_hit_at", "ALTER TABLE memories ADD COLUMN last_hit_at TEXT DEFAULT NULL");
|
|
425
|
+
}
|
|
426
|
+
|
|
419
427
|
export interface MigrationV22Hooks {
|
|
420
428
|
copyQualityGateRowsToRepairedTable(db: DbAdapter): void;
|
|
421
429
|
}
|