vibe-coding-master 0.7.47 → 0.7.49
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 +7 -0
- package/dist/backend/api/session-routes.js +41 -1
- package/dist/backend/server.js +17 -3
- package/dist/backend/services/architect-restart-service.js +251 -78
- package/dist/backend/services/artifact-service.js +60 -131
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/gate-review-service.js +42 -217
- package/dist/backend/services/harness-feedback-service.js +13 -24
- package/dist/backend/services/managed-artifact-validation.js +403 -0
- package/dist/backend/services/message-service.js +13 -64
- package/dist/backend/services/role-context-restart-service.js +240 -0
- package/dist/backend/services/runtime-recovery-service.js +2 -0
- package/dist/backend/services/session-service.js +39 -2
- package/dist/backend/services/task-close-service.js +6 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -0
- package/dist/backend/templates/harness/gate-review.js +13 -98
- package/dist/backend/templates/harness/tester-agent.js +1 -0
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +1 -1
- package/dist/shared/validation/artifact-registry.js +68 -0
- package/dist-frontend/assets/{index-nAV6toi8.js → index-DLsIPTvK.js} +34 -34
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -34
package/README.md
CHANGED
|
@@ -221,8 +221,15 @@ Controls:
|
|
|
221
221
|
- `Start`: start a new Claude Code role session
|
|
222
222
|
- `Resume`: resume a saved Claude Code session
|
|
223
223
|
- `Restart`: stop current process and start fresh
|
|
224
|
+
- `Restart With Context`: restart a VCM workflow role and continue from its current task artifacts
|
|
224
225
|
- `Stop`: stop the embedded terminal process
|
|
225
226
|
|
|
227
|
+
`Restart With Context` is available only for Project Manager, Architect, Coder,
|
|
228
|
+
Tester, and Reviewer. It reuses their existing handoffs, workflow state, Gate
|
|
229
|
+
requests, worktree, and Git state; it does not create a separate context file.
|
|
230
|
+
Translator and Harness Engineer are tool Agents and do not use this workflow-role
|
|
231
|
+
recovery path.
|
|
232
|
+
|
|
226
233
|
Permission modes:
|
|
227
234
|
|
|
228
235
|
```text
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDispatchableRole, isRoleName } from "../../shared/constants.js";
|
|
1
|
+
import { isDispatchableRole, isRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
export function registerSessionRoutes(app, deps) {
|
|
4
4
|
app.get("/api/tasks/:taskSlug/sessions", async (request) => {
|
|
@@ -8,6 +8,9 @@ export function registerSessionRoutes(app, deps) {
|
|
|
8
8
|
app.post("/api/tasks/:taskSlug/sessions/:role/start", async (request) => {
|
|
9
9
|
const project = await requireCurrentProject(deps.projectService);
|
|
10
10
|
const role = parseRole(request.params.role);
|
|
11
|
+
if (isVcmRoleName(role)) {
|
|
12
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
13
|
+
}
|
|
11
14
|
return deps.sessionService.startRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
12
15
|
});
|
|
13
16
|
app.post("/api/tasks/:taskSlug/sessions/architect/restart-after-planning", async (request, reply) => {
|
|
@@ -18,14 +21,38 @@ export function registerSessionRoutes(app, deps) {
|
|
|
18
21
|
app.post("/api/tasks/:taskSlug/sessions/:role/stop", async (request) => {
|
|
19
22
|
const project = await requireCurrentProject(deps.projectService);
|
|
20
23
|
const role = parseRole(request.params.role);
|
|
24
|
+
if (isVcmRoleName(role)) {
|
|
25
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
26
|
+
}
|
|
21
27
|
const session = await deps.sessionService.stopRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
22
28
|
await deps.translationService.stopSession(session.id);
|
|
23
29
|
deps.roundService.stopSession(session.id);
|
|
24
30
|
return session;
|
|
25
31
|
});
|
|
32
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/restart-with-context", async (request) => {
|
|
33
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
34
|
+
const role = parseVcmRole(request.params.role);
|
|
35
|
+
if (role === "architect" && deps.architectRestartService.getState(project.repoRoot, request.params.taskSlug)) {
|
|
36
|
+
throw new VcmError({
|
|
37
|
+
code: "ARCHITECT_RESTART_CONFLICT",
|
|
38
|
+
message: "Architect already has a pending post-planning restart.",
|
|
39
|
+
statusCode: 409
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
43
|
+
await deps.sessionService.assertModelLaunchReady(request.body?.model ?? existing?.model);
|
|
44
|
+
if (existing) {
|
|
45
|
+
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
46
|
+
deps.roundService.stopSession(existing.id);
|
|
47
|
+
}
|
|
48
|
+
return deps.roleContextRestartService.restart(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
49
|
+
});
|
|
26
50
|
app.post("/api/tasks/:taskSlug/sessions/:role/restart", async (request) => {
|
|
27
51
|
const project = await requireCurrentProject(deps.projectService);
|
|
28
52
|
const role = parseRole(request.params.role);
|
|
53
|
+
if (isVcmRoleName(role)) {
|
|
54
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
55
|
+
}
|
|
29
56
|
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
30
57
|
await deps.sessionService.assertModelLaunchReady(request.body?.model ?? existing?.model);
|
|
31
58
|
if (existing) {
|
|
@@ -37,6 +64,9 @@ export function registerSessionRoutes(app, deps) {
|
|
|
37
64
|
app.post("/api/tasks/:taskSlug/sessions/:role/resume", async (request) => {
|
|
38
65
|
const project = await requireCurrentProject(deps.projectService);
|
|
39
66
|
const role = parseRole(request.params.role);
|
|
67
|
+
if (isVcmRoleName(role)) {
|
|
68
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
69
|
+
}
|
|
40
70
|
return deps.sessionService.resumeRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
41
71
|
});
|
|
42
72
|
app.post("/api/tasks/:taskSlug/sessions/:role/notify-harness", async (request) => {
|
|
@@ -70,6 +100,16 @@ function parseRole(role) {
|
|
|
70
100
|
}
|
|
71
101
|
return role;
|
|
72
102
|
}
|
|
103
|
+
function parseVcmRole(role) {
|
|
104
|
+
if (!isVcmRoleName(role)) {
|
|
105
|
+
throw new VcmError({
|
|
106
|
+
code: "ROLE_CONTEXT_RESTART_UNSUPPORTED",
|
|
107
|
+
message: `Restart With Context is available only for VCM workflow roles: ${role}`,
|
|
108
|
+
statusCode: 400
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return role;
|
|
112
|
+
}
|
|
73
113
|
async function requireCurrentProject(projectService) {
|
|
74
114
|
const project = await projectService.getCurrentProject();
|
|
75
115
|
if (!project) {
|
package/dist/backend/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createAppSettingsService } from "./services/app-settings-service.js";
|
|
|
13
13
|
import { createCodexBridgeIntegrationService } from "./services/codex-bridge-integration-service.js";
|
|
14
14
|
import { createAutoMemoryService } from "./services/auto-memory-service.js";
|
|
15
15
|
import { createArchitectRestartService } from "./services/architect-restart-service.js";
|
|
16
|
+
import { createRoleContextRestartService } from "./services/role-context-restart-service.js";
|
|
16
17
|
import { createRoleStallDetectorService } from "./services/role-stall-detector-service.js";
|
|
17
18
|
import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
|
|
18
19
|
import { createGateReviewService } from "./services/gate-review-service.js";
|
|
@@ -144,7 +145,8 @@ export async function createServer(deps, options = {}) {
|
|
|
144
145
|
commandDispatcher: deps.commandDispatcher,
|
|
145
146
|
translationService: deps.translationService,
|
|
146
147
|
roundService: deps.roundService,
|
|
147
|
-
architectRestartService: deps.architectRestartService
|
|
148
|
+
architectRestartService: deps.architectRestartService,
|
|
149
|
+
roleContextRestartService: deps.roleContextRestartService
|
|
148
150
|
});
|
|
149
151
|
registerArtifactRoutes(app, {
|
|
150
152
|
projectService: deps.projectService,
|
|
@@ -297,9 +299,16 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
297
299
|
const architectRestartService = createArchitectRestartService({
|
|
298
300
|
fs,
|
|
299
301
|
taskService,
|
|
302
|
+
projectService,
|
|
300
303
|
sessionService,
|
|
301
304
|
appSettings
|
|
302
305
|
});
|
|
306
|
+
const roleContextRestartService = createRoleContextRestartService({
|
|
307
|
+
fs,
|
|
308
|
+
projectService,
|
|
309
|
+
taskService,
|
|
310
|
+
sessionService
|
|
311
|
+
});
|
|
303
312
|
const messageService = createMessageService({
|
|
304
313
|
fs,
|
|
305
314
|
runtime,
|
|
@@ -377,7 +386,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
377
386
|
roundService,
|
|
378
387
|
projectService,
|
|
379
388
|
taskWorkflowService,
|
|
380
|
-
architectRestartService
|
|
389
|
+
architectRestartService,
|
|
390
|
+
roleContextRestartService
|
|
381
391
|
});
|
|
382
392
|
const gatewayService = createGatewayService({
|
|
383
393
|
fs,
|
|
@@ -399,7 +409,9 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
399
409
|
runtime,
|
|
400
410
|
projectService,
|
|
401
411
|
taskService,
|
|
402
|
-
translationWorkerService
|
|
412
|
+
translationWorkerService,
|
|
413
|
+
architectRestartService,
|
|
414
|
+
roleContextRestartService
|
|
403
415
|
});
|
|
404
416
|
const claudeHookService = createClaudeHookService({
|
|
405
417
|
projectService,
|
|
@@ -417,6 +429,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
417
429
|
jobGuard: createJobGuardService(),
|
|
418
430
|
translationWorkerService,
|
|
419
431
|
architectRestartService,
|
|
432
|
+
roleContextRestartService,
|
|
420
433
|
roleStallDetector,
|
|
421
434
|
workflowControlService
|
|
422
435
|
});
|
|
@@ -464,6 +477,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
464
477
|
taskCloseService,
|
|
465
478
|
taskWorkflowService,
|
|
466
479
|
architectRestartService,
|
|
480
|
+
roleContextRestartService,
|
|
467
481
|
sessionService,
|
|
468
482
|
artifactService,
|
|
469
483
|
harnessService,
|
|
@@ -17,55 +17,61 @@ Before performing any assigned work, read:
|
|
|
17
17
|
- the latest Gate Review report when present
|
|
18
18
|
|
|
19
19
|
Treat the current artifacts and worktree as the source of truth. The architecture-plan Gate has accepted the current planning artifacts or VCM recorded an explicit Gate exception. Do not repeat the completed interview or planning work unless a later route explicitly reopens it.`;
|
|
20
|
+
const ARCHITECT_RESTART_STATE_FILE = "architect-restart.json";
|
|
20
21
|
export function createArchitectRestartService(deps) {
|
|
21
22
|
const pendingByTask = new Map();
|
|
23
|
+
const operationsByTask = new Map();
|
|
22
24
|
return {
|
|
23
25
|
async schedule(repoRoot, taskSlug) {
|
|
24
|
-
const session = await requireRunningArchitect(repoRoot, taskSlug);
|
|
25
|
-
await requireCompletePlan(repoRoot, taskSlug);
|
|
26
26
|
const key = taskKey(repoRoot, taskSlug);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await tryRestart(existing);
|
|
27
|
+
return withTaskLock(key, async () => {
|
|
28
|
+
const session = await requireRunningArchitect(repoRoot, taskSlug);
|
|
29
|
+
await requireCompletePlan(repoRoot, taskSlug);
|
|
30
|
+
const existing = pendingByTask.get(key);
|
|
31
|
+
if (existing?.status === "executing") {
|
|
33
32
|
return {
|
|
34
33
|
taskSlug,
|
|
35
|
-
sessionId:
|
|
36
|
-
status: "
|
|
34
|
+
sessionId: existing.replacementSessionId ?? existing.sourceSessionId,
|
|
35
|
+
status: "already_scheduled",
|
|
37
36
|
...(existing.memoryCandidatePath
|
|
38
37
|
? { memoryCandidatePath: existing.memoryCandidatePath }
|
|
39
38
|
: {})
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
|
-
|
|
41
|
+
if (existing && isSourceSession(existing, session)) {
|
|
42
|
+
if (existing.status === "blocked") {
|
|
43
|
+
existing.status = "pending";
|
|
44
|
+
existing.blocker = undefined;
|
|
45
|
+
existing.sourceSessionId = session.id;
|
|
46
|
+
existing.updatedAt = timestamp();
|
|
47
|
+
await persistPending(existing);
|
|
48
|
+
await tryRestart(existing);
|
|
49
|
+
return scheduleResult(existing, "scheduled");
|
|
50
|
+
}
|
|
51
|
+
return scheduleResult(existing, "already_scheduled");
|
|
52
|
+
}
|
|
53
|
+
const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
|
|
54
|
+
? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
|
|
55
|
+
: undefined;
|
|
56
|
+
const pending = {
|
|
57
|
+
version: 1,
|
|
58
|
+
repoRoot,
|
|
43
59
|
taskSlug,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
sourceSessionId: session.id,
|
|
61
|
+
...(session.claudeSessionId ? { sourceClaudeSessionId: session.claudeSessionId } : {}),
|
|
62
|
+
stopped: false,
|
|
63
|
+
gateAccepted: false,
|
|
64
|
+
status: "pending",
|
|
65
|
+
permissionMode: session.permissionMode,
|
|
66
|
+
model: session.model,
|
|
67
|
+
effort: session.effort,
|
|
68
|
+
memoryCandidatePath: existing?.memoryCandidatePath ?? memoryCandidatePath,
|
|
69
|
+
updatedAt: timestamp()
|
|
49
70
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
: undefined;
|
|
54
|
-
pendingByTask.set(key, {
|
|
55
|
-
repoRoot,
|
|
56
|
-
taskSlug,
|
|
57
|
-
sessionId: session.id,
|
|
58
|
-
stopped: false,
|
|
59
|
-
gateAccepted: false,
|
|
60
|
-
status: "pending",
|
|
61
|
-
memoryCandidatePath
|
|
71
|
+
pendingByTask.set(key, pending);
|
|
72
|
+
await persistPending(pending);
|
|
73
|
+
return scheduleResult(pending, "scheduled");
|
|
62
74
|
});
|
|
63
|
-
return {
|
|
64
|
-
taskSlug,
|
|
65
|
-
sessionId: session.id,
|
|
66
|
-
status: "scheduled",
|
|
67
|
-
...(memoryCandidatePath ? { memoryCandidatePath } : {})
|
|
68
|
-
};
|
|
69
75
|
},
|
|
70
76
|
getState(repoRoot, taskSlug) {
|
|
71
77
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
@@ -74,7 +80,7 @@ export function createArchitectRestartService(deps) {
|
|
|
74
80
|
}
|
|
75
81
|
return {
|
|
76
82
|
taskSlug: pending.taskSlug,
|
|
77
|
-
sessionId: pending.
|
|
83
|
+
sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
|
|
78
84
|
status: pending.status,
|
|
79
85
|
...(pending.memoryCandidatePath
|
|
80
86
|
? { memoryCandidatePath: pending.memoryCandidatePath }
|
|
@@ -82,46 +88,128 @@ export function createArchitectRestartService(deps) {
|
|
|
82
88
|
...(pending.blocker ? { blocker: { ...pending.blocker } } : {})
|
|
83
89
|
};
|
|
84
90
|
},
|
|
91
|
+
async recoverTask(repoRoot, taskSlug) {
|
|
92
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
93
|
+
await withTaskLock(key, async () => {
|
|
94
|
+
const stored = await loadPending(repoRoot, taskSlug);
|
|
95
|
+
if (!stored) {
|
|
96
|
+
pendingByTask.delete(key);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const pending = { ...stored, repoRoot };
|
|
100
|
+
pendingByTask.set(key, pending);
|
|
101
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
102
|
+
if (pending.status === "executing") {
|
|
103
|
+
if (isConfirmedReplacement(pending, session)) {
|
|
104
|
+
await removePending(pending);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (session?.status === "running"
|
|
108
|
+
&& pending.replacementSessionId === session.id) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await launchReplacement(pending);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (pending.status === "pending" && restartPrerequisitesMet(pending)) {
|
|
115
|
+
if (session?.status === "running") {
|
|
116
|
+
await tryRestart(pending);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
await launchReplacement(pending);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
},
|
|
85
124
|
async recordArchitectStop(repoRoot, taskSlug, sessionId) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
126
|
+
await withTaskLock(key, async () => {
|
|
127
|
+
const pending = pendingByTask.get(key);
|
|
128
|
+
if (!pending || pending.status !== "pending") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
132
|
+
if (!session || session.id !== sessionId || !isSourceSession(pending, session)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
pending.sourceSessionId = session.id;
|
|
136
|
+
pending.stopped = true;
|
|
137
|
+
pending.updatedAt = timestamp();
|
|
138
|
+
await persistPending(pending);
|
|
139
|
+
await tryRestart(pending);
|
|
140
|
+
});
|
|
92
141
|
},
|
|
93
142
|
async recordRouteDelivered(repoRoot, taskSlug, message) {
|
|
94
143
|
if (!isArchitectToPm(message)) {
|
|
95
144
|
return;
|
|
96
145
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
147
|
+
await withTaskLock(key, async () => {
|
|
148
|
+
const pending = pendingByTask.get(key);
|
|
149
|
+
if (!pending || pending.status !== "pending") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
pending.deliveredMessageId = message.id;
|
|
153
|
+
pending.updatedAt = timestamp();
|
|
154
|
+
await persistPending(pending);
|
|
155
|
+
await tryRestart(pending);
|
|
156
|
+
});
|
|
103
157
|
},
|
|
104
158
|
async recordRouteAccepted(repoRoot, taskSlug, message) {
|
|
105
159
|
if (!isArchitectToPm(message)) {
|
|
106
160
|
return;
|
|
107
161
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
162
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
163
|
+
await withTaskLock(key, async () => {
|
|
164
|
+
const pending = pendingByTask.get(key);
|
|
165
|
+
if (!pending || pending.status !== "pending") {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
pending.acceptedMessageId = message.id;
|
|
169
|
+
pending.updatedAt = timestamp();
|
|
170
|
+
await persistPending(pending);
|
|
171
|
+
await tryRestart(pending);
|
|
172
|
+
});
|
|
114
173
|
},
|
|
115
174
|
async recordArchitectureGateDisposition(repoRoot, taskSlug, accepted) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
176
|
+
await withTaskLock(key, async () => {
|
|
177
|
+
const pending = pendingByTask.get(key);
|
|
178
|
+
if (!pending || pending.status !== "pending") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
pending.gateAccepted = accepted;
|
|
182
|
+
pending.updatedAt = timestamp();
|
|
183
|
+
await persistPending(pending);
|
|
184
|
+
await tryRestart(pending);
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
async recordReplacementPromptSubmitted(repoRoot, taskSlug, sessionId) {
|
|
188
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
189
|
+
await withTaskLock(key, async () => {
|
|
190
|
+
const pending = pendingByTask.get(key);
|
|
191
|
+
if (!pending || pending.status !== "executing") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
195
|
+
if (!session
|
|
196
|
+
|| session.id !== sessionId
|
|
197
|
+
|| !session.claudeSessionId
|
|
198
|
+
|| (pending.replacementSessionId && pending.replacementSessionId !== session.id)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await removePending(pending);
|
|
202
|
+
});
|
|
122
203
|
},
|
|
123
|
-
clear(repoRoot, taskSlug) {
|
|
124
|
-
|
|
204
|
+
async clear(repoRoot, taskSlug) {
|
|
205
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
206
|
+
await withTaskLock(key, async () => {
|
|
207
|
+
const pending = pendingByTask.get(key) ?? await loadPending(repoRoot, taskSlug);
|
|
208
|
+
pendingByTask.delete(key);
|
|
209
|
+
if (pending) {
|
|
210
|
+
await removePending({ ...pending, repoRoot });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
125
213
|
}
|
|
126
214
|
};
|
|
127
215
|
async function requireRunningArchitect(repoRoot, taskSlug) {
|
|
@@ -147,17 +235,12 @@ export function createArchitectRestartService(deps) {
|
|
|
147
235
|
}
|
|
148
236
|
}
|
|
149
237
|
async function tryRestart(pending) {
|
|
150
|
-
if (pending.status !== "pending"
|
|
151
|
-
|| !pending.stopped
|
|
152
|
-
|| !pending.deliveredMessageId
|
|
153
|
-
|| pending.deliveredMessageId !== pending.acceptedMessageId
|
|
154
|
-
|| !pending.gateAccepted) {
|
|
238
|
+
if (pending.status !== "pending" || !restartPrerequisitesMet(pending)) {
|
|
155
239
|
return;
|
|
156
240
|
}
|
|
157
241
|
const session = await deps.sessionService.getRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE);
|
|
158
|
-
if (!session
|
|
159
|
-
|
|
160
|
-
blockPending(pending, new VcmError({
|
|
242
|
+
if (!session || !isSourceSession(pending, session)) {
|
|
243
|
+
await blockPending(pending, new VcmError({
|
|
161
244
|
code: "ARCHITECT_RESTART_SESSION_UNAVAILABLE",
|
|
162
245
|
message: "Architect restart is blocked because the scheduled Architect session no longer exists.",
|
|
163
246
|
statusCode: 409
|
|
@@ -165,7 +248,7 @@ export function createArchitectRestartService(deps) {
|
|
|
165
248
|
return;
|
|
166
249
|
}
|
|
167
250
|
if (session.status !== "running") {
|
|
168
|
-
blockPending(pending, new VcmError({
|
|
251
|
+
await blockPending(pending, new VcmError({
|
|
169
252
|
code: "ARCHITECT_RESTART_SESSION_NOT_RUNNING",
|
|
170
253
|
message: "Architect restart is blocked because the scheduled Architect session is not running.",
|
|
171
254
|
statusCode: 409
|
|
@@ -175,31 +258,121 @@ export function createArchitectRestartService(deps) {
|
|
|
175
258
|
if (session.activityStatus !== "idle") {
|
|
176
259
|
return;
|
|
177
260
|
}
|
|
261
|
+
pending.sourceSessionId = session.id;
|
|
262
|
+
await launchReplacement(pending);
|
|
263
|
+
}
|
|
264
|
+
async function launchReplacement(pending) {
|
|
178
265
|
pending.status = "executing";
|
|
266
|
+
pending.blocker = undefined;
|
|
267
|
+
pending.updatedAt = timestamp();
|
|
268
|
+
await persistPending(pending);
|
|
179
269
|
try {
|
|
180
270
|
await requireCompletePlan(pending.repoRoot, pending.taskSlug);
|
|
181
271
|
await requirePlanningMemoryCandidate(pending);
|
|
182
|
-
await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
183
|
-
permissionMode:
|
|
184
|
-
model:
|
|
185
|
-
effort:
|
|
272
|
+
const replacement = await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
273
|
+
permissionMode: pending.permissionMode,
|
|
274
|
+
model: pending.model,
|
|
275
|
+
effort: pending.effort,
|
|
186
276
|
appendSystemPrompt: ARCHITECT_RESTORE_PROMPT
|
|
187
277
|
});
|
|
188
|
-
|
|
278
|
+
pending.replacementSessionId = replacement.id;
|
|
279
|
+
pending.updatedAt = timestamp();
|
|
280
|
+
await persistPending(pending);
|
|
189
281
|
}
|
|
190
282
|
catch (error) {
|
|
191
|
-
blockPending(pending, error);
|
|
283
|
+
await blockPending(pending, error);
|
|
192
284
|
}
|
|
193
285
|
}
|
|
194
|
-
function blockPending(pending, error) {
|
|
286
|
+
async function blockPending(pending, error) {
|
|
195
287
|
const normalized = toVcmError(error);
|
|
196
288
|
pending.status = "blocked";
|
|
197
289
|
pending.blocker = {
|
|
198
290
|
code: normalized.code,
|
|
199
291
|
message: normalized.message,
|
|
200
|
-
blockedAt:
|
|
292
|
+
blockedAt: timestamp()
|
|
293
|
+
};
|
|
294
|
+
pending.updatedAt = timestamp();
|
|
295
|
+
await persistPending(pending);
|
|
296
|
+
}
|
|
297
|
+
function restartPrerequisitesMet(pending) {
|
|
298
|
+
return Boolean(pending.stopped
|
|
299
|
+
&& pending.deliveredMessageId
|
|
300
|
+
&& pending.deliveredMessageId === pending.acceptedMessageId
|
|
301
|
+
&& pending.gateAccepted);
|
|
302
|
+
}
|
|
303
|
+
function isSourceSession(pending, session) {
|
|
304
|
+
return session.id === pending.sourceSessionId
|
|
305
|
+
|| Boolean(pending.sourceClaudeSessionId
|
|
306
|
+
&& session.claudeSessionId
|
|
307
|
+
&& pending.sourceClaudeSessionId === session.claudeSessionId);
|
|
308
|
+
}
|
|
309
|
+
function isConfirmedReplacement(pending, session) {
|
|
310
|
+
return Boolean(session?.claudeSessionId
|
|
311
|
+
&& ((pending.replacementSessionId && session.id === pending.replacementSessionId)
|
|
312
|
+
|| !pending.sourceClaudeSessionId
|
|
313
|
+
|| session.claudeSessionId !== pending.sourceClaudeSessionId));
|
|
314
|
+
}
|
|
315
|
+
function scheduleResult(pending, status) {
|
|
316
|
+
return {
|
|
317
|
+
taskSlug: pending.taskSlug,
|
|
318
|
+
sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
|
|
319
|
+
status,
|
|
320
|
+
...(pending.memoryCandidatePath ? { memoryCandidatePath: pending.memoryCandidatePath } : {})
|
|
201
321
|
};
|
|
202
322
|
}
|
|
323
|
+
async function persistPending(pending) {
|
|
324
|
+
const { repoRoot: _repoRoot, ...stored } = pending;
|
|
325
|
+
await deps.fs.writeJsonAtomic(await statePath(pending.repoRoot, pending.taskSlug), stored);
|
|
326
|
+
pendingByTask.set(taskKey(pending.repoRoot, pending.taskSlug), pending);
|
|
327
|
+
}
|
|
328
|
+
async function loadPending(repoRoot, taskSlug) {
|
|
329
|
+
const target = await statePath(repoRoot, taskSlug);
|
|
330
|
+
if (!(await deps.fs.pathExists(target))) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
const stored = await deps.fs.readJson(target);
|
|
334
|
+
if (stored.version !== 1
|
|
335
|
+
|| stored.taskSlug !== taskSlug
|
|
336
|
+
|| !stored.sourceSessionId
|
|
337
|
+
|| !["pending", "executing", "blocked"].includes(stored.status)) {
|
|
338
|
+
throw new VcmError({
|
|
339
|
+
code: "ARCHITECT_RESTART_STATE_INVALID",
|
|
340
|
+
message: `Architect restart state is invalid for task ${taskSlug}.`,
|
|
341
|
+
statusCode: 500
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return stored;
|
|
345
|
+
}
|
|
346
|
+
async function removePending(pending) {
|
|
347
|
+
pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
|
|
348
|
+
const target = await statePath(pending.repoRoot, pending.taskSlug);
|
|
349
|
+
if (await deps.fs.pathExists(target)) {
|
|
350
|
+
await deps.fs.removePath?.(target, { force: true });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function statePath(repoRoot, taskSlug) {
|
|
354
|
+
const [config, task] = await Promise.all([
|
|
355
|
+
deps.projectService.loadConfig(repoRoot),
|
|
356
|
+
deps.taskService.loadTask(repoRoot, taskSlug)
|
|
357
|
+
]);
|
|
358
|
+
return resolveRepoPath(getTaskRuntimeRepoRoot(task), path.posix.join(config.stateRoot, ARCHITECT_RESTART_STATE_FILE));
|
|
359
|
+
}
|
|
360
|
+
async function withTaskLock(key, run) {
|
|
361
|
+
const previous = operationsByTask.get(key) ?? Promise.resolve();
|
|
362
|
+
const current = previous.catch(() => undefined).then(run);
|
|
363
|
+
operationsByTask.set(key, current);
|
|
364
|
+
try {
|
|
365
|
+
return await current;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
if (operationsByTask.get(key) === current) {
|
|
369
|
+
operationsByTask.delete(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function timestamp() {
|
|
374
|
+
return new Date().toISOString();
|
|
375
|
+
}
|
|
203
376
|
async function requirePlanningMemoryCandidate(pending) {
|
|
204
377
|
if (!pending.memoryCandidatePath) {
|
|
205
378
|
return;
|