@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.
- package/README.md +266 -68
- package/dist/api/client.d.ts +7 -1
- package/dist/api/client.js +117 -21
- package/dist/api/client.js.map +1 -1
- package/dist/api/json-source.d.ts +3 -0
- package/dist/api/json-source.js +113 -0
- package/dist/api/json-source.js.map +1 -0
- package/dist/api/parse.d.ts +2 -1
- package/dist/api/parse.js +16 -1
- package/dist/api/parse.js.map +1 -1
- package/dist/api/thread.d.ts +6 -0
- package/dist/api/thread.js +79 -0
- package/dist/api/thread.js.map +1 -0
- package/dist/api/types.d.ts +50 -0
- package/dist/cli/application.js +12 -2
- package/dist/cli/application.js.map +1 -1
- package/dist/cli/context.d.ts +9 -1
- package/dist/cli/context.js +3 -1
- package/dist/cli/context.js.map +1 -1
- package/dist/cli/schema.js +10 -10
- package/dist/cli/schema.js.map +1 -1
- 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/providers/projection.js +12 -0
- package/dist/commands/controller/providers/projection.js.map +1 -1
- package/dist/commands/controller/runtime.js +4 -2
- package/dist/commands/controller/runtime.js.map +1 -1
- package/dist/commands/server/runtime.d.ts +10 -1
- package/dist/commands/server/runtime.js +24 -7
- package/dist/commands/server/runtime.js.map +1 -1
- package/dist/commands/skill/skills/using-daynight/SKILL.md +5 -3
- package/dist/commands/skill/skills/using-daynight/references/authorization.md +2 -2
- package/dist/commands/skill/skills/using-daynight/references/autopilot-managed-task.md +4 -3
- package/dist/commands/skill/skills/using-daynight/references/handbook.md +4 -1
- package/dist/commands/skill/skills/using-daynight/references/handoff.md +25 -15
- package/dist/commands/skill/skills/using-daynight/references/task-content.md +7 -4
- package/dist/commands/skill/skills/using-daynight/references/task-management.md +56 -6
- package/dist/commands/skill/skills/using-daynight/references/write-safety.md +22 -3
- package/dist/commands/skill/usage.md +84 -7
- package/dist/commands/task/command.js +28 -3
- package/dist/commands/task/command.js.map +1 -1
- package/dist/commands/task/definition.js +40 -6
- package/dist/commands/task/definition.js.map +1 -1
- package/dist/commands/task/rendering.js +4 -0
- package/dist/commands/task/rendering.js.map +1 -1
- package/dist/commands/task/thread-diagnostics.d.ts +5 -0
- package/dist/commands/task/thread-diagnostics.js +32 -0
- package/dist/commands/task/thread-diagnostics.js.map +1 -0
- package/dist/commands/task/thread-rendering.d.ts +8 -0
- package/dist/commands/task/thread-rendering.js +80 -0
- package/dist/commands/task/thread-rendering.js.map +1 -0
- package/dist/commands/task/thread.d.ts +11 -0
- package/dist/commands/task/thread.js +74 -0
- package/dist/commands/task/thread.js.map +1 -0
- 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 +186 -63
- 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/remote.js +3 -4
- package/dist/commands/worker/remote.js.map +1 -1
- package/dist/commands/worker/remove-worktree-contents.d.ts +3 -0
- package/dist/commands/worker/remove-worktree-contents.js +106 -0
- package/dist/commands/worker/remove-worktree-contents.js.map +1 -0
- 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 +70 -24
- package/dist/commands/worker/worktrees.js.map +1 -1
- package/dist/platform/diagnostics.d.ts +5 -0
- package/dist/platform/diagnostics.js +12 -0
- package/dist/platform/diagnostics.js.map +1 -1
- package/dist/session/codex-subagent-activity.d.ts +4 -0
- package/dist/session/codex-subagent-activity.js +13 -0
- package/dist/session/codex-subagent-activity.js.map +1 -0
- package/dist/session/v2/archive.d.ts +6 -0
- package/dist/session/v2/archive.js +163 -0
- package/dist/session/v2/archive.js.map +1 -0
- package/dist/session/v2/protocol.d.ts +19 -0
- package/dist/session/v2/protocol.js +260 -0
- package/dist/session/v2/protocol.js.map +1 -0
- package/dist/session/v2/responsibility.d.ts +3 -0
- package/dist/session/v2/responsibility.js +12 -0
- package/dist/session/v2/responsibility.js.map +1 -0
- package/dist/session/v2/types.d.ts +293 -0
- package/dist/session/v2/types.js +2 -0
- package/dist/session/v2/types.js.map +1 -0
- package/dist/session/v2/values.d.ts +59 -0
- package/dist/session/v2/values.js +201 -0
- package/dist/session/v2/values.js.map +1 -0
- package/package.json +5 -3
|
@@ -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,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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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, () =>
|
|
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
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|