@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.124

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 (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +23 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli/run-cli.js +3 -0
  5. package/dist/domain/stage-catalog.js +1 -1
  6. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  7. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  8. package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
  10. package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
  12. package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
  15. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
  16. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
  17. package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
  18. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  19. package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
  20. package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
  21. package/dist/harness-runtime/lib/model-observations.mjs +16 -9
  22. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  23. package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
  24. package/dist/schemas/flow-run.schema.json +1 -0
  25. package/dist/schemas/plan-review-decision.schema.json +3 -3
  26. package/dist/services/code-checks.js +14 -18
  27. package/dist/services/codex-capacity.js +251 -31
  28. package/dist/services/controller-fanout.js +1 -0
  29. package/dist/services/eval-snapshots.js +13 -8
  30. package/dist/services/external-work-launch.js +36 -11
  31. package/dist/services/harness-adapter.js +21 -15
  32. package/dist/services/managed-processes.js +14 -6
  33. package/dist/services/merge-server.js +35 -12
  34. package/dist/services/review-copy.js +21 -0
  35. package/dist/services/review-deferral.js +23 -0
  36. package/dist/services/run-controller-adapter.js +33 -8
  37. package/dist/services/run-controller-recovery.js +26 -2
  38. package/dist/services/run-controller.js +91 -24
  39. package/dist/services/run-observations.js +7 -2
  40. package/dist/services/run-recovery-runtime.js +49 -3
  41. package/dist/services/runs.js +1 -1
  42. package/dist/services/stage-report-renderer.js +25 -2
  43. package/dist/services/verification-matrix.js +57 -8
  44. package/dist/services/vnext-code-review.js +22 -8
  45. package/dist/services/vnext-merge.js +20 -2
  46. package/dist/services/vnext-plan-review.js +87 -19
  47. package/dist/services/work-registry.js +11 -2
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.124
4
+
5
+ ### Patch Changes
6
+
7
+ - Stop only two confirmed continuation overload refusals within the inclusive two-minute window, preserving native refusal times and exact owner admission across recovery.
8
+
9
+ Retain final PLAN check obligations, completion markers and native output bytes in verification evidence; require canonical fixes for duplicated CODE P0/P1 findings.
10
+
11
+ Join native daemon cleanup before durable stop acknowledgement and reconcile already-closed provider bridges without replay.
12
+
13
+ ## 0.9.0-beta.123
14
+
15
+ ### Patch Changes
16
+
17
+ - Continue confirmed Codex overloads in the retained Session with bounded backoff, exact owner admission and no concurrent productive Turns.
18
+ - Retain content-addressed verification evidence and reviewer input baselines across publication, relocation and review waves.
19
+ - Preserve canonical native model/tool observations and AGY child transcript evidence without rewriting historical results.
20
+
3
21
  ## 0.9.0-beta.122
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -3,6 +3,29 @@
3
3
  See [dd-zcode integration](src/harness-runtime/DD-ZCODE.md) for native transport,
4
4
  adapter ownership and the versioned bridge contract.
5
5
 
6
+ ## Native daemon shutdown
7
+
8
+ `daemon.stop` fences productive dispatch before awaiting settlement. A successful
9
+ durable stop receipt follows native tree settlement, physical provider exit,
10
+ journal drain and required resource/auth finalization. The endpoint then retires
11
+ before ACK; clients also verify the retained daemon PID has exited. A missing
12
+ socket alone is not physical shutdown proof.
13
+
14
+ Partial cleanup preserves the same incarnation's completed phases in
15
+ `daemon.json` (`dd-flow/daemon-shutdown@1`). A fresh control operation can finish
16
+ remaining cleanup without replaying agent work or inspecting a closed pipe.
17
+ Failed operation IDs remain immutable. Unknown ownership and persistent EPERM
18
+ remain failures; never remove sockets or repair runtime databases manually to
19
+ make an outcome look clean.
20
+
21
+ Shutdown proof records `required_phases`; every recorded phase must be complete,
22
+ including native tree and daemon resource settlement. A `clean` label without
23
+ this proof cannot authorize retained restart. `stopping`, `cleanup_failed` and
24
+ `unclean` cannot start a replacement native bridge. Forced AGY termination
25
+ without a current native terminal receipt is unclean, even after a previous
26
+ successful Turn. Mandatory stream-journal failures remain failures after
27
+ physical cleanup. Listener retirement must not unlink a replacement endpoint.
28
+
6
29
  ## Managed temporary HTTP services
7
30
 
8
31
  For an interactive scenario, `runtime process start --run <RUN-ID>
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.122",
4
- "cli_commit": "b03c699ac77fa596e684408c8e46e8710edbe9f6",
5
- "built_at": "2026-09-30T01:56:09.576Z",
3
+ "cli_version": "0.9.0-beta.124",
4
+ "cli_commit": "7bc8e8d17471c2cd33a93ca25e55770a4cb6e336",
5
+ "built_at": "2026-10-01T12:53:56.722Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.2",
8
- "commit": "0cf38231406614cfeb0a96001ba0201de2316e47",
8
+ "commit": "2e57b987ec91b7c3b0fa97f6169047802a1233fb",
9
9
  "flow_contract": "dd-flow-canonical-2026-08",
10
10
  "repo_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank",
11
11
  "memorybank_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank/.memory-bank",
@@ -75,6 +75,7 @@ import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScop
75
75
  import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
76
76
  import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
77
77
  import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
78
+ import { assertCapacityOwnerAdmission } from "../services/codex-capacity.js";
78
79
  import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
79
80
  import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
80
81
  import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
@@ -2656,6 +2657,8 @@ async function dispatchRuntime(context, command, parsed, progress, prepared = {}
2656
2657
  const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
2657
2658
  if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
2658
2659
  throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
2660
+ if (!recovering)
2661
+ assertCapacityOwnerAdmission(context, record, optionalOption(parsed, "operation-id"), { nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
2659
2662
  }
2660
2663
  const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
2661
2664
  return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
@@ -3,7 +3,7 @@ export const vnextStages = [
3
3
  { id: "specify", directory: "01-specify", result: "specify.json", schema: "dd-flow/vnext-specify-result@4", next: ["protocolize"] },
4
4
  { id: "protocolize", directory: "02-protocolize", result: "protocolize-result.json", schema: "dd-flow/vnext-protocolize-result@3", next: ["plan"] },
5
5
  { id: "plan", directory: "03-plan", result: "stage-report.json", schema: "dd-flow/stage-report@2", next: ["plan-review", "code"] },
6
- { id: "plan-review", directory: "04-plan-review", result: "decision.json", schema: "dd-flow/plan-review-decision@3", next: ["code"] },
6
+ { id: "plan-review", directory: "04-plan-review", result: "decision.json", schema: "dd-flow/plan-review-decision@4", next: ["code"] },
7
7
  { id: "code", directory: "05-code", result: "code-verification.json", schema: "dd-flow/code-verification@2", next: ["code-review", "merge"] },
8
8
  { id: "code-review", directory: "06-code-review", result: "decision.json", schema: "dd-flow/code-review-decision@3", next: ["merge"] },
9
9
  { id: "merge", directory: "07-merge", result: "stage-report.json", schema: "dd-flow/stage-report@2", next: [] }
@@ -0,0 +1,10 @@
1
+ export const CAPACITY_POLICY: "codex-overload-burst@1";
2
+ export const OVERLOAD_WINDOW_MS: number;
3
+ export type CapacityRefusal = { policy: string; ordinal: number; session_id: string; turn_id: string; first_observed_at: string; native_terminal_at?: string; native_terminal_basis?: string };
4
+ export function parseCapacityOperation(id: string): { root: string; ordinal: number };
5
+ export function normalizeCodexFailure(error: unknown, sessionId?: string, turnId?: string): null | { code: string; session_id: string; turn_id: string; native_error: Record<string, unknown>; observed_at: unknown; terminal_at: unknown; native_turn_items: unknown; primary_error: { details?: Record<string, unknown> } };
6
+ export function terminalCodexOverload(error: unknown, sessionId?: string, turnId?: string): boolean;
7
+ export function nativeItemsSettled(items: unknown): boolean;
8
+ export function capacityBackoff(error: unknown, ordinal: number, now?: number): number;
9
+ export function refusalObservation(error: unknown, ordinal: number, firstObservedAt: string): CapacityRefusal;
10
+ export function overloadBurst(refusals: CapacityRefusal[]): null | { policy: string; basis: string; elapsed_ms: number; refusals: CapacityRefusal[] };
@@ -0,0 +1,89 @@
1
+ /** Pure policy shared by the controller and pinned-engine EVAL consumers. */
2
+ export const CAPACITY_POLICY = "codex-overload-burst@1";
3
+ export const OVERLOAD_WINDOW_MS = 120_000;
4
+
5
+ function policyError(code, message, details = {}) { return Object.assign(new Error(message), { code, details }); }
6
+
7
+ export function parseCapacityOperation(id) {
8
+ if (typeof id !== "string" || !id) throw policyError("capacity_continuation_conflict", "Missing capacity operation identity");
9
+ const match = /^(.*):capacity:(\d+):session\.prompt$/.exec(id);
10
+ if (!match) return { root: id, ordinal: 0 };
11
+ const ordinal = Number(match[2]);
12
+ if (!Number.isSafeInteger(ordinal) || ordinal < 1 || String(ordinal) !== match[2]) throw policyError("capacity_continuation_conflict", "Capacity ordinal is not a canonical positive safe integer", { operation_id: id });
13
+ return { root: match[1], ordinal };
14
+ }
15
+
16
+ export function normalizeCodexFailure(error, expectedSessionId, expectedTurnId) {
17
+ let current = error;
18
+ for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
19
+ if (current.code === "turn_interrupted") {
20
+ const details = current.details ?? {}, native = details.provider_error;
21
+ const code = typeof native?.codexErrorInfo === "string" ? native.codexErrorInfo : native?.codexErrorInfo?.code;
22
+ if (details.terminal_status !== "failed" || typeof details.provider_session_id !== "string" || !details.provider_session_id
23
+ || expectedSessionId !== undefined && details.provider_session_id !== expectedSessionId
24
+ || typeof details.turn_id !== "string" || !details.turn_id || details.native_turn_id !== details.turn_id
25
+ || expectedTurnId !== undefined && details.turn_id !== expectedTurnId || typeof code !== "string") return null;
26
+ return { code, session_id: details.provider_session_id, turn_id: details.turn_id, native_error: native,
27
+ observed_at: details.observed_at ?? null, terminal_at: details.terminal_at ?? null, terminal_basis: details.terminal_basis ?? null,
28
+ native_turn_items: details.native_turn_items ?? null, primary_error: current };
29
+ }
30
+ // No arbitrary recursive cause search: an ownership/hook/lifecycle error wins.
31
+ if (current.code !== "harness_adapter_failed") return null;
32
+ current = current.details?.cause ?? current.cause;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export function terminalCodexOverload(error, expectedSessionId, expectedTurnId) {
38
+ return normalizeCodexFailure(error, expectedSessionId, expectedTurnId)?.code === "serverOverloaded";
39
+ }
40
+
41
+ export function nativeItemsSettled(items) {
42
+ return items?.observed === true && typeof items.possible_effects === "boolean" && items.pending !== true
43
+ && (items.possible_effects === false || items.pending === false);
44
+ }
45
+
46
+ export function capacityBackoff(error, ordinal, now = Date.now()) {
47
+ if (!Number.isSafeInteger(ordinal) || ordinal < 1 || !Number.isFinite(now)) throw policyError("capacity_continuation_conflict", "Invalid capacity timing identity");
48
+ const native = normalizeCodexFailure(error)?.native_error ?? error?.details?.provider_error ?? error;
49
+ const header = Object.entries(native?.headers ?? {}).find(([key]) => key.toLowerCase() === "retry-after")?.[1];
50
+ const value = header ?? native?.retryAfter ?? native?.retry_after;
51
+ let retry = 0;
52
+ if (value !== undefined && value !== null) {
53
+ retry = typeof value === "number" || typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim()) ? Number(value) * 1000 : typeof value === "string" ? Math.max(0, Date.parse(value) - now) : NaN;
54
+ if (!Number.isFinite(retry) || retry < 0 || !Number.isFinite(now + retry) || now + retry > 8.64e15)
55
+ throw policyError("capacity_retry_metadata_invalid", "Retry-After cannot be represented safely", { retry_after: value });
56
+ }
57
+ return Math.max(ordinal === 1 ? 5_000 : 15_000, retry);
58
+ }
59
+
60
+ export function refusalObservation(error, ordinal, firstObservedAt) {
61
+ const failure = normalizeCodexFailure(error);
62
+ const observed = Date.parse(firstObservedAt);
63
+ if (!failure || !Number.isSafeInteger(ordinal) || ordinal < 0 || !Number.isFinite(observed))
64
+ throw policyError("capacity_continuation_conflict", "Refusal lacks an immutable native identity/time");
65
+ if (failure.observed_at !== null && (typeof failure.observed_at !== "string" || !Number.isFinite(Date.parse(failure.observed_at)) || Date.parse(failure.observed_at) !== observed))
66
+ throw policyError("capacity_continuation_conflict", "Native refusal observation time is invalid or changed");
67
+ const native = failure.terminal_at && Date.parse(failure.terminal_at);
68
+ return { policy: CAPACITY_POLICY, ordinal, session_id: failure.session_id, turn_id: failure.turn_id,
69
+ first_observed_at: firstObservedAt, ...(Number.isFinite(native) && typeof failure.terminal_basis === "string" && failure.terminal_basis ? { native_terminal_at: failure.terminal_at, native_terminal_basis: failure.terminal_basis } : {}) };
70
+ }
71
+
72
+ export function overloadBurst(refusals) {
73
+ for (const [index, refusal] of refusals.entries()) {
74
+ const previous = refusals[index - 1];
75
+ if (typeof refusal.first_observed_at !== "string" || !Number.isFinite(Date.parse(refusal.first_observed_at))
76
+ || previous && Date.parse(refusal.first_observed_at) < Date.parse(previous.first_observed_at))
77
+ throw policyError("capacity_continuation_conflict", "Refusal observation clock reversed or is invalid");
78
+ }
79
+ const continuations = refusals.filter(item => item.ordinal > 0);
80
+ const pair = continuations.slice(-2);
81
+ if (pair.length !== 2) return null;
82
+ const [previous, current] = pair;
83
+ if (previous.session_id !== current.session_id || previous.turn_id === current.turn_id || current.ordinal !== previous.ordinal + 1)
84
+ throw policyError("capacity_continuation_conflict", "Refusal chain identity is inconsistent");
85
+ const basis = previous.native_terminal_at && current.native_terminal_at && previous.native_terminal_basis && previous.native_terminal_basis === current.native_terminal_basis ? "native_terminal_at" : "first_observed_at";
86
+ const elapsed = Date.parse(current[basis]) - Date.parse(previous[basis]);
87
+ if (!Number.isFinite(elapsed) || elapsed < 0) throw policyError("capacity_continuation_conflict", "Refusal clock reversed or is invalid");
88
+ return elapsed <= OVERLOAD_WINDOW_MS ? { policy: CAPACITY_POLICY, basis, elapsed_ms: elapsed, refusals: pair } : null;
89
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { lstatSync } from "node:fs";
2
3
  import { mkdir, readFile, readdir, link, unlink, open } from "node:fs/promises";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import path from "node:path";
@@ -10,6 +11,99 @@ import { readModelObservations, modelAttribution } from "./model-observations.mj
10
11
  const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork", "session.resume"]);
11
12
  const controls = new Map();
12
13
  const dispatchGenerations = new Map();
14
+ const shutdowns = new Map();
15
+
16
+ export function assertDaemonDispatch(root) {
17
+ const stop = shutdowns.get(root);
18
+ if (stop?.pending || stop?.committed) throw Object.assign(new Error("daemon shutdown fences productive dispatch"), { code: "daemon_stopping" });
19
+ }
20
+ export function assertDaemonRestartable(state) {
21
+ if (["unclean", "cleanup_failed", "stopping"].includes(state?.shutdown_state)) throw Object.assign(new Error("previous daemon shutdown is unresolved; do not restart a native bridge for cleanup"), { code: "daemon_state_terminal", details: { daemon_id: state.daemon_id ?? state.config?.daemonId, shutdown_state: state.shutdown_state } });
22
+ }
23
+ export function commitDaemonShutdown(root) {
24
+ if (typeof root !== 'string') return; // In-memory adapter diagnostics own no daemon endpoint.
25
+ const fence = shutdowns.get(root) ?? { pending: 0 };
26
+ fence.committed = true; shutdowns.set(root, fence);
27
+ }
28
+
29
+ /** The same incarnation may retry remaining cleanup, never a native Turn. */
30
+ export async function completeDaemonShutdown(root, state, persist, prepare, phases) {
31
+ const daemonId = state.daemon_id ?? state.config?.daemonId;
32
+ let shutdown = state.shutdown;
33
+ if (!shutdown || shutdown.daemon_id !== daemonId) shutdown = { schema_id: "dd-flow/daemon-shutdown@1", daemon_id: daemonId, generation: dispatchGenerations.get(root) ?? 0, phases: {} };
34
+ shutdown = { ...shutdown, required_phases: ["tree", ...phases.map(([phase]) => phase)] };
35
+ try {
36
+ if (!shutdown.phases.tree) {
37
+ const result = await prepare();
38
+ shutdown = { ...shutdown, phases: { tree: true }, tree_observed_at: new Date().toISOString(), result: result ?? { clean: true } };
39
+ commitDaemonShutdown(root);
40
+ await persist({ shutdown, shutdown_state: "stopping", active_tree: false });
41
+ }
42
+ commitDaemonShutdown(root);
43
+ for (const [phase, action] of phases) {
44
+ if (shutdown.phases[phase]) continue;
45
+ await action();
46
+ shutdown = { ...shutdown, phases: { ...shutdown.phases, [phase]: true } };
47
+ await persist({ shutdown });
48
+ }
49
+ const result = { ...shutdown.result, daemon_id: daemonId, stopped: true, settled: true, shutdown_contract: "dd-flow/daemon-shutdown@1", _shutdown: true };
50
+ await persist({ shutdown, shutdown_state: result.clean === true ? "clean" : "unclean", active_tree: false, stopped_at: new Date().toISOString(), cleanup_error: null });
51
+ return result;
52
+ } catch (error) {
53
+ if (error.code === "tree_not_settled" && !shutdowns.get(root)?.committed) throw error;
54
+ try { await persist({ shutdown_state: "cleanup_failed", cleanup_error: errorRecord(error) }); }
55
+ catch (storage) { error.details = { ...error.details, persistence_error: errorRecord(storage) }; }
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ const daemonConnections = new WeakMap();
61
+ export function trackDaemonConnections(server) {
62
+ const sockets = new Set(); daemonConnections.set(server, sockets);
63
+ server.once("listening", () => {
64
+ const endpoint = server.address();
65
+ if (typeof endpoint !== "string") return;
66
+ try { const info = lstatSync(endpoint); daemonEndpoints.set(server, { endpoint, dev: info.dev, ino: info.ino }); }
67
+ catch (error) { daemonEndpoints.set(server, { endpoint, error }); }
68
+ });
69
+ server.on("connection", socket => { sockets.add(socket); socket.on("error", () => {}); socket.once("close", () => sockets.delete(socket)); });
70
+ }
71
+ const daemonEndpoints = new WeakMap();
72
+
73
+ /** Retire the listener after durable publication, before ACK; no provider IO. */
74
+ export function retireDaemonEndpoint(server, exitCode = 0) {
75
+ if (!server.listening) return;
76
+ const retained = daemonEndpoints.get(server);
77
+ let endpointError = retained?.error;
78
+ if (retained && !endpointError) {
79
+ try {
80
+ const current = lstatSync(retained.endpoint);
81
+ if (current.dev !== retained.dev || current.ino !== retained.ino || !current.isSocket()) endpointError = Object.assign(new Error("daemon endpoint was replaced; refusing to unlink another listener"), { code: "daemon_endpoint_identity_mismatch", details: { socket: retained.endpoint, phase: "retire_listener" } });
82
+ } catch (error) { if (error.code !== "ENOENT") endpointError = error; }
83
+ }
84
+ if (endpointError) {
85
+ // libuv's close unlinks by pathname, not inode. Exit without closing its
86
+ // listener handle when the name belongs to a different incarnation.
87
+ const timer = setTimeout(() => { for (const socket of daemonConnections.get(server) ?? []) socket.destroy(); process.exit(1); }, 1_000);
88
+ timer.unref();
89
+ throw endpointError;
90
+ }
91
+ const timer = setTimeout(() => { for (const socket of daemonConnections.get(server) ?? []) socket.destroy(); }, 1_000);
92
+ timer.unref();
93
+ server.close(() => { clearTimeout(timer); process.exit(exitCode); });
94
+ }
95
+
96
+ export async function retireFailedDaemon(server, state, error, persist) {
97
+ // An already-finished registry lease cannot own a listening productive daemon.
98
+ if (state.shutdown?.phases?.daemon_resource) {
99
+ // Durable result publication and listener retirement occur after the
100
+ // cleanup helper returns. Their failures still invalidate overall clean.
101
+ if (persist) try { await persist({ shutdown_state: "cleanup_failed", cleanup_error: errorRecord(error) }); }
102
+ catch (storage) { error.details = { ...error.details, persistence_error: errorRecord(storage) }; }
103
+ process.stderr.write(`${JSON.stringify({ shutdown_failure: errorRecord(error) })}\n`);
104
+ try { retireDaemonEndpoint(server, 1); } catch { /* Retirement already scheduled a nonzero exit without unlinking a foreign endpoint. */ }
105
+ }
106
+ }
13
107
 
14
108
  async function controlDispatch(root, request, action) {
15
109
  const key = request.operation === "daemon.stop" ? `stop:${request.params?.cancelTree === true}` : `cancel:${request.params?.sessionId ?? ""}`;
@@ -122,13 +216,16 @@ export async function durableDaemonDispatch(root, request, action, observe) {
122
216
  if (!productive.has(request.operation) && !control) return await action();
123
217
  const generation = dispatchGenerations.get(root) ?? 0;
124
218
  if (control) dispatchGenerations.set(root, generation + 1);
125
- const assertCurrent = () => { if (generation !== (dispatchGenerations.get(root) ?? 0)) throw Object.assign(new Error("control cancelled a queued productive dispatch"), { code: "operation_cancelled" }); };
126
- const result = await journalDispatch(root, request, async () => {
219
+ const assertCurrent = () => { assertDaemonDispatch(root); if (generation !== (dispatchGenerations.get(root) ?? 0)) throw Object.assign(new Error("control cancelled a queued productive dispatch"), { code: "operation_cancelled" }); };
220
+ const stop = request.operation === "daemon.stop" ? (shutdowns.get(root) ?? { pending: 0, committed: false }) : null;
221
+ if (stop) { stop.pending++; shutdowns.set(root, stop); }
222
+ let result;
223
+ try { result = await journalDispatch(root, request, async () => {
127
224
  // Persist before admission so a racing fence can observe the writer.
128
225
  if (!control) await assertDaemonOwnership(root, { "operation-id": request.id, operation: request.operation, "native-session-id": request.params?.sessionId, "prompt-sha256": typeof request.params?.prompt === "string" ? createHash("sha256").update(request.params.prompt).digest("hex") : undefined }, assertCurrent);
129
226
  if (!control) assertCurrent();
130
227
  return control ? await controlDispatch(root, request, action) : await action();
131
- });
228
+ }); } finally { if (stop) { stop.pending--; if (!stop.pending && !stop.committed) shutdowns.delete(root); } }
132
229
  if (control) return result;
133
230
  let settlement;
134
231
  try {