@deksden-com/dd-flow-cli 0.9.0-beta.127 → 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 +58 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +2 -2
- package/dist/cli/run-cli.js +6 -6
- package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
- package/dist/cli/runtime-maintenance-ingress.js +5 -4
- package/dist/cli/runtime-process.js +27 -2
- package/dist/cli.js +7 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
- package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
- package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +41 -9
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/services/branch-context.js +3 -3
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +1 -1
- package/dist/services/code-checks.js +113 -31
- package/dist/services/compatibility-preflight.js +9 -2
- package/dist/services/dashboard.js +33 -26
- package/dist/services/engines.js +88 -57
- package/dist/services/lifecycle-invocations.js +4 -2
- package/dist/services/managed-lease-monitor.js +9 -4
- package/dist/services/prompts.js +3 -1
- package/dist/services/protocols.js +3 -1
- package/dist/services/repair-intents.js +4 -2
- package/dist/services/run-control-worker.js +2 -1
- package/dist/services/run-controller.js +20 -17
- package/dist/services/run-fork.js +2 -2
- package/dist/services/runtime-scope-worker.js +2 -2
- package/dist/services/stage-lifecycle.js +13 -7
- package/dist/services/tooling.js +32 -23
- package/dist/services/vnext-code-review.js +2 -0
- package/dist/services/vnext-code.js +5 -1
- package/dist/services/vnext-merge.js +4 -1
- package/dist/services/vnext-protocolize.js +17 -16
- package/dist/services/work-registry.js +3 -1
- package/dist/services/worktrees.js +16 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.133
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Use the exact retained committed lease for admission and capacity waits,
|
|
8
|
+
rather than an expired daemon-state snapshot.
|
|
9
|
+
- Validate direct check runner evidence contracts before PLAN/Work admission.
|
|
10
|
+
- beta.132 is retained as a superseded local candidate; review found the stale
|
|
11
|
+
admission horizon before release or scored E2E.
|
|
12
|
+
|
|
13
|
+
## 0.9.0-beta.132
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Preserve structured CLI failure receipts when valid setup-phase diagnostics
|
|
18
|
+
precede the error envelope; reject malformed/error-bearing phase records.
|
|
19
|
+
- beta.131 remains an unaccepted immutable local candidate; its full gate
|
|
20
|
+
exposed this parsing defect before engine promotion.
|
|
21
|
+
|
|
22
|
+
## 0.9.0-beta.131
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Bound maintenance to verified lease authority, reconcile settled unknown
|
|
27
|
+
heartbeat acknowledgements without replay, and separate resource/RUN effects.
|
|
28
|
+
- Keep scope-free maintenance cold imports narrow; finish observes its own
|
|
29
|
+
settlement window after the preceding flight.
|
|
30
|
+
- Retain bound structured test outcomes and progress with profile@7, preserving
|
|
31
|
+
profile@6 compatibility. Route tooling/unknown failures away from product repair.
|
|
32
|
+
|
|
33
|
+
## 0.9.0-beta.130
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Bind AGY startup observation to its actual daemon configuration, preserve
|
|
38
|
+
explicit OpenCode observation windows and retain the last confirmed setup phase.
|
|
39
|
+
- beta.129 remained an unaccepted local candidate; its failed native gate is
|
|
40
|
+
retained separately rather than reissued under a different artifact digest.
|
|
41
|
+
|
|
42
|
+
## 0.9.0-beta.129
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- Observe setup phases across engine admission and all native daemon startups;
|
|
47
|
+
retain operation-bound progress and distinguish silence from observation loss.
|
|
48
|
+
- Run engine health/bind/fork probes and managed tooling asynchronously outside
|
|
49
|
+
storage writers, preserving dependency failures, cancellation and owned cleanup.
|
|
50
|
+
- Stream ZCode binary identity and preserve startup progress through EVAL control.
|
|
51
|
+
|
|
52
|
+
## 0.9.0-beta.128
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- Separate cold maintenance CLI imports from storage inactivity. Start the short
|
|
57
|
+
sliding storage window only after request-bound readiness, retaining the finite
|
|
58
|
+
ownership horizon and unknown-write recovery. Diagnose startup versus storage
|
|
59
|
+
expiry and preserve exact registration operation identity.
|
|
60
|
+
|
|
3
61
|
## 0.9.0-beta.127
|
|
4
62
|
|
|
5
63
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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-10-
|
|
3
|
+
"cli_version": "0.9.0-beta.133",
|
|
4
|
+
"cli_commit": "43d88ec57e1b100fa29f31be925d6abfc976e7a2",
|
|
5
|
+
"built_at": "2026-10-12T17:50:10.957Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.2",
|
|
8
8
|
"commit": "2ba0b9ceedf8e4544ad9566c240ef37ea5e8d099",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
|
-
"repo_root": "/Users/deksden
|
|
11
|
-
"memorybank_root": "/Users/deksden
|
|
12
|
-
"flow_root": "/Users/deksden
|
|
10
|
+
"repo_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-memorybank-081-canon",
|
|
11
|
+
"memorybank_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-memorybank-081-canon/.memory-bank",
|
|
12
|
+
"flow_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-memorybank-081-canon/.memory-bank/dd-flow",
|
|
13
13
|
"layout": "dot_memory_bank"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -128,11 +128,11 @@ export const commandInputs = {
|
|
|
128
128
|
"runtime process stop": route(0, "id lease-token", "grace-ms"),
|
|
129
129
|
"runtime process register": route(0, "kind owner", "id lease-ms budget-json project-id run pid process-group-id owner-pid work check operation stdout stderr harness dispatch-barrier role state-dir adapter-executable"),
|
|
130
130
|
"runtime process confirm": route(0, "id lease-token pid", "owner-pid lease-ms process-group-id"),
|
|
131
|
-
"runtime process heartbeat": route(0, "id lease-token", "lease-ms"),
|
|
131
|
+
"runtime process heartbeat": route(0, "id lease-token", "lease-ms registration-sha256"),
|
|
132
132
|
"runtime process admission": route(0, "id lease-token owner state-dir", "daemon-id operation-id operation native-session-id prompt-sha256"),
|
|
133
133
|
"runtime process release-turn": route(0, "id lease-token owner daemon-id", "operation-id observation-file"),
|
|
134
134
|
"runtime process finish": route(0, "id lease-token state", "reason", "", "", { state: ["stopped", "failed"] }),
|
|
135
|
-
"runtime process status": route(0),
|
|
135
|
+
"runtime process status": route(0, "", "id lease-token registration-sha256"),
|
|
136
136
|
"runtime process reconcile": route(0, "owner", "grace-ms"),
|
|
137
137
|
"lane status": route(0, project, "lane"),
|
|
138
138
|
"lane waiters": route(0, project, "lane"),
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -344,7 +344,7 @@ async function runCliRequest(args, io, env) {
|
|
|
344
344
|
if (!retainedOutcome && !preparedInput)
|
|
345
345
|
preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile);
|
|
346
346
|
if (!retainedOutcome)
|
|
347
|
-
preflightCliCompatibility(dispatchContext, invocationArgs, env, scopeProjectRoot);
|
|
347
|
+
await preflightCliCompatibility(dispatchContext, invocationArgs, env, scopeProjectRoot);
|
|
348
348
|
if (!retainedOutcome) {
|
|
349
349
|
const command = !invocationId ? nativeCommand : quote(["dd-flow", ...dispatchArgs]);
|
|
350
350
|
const event = !invocationId ? nativeEvent : null;
|
|
@@ -937,7 +937,7 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
|
|
|
937
937
|
if (route.key === "run override")
|
|
938
938
|
prepareFlowRunCompletion(context, runOverrideInput(parsed));
|
|
939
939
|
if (route.key === "engine bind-run")
|
|
940
|
-
prepared.engineBind = prepareRunEngineBind(context, engineBindInput(parsed));
|
|
940
|
+
prepared.engineBind = await prepareRunEngineBind(context, engineBindInput(parsed));
|
|
941
941
|
if (route.key === "worktree create")
|
|
942
942
|
prepared.worktreeCreate = prepareWorktreeCreate(context, { protocolId: requiredOption(parsed, "protocol-id"), projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "worktree create"), branch: requiredOption(parsed, "branch"), base: requiredOption(parsed, "base"), path: requiredOption(parsed, "path") });
|
|
943
943
|
if (route.key === "worktree bootstrap")
|
|
@@ -1792,7 +1792,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1792
1792
|
}
|
|
1793
1793
|
if (!bootstrap && stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1794
1794
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
1795
|
-
return attachExternalStageContext(startVnextProtocolize(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.protocolizeStart), externalContext);
|
|
1795
|
+
return attachExternalStageContext(await startVnextProtocolize(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id"), hookEventId, ...(contextSha256 ? { contextSha256 } : {}), ...(externalContext ? { externalContext } : {}) }, prepared.protocolizeStart), externalContext);
|
|
1796
1796
|
}
|
|
1797
1797
|
if (!bootstrap && stage === "plan" && isVnextPlanRun(context, { projectRoot, runId: requiredPosition(parsed, 0, "run-id") })) {
|
|
1798
1798
|
hookEventId ??= claimVnextStageStart(requiredPosition(parsed, 0, "run-id"));
|
|
@@ -2698,7 +2698,7 @@ function dispatchCmux(context, parsed) {
|
|
|
2698
2698
|
}
|
|
2699
2699
|
throw new AppError("usage", `Unknown integration cmux action: ${action}`, 2);
|
|
2700
2700
|
}
|
|
2701
|
-
function dispatchDashboard(context, command, parsed) {
|
|
2701
|
+
async function dispatchDashboard(context, command, parsed) {
|
|
2702
2702
|
const action = dashboardAction(command);
|
|
2703
2703
|
if (command === "data") {
|
|
2704
2704
|
const target = resolveDashboardTarget(context, {
|
|
@@ -2733,7 +2733,7 @@ function dispatchDashboard(context, command, parsed) {
|
|
|
2733
2733
|
projectRoot: optionalOption(parsed, "project-root"),
|
|
2734
2734
|
protocol: optionalOption(parsed, "protocol")
|
|
2735
2735
|
});
|
|
2736
|
-
return withDashboardGuidance(action, target, openDashboardForResolvedTarget(context, target, parsed));
|
|
2736
|
+
return withDashboardGuidance(action, target, await openDashboardForResolvedTarget(context, target, parsed));
|
|
2737
2737
|
}
|
|
2738
2738
|
if (command === "refresh") {
|
|
2739
2739
|
const open = optionalOption(parsed, "open");
|
|
@@ -2747,7 +2747,7 @@ function dispatchDashboard(context, command, parsed) {
|
|
|
2747
2747
|
protocol: optionalOption(parsed, "protocol"),
|
|
2748
2748
|
all: hasOption(parsed, "all")
|
|
2749
2749
|
});
|
|
2750
|
-
return withDashboardGuidance(action, target, refreshDashboardForResolvedTarget(context, target, parsed, open));
|
|
2750
|
+
return withDashboardGuidance(action, target, await refreshDashboardForResolvedTarget(context, target, parsed, open));
|
|
2751
2751
|
}
|
|
2752
2752
|
if (command === "refresh-global") {
|
|
2753
2753
|
const target = resolveDashboardTarget(context, { action: "refresh", global: true });
|
|
@@ -11,7 +11,7 @@ export function runtimeMaintenanceDiagnostics(args, deadline, write) {
|
|
|
11
11
|
const request = requests.getStore();
|
|
12
12
|
const option = (name) => { const at = args.indexOf(`--${name}`); return at < 0 ? undefined : args[at + 1]; };
|
|
13
13
|
const safe = (value) => typeof value === "string" && /^[A-Za-z0-9:._-]{1,128}$/.test(value) ? value : undefined;
|
|
14
|
-
const identity = { action: safe(args[2]), process_id: safe(option("id")), operation_id: safe(option("operation-id")), native_dispatch_started: false };
|
|
14
|
+
const identity = { action: safe(args[2]), process_id: safe(option("id")), operation_id: safe(option("operation-id") ?? option("operation")), native_dispatch_started: false };
|
|
15
15
|
let previous = "", count = 0, committed = false;
|
|
16
16
|
const phase = (name, details = {}) => {
|
|
17
17
|
const key = `${details.store_kind ?? ""}:${name}`;
|
|
@@ -16,9 +16,11 @@ export async function runRuntimeMaintenanceIngress(args, env = process.env, io =
|
|
|
16
16
|
const diagnostics = runtimeMaintenanceDiagnostics(args, deadline, line => io.stderr.write(line));
|
|
17
17
|
let context;
|
|
18
18
|
try {
|
|
19
|
-
const [{ createContext }, { prepareCommandRoute }, { resolveCommandProjectRoot, resolveCommandPaths }, processCommands] = await Promise.all([
|
|
20
|
-
import("../runtime/context.js"), import("./command-inputs.js"), import("../services/command-context.js"), import("./runtime-process.js")
|
|
19
|
+
const [{ createContext }, { prepareCommandRoute }, { resolveCommandProjectRoot, resolveCommandPaths }, processCommands, { preflightCliCompatibility }] = await Promise.all([
|
|
20
|
+
import("../runtime/context.js"), import("./command-inputs.js"), import("../services/command-context.js"), import("./runtime-process.js"),
|
|
21
|
+
import("../services/compatibility-preflight.js")
|
|
21
22
|
]);
|
|
23
|
+
diagnostics.phase("imports_ready");
|
|
22
24
|
const invocation = args.filter(arg => arg !== "--json");
|
|
23
25
|
const options = { databaseDeadlineAt: deadline, resourceDatabaseMode: args[2] === "register" ? "initialize" : args[2] === "status" ? "read_existing" : "prepared_existing" };
|
|
24
26
|
context = createContext({ ...env, DD_FLOW_INVOCATION_READONLY: args[2] === "status" ? "1" : undefined }, "read_existing", undefined, options);
|
|
@@ -28,8 +30,7 @@ export async function runRuntimeMaintenanceIngress(args, env = process.env, io =
|
|
|
28
30
|
const prepared = processCommands.prepareRuntimeProcess(context, route.key, route.parsed);
|
|
29
31
|
// Preserve the common compatibility gate; this is not a second dispatch
|
|
30
32
|
// implementation or permission granted by the ingress predicate.
|
|
31
|
-
|
|
32
|
-
preflightCliCompatibility(context, routedArgs, env, root);
|
|
33
|
+
await preflightCliCompatibility(context, routedArgs, env, root);
|
|
33
34
|
const result = await processCommands.dispatchRuntimeProcess(context, args[2], route.parsed, prepared);
|
|
34
35
|
const receipt = result;
|
|
35
36
|
const expiry = receipt?.lease_expires_at ?? receipt?.process?.lease_expires_at;
|
|
@@ -5,6 +5,7 @@ import { recoveryGuard, assertRunMutationAllowed } from "../services/run-recover
|
|
|
5
5
|
import { recoveryRuntimeAdmission } from "../services/run-recovery-guard.js";
|
|
6
6
|
import { prepareManagedProcessRegistration, prepareManagedProcessConfirmation, prepareManagedProcessHeartbeat, prepareManagedProcessFinish, prepareManagedProcessStopOperation, prepareManagedProcessReconcile, stopManagedProcess, confirmManagedProcess, finishManagedProcess, renewManagedProcess, assertManagedProcessAdmission, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
7
7
|
import { admitRuntimeCheck, parseRuntimeBudget, reserveProviderTurn, releaseProviderTurns, prepareProviderTurnRelease } from "../services/runtime-budget.js";
|
|
8
|
+
import { managedProcessBinding, resourceHome } from "../services/managed-processes.js";
|
|
8
9
|
function runtimeProcessStopInput(parsed) {
|
|
9
10
|
const graceMs = optionalNumber(parsed, "grace-ms");
|
|
10
11
|
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
|
|
@@ -80,6 +81,12 @@ export async function dispatchRuntimeProcess(context, action, parsed, prepared =
|
|
|
80
81
|
if (action === "confirm")
|
|
81
82
|
return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
|
|
82
83
|
if (action === "heartbeat") {
|
|
84
|
+
const hash = optionalOption(parsed, "registration-sha256");
|
|
85
|
+
if (hash) {
|
|
86
|
+
const record = managedProcessStatus(context).find(value => value.id === requiredOption(parsed, "id"));
|
|
87
|
+
if (!/^[a-f0-9]{64}$/.test(hash) || !record || managedProcessBinding(record, resourceHome(context)).registration_sha256 !== hash)
|
|
88
|
+
throw new AppError("process_maintenance_receipt_invalid", "Heartbeat registration changed", 1);
|
|
89
|
+
}
|
|
83
90
|
const receipt = renewManagedProcess(context, runtimeProcessHeartbeatInput(parsed));
|
|
84
91
|
return { ...receipt, ok: receipt.owned };
|
|
85
92
|
}
|
|
@@ -120,8 +127,26 @@ export async function dispatchRuntimeProcess(context, action, parsed, prepared =
|
|
|
120
127
|
}
|
|
121
128
|
if (action === "finish")
|
|
122
129
|
return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
|
|
123
|
-
if (action === "status")
|
|
124
|
-
|
|
130
|
+
if (action === "status") {
|
|
131
|
+
const id = optionalOption(parsed, "id"), token = optionalOption(parsed, "lease-token"), hash = optionalOption(parsed, "registration-sha256");
|
|
132
|
+
if (!id && !token && !hash)
|
|
133
|
+
return { ok: true, processes: managedProcessStatus(context) };
|
|
134
|
+
if (!id || !token || !hash || !/^[a-f0-9]{64}$/.test(hash))
|
|
135
|
+
throw new AppError("usage", "Exact status requires id, lease-token and registration-sha256", 2);
|
|
136
|
+
const record = managedProcessStatus(context).find(value => value.id === id);
|
|
137
|
+
if (!record || record.lease_token !== token || managedProcessBinding(record, resourceHome(context)).registration_sha256 !== hash || record.state !== "running" || !processIsAlive(record))
|
|
138
|
+
throw new AppError("process_maintenance_receipt_invalid", "Readback binding or physical owner changed", 1);
|
|
139
|
+
assertManagedProcessAdmission(context, record);
|
|
140
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
141
|
+
if (record.project_id && record.run_id) {
|
|
142
|
+
if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
|
|
143
|
+
throw new AppError("run_recovery_guarded", "Readback has no admitted generation", 1);
|
|
144
|
+
assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
|
|
145
|
+
if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
146
|
+
throw new AppError("process_runtime_scope_mismatch", "Readback belongs to another engine home", 1);
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, process_id: id, lease_expires_at: record.lease_expires_at, registration_sha256: hash, readback: true };
|
|
149
|
+
}
|
|
125
150
|
if (action === "reconcile")
|
|
126
151
|
return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
|
|
127
152
|
throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { isNativeHookIngress } from "./cli/native-hook-ingress.js";
|
|
3
3
|
import { isRuntimeMaintenanceIngress, runRuntimeMaintenanceIngress } from "./cli/runtime-maintenance-ingress.js";
|
|
4
|
+
import { publishSetup } from './harness-runtime/lib/setup-progress.mjs';
|
|
4
5
|
const args = process.argv.slice(2);
|
|
5
6
|
const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle" && !args.includes("--help") && !args.includes("-h");
|
|
6
7
|
const exitCode = isRuntimeMaintenanceIngress(args, process.env)
|
|
@@ -9,5 +10,10 @@ const exitCode = isRuntimeMaintenanceIngress(args, process.env)
|
|
|
9
10
|
? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
|
|
10
11
|
: isNativeHookIngress(args) && !args.includes("--help") && !args.includes("-h")
|
|
11
12
|
? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
|
|
12
|
-
: await (
|
|
13
|
+
: await (async () => {
|
|
14
|
+
publishSetup('ingress_entered');
|
|
15
|
+
const cli = await import('./cli/run-cli.js');
|
|
16
|
+
publishSetup('imports_ready');
|
|
17
|
+
return await cli.runCli(args);
|
|
18
|
+
})();
|
|
13
19
|
process.exitCode = exitCode;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
1
2
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
3
|
import { operationProgressReporter } from './operation-progress.mjs';
|
|
3
4
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
@@ -201,7 +202,7 @@ export async function startDaemon(options) {
|
|
|
201
202
|
const log = await open(paths.log, "a", 0o600), child = spawn(process.execPath, [config.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: sanitizedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
202
203
|
try {
|
|
203
204
|
await confirmDaemonProcess(config, resourceProcess, child);
|
|
204
|
-
const startupClock = new ObservationClock({ timeoutMs:
|
|
205
|
+
const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, config.daemonId); while (!startupClock.sample(await startupProgress())) { await new Promise(resolve => setTimeout(resolve, 100));
|
|
205
206
|
const persisted = await readState(paths.dir);
|
|
206
207
|
if (persisted?.shutdown_state === "failed") {
|
|
207
208
|
const failure = persisted.startup_error ?? {};
|
|
@@ -209,7 +210,7 @@ export async function startDaemon(options) {
|
|
|
209
210
|
}
|
|
210
211
|
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (status.provider_ready) return status; } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
|
|
211
212
|
}
|
|
212
|
-
throw new DaemonError("daemon_start_failed", "Antigravity daemon did not become ready", true);
|
|
213
|
+
throw Object.assign(new DaemonError("daemon_start_failed", "Antigravity daemon did not become ready", true), { details: observationDetails(startupClock, 'daemon-start', child.pid) });
|
|
213
214
|
} catch (error) {
|
|
214
215
|
await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.dir), config.daemonId);
|
|
215
216
|
throw error;
|
|
@@ -322,7 +323,7 @@ export class Runtime {
|
|
|
322
323
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
|
|
323
324
|
finally { allowProviderFinalization(); }
|
|
324
325
|
let readyTimer;
|
|
325
|
-
try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = observedTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)),
|
|
326
|
+
try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = observedTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), SETUP_STARTUP_MS, { progress: () => this.lastActivityAt }); })]); }
|
|
326
327
|
finally { clearTimeout(readyTimer); }
|
|
327
328
|
}
|
|
328
329
|
async drain(readyResolve) { let index; while ((index = this.buffer.indexOf("\n")) >= 0) { const line = this.buffer.slice(0, index).trim(); this.buffer = this.buffer.slice(index + 1); if (!line) continue; let event; try { event = JSON.parse(line); } catch { await this.journal("invalid_json", { line: line.slice(0, 1000) }); continue; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { SETUP_STARTUP_MS } from './setup-progress.mjs';
|
|
4
5
|
|
|
5
6
|
import { checkObservedProfile } from "./model-observations.mjs";
|
|
6
7
|
|
|
@@ -24,7 +25,7 @@ export function executable(bin, args = []) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export async function runAgy(bin, args, options = {}) {
|
|
27
|
-
const stdout = await commandText(bin, args, { cwd: options.cwd, env: options.env, input: options.stdin ?? "", timeoutMs: options.timeoutMs, phase: "native-doctor" });
|
|
28
|
+
const stdout = await commandText(bin, args, { cwd: options.cwd, env: options.env, input: options.stdin ?? "", timeoutMs: options.timeoutMs, startupMs: options.startupMs ?? options.timeoutMs ?? SETUP_STARTUP_MS, phase: "native-doctor" });
|
|
28
29
|
return { stdout, stderr: "", code: 0 };
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -48,8 +49,8 @@ export function assertProfile(requested, observed) { return checkObservedProfile
|
|
|
48
49
|
export async function doctor(options = {}) {
|
|
49
50
|
const bin = options.bin ?? process.env.DD_AGY_BIN ?? "agy", temporary = await mkdtemp(path.join(os.tmpdir(), "dd-agy-doctor-"));
|
|
50
51
|
try {
|
|
51
|
-
const version = (await runAgy(bin, ["--version"], { timeoutMs: AGY_VERSION_TIMEOUT_MS })).stdout.trim();
|
|
52
|
-
const models = await runAgy(bin, [`--gemini_dir=${temporary}`, "--app_data_dir=runtime", "models"], { timeoutMs: options.timeoutMs ?? 30_000 });
|
|
52
|
+
const version = (await runAgy(bin, ["--version"], { timeoutMs: AGY_VERSION_TIMEOUT_MS, startupMs: options.timeoutMs ?? SETUP_STARTUP_MS })).stdout.trim();
|
|
53
|
+
const models = await runAgy(bin, [`--gemini_dir=${temporary}`, "--app_data_dir=runtime", "models"], { timeoutMs: options.timeoutMs ?? 30_000, startupMs: options.timeoutMs ?? SETUP_STARTUP_MS });
|
|
53
54
|
const modelIds = models.stdout.split(/\r?\n/).map(line => line.split(/\s+/)[0]).filter(Boolean);
|
|
54
55
|
if (!modelIds.includes(options.model ?? DEFAULT_MODEL)) throw new AgyError("agy_model_unavailable", `required Antigravity model is unavailable: ${options.model ?? DEFAULT_MODEL}`);
|
|
55
56
|
return { compatible: true, observed_runtime: { agy: version, dd_harness_contract: "dd-agy-harness@1" }, isolation: { gemini_dir: "verified", app_data_dir: "verified" }, auth: "available", models: modelIds };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
1
2
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
3
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
3
4
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
@@ -87,7 +88,7 @@ export async function startDaemon(options) {
|
|
|
87
88
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
88
89
|
await removeSocket(paths.socket); const resourceProcess = await registerDaemonProcess(config, { kind: "codex-daemon", owner: `codex:${path.basename(paths.dir)}`, operation: `codex-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log }); const state = { schema_id: stateSchema, daemon_id: randomUUID(), pid: null, cwd: config.cwd, journal: config.journal, config, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", sessions: previous?.sessions ?? [], resource_process: resourceProcess }; await writeState(paths.state, state);
|
|
89
90
|
const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [options.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
90
|
-
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs:
|
|
91
|
+
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, state.daemon_id); while (!startupClock.sample(await startupProgress())) { await new Promise((resolve) => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 5_000); } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; } } throw Object.assign(new Error("daemon did not become ready"), { code: "daemon_start_failed", details: observationDetails(startupClock, "daemon-start", child.pid) }); }
|
|
91
92
|
catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.state), state.daemon_id); throw error; }
|
|
92
93
|
}
|
|
93
94
|
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(locations(options.stateDir).state) }); }
|
|
@@ -11,6 +11,7 @@ import { observeModel } from "./model-observations.mjs";
|
|
|
11
11
|
import { classifyCodexItem } from "./tool-observations.mjs";
|
|
12
12
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
13
13
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
14
|
+
import { publishSetup } from './setup-progress.mjs';
|
|
14
15
|
|
|
15
16
|
const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
16
17
|
const CODEX_SESSION_CONFIG = Object.freeze({
|
|
@@ -75,7 +76,9 @@ export class CodexBridge {
|
|
|
75
76
|
this.child.on("close", code => { void Promise.all([this.nativeDrain, this.diagnostics]).then(() => this.rejectAll(Object.assign(new Error(`Codex app-server exited ${code}${this.stderr.trim() ? `: ${this.stderr.trim()}` : ""}`), { code: "bridge_exited" }))); });
|
|
76
77
|
this.nativeDrain = readNativeLines(this.child.stdout, async line => { this.receive(line); await this.journal.flush(); }, error => this.rejectAll(error));
|
|
77
78
|
if (this.options.onSpawn) await this.options.onSpawn(this.child);
|
|
79
|
+
publishSetup('probe_started', 'codex-initialize');
|
|
78
80
|
await this.request("initialize", { clientInfo: { name: "dd-codex", version: "0.1.0" }, capabilities: { optOutNotificationMethods: [], experimentalApi: true } });
|
|
81
|
+
publishSetup('native_initialized', 'codex-initialize');
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
receive(line) {
|
|
@@ -501,7 +504,7 @@ function threadReceipt(thread) {
|
|
|
501
504
|
}
|
|
502
505
|
|
|
503
506
|
export async function doctor(options = {}) {
|
|
504
|
-
const codex_cli = parseCodexCliVersion(await commandText(options.bin ?? process.env.DD_CODEX_BIN ?? "codex", ["--version"]));
|
|
507
|
+
const codex_cli = parseCodexCliVersion(await commandText(options.bin ?? process.env.DD_CODEX_BIN ?? "codex", ["--version"], { phase: 'native-version' }));
|
|
505
508
|
return await withBridge(options, async () => ({ harness: "codex-desktop", app_server: "available", observed_runtime: { codex_cli, dd_harness_contract: "dd-codex-harness@1" }, requested_profile: profile(options), observed_at: now() }));
|
|
506
509
|
}
|
|
507
510
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
1
2
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
3
|
import { admitNativeRequestBuffer, nativeObservationError } from "./native-streams.mjs";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -44,9 +45,9 @@ export async function startDaemon(options) {
|
|
|
44
45
|
const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [config.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
45
46
|
try {
|
|
46
47
|
await confirmDaemonProcess(config, resource, child);
|
|
47
|
-
const startupClock = new ObservationClock({ timeoutMs:
|
|
48
|
-
while (!startupClock.sample()) { await new Promise(resolve => setTimeout(resolve, 100)); const saved = await readJson(paths.state); if (saved?.startup_error) throw Object.assign(new Error(saved.startup_error.message), saved.startup_error); try { return await callDaemon(paths.dir, "daemon.status", {}, 1_000); } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; } }
|
|
49
|
-
throw new DroidError("daemon_start_failed", "Droid daemon did not become ready");
|
|
48
|
+
const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, config.daemonId);
|
|
49
|
+
while (!startupClock.sample(await startupProgress())) { await new Promise(resolve => setTimeout(resolve, 100)); const saved = await readJson(paths.state); if (saved?.startup_error) throw Object.assign(new Error(saved.startup_error.message), saved.startup_error); try { return await callDaemon(paths.dir, "daemon.status", {}, 1_000); } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; } }
|
|
50
|
+
throw Object.assign(new DroidError("daemon_start_failed", "Droid daemon did not become ready"), { details: observationDetails(startupClock, 'daemon-start', child.pid) });
|
|
50
51
|
} catch (error) { await cleanupFailedStart(config, resource, child, error); throw error; }
|
|
51
52
|
}
|
|
52
53
|
export async function stopDaemon(options) { return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, 30_000), droidPaths(options.stateDir).socket, 30_000, { readState: () => readJson(droidPaths(options.stateDir).state) }); }
|
|
@@ -65,7 +65,7 @@ export async function prepareDroidHome(config) {
|
|
|
65
65
|
export async function removeDroidAuth(config) {
|
|
66
66
|
for (const name of ["auth.encrypted", "auth.v2.key", "auth.v2.file", "auth.json"]) await unlink(path.join(config.home, ".factory", name)).catch(error => { if (error.code !== "ENOENT") throw error; });
|
|
67
67
|
}
|
|
68
|
-
export async function droidVersion(bin) { const target = executable(bin, ["--version"]); return await commandText(target.command, target.args, {
|
|
68
|
+
export async function droidVersion(bin) { const target = executable(bin, ["--version"]); return await commandText(target.command, target.args, { phase: "native-version" }); }
|
|
69
69
|
|
|
70
70
|
/** The qualified OpenAI profile reports uncached input separately from caches;
|
|
71
71
|
* output already includes thinking. Keep native counters alongside normalization. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
1
2
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
3
|
import { canonicalNativeOperation } from './adapter-timeouts.mjs';
|
|
3
4
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
@@ -174,7 +175,7 @@ export async function startDaemon(options) {
|
|
|
174
175
|
const daemonId = randomUUID();
|
|
175
176
|
const state = { descendants: previous?.descendants ?? [], schema_id: STATE_SCHEMA, daemon_id: daemonId, pid: null, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", active_tree: false, recovery_status: previous?.shutdown_state === "running" ? "recovered_idle" : "clean_start", auth_status: copiedAuth ? "copied" : "absent", ...(importedSession ? { imported_session: importedSession } : {}), versions: versions.versions, config_isolation: configIsolation, config: { ...config, daemonId }, sessions: importedSession ? [{ provider_session_id: importedSession.provider_session_id, adapter_session_id: importedSession.provider_session_id, parent_provider_session_id: null, root_provider_session_id: importedSession.provider_session_id, cwd: importedSession.source_cwd }] : (previous?.sessions ?? []), resource_process: resourceProcess };
|
|
176
177
|
await writeState(paths.state, state); const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [absolute(options.entryPath, "dd-grok entry"), "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: isolatedEnv(state.config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
177
|
-
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs:
|
|
178
|
+
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, state.daemon_id); while (!startupClock.sample(await startupProgress())) { await new Promise((resolve) => setTimeout(resolve, 100)); const failure = grokStartupFailure(await readState(paths.dir)); if (failure) throw failure; try { const status = await callDaemon(paths.dir, "daemon.ready", {}, 5000); if (status.ready) return status; } catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; } } throw Object.assign(new DaemonError("daemon_start_failed", "daemon did not become ready", true), { details: observationDetails(startupClock, 'daemon-start', child.pid) }); }
|
|
178
179
|
catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.dir), daemonId); throw error; }
|
|
179
180
|
}
|
|
180
181
|
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
1
2
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
3
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
3
4
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
@@ -26,8 +27,9 @@ async function readState(dir) { try { return JSON.parse(await readFile(locations
|
|
|
26
27
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
27
28
|
function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
|
|
28
29
|
function isolatedEnv(config) { return { ...process.env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome, OPENCODE_CONFIG_DIR: config.adapterDir, OPENCODE_DISABLE_PROJECT_CONFIG: "1", OPENCODE_SERVER_PASSWORD: config.password, ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}), ...(config.ddFlowBin ? { DD_OPENCODE_DD_FLOW_BIN: config.ddFlowBin, DD_OPENCODE_NODE_BIN: process.execPath, DD_OPENCODE_PROJECT_ROOT: config.projectRoot } : {}), DD_OPENCODE_DAEMON_ID: config.daemonId, DD_OPENCODE_PROVIDER: config.provider, DD_OPENCODE_MODEL: config.model, DD_OPENCODE_VARIANT: config.variant, DD_OPENCODE_AGENT: config.agent, DD_OPENCODE_SERVER_VERSION: config.version ?? "unknown", DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "unknown" }; }
|
|
29
|
-
export async function run(bin, args, config,
|
|
30
|
-
|
|
30
|
+
export async function run(bin, args, config, options = {}) {
|
|
31
|
+
const { stdin, timeoutMs = 30_000, startupMs = options.timeoutMs ?? SETUP_STARTUP_MS } = options;
|
|
32
|
+
return await commandText(bin, args, { cwd: config.cwd, env: isolatedEnv(config), input: stdin ?? null, timeoutMs, startupMs, phase: args.includes('--version') ? 'native-version' : 'native-utility' });
|
|
31
33
|
}
|
|
32
34
|
async function forwardUsage(config, sessionId, snapshot) {
|
|
33
35
|
if (!config.ddFlowBin) return;
|
|
@@ -70,7 +72,7 @@ export async function startDaemon(options) {
|
|
|
70
72
|
const resourceProcess = await registerDaemonProcess(config, { kind: "opencode-daemon", owner: `opencode:${path.basename(paths.dir)}`, operation: `opencode-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
|
|
71
73
|
const state = { schema_id: STATE_SCHEMA, daemon_id: daemonId, shutdown_state: "starting", active_tree: false, auth_status: config.authStatus, versions: { opencode: config.version }, config, sessions: imported ? [{ provider_session_id: imported.provider_session_id, parent_provider_session_id: null, root_provider_session_id: imported.provider_session_id, cwd: config.cwd }] : (previous?.sessions ?? []), resource_process: resourceProcess }; await writeJson(paths.state, state);
|
|
72
74
|
const log = await open(paths.log, "a", 0o600), entry = absolute(options.entryPath, "dd-opencode entry"); const child = spawn(process.execPath, [entry, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: isolatedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
73
|
-
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs:
|
|
75
|
+
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, state.daemon_id); while (!startupClock.sample(await startupProgress())) { await new Promise(resolve => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; } } throw Object.assign(new DaemonError("daemon_start_failed", "daemon did not become ready", true), { details: observationDetails(startupClock, 'daemon-start', child.pid) }); }
|
|
74
76
|
catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
|
|
75
77
|
}
|
|
76
78
|
export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); };
|
|
@@ -223,7 +225,7 @@ export async function serveDaemon(stateDir) {
|
|
|
223
225
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
224
226
|
scheduleDaemonProcessHeartbeat(state.config, providerProcess);
|
|
225
227
|
state.provider_process = providerProcess; state.provider_pid = child.pid; await writeJson(paths.state, state);
|
|
226
|
-
const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const startupClock = new ObservationClock({ timeoutMs:
|
|
228
|
+
const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const startupProgress = startupObserver(paths.dir, state.daemon_id); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); while (!startupClock.sample(await startupProgress())) { try { await client.health(); break; } catch { await new Promise(resolve => setTimeout(resolve, 100)); } } await client.health(); const runtime = new Runtime(paths, state, client, child, providerProcess); await removeSocket(paths);
|
|
227
229
|
const server = net.createServer(connection => {
|
|
228
230
|
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
229
231
|
connection.on("data", chunk => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
1
2
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
3
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
3
4
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
@@ -180,13 +181,13 @@ export async function startDaemon(options) {
|
|
|
180
181
|
await log.close();
|
|
181
182
|
try {
|
|
182
183
|
await confirmDaemonProcess(config, resourceProcess, child);
|
|
183
|
-
const startupClock = new ObservationClock({ timeoutMs:
|
|
184
|
-
while (!startupClock.sample()) {
|
|
184
|
+
const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, state.daemon_id);
|
|
185
|
+
while (!startupClock.sample(await startupProgress())) {
|
|
185
186
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
186
187
|
try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); }
|
|
187
188
|
catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
|
|
188
189
|
}
|
|
189
|
-
throw new DaemonError("daemon_start_failed", "daemon did not become ready", true);
|
|
190
|
+
throw Object.assign(new DaemonError("daemon_start_failed", "daemon did not become ready", true), { details: observationDetails(startupClock, 'daemon-start', child.pid) });
|
|
190
191
|
} catch (error) {
|
|
191
192
|
await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.dir), state.daemon_id);
|
|
192
193
|
throw error;
|
|
@@ -4,6 +4,8 @@ import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
|
|
|
4
4
|
import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { createReadStream } from 'node:fs';
|
|
8
|
+
import { publishSetup, SETUP_STARTUP_MS } from './setup-progress.mjs';
|
|
7
9
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
10
|
import path from "node:path";
|
|
9
11
|
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
@@ -185,11 +187,14 @@ export class AcpBridge {
|
|
|
185
187
|
this.child.on("close", code => { void Promise.all([this.nativeDrain, this.diagnostics]).then(() => { const error = new Error(`zcode-acp exited ${code}${this.bridgeStderr.trim() ? `: ${this.bridgeStderr.trim()}` : ""}`); error.code = "bridge_exited"; this.rejectAll(error); }); });
|
|
186
188
|
this.nativeDrain = readNativeLines(this.child.stdout, async line => { this.receive(line); await this.journal.flush(); }, error => this.rejectAll(error));
|
|
187
189
|
if (this.options.onSpawn) await this.options.onSpawn(this.child);
|
|
188
|
-
|
|
190
|
+
publishSetup('probe_started', 'native-initialize');
|
|
191
|
+
const initialized = await this.request("initialize", {
|
|
189
192
|
protocolVersion: 1,
|
|
190
193
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, elicitation: { form: {} } },
|
|
191
194
|
clientInfo: this.options.clientInfo ?? { name: "dd-zcode", version: "0.1.0" },
|
|
192
|
-
}, this.options.initializeTimeoutMs ??
|
|
195
|
+
}, this.options.initializeTimeoutMs ?? SETUP_STARTUP_MS);
|
|
196
|
+
publishSetup('native_initialized', 'native-initialize');
|
|
197
|
+
return initialized;
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
receive(line) {
|
|
@@ -931,8 +936,12 @@ export async function doctor(options = {}) {
|
|
|
931
936
|
const ddHarness = await commandOutput(options.bin, ["--dd-harness-version"]);
|
|
932
937
|
const zcodePath = options.zcodePath ?? process.env.ZCODE_BIN ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
|
|
933
938
|
const zcode = await commandOutput(zcodePath, ["--version"]);
|
|
934
|
-
const
|
|
935
|
-
const
|
|
939
|
+
const hash = createHash('sha256'); let bytes = 0;
|
|
940
|
+
for await (const chunk of createReadStream(zcodePath)) {
|
|
941
|
+
hash.update(chunk); bytes += chunk.length;
|
|
942
|
+
if (bytes % (4 * 1024 * 1024) < chunk.length) publishSetup('identity_digest_progress', bytes);
|
|
943
|
+
}
|
|
944
|
+
const sha256 = hash.digest('hex'); publishSetup('identity_digest_completed');
|
|
936
945
|
const qualification = zcodeLifecycleQualification(sha256, zcodeAcpCommit, ddHarness);
|
|
937
946
|
return { compatible: qualification.status === "qualified", contract_supported: supportedZcodeHarnessContract(ddHarness), lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
|
|
938
947
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export const RENEWAL_POLICY: Readonly<{ budgetMs: number; attemptMs: number }>;
|
|
2
|
-
export type RenewalState = { uncertainSince: number | null; attempt: number; exhausted: boolean };
|
|
3
|
-
export function createRenewalState(): RenewalState;
|
|
4
|
-
export function renewalRemaining(state: RenewalState, now?: number): number;
|
|
1
|
+
export const RENEWAL_POLICY: Readonly<{ startupMs: number; budgetMs: number; attemptMs: number; safetyMs: number }>;
|
|
2
|
+
export type RenewalState = { uncertainSince: number | null; attempt: number; exhausted: boolean; leaseExpiresAt: string | null };
|
|
3
|
+
export function createRenewalState(options?: { leaseExpiresAt?: string | null }): RenewalState;
|
|
4
|
+
export function renewalRemaining(state: RenewalState, now?: number, wallNow?: number): number;
|
|
5
5
|
export function renewalFailure(state: RenewalState, now?: number): number;
|
|
6
6
|
export function renewalDelay(state: RenewalState): number;
|
|
7
|
-
export function renewalConfirmed(state: RenewalState, now?: number): boolean;
|
|
7
|
+
export function renewalConfirmed(state: RenewalState, now?: number, leaseExpiresAt?: string | null): boolean;
|
|
8
8
|
export function maintenanceRetryable(error: unknown): boolean;
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
/** Shared policy only; transports and owner-specific fences stay at callsites. */
|
|
2
|
-
export const RENEWAL_POLICY = Object.freeze({ budgetMs:
|
|
3
|
-
export function createRenewalState() { return { uncertainSince: null, attempt: 0, exhausted: false }; }
|
|
4
|
-
export function renewalRemaining(state, now = performance.now()) {
|
|
5
|
-
const
|
|
2
|
+
export const RENEWAL_POLICY = Object.freeze({ startupMs: 120_000, budgetMs: 126_000, attemptMs: 5_000, safetyMs: 1_000 });
|
|
3
|
+
export function createRenewalState({ leaseExpiresAt = null } = {}) { return { uncertainSince: null, attempt: 0, exhausted: false, leaseExpiresAt }; }
|
|
4
|
+
export function renewalRemaining(state, now = performance.now(), wallNow = Date.now()) {
|
|
5
|
+
const expiry = Date.parse(state.leaseExpiresAt ?? "");
|
|
6
|
+
const budget = Number.isFinite(expiry) ? Math.max(0, expiry - wallNow - RENEWAL_POLICY.safetyMs) : state.leaseExpiresAt != null ? 0 : RENEWAL_POLICY.budgetMs;
|
|
7
|
+
const remaining = state.exhausted ? 0 : Number.isFinite(expiry) || state.uncertainSince === null ? budget : Math.max(0, budget - (now - state.uncertainSince));
|
|
6
8
|
if (remaining === 0) state.exhausted = true;
|
|
7
9
|
return remaining;
|
|
8
10
|
}
|
|
9
11
|
export function renewalFailure(state, now = performance.now()) { state.uncertainSince ??= now; state.attempt++; return renewalRemaining(state, now); }
|
|
10
12
|
export function renewalDelay(state) { return Math.min(2_000, 250 * 2 ** Math.min(3, Math.max(0, state.attempt - 1))); }
|
|
11
|
-
export function renewalConfirmed(state, now = performance.now()
|
|
13
|
+
export function renewalConfirmed(state, now = performance.now(), leaseExpiresAt = null) {
|
|
14
|
+
// Caller verifies exact owner/fence. A committed receipt, not elapsed time,
|
|
15
|
+
// establishes the new authority horizon.
|
|
16
|
+
if (leaseExpiresAt !== null) {
|
|
17
|
+
const expiry = Date.parse(leaseExpiresAt);
|
|
18
|
+
if (!Number.isFinite(expiry) || expiry <= Date.now() + RENEWAL_POLICY.safetyMs) return false;
|
|
19
|
+
state.leaseExpiresAt = leaseExpiresAt; state.exhausted = false;
|
|
20
|
+
}
|
|
21
|
+
if (!renewalRemaining(state, now)) return false;
|
|
22
|
+
state.uncertainSince = null; state.attempt = 0; return true;
|
|
23
|
+
}
|
|
12
24
|
export function maintenanceRetryable(error) {
|
|
13
25
|
let retryable = false;
|
|
14
26
|
// Inspect only bounded typed causes from our maintenance transport, never text.
|
|
15
27
|
for (let depth = 0; error && depth < 8; depth++, error = error.cause ?? error.details?.cause) {
|
|
16
28
|
if (error.details?.cleanup_unconfirmed === true) return false;
|
|
29
|
+
if (error.details?.effect === "unknown") return false;
|
|
17
30
|
if (["process_lease_lost", "controller_lease_lost", "process_maintenance_receipt_invalid", "runtime_owner_mismatch"].includes(error.code)) return false;
|
|
18
31
|
if (error.code === "process_ownership_unknown" && error.details?.reason === "physical_identity_unavailable" && ["process_confirmation", "observer_registration"].includes(error.details.phase) && error.details.effect === "no_effect" && error.details.native_dispatch_started === false) retryable = true;
|
|
19
32
|
if (["process_heartbeat_timeout", "process_maintenance_timeout", "hook_deadline_exceeded", "native_hook_timeout", "SQLITE_BUSY", "SQLITE_LOCKED"].includes(error.code)) retryable = true;
|
|
@@ -2,6 +2,7 @@ import type { ChildProcess } from "node:child_process";
|
|
|
2
2
|
type RuntimeConfig = Record<string, unknown>;
|
|
3
3
|
type ProcessBinding = { id: string; lease_token: string; kind?: string; lease_expires_at?: string; registration_sha256?: string };
|
|
4
4
|
export function runtimeProcess(config: RuntimeConfig, action: string, options?: Record<string, unknown>, transport?: { timeoutMs?: number; signal?: AbortSignal }): Promise<Record<string, unknown> | null>;
|
|
5
|
+
export function readbackDaemonProcess(config: RuntimeConfig, record: ProcessBinding, transport?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;
|
|
5
6
|
export function registerDaemonProcess(config: RuntimeConfig, input: { kind: string; [key: string]: unknown }): Promise<ProcessBinding | null>;
|
|
6
7
|
export function confirmDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, child: Pick<ChildProcess, "pid"> | null | undefined): Promise<void>;
|
|
7
8
|
export function assertDaemonOwnership(stateDir: string, dispatch?: Record<string, unknown>, assertCurrent?: () => void): Promise<void>;
|