@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/build-info.json +6 -6
  3. package/dist/cli/command-inputs.js +2 -2
  4. package/dist/cli/run-cli.js +6 -6
  5. package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
  6. package/dist/cli/runtime-maintenance-ingress.js +5 -4
  7. package/dist/cli/runtime-process.js +27 -2
  8. package/dist/cli.js +7 -1
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
  10. package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
  12. package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  14. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  15. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
  16. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
  17. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
  18. package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
  19. package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
  20. package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
  21. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  22. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  23. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
  24. package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
  25. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  26. package/dist/harness-runtime/lib/process-json.mjs +41 -9
  27. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  28. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  29. package/dist/services/branch-context.js +3 -3
  30. package/dist/services/check-runner-evidence.js +121 -0
  31. package/dist/services/cleanup.js +1 -1
  32. package/dist/services/code-checks.js +113 -31
  33. package/dist/services/compatibility-preflight.js +9 -2
  34. package/dist/services/dashboard.js +33 -26
  35. package/dist/services/engines.js +88 -57
  36. package/dist/services/lifecycle-invocations.js +4 -2
  37. package/dist/services/managed-lease-monitor.js +9 -4
  38. package/dist/services/prompts.js +3 -1
  39. package/dist/services/protocols.js +3 -1
  40. package/dist/services/repair-intents.js +4 -2
  41. package/dist/services/run-control-worker.js +2 -1
  42. package/dist/services/run-controller.js +20 -17
  43. package/dist/services/run-fork.js +2 -2
  44. package/dist/services/runtime-scope-worker.js +2 -2
  45. package/dist/services/stage-lifecycle.js +13 -7
  46. package/dist/services/tooling.js +32 -23
  47. package/dist/services/vnext-code-review.js +2 -0
  48. package/dist/services/vnext-code.js +5 -1
  49. package/dist/services/vnext-merge.js +4 -1
  50. package/dist/services/vnext-protocolize.js +17 -16
  51. package/dist/services/work-registry.js +3 -1
  52. package/dist/services/worktrees.js +16 -14
  53. 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
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.127",
4
- "cli_commit": "8c98b4fa9e0f69be85ef616d7c7c3dac884fc0f5",
5
- "built_at": "2026-10-11T11:57:17.805Z",
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/.dd-eval/qualification/cp-214-release-canon",
11
- "memorybank_root": "/Users/deksden/.dd-eval/qualification/cp-214-release-canon/.memory-bank",
12
- "flow_root": "/Users/deksden/.dd-eval/qualification/cp-214-release-canon/.memory-bank/dd-flow",
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"),
@@ -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
- const { preflightCliCompatibility } = await import("../services/compatibility-preflight.js");
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
- return { ok: true, processes: managedProcessStatus(context) };
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 (await import("./cli/run-cli.js")).runCli(args);
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: 30_000 }); while (!startupClock.sample()) { await new Promise(resolve => setTimeout(resolve, 100));
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)), 30_000); })]); }
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: 60_000 }); while (!startupClock.sample()) { 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" }); }
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: 30_000 });
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, { timeoutMs: 10_000, phase: "native-version" }); }
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: 120_000 }); while (!startupClock.sample()) { 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 new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
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, { stdin, timeoutMs = 30_000 } = {}) {
30
- return await commandText(bin, args, { cwd: config.cwd, env: isolatedEnv(config), input: stdin ?? null, timeoutMs, phase: "native-utility" });
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: 20_000 }); while (!startupClock.sample()) { 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 new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
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: 15_000 }); while (!startupClock.sample()) { 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);
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: 30_000 });
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
- return await this.request("initialize", {
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 ?? 30_000);
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 native = await readFile(zcodePath);
935
- const sha256 = createHash("sha256").update(native).digest("hex");
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: 30_000, attemptMs: 5_000 });
3
- export function createRenewalState() { return { uncertainSince: null, attempt: 0, exhausted: false }; }
4
- export function renewalRemaining(state, now = performance.now()) {
5
- const remaining = state.exhausted ? 0 : state.uncertainSince === null ? RENEWAL_POLICY.budgetMs : Math.max(0, RENEWAL_POLICY.budgetMs - (now - state.uncertainSince));
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()) { if (!renewalRemaining(state, now)) return false; state.uncertainSince = null; state.attempt = 0; return true; }
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>;