@mono-agent/agent-app 0.8.0 → 0.9.0

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 (186) hide show
  1. package/README.md +71 -22
  2. package/dist/account-home.d.ts +6 -0
  3. package/dist/account-home.d.ts.map +1 -0
  4. package/dist/account-home.js +9 -0
  5. package/dist/account-home.js.map +1 -0
  6. package/dist/app-config.d.ts +3 -1
  7. package/dist/app-config.d.ts.map +1 -1
  8. package/dist/app-config.js +8 -5
  9. package/dist/app-config.js.map +1 -1
  10. package/dist/app.d.ts +25 -0
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +237 -16
  13. package/dist/app.js.map +1 -1
  14. package/dist/background-environment.d.ts +16 -0
  15. package/dist/background-environment.d.ts.map +1 -0
  16. package/dist/background-environment.js +68 -0
  17. package/dist/background-environment.js.map +1 -0
  18. package/dist/background-runtime.d.ts +62 -0
  19. package/dist/background-runtime.d.ts.map +1 -0
  20. package/dist/background-runtime.js +1115 -0
  21. package/dist/background-runtime.js.map +1 -0
  22. package/dist/background-snapshot-key.d.ts +13 -0
  23. package/dist/background-snapshot-key.d.ts.map +1 -0
  24. package/dist/background-snapshot-key.js +194 -0
  25. package/dist/background-snapshot-key.js.map +1 -0
  26. package/dist/background-snapshot.d.ts +96 -0
  27. package/dist/background-snapshot.d.ts.map +1 -0
  28. package/dist/background-snapshot.js +450 -0
  29. package/dist/background-snapshot.js.map +1 -0
  30. package/dist/background-worker-lease.d.ts +39 -0
  31. package/dist/background-worker-lease.d.ts.map +1 -0
  32. package/dist/background-worker-lease.js +356 -0
  33. package/dist/background-worker-lease.js.map +1 -0
  34. package/dist/background.d.ts +74 -2
  35. package/dist/background.d.ts.map +1 -1
  36. package/dist/background.js +815 -73
  37. package/dist/background.js.map +1 -1
  38. package/dist/channel-plugins.d.ts +2 -0
  39. package/dist/channel-plugins.d.ts.map +1 -1
  40. package/dist/channel-plugins.js +8 -0
  41. package/dist/channel-plugins.js.map +1 -1
  42. package/dist/channels.d.ts +10 -0
  43. package/dist/channels.d.ts.map +1 -1
  44. package/dist/channels.js +71 -3
  45. package/dist/channels.js.map +1 -1
  46. package/dist/cli.d.ts +12 -2
  47. package/dist/cli.d.ts.map +1 -1
  48. package/dist/cli.js +341 -63
  49. package/dist/cli.js.map +1 -1
  50. package/dist/config-reference.d.ts.map +1 -1
  51. package/dist/config-reference.js +253 -0
  52. package/dist/config-reference.js.map +1 -1
  53. package/dist/configuration-proposal-tool.d.ts +7 -0
  54. package/dist/configuration-proposal-tool.d.ts.map +1 -1
  55. package/dist/configuration-proposal-tool.js +55 -6
  56. package/dist/configuration-proposal-tool.js.map +1 -1
  57. package/dist/configured-agent.d.ts +4 -0
  58. package/dist/configured-agent.d.ts.map +1 -1
  59. package/dist/configured-agent.js +15 -0
  60. package/dist/configured-agent.js.map +1 -1
  61. package/dist/continuation-command.d.ts +15 -0
  62. package/dist/continuation-command.d.ts.map +1 -0
  63. package/dist/continuation-command.js +170 -0
  64. package/dist/continuation-command.js.map +1 -0
  65. package/dist/continuation-config.d.ts +25 -0
  66. package/dist/continuation-config.d.ts.map +1 -0
  67. package/dist/continuation-config.js +236 -0
  68. package/dist/continuation-config.js.map +1 -0
  69. package/dist/continuation-service.d.ts +99 -0
  70. package/dist/continuation-service.d.ts.map +1 -0
  71. package/dist/continuation-service.js +1313 -0
  72. package/dist/continuation-service.js.map +1 -0
  73. package/dist/continuation-store.d.ts +104 -0
  74. package/dist/continuation-store.d.ts.map +1 -0
  75. package/dist/continuation-store.js +688 -0
  76. package/dist/continuation-store.js.map +1 -0
  77. package/dist/continuations.d.ts +167 -0
  78. package/dist/continuations.d.ts.map +1 -0
  79. package/dist/continuations.js +71 -0
  80. package/dist/continuations.js.map +1 -0
  81. package/dist/doctor.d.ts +2 -0
  82. package/dist/doctor.d.ts.map +1 -1
  83. package/dist/doctor.js +426 -27
  84. package/dist/doctor.js.map +1 -1
  85. package/dist/first-run-managed-memory.d.ts.map +1 -1
  86. package/dist/first-run-managed-memory.js +66 -3
  87. package/dist/first-run-managed-memory.js.map +1 -1
  88. package/dist/first-run-readiness.d.ts.map +1 -1
  89. package/dist/first-run-readiness.js +2 -31
  90. package/dist/first-run-readiness.js.map +1 -1
  91. package/dist/index.d.ts +9 -0
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +4 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/init.d.ts +7 -0
  96. package/dist/init.d.ts.map +1 -1
  97. package/dist/init.js +7 -2
  98. package/dist/init.js.map +1 -1
  99. package/dist/launchd.d.ts +16 -6
  100. package/dist/launchd.d.ts.map +1 -1
  101. package/dist/launchd.js +30 -15
  102. package/dist/launchd.js.map +1 -1
  103. package/dist/local-configuration.d.ts +61 -18
  104. package/dist/local-configuration.d.ts.map +1 -1
  105. package/dist/local-configuration.js +530 -145
  106. package/dist/local-configuration.js.map +1 -1
  107. package/dist/managed-runtime-packages.d.ts +9 -0
  108. package/dist/managed-runtime-packages.d.ts.map +1 -0
  109. package/dist/managed-runtime-packages.js +56 -0
  110. package/dist/managed-runtime-packages.js.map +1 -0
  111. package/dist/memory-embedding-service.d.ts +33 -0
  112. package/dist/memory-embedding-service.d.ts.map +1 -0
  113. package/dist/memory-embedding-service.js +207 -0
  114. package/dist/memory-embedding-service.js.map +1 -0
  115. package/dist/memory-recall.d.ts +5 -4
  116. package/dist/memory-recall.d.ts.map +1 -1
  117. package/dist/memory-recall.js +21 -8
  118. package/dist/memory-recall.js.map +1 -1
  119. package/dist/modules/catalog.d.ts.map +1 -1
  120. package/dist/modules/catalog.js +91 -9
  121. package/dist/modules/catalog.js.map +1 -1
  122. package/dist/notify-runtime.d.ts +26 -0
  123. package/dist/notify-runtime.d.ts.map +1 -0
  124. package/dist/notify-runtime.js +27 -0
  125. package/dist/notify-runtime.js.map +1 -0
  126. package/dist/notify-tool.d.ts +51 -0
  127. package/dist/notify-tool.d.ts.map +1 -0
  128. package/dist/notify-tool.js +182 -0
  129. package/dist/notify-tool.js.map +1 -0
  130. package/dist/proactive-notify.d.ts +2 -0
  131. package/dist/proactive-notify.d.ts.map +1 -1
  132. package/dist/proactive-notify.js +1 -0
  133. package/dist/proactive-notify.js.map +1 -1
  134. package/dist/process-incarnation.d.ts +23 -0
  135. package/dist/process-incarnation.d.ts.map +1 -0
  136. package/dist/process-incarnation.js +208 -0
  137. package/dist/process-incarnation.js.map +1 -0
  138. package/dist/project-skills.d.ts +1 -1
  139. package/dist/project-skills.js +6 -6
  140. package/dist/recipes/base.d.ts +15 -0
  141. package/dist/recipes/base.d.ts.map +1 -0
  142. package/dist/recipes/base.js +51 -0
  143. package/dist/recipes/base.js.map +1 -0
  144. package/dist/recipes/catalog.d.ts +4 -0
  145. package/dist/recipes/catalog.d.ts.map +1 -0
  146. package/dist/recipes/catalog.js +525 -0
  147. package/dist/recipes/catalog.js.map +1 -0
  148. package/dist/recipes/index.d.ts +11 -0
  149. package/dist/recipes/index.d.ts.map +1 -0
  150. package/dist/recipes/index.js +14 -0
  151. package/dist/recipes/index.js.map +1 -0
  152. package/dist/recipes/types.d.ts +70 -0
  153. package/dist/recipes/types.d.ts.map +1 -0
  154. package/dist/recipes/types.js +15 -0
  155. package/dist/recipes/types.js.map +1 -0
  156. package/dist/sandbox-manager.d.ts +17 -0
  157. package/dist/sandbox-manager.d.ts.map +1 -1
  158. package/dist/sandbox-manager.js +673 -127
  159. package/dist/sandbox-manager.js.map +1 -1
  160. package/dist/setup.d.ts +29 -0
  161. package/dist/setup.d.ts.map +1 -0
  162. package/dist/setup.js +97 -0
  163. package/dist/setup.js.map +1 -0
  164. package/dist/supermemory-plugin.d.ts +5 -3
  165. package/dist/supermemory-plugin.d.ts.map +1 -1
  166. package/dist/supermemory-plugin.js +11 -5
  167. package/dist/supermemory-plugin.js.map +1 -1
  168. package/dist/tui-command.d.ts +43 -2
  169. package/dist/tui-command.d.ts.map +1 -1
  170. package/dist/tui-command.js +261 -27
  171. package/dist/tui-command.js.map +1 -1
  172. package/dist/wizard/answers.d.ts +4 -4
  173. package/dist/wizard/answers.d.ts.map +1 -1
  174. package/dist/wizard/answers.js +4 -21
  175. package/dist/wizard/answers.js.map +1 -1
  176. package/dist/wizard/run.d.ts.map +1 -1
  177. package/dist/wizard/run.js +162 -5
  178. package/dist/wizard/run.js.map +1 -1
  179. package/package.json +15 -15
  180. package/schema/mono-agent.config.schema.json +206 -3
  181. package/skills/mono-agent-composer/SKILL.md +4 -4
  182. package/skills/mono-agent-composer/references/config-blueprint.md +6 -6
  183. package/skills/mono-agent-composer/references/discovery-questions.md +25 -10
  184. package/skills/mono-agent-composer/references/feature-coverage.md +7 -7
  185. package/skills/mono-agent-composer/references/package-map.md +1 -1
  186. package/skills/mono-agent-composer/references/validation.md +2 -2
package/dist/cli.js CHANGED
@@ -13,7 +13,9 @@ import { startMonoAgentApp } from "./app.js";
13
13
  import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, } from "./app-config.js";
14
14
  import { runAuditRuns } from "./audit-runs.js";
15
15
  import { runBackfill } from "./backfill.js";
16
- import { defaultBackgroundDeps, resolveInstanceTarget, restartBackground, startBackground, statusBackground, stopBackground, tailLogs, } from "./background.js";
16
+ import { acquireBackgroundWorkerLease, canonicalBackgroundConfigPath, defaultBackgroundDeps, ensureBackgroundReady, forceRestartBackground, managedBackgroundEnvironment, resolveInstanceTarget, restartBackground, startBackground, statusBackground, stopBackground, tailLogs, } from "./background.js";
17
+ import { MANAGED_BACKGROUND_WORKER_ENV, sanitizeManagedBackgroundWorkerEnvironment, } from "./background-runtime.js";
18
+ import { captureBackgroundSnapshot, decodeBackgroundSnapshot, captureDurableBackgroundInputs, loadDurableBackgroundEnvironment, materializeBackgroundRuntimeInputs, } from "./background-snapshot.js";
17
19
  import { collectChannelConfigViews } from "./channel-config-view.js";
18
20
  import { formatChannelFactValue } from "./channel-fact-format.js";
19
21
  import { resolveChannelDrivers } from "./channels.js";
@@ -22,6 +24,7 @@ import { validateMonoAgentFolder } from "./doctor.js";
22
24
  import { effectiveFirstRunEnvironment, evaluateFirstRunConfigurationReadiness, evaluateFirstRunReadiness, hasSensitivePersistedEnvironmentValue, piAuthPathBackgroundConflict, readCliConfigSnapshot, readCliDotenvFile, readCliDotenvSnapshot, resolveEffectivePiAuthPath, selectedSecretEnvironmentConflicts, selectedSecretValues, unexpectedPersistedMonoAgentOverrides, validateWizardPlanInStaging, withExactProcessEnvironment, } from "./first-run-readiness.js";
23
25
  import { initMonoAgentFolder, SecretEnvConcurrentModificationError, verifySecretEnvPersistenceGuard, } from "./init.js";
24
26
  import { installComposerSkill } from "./install-skill.js";
27
+ import { deriveLaunchdLabel, launchdPathsFor } from "./launchd.js";
25
28
  import { checkManagedProjectSkills, updateManagedProjectSkills } from "./project-skills.js";
26
29
  import { runMetrics } from "./metrics.js";
27
30
  import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
@@ -41,7 +44,7 @@ const DEFAULT_LOG_LINES = 200;
41
44
  // busy-waiting; larger values silently overflow to a 1ms delay.
42
45
  const KEEP_ALIVE_INTERVAL_MS = 2_147_483_647;
43
46
  const BACKGROUND_COMMANDS = ["start", "restart", "stop", "status", "logs"];
44
- const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "auth", "sandbox", "config", "recipes", "presets", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory"];
47
+ const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "auth", "sandbox", "config", "recipes", "presets", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory", "continuations"];
45
48
  export function parseCliArgs(argv) {
46
49
  const [command, ...rest] = argv;
47
50
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
@@ -83,6 +86,7 @@ export function parseCliArgs(argv) {
83
86
  let codexAuthMode;
84
87
  const positionals = [];
85
88
  let envFile;
89
+ let expectedBackgroundSnapshot;
86
90
  let target;
87
91
  let force = false;
88
92
  let foreground = false;
@@ -108,6 +112,7 @@ export function parseCliArgs(argv) {
108
112
  let json = false;
109
113
  let strict = false;
110
114
  let limit;
115
+ let cursor;
111
116
  let host;
112
117
  let port;
113
118
  let open;
@@ -196,12 +201,18 @@ export function parseCliArgs(argv) {
196
201
  case "--limit": {
197
202
  const raw = requireValue(rest, ++i, flag);
198
203
  const parsed = Number(raw);
199
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
200
- throw new Error("--limit must be an integer between 1 and 100.");
204
+ const maximum = cmd === "continuations" ? 500 : 100;
205
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maximum) {
206
+ throw new Error(`--limit must be an integer between 1 and ${String(maximum)}.`);
201
207
  }
202
208
  limit = parsed;
203
209
  break;
204
210
  }
211
+ case "--cursor":
212
+ cursor = requireValue(rest, ++i, flag).trim();
213
+ if (cursor.length === 0 || cursor.length > 512)
214
+ throw new Error("--cursor must be 1-512 characters.");
215
+ break;
205
216
  case "--host":
206
217
  host = requireValue(rest, ++i, flag);
207
218
  break;
@@ -335,6 +346,9 @@ export function parseCliArgs(argv) {
335
346
  case "--env-file":
336
347
  envFile = requireValue(rest, ++i, flag);
337
348
  break;
349
+ case "--expected-background-snapshot":
350
+ expectedBackgroundSnapshot = requireValue(rest, ++i, flag);
351
+ break;
338
352
  case "--target": {
339
353
  const raw = requireValue(rest, ++i, flag);
340
354
  if (raw !== "claude" && raw !== "codex" && raw !== "both") {
@@ -388,8 +402,8 @@ export function parseCliArgs(argv) {
388
402
  if ((local || configure) && cmd !== "tui") {
389
403
  throw new Error("--local and --configure are only supported for `mono-agent tui`.");
390
404
  }
391
- if (configure && !local) {
392
- throw new Error("--configure requires `mono-agent tui --local`.");
405
+ if (configure && local) {
406
+ throw new Error("--configure attaches to the authoritative background agent; omit --local.");
393
407
  }
394
408
  if ((project || check || update) && cmd !== "install-skill") {
395
409
  throw new Error("--project, --check, and --update are only supported for `mono-agent install-skill`.");
@@ -412,8 +426,14 @@ export function parseCliArgs(argv) {
412
426
  if (includeMemory && cmd !== "audit-runs" && cmd !== "metrics" && cmd !== "backfill" && cmd !== "web") {
413
427
  throw new Error("--include-memory is only supported for `mono-agent audit-runs`, `mono-agent metrics`, `mono-agent backfill`, and `mono-agent web`.");
414
428
  }
415
- if (limit !== undefined && cmd !== "memory") {
416
- throw new Error("--limit is only supported for `mono-agent memory`.");
429
+ if (limit !== undefined && cmd !== "memory" && cmd !== "continuations") {
430
+ throw new Error("--limit is only supported for `mono-agent memory` and `mono-agent continuations list`.");
431
+ }
432
+ if ((limit !== undefined || cursor !== undefined) && cmd === "continuations" && (positionals[0] ?? "list") !== "list") {
433
+ throw new Error("--limit and --cursor are only supported for `mono-agent continuations list`.");
434
+ }
435
+ if (cursor !== undefined && cmd !== "continuations") {
436
+ throw new Error("--cursor is only supported for `mono-agent continuations list`.");
417
437
  }
418
438
  if (strict && (cmd !== "memory" || (positionals[0] ?? "stats") !== "audit")) {
419
439
  throw new Error("--strict is only supported for `mono-agent memory audit`.");
@@ -460,6 +480,7 @@ export function parseCliArgs(argv) {
460
480
  ...(codexAuthMode === undefined ? {} : { codexAuthMode }),
461
481
  positionals,
462
482
  ...(envFile === undefined ? {} : { envFile }),
483
+ ...(expectedBackgroundSnapshot === undefined ? {} : { expectedBackgroundSnapshot }),
463
484
  ...(target === undefined ? {} : { target }),
464
485
  force,
465
486
  foreground,
@@ -478,6 +499,7 @@ export function parseCliArgs(argv) {
478
499
  ...(json ? { json } : {}),
479
500
  ...(strict ? { strict } : {}),
480
501
  ...(limit === undefined ? {} : { limit }),
502
+ ...(cursor === undefined ? {} : { cursor }),
481
503
  ...(agent === undefined ? {} : { agent }),
482
504
  ...(conversation === undefined ? {} : { conversation }),
483
505
  ...(local ? { local } : {}),
@@ -617,14 +639,14 @@ const HELP_COMMANDS = [
617
639
  },
618
640
  {
619
641
  signature: "mono-agent tui [--agent <label|sourceId>] [--conversation <id>]\n" +
620
- " [--local [--configure]]",
642
+ " [--configure | --local]",
621
643
  lines: [
622
644
  "Open the operator console from any directory: live chat with full",
623
645
  "thinking/tool/telemetry insight, recorded-run replay, and config view.",
624
646
  "Discovers running agents via the trace-source registry; one running",
625
647
  "agent connects directly, several open a picker.",
626
- "--local builds the current folder's responder in-process without a",
627
- "daemon; --configure starts the recorded local configuration invitation.",
648
+ "--configure opens the guided configuration chat on the authoritative",
649
+ "background agent; --local is an ordinary in-process chat only.",
628
650
  ],
629
651
  },
630
652
  {
@@ -694,6 +716,14 @@ const HELP_COMMANDS = [
694
716
  "operation. It returns metadata only and requires rebuild before restart.",
695
717
  ],
696
718
  },
719
+ {
720
+ signature: "mono-agent continuations [list [--limit <n>] [--cursor <opaque>]|health|retry <id>|cancel <id>|resolve <id> delivered|not-delivered|dead-lettered [delivery-id]] [--json]",
721
+ lines: [
722
+ "Inspect and operate the authenticated durable-continuation service.",
723
+ "Ambiguous delivery cannot be retried until it is explicitly resolved;",
724
+ "no command accepts or changes a channel destination.",
725
+ ],
726
+ },
697
727
  ];
698
728
  const HELP_NOTES = `Background mode runs the agent under launchd, keeping it alive across logins
699
729
  (auto-restarting only on crash) until you run stop. Secrets are read from the
@@ -776,6 +806,21 @@ export async function runCli(argv) {
776
806
  process.stdout.write(`\n${renderHelp()}`);
777
807
  return 2;
778
808
  }
809
+ // Only the internal launchd foreground shape may honor the managed-worker
810
+ // marker. A hostile/global launchctl environment must not sanitize unrelated
811
+ // commands such as `mono-agent validate` or `mono-agent status`.
812
+ const managedBackgroundWorker = args.command === "start" && args.foreground && process.env[MANAGED_BACKGROUND_WORKER_ENV] === "1";
813
+ if (args.expectedBackgroundSnapshot !== undefined && !managedBackgroundWorker) {
814
+ process.stderr.write(ui.errorLine("--expected-background-snapshot is reserved for the managed LaunchAgent worker."));
815
+ return 2;
816
+ }
817
+ if (managedBackgroundWorker && args.expectedBackgroundSnapshot === undefined) {
818
+ process.stderr.write(ui.errorLine("Managed LaunchAgent worker is missing its approved background snapshot."));
819
+ return 0;
820
+ }
821
+ if (managedBackgroundWorker) {
822
+ sanitizeManagedBackgroundWorkerEnvironment(process.env);
823
+ }
779
824
  if (args.command === "memory"
780
825
  && args.positionals[0] === "adopt-replay"
781
826
  && hasUnsupportedReplayAdoptionFlag(argv)) {
@@ -822,7 +867,7 @@ export async function runCli(argv) {
822
867
  case "presets":
823
868
  return runPresets(args);
824
869
  case "start":
825
- return await runStart(args);
870
+ return await runStart(args, undefined, managedBackgroundWorker);
826
871
  case "restart":
827
872
  case "stop":
828
873
  case "status":
@@ -835,6 +880,7 @@ export async function runCli(argv) {
835
880
  configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
836
881
  cwd: process.cwd(),
837
882
  env: process.env,
883
+ ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
838
884
  ...(args.agent === undefined ? {} : { agent: args.agent }),
839
885
  ...(args.conversation === undefined ? {} : { conversationId: args.conversation }),
840
886
  ...(args.local === true ? { local: true } : {}),
@@ -859,6 +905,18 @@ export async function runCli(argv) {
859
905
  }
860
906
  case "install-skill":
861
907
  return await runInstallSkill(args);
908
+ case "continuations": {
909
+ const { runContinuationCommand } = await import("./continuation-command.js");
910
+ return await runContinuationCommand({
911
+ cwd: process.cwd(),
912
+ configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
913
+ env: process.env,
914
+ positionals: args.positionals,
915
+ ...(args.json === true ? { json: true } : {}),
916
+ ...(args.limit === undefined ? {} : { limit: args.limit }),
917
+ ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
918
+ });
919
+ }
862
920
  case "backfill":
863
921
  return await runBackfill({
864
922
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
@@ -1346,6 +1404,14 @@ async function runInit(args, environment) {
1346
1404
  printIncompleteSetup(["The committed .env could not be read back safely; no readiness claim is safe."], result.configPath);
1347
1405
  return 1;
1348
1406
  }
1407
+ // A missing default .env is a valid, durable empty environment. Keep
1408
+ // proving its absence in the approved snapshot, but do not turn that
1409
+ // default into an explicit --env-file argument: the managed worker
1410
+ // treats an explicitly named missing file as an operator error.
1411
+ const backgroundEnvFile = args.envFile === undefined
1412
+ && committedDotenvSnapshot.fingerprint === "missing"
1413
+ ? undefined
1414
+ : environment.dotenvPath;
1349
1415
  const postWriteConflicts = selectedSecretEnvironmentConflicts(result.plan, environment.shellEnv, committedDotenvSnapshot.env, moduleSecrets);
1350
1416
  const postWriteOverrides = unexpectedPersistedMonoAgentOverrides(result.plan, committedDotenvSnapshot.env);
1351
1417
  const postWritePiAuthConflict = piAuthPathBackgroundConflict({
@@ -1369,6 +1435,28 @@ async function runInit(args, environment) {
1369
1435
  printIncompleteSetup(["The durable environment changed after the primary-model check. Retry setup before claiming readiness."], result.configPath);
1370
1436
  return 1;
1371
1437
  }
1438
+ let committedBackgroundSnapshot;
1439
+ let committedBackgroundEnvironment;
1440
+ let committedBackgroundConfigSourceFingerprint;
1441
+ try {
1442
+ const durableInputs = await captureDurableBackgroundInputs({
1443
+ cwd,
1444
+ configPath: result.configPath,
1445
+ envFile: environment.dotenvPath,
1446
+ operationalEnvironment: managedBackgroundEnvironment(postWriteEnv),
1447
+ });
1448
+ committedBackgroundSnapshot = durableInputs.snapshot;
1449
+ committedBackgroundEnvironment = durableInputs.environment;
1450
+ committedBackgroundConfigSourceFingerprint = durableInputs.configSourceFingerprint;
1451
+ }
1452
+ catch {
1453
+ printIncompleteSetup(["The complete committed config, dotenv, Identity, Soul, and MCP snapshot could not be proven safely."], result.configPath);
1454
+ return 1;
1455
+ }
1456
+ if (committedBackgroundConfigSourceFingerprint !== committedConfigSnapshot.fingerprint) {
1457
+ printIncompleteSetup(["The committed config or durable environment changed before the background snapshot was captured."], result.configPath);
1458
+ return 1;
1459
+ }
1372
1460
  printInitResult(result);
1373
1461
  let report;
1374
1462
  try {
@@ -1416,7 +1504,7 @@ async function runInit(args, environment) {
1416
1504
  return 1;
1417
1505
  }
1418
1506
  process.stdout.write(ui.badge("ok") + ui.style.green("All runtime route checks passed — every selected model produced a real no-tool response.\n") +
1419
- ui.badge("ok") + ui.style.green("Agent ready — every selected capability passed full validation.\n"));
1507
+ ui.badge("ok") + ui.style.green("Agent configuration validated — every selected capability passed full validation.\n"));
1420
1508
  const preTuiConfigDrift = await firstRunConfigDrift(result.configPath, committedConfigSnapshot);
1421
1509
  if (preTuiConfigDrift !== undefined) {
1422
1510
  printIncompleteSetup([preTuiConfigDrift.message], result.configPath);
@@ -1432,13 +1520,46 @@ async function runInit(args, environment) {
1432
1520
  printIncompleteSetup([preTuiSecretGuard.message], result.configPath);
1433
1521
  return 1;
1434
1522
  }
1435
- process.stdout.write(ui.badge("ok") + ui.style.green("Opening the ready agent in the local configuration TUI.\n"));
1523
+ if (process.platform !== "darwin") {
1524
+ printUnsupportedGuidedInitHandoff(result.configPath, backgroundEnvFile);
1525
+ return 0;
1526
+ }
1527
+ process.stdout.write(ui.hint("Starting the authoritative background agent before configuration chat…"));
1528
+ let background;
1529
+ try {
1530
+ const resolvedBackgroundTarget = await resolveInstanceTarget({
1531
+ args: {
1532
+ configPath: result.configPath,
1533
+ ...(backgroundEnvFile === undefined ? {} : { envFile: backgroundEnvFile }),
1534
+ },
1535
+ env: { ...committedBackgroundEnvironment },
1536
+ cwd,
1537
+ cliPath: fileURLToPath(import.meta.url),
1538
+ requireTui: true,
1539
+ });
1540
+ const backgroundTarget = {
1541
+ ...resolvedBackgroundTarget,
1542
+ expectedSnapshot: committedBackgroundSnapshot,
1543
+ };
1544
+ background = await ensureBackgroundReady(backgroundTarget, defaultBackgroundDeps());
1545
+ }
1546
+ catch (error) {
1547
+ printUnexpectedGuidedBackgroundFailure(result.configPath, backgroundEnvFile, error);
1548
+ return 1;
1549
+ }
1550
+ if (!background.ok) {
1551
+ process.stderr.write(ui.hint("The validated agent files were preserved, but configuration chat was not opened because the background agent is not ready."));
1552
+ return 1;
1553
+ }
1554
+ process.stdout.write(ui.badge("ok") + ui.style.green("Agent ready — the background process reported startup complete.\n") +
1555
+ ui.badge("ok") + ui.style.green("Opening configuration chat on the authoritative background agent.\n"));
1436
1556
  const { runTui } = await import("./tui-command.js");
1437
- return await withExactProcessEnvironment(postWriteEnv, () => runTui({
1557
+ return await withExactProcessEnvironment(committedBackgroundEnvironment, () => runTui({
1438
1558
  configPath: result.configPath,
1439
1559
  cwd,
1440
- env: postWriteEnv,
1441
- local: true,
1560
+ env: committedBackgroundEnvironment,
1561
+ ...(backgroundEnvFile === undefined ? {} : { envFile: backgroundEnvFile }),
1562
+ agent: background.source.sourceId,
1442
1563
  configure: true,
1443
1564
  }));
1444
1565
  }
@@ -2521,7 +2642,7 @@ async function runAuth(args) {
2521
2642
  return 2;
2522
2643
  }
2523
2644
  const cwd = process.cwd();
2524
- const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
2645
+ const configPath = await canonicalBackgroundConfigPath(cwd, args.configPath);
2525
2646
  const directCodex = provider === "codex";
2526
2647
  if (directCodex && args.piAuthPath !== undefined) {
2527
2648
  process.stderr.write(ui.errorLine("--pi-auth-path does not apply to direct Codex login."));
@@ -2728,6 +2849,19 @@ export function initChangeDisplayRows(result) {
2728
2849
  unchanged: change.kind === "unchanged",
2729
2850
  }));
2730
2851
  }
2852
+ /** Human-readable, value-free proof of where the wizard Role did (or did not) land. */
2853
+ export function identityRoleDisplayLine(identityRole) {
2854
+ switch (identityRole.status) {
2855
+ case "created":
2856
+ return `Role saved to ${identityRole.path} → ${identityRole.section}. ` +
2857
+ `Edit ${identityRole.path} → ${identityRole.section} later to change it.`;
2858
+ case "preserved":
2859
+ return `${identityRole.path} already existed and was preserved; the entered Role was not written. ` +
2860
+ `Add or edit ${identityRole.path} → ${identityRole.section} to set it.`;
2861
+ case "planned-create":
2862
+ return `Dry run: Role would be saved to ${identityRole.path} → ${identityRole.section}.`;
2863
+ }
2864
+ }
2731
2865
  export function secretChecklistDisplayRows(secrets, configured) {
2732
2866
  return secrets.map((secret) => ({
2733
2867
  envVar: secret.envVar,
@@ -2749,6 +2883,7 @@ function printInitResult(result) {
2749
2883
  // included in this result or printed here.
2750
2884
  process.stdout.write(`${prefix}${rendered} ${row.path}\n`);
2751
2885
  }
2886
+ process.stdout.write(`\n${identityRoleDisplayLine(result.identityRole)}\n`);
2752
2887
  if (result.knowledgeFiles.length > 0) {
2753
2888
  process.stdout.write(`\nIdentity references existing knowledge: ${ui.style.cyan(result.knowledgeFiles.join(", "))}\n`);
2754
2889
  }
@@ -2792,12 +2927,45 @@ function printSecretsChecklist(secrets, configured = new Set()) {
2792
2927
  }
2793
2928
  }
2794
2929
  function printNextSteps(configPath) {
2930
+ const startCommand = process.platform === "darwin" ? "mono-agent start" : "mono-agent start --foreground";
2931
+ const tuiCommand = process.platform === "darwin"
2932
+ ? `mono-agent tui --configure ${ui.style.dim("(after the agent reports ready)")}`
2933
+ : `mono-agent tui ${ui.style.dim("(ordinary chat after foreground startup; edit config files manually)")}`;
2795
2934
  process.stdout.write("\n" +
2796
2935
  ui.heading("Next steps") +
2797
2936
  ` ${ui.style.bold("1.")} Edit ${configPath} ${ui.style.dim("(model, channels, skills, memory, sandbox)")}\n` +
2798
2937
  ` ${ui.style.bold("2.")} mono-agent validate\n` +
2799
- ` ${ui.style.bold("3.")} mono-agent tui --local --configure\n` +
2800
- ` ${ui.style.bold("4.")} ${process.platform === "darwin" ? "mono-agent start" : "mono-agent start --foreground"} ${ui.style.dim("(optional background/long-running service)")}\n`);
2938
+ ` ${ui.style.bold("3.")} ${startCommand}\n` +
2939
+ ` ${ui.style.bold("4.")} ${tuiCommand}\n`);
2940
+ }
2941
+ function printUnsupportedGuidedInitHandoff(configPath, envFile) {
2942
+ const flags = guidedHandoffFlags(configPath, envFile);
2943
+ process.stdout.write("\n" +
2944
+ ui.heading("Manual start required") +
2945
+ ui.style.yellow("Automatic background start and configuration chat require macOS launchd.\n") +
2946
+ ui.style.dim("The validated agent files were preserved, but no agent process was started and readiness is not claimed.\n") +
2947
+ ` ${ui.style.bold("Configure manually:")} edit ${configPath} and IDENTITY.md, then run mono-agent validate${flags}\n` +
2948
+ ` ${ui.style.bold("Terminal 1:")} mono-agent start --foreground${flags}\n` +
2949
+ ` ${ui.style.bold("Terminal 2:")} mono-agent tui${flags} ${ui.style.dim("(ordinary chat after startup completes)")}\n` +
2950
+ ui.style.dim("Conversational configuration requires the managed macOS background lifecycle.\n"));
2951
+ }
2952
+ function printUnexpectedGuidedBackgroundFailure(configPath, envFile, error) {
2953
+ const flags = guidedHandoffFlags(configPath, envFile);
2954
+ const paths = launchdPathsFor(deriveLaunchdLabel(configPath));
2955
+ process.stderr.write(ui.errorLine(`The background lifecycle failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`));
2956
+ process.stderr.write(ui.style.dim("The validated agent files were preserved, but configuration chat was not opened. Retry or inspect with:\n"));
2957
+ process.stderr.write(` ${ui.style.gray("logs: ")} ${paths.stderrPath}\n` +
2958
+ ` ${paths.stdoutPath}\n` +
2959
+ ` ${ui.style.gray("retry: ")} mono-agent start${flags}\n` +
2960
+ ` ${ui.style.gray("status:")} mono-agent status${flags}\n` +
2961
+ ` ${ui.style.gray("follow:")} mono-agent logs${flags} --follow\n`);
2962
+ }
2963
+ function guidedHandoffFlags(configPath, envFile) {
2964
+ return ` --config ${shellCommandArgument(configPath)}`
2965
+ + (envFile === undefined ? "" : ` --env-file ${shellCommandArgument(envFile)}`);
2966
+ }
2967
+ function shellCommandArgument(value) {
2968
+ return /^[a-zA-Z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
2801
2969
  }
2802
2970
  async function runInstallSkill(args) {
2803
2971
  if (args.project === true) {
@@ -3150,13 +3318,19 @@ function colorDetail(status, detail) {
3150
3318
  * refuse on any `error` section. `waiting` (e.g. Ollama/Phoenix not up yet) is
3151
3319
  * runtime-soft and never blocks.
3152
3320
  */
3153
- export async function ensureStartable(args, env = process.env) {
3154
- const cwd = process.cwd();
3155
- const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
3321
+ export async function ensureStartable(args, env = process.env, options = {}) {
3322
+ const cwd = options.cwd ?? process.cwd();
3323
+ const configPath = options.configPath ?? resolve(cwd, args.configPath ?? "mono-agent.config.json");
3156
3324
  if (!(await pathExists(configPath))) {
3157
3325
  return { ok: false, code: 2, kind: "missing-config", configPath };
3158
3326
  }
3159
- const report = await validateMonoAgentFolder({ env, cwd, configPath, liveness: false });
3327
+ const report = await validateMonoAgentFolder({
3328
+ env,
3329
+ cwd,
3330
+ configPath,
3331
+ liveness: false,
3332
+ ...(options.preferAppPluginInstall === true ? { preferAppPluginInstall: true } : {}),
3333
+ });
3160
3334
  if (!report.ok) {
3161
3335
  return { ok: false, code: 1, kind: "validation", report };
3162
3336
  }
@@ -3185,9 +3359,9 @@ async function pathExists(path) {
3185
3359
  return false;
3186
3360
  }
3187
3361
  }
3188
- async function runStart(args, env) {
3362
+ async function runStart(args, env, managedBackgroundWorker = false) {
3189
3363
  if (args.foreground) {
3190
- return await runForeground(args, env);
3364
+ return await runForeground(args, env, managedBackgroundWorker);
3191
3365
  }
3192
3366
  return await runBackgroundCommand(args, "start", env);
3193
3367
  }
@@ -3196,56 +3370,155 @@ async function runStart(args, env) {
3196
3370
  * plus traceability, and stays alive until a signal. This is what launchd
3197
3371
  * invokes (via `start --foreground`) and what users get with `--foreground`/`-f`.
3198
3372
  */
3199
- async function runForeground(args, env = process.env) {
3200
- const pre = await ensureStartable(args, env);
3201
- if (!pre.ok) {
3202
- printPreflightFailure(pre);
3203
- return pre.code;
3373
+ async function runForeground(args, env = process.env, managedBackgroundWorker = false) {
3374
+ const cwd = process.cwd();
3375
+ const configPath = await canonicalBackgroundConfigPath(cwd, args.configPath);
3376
+ const startupEnvironment = { ...env };
3377
+ let lease;
3378
+ try {
3379
+ lease = await acquireBackgroundWorkerLease(configPath);
3380
+ }
3381
+ catch (error) {
3382
+ process.stderr.write(ui.errorLine(`Could not acquire the worker singleton lease: ${error instanceof Error ? error.message : String(error)}`));
3383
+ return 1;
3384
+ }
3385
+ if (lease === undefined) {
3386
+ process.stderr.write(ui.errorLine(`Another foreground or managed background worker already owns ${configPath}; refusing to start a duplicate.`));
3387
+ return 1;
3388
+ }
3389
+ let runtimeInputs;
3390
+ let app;
3391
+ try {
3392
+ let backgroundSnapshot;
3393
+ if (managedBackgroundWorker) {
3394
+ try {
3395
+ backgroundSnapshot = decodeBackgroundSnapshot(args.expectedBackgroundSnapshot ?? "");
3396
+ const dotenvPath = resolve(cwd, args.envFile ?? ".env");
3397
+ if (backgroundSnapshot.configPath !== configPath || backgroundSnapshot.dotenvPath !== dotenvPath) {
3398
+ throw new Error("The approved snapshot paths do not match the managed worker arguments.");
3399
+ }
3400
+ runtimeInputs = await materializeBackgroundRuntimeInputs({
3401
+ snapshot: backgroundSnapshot,
3402
+ cwd,
3403
+ env: startupEnvironment,
3404
+ });
3405
+ }
3406
+ catch (error) {
3407
+ process.stderr.write(ui.errorLine(`Managed worker could not freeze its startup snapshot: ${error instanceof Error ? error.message : String(error)}`));
3408
+ // KeepAlive restarts only unsuccessful exits. This refusal cannot heal
3409
+ // without a new approved snapshot, so exit successfully and let the
3410
+ // controller unload/recreate the job instead of retrying forever.
3411
+ return 0;
3412
+ }
3413
+ }
3414
+ const preflightEnvironment = runtimeInputs?.environment ?? startupEnvironment;
3415
+ const pre = await ensureStartable(args, preflightEnvironment, runtimeInputs === undefined
3416
+ ? {}
3417
+ : {
3418
+ cwd,
3419
+ configPath: runtimeInputs.configPath,
3420
+ preferAppPluginInstall: true,
3421
+ });
3422
+ if (!pre.ok) {
3423
+ printPreflightFailure(pre);
3424
+ return managedBackgroundWorker ? 0 : pre.code;
3425
+ }
3426
+ try {
3427
+ await readCliConfigSnapshot(runtimeInputs?.configPath ?? configPath);
3428
+ }
3429
+ catch (error) {
3430
+ process.stderr.write(ui.errorLine(`Cannot establish the foreground config identity: ${error instanceof Error ? error.message : String(error)}`));
3431
+ return managedBackgroundWorker ? 0 : 1;
3432
+ }
3433
+ app = await startMonoAgentApp({
3434
+ cwd,
3435
+ configPath,
3436
+ ...(runtimeInputs === undefined ? {} : { configReadPath: runtimeInputs.configPath }),
3437
+ env: runtimeInputs?.environment ?? startupEnvironment,
3438
+ logger: consoleLogger(),
3439
+ ...(backgroundSnapshot === undefined ? {} : { backgroundSnapshot }),
3440
+ });
3441
+ await printAppStatus(app);
3442
+ // Block until a shutdown signal. Returning here (the old behavior) let the
3443
+ // process exit immediately whenever no channel owned a live handle — e.g. a
3444
+ // traceability-only config, now that the operator console is retired and the
3445
+ // trace heartbeat timer is unref'd.
3446
+ return await waitForShutdownSignal(app);
3447
+ }
3448
+ finally {
3449
+ await app?.stop().catch(() => undefined);
3450
+ await runtimeInputs?.dispose().catch(() => undefined);
3451
+ await lease.release().catch((error) => {
3452
+ process.stderr.write(ui.style.yellow(`⚠ Could not cleanly release worker singleton lease ${lease.path}: ${error instanceof Error ? error.message : String(error)}`) + "\n");
3453
+ });
3204
3454
  }
3205
- const app = await startMonoAgentApp({
3206
- cwd: process.cwd(),
3207
- env,
3208
- ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
3209
- logger: consoleLogger(),
3210
- });
3211
- await printAppStatus(app);
3212
- // Block until a shutdown signal. Returning here (the old behavior) let the
3213
- // process exit immediately whenever no channel owned a live handle — e.g. a
3214
- // traceability-only config, now that the operator console is retired and the
3215
- // trace heartbeat timer is unref'd.
3216
- return await waitForShutdownSignal(app);
3217
3455
  }
3218
3456
  async function runBackgroundCommand(args, command, env = process.env) {
3219
3457
  const guard = requireDarwin(command);
3220
3458
  if (guard !== undefined) {
3221
3459
  return guard;
3222
3460
  }
3461
+ let controllerEnvironment;
3462
+ try {
3463
+ controllerEnvironment = await loadDurableBackgroundEnvironment({
3464
+ cwd: process.cwd(),
3465
+ ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
3466
+ operationalEnvironment: managedBackgroundEnvironment(env),
3467
+ });
3468
+ }
3469
+ catch (error) {
3470
+ if (command === "start" || command === "restart") {
3471
+ process.stderr.write(ui.errorLine(`Cannot reconstruct the managed worker environment: ${error instanceof Error ? error.message : String(error)}`));
3472
+ process.stderr.write(ui.hint("No LaunchAgent changes were made. Fix the dotenv path and retry."));
3473
+ return 1;
3474
+ }
3475
+ controllerEnvironment = { ...env };
3476
+ process.stderr.write(ui.style.yellow(`⚠ Could not reconstruct the managed worker environment; ${command} will use the current shell only: ${error instanceof Error ? error.message : String(error)}`) + "\n");
3477
+ }
3223
3478
  // Refuse to launch (or relaunch) an unconfigured/broken folder BEFORE writing
3224
3479
  // the plist and bootstrapping launchctl — otherwise the worker would crash and
3225
3480
  // launchd's KeepAlive would retry it forever. stop/status/logs stay ungated so
3226
3481
  // a broken instance can still be inspected and torn down.
3227
3482
  if (command === "start" || command === "restart") {
3228
- const pre = await ensureStartable(args, env);
3483
+ const pre = await ensureStartable(args, controllerEnvironment);
3229
3484
  if (!pre.ok) {
3230
3485
  printPreflightFailure(pre);
3231
3486
  return pre.code;
3232
3487
  }
3233
3488
  }
3234
- const target = await resolveInstanceTarget({
3489
+ let target = await resolveInstanceTarget({
3235
3490
  args: {
3236
3491
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
3237
3492
  ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
3238
3493
  },
3239
- env,
3494
+ env: controllerEnvironment,
3240
3495
  cwd: process.cwd(),
3241
3496
  cliPath: fileURLToPath(import.meta.url),
3242
3497
  });
3498
+ if (command === "start" || command === "restart") {
3499
+ try {
3500
+ const expectedSnapshot = await captureBackgroundSnapshot({
3501
+ cwd: target.cwd,
3502
+ configPath: target.configPath,
3503
+ ...(target.envFile === undefined ? {} : { envFile: target.envFile }),
3504
+ env: controllerEnvironment,
3505
+ });
3506
+ target = { ...target, expectedSnapshot };
3507
+ }
3508
+ catch (error) {
3509
+ process.stderr.write(ui.errorLine(`Cannot prove the durable background snapshot: ${error instanceof Error ? error.message : String(error)}`));
3510
+ process.stderr.write(ui.hint("No LaunchAgent changes were made. Fix the config/dotenv/Identity/Soul/MCP mismatch and retry."));
3511
+ return 1;
3512
+ }
3513
+ }
3243
3514
  const deps = defaultBackgroundDeps();
3244
3515
  switch (command) {
3245
3516
  case "start":
3246
3517
  return await startBackground(target, deps);
3247
3518
  case "restart":
3248
- return args.force ? await runForceRestart(target, deps) : await restartBackground(target, deps);
3519
+ return args.force
3520
+ ? await runForceRestart(target, deps, controllerEnvironment)
3521
+ : await restartBackground(target, deps);
3249
3522
  case "stop":
3250
3523
  return await stopBackground(target, deps);
3251
3524
  case "status":
@@ -3260,20 +3533,17 @@ async function runBackgroundCommand(args, command, env = process.env) {
3260
3533
  * they are deleted; the runtime recreates the store on the next session, and the
3261
3534
  * agent's durable memory lives elsewhere, so only resumable transcripts are dropped.
3262
3535
  */
3263
- async function runForceRestart(target, deps) {
3264
- const stopCode = await stopBackground(target, deps);
3265
- if (stopCode !== 0) {
3266
- return stopCode;
3267
- }
3268
- const result = await purgeSessions({ env: process.env, cwd: target.cwd, configPath: target.configPath });
3269
- if (result.removed) {
3270
- const count = result.files === 0 ? "" : ` (${result.files} session file${result.files === 1 ? "" : "s"})`;
3271
- process.stdout.write(`${ui.badge("ok")}${ui.style.bold("Cleared persisted sessions")}${count}.\n`);
3272
- }
3273
- else {
3274
- process.stdout.write(ui.style.dim("No persisted sessions to clear (in-memory or none on disk).") + "\n");
3275
- }
3276
- return await startBackground(target, deps);
3536
+ async function runForceRestart(target, deps, environment) {
3537
+ return await forceRestartBackground(target, deps, async () => {
3538
+ const result = await purgeSessions({ env: environment, cwd: target.cwd, configPath: target.configPath });
3539
+ if (result.removed) {
3540
+ const count = result.files === 0 ? "" : ` (${result.files} session file${result.files === 1 ? "" : "s"})`;
3541
+ process.stdout.write(`${ui.badge("ok")}${ui.style.bold("Cleared persisted sessions")}${count}.\n`);
3542
+ }
3543
+ else {
3544
+ process.stdout.write(ui.style.dim("No persisted sessions to clear (in-memory or none on disk).") + "\n");
3545
+ }
3546
+ });
3277
3547
  }
3278
3548
  /**
3279
3549
  * Background service mode is launchd-specific. On other platforms point the
@@ -3419,8 +3689,16 @@ export function waitForShutdownSignal(app) {
3419
3689
  clearInterval(keepAlive);
3420
3690
  void (async () => {
3421
3691
  process.stdout.write("\n" + ui.hint(`Received ${signal}; stopping mono agent app…`));
3422
- await app.stop();
3423
- resolve(0);
3692
+ try {
3693
+ await app.stop();
3694
+ resolve(0);
3695
+ }
3696
+ catch (error) {
3697
+ process.stderr.write(ui.errorLine(`Foreground shutdown failed: ${error instanceof Error ? error.message : String(error)}`));
3698
+ // Resolve so runForeground's finally block can retry idempotent app
3699
+ // cleanup and release the process-lifetime singleton lease.
3700
+ resolve(1);
3701
+ }
3424
3702
  })();
3425
3703
  };
3426
3704
  process.on("SIGINT", onSignal);