@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.133
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 +666 -0
- package/README.md +101 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +306 -0
- package/dist/cli/help.js +88 -12
- package/dist/cli/hook-ingress.js +78 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +80 -0
- package/dist/cli/run-cli.js +1465 -450
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +52 -0
- package/dist/cli/runtime-process.js +153 -0
- package/dist/cli.js +17 -2
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- 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-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +1108 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
- package/dist/harness-runtime/lib/model-observations.mjs +101 -0
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +201 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-verification.schema.json +7 -5
- package/dist/schemas/code-work-batch.schema.json +2 -2
- package/dist/schemas/flow-run.schema.json +2 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +118 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/branch-context.js +3 -3
- package/dist/services/canon.js +11 -8
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +133 -40
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +851 -109
- package/dist/services/codex-capacity.js +367 -0
- package/dist/services/codex-hook-delivery.js +32 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/compatibility-preflight.js +13 -3
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +119 -0
- package/dist/services/dashboard.js +135 -56
- package/dist/services/engines.js +245 -119
- package/dist/services/eval-snapshots.js +1146 -105
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +230 -0
- package/dist/services/harness-adapter.js +329 -36
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +747 -325
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +1 -288
- package/dist/services/lifecycle-contract.js +1 -0
- package/dist/services/lifecycle-invocations.js +1774 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-lease-monitor.js +133 -0
- package/dist/services/managed-processes.js +409 -51
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +163 -35
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +28 -17
- package/dist/services/protocols.js +163 -87
- package/dist/services/recovery-observation-budget.js +121 -0
- package/dist/services/recovery-snapshot-database.js +145 -0
- package/dist/services/repair-intents.js +42 -0
- package/dist/services/review-copy.js +157 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +411 -0
- package/dist/services/run-control.js +985 -0
- package/dist/services/run-controller-adapter.js +399 -0
- package/dist/services/run-controller-capture.js +179 -0
- package/dist/services/run-controller-process.js +187 -0
- package/dist/services/run-controller-recovery.js +289 -0
- package/dist/services/run-controller-state.js +97 -0
- package/dist/services/run-controller.js +1035 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +147 -0
- package/dist/services/run-observations.js +152 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +112 -0
- package/dist/services/run-recovery.js +349 -0
- package/dist/services/runs.js +558 -217
- package/dist/services/runtime-budget.js +430 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +468 -0
- package/dist/services/runtime-scope-resume.js +599 -0
- package/dist/services/runtime-scope-stop.js +180 -0
- package/dist/services/runtime-scope-worker.js +287 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +27 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +185 -19
- package/dist/services/stage-lifecycle.js +98 -107
- package/dist/services/stage-pause.js +89 -75
- package/dist/services/stage-report-renderer.js +158 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/tooling.js +32 -23
- package/dist/services/usage.js +49 -101
- package/dist/services/verification-matrix.js +374 -0
- package/dist/services/vnext-code-review.js +222 -87
- package/dist/services/vnext-code.js +324 -251
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +79 -4
- package/dist/services/vnext-fanout.js +177 -17
- package/dist/services/vnext-merge.js +335 -113
- package/dist/services/vnext-plan-review.js +276 -126
- package/dist/services/vnext-plan.js +227 -102
- package/dist/services/vnext-protocolize.js +335 -82
- package/dist/services/vnext-specify.js +119 -81
- package/dist/services/work-registry.js +1038 -192
- package/dist/services/workspace-bootstrap.js +87 -0
- package/dist/services/workspace-files.js +85 -0
- package/dist/services/worktrees.js +98 -44
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +17 -0
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +781 -37
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +30 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
|
|
3
|
+
import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
|
|
4
|
+
import { hookBudget, invokeNativeHook, readNativeHookInput, writeNativeHookFailure } from "../lib/native-hook-command.mjs";
|
|
5
|
+
import { cancelChild, cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-zcode.mjs";
|
|
6
|
+
import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-zcode-daemon.mjs";
|
|
7
|
+
import { errorRecord } from "../lib/operation-errors.mjs";
|
|
8
|
+
|
|
9
|
+
function parse(argv) {
|
|
10
|
+
const positional = []; const options = {};
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const token = argv[index];
|
|
13
|
+
if (!token.startsWith("--")) { positional.push(token); continue; }
|
|
14
|
+
const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; }
|
|
15
|
+
const value = argv[++index]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value;
|
|
16
|
+
}
|
|
17
|
+
return { positional, options };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function common(options, operation) {
|
|
21
|
+
return {
|
|
22
|
+
bin: options["zcode-acp-bin"], zcodePath: options["zcode-path"], journal: options.journal,
|
|
23
|
+
stateDir: options["state-dir"],
|
|
24
|
+
cwd: options.cwd, sessionId: options["session-id"], adapterSessionId: options["adapter-session-id"], provider: options.provider,
|
|
25
|
+
childSessionId: options["child-session-id"],
|
|
26
|
+
model: options.model, reasoning: options.reasoning, mode: options.mode,
|
|
27
|
+
permission: options.permission ?? "deny", ddFlowBin: options["dd-flow-bin"],
|
|
28
|
+
ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true,
|
|
29
|
+
env: options["dd-flow-home"] ? { DD_FLOW_HOME: options["dd-flow-home"] } : undefined,
|
|
30
|
+
...adapterTimeoutOptions(options, operation),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function hook(options) {
|
|
35
|
+
const budget = hookBudget();
|
|
36
|
+
const payload = JSON.parse((await readNativeHookInput(process.stdin, budget)) || "{}");
|
|
37
|
+
const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId, payload }, budget.remaining("identity"));
|
|
38
|
+
const command = options["dd-flow-bin"]; const args = ["zcode", "hook", "handle", "--project-root", options["project-root"], "--json"];
|
|
39
|
+
|
|
40
|
+
try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], timeoutMs: budget.remaining("admission", 15_000), payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } }); }
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
|
|
43
|
+
try {
|
|
44
|
+
// The 25s work budget leaves this bounded 2s evidence tail inside the
|
|
45
|
+
// qualified 30s native hook limit, even after the work deadline expired.
|
|
46
|
+
await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error, hookBudget(1_000, null));
|
|
47
|
+
if (identity.nativeHookBinding) await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id }, 1_000);
|
|
48
|
+
} catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; let result;
|
|
55
|
+
const answers = options["answers-file"] ? JSON.parse(await promptFromFile(options["answers-file"])) : undefined;
|
|
56
|
+
const shared = common(options, family === "session" ? command : null);
|
|
57
|
+
if (family === "doctor") result = await doctor(shared);
|
|
58
|
+
else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
|
|
59
|
+
else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
|
|
60
|
+
else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
|
|
61
|
+
else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(shared.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
|
|
62
|
+
else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(shared.stateDir, options["operation-id"]);
|
|
63
|
+
else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
|
|
64
|
+
else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
|
|
65
|
+
else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
|
|
66
|
+
else if (family === "session") {
|
|
67
|
+
const params = { ...shared, answers, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}) };
|
|
68
|
+
// The daemon owns the durable native-request deadline. A disposable RPC
|
|
69
|
+
// wait must not manufacture a second provider timeout.
|
|
70
|
+
const operation = `session.${command === "status" ? "inspect" : command}`;
|
|
71
|
+
const rpcTimeout = command === "prompt" && shared.timeoutMs === undefined
|
|
72
|
+
? null
|
|
73
|
+
: (shared.timeoutMs ?? 1_800_000);
|
|
74
|
+
if (shared.stateDir) result = await callDaemon(shared.stateDir, operation, params, rpcTimeout);
|
|
75
|
+
else if (command === "create") result = await createSession(params);
|
|
76
|
+
else if (command === "prompt") result = await promptSession(params);
|
|
77
|
+
else if (["inspect", "status", "resume"].includes(command)) result = await inspectSession(params);
|
|
78
|
+
else if (command === "cancel") result = await cancelSession(params);
|
|
79
|
+
else if (command === "cancel-child") result = await cancelChild(params);
|
|
80
|
+
else if (command === "fork") result = await forkSession(params);
|
|
81
|
+
else throw new Error(`unknown session command: ${command}`);
|
|
82
|
+
} else throw new Error("usage: dd-zcode doctor | daemon start|status|stop | hook handle | session create|prompt|inspect|status|resume|cancel|cancel-child|fork [options] --json");
|
|
83
|
+
process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (process.argv[2] === "hook") { process.stderr.write(`${JSON.stringify({ native_hook_failure: errorRecord(error), bound: Boolean(error.details?.native_hook_binding) })}\n`); process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); }
|
|
86
|
+
const failure = errorRecord(error); process.stderr.write(`${JSON.stringify({ ok: false, error: failure.message, ...failure })}\n`); process.exit(1);
|
|
87
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const NATIVE_OPERATION_WAIT_CONTRACT: "native-operation-wait@1";
|
|
2
|
+
export function canonicalNativeOperation(harness: string, operation: string): string;
|
|
3
|
+
export function nativeOperationWait(harness: string, operation: string): "native-work" | "control";
|
|
4
|
+
export function validateDaemonWait(harness: string, operation: string, duration: number | null): void;
|
|
5
|
+
export function adapterTimeoutOptions(options: Record<string, unknown>, operation?: string | null, harness?: string): { timeoutMs?: number; livenessTimeoutMs?: number; requestTimeoutMs?: number };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Public work flags express silence, never the lifetime of a native Turn.
|
|
2
|
+
import { validateObservationDuration } from "./observation-clock.mjs";
|
|
3
|
+
export const NATIVE_OPERATION_WAIT_CONTRACT = "native-operation-wait@1";
|
|
4
|
+
// Grok's resume only inspects its retained native Session. Other adapters
|
|
5
|
+
// materialize that Session, so their original operation remains productive.
|
|
6
|
+
export function canonicalNativeOperation(harness, operation) {
|
|
7
|
+
return ["grok", "grok-acp"].includes(harness) && operation === "session.resume" ? "session.inspect" : operation;
|
|
8
|
+
}
|
|
9
|
+
// Only Droid's start waits for the full Turn. Codex start returns acceptance;
|
|
10
|
+
// the other bundled adapters do not expose start as a productive operation.
|
|
11
|
+
export function nativeOperationWait(harness, operation) {
|
|
12
|
+
operation = canonicalNativeOperation(harness, operation);
|
|
13
|
+
return operation === "session.prompt" || ["droid", "droid-cli"].includes(harness) && operation === "session.start" ? "native-work" : "control";
|
|
14
|
+
}
|
|
15
|
+
export function validateDaemonWait(harness, operation, duration) {
|
|
16
|
+
if (duration === null && nativeOperationWait(harness, operation) === "native-work") return;
|
|
17
|
+
validateObservationDuration(duration, "daemon control duration");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function adapterTimeoutOptions(options, operation, harness) {
|
|
21
|
+
const duration = name => {
|
|
22
|
+
if (options[name] === undefined) return undefined;
|
|
23
|
+
const ms = Number(options[name]) * 1_000;
|
|
24
|
+
if (!Number.isInteger(ms) || ms < 1 || ms > 2_147_483_647) throw Object.assign(new Error(`--${name} requires positive finite seconds within the timer range`), { code: "invalid_timeout" });
|
|
25
|
+
return ms;
|
|
26
|
+
};
|
|
27
|
+
const timeoutMs = duration("timeout"), livenessTimeoutMs = duration("liveness-timeout"), requestTimeoutMs = duration("request-timeout");
|
|
28
|
+
if (operation === "prompt" || nativeOperationWait(harness, `session.${operation}`) === "native-work") {
|
|
29
|
+
if (timeoutMs !== undefined && livenessTimeoutMs !== undefined && timeoutMs !== livenessTimeoutMs) throw Object.assign(new Error("--timeout and --liveness-timeout must agree for session.prompt"), { code: "invalid_timeout" });
|
|
30
|
+
if (requestTimeoutMs !== undefined) throw Object.assign(new Error("--request-timeout cannot limit a productive prompt"), { code: "invalid_timeout" });
|
|
31
|
+
return { timeoutMs: undefined, livenessTimeoutMs: livenessTimeoutMs ?? timeoutMs };
|
|
32
|
+
}
|
|
33
|
+
return { timeoutMs, livenessTimeoutMs, requestTimeoutMs };
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function agyWorkspaceHooks(): Record<string, unknown>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Qualified hooks are stable; only the launching process selects its pinned
|
|
2
|
+
// entrypoint and daemon. Keep this document shared with product-file policy.
|
|
3
|
+
export function agyWorkspaceHooks() {
|
|
4
|
+
const command = '"${DD_AGY_HOOK_NODE:?missing AGY hook binding}" "${DD_AGY_HOOK_ENTRY:?missing AGY hook binding}" hook handle --event __EVENT__ --state-dir "${DD_AGY_STATE_DIR:?missing AGY hook binding}" --daemon-id "${DD_AGY_DAEMON_ID:?missing AGY hook binding}" --json';
|
|
5
|
+
return { "dd-flow": {
|
|
6
|
+
PreToolUse: [{ matcher: "^run_command$", hooks: [{ type: "command", command: command.replace("__EVENT__", "PreToolUse"), timeout: 30 }] }],
|
|
7
|
+
Stop: [{ type: "command", command: command.replace("__EVENT__", "Stop"), timeout: 30 }]
|
|
8
|
+
} };
|
|
9
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export function recoverDaemonReply(root: string, id: string, options?: { timeoutMs?: number | null; signal?: AbortSignal; binding?: Record<string, unknown>; onProgress?: (value: Record<string, unknown>) => void; onOutcome?: (value: Record<string, unknown>) => void; onPendingSettlement?: (value: Record<string, unknown>) => Promise<void>; observationClock?: import("./observation-clock.mjs").ObservationClock }): Promise<Record<string, unknown>>;
|
|
2
|
+
export function retainedDaemonReply(root: string, id: string, binding?: Record<string, unknown>): Promise<{ ready: boolean; outcome: unknown; value?: unknown; settlement?: Record<string, unknown> | null; reason?: string } | null>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hashJson, writeJsonAtomic } from './runner-events.mjs';
|
|
5
|
+
import { ObservationClock } from './observation-clock.mjs';
|
|
6
|
+
import { canonicalNativeOperation, nativeOperationWait, validateDaemonWait } from './adapter-timeouts.mjs';
|
|
7
|
+
import { assertNativeLine, retainNativeReplyBuffer } from './native-streams.mjs';
|
|
8
|
+
import { operationDirectory, readOperationRecord, readOperationProgress, readOperationWaiting, suspendOperationClock } from './operation-progress.mjs';
|
|
9
|
+
|
|
10
|
+
const productive = new Set(['session.create', 'session.prompt', 'session.start', 'session.fork', 'session.resume']);
|
|
11
|
+
const fault = (code, message, details) => Object.assign(new Error(message), { code, retryable: false, details });
|
|
12
|
+
export async function retainedDaemonReply(root, id, binding = {}) {
|
|
13
|
+
const requested = await readOperationRecord(root, id, 'requested.json');
|
|
14
|
+
if (!requested) return null;
|
|
15
|
+
if (requested.operation_id !== id || Object.entries(binding).some(([key, value]) => (requested[key] ?? null) !== (value ?? null))) throw fault('operation_conflict', 'retained operation differs from dispatched request', { operation_id: id });
|
|
16
|
+
const terminal = await readOperationRecord(root, id, 'result.json');
|
|
17
|
+
if (!terminal) return null;
|
|
18
|
+
if (['operation_id','daemon_id','operation','params_sha256','owner_id','generation'].some(field=>Object.hasOwn(terminal,field)&&(terminal[field] ?? null)!==(requested[field] ?? null))) throw fault('operation_conflict','native outcome binding differs from original operation',{operation_id:id});
|
|
19
|
+
if (terminal.state !== 'completed') throw Object.assign(new Error(terminal.error?.message ?? 'native operation failed'), terminal.error);
|
|
20
|
+
if (Number.isSafeInteger(requested.generation)) try {
|
|
21
|
+
const fence = JSON.parse(await readFile(path.join(root,'operation-fence.json'),'utf8'));
|
|
22
|
+
if (fence.daemon_id === requested.daemon_id && fence.generation > requested.generation) throw fault('operation_cancelled','continuation is fenced by newer control',{operation_id:id,native_result:terminal.result});
|
|
23
|
+
} catch(error) { if (error.code !== 'ENOENT') throw error; }
|
|
24
|
+
const settlement = await readOperationRecord(root, id, 'settlement.json');
|
|
25
|
+
try {
|
|
26
|
+
const daemon = JSON.parse(await readFile(path.join(root,'daemon.json'),'utf8'));
|
|
27
|
+
if ((daemon.daemon_id ?? daemon.config?.daemonId ?? null)!==requested.daemon_id || (daemon.config?.runtime_owner?.owner_id ?? null)!==(requested.owner_id ?? null) || ['stopping','clean','unclean','cleanup_failed','failed'].includes(daemon.shutdown_state)) return {ready:false,outcome:terminal.result,settlement,reason:'daemon_authority_changed'};
|
|
28
|
+
if (requested.owner_id) try { process.kill(daemon.pid,0); } catch { return {ready:false,outcome:terminal.result,settlement,reason:'daemon_owner_dead'}; }
|
|
29
|
+
} catch(error) { if(error.code!=='ENOENT') throw error; if(requested.daemon_id || requested.owner_id) return {ready:false,outcome:terminal.result,settlement,reason:'daemon_absent'}; }
|
|
30
|
+
if (!['settled', 'not_required'].includes(settlement?.state)) return { ready: false, outcome: terminal.result, settlement };
|
|
31
|
+
return { ready: true, outcome: terminal.result, settlement, value: settlement.state === 'not_required' ? terminal.result : { ...terminal.result, settlement } };
|
|
32
|
+
}
|
|
33
|
+
export async function recoverDaemonReply(root, id, { timeoutMs = 30_000, signal, binding = {}, onProgress = () => {}, onOutcome = () => {}, onPendingSettlement, observationClock } = {}) {
|
|
34
|
+
const saved = observationClock ? null : await readOperationRecord(root, id, 'observer-recovery.json') ?? await readOperationRecord(root, id, 'observer-transport.json');
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
const clock = observationClock ?? new ObservationClock({ timeoutMs: saved?.timeout_ms ?? timeoutMs, saved });
|
|
37
|
+
let lastSequence = clock.progress ?? 0, lastSaved = 0, latestProgress, settlementAttempted = false, settlementError;
|
|
38
|
+
const exhaustedCursor = clock.progress;
|
|
39
|
+
let exhausted = clock.observationLost || clock.elapsed >= clock.timeoutMs;
|
|
40
|
+
for (;;) {
|
|
41
|
+
signal?.throwIfAborted();
|
|
42
|
+
const retained = await retainedDaemonReply(root, id, binding);
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
if (retained) {
|
|
45
|
+
onOutcome(retained.outcome);
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
if (retained.ready) return retained.value;
|
|
48
|
+
}
|
|
49
|
+
const progress = await readOperationProgress(root, id, binding);
|
|
50
|
+
signal?.throwIfAborted();
|
|
51
|
+
if (progress && progress.sequence > lastSequence) {
|
|
52
|
+
lastSequence = progress.sequence;
|
|
53
|
+
const observedAt = Date.parse(progress.observed_at);
|
|
54
|
+
// The outer cursor counts diagnostics, not durable sequences. A handoff
|
|
55
|
+
// may recognize a higher sequence with the same already-observed event.
|
|
56
|
+
if (clock.progressAt === null || observedAt > clock.progressAt) {
|
|
57
|
+
latestProgress={cursor:progress.sequence,observed_at:observedAt}; onProgress(progress);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const waiting = await readOperationWaiting(root,id,binding);
|
|
61
|
+
signal?.throwIfAborted();
|
|
62
|
+
if (waiting) suspendOperationClock(clock);
|
|
63
|
+
const sampled = waiting ? false : clock.sample(latestProgress);
|
|
64
|
+
if (clock.progress !== exhaustedCursor && clock.elapsed < clock.timeoutMs && !clock.observationLost) exhausted = false;
|
|
65
|
+
const expired = !waiting && (exhausted || sampled);
|
|
66
|
+
if ((expired || Date.now() - lastSaved >= 1_000) && await readOperationRecord(root,id,'requested.json')) { await writeJsonAtomic(path.join(operationDirectory(root,id),'observer-recovery.json'),clock.state()); lastSaved = Date.now(); }
|
|
67
|
+
if (expired) {
|
|
68
|
+
const final = await retainedDaemonReply(root, id, binding);
|
|
69
|
+
signal?.throwIfAborted();
|
|
70
|
+
if (final?.ready) return final.value;
|
|
71
|
+
throw fault(final ? 'settlement_reconciliation_required' : 'operation_observation_lost', 'original operation has no dispatch-ready reply', { operation_id: id, native_outcome: final ? 'completed' : 'unknown', native_result: final?.outcome, settlement: final?.settlement, reason:final?.reason, ...(settlementError ? { settlement_error:settlementError } : {}), state_dir: root });
|
|
72
|
+
}
|
|
73
|
+
if (retained && onPendingSettlement && !settlementAttempted && !retained.reason) {
|
|
74
|
+
settlementAttempted = true;
|
|
75
|
+
try { await onPendingSettlement(retained); } catch (error) { settlementError = {code:error.code,message:error.message}; }
|
|
76
|
+
signal?.throwIfAborted();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(100, timeoutMs / 4)));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function callDaemonObserved({ stateDir, socket, request, responseSchema, harness, timeoutMs }) {
|
|
83
|
+
request = { ...request, operation: canonicalNativeOperation(harness, request.operation) };
|
|
84
|
+
validateDaemonWait(harness, request.operation, timeoutMs);
|
|
85
|
+
assertNativeLine(JSON.stringify(request), 'request');
|
|
86
|
+
const durable = productive.has(request.operation);
|
|
87
|
+
const binding = { operation: request.operation, params_sha256: hashJson({ operation: request.operation, params: request.params ?? {} }) };
|
|
88
|
+
let initialDaemon;
|
|
89
|
+
if (durable) try {
|
|
90
|
+
const state = JSON.parse(await readFile(path.join(stateDir,'daemon.json'),'utf8'));
|
|
91
|
+
initialDaemon = state.daemon_id ?? state.config?.daemonId ?? null;
|
|
92
|
+
binding.owner_id = state.config?.runtime_owner?.owner_id ?? null;
|
|
93
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
94
|
+
// Reject a reused ID with different inputs before opening a transport.
|
|
95
|
+
// Otherwise its retained progress can win the race with the daemon's
|
|
96
|
+
// conflict reply and misclassify a valid old receipt as corrupt progress.
|
|
97
|
+
if (durable) await retainedDaemonReply(stateDir, request.id, {
|
|
98
|
+
...binding, ...(initialDaemon === undefined ? {} : { daemon_id: initialDaemon })
|
|
99
|
+
});
|
|
100
|
+
const savedClock = durable ? await readOperationRecord(stateDir,request.id,'observer-transport.json') : null;
|
|
101
|
+
return await new Promise((resolve, reject) => {
|
|
102
|
+
const client = net.createConnection(socket);
|
|
103
|
+
let buffer = '', done = false, polling = false, pollFlight, lastSequence = savedClock?.cursor ?? 0, frozenDaemon = initialDaemon, disconnected = false, latestProgress, lastSaved = 0;
|
|
104
|
+
const bound = () => ({ ...binding, ...(frozenDaemon === undefined ? {} : { daemon_id: frozenDaemon }) });
|
|
105
|
+
const nativeWork = nativeOperationWait(harness, request.operation) === 'native-work';
|
|
106
|
+
let clock = nativeWork ? null : new ObservationClock({ timeoutMs, saved: savedClock });
|
|
107
|
+
const finish = (error, result) => { if (done) return; done = true; clearInterval(timer); client.destroy(); void Promise.resolve(pollFlight).then(() => error ? reject(error) : resolve(result)); };
|
|
108
|
+
const reconcile = async error => {
|
|
109
|
+
if (done) return;
|
|
110
|
+
try {
|
|
111
|
+
const reply = durable ? await retainedDaemonReply(stateDir, request.id, bound()) : null;
|
|
112
|
+
if (reply?.ready) return finish(null, reply.value);
|
|
113
|
+
if (reply) error = fault('settlement_reconciliation_required', 'native outcome retained; dispatch readiness is not confirmed', { operation_id: request.id, native_outcome: 'completed', native_result: reply.outcome, settlement: reply.settlement, reason:reply.reason, state_dir: stateDir });
|
|
114
|
+
else if (durable) error = fault(error.code, error.message, { ...error.details, operation_id: request.id, native_outcome: 'unknown', state_dir: stateDir });
|
|
115
|
+
finish(error);
|
|
116
|
+
} catch (cause) { finish(cause); }
|
|
117
|
+
};
|
|
118
|
+
const poll = () => {
|
|
119
|
+
if (done || polling) return pollFlight; polling = true;
|
|
120
|
+
pollFlight = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const progress = durable ? await readOperationProgress(stateDir, request.id, { ...binding, ...(frozenDaemon === undefined ? {} : { daemon_id: frozenDaemon }) }) : null;
|
|
123
|
+
if (done) return;
|
|
124
|
+
if (progress && progress.sequence > lastSequence) {
|
|
125
|
+
frozenDaemon ??= progress.daemon_id; lastSequence = progress.sequence;
|
|
126
|
+
binding.generation ??= progress.generation;
|
|
127
|
+
latestProgress = {cursor:progress.sequence,observed_at:Date.parse(progress.observed_at)};
|
|
128
|
+
process.stderr.write(`${JSON.stringify({ kind: 'operation_progress', ...progress })}\n`);
|
|
129
|
+
}
|
|
130
|
+
if (durable && disconnected) {
|
|
131
|
+
const reply = await retainedDaemonReply(stateDir, request.id, bound());
|
|
132
|
+
if (reply?.ready) return finish(null, reply.value);
|
|
133
|
+
}
|
|
134
|
+
const waiting = durable && await readOperationWaiting(stateDir,request.id,bound());
|
|
135
|
+
if (done) return;
|
|
136
|
+
if (clock && waiting) suspendOperationClock(clock);
|
|
137
|
+
const expired = !waiting && clock?.sample(latestProgress ?? (durable ? undefined : buffer.length || undefined));
|
|
138
|
+
if (durable && clock && (expired || Date.now()-lastSaved>=1_000) && await readOperationRecord(stateDir,request.id,'requested.json')) { await writeJsonAtomic(path.join(operationDirectory(stateDir,request.id),'observer-transport.json'),clock.state()); lastSaved=Date.now(); }
|
|
139
|
+
if (expired) await reconcile(fault('daemon_timeout', `${request.operation} timed out`));
|
|
140
|
+
} catch (error) { finish(error); } finally { polling = false; }
|
|
141
|
+
})();
|
|
142
|
+
return pollFlight;
|
|
143
|
+
};
|
|
144
|
+
const timer = setInterval(poll, Math.max(1, Math.min(100, (timeoutMs ?? 400) / 4)));
|
|
145
|
+
client.setEncoding('utf8'); client.on('connect', () => client.write(`${JSON.stringify(request)}\n`));
|
|
146
|
+
const lost = error => { if (durable) { disconnected = true; clock ??= new ObservationClock({timeoutMs: timeoutMs ?? 30_000,saved:savedClock}); void poll(); } else void reconcile(error); };
|
|
147
|
+
client.on('error', error => lost(fault('daemon_not_running', error.message)));
|
|
148
|
+
client.on('end', () => lost(fault('daemon_connection_closed', 'daemon closed before reply')));
|
|
149
|
+
client.on('close', () => lost(fault('daemon_connection_closed', 'daemon closed before reply')));
|
|
150
|
+
client.on('data', chunk => { void (async () => {
|
|
151
|
+
buffer += chunk; if (!retainNativeReplyBuffer(buffer, finish)) return;
|
|
152
|
+
const at = buffer.indexOf('\n'); if (at < 0) return;
|
|
153
|
+
try {
|
|
154
|
+
const response = JSON.parse(buffer.slice(0, at));
|
|
155
|
+
if (response.schema_id !== responseSchema || response.id !== request.id) throw fault('operation_observation_lost', 'daemon reply identity mismatch');
|
|
156
|
+
if (!response.ok) return finish(Object.assign(new Error(response.error?.message ?? 'daemon operation failed'), response.error));
|
|
157
|
+
if (durable && (binding.owner_id && !response.result?.settlement || response.result?.settlement && !['settled','not_required'].includes(response.result.settlement.state))) { disconnected = true; clock ??= new ObservationClock({timeoutMs:savedClock?.timeout_ms ?? timeoutMs ?? 30_000,saved:savedClock}); void poll(); return; }
|
|
158
|
+
if (durable) { const retained=await retainedDaemonReply(stateDir,request.id,bound()); if(retained) { if(retained.ready) return finish(null,retained.value); disconnected=true; clock ??= new ObservationClock({timeoutMs:timeoutMs ?? 30_000,saved:savedClock}); void poll(); return; } return void reconcile(fault('operation_observation_lost','productive native reply has no durable original receipt')); }
|
|
159
|
+
finish(null, response.result);
|
|
160
|
+
} catch (error) { void reconcile(error.code ? error : Object.assign(fault('operation_observation_lost', 'daemon reply could not be decoded', {native_outcome:'unknown'}), {cause:error})); }
|
|
161
|
+
})(); });
|
|
162
|
+
});
|
|
163
|
+
}
|