@codetreker/daynight 0.32.1 → 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 (117) hide show
  1. package/README.md +266 -68
  2. package/dist/api/client.d.ts +7 -1
  3. package/dist/api/client.js +117 -21
  4. package/dist/api/client.js.map +1 -1
  5. package/dist/api/json-source.d.ts +3 -0
  6. package/dist/api/json-source.js +113 -0
  7. package/dist/api/json-source.js.map +1 -0
  8. package/dist/api/parse.d.ts +2 -1
  9. package/dist/api/parse.js +16 -1
  10. package/dist/api/parse.js.map +1 -1
  11. package/dist/api/thread.d.ts +6 -0
  12. package/dist/api/thread.js +79 -0
  13. package/dist/api/thread.js.map +1 -0
  14. package/dist/api/types.d.ts +50 -0
  15. package/dist/cli/application.js +12 -2
  16. package/dist/cli/application.js.map +1 -1
  17. package/dist/cli/context.d.ts +9 -1
  18. package/dist/cli/context.js +3 -1
  19. package/dist/cli/context.js.map +1 -1
  20. package/dist/cli/schema.js +10 -10
  21. package/dist/cli/schema.js.map +1 -1
  22. package/dist/commands/cleanup-operation.d.ts +3 -0
  23. package/dist/commands/cleanup-operation.js +12 -0
  24. package/dist/commands/cleanup-operation.js.map +1 -0
  25. package/dist/commands/controller/provider-display.d.ts +2 -0
  26. package/dist/commands/controller/provider-display.js +63 -0
  27. package/dist/commands/controller/provider-display.js.map +1 -0
  28. package/dist/commands/controller/providers/projection.js +12 -0
  29. package/dist/commands/controller/providers/projection.js.map +1 -1
  30. package/dist/commands/controller/runtime.js +4 -2
  31. package/dist/commands/controller/runtime.js.map +1 -1
  32. package/dist/commands/server/runtime.d.ts +10 -1
  33. package/dist/commands/server/runtime.js +24 -7
  34. package/dist/commands/server/runtime.js.map +1 -1
  35. package/dist/commands/skill/skills/using-daynight/SKILL.md +5 -3
  36. package/dist/commands/skill/skills/using-daynight/references/authorization.md +2 -2
  37. package/dist/commands/skill/skills/using-daynight/references/autopilot-managed-task.md +4 -3
  38. package/dist/commands/skill/skills/using-daynight/references/handbook.md +4 -1
  39. package/dist/commands/skill/skills/using-daynight/references/handoff.md +25 -15
  40. package/dist/commands/skill/skills/using-daynight/references/task-content.md +7 -4
  41. package/dist/commands/skill/skills/using-daynight/references/task-management.md +56 -6
  42. package/dist/commands/skill/skills/using-daynight/references/write-safety.md +22 -3
  43. package/dist/commands/skill/usage.md +84 -7
  44. package/dist/commands/task/command.js +28 -3
  45. package/dist/commands/task/command.js.map +1 -1
  46. package/dist/commands/task/definition.js +40 -6
  47. package/dist/commands/task/definition.js.map +1 -1
  48. package/dist/commands/task/rendering.js +4 -0
  49. package/dist/commands/task/rendering.js.map +1 -1
  50. package/dist/commands/task/thread-diagnostics.d.ts +5 -0
  51. package/dist/commands/task/thread-diagnostics.js +32 -0
  52. package/dist/commands/task/thread-diagnostics.js.map +1 -0
  53. package/dist/commands/task/thread-rendering.d.ts +8 -0
  54. package/dist/commands/task/thread-rendering.js +80 -0
  55. package/dist/commands/task/thread-rendering.js.map +1 -0
  56. package/dist/commands/task/thread.d.ts +11 -0
  57. package/dist/commands/task/thread.js +74 -0
  58. package/dist/commands/task/thread.js.map +1 -0
  59. package/dist/commands/worker/backends/session-api.d.ts +1 -1
  60. package/dist/commands/worker/backends/session-api.js +4 -4
  61. package/dist/commands/worker/backends/session-api.js.map +1 -1
  62. package/dist/commands/worker/backends/web-store.d.ts +4 -0
  63. package/dist/commands/worker/backends/web-store.js +40 -6
  64. package/dist/commands/worker/backends/web-store.js.map +1 -1
  65. package/dist/commands/worker/backends/web.js +248 -127
  66. package/dist/commands/worker/backends/web.js.map +1 -1
  67. package/dist/commands/worker/cleanup-error.d.ts +5 -0
  68. package/dist/commands/worker/cleanup-error.js +9 -0
  69. package/dist/commands/worker/cleanup-error.js.map +1 -0
  70. package/dist/commands/worker/cleanup-schedule.d.ts +3 -0
  71. package/dist/commands/worker/cleanup-schedule.js +11 -0
  72. package/dist/commands/worker/cleanup-schedule.js.map +1 -0
  73. package/dist/commands/worker/cleanup.js +47 -31
  74. package/dist/commands/worker/cleanup.js.map +1 -1
  75. package/dist/commands/worker/coordinator.js +186 -63
  76. package/dist/commands/worker/coordinator.js.map +1 -1
  77. package/dist/commands/worker/execution.d.ts +1 -1
  78. package/dist/commands/worker/execution.js +2 -2
  79. package/dist/commands/worker/execution.js.map +1 -1
  80. package/dist/commands/worker/prompts.js +2 -2
  81. package/dist/commands/worker/prompts.js.map +1 -1
  82. package/dist/commands/worker/remote.js +3 -4
  83. package/dist/commands/worker/remote.js.map +1 -1
  84. package/dist/commands/worker/remove-worktree-contents.d.ts +3 -0
  85. package/dist/commands/worker/remove-worktree-contents.js +106 -0
  86. package/dist/commands/worker/remove-worktree-contents.js.map +1 -0
  87. package/dist/commands/worker/runtime.js +1 -1
  88. package/dist/commands/worker/runtime.js.map +1 -1
  89. package/dist/commands/worker/sqlite/006-cleanup-schedules.sql +14 -0
  90. package/dist/commands/worker/store.d.ts +5 -1
  91. package/dist/commands/worker/store.js +53 -0
  92. package/dist/commands/worker/store.js.map +1 -1
  93. package/dist/commands/worker/types.d.ts +10 -0
  94. package/dist/commands/worker/worktrees.js +70 -24
  95. package/dist/commands/worker/worktrees.js.map +1 -1
  96. package/dist/platform/diagnostics.d.ts +5 -0
  97. package/dist/platform/diagnostics.js +12 -0
  98. package/dist/platform/diagnostics.js.map +1 -1
  99. package/dist/session/codex-subagent-activity.d.ts +4 -0
  100. package/dist/session/codex-subagent-activity.js +13 -0
  101. package/dist/session/codex-subagent-activity.js.map +1 -0
  102. package/dist/session/v2/archive.d.ts +6 -0
  103. package/dist/session/v2/archive.js +163 -0
  104. package/dist/session/v2/archive.js.map +1 -0
  105. package/dist/session/v2/protocol.d.ts +19 -0
  106. package/dist/session/v2/protocol.js +260 -0
  107. package/dist/session/v2/protocol.js.map +1 -0
  108. package/dist/session/v2/responsibility.d.ts +3 -0
  109. package/dist/session/v2/responsibility.js +12 -0
  110. package/dist/session/v2/responsibility.js.map +1 -0
  111. package/dist/session/v2/types.d.ts +293 -0
  112. package/dist/session/v2/types.js +2 -0
  113. package/dist/session/v2/types.js.map +1 -0
  114. package/dist/session/v2/values.d.ts +59 -0
  115. package/dist/session/v2/values.js +201 -0
  116. package/dist/session/v2/values.js.map +1 -0
  117. package/package.json +5 -3
@@ -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":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAKnD,MAAM,GAAG,GAAG,CAAC,KAAoB,EAAU,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;AAEpG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,IAA6C,EAAE,EAAE;IACnF,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA0B,CAAC;IACnD,OAAO;QACL,OAAO,EAAE,KAAK,EAAE,KAAmB,EAAE,SAAyC,EAAE,cAAuB,EAAE,MAAoB,EAAoB,EAAE;YACjJ,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAClC,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACrC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,OAAO,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;gBAChF,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAChC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,kBAAkB,KAAK,CAAC,KAAK,CAAC,OAAO,2CAA2C,EAAE,CAAC,CAAC;YAC/L,CAAC;YACD,MAAM,WAAW,GAAG,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9H,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACnE,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YAC1E,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,MAAM,KAAK,GAA4B,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC;gBACnG,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACxB,MAAM,eAAe,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;gBACpH,CAAC;qBAAM,CAAC;oBACN,MAAM,OAAO,GAAG,gBAAgB,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;oBAChE,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;wBAC1E,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;wBACxB,IAAI,CAAC;4BACH,MAAM,eAAe,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,KAAK,EAAE,OAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;4BACjO,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;wBAC3B,CAAC;wBAAC,OAAO,KAAK,EAAE,CAAC;4BACf,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,4BAA4B,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,6EAA6E,EAAE,KAAK,EAAE,CAAC,CAAC;wBAC9M,CAAC;wBACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,QAAQ;4BAAE,OAAO,KAAK,CAAC;oBAClD,CAAC;oBACD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC;oBAC9D,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,QAAQ;wBAAE,OAAO,KAAK,CAAC;oBACjF,MAAM,eAAe,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;gBAChH,CAAC;YACH,CAAC;YACD,IAAI,cAAc;gBAAE,MAAM,eAAe,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;YAC1I,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,0EAA0E,CAAC,CAAC,CAAC,wDAAwD,EAAE,CAAC,CAAC;YAC9R,OAAO,IAAI,CAAC;QACd,CAAC;QACD,SAAS,EAAE,CAAC,KAAoB,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;KAC1H,CAAC;AACJ,CAAC,CAAC"}
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,16 +240,30 @@ 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
  }
207
249
  return;
208
250
  }
251
+ if (webRecord.status === "done") {
252
+ // A resource snapshot can finish after the cleanup job it observed.
253
+ // Completed stop evidence is reusable, but present Git resources still require attention.
254
+ if (!webRecord.cleanup?.agentStopped || !webRecord.cleanup.backendResourcesClosed || !webRecord.cleanup.worktreeDeleted)
255
+ throw new Error("The completed Web execution has incomplete cleanup evidence.");
256
+ if (deleteWorktree) {
257
+ const retained = await cleanupOperation(operation => options.git.findWorktree(projectFor(owner), owner, operation), signal);
258
+ if (retained !== null)
259
+ throw new Error("The completed Web execution still has a worktree; its resources were retained.");
260
+ if (local !== null && local.status !== "done")
261
+ options.store.completeTask(owner);
262
+ }
263
+ return;
264
+ }
209
265
  while (true) {
210
- 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);
211
267
  if (result.cleanup?.agentStopped && result.cleanup.backendResourcesClosed && (!deleteWorktree || result.cleanup.worktreeDeleted)) {
212
268
  if (deleteWorktree) {
213
269
  options.webStore.completeTask(owner, now());
@@ -219,8 +275,9 @@ export const createWorkerCoordinator = (options) => {
219
275
  await delay(1000, undefined, signal === undefined ? {} : { signal });
220
276
  }
221
277
  }
278
+ await cleanupOperation(operation => options.herdr.ensureInstance(operation), signal);
222
279
  while (true) {
223
- const resources = await workerOperation((operation) => options.herdr.tasks.listResources(operation), signal);
280
+ const resources = await cleanupOperation((operation) => options.herdr.tasks.listResources(operation), signal);
224
281
  const task = recordMissingClaim(owner, actualTaskPane(owner, resources));
225
282
  const completed = await cleanup.advance({ owner, project: projectFor(owner), ...(task === null ? {} : { task }) }, resources, deleteWorktree, signal);
226
283
  if (completed) {
@@ -235,7 +292,22 @@ export const createWorkerCoordinator = (options) => {
235
292
  for (const [identityKey, owner] of ending) {
236
293
  if (jobs.has(identityKey))
237
294
  continue;
238
- 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);
239
311
  }
240
312
  };
241
313
  const syncWorkspaceName = async (task, owner, title, signal) => {
@@ -271,37 +343,63 @@ export const createWorkerCoordinator = (options) => {
271
343
  if (!sameClaim(task, owner) || readWorkerClaim(remoteTask.properties).workerId !== options.workerId || readWorkerClaim(remoteTask.properties).agent !== task.agent || readWorkerClaim(remoteTask.properties).agentControlMode !== (task.agentControlMode ?? "herdr"))
272
344
  throw new Error("The local execution does not match the observed original worker claim.");
273
345
  if (task.agentControlMode === "web") {
274
- if (!execute)
275
- return;
276
346
  if (!options.webStore || !options.webBackend)
277
347
  throw new Error("The Web execution backend is unavailable.");
278
348
  const configuration = configurationFor(task);
279
- requireImplementedWorkerBackend(configuration);
280
- await options.verifyWebServer?.(configuration.agent, signal);
281
- if (options.webStore.getTask(task) === null) {
282
- if (!options.verifyWebUnregistered)
283
- throw new Error("Web launch provenance cannot be verified.");
284
- await options.verifyWebUnregistered(task, signal);
285
- if (task.sessionId !== null || readWorkerClaim(remoteTask.properties).nativeSessionId !== null)
286
- throw new Error("The original native identity has no local Web execution evidence.");
287
- }
288
- let record = options.webStore.register({ ...owner, project: projectFor(task), serverOrigin: options.serverOrigin, workerId: options.workerId, userId: readWorkerClaim(remoteTask.properties).userId, configuration }, now());
289
- if (record.cleanup !== null)
290
- record = options.webStore.clearShutdown(record, now());
291
- const result = await options.webBackend.advance(record, signal);
292
- const nativeSessionId = result.resources.nativeSession?.sessionId ?? task.sessionId;
293
- if (nativeSessionId !== null)
294
- options.store.setSessionId(task, nativeSessionId);
295
- const phase = "executing";
296
349
  const worker = readWorkerClaim(remoteTask.properties);
297
- if (worker.phase !== phase || worker.nativeSessionId !== nativeSessionId || worker.herdr !== null) {
298
- try {
299
- 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());
300
358
  }
301
- catch (error) {
302
- if (isWorkerCancellation(error, signal))
303
- throw error;
304
- 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
+ }
305
403
  }
306
404
  }
307
405
  return;
@@ -365,11 +463,11 @@ export const createWorkerCoordinator = (options) => {
365
463
  const owner = resources.owners.get(key(local));
366
464
  if (owner !== undefined)
367
465
  ending.set(key(local), owner);
368
- else if (!jobs.has(key(local))) {
466
+ else if (!jobs.has(key(local)) && !ending.has(key(local))) {
369
467
  if (local.agentControlMode === "web") {
370
468
  if (!options.verifyWebUnregistered || local.sessionId !== null)
371
469
  throw new Error("The ended Web claim has no original resource evidence; its ownership record is retained.");
372
- await options.verifyWebUnregistered(local, normalSignal);
470
+ await options.verifyWebUnregistered(local, normalSignal, CLEANUP_OPERATION_MS);
373
471
  }
374
472
  options.store.completeTask(local);
375
473
  }
@@ -399,6 +497,14 @@ export const createWorkerCoordinator = (options) => {
399
497
  if (!started)
400
498
  break;
401
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
+ }
402
508
  if (capacity(resources) >= options.maxConcurrent || options.repositories.length === 0)
403
509
  return;
404
510
  const candidates = new Map();
@@ -471,21 +577,36 @@ export const createWorkerCoordinator = (options) => {
471
577
  const { task, remoteTask } = await readSource(identity);
472
578
  if (task.status !== "active")
473
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.");
474
586
  failures.delete(key(identity));
475
- if (task.agentControlMode === "web" && options.webStore?.getTask(task))
476
- options.webStore.authorizeRecoveryRetry(task, now());
477
587
  await ensureHerdr(normalSignal);
478
- 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
+ });
479
599
  },
480
600
  cleanupSource: async (identity, settings = {}) => {
481
601
  const { owner } = await readSource(identity);
482
602
  await ensureHerdr(normalSignal);
483
- 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); });
484
605
  return owner;
485
606
  },
486
607
  completeSource: async (owner) => {
487
608
  await ensureHerdr(normalSignal);
488
- await serialize(owner, () => runCleanup(owner, true, normalSignal));
609
+ await serialize(owner, async () => { await runCleanup(owner, true, normalSignal); options.store.deleteCleanupSchedule(owner); });
489
610
  },
490
611
  shutdown: async () => {
491
612
  cancel();
@@ -496,7 +617,7 @@ export const createWorkerCoordinator = (options) => {
496
617
  if (task.agentControlMode === "web" && options.webStore?.getTask(task) == null) {
497
618
  if (task.status === "pending" && task.sessionId === null && options.verifyWebUnregistered) {
498
619
  try {
499
- await options.verifyWebUnregistered(task, new AbortController().signal);
620
+ await options.verifyWebUnregistered(task, new AbortController().signal, CLEANUP_OPERATION_MS);
500
621
  continue;
501
622
  }
502
623
  catch { /* Missing launch evidence must not be interpreted as a confirmed native stop. */ }
@@ -514,7 +635,11 @@ export const createWorkerCoordinator = (options) => {
514
635
  owners.set(key(context), context);
515
636
  }
516
637
  try {
517
- 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
+ }
518
643
  for (const workspace of workspaces) {
519
644
  if (options.store.getProject(workspace.owner) === null) {
520
645
  complete = false;
@@ -528,23 +653,21 @@ export const createWorkerCoordinator = (options) => {
528
653
  complete = false;
529
654
  options.emit({ kind: "shutdown-error", message: "Could not inspect every owned Herdr workspace during shutdown.", error });
530
655
  }
531
- await Promise.all(options.store.listProjects().map(async (project) => {
532
- try {
533
- const resources = await snapshot(undefined, project);
534
- for (const [identity, owner] of resources.owners)
535
- if (resources.proven.has(identity))
536
- owners.set(identity, owner);
537
- }
538
- catch (error) {
539
- complete = false;
540
- options.emit({ kind: "shutdown-error", message: `Could not observe owned resources in project ${project.projectId} during shutdown.`, error });
541
- }
542
- }));
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);
543
659
  await Promise.all([...owners.values()].map(async (owner) => {
544
660
  try {
545
661
  await serialize(owner, async () => {
546
662
  const webRecord = options.webStore?.getTask(owner);
547
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
+ }
548
671
  if (!options.webBackend)
549
672
  throw new Error("The Web execution backend is unavailable during shutdown.");
550
673
  await options.webBackend.cleanup(webRecord, "shutdown", now(), undefined, true);
@@ -553,21 +676,21 @@ export const createWorkerCoordinator = (options) => {
553
676
  if (options.store.getTask(owner)?.agentControlMode === "web")
554
677
  return;
555
678
  if (options.store.getTask(owner) === null) {
556
- const observed = await workerOperation((operation) => options.herdr.tasks.listResources(operation));
679
+ const observed = await cleanupOperation((operation) => options.herdr.tasks.listResources(operation));
557
680
  recordMissingClaim(owner, actualTaskPane(owner, observed));
558
681
  }
559
- await settleWorkerStops(deps, owner);
560
- 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));
561
684
  const pane = actualTaskPane(owner, resources);
562
685
  if (pane === undefined)
563
686
  return;
564
687
  const context = { ...owner, cwd: path.join(projectFor(owner).localPath, ".worktrees", "daynight", owner.taskId) };
565
- await workerOperation((operation) => options.herdr.tasks.stopAndClose(context, pane.binding, operation));
688
+ await cleanupOperation((operation) => options.herdr.tasks.stopAndClose(context, pane.binding, operation));
566
689
  });
567
690
  }
568
691
  catch (error) {
569
692
  complete = false;
570
- 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 });
571
694
  }
572
695
  }));
573
696
  return complete;