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 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) {
@@ -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
- const existing = pendingByTask.get(key);
28
- if (existing?.sessionId === session.id) {
29
- if (existing.status === "blocked") {
30
- existing.status = "pending";
31
- existing.blocker = undefined;
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: session.id,
36
- status: "scheduled",
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
- return {
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
- sessionId: session.id,
45
- status: "already_scheduled",
46
- ...(existing.memoryCandidatePath
47
- ? { memoryCandidatePath: existing.memoryCandidatePath }
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
- const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
52
- ? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
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.sessionId,
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 pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
87
- if (!pending || pending.sessionId !== sessionId || pending.status === "blocked") {
88
- return;
89
- }
90
- pending.stopped = true;
91
- await tryRestart(pending);
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 pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
98
- if (!pending || pending.status === "blocked") {
99
- return;
100
- }
101
- pending.deliveredMessageId = message.id;
102
- await tryRestart(pending);
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 pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
109
- if (!pending || pending.status === "blocked") {
110
- return;
111
- }
112
- pending.acceptedMessageId = message.id;
113
- await tryRestart(pending);
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 pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
117
- if (!pending || pending.status === "blocked") {
118
- return;
119
- }
120
- pending.gateAccepted = accepted;
121
- await tryRestart(pending);
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
- pendingByTask.delete(taskKey(repoRoot, taskSlug));
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
- || session.id !== pending.sessionId) {
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: session.permissionMode,
184
- model: session.model,
185
- effort: session.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
- pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
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: new Date().toISOString()
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;