@deksden-com/dd-flow-cli 0.9.0-beta.123 → 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 (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -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/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -1,13 +1,16 @@
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";
7
8
  import { parse } from "shell-quote";
8
9
  import { assertRunMutationAllowed } from "./run-recovery.js";
9
- import { AppError } from "../shared/errors.js";
10
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
10
+ import { AppError, errorRecord } from "../shared/errors.js";
11
+ import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
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";
11
14
  import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
12
15
  import { getResourceDatabase } from "../storage/database.js";
13
16
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
@@ -233,6 +236,8 @@ function assertDirectCheckExecutable(workspaceRoot, command, checkId) {
233
236
  if (!program)
234
237
  return;
235
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 });
236
241
  if (probe.status !== 0)
237
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 });
238
243
  }
@@ -330,7 +335,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
330
335
  assertRunMutationAllowed(context, input.projectId, input.runId, generation);
331
336
  if (budget)
332
337
  admitRuntimeCheck(context, managed.id, managed.lease_token);
333
- }, () => 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 }));
334
339
  }
335
340
  finally {
336
341
  fs.closeSync(stdout);
@@ -340,7 +345,8 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
340
345
  if (processTreeIsAlive(current))
341
346
  throw new AppError("check_in_progress", "The check shell exited but its process tree is still live; retain this receipt and its resources until settlement", 2, { process_id: managed.id, check_receipt_id: id });
342
347
  allocation.release();
343
- finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
348
+ if (!result.cleanupUnconfirmed)
349
+ finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
344
350
  }
345
351
  catch (error) {
346
352
  if (error instanceof AppError && error.code === "check_in_progress")
@@ -421,7 +427,7 @@ export function reconcileUnfinishedChecks(context, input) {
421
427
  if (process)
422
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" });
423
429
  if (receipt) {
424
- 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 };
425
431
  writeReceipt(pendingReceipt.receipt_path, repaired);
426
432
  reconciled.push(repaired);
427
433
  }
@@ -485,7 +491,7 @@ function reserveOrReuseReceipt(context, input) {
485
491
  return { reserved };
486
492
  }, "checks.reserveOrReuseReceipt");
487
493
  }
488
- 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() }; }
489
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); }
490
496
  function readReceipt(file) { try {
491
497
  return JSON.parse(fs.readFileSync(file, "utf8"));
@@ -818,14 +824,23 @@ function listFiles(root) {
818
824
  });
819
825
  return visit(root).filter(file => file.split(path.sep).join("/") !== ownedHook).sort();
820
826
  }
821
- 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);
822
829
  return new Promise((resolve) => {
823
830
  const started = Date.now();
824
- 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" });
825
832
  let error = null;
826
833
  let stopping = false;
834
+ let closed = false;
827
835
  let progress;
828
- let escalation;
836
+ let cleanup;
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()); }));
829
844
  const stop = (cause) => {
830
845
  if (stopping)
831
846
  return;
@@ -835,23 +850,42 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
835
850
  clearInterval(progress);
836
851
  progress = undefined;
837
852
  }
838
- if (child.pid) {
839
- terminateProcessGroup(child.pid, "SIGTERM");
840
- escalation = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 5_000);
841
- }
853
+ if (renewalTimer)
854
+ clearTimeout(renewalTimer);
855
+ if (observationTimer)
856
+ clearInterval(observationTimer);
857
+ cleanup = stopProcessGroup(child, 5_000).catch(cause => {
858
+ cleanupUnconfirmed = true;
859
+ error = `${error}; cleanup: ${JSON.stringify(errorRecord(cause))}`;
860
+ resolve({ exitCode: null, error, aborted: true, cleanupUnconfirmed });
861
+ });
842
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));
843
869
  // Register handlers before checking pid or calling persistence callbacks.
844
870
  // Failure is not completion until the spawned process has actually closed.
845
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
+ }
846
877
  child.stdin.on("error", stop);
847
- child.once("close", (exitCode, signal) => {
878
+ child.once("close", async (exitCode, signal) => {
879
+ closed = true;
848
880
  if (progress)
849
881
  clearInterval(progress);
850
- if (escalation)
851
- clearTimeout(escalation);
852
- if (stopping)
853
- terminateProcessGroup(child.pid, "SIGKILL");
854
- resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error) });
882
+ if (renewalTimer)
883
+ clearTimeout(renewalTimer);
884
+ if (observationTimer)
885
+ clearInterval(observationTimer);
886
+ await cleanup;
887
+ await Promise.all(flushed);
888
+ resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error), ...(cleanupUnconfirmed ? { cleanupUnconfirmed } : {}) });
855
889
  });
856
890
  if (!child.pid) {
857
891
  error = "check process did not provide a PID";
@@ -865,18 +899,55 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
865
899
  return;
866
900
  }
867
901
  child.stdin.end("admitted\n");
868
- 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
+ }
869
910
  try {
870
- 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) {
871
919
  stop("managed process lease lost");
872
920
  return;
873
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 {
874
944
  heartbeat(Math.floor((Date.now() - started) / 1000));
875
945
  }
876
946
  catch (cause) {
877
947
  stop(cause);
878
948
  }
879
- }, 15_000);
949
+ };
950
+ progress = setInterval(renew, 15_000);
880
951
  });
881
952
  }
882
953
  function checkShellScript() {
@@ -890,8 +961,3 @@ function checkShellScript() {
890
961
  'exit "$code"'
891
962
  ].join("\n");
892
963
  }
893
- function terminateProcessGroup(pid, signal) { if (!pid)
894
- return; try {
895
- process.kill(process.platform === "win32" ? pid : -pid, signal);
896
- }
897
- catch { /* already exited */ } }