@codetreker/daynight 0.37.2 → 0.38.0

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.
Files changed (52) hide show
  1. package/README.md +112 -41
  2. package/dist/commands/cleanup-operation.d.ts +3 -0
  3. package/dist/commands/cleanup-operation.js +12 -0
  4. package/dist/commands/cleanup-operation.js.map +1 -0
  5. package/dist/commands/controller/provider-display.d.ts +2 -0
  6. package/dist/commands/controller/provider-display.js +63 -0
  7. package/dist/commands/controller/provider-display.js.map +1 -0
  8. package/dist/commands/controller/runtime.js +4 -2
  9. package/dist/commands/controller/runtime.js.map +1 -1
  10. package/dist/commands/skill/skills/using-daynight/SKILL.md +1 -1
  11. package/dist/commands/skill/skills/using-daynight/references/authorization.md +1 -1
  12. package/dist/commands/skill/skills/using-daynight/references/autopilot-managed-task.md +4 -3
  13. package/dist/commands/skill/skills/using-daynight/references/handoff.md +25 -15
  14. package/dist/commands/skill/skills/using-daynight/references/task-management.md +1 -1
  15. package/dist/commands/skill/usage.md +5 -3
  16. package/dist/commands/task/command.js +3 -1
  17. package/dist/commands/task/command.js.map +1 -1
  18. package/dist/commands/task/definition.js +1 -1
  19. package/dist/commands/task/definition.js.map +1 -1
  20. package/dist/commands/worker/backends/session-api.d.ts +1 -1
  21. package/dist/commands/worker/backends/session-api.js +4 -4
  22. package/dist/commands/worker/backends/session-api.js.map +1 -1
  23. package/dist/commands/worker/backends/web-store.d.ts +4 -0
  24. package/dist/commands/worker/backends/web-store.js +40 -6
  25. package/dist/commands/worker/backends/web-store.js.map +1 -1
  26. package/dist/commands/worker/backends/web.js +248 -127
  27. package/dist/commands/worker/backends/web.js.map +1 -1
  28. package/dist/commands/worker/cleanup-error.d.ts +5 -0
  29. package/dist/commands/worker/cleanup-error.js +9 -0
  30. package/dist/commands/worker/cleanup-error.js.map +1 -0
  31. package/dist/commands/worker/cleanup-schedule.d.ts +3 -0
  32. package/dist/commands/worker/cleanup-schedule.js +11 -0
  33. package/dist/commands/worker/cleanup-schedule.js.map +1 -0
  34. package/dist/commands/worker/cleanup.js +47 -31
  35. package/dist/commands/worker/cleanup.js.map +1 -1
  36. package/dist/commands/worker/coordinator.js +173 -64
  37. package/dist/commands/worker/coordinator.js.map +1 -1
  38. package/dist/commands/worker/execution.d.ts +1 -1
  39. package/dist/commands/worker/execution.js +2 -2
  40. package/dist/commands/worker/execution.js.map +1 -1
  41. package/dist/commands/worker/prompts.js +2 -2
  42. package/dist/commands/worker/prompts.js.map +1 -1
  43. package/dist/commands/worker/runtime.js +1 -1
  44. package/dist/commands/worker/runtime.js.map +1 -1
  45. package/dist/commands/worker/sqlite/006-cleanup-schedules.sql +14 -0
  46. package/dist/commands/worker/store.d.ts +5 -1
  47. package/dist/commands/worker/store.js +53 -0
  48. package/dist/commands/worker/store.js.map +1 -1
  49. package/dist/commands/worker/types.d.ts +10 -0
  50. package/dist/commands/worker/worktrees.js +1 -1
  51. package/dist/commands/worker/worktrees.js.map +1 -1
  52. package/package.json +2 -2
@@ -1,5 +1,8 @@
1
1
  import path from "node:path";
2
- import { actualTaskPane, executionContext, liveAgent, settleWorkerStops, workerOperation } from "./execution.js";
2
+ import { CLEANUP_OPERATION_MS, cleanupOperation } from "../cleanup-operation.js";
3
+ import { ensureCleanupSchedule } from "./cleanup-schedule.js";
4
+ import { WorkerCleanupError } from "./cleanup-error.js";
5
+ import { actualTaskPane, executionContext, liveAgent, settleWorkerStops } from "./execution.js";
3
6
  import { workerCleanupPrompt } from "./prompts.js";
4
7
  const key = (owner) => `${owner.projectId}/${owner.taskId}/${owner.claimId}`;
5
8
  export const createCleanupDriver = (deps) => {
@@ -7,47 +10,60 @@ export const createCleanupDriver = (deps) => {
7
10
  return {
8
11
  advance: async (input, resources, deleteWorktree, signal) => {
9
12
  const identity = key(input.owner);
10
- let attempt = attempts.get(identity);
13
+ let attempt = deleteWorktree ? ensureCleanupSchedule(deps.store, input.owner, deps.now()) : attempts.get(identity);
11
14
  if (attempt === undefined) {
12
15
  attempt = { deadline: deps.now() + 600_000, notified: false, submitted: false };
13
16
  attempts.set(identity, attempt);
14
17
  deps.emit({ kind: "cleanup-started", ...(input.task === undefined ? {} : { task: input.task }), message: `Cleaning claim ${input.owner.claimId}; this attempt has a ten-minute deadline.` });
15
18
  }
16
- const stopContext = { ...input.owner, cwd: path.join(input.project.localPath, ".worktrees", "daynight", input.owner.taskId) };
17
- const stopped = await settleWorkerStops(deps, input.owner, signal);
18
- const pane = stopped ? undefined : actualTaskPane(input.owner, resources);
19
- if (pane !== undefined) {
20
- const agent = input.task?.agent ?? pane.kind ?? pane.nativeSession?.agent;
21
- if (agent === undefined) {
22
- await workerOperation((operation) => deps.herdr.tasks.stopAndClose(stopContext, pane.binding, operation), signal);
23
- }
24
- else {
25
- const context = executionContext({ ...input, task: { agent } });
26
- if (liveAgent(pane) && !attempt.notified && deps.now() < attempt.deadline) {
27
- attempt.notified = true;
28
- try {
29
- await workerOperation((operation) => deps.herdr.tasks.submitPrompt(context, pane.binding, workerCleanupPrompt(input.owner, attempt.deadline), operation), signal, Math.max(1, Math.min(30_000, attempt.deadline - deps.now())));
30
- attempt.submitted = true;
31
- }
32
- catch (error) {
33
- deps.emit({ kind: "cleanup-notification-error", ...(input.task === undefined ? {} : { task: input.task }), message: "Cleanup notification failed; the current bounded cleanup attempt continues.", error });
19
+ const saveAttempt = () => {
20
+ if (deleteWorktree)
21
+ deps.store.saveCleanupSchedule(input.owner, { ...deps.store.getCleanupSchedule(input.owner), deadline: attempt.deadline, notified: attempt.notified, submitted: attempt.submitted });
22
+ };
23
+ let stage = "agent-stop";
24
+ try {
25
+ const stopContext = { ...input.owner, cwd: path.join(input.project.localPath, ".worktrees", "daynight", input.owner.taskId) };
26
+ const stopped = await settleWorkerStops(deps, input.owner, signal, cleanupOperation);
27
+ const pane = stopped ? undefined : actualTaskPane(input.owner, resources);
28
+ if (pane !== undefined) {
29
+ const agent = input.task?.agent ?? pane.kind ?? pane.nativeSession?.agent;
30
+ if (agent === undefined) {
31
+ await cleanupOperation((operation) => deps.herdr.tasks.stopAndClose(stopContext, pane.binding, operation), signal);
32
+ }
33
+ else {
34
+ const context = executionContext({ ...input, task: { agent } });
35
+ if (liveAgent(pane) && !attempt.notified && deps.now() < attempt.deadline) {
36
+ attempt.notified = true;
37
+ saveAttempt();
38
+ try {
39
+ await cleanupOperation((operation) => deps.herdr.tasks.submitPrompt(context, pane.binding, workerCleanupPrompt(input.owner, attempt.deadline), operation), signal, Math.max(1, Math.min(CLEANUP_OPERATION_MS, attempt.deadline - deps.now())));
40
+ attempt.submitted = true;
41
+ saveAttempt();
42
+ }
43
+ catch (error) {
44
+ deps.emit({ kind: "cleanup-notification-error", ...(input.task === undefined ? {} : { task: input.task }), message: "Cleanup notification failed; the current bounded cleanup attempt continues.", error });
45
+ }
46
+ if (deps.now() < attempt.deadline)
47
+ return false;
34
48
  }
35
- if (deps.now() < attempt.deadline)
49
+ const completed = attempt.submitted && pane.status === "idle";
50
+ if (liveAgent(pane) && !completed && deps.now() < attempt.deadline)
36
51
  return false;
52
+ await cleanupOperation((operation) => deps.herdr.tasks.stopAndClose(context, pane.binding, operation), signal);
37
53
  }
38
- const completed = attempt.submitted && pane.status === "idle";
39
- if (liveAgent(pane) && !completed && deps.now() < attempt.deadline)
40
- return false;
41
- await workerOperation((operation) => deps.herdr.tasks.stopAndClose(context, pane.binding, operation), signal);
42
54
  }
55
+ stage = "worktree-removal";
56
+ if (deleteWorktree)
57
+ await cleanupOperation((operation) => deps.git.removeWorktree(input.project, input.owner, operation), signal);
58
+ attempts.delete(identity);
59
+ deps.emit({ kind: deleteWorktree ? "cleaned" : "agent-stopped", ...(input.task === undefined ? {} : { task: input.task }), message: deleteWorktree ? "Stopped the task resources and deleted its worktree; the branch remains." : "Stopped the task resources and preserved its worktree." });
60
+ return true;
61
+ }
62
+ catch (error) {
63
+ throw new WorkerCleanupError(stage, error);
43
64
  }
44
- if (deleteWorktree)
45
- await workerOperation((operation) => deps.git.removeWorktree(input.project, input.owner, operation), signal, 180_000);
46
- attempts.delete(identity);
47
- deps.emit({ kind: deleteWorktree ? "cleaned" : "agent-stopped", ...(input.task === undefined ? {} : { task: input.task }), message: deleteWorktree ? "Stopped the task resources and deleted its worktree; the branch remains." : "Stopped the task resources and preserved its worktree." });
48
- return true;
49
65
  },
50
- remaining: (owner) => Math.max(0, (attempts.get(key(owner))?.deadline ?? deps.now()) - deps.now()),
66
+ remaining: (owner) => Math.max(0, (deps.store.getCleanupSchedule(owner)?.deadline ?? attempts.get(key(owner))?.deadline ?? deps.now()) - deps.now()),
51
67
  };
52
68
  };
53
69
  //# sourceMappingURL=cleanup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../../src/commands/worker/cleanup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../../src/commands/worker/cleanup.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,12 @@
1
+ import { CLEANUP_OPERATION_MS, cleanupOperation } from "../cleanup-operation.js";
1
2
  import { readWorkerClaim } from "../../api/task-properties.js";
2
3
  import { requireImplementedWorkerBackend } from "./backend.js";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import path from "node:path";
5
6
  import { setTimeout as delay } from "node:timers/promises";
6
7
  import { OperationError } from "../../api/operation.js";
8
+ import { ensureCleanupSchedule } from "./cleanup-schedule.js";
9
+ import { WorkerCleanupError } from "./cleanup-error.js";
7
10
  import { createCleanupDriver } from "./cleanup.js";
8
11
  import { actualTaskPane, createExecutionDriver, executionContext, isWorkerCancellation, liveAgent, sameClaim, settleWorkerStops, workerOperation } from "./execution.js";
9
12
  import { HerdrAgentActionError, HerdrRpcError } from "./herdr.js";
@@ -24,7 +27,9 @@ export const createWorkerCoordinator = (options) => {
24
27
  const tails = new Map();
25
28
  const jobs = new Map();
26
29
  const controllers = new Map();
27
- const ending = new Map();
30
+ const ending = new Map(options.store.listCleanupSchedules().map(record => [key(record), record]));
31
+ let capacityWarning = "";
32
+ const cleanupRetries = new Map();
28
33
  const failures = new Map();
29
34
  const snapshotWarnings = new Map();
30
35
  let nextRepository = 0;
@@ -72,10 +77,12 @@ export const createWorkerCoordinator = (options) => {
72
77
  const current = options.repositories.find((repository) => repository.project.projectId === task.projectId)?.configuration ?? options.globalConfiguration;
73
78
  return current.agent === task.agent ? { ...current, agentControlMode: task.agentControlMode ?? "herdr" } : { agent: task.agent, agentControlMode: task.agentControlMode ?? "herdr", model: null, args: [], recoveryLimit: current.recoveryLimit };
74
79
  };
80
+ // Cleanup-only backend access stays inside its claim job and retry schedule.
75
81
  const needsHerdr = () => options.globalConfiguration.agentControlMode !== "web"
76
82
  || options.repositories.some(repository => repository.configuration.agentControlMode !== "web")
77
- || options.store.listUnfinishedTasks().some(task => task.agentControlMode !== "web");
78
- const pendingStops = () => needsHerdr() ? options.herdr.tasks.pendingStops() : [];
83
+ || options.store.listUnfinishedTasks().some(task => task.agentControlMode !== "web" && !ending.has(key(task)));
84
+ const hasHerdrCleanup = () => [...ending.values()].some(owner => options.store.getTask(owner)?.agentControlMode !== "web" && options.webStore?.getTask(owner) == null);
85
+ const pendingStops = () => needsHerdr() || hasHerdrCleanup() ? options.herdr.tasks.pendingStops() : [];
79
86
  const ensureHerdr = async (signal) => { if (needsHerdr())
80
87
  await workerOperation(operation => options.herdr.ensureInstance(operation), signal); };
81
88
  const listHerdr = async (signal) => needsHerdr() ? workerOperation(operation => options.herdr.tasks.listResources(operation), signal) : [];
@@ -164,10 +171,42 @@ export const createWorkerCoordinator = (options) => {
164
171
  const job = serialize(identity, () => action(signal)).catch((error) => {
165
172
  if (isWorkerCancellation(error, signal))
166
173
  return;
167
- if (!terminal)
174
+ let cleanupMessage;
175
+ let reportedError = error;
176
+ let task = null;
177
+ if (terminal) {
178
+ const owner = ending.get(identityKey);
179
+ let count = cleanupRetries.get(identityKey)?.failures ?? 0;
180
+ let previous = null;
181
+ try {
182
+ previous = options.store.getCleanupSchedule(owner);
183
+ count = Math.max(count, previous?.failures ?? 0);
184
+ }
185
+ catch (storageError) {
186
+ reportedError = new AggregateError([error, storageError], "Cleanup failed and its retry state could not be read.");
187
+ }
188
+ count = Math.min(count + 1, Number.MAX_SAFE_INTEGER);
189
+ const retryMs = Math.min(60_000, 1000 * 2 ** Math.min(count - 1, 6));
190
+ const stage = error instanceof WorkerCleanupError ? error.stage : "observation";
191
+ const nextAttemptAt = now() + retryMs;
192
+ // Storage failures must not reject the job or prevent shutdown from proving stop.
193
+ cleanupRetries.set(identityKey, { failures: count, nextAttemptAt });
194
+ try {
195
+ previous ??= ensureCleanupSchedule(options.store, owner, now());
196
+ options.store.saveCleanupSchedule(previous, { ...previous, failures: count, nextAttemptAt, stage, lastError: error instanceof Error ? error.message || error.name : String(error) || "Unknown cleanup error" });
197
+ task = options.store.getTask(identity);
198
+ }
199
+ catch (storageError) {
200
+ reportedError = new AggregateError([reportedError, storageError], "Cleanup failed; retry persistence is unconfirmed and in-process backoff remains active.");
201
+ }
202
+ const stopStatus = stage === "archive" || stage === "worktree-removal" || stage === "backend-close" ? "Agent stop confirmed; " : "";
203
+ cleanupMessage = `Cleanup ${key(owner)} failed at ${stage}; ${stopStatus}next retry ${new Date(nextAttemptAt).toISOString()} (${retryMs} ms). Resources retain capacity; correct the reported cause and normal reconciliation retries automatically.`;
204
+ }
205
+ else {
168
206
  failures.set(identityKey, (failures.get(identityKey) ?? 0) + 1);
169
- const task = options.store.getTask(identity);
170
- options.emit({ kind: terminal ? "cleanup-error" : "execution-error", ...(task === null ? {} : { task }), message: terminal ? "The resource cleanup failed and will be retried from actual observations." : "The task operation failed; this process retains its recovery count.", error });
207
+ task = options.store.getTask(identity);
208
+ }
209
+ options.emit({ kind: terminal ? "cleanup-error" : "execution-error", ...(task === null ? {} : { task }), message: terminal ? cleanupMessage : "The task operation failed; this process retains its recovery count.", error: reportedError });
171
210
  }).finally(() => { if (jobs.get(identityKey) === job) {
172
211
  jobs.delete(identityKey);
173
212
  controllers.delete(identityKey);
@@ -187,8 +226,11 @@ export const createWorkerCoordinator = (options) => {
187
226
  return options.store.registerPending({ ...owner, agent, sessionId: pane !== undefined && liveAgent(pane) && pane.nativeSession !== null ? pane.nativeSession.sessionId : null });
188
227
  };
189
228
  const runCleanup = async (owner, deleteWorktree, signal) => {
229
+ const schedule = deleteWorktree ? ensureCleanupSchedule(options.store, owner, now()) : null;
190
230
  const local = options.store.getTask(owner);
191
231
  const webRecord = options.webStore?.getTask(owner);
232
+ if (webRecord != null && webRecord.worktreeId !== owner.worktreeId)
233
+ throw new Error("The Web cleanup claim has conflicting worktree identities.");
192
234
  if (local?.agentControlMode === "web" || webRecord != null) {
193
235
  if (!options.webBackend || !options.webStore)
194
236
  throw new Error("The Web execution backend is unavailable.");
@@ -198,9 +240,9 @@ export const createWorkerCoordinator = (options) => {
198
240
  throw new Error("The original native identity has no local Web stop evidence.");
199
241
  if (!options.verifyWebUnregistered)
200
242
  throw new Error("Web launch provenance cannot be verified.");
201
- await options.verifyWebUnregistered(owner, signal ?? new AbortController().signal);
243
+ await options.verifyWebUnregistered(owner, signal ?? new AbortController().signal, CLEANUP_OPERATION_MS);
202
244
  if (deleteWorktree) {
203
- await workerOperation(operation => options.git.removeWorktree(projectFor(owner), owner, operation), signal);
245
+ await cleanupOperation(operation => options.git.removeWorktree(projectFor(owner), owner, operation), signal);
204
246
  if (local)
205
247
  options.store.completeTask(owner);
206
248
  }
@@ -212,7 +254,7 @@ export const createWorkerCoordinator = (options) => {
212
254
  if (!webRecord.cleanup?.agentStopped || !webRecord.cleanup.backendResourcesClosed || !webRecord.cleanup.worktreeDeleted)
213
255
  throw new Error("The completed Web execution has incomplete cleanup evidence.");
214
256
  if (deleteWorktree) {
215
- const retained = await workerOperation(operation => options.git.findWorktree(projectFor(owner), owner, operation), signal);
257
+ const retained = await cleanupOperation(operation => options.git.findWorktree(projectFor(owner), owner, operation), signal);
216
258
  if (retained !== null)
217
259
  throw new Error("The completed Web execution still has a worktree; its resources were retained.");
218
260
  if (local !== null && local.status !== "done")
@@ -221,7 +263,7 @@ export const createWorkerCoordinator = (options) => {
221
263
  return;
222
264
  }
223
265
  while (true) {
224
- const result = await options.webBackend.cleanup(webRecord, deleteWorktree ? "terminal" : "shutdown", now() + 600_000, signal);
266
+ const result = await options.webBackend.cleanup(webRecord, deleteWorktree ? "terminal" : "shutdown", schedule?.deadline ?? now() + 600_000, signal);
225
267
  if (result.cleanup?.agentStopped && result.cleanup.backendResourcesClosed && (!deleteWorktree || result.cleanup.worktreeDeleted)) {
226
268
  if (deleteWorktree) {
227
269
  options.webStore.completeTask(owner, now());
@@ -233,8 +275,9 @@ export const createWorkerCoordinator = (options) => {
233
275
  await delay(1000, undefined, signal === undefined ? {} : { signal });
234
276
  }
235
277
  }
278
+ await cleanupOperation(operation => options.herdr.ensureInstance(operation), signal);
236
279
  while (true) {
237
- const resources = await workerOperation((operation) => options.herdr.tasks.listResources(operation), signal);
280
+ const resources = await cleanupOperation((operation) => options.herdr.tasks.listResources(operation), signal);
238
281
  const task = recordMissingClaim(owner, actualTaskPane(owner, resources));
239
282
  const completed = await cleanup.advance({ owner, project: projectFor(owner), ...(task === null ? {} : { task }) }, resources, deleteWorktree, signal);
240
283
  if (completed) {
@@ -249,7 +292,22 @@ export const createWorkerCoordinator = (options) => {
249
292
  for (const [identityKey, owner] of ending) {
250
293
  if (jobs.has(identityKey))
251
294
  continue;
252
- startJob(owner, async (signal) => { await runCleanup(owner, true, signal); ending.delete(identityKey); }, true);
295
+ if (now() < (cleanupRetries.get(identityKey)?.nextAttemptAt ?? 0))
296
+ continue;
297
+ // Creation and ownership validation run inside the isolated job's error boundary.
298
+ let nextAttemptAt = 0;
299
+ try {
300
+ nextAttemptAt = options.store.getCleanupSchedule(owner)?.nextAttemptAt ?? 0;
301
+ }
302
+ catch { /* The job records the storage failure without stopping other claims. */ }
303
+ if (now() < nextAttemptAt)
304
+ continue;
305
+ startJob(owner, async (signal) => {
306
+ await runCleanup(owner, true, signal);
307
+ options.store.deleteCleanupSchedule(owner);
308
+ ending.delete(identityKey);
309
+ cleanupRetries.delete(identityKey);
310
+ }, true);
253
311
  }
254
312
  };
255
313
  const syncWorkspaceName = async (task, owner, title, signal) => {
@@ -285,37 +343,63 @@ export const createWorkerCoordinator = (options) => {
285
343
  if (!sameClaim(task, owner) || readWorkerClaim(remoteTask.properties).workerId !== options.workerId || readWorkerClaim(remoteTask.properties).agent !== task.agent || readWorkerClaim(remoteTask.properties).agentControlMode !== (task.agentControlMode ?? "herdr"))
286
344
  throw new Error("The local execution does not match the observed original worker claim.");
287
345
  if (task.agentControlMode === "web") {
288
- if (!execute)
289
- return;
290
346
  if (!options.webStore || !options.webBackend)
291
347
  throw new Error("The Web execution backend is unavailable.");
292
348
  const configuration = configurationFor(task);
293
- requireImplementedWorkerBackend(configuration);
294
- await options.verifyWebServer?.(configuration.agent, signal);
295
- if (options.webStore.getTask(task) === null) {
296
- if (!options.verifyWebUnregistered)
297
- throw new Error("Web launch provenance cannot be verified.");
298
- await options.verifyWebUnregistered(task, signal);
299
- if (task.sessionId !== null || readWorkerClaim(remoteTask.properties).nativeSessionId !== null)
300
- throw new Error("The original native identity has no local Web execution evidence.");
301
- }
302
- let record = options.webStore.register({ ...owner, project: projectFor(task), serverOrigin: options.serverOrigin, workerId: options.workerId, userId: readWorkerClaim(remoteTask.properties).userId, configuration }, now());
303
- if (record.cleanup !== null)
304
- record = options.webStore.clearShutdown(record, now());
305
- const result = await options.webBackend.advance(record, signal);
306
- const nativeSessionId = result.resources.nativeSession?.sessionId ?? task.sessionId;
307
- if (nativeSessionId !== null)
308
- options.store.setSessionId(task, nativeSessionId);
309
- const phase = "executing";
310
349
  const worker = readWorkerClaim(remoteTask.properties);
311
- if (worker.phase !== phase || worker.nativeSessionId !== nativeSessionId || worker.herdr !== null) {
312
- try {
313
- await options.remote.update(remoteTask, { phase, nativeSessionId, herdr: null }, signal);
350
+ let record = options.webStore.getTask(task);
351
+ if (record !== null && (record.worktreeId !== owner.worktreeId || record.workerId !== options.workerId || record.userId !== worker.userId || record.serverOrigin !== options.serverOrigin))
352
+ throw new Error("The durable Web execution does not match the observed original worker claim.");
353
+ let observed = false;
354
+ try {
355
+ if (!execute) {
356
+ if (record !== null && record.recovery.pauseReason === null)
357
+ record = options.webStore.pauseRecovery(record, "The Web execution failure limit was reached; explicit retry is required.", now());
314
358
  }
315
- catch (error) {
316
- if (isWorkerCancellation(error, signal))
317
- throw error;
318
- options.emit({ kind: "summary-error", task, message: "The Web execution summary was not confirmed; the next round will read it again.", error });
359
+ else if (record === null || record.recovery.pauseReason === null || record.recovery.retryAuthorized) {
360
+ requireImplementedWorkerBackend(configuration);
361
+ await options.verifyWebServer?.(configuration.agent, signal);
362
+ if (record === null) {
363
+ if (!options.verifyWebUnregistered)
364
+ throw new Error("Web launch provenance cannot be verified.");
365
+ await options.verifyWebUnregistered(task, signal);
366
+ if (task.sessionId !== null || worker.nativeSessionId !== null)
367
+ throw new Error("The original native identity has no local Web execution evidence.");
368
+ }
369
+ record = options.webStore.register({ ...owner, project: projectFor(task), serverOrigin: options.serverOrigin, workerId: options.workerId, userId: worker.userId, configuration }, now());
370
+ if (record.cleanup !== null)
371
+ record = options.webStore.clearShutdown(record, now());
372
+ record = await options.webBackend.advance(record, signal);
373
+ observed = true;
374
+ }
375
+ }
376
+ catch (error) {
377
+ if (isWorkerCancellation(error, signal))
378
+ throw error;
379
+ record = options.webStore.getTask(task);
380
+ if (record !== null && record.cleanup === null && (record.recovery.retryAuthorized || (failures.get(key(task)) ?? 0) + 1 > configuration.recoveryLimit)) {
381
+ record = options.webStore.pauseRecovery(record, error instanceof Error ? error.message : String(error), now());
382
+ }
383
+ throw error;
384
+ }
385
+ finally {
386
+ // Summary transport is independent of native recovery and must remain live while paused.
387
+ record = options.webStore.getTask(task);
388
+ if (!signal.aborted && (observed || !execute || record?.recovery.pauseReason != null)) {
389
+ const nativeSessionId = record?.resources.nativeSession?.sessionId ?? task.sessionId ?? worker.nativeSessionId;
390
+ if (nativeSessionId !== null)
391
+ options.store.setSessionId(task, nativeSessionId);
392
+ const phase = !execute || record?.recovery.pauseReason != null || record?.recovery.retryAuthorized
393
+ || record?.webExecution?.activity === "initializing" ? "waiting" : "executing";
394
+ if (worker.phase !== phase || worker.nativeSessionId !== nativeSessionId || worker.herdr !== null) {
395
+ try {
396
+ await options.remote.update(remoteTask, { phase, nativeSessionId, herdr: null }, signal);
397
+ }
398
+ catch (error) {
399
+ if (!isWorkerCancellation(error, signal))
400
+ options.emit({ kind: "summary-error", task, message: "The Web execution summary was not confirmed; the next round will read it again.", error });
401
+ }
402
+ }
319
403
  }
320
404
  }
321
405
  return;
@@ -379,11 +463,11 @@ export const createWorkerCoordinator = (options) => {
379
463
  const owner = resources.owners.get(key(local));
380
464
  if (owner !== undefined)
381
465
  ending.set(key(local), owner);
382
- else if (!jobs.has(key(local))) {
466
+ else if (!jobs.has(key(local)) && !ending.has(key(local))) {
383
467
  if (local.agentControlMode === "web") {
384
468
  if (!options.verifyWebUnregistered || local.sessionId !== null)
385
469
  throw new Error("The ended Web claim has no original resource evidence; its ownership record is retained.");
386
- await options.verifyWebUnregistered(local, normalSignal);
470
+ await options.verifyWebUnregistered(local, normalSignal, CLEANUP_OPERATION_MS);
387
471
  }
388
472
  options.store.completeTask(local);
389
473
  }
@@ -413,6 +497,14 @@ export const createWorkerCoordinator = (options) => {
413
497
  if (!started)
414
498
  break;
415
499
  }
500
+ const blocked = capacity(resources) >= options.maxConcurrent
501
+ ? [...ending.keys()].filter(identity => options.store.getCleanupSchedule(ending.get(identity))?.lastError != null).sort() : [];
502
+ const warning = blocked.join(",");
503
+ if (warning !== capacityWarning) {
504
+ capacityWarning = warning;
505
+ if (warning !== "")
506
+ options.emit({ kind: "cleanup-capacity-blocked", message: `Worker capacity is full; failed cleanup retains ${blocked.length} slot(s). ${blocked.map(identity => { const state = options.store.getCleanupSchedule(ending.get(identity)); return `${identity}: ${state.stage}, next retry ${new Date(state.nextAttemptAt).toISOString()}, ${state.lastError}`; }).join("; ")}. Correct the reported resource or archive failure; retries are automatic. No slot is released until cleanup is confirmed.` });
507
+ }
416
508
  if (capacity(resources) >= options.maxConcurrent || options.repositories.length === 0)
417
509
  return;
418
510
  const candidates = new Map();
@@ -485,21 +577,36 @@ export const createWorkerCoordinator = (options) => {
485
577
  const { task, remoteTask } = await readSource(identity);
486
578
  if (task.status !== "active")
487
579
  throw new Error("Only an active original claim may be retried; pending work is started by worker run.");
580
+ const webRecord = task.agentControlMode === "web" ? options.webStore?.getTask(task) : null;
581
+ const worker = readWorkerClaim(remoteTask.properties);
582
+ if (task.agentControlMode === "web" && !webRecord)
583
+ throw new Error("Retry requires the original Web execution evidence; it cannot start a replacement assignment.");
584
+ if (webRecord && (webRecord.worktreeId !== worker.worktreeId || webRecord.workerId !== worker.workerId || webRecord.userId !== worker.userId || webRecord.serverOrigin !== options.serverOrigin))
585
+ throw new Error("The original Web execution ownership no longer matches the active claim.");
488
586
  failures.delete(key(identity));
489
- if (task.agentControlMode === "web" && options.webStore?.getTask(task))
490
- options.webStore.authorizeRecoveryRetry(task, now());
491
587
  await ensureHerdr(normalSignal);
492
- await serialize(identity, () => ensure(task, remoteTask, normalSignal));
588
+ await serialize(identity, async () => {
589
+ if (task.agentControlMode === "web" && options.webStore) {
590
+ let record = options.webStore.getTask(task);
591
+ if (record !== null) {
592
+ if (record.cleanup !== null)
593
+ record = options.webStore.clearShutdown(record, now());
594
+ options.webStore.authorizeRecoveryRetry(record, now());
595
+ }
596
+ }
597
+ await ensure(task, remoteTask, normalSignal);
598
+ });
493
599
  },
494
600
  cleanupSource: async (identity, settings = {}) => {
495
601
  const { owner } = await readSource(identity);
496
602
  await ensureHerdr(normalSignal);
497
- await serialize(identity, () => runCleanup(owner, settings.deleteWorktree === true, normalSignal));
603
+ await serialize(identity, async () => { await runCleanup(owner, settings.deleteWorktree === true, normalSignal); if (settings.deleteWorktree === true)
604
+ options.store.deleteCleanupSchedule(owner); });
498
605
  return owner;
499
606
  },
500
607
  completeSource: async (owner) => {
501
608
  await ensureHerdr(normalSignal);
502
- await serialize(owner, () => runCleanup(owner, true, normalSignal));
609
+ await serialize(owner, async () => { await runCleanup(owner, true, normalSignal); options.store.deleteCleanupSchedule(owner); });
503
610
  },
504
611
  shutdown: async () => {
505
612
  cancel();
@@ -510,7 +617,7 @@ export const createWorkerCoordinator = (options) => {
510
617
  if (task.agentControlMode === "web" && options.webStore?.getTask(task) == null) {
511
618
  if (task.status === "pending" && task.sessionId === null && options.verifyWebUnregistered) {
512
619
  try {
513
- await options.verifyWebUnregistered(task, new AbortController().signal);
620
+ await options.verifyWebUnregistered(task, new AbortController().signal, CLEANUP_OPERATION_MS);
514
621
  continue;
515
622
  }
516
623
  catch { /* Missing launch evidence must not be interpreted as a confirmed native stop. */ }
@@ -528,7 +635,11 @@ export const createWorkerCoordinator = (options) => {
528
635
  owners.set(key(context), context);
529
636
  }
530
637
  try {
531
- const workspaces = await listHerdr();
638
+ let workspaces = [];
639
+ if (needsHerdr() || hasHerdrCleanup()) {
640
+ await cleanupOperation(operation => options.herdr.ensureInstance(operation));
641
+ workspaces = await cleanupOperation(operation => options.herdr.tasks.listResources(operation));
642
+ }
532
643
  for (const workspace of workspaces) {
533
644
  if (options.store.getProject(workspace.owner) === null) {
534
645
  complete = false;
@@ -542,23 +653,21 @@ export const createWorkerCoordinator = (options) => {
542
653
  complete = false;
543
654
  options.emit({ kind: "shutdown-error", message: "Could not inspect every owned Herdr workspace during shutdown.", error });
544
655
  }
545
- await Promise.all(options.store.listProjects().map(async (project) => {
546
- try {
547
- const resources = await snapshot(undefined, project);
548
- for (const [identity, owner] of resources.owners)
549
- if (resources.proven.has(identity))
550
- owners.set(identity, owner);
551
- }
552
- catch (error) {
553
- complete = false;
554
- options.emit({ kind: "shutdown-error", message: `Could not observe owned resources in project ${project.projectId} during shutdown.`, error });
555
- }
556
- }));
656
+ // Git inventory cannot prove whether an agent stopped. Terminal deletion stays pending.
657
+ for (const record of options.webStore?.listUnfinishedTasks() ?? [])
658
+ owners.set(key(record), record);
557
659
  await Promise.all([...owners.values()].map(async (owner) => {
558
660
  try {
559
661
  await serialize(owner, async () => {
560
662
  const webRecord = options.webStore?.getTask(owner);
561
663
  if (webRecord != null) {
664
+ if (webRecord.worktreeId !== owner.worktreeId)
665
+ throw new Error("The Web cleanup claim has conflicting worktree identities.");
666
+ if (webRecord.status === "done") {
667
+ if (!webRecord.cleanup?.agentStopped || !webRecord.cleanup.backendResourcesClosed || !webRecord.cleanup.worktreeDeleted)
668
+ throw new Error("The completed Web execution has incomplete cleanup evidence.");
669
+ return;
670
+ }
562
671
  if (!options.webBackend)
563
672
  throw new Error("The Web execution backend is unavailable during shutdown.");
564
673
  await options.webBackend.cleanup(webRecord, "shutdown", now(), undefined, true);
@@ -567,21 +676,21 @@ export const createWorkerCoordinator = (options) => {
567
676
  if (options.store.getTask(owner)?.agentControlMode === "web")
568
677
  return;
569
678
  if (options.store.getTask(owner) === null) {
570
- const observed = await workerOperation((operation) => options.herdr.tasks.listResources(operation));
679
+ const observed = await cleanupOperation((operation) => options.herdr.tasks.listResources(operation));
571
680
  recordMissingClaim(owner, actualTaskPane(owner, observed));
572
681
  }
573
- await settleWorkerStops(deps, owner);
574
- const resources = await workerOperation((operation) => options.herdr.tasks.listResources(operation));
682
+ await settleWorkerStops(deps, owner, undefined, cleanupOperation);
683
+ const resources = await cleanupOperation((operation) => options.herdr.tasks.listResources(operation));
575
684
  const pane = actualTaskPane(owner, resources);
576
685
  if (pane === undefined)
577
686
  return;
578
687
  const context = { ...owner, cwd: path.join(projectFor(owner).localPath, ".worktrees", "daynight", owner.taskId) };
579
- await workerOperation((operation) => options.herdr.tasks.stopAndClose(context, pane.binding, operation));
688
+ await cleanupOperation((operation) => options.herdr.tasks.stopAndClose(context, pane.binding, operation));
580
689
  });
581
690
  }
582
691
  catch (error) {
583
692
  complete = false;
584
- options.emit({ kind: "shutdown-error", ...(options.store.getTask(owner) === null ? {} : { task: options.store.getTask(owner) }), message: "Could not stop the original task resources.", error });
693
+ options.emit({ kind: "shutdown-error", ...(options.store.getTask(owner) === null ? {} : { task: options.store.getTask(owner) }), message: error instanceof WorkerCleanupError && error.stage === "backend-close" ? "The original Agent is stopped, but backend resource closure is unconfirmed." : "Could not confirm the original task resources stopped and closed.", error });
585
694
  }
586
695
  }));
587
696
  return complete;