vibe-coding-master 0.4.19 → 0.4.21
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/dist/backend/adapters/git-adapter.js +15 -0
- package/dist/backend/api/harness-routes.js +17 -0
- package/dist/backend/api/task-routes.js +2 -2
- package/dist/backend/app-version.js +12 -0
- package/dist/backend/cli/install-vcm-harness.js +3 -2
- package/dist/backend/gateway/gateway-service.js +2 -5
- package/dist/backend/server.js +6 -2
- package/dist/backend/services/app-settings-service.js +2 -0
- package/dist/backend/services/claude-hook-service.js +188 -36
- package/dist/backend/services/diagnostics-service.js +2 -13
- package/dist/backend/services/harness-feedback-service.js +149 -2
- package/dist/backend/services/harness-service.js +159 -22
- package/dist/backend/services/round-service.js +58 -0
- package/dist/backend/services/session-service.js +6 -301
- package/dist/backend/templates/harness/architect-agent.js +37 -3
- package/dist/backend/templates/harness/coder-agent.js +6 -1
- package/dist/backend/templates/harness/harness-engineer-agent.js +31 -0
- package/dist/backend/templates/harness/project-manager-agent.js +7 -0
- package/dist/backend/templates/harness/reviewer-agent.js +10 -0
- package/dist/main.js +2 -11
- package/dist-frontend/assets/index-B3sODrcw.css +32 -0
- package/dist-frontend/assets/index-_aOTGZCq.js +96 -0
- package/dist-frontend/index.html +2 -2
- package/docs/full-harness-baseline.md +1 -2
- package/docs/gate-review-gates.md +8 -17
- package/docs/product-design.md +5 -6
- package/docs/v0.4-harness-optimization-plan.md +3 -2
- package/package.json +1 -1
- package/dist-frontend/assets/index-BrY-xd6U.js +0 -95
- package/dist-frontend/assets/index-D1LTJ-sY.css +0 -32
|
@@ -201,6 +201,21 @@ export function createGitAdapter(runner) {
|
|
|
201
201
|
hint: result.stderr
|
|
202
202
|
});
|
|
203
203
|
},
|
|
204
|
+
async mergeBranchFastForward(repoRoot, branch) {
|
|
205
|
+
const result = await runGit(runner, repoRoot, ["merge", "--ff-only", branch]);
|
|
206
|
+
if (result.exitCode !== 0) {
|
|
207
|
+
throw new VcmError({
|
|
208
|
+
code: "GIT_MERGE_FAILED",
|
|
209
|
+
message: `Unable to fast-forward merge branch: ${branch}`,
|
|
210
|
+
statusCode: 409,
|
|
211
|
+
hint: result.stderr || result.stdout || "Rebase the task branch onto the connected repository branch, then try again."
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
stdout: result.stdout,
|
|
216
|
+
stderr: result.stderr
|
|
217
|
+
};
|
|
218
|
+
},
|
|
204
219
|
async addPaths(repoRoot, paths) {
|
|
205
220
|
if (paths.length === 0) {
|
|
206
221
|
return;
|
|
@@ -38,6 +38,13 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
38
38
|
commitSha: request.query.commit
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
app.post("/api/projects/harness/repository-diff/merge-to-current-branch", async (request) => {
|
|
42
|
+
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
43
|
+
return deps.harnessService.mergeRepositoryDiffToCurrentBranch(project.repoRoot, {
|
|
44
|
+
taskRepoRoot: task.worktreePath,
|
|
45
|
+
taskBranch: task.branch
|
|
46
|
+
});
|
|
47
|
+
});
|
|
41
48
|
app.get("/api/projects/harness/bootstrap", async (request) => {
|
|
42
49
|
const { project, task } = await requireHarnessTaskContext(deps, request.query.taskSlug);
|
|
43
50
|
try {
|
|
@@ -119,6 +126,16 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
119
126
|
comment: typeof request.body?.comment === "string" ? request.body.comment : undefined
|
|
120
127
|
});
|
|
121
128
|
});
|
|
129
|
+
app.post("/api/projects/harness/task-retrospective", async (request) => {
|
|
130
|
+
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
131
|
+
const trigger = request.body?.trigger === "auto" ? "auto" : "manual";
|
|
132
|
+
return deps.harnessFeedbackService.startTaskRetrospective(project.repoRoot, {
|
|
133
|
+
taskSlug: task.taskSlug,
|
|
134
|
+
taskRepoRoot: task.worktreePath,
|
|
135
|
+
handoffDir: task.handoffDir,
|
|
136
|
+
trigger
|
|
137
|
+
});
|
|
138
|
+
});
|
|
122
139
|
}
|
|
123
140
|
function degradedHarnessStatus(error) {
|
|
124
141
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DISPATCHABLE_ROLES, VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
2
2
|
import { isOpenFileLimitError, VcmError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
4
4
|
export function registerTaskRoutes(app, deps) {
|
|
@@ -41,7 +41,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
41
41
|
async function stopRunningRoleSessions(deps, repoRoot, taskSlug) {
|
|
42
42
|
const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
|
|
43
43
|
for (const session of sessions) {
|
|
44
|
-
if (session.status === "running" &&
|
|
44
|
+
if (session.status === "running" && VCM_ROLE_NAMES.some((role) => role === session.role)) {
|
|
45
45
|
await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function readVcmPackageVersion(appRoot) {
|
|
4
|
+
const packageJsonPath = path.join(appRoot, "package.json");
|
|
5
|
+
try {
|
|
6
|
+
const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
7
|
+
return typeof raw.version === "string" && raw.version.trim() ? raw.version : "unknown";
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return process.env.npm_package_version || "unknown";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -18,9 +18,10 @@ import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-ha
|
|
|
18
18
|
import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
|
|
19
19
|
import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
|
|
20
20
|
import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
|
|
21
|
-
|
|
21
|
+
import { readVcmPackageVersion } from "../app-version.js";
|
|
22
22
|
const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
const APP_ROOT = path.resolve(CLI_DIR, "../../..");
|
|
24
|
+
const VCM_PACKAGE_VERSION = readVcmPackageVersion(APP_ROOT);
|
|
24
25
|
const MANIFEST_PATH = ".ai/vcm-harness-manifest.json";
|
|
25
26
|
const HTML_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=\d+)? -->[\s\S]*?<!-- VCM:END -->/m;
|
|
26
27
|
const HASH_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=\d+)?\n[\s\S]*?# VCM:END/m;
|
|
@@ -331,7 +332,7 @@ async function buildManifest(projectRoot) {
|
|
|
331
332
|
return {
|
|
332
333
|
schemaVersion: 1,
|
|
333
334
|
manager: "vcm",
|
|
334
|
-
harnessVersion:
|
|
335
|
+
harnessVersion: VCM_PACKAGE_VERSION,
|
|
335
336
|
installMode: "fixed",
|
|
336
337
|
installedAt: typeof current?.installedAt === "string" ? current.installedAt : now,
|
|
337
338
|
updatedAt: now,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { CORE_VCM_ROLE_DEFINITIONS,
|
|
2
|
+
import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
4
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
5
5
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
@@ -425,9 +425,6 @@ export function createGatewayService(deps) {
|
|
|
425
425
|
};
|
|
426
426
|
const existing = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, definition.name);
|
|
427
427
|
if (existing?.status === "running") {
|
|
428
|
-
if (definition.name === "gate-reviewer") {
|
|
429
|
-
await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
|
|
430
|
-
}
|
|
431
428
|
startedRoles.push(definition.name);
|
|
432
429
|
continue;
|
|
433
430
|
}
|
|
@@ -556,7 +553,7 @@ export function createGatewayService(deps) {
|
|
|
556
553
|
async function stopRunningRoleSessions(repoRoot, taskSlug) {
|
|
557
554
|
const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
|
|
558
555
|
for (const session of sessions) {
|
|
559
|
-
if (session.status === "running" &&
|
|
556
|
+
if (session.status === "running" && VCM_ROLE_NAMES.some((role) => role === session.role)) {
|
|
560
557
|
await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
|
|
561
558
|
}
|
|
562
559
|
}
|
package/dist/backend/server.js
CHANGED
|
@@ -46,6 +46,7 @@ import { registerTaskRoutes } from "./api/task-routes.js";
|
|
|
46
46
|
import { registerTranslationRoutes } from "./api/translation-routes.js";
|
|
47
47
|
import { registerTerminalWs } from "./ws/terminal-ws.js";
|
|
48
48
|
import { toVcmError } from "./errors.js";
|
|
49
|
+
import { readVcmPackageVersion } from "./app-version.js";
|
|
49
50
|
export async function createServer(deps, options = {}) {
|
|
50
51
|
const app = Fastify({
|
|
51
52
|
logger: false
|
|
@@ -161,6 +162,8 @@ export async function startServer(options = {}) {
|
|
|
161
162
|
}
|
|
162
163
|
export function createDefaultServerDeps(options = {}) {
|
|
163
164
|
const fs = createNodeFileSystemAdapter();
|
|
165
|
+
const appRoot = getAppRoot();
|
|
166
|
+
const vcmVersion = readVcmPackageVersion(appRoot);
|
|
164
167
|
const runner = createCommandRunner();
|
|
165
168
|
const git = createGitAdapter(runner);
|
|
166
169
|
const claude = createClaudeAdapter(runner);
|
|
@@ -185,7 +188,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
185
188
|
git,
|
|
186
189
|
runtime,
|
|
187
190
|
harnessEngineerSessions: sessionService,
|
|
188
|
-
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(
|
|
191
|
+
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(appRoot, "scripts/install-vcm-harness.mjs")),
|
|
192
|
+
vcmVersion
|
|
189
193
|
});
|
|
190
194
|
const harnessFeedbackService = createHarnessFeedbackService({
|
|
191
195
|
fs,
|
|
@@ -277,7 +281,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
277
281
|
translationWorkerService
|
|
278
282
|
});
|
|
279
283
|
const diagnosticsService = createDiagnosticsService({
|
|
280
|
-
appRoot
|
|
284
|
+
appRoot,
|
|
281
285
|
runtime,
|
|
282
286
|
gatewayService,
|
|
283
287
|
translationService
|
|
@@ -230,7 +230,9 @@ function normalizePreferences(input) {
|
|
|
230
230
|
return {
|
|
231
231
|
themeMode: normalizeThemeMode(candidate.themeMode),
|
|
232
232
|
flowPauseAlerts: rawFlowPauseAlerts !== false,
|
|
233
|
+
roleRetryEnabled: candidate.roleRetryEnabled !== false,
|
|
233
234
|
permissionRequestMode: normalizePermissionRequestMode(candidate.permissionRequestMode),
|
|
235
|
+
autoTaskHarnessReviewEnabled: candidate.autoTaskHarnessReviewEnabled === true,
|
|
234
236
|
translationEnabled: candidate.translationEnabled === true,
|
|
235
237
|
translationAutoSendEnabled: candidate.translationAutoSendEnabled === true,
|
|
236
238
|
translationTargetLanguage: normalizeTranslationTargetLanguage(candidate.translationTargetLanguage),
|
|
@@ -2,9 +2,22 @@ import { isGateReviewerRoleName, isHarnessEngineerToolRoleName, isTranslatorTool
|
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
4
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
5
|
-
const
|
|
5
|
+
const MAX_ROLE_RETRY_ATTEMPTS = 20;
|
|
6
|
+
const ROLE_RETRY_BASE_DELAY_MS = 60_000;
|
|
7
|
+
const NON_RETRYABLE_STOP_FAILURE_ERRORS = new Set([
|
|
8
|
+
"authentication_failed",
|
|
9
|
+
"oauth_org_not_allowed",
|
|
10
|
+
"billing_error",
|
|
11
|
+
"invalid_request",
|
|
12
|
+
"model_not_found",
|
|
13
|
+
"max_output_tokens"
|
|
14
|
+
]);
|
|
15
|
+
const DIAGNOSTIC_SNIPPET_MAX_LENGTH = 2000;
|
|
6
16
|
export function createClaudeHookService(deps) {
|
|
7
|
-
const
|
|
17
|
+
const stopFailureRetryTimers = new Map();
|
|
18
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
19
|
+
const retrySetTimeout = deps.retrySetTimeout ?? ((callback, delayMs) => globalThis.setTimeout(callback, delayMs));
|
|
20
|
+
const retryClearTimeout = deps.retryClearTimeout ?? ((timer) => globalThis.clearTimeout(timer));
|
|
8
21
|
async function getHookContext(input) {
|
|
9
22
|
if (!isVcmRoleName(input.role)) {
|
|
10
23
|
throw new VcmError({
|
|
@@ -99,19 +112,8 @@ export function createClaudeHookService(deps) {
|
|
|
99
112
|
};
|
|
100
113
|
}
|
|
101
114
|
async function resolveHookTaskSlug(repoRoot, input) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
const session = await deps.sessionService.getProjectGateReviewerSession(repoRoot);
|
|
106
|
-
if (session?.activeTaskSlug) {
|
|
107
|
-
return session.activeTaskSlug;
|
|
108
|
-
}
|
|
109
|
-
throw new VcmError({
|
|
110
|
-
code: "GATE_REVIEWER_TASK_UNBOUND",
|
|
111
|
-
message: "Gate Reviewer hook arrived without an active task binding.",
|
|
112
|
-
statusCode: 409,
|
|
113
|
-
hint: "Start or resume Gate Reviewer from the current task before submitting work."
|
|
114
|
-
});
|
|
115
|
+
void repoRoot;
|
|
116
|
+
return input.taskSlug;
|
|
115
117
|
}
|
|
116
118
|
async function handleUserPromptSubmitHook(input) {
|
|
117
119
|
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
@@ -177,7 +179,7 @@ export function createClaudeHookService(deps) {
|
|
|
177
179
|
throwUnsupportedEvent(eventName);
|
|
178
180
|
}
|
|
179
181
|
const context = await getHookContext(input);
|
|
180
|
-
|
|
182
|
+
await clearStopFailureRecoveryState(context, input.role);
|
|
181
183
|
if (options.allowBlock && deps.jobGuard) {
|
|
182
184
|
const verdict = await deps.jobGuard.evaluateStop({
|
|
183
185
|
repoRoot: context.project.repoRoot,
|
|
@@ -215,15 +217,24 @@ export function createClaudeHookService(deps) {
|
|
|
215
217
|
const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
|
|
216
218
|
const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
|
|
217
219
|
if (hasCompletionEvidence) {
|
|
218
|
-
|
|
220
|
+
await clearStopFailureRecoveryState(context, input.role);
|
|
219
221
|
return recordTurnEnd(input, context, eventName, {
|
|
220
222
|
dispatchRouteFiles: !isGateReviewerRoleName(input.role),
|
|
221
223
|
notifyGateway: false,
|
|
222
224
|
settleGuard: true
|
|
223
225
|
});
|
|
224
226
|
}
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
+
const failure = parseStopFailureDiagnostic(input.event);
|
|
228
|
+
if (!failure.retryable) {
|
|
229
|
+
await markStopFailureRecoveryFailed(input, context, failure, 0);
|
|
230
|
+
return recordTurnEnd(input, context, eventName, {
|
|
231
|
+
dispatchRouteFiles: false,
|
|
232
|
+
notifyGateway: false,
|
|
233
|
+
settleGuard: false
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const retryScheduled = await scheduleStopFailureRetry(input, context, failure);
|
|
237
|
+
if (retryScheduled) {
|
|
227
238
|
return {
|
|
228
239
|
ok: true,
|
|
229
240
|
eventName,
|
|
@@ -336,38 +347,147 @@ export function createClaudeHookService(deps) {
|
|
|
336
347
|
...(stoppedRole ? { stoppedRole } : {})
|
|
337
348
|
};
|
|
338
349
|
}
|
|
339
|
-
async function
|
|
340
|
-
|
|
350
|
+
async function scheduleStopFailureRetry(input, context, failure) {
|
|
351
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
352
|
+
if (!preferences.roleRetryEnabled || !deps.runtime) {
|
|
341
353
|
return false;
|
|
342
354
|
}
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
355
|
+
const stateInput = createRoundStateInput(context);
|
|
356
|
+
const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
|
|
357
|
+
const previousAttempt = currentRoundState.roleRecovery?.role === input.role &&
|
|
358
|
+
currentRoundState.roleRecovery.status !== "failed"
|
|
359
|
+
? currentRoundState.roleRecovery.attempt
|
|
360
|
+
: 0;
|
|
361
|
+
const attempt = previousAttempt + 1;
|
|
362
|
+
const timestamp = now();
|
|
363
|
+
if (attempt > MAX_ROLE_RETRY_ATTEMPTS) {
|
|
364
|
+
clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
|
|
365
|
+
await markStopFailureRecoveryFailed(input, context, failure, MAX_ROLE_RETRY_ATTEMPTS, timestamp);
|
|
346
366
|
return false;
|
|
347
367
|
}
|
|
368
|
+
const nextRetryAt = new Date(Date.parse(timestamp) + attempt * ROLE_RETRY_BASE_DELAY_MS).toISOString();
|
|
369
|
+
await deps.roundService.setRoleRecovery({
|
|
370
|
+
...stateInput,
|
|
371
|
+
recovery: {
|
|
372
|
+
role: input.role,
|
|
373
|
+
status: "waiting",
|
|
374
|
+
attempt,
|
|
375
|
+
maxAttempts: MAX_ROLE_RETRY_ATTEMPTS,
|
|
376
|
+
lastFailureAt: timestamp,
|
|
377
|
+
error: failure.error,
|
|
378
|
+
errorDetails: failure.errorDetails,
|
|
379
|
+
lastAssistantMessage: failure.lastAssistantMessage,
|
|
380
|
+
retryable: failure.retryable,
|
|
381
|
+
nextRetryAt
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
|
|
385
|
+
scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
function scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt) {
|
|
389
|
+
const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
|
|
390
|
+
clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
|
|
391
|
+
const delayMs = Math.max(0, Date.parse(nextRetryAt) - Date.parse(now()));
|
|
392
|
+
const timer = retrySetTimeout(() => {
|
|
393
|
+
stopFailureRetryTimers.delete(key);
|
|
394
|
+
void runScheduledStopFailureRetry(input, context, attempt).catch(() => undefined);
|
|
395
|
+
}, delayMs);
|
|
396
|
+
stopFailureRetryTimers.set(key, timer);
|
|
397
|
+
}
|
|
398
|
+
async function runScheduledStopFailureRetry(input, context, attempt) {
|
|
399
|
+
const stateInput = createRoundStateInput(context);
|
|
400
|
+
const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
|
|
401
|
+
const recovery = currentRoundState.roleRecovery;
|
|
402
|
+
if (!recovery || recovery.role !== input.role || recovery.attempt !== attempt || recovery.status !== "waiting") {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const timestamp = now();
|
|
348
406
|
const session = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
|
|
349
|
-
if (!session || session.status !== "running") {
|
|
350
|
-
|
|
407
|
+
if (!session || session.status !== "running" || !deps.runtime) {
|
|
408
|
+
await deps.roundService.setRoleRecovery({
|
|
409
|
+
...stateInput,
|
|
410
|
+
recovery: {
|
|
411
|
+
...recovery,
|
|
412
|
+
status: "failed",
|
|
413
|
+
failedAt: timestamp
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
await recordTurnEnd(input, context, "StopFailure", {
|
|
417
|
+
dispatchRouteFiles: false,
|
|
418
|
+
notifyGateway: false,
|
|
419
|
+
settleGuard: false
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
351
422
|
}
|
|
352
|
-
|
|
353
|
-
|
|
423
|
+
await deps.roundService.setRoleRecovery({
|
|
424
|
+
...stateInput,
|
|
425
|
+
recovery: {
|
|
426
|
+
...recovery,
|
|
427
|
+
status: "retrying",
|
|
428
|
+
nextRetryAt: undefined,
|
|
429
|
+
lastRetryAt: timestamp
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt());
|
|
354
433
|
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
|
|
355
|
-
|
|
434
|
+
}
|
|
435
|
+
async function markStopFailureRecoveryFailed(input, context, failure, attempt, timestamp = now()) {
|
|
436
|
+
clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
|
|
437
|
+
await deps.roundService.setRoleRecovery({
|
|
438
|
+
...createRoundStateInput(context),
|
|
439
|
+
recovery: {
|
|
440
|
+
role: input.role,
|
|
441
|
+
status: "failed",
|
|
442
|
+
attempt,
|
|
443
|
+
maxAttempts: MAX_ROLE_RETRY_ATTEMPTS,
|
|
444
|
+
lastFailureAt: timestamp,
|
|
445
|
+
error: failure.error,
|
|
446
|
+
errorDetails: failure.errorDetails,
|
|
447
|
+
lastAssistantMessage: failure.lastAssistantMessage,
|
|
448
|
+
retryable: failure.retryable,
|
|
449
|
+
failedAt: timestamp
|
|
450
|
+
}
|
|
451
|
+
});
|
|
356
452
|
}
|
|
357
453
|
function clearStopFailureRecovery(repoRoot, taskSlug, role) {
|
|
358
|
-
|
|
454
|
+
clearStopFailureRetryTimer(repoRoot, taskSlug, role);
|
|
455
|
+
}
|
|
456
|
+
async function clearStopFailureRecoveryState(context, role) {
|
|
457
|
+
clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, role);
|
|
458
|
+
await deps.roundService.clearRoleRecovery?.({
|
|
459
|
+
...createRoundStateInput(context),
|
|
460
|
+
role
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function clearStopFailureRetryTimer(repoRoot, taskSlug, role) {
|
|
464
|
+
const key = stopFailureRecoveryKey(repoRoot, taskSlug, role);
|
|
465
|
+
const timer = stopFailureRetryTimers.get(key);
|
|
466
|
+
if (timer === undefined) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
retryClearTimeout(timer);
|
|
470
|
+
stopFailureRetryTimers.delete(key);
|
|
359
471
|
}
|
|
360
472
|
function stopFailureRecoveryKey(repoRoot, taskSlug, role) {
|
|
361
473
|
return `${repoRoot}:${taskSlug}:${role}`;
|
|
362
474
|
}
|
|
363
|
-
function
|
|
475
|
+
function createRoundStateInput(context) {
|
|
476
|
+
return {
|
|
477
|
+
repoRoot: context.project.repoRoot,
|
|
478
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
479
|
+
stateRoot: context.config.stateRoot,
|
|
480
|
+
taskSlug: context.taskSlug
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function renderStopFailureRecoveryPrompt() {
|
|
364
484
|
return [
|
|
365
485
|
"[VCM Recovery]",
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"
|
|
369
|
-
"If
|
|
370
|
-
|
|
486
|
+
"Previous turn ended unexpectedly. Continue from current repo + VCM handoff state.",
|
|
487
|
+
"",
|
|
488
|
+
"Check whether your assigned work is already complete.",
|
|
489
|
+
"If complete, write the expected VCM completion artifact now.",
|
|
490
|
+
"Do not repeat completed edits, validation, or route messages."
|
|
371
491
|
].join("\n");
|
|
372
492
|
}
|
|
373
493
|
async function handlePermissionRequestHook(input) {
|
|
@@ -470,6 +590,38 @@ function throwUnsupportedEvent(eventName) {
|
|
|
470
590
|
hint: "Use the matching VCM hook endpoint for this event."
|
|
471
591
|
});
|
|
472
592
|
}
|
|
593
|
+
function parseStopFailureDiagnostic(event) {
|
|
594
|
+
const error = (normalizeDiagnosticString(event.error, 200) ?? "unknown").toLowerCase();
|
|
595
|
+
const errorDetails = normalizeDiagnosticString(event.error_details, DIAGNOSTIC_SNIPPET_MAX_LENGTH);
|
|
596
|
+
const lastAssistantMessage = normalizeDiagnosticString(event.last_assistant_message, DIAGNOSTIC_SNIPPET_MAX_LENGTH);
|
|
597
|
+
return {
|
|
598
|
+
error,
|
|
599
|
+
errorDetails,
|
|
600
|
+
lastAssistantMessage,
|
|
601
|
+
retryable: !NON_RETRYABLE_STOP_FAILURE_ERRORS.has(error)
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function normalizeDiagnosticString(value, maxLength) {
|
|
605
|
+
let text;
|
|
606
|
+
if (typeof value === "string") {
|
|
607
|
+
text = value;
|
|
608
|
+
}
|
|
609
|
+
else if (value !== undefined && value !== null) {
|
|
610
|
+
try {
|
|
611
|
+
text = JSON.stringify(value);
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
text = String(value);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const trimmed = text?.trim();
|
|
618
|
+
if (!trimmed) {
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
return trimmed.length > maxLength
|
|
622
|
+
? `${trimmed.slice(0, maxLength)}...`
|
|
623
|
+
: trimmed;
|
|
624
|
+
}
|
|
473
625
|
function stringOrUndefined(value) {
|
|
474
626
|
return typeof value === "string" ? value : undefined;
|
|
475
627
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
|
-
import
|
|
2
|
+
import { readVcmPackageVersion } from "../app-version.js";
|
|
4
3
|
export function createDiagnosticsService(deps) {
|
|
5
|
-
const version =
|
|
4
|
+
const version = readVcmPackageVersion(deps.appRoot);
|
|
6
5
|
return {
|
|
7
6
|
async getRuntimeDiagnostics() {
|
|
8
7
|
const runtimeSessions = deps.runtime.listSessions();
|
|
@@ -34,16 +33,6 @@ export function createDiagnosticsService(deps) {
|
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
35
|
}
|
|
37
|
-
function readPackageVersion(appRoot) {
|
|
38
|
-
const packageJsonPath = path.join(appRoot, "package.json");
|
|
39
|
-
try {
|
|
40
|
-
const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
41
|
-
return typeof raw.version === "string" && raw.version.trim() ? raw.version : "unknown";
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return process.env.npm_package_version || "unknown";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
36
|
async function readFdCount() {
|
|
48
37
|
try {
|
|
49
38
|
return (await fs.readdir("/proc/self/fd")).length;
|