@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.
- package/CHANGELOG.md +18 -0
- package/README.md +23 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +3 -0
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
- package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
- package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/services/code-checks.js +14 -18
- package/dist/services/codex-capacity.js +251 -31
- package/dist/services/controller-fanout.js +1 -0
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +36 -11
- package/dist/services/harness-adapter.js +21 -15
- package/dist/services/managed-processes.js +14 -6
- package/dist/services/merge-server.js +35 -12
- package/dist/services/review-copy.js +21 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-controller-adapter.js +33 -8
- package/dist/services/run-controller-recovery.js +26 -2
- package/dist/services/run-controller.js +91 -24
- package/dist/services/run-observations.js +7 -2
- package/dist/services/run-recovery-runtime.js +49 -3
- package/dist/services/runs.js +1 -1
- package/dist/services/stage-report-renderer.js +25 -2
- package/dist/services/verification-matrix.js +57 -8
- package/dist/services/vnext-code-review.js +22 -8
- package/dist/services/vnext-merge.js +20 -2
- package/dist/services/vnext-plan-review.js +87 -19
- package/dist/services/work-registry.js +11 -2
- 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>
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-
|
|
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": "
|
|
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",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -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@
|
|
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
|
|
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 {
|