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

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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. package/package.json +7 -4
@@ -0,0 +1,112 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { createHash } from 'node:crypto';
3
+ import { open, readFile } from 'node:fs/promises';
4
+ import { constants } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { writeJsonAtomic } from './runner-events.mjs';
7
+
8
+ export const OPERATION_PROGRESS_CONTRACT = 'dd-flow/operation-progress@1';
9
+ const context = new AsyncLocalStorage(), flights = new Map();
10
+ const fields = ['operation_id', 'daemon_id', 'operation', 'params_sha256', 'owner_id', 'generation'];
11
+ const phases = new Set(['accepted','ownership_confirmed','admission_confirmed','native_dispatched','native_progress','native_outcome_retained','settlement_started','settlement_completed','settlement_pending']);
12
+ export function operationDirectory(root,id) {
13
+ if (typeof root!=='string' || !path.isAbsolute(root) || typeof id!=='string' || !id || id.length>1_024) throw Object.assign(new Error('invalid operation observation binding'),{code:'operation_progress_invalid'});
14
+ return path.join(root, 'operations', createHash('sha256').update(id).digest('hex'));
15
+ }
16
+ export async function readOperationRecord(root, id, name) {
17
+ try {
18
+ const handle = await open(path.join(operationDirectory(root, id), name), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
19
+ try {
20
+ const stat = await handle.stat();
21
+ if (!stat.isFile() || name === 'progress.json' && stat.size > 16_384) throw Object.assign(new Error('invalid operation record file'), {code:'operation_progress_invalid'});
22
+ const bytes = name === 'progress.json'
23
+ ? await handle.read({buffer:Buffer.alloc(16_385),position:0}).then(({buffer,bytesRead})=>buffer.subarray(0,bytesRead))
24
+ : await handle.readFile();
25
+ if (name === 'progress.json' && bytes.length > 16_384) throw Object.assign(new Error('invalid operation record file'), {code:'operation_progress_invalid'});
26
+ try {
27
+ const value = JSON.parse(bytes.toString('utf8'));
28
+ if (name === 'progress.json' && (!value || typeof value !== 'object' || Array.isArray(value))) throw new Error('operation progress must be an object');
29
+ return value;
30
+ }
31
+ catch (cause) { if (name !== 'progress.json') throw cause; throw Object.assign(new Error('invalid operation progress JSON'), {code:'operation_progress_invalid',cause}); }
32
+ } finally { await handle.close(); }
33
+ } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
34
+ }
35
+ export async function readOperationProgress(root, id, expected = {}) {
36
+ const requested = await readOperationRecord(root, id, 'requested.json');
37
+ const progress = await readOperationRecord(root, id, 'progress.json');
38
+ if (!progress) return null;
39
+ const invalid = () => Object.assign(new Error('operation progress binding is invalid'), { code: 'operation_progress_invalid', details: { operation_id: id } });
40
+ if (!requested || progress.schema_id !== OPERATION_PROGRESS_CONTRACT || progress.operation_id !== id
41
+ || typeof progress.operation!=='string' || !progress.operation || progress.operation.length>128
42
+ || !/^[a-f0-9]{64}$/.test(progress.params_sha256 ?? '')
43
+ || ['daemon_id','owner_id'].some(field=>progress[field]!==null && (typeof progress[field]!=='string' || !progress[field] || progress[field].length>1_024))
44
+ || progress.generation!==null && (!Number.isSafeInteger(progress.generation) || progress.generation<0)
45
+ || !Number.isSafeInteger(progress.sequence) || progress.sequence < 1 || !phases.has(progress.phase)
46
+ || !Number.isFinite(Date.parse(requested.requested_at))
47
+ || !Number.isFinite(Date.parse(progress.observed_at)) || Date.parse(progress.observed_at) > Date.now()
48
+ || Date.parse(progress.observed_at) < Date.parse(requested.requested_at)
49
+ || fields.some(field => (progress[field] ?? null) !== (requested[field] ?? null))) throw invalid();
50
+ assertExpectedBinding(requested, expected, id);
51
+ return progress;
52
+ }
53
+ function assertExpectedBinding(requested, expected, id) {
54
+ if (fields.some(field => Object.hasOwn(expected, field) && (expected[field] ?? null) !== (requested[field] ?? null))) {
55
+ throw Object.assign(new Error('retained operation differs from dispatched request'), {code:'operation_conflict',details:{operation_id:id}});
56
+ }
57
+ }
58
+ export function publishOperationProgress(root, id, phase, metadata = {}) {
59
+ if (!phases.has(phase)) return Promise.reject(Object.assign(new Error('unknown operation progress phase'), {code:'operation_progress_invalid'}));
60
+ const key = `${root}\0${id}`;
61
+ const next = (flights.get(key) ?? Promise.resolve()).catch(() => {}).then(async () => {
62
+ const requested = await readOperationRecord(root, id, 'requested.json');
63
+ if (!requested) return null;
64
+ const prior = await readOperationProgress(root, id);
65
+ const observedAt = metadata.observed_at ?? new Date().toISOString();
66
+ if (prior && Date.parse(observedAt)<Date.parse(prior.observed_at)) return prior;
67
+ if (prior && phase === prior.phase && phase !== 'native_progress') return prior;
68
+ if (prior && phase === 'native_progress' && Date.parse(observedAt) - Date.parse(prior.observed_at) < 1_000) return prior;
69
+ const value = { schema_id: OPERATION_PROGRESS_CONTRACT,
70
+ ...Object.fromEntries(fields.map(field => [field, requested[field] ?? null])),
71
+ sequence: (prior?.sequence ?? 0) + 1, phase, observed_at: observedAt,
72
+ ...Object.fromEntries(['native_request_id', 'session_id'].filter(field => typeof metadata[field] === 'string').map(field => [field, metadata[field].slice(0, 256)])) };
73
+ if (!Number.isFinite(Date.parse(observedAt)) || Date.parse(observedAt) > Date.now() || Date.parse(observedAt) < Date.parse(requested.requested_at)) throw Object.assign(new Error('invalid progress event timestamp'), { code: 'operation_progress_invalid' });
74
+ await writeJsonAtomic(path.join(operationDirectory(root, id), 'progress.json'), value);
75
+ return value;
76
+ });
77
+ flights.set(key, next);
78
+ void next.finally(() => { if (flights.get(key) === next) flights.delete(key); }).catch(() => {});
79
+ return next;
80
+ }
81
+ export const withOperationProgress = (root, id, action) => context.run({ root, id }, action);
82
+ export function operationProgressReporter() {
83
+ const binding = context.getStore();
84
+ return (phase, metadata) => binding ? publishOperationProgress(binding.root, binding.id, phase, metadata) : Promise.resolve(null);
85
+ }
86
+ export const noteOperationProgress = (phase, metadata) => operationProgressReporter()(phase, metadata);
87
+
88
+ /** Confirmed admission waiting is ownership evidence, not native activity. */
89
+ export async function publishOperationWaiting(root,id,receipt) {
90
+ const requested = await readOperationRecord(root,id,'requested.json');
91
+ if (!requested) return;
92
+ const prior = await readOperationRecord(root,id,'admission-wait.json');
93
+ if (prior?.state === (receipt.admitted ? 'admitted' : 'provider_capacity') && Date.now()-Date.parse(prior.observed_at)<1_000) return;
94
+ const value = { schema_id:'dd-flow/operation-waiting@1', ...Object.fromEntries(fields.map(field=>[field,requested[field] ?? null])), observed_at:new Date().toISOString(), lease_expires_at:receipt.lease_expires_at, process_id:receipt.process_id, state:receipt.admitted ? 'admitted' : 'provider_capacity' };
95
+ await writeJsonAtomic(path.join(operationDirectory(root,id),'admission-wait.json'),value);
96
+ }
97
+ export async function readOperationWaiting(root,id,expected={}) {
98
+ const requested = await readOperationRecord(root,id,'requested.json'), waiting = await readOperationRecord(root,id,'admission-wait.json');
99
+ if (!waiting || waiting.state!=='provider_capacity') return false;
100
+ if (!requested || waiting.schema_id!=='dd-flow/operation-waiting@1' || fields.some(field=>(waiting[field] ?? null)!==(requested[field] ?? null))) throw Object.assign(new Error('admission waiting binding differs'),{code:'operation_progress_invalid'});
101
+ assertExpectedBinding(requested, expected, id);
102
+ if (!Number.isFinite(Date.parse(waiting.observed_at)) || Date.parse(waiting.observed_at)>Date.now() || Date.parse(waiting.observed_at)<Date.parse(requested.requested_at) || typeof waiting.process_id!=='string' || !waiting.process_id) throw Object.assign(new Error('invalid admission observation'),{code:'operation_progress_invalid'});
103
+ if (Date.now()-Date.parse(waiting.observed_at)>30_000) return false;
104
+ if (!Number.isFinite(Date.parse(waiting.lease_expires_at)) || Date.parse(waiting.lease_expires_at)<=Date.now()) return false;
105
+ const daemon = JSON.parse(await readFile(path.join(root,'daemon.json'),'utf8'));
106
+ if ((daemon.daemon_id ?? daemon.config?.daemonId)!==waiting.daemon_id || daemon.config?.runtime_owner?.owner_id!==waiting.owner_id || ['stopping','clean','unclean','cleanup_failed','failed'].includes(daemon.shutdown_state)) return false;
107
+ if (waiting.process_id !== daemon.resource_process?.id) return false;
108
+ try { const fence = JSON.parse(await readFile(path.join(root,'operation-fence.json'),'utf8')); if (fence.daemon_id===waiting.daemon_id && fence.generation>waiting.generation) return false; } catch(error) { if(error.code!=='ENOENT') throw error; }
109
+ try { process.kill(daemon.pid,0); } catch { return false; }
110
+ return true;
111
+ }
112
+ export function suspendOperationClock(clock) { clock.lastWall = clock.wall(); clock.lastMono = clock.monotonic(); }
@@ -1,21 +1,32 @@
1
1
  import { spawn } from "node:child_process";
2
- import { reportedError } from "./operation-errors.mjs";
2
+ import { errorRecord, reportedError } from "./operation-errors.mjs";
3
+ import { ObservationClock, validateObservationDuration } from "./observation-clock.mjs";
4
+ import { terminateCommandTree } from "./native-hook-command.mjs";
3
5
 
4
6
  function failure(message, code) {
5
7
  return Object.assign(new Error(message), { code });
6
8
  }
7
9
 
8
- export function reportedFailure(stdout, stderr, fallback, fallbackCode = "flow_reconciliation_failed") {
10
+ export function reportedFailure(stdout, stderr, fallback, fallbackCode = "flow_reconciliation_failed", retainedError = null) {
9
11
  for (const text of [stdout, stderr]) {
10
- for (const line of [text.trim(), ...text.trim().split(/\n/).reverse()]) {
12
+ // Bounded CLI transport may prepend JSONL phase records to the usual
13
+ // pretty-printed error envelope. Remove only our diagnostics, not arbitrary
14
+ // text or a substring that might masquerade as an authoritative receipt.
15
+ const envelope = text.split(/\n/).filter(line => {
16
+ try { return !["native_hook_phase", "process_maintenance_phase"].includes(JSON.parse(line)?.kind); }
17
+ catch { return true; }
18
+ }).join("\n").trim();
19
+ for (const line of [text.trim(), envelope, ...text.trim().split(/\n/).reverse()]) {
11
20
  try {
12
21
  const value = JSON.parse(line);
22
+ if (["native_hook_phase", "process_maintenance_phase"].includes(value?.kind)) continue;
13
23
  const error = value?.error;
14
24
  if (typeof error?.code === "string" && error.code) return reportedError(error, fallback);
15
25
  if (typeof value?.code === "string" && value.code) return reportedError({ ...value, message: typeof error === "string" ? error : value.message }, fallback);
16
26
  } catch { /* Not a structured CLI error line. */ }
17
27
  }
18
28
  }
29
+ if (retainedError) return reportedError(retainedError, fallback);
19
30
  return failure(stderr.trim() || stdout.trim() || fallback, fallbackCode);
20
31
  }
21
32
 
@@ -23,49 +34,136 @@ function processTarget(executable, args) {
23
34
  return /\.[cm]?js$/.test(executable) ? { command: process.execPath, args: [executable, ...args] } : { command: executable, args };
24
35
  }
25
36
 
26
- export async function commandJson(bin, args, { cwd, env = {}, input = null, onProgress = null } = {}) {
37
+ // These are utility/transport children, never a second productive-work owner.
38
+ async function command(bin, args, json, { cwd, env = {}, input = null, onProgress = null,
39
+ timeoutMs = 30_000, phase = "utility", signal = null, onDiagnostic = null } = {}) {
40
+ if (timeoutMs !== null) validateObservationDuration(timeoutMs);
41
+ if (timeoutMs === null && phase !== "native-work") throw new RangeError("only native-work may omit the utility quiet timeout");
42
+ if (signal?.aborted) throw signal.reason ?? failure("command aborted", "command_aborted");
43
+ const executable = bin === "dd-flow" && typeof env.DD_FLOW_BIN === "string" ? env.DD_FLOW_BIN : bin;
44
+ const target = processTarget(executable, json ? [...args, "--json"] : args);
27
45
  return await new Promise((resolve, reject) => {
28
- // A canonical runtime may intentionally shadow the ambient `dd-flow`
29
- // executable with its captured engine. Keep that choice in the supplied
30
- // environment rather than letting a host-global shim leak into a run.
31
- const executable = bin === "dd-flow" && typeof env.DD_FLOW_BIN === "string" ? env.DD_FLOW_BIN : bin;
32
- const target = processTarget(executable, [...args, "--json"]);
33
- const child = spawn(target.command, target.args, { cwd, env: { ...process.env, ...env }, stdio: [input === null ? "ignore" : "pipe", "pipe", "pipe"] });
34
- let stdout = ""; let stderr = ""; let pendingStderr = ""; let settled = false; let progressError = null;
35
- const finish = (error, value) => { if (settled) return; settled = true; error ? reject(error) : resolve(value); };
36
- const deliverProgress = (line) => {
37
- let value; try { value = JSON.parse(line); } catch { return; }
38
- if (typeof onProgress !== "function" || progressError) return;
39
- try { const returned = onProgress(value); if (returned && typeof returned.then === "function") { Promise.resolve(returned).catch(() => {}); throw failure("commandJson onProgress must be synchronous", "progress_callback_async"); } }
40
- catch (error) { progressError = error; child.kill("SIGKILL"); }
46
+ const child = spawn(target.command, target.args, { cwd, env: { ...process.env, ...env },
47
+ detached: process.platform !== "win32", stdio: [input === null ? "ignore" : "pipe", "pipe", "pipe"] });
48
+ const maxOutput = 8 * 1024 * 1024, maxTail = 64 * 1024, maxLine = 1024 * 1024;
49
+ let stdout = "", stderr = "", pendingStderr = "", settled = false, primary = null, cursor = 0, droppingStructured = false;
50
+ let outputBytes = 0, cleanupTimer = null, inputError = null, termination = null, retainedError = null, prettyError = "";
51
+ const phases = new Set();
52
+ const clock = timeoutMs === null ? null : new ObservationClock({ timeoutMs });
53
+ const productive = () => { cursor++; clock?.sample(cursor, Date.now()); };
54
+ const finish = (error, value) => {
55
+ if (settled) return; settled = true;
56
+ clearInterval(timer); clearTimeout(cleanupTimer); signal?.removeEventListener("abort", abort);
57
+ error ? reject(error) : resolve(value);
41
58
  };
42
- child.stdout.setEncoding("utf8").on("data", (data) => { stdout += data; });
43
- child.stderr.setEncoding("utf8").on("data", (data) => {
44
- stderr += data; pendingStderr += data;
59
+ const stop = error => {
60
+ if (primary || settled) return;
61
+ primary = error;
62
+ termination = terminateCommandTree(child).catch(cause => { if (cause.code !== "ESRCH") primary.cleanup_error = cause; });
63
+ // A missing close after owned SIGKILL is cleanup uncertainty, not success.
64
+ cleanupTimer = setTimeout(() => {
65
+ primary.cleanup_error ??= failure("owned utility child did not close", "command_cleanup_unconfirmed");
66
+ finish(primary);
67
+ }, 5_000);
68
+ };
69
+ const abort = () => stop(signal.reason ?? failure("command aborted", "command_aborted"));
70
+ const deliverProgress = line => {
71
+ let value;
72
+ // Parse a bounded whole pretty envelope before its diagnostic bytes leave
73
+ // the tail. An oversized envelope cannot become plain-text keepalive.
74
+ if (prettyError === null) { if (line.trimEnd() === "}") prettyError = ""; return; }
75
+ const candidate = prettyError ? `${prettyError}\n${line}` : line;
76
+ if (prettyError && candidate.length > maxTail) { prettyError = null; return; }
77
+ try { value = JSON.parse(candidate); prettyError = ""; }
78
+ catch {
79
+ if (!prettyError && !line.trimStart().startsWith("{")) { productive(); return; }
80
+ prettyError = candidate.length > maxTail ? null : candidate; return;
81
+ }
82
+ const kind = value?.kind;
83
+ const passive = ["heartbeat", "poll", "status", "elapsed", "wait", "waiting"].some(marker => [kind, value?.phase, value?.action].includes(marker));
84
+ if (!["native_hook_phase", "process_maintenance_phase"].includes(kind)) {
85
+ const error = typeof value?.error?.code === "string" && value.error.code ? value.error
86
+ : typeof value?.code === "string" && value.code ? { ...value, message: typeof value.error === "string" ? value.error : value.message } : null;
87
+ // Retain one bounded typed summary independently of the diagnostic tail.
88
+ // It is consulted only on failed exit, after authoritative stdout.
89
+ if (error) retainedError = errorRecord(error);
90
+ }
91
+ if (["native_hook_phase", "process_maintenance_phase"].includes(kind)) {
92
+ const marker = JSON.stringify([kind, value.phase, value.operation_id ?? value.process_id]);
93
+ if (typeof value.phase === "string" && !passive && !phases.has(marker)) { phases.add(marker); productive(); }
94
+ } else if (!passive) productive();
95
+ if (typeof onProgress !== "function" || primary) return;
96
+ try {
97
+ const returned = onProgress(value);
98
+ if (returned && typeof returned.then === "function") {
99
+ Promise.resolve(returned).catch(() => {});
100
+ throw failure("command onProgress must be synchronous", "progress_callback_async");
101
+ }
102
+ } catch (error) { stop(error); }
103
+ };
104
+ const diagnostic = (data, stream) => {
105
+ try {
106
+ const returned = onDiagnostic?.(data, stream);
107
+ if (returned && typeof returned.then === "function") {
108
+ Promise.resolve(returned).catch(() => {});
109
+ throw failure("command onDiagnostic must be synchronous", "progress_callback_async");
110
+ }
111
+ } catch (error) { stop(error); }
112
+ };
113
+ const timer = clock ? setInterval(() => {
114
+ if (clock.sample(cursor)) stop(Object.assign(failure("command observation became quiet", "command_observation_lost"),
115
+ { details: { phase, timeout_ms: timeoutMs, observation_lost: clock.observationLost } }));
116
+ }, Math.max(1, Math.min(1_000, timeoutMs / 4))) : null;
117
+ clock?.sample(cursor);
118
+ signal?.addEventListener("abort", abort, { once: true });
119
+ child.stdout.setEncoding("utf8").on("data", data => {
120
+ productive();
121
+ diagnostic(data, "stdout"); outputBytes += Buffer.byteLength(data);
122
+ if (outputBytes > maxOutput) stop(failure("command final output exceeds 8 MiB", "command_output_limit"));
123
+ else stdout += data;
124
+ });
125
+ child.stderr.setEncoding("utf8").on("data", data => {
126
+ diagnostic(data, "stderr"); stderr = (stderr + data).slice(-maxTail);
127
+ if (droppingStructured) {
128
+ const end = data.indexOf("\n");
129
+ if (end < 0) return;
130
+ data = data.slice(end + 1); droppingStructured = false;
131
+ }
132
+ pendingStderr += data;
45
133
  for (;;) {
46
134
  const end = pendingStderr.indexOf("\n"); if (end < 0) break;
47
135
  const line = pendingStderr.slice(0, end); pendingStderr = pendingStderr.slice(end + 1);
48
- deliverProgress(line);
136
+ if (line.length <= maxLine) deliverProgress(line);
137
+ }
138
+ if (pendingStderr && prettyError === "" && !pendingStderr.trimStart().startsWith("{")) productive();
139
+ // Plain streaming diagnostics are not the final reply and cannot kill a
140
+ // productive operation merely because their cumulative size is large.
141
+ if (pendingStderr.length > maxLine) {
142
+ droppingStructured = prettyError !== "" || pendingStderr.trimStart().startsWith("{");
143
+ pendingStderr = droppingStructured ? "" : pendingStderr.slice(-maxTail);
49
144
  }
50
145
  });
51
- child.once("error", (error) => finish(progressError ?? error));
52
- if (input !== null) child.stdin.end(input);
53
- child.on("close", (status) => {
146
+ child.once("error", error => { if (!child.pid) finish(primary ?? error); else stop(error); });
147
+ if (input !== null) {
148
+ child.stdin.on("error", error => { inputError = error; if (error.code !== "EPIPE") stop(error); });
149
+ child.stdin.end(input);
150
+ }
151
+ child.once("close", status => {
54
152
  if (pendingStderr) deliverProgress(pendingStderr);
55
- if (progressError) return finish(progressError);
56
- if (status !== 0) return finish(reportedFailure(stdout, stderr, `${executable} exited ${status}`));
57
- try { const value = JSON.parse(stdout.trim()); if (value?.ok === false) { const error = value.error && typeof value.error === "object" ? value.error : value; if (typeof error.code === "string") return finish(reportedError(error, `${bin} returned an error envelope`)); } finish(null, value); }
58
- catch (error) { finish(failure(`${bin} returned invalid JSON: ${error.message}`, "flow_reconciliation_failed")); }
153
+ if (primary) { void (termination ?? Promise.resolve()).then(() => finish(primary)); return; }
154
+ if (status !== 0) return finish(reportedFailure(stdout, stderr, `${executable} exited ${status}`, json ? "flow_reconciliation_failed" : "command_failed", retainedError));
155
+ if (!json) return finish(inputError, stdout.trim());
156
+ try {
157
+ const value = JSON.parse(stdout.trim());
158
+ if (value?.ok === false) {
159
+ const error = value.error && typeof value.error === "object" ? value.error : value;
160
+ return finish(reportedError(error, `${bin} returned an error envelope`));
161
+ }
162
+ if (inputError) return finish(inputError);
163
+ finish(null, value);
164
+ } catch (error) { finish(failure(`${bin} returned invalid JSON: ${error.message}`, "flow_reconciliation_failed")); }
59
165
  });
60
166
  });
61
167
  }
62
-
63
- export async function commandText(bin, args, { cwd, env = {} } = {}) {
64
- return await new Promise((resolve, reject) => {
65
- const executable = bin === "dd-flow" && typeof env.DD_FLOW_BIN === "string" ? env.DD_FLOW_BIN : bin;
66
- const target = processTarget(executable, args);
67
- const child = spawn(target.command, target.args, { cwd, env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = "";
68
- child.stdout.setEncoding("utf8").on("data", (data) => { stdout += data; }); child.stderr.setEncoding("utf8").on("data", (data) => { stderr += data; }); child.on("error", reject);
69
- child.on("close", (status) => status === 0 ? resolve(stdout.trim()) : reject(failure(stderr.trim() || stdout.trim() || `${executable} exited ${status}`, "command_failed")));
70
- });
71
- }
168
+ export async function commandJson(bin, args, options = {}) { return await command(bin, args, true, options); }
169
+ export async function commandText(bin, args, options = {}) { return await command(bin, args, false, options); }
@@ -6,6 +6,7 @@ export interface ToolEvent {
6
6
  reason?: string;
7
7
  [key: string]: unknown;
8
8
  }
9
+ export function classifyCodexItem(type: unknown): "tool" | "non_tool" | "unknown";
9
10
  export class ToolObservations {
10
11
  calls: Map<string, ToolEvent>;
11
12
  turns: Map<string, { session: { harness_id: string; session_id: string }; turn_id: string; started: boolean; completed: boolean }>;
@@ -128,11 +128,14 @@ function structuredFailure(value) {
128
128
 
129
129
  const codexTools = new Set(["commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall", "collabAgentToolCall", "webSearch", "imageView", "imageGeneration", "sleep"]);
130
130
  const codexNonTools = new Set(["userMessage", "hookPrompt", "agentMessage", "functionCallOutput", "plan", "reasoning", "subAgentActivity", "enteredReviewMode", "exitedReviewMode", "contextCompaction"]);
131
+ export function classifyCodexItem(type) {
132
+ return codexTools.has(type) ? "tool" : codexNonTools.has(type) ? "non_tool" : "unknown";
133
+ }
131
134
  export function codexAppServerObservation(message) {
132
135
  if (!["item/started", "item/completed"].includes(message?.method)) return null;
133
136
  const { threadId, turnId, item } = message.params ?? {};
134
- if (!item || codexNonTools.has(item.type)) return null;
135
- if (!codexTools.has(item.type)) return { reason: "codex_item_type_unsupported" };
137
+ if (!item || classifyCodexItem(item.type) === "non_tool") return null;
138
+ if (classifyCodexItem(item.type) === "unknown") return { reason: "codex_item_type_unsupported" };
136
139
  if (!threadId || !item.id) return { reason: "tool_identity_missing" };
137
140
  const native = item.status;
138
141
  const failed = ["failed", "declined", "interrupted"].includes(native) || item.type === "commandExecution" && typeof item.exitCode === "number" && item.exitCode !== 0;
@@ -26,8 +26,9 @@ export function normalizeZcodeUsage(usage, read) {
26
26
  totals.requestOutputTokens += tokens.output;
27
27
  for (const [key, value] of [["requestReasoningTokens", tokens.reasoning],
28
28
  ["requestCacheCreationTokens", tokens.cache?.write], ["requestCacheReadTokens", tokens.cache?.read]]) {
29
- if (value !== undefined && !valid(value)) incomplete = true;
30
- else totals[key] += value ?? 0;
29
+ if (value == null) totals[key] = null;
30
+ else if (!valid(value)) incomplete = true;
31
+ else if (totals[key] !== null) totals[key] += value;
31
32
  }
32
33
  totals.requestCount++;
33
34
  }
@@ -1,18 +1,22 @@
1
1
  import { getDatabase } from "../storage/database.js";
2
2
  import { resolveDdFlowHome } from "../storage/paths.js";
3
- export function createRouterContext(env) {
3
+ import { assertImportAdmission } from "../storage/import-guard.js";
4
+ export function createRouterContext(env, deadlineAt) {
4
5
  const ddFlowHome = resolveDdFlowHome(env);
5
6
  return {
6
7
  ddFlowHome,
7
- db: getDatabase(ddFlowHome, "read_existing"),
8
+ db: getDatabase(ddFlowHome, "read_existing", undefined, deadlineAt),
8
9
  now: () => new Date().toISOString()
9
10
  };
10
11
  }
11
- export function createContext(env, mode = "initialize", importPermitId) {
12
+ export function createContext(env, mode = "initialize", importPermitId, options = {}) {
12
13
  const ddFlowHome = resolveDdFlowHome(env);
14
+ if (options.resourceDatabaseMode === "prepared_existing" || options.resourceDatabaseMode === "initialize")
15
+ assertImportAdmission(ddFlowHome, importPermitId);
13
16
  return {
14
17
  ddFlowHome,
15
- db: getDatabase(ddFlowHome, mode, importPermitId, mode === "hook" && env.DD_FLOW_HOOK_DEADLINE_AT ? Number(env.DD_FLOW_HOOK_DEADLINE_AT) : undefined),
18
+ db: getDatabase(ddFlowHome, mode, importPermitId, options.databaseDeadlineAt ?? (mode === "hook" && env.DD_FLOW_HOOK_DEADLINE_AT ? Number(env.DD_FLOW_HOOK_DEADLINE_AT) : undefined)),
19
+ ...options,
16
20
  env,
17
21
  now: () => new Date().toISOString()
18
22
  };
@@ -49,6 +49,25 @@
49
49
  "reused": { "type": "boolean" },
50
50
  "next_cursor": { "type": "integer", "minimum": 0 },
51
51
  "run_status": { "type": "string" },
52
+ "finalization": {
53
+ "type": ["object", "null"], "additionalProperties": false,
54
+ "required": ["schema_id", "issuer", "phase", "project_root", "run_id", "controller_id", "generation", "controller_status", "run_status", "status", "owner", "capture", "error"],
55
+ "properties": {
56
+ "schema_id": { "const": "dd-flow/terminal-finalization@1" },
57
+ "issuer": { "const": "dd-flow" },
58
+ "phase": { "const": "terminal_finalization" },
59
+ "project_root": { "type": "string", "minLength": 1 },
60
+ "run_id": { "type": "string", "minLength": 1 },
61
+ "controller_id": { "type": "string", "minLength": 1 },
62
+ "generation": { "type": "integer", "minimum": 0 },
63
+ "controller_status": { "enum": ["recovery_required", "capture_failed"] },
64
+ "run_status": { "enum": ["done", "failed", "cancelled"] },
65
+ "status": { "enum": ["pending", "blocked"] },
66
+ "owner": { "enum": ["live", "absent", "unknown"] },
67
+ "capture": { "enum": ["recorded", "missing"] },
68
+ "error": { "type": "object" }
69
+ }
70
+ },
52
71
  "control": { "type": ["object", "null"] },
53
72
  "operations": { "type": "array", "items": { "type": "object" } },
54
73
  "worker": { "type": ["object", "null"] },
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
+ import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { isDeepStrictEqual } from "node:util";
@@ -8,7 +9,8 @@ import { parse } from "shell-quote";
8
9
  import { assertRunMutationAllowed } from "./run-recovery.js";
9
10
  import { AppError, errorRecord } from "../shared/errors.js";
10
11
  import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
11
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
12
+ import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDelay, renewalFailure, renewalRemaining, RENEWAL_POLICY } from "../harness-runtime/lib/lease-renewal.mjs";
13
+ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
12
14
  import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
13
15
  import { getResourceDatabase } from "../storage/database.js";
14
16
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
@@ -234,6 +236,8 @@ function assertDirectCheckExecutable(workspaceRoot, command, checkId) {
234
236
  if (!program)
235
237
  return;
236
238
  const probe = spawnSync("/bin/sh", ["-c", "command -v -- \"$1\" >/dev/null", "dd-flow-check", program], { cwd: workspaceRoot, env, encoding: "utf8", timeout: 5000 });
239
+ if (probe.error || probe.signal)
240
+ throw new AppError("check_command_probe_unavailable", "Executable probe could not be observed; availability is unknown", 2, { check_id: checkId, executable: program, cause: probe.error?.message, signal: probe.signal });
237
241
  if (probe.status !== 0)
238
242
  throw new AppError("check_command_unavailable", "Available CODE check names an executable that is unavailable in this environment", 2, { check_id: checkId, command, executable: program });
239
243
  }
@@ -331,7 +335,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
331
335
  assertRunMutationAllowed(context, input.projectId, input.runId, generation);
332
336
  if (budget)
333
337
  admitRuntimeCheck(context, managed.id, managed.lease_token);
334
- }, () => heartbeatManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token }));
338
+ }, attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }));
335
339
  }
336
340
  finally {
337
341
  fs.closeSync(stdout);
@@ -423,7 +427,7 @@ export function reconcileUnfinishedChecks(context, input) {
423
427
  if (process)
424
428
  finishManagedProcess(context, { id: process.id, leaseToken: process.lease_token, state: status === "passed" ? "stopped" : "failed", reason: status === "passed" ? "reconciled_completion" : completion === null ? "completion_marker_missing" : "reconciled_check_failed" });
425
429
  if (receipt) {
426
- const repaired = { ...receipt, completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], status, exit_code: exitCode, after_fingerprint: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
430
+ const repaired = { ...receipt, completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
427
431
  writeReceipt(pendingReceipt.receipt_path, repaired);
428
432
  reconciled.push(repaired);
429
433
  }
@@ -487,7 +491,7 @@ function reserveOrReuseReceipt(context, input) {
487
491
  return { reserved };
488
492
  }, "checks.reserveOrReuseReceipt");
489
493
  }
490
- function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
494
+ function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, ...(input.status === "aborted" ? { abort_reason: input.result.error } : {}), exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
491
495
  function writeReceipt(file, receipt) { const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(temporary, file); }
492
496
  function readReceipt(file) { try {
493
497
  return JSON.parse(fs.readFileSync(file, "utf8"));
@@ -820,15 +824,23 @@ function listFiles(root) {
820
824
  });
821
825
  return visit(root).filter(file => file.split(path.sep).join("/") !== ownedHook).sort();
822
826
  }
823
- export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease) {
827
+ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease, observation = {}) {
828
+ const inactivityTimeoutMs = validateObservationDuration(observation.inactivityTimeoutMs ?? 30 * 60_000);
824
829
  return new Promise((resolve) => {
825
830
  const started = Date.now();
826
- const child = spawn("/bin/sh", ["-c", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["pipe", stdout, stderr], detached: process.platform !== "win32" });
831
+ const child = spawn("/bin/sh", ["-c", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
827
832
  let error = null;
828
833
  let stopping = false;
834
+ let closed = false;
829
835
  let progress;
830
836
  let cleanup;
831
837
  let cleanupUnconfirmed = false;
838
+ const renewal = createRenewalState();
839
+ let renewalTimer;
840
+ const clock = new ObservationClock({ timeoutMs: inactivityTimeoutMs });
841
+ let outputCursor = 0;
842
+ const outputs = [fs.createWriteStream("", { fd: stdout, autoClose: false }), fs.createWriteStream("", { fd: stderr, autoClose: false })];
843
+ const flushed = outputs.map(output => new Promise(resolve => { output.once("finish", resolve); output.once("error", () => resolve()); }));
832
844
  const stop = (cause) => {
833
845
  if (stopping)
834
846
  return;
@@ -838,20 +850,41 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
838
850
  clearInterval(progress);
839
851
  progress = undefined;
840
852
  }
853
+ if (renewalTimer)
854
+ clearTimeout(renewalTimer);
855
+ if (observationTimer)
856
+ clearInterval(observationTimer);
841
857
  cleanup = stopProcessGroup(child, 5_000).catch(cause => {
842
858
  cleanupUnconfirmed = true;
843
859
  error = `${error}; cleanup: ${JSON.stringify(errorRecord(cause))}`;
844
860
  resolve({ exitCode: null, error, aborted: true, cleanupUnconfirmed });
845
861
  });
846
862
  };
863
+ const observationTimer = setInterval(() => {
864
+ if (stopping || closed || observation.phase?.() === "serving")
865
+ return;
866
+ if (clock.sample(outputCursor))
867
+ stop(new AppError("operation_observation_lost", "Owned command has no confirmed progress within its inactivity window", 1, { phase: observation.phase?.() ?? "finite", inactivity_timeout_ms: inactivityTimeoutMs }));
868
+ }, Math.min(1000, inactivityTimeoutMs));
847
869
  // Register handlers before checking pid or calling persistence callbacks.
848
870
  // Failure is not completion until the spawned process has actually closed.
849
871
  child.once("error", (value) => { error = value.message; });
872
+ for (const [index, stream] of [child.stdout, child.stderr].entries()) {
873
+ outputs[index].once("error", stop);
874
+ stream.on("data", () => { outputCursor++; clock.sample(outputCursor); });
875
+ stream.pipe(outputs[index]);
876
+ }
850
877
  child.stdin.on("error", stop);
851
878
  child.once("close", async (exitCode, signal) => {
879
+ closed = true;
852
880
  if (progress)
853
881
  clearInterval(progress);
882
+ if (renewalTimer)
883
+ clearTimeout(renewalTimer);
884
+ if (observationTimer)
885
+ clearInterval(observationTimer);
854
886
  await cleanup;
887
+ await Promise.all(flushed);
855
888
  resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error), ...(cleanupUnconfirmed ? { cleanupUnconfirmed } : {}) });
856
889
  });
857
890
  if (!child.pid) {
@@ -866,18 +899,55 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
866
899
  return;
867
900
  }
868
901
  child.stdin.end("admitted\n");
869
- progress = setInterval(() => {
902
+ clock.sample(outputCursor);
903
+ const renew = () => {
904
+ if (stopping || closed)
905
+ return;
906
+ if (renewalTimer) {
907
+ clearTimeout(renewalTimer);
908
+ renewalTimer = undefined;
909
+ }
870
910
  try {
871
- if (!renewLease()) {
911
+ renewal.uncertainSince ??= performance.now();
912
+ const remaining = renewalRemaining(renewal);
913
+ if (!remaining) {
914
+ stop("managed process renewal budget exhausted");
915
+ return;
916
+ }
917
+ const receipt = renewLease(Math.min(RENEWAL_POLICY.attemptMs, remaining));
918
+ if (receipt === false || typeof receipt === "object" && !receipt.owned) {
872
919
  stop("managed process lease lost");
873
920
  return;
874
921
  }
922
+ if (typeof receipt === "object" && receipt.owned) {
923
+ const expiry = Date.parse(receipt.lease_expires_at);
924
+ if (!Number.isFinite(expiry))
925
+ throw new AppError("managed_lease_receipt_invalid", "Check renewal returned invalid expiry", 1);
926
+ if (expiry <= Date.now())
927
+ throw Object.assign(new Error("Check renewal receipt expired before acknowledgement"), { code: "process_maintenance_timeout" });
928
+ }
929
+ if (!renewalRemaining(renewal) || !renewalConfirmed(renewal)) {
930
+ stop("managed process renewal budget exhausted");
931
+ return;
932
+ }
933
+ }
934
+ catch (cause) {
935
+ if (!maintenanceRetryable(cause) || !renewalFailure(renewal)) {
936
+ stop(cause);
937
+ return;
938
+ }
939
+ renewalTimer = setTimeout(renew, Math.min(renewalDelay(renewal), renewalRemaining(renewal)));
940
+ return;
941
+ }
942
+ // A progress callback error is not a retryable renewal failure.
943
+ try {
875
944
  heartbeat(Math.floor((Date.now() - started) / 1000));
876
945
  }
877
946
  catch (cause) {
878
947
  stop(cause);
879
948
  }
880
- }, 15_000);
949
+ };
950
+ progress = setInterval(renew, 15_000);
881
951
  });
882
952
  }
883
953
  function checkShellScript() {