@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.
- package/README.md +112 -41
- package/dist/commands/cleanup-operation.d.ts +3 -0
- package/dist/commands/cleanup-operation.js +12 -0
- package/dist/commands/cleanup-operation.js.map +1 -0
- package/dist/commands/controller/provider-display.d.ts +2 -0
- package/dist/commands/controller/provider-display.js +63 -0
- package/dist/commands/controller/provider-display.js.map +1 -0
- package/dist/commands/controller/runtime.js +4 -2
- package/dist/commands/controller/runtime.js.map +1 -1
- package/dist/commands/skill/skills/using-daynight/SKILL.md +1 -1
- package/dist/commands/skill/skills/using-daynight/references/authorization.md +1 -1
- package/dist/commands/skill/skills/using-daynight/references/autopilot-managed-task.md +4 -3
- package/dist/commands/skill/skills/using-daynight/references/handoff.md +25 -15
- package/dist/commands/skill/skills/using-daynight/references/task-management.md +1 -1
- package/dist/commands/skill/usage.md +5 -3
- package/dist/commands/task/command.js +3 -1
- package/dist/commands/task/command.js.map +1 -1
- package/dist/commands/task/definition.js +1 -1
- package/dist/commands/task/definition.js.map +1 -1
- package/dist/commands/worker/backends/session-api.d.ts +1 -1
- package/dist/commands/worker/backends/session-api.js +4 -4
- package/dist/commands/worker/backends/session-api.js.map +1 -1
- package/dist/commands/worker/backends/web-store.d.ts +4 -0
- package/dist/commands/worker/backends/web-store.js +40 -6
- package/dist/commands/worker/backends/web-store.js.map +1 -1
- package/dist/commands/worker/backends/web.js +248 -127
- package/dist/commands/worker/backends/web.js.map +1 -1
- package/dist/commands/worker/cleanup-error.d.ts +5 -0
- package/dist/commands/worker/cleanup-error.js +9 -0
- package/dist/commands/worker/cleanup-error.js.map +1 -0
- package/dist/commands/worker/cleanup-schedule.d.ts +3 -0
- package/dist/commands/worker/cleanup-schedule.js +11 -0
- package/dist/commands/worker/cleanup-schedule.js.map +1 -0
- package/dist/commands/worker/cleanup.js +47 -31
- package/dist/commands/worker/cleanup.js.map +1 -1
- package/dist/commands/worker/coordinator.js +173 -64
- package/dist/commands/worker/coordinator.js.map +1 -1
- package/dist/commands/worker/execution.d.ts +1 -1
- package/dist/commands/worker/execution.js +2 -2
- package/dist/commands/worker/execution.js.map +1 -1
- package/dist/commands/worker/prompts.js +2 -2
- package/dist/commands/worker/prompts.js.map +1 -1
- package/dist/commands/worker/runtime.js +1 -1
- package/dist/commands/worker/runtime.js.map +1 -1
- package/dist/commands/worker/sqlite/006-cleanup-schedules.sql +14 -0
- package/dist/commands/worker/store.d.ts +5 -1
- package/dist/commands/worker/store.js +53 -0
- package/dist/commands/worker/store.js.map +1 -1
- package/dist/commands/worker/types.d.ts +10 -0
- package/dist/commands/worker/worktrees.js +1 -1
- package/dist/commands/worker/worktrees.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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, () =>
|
|
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
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|