@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/doctor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile as execFileCallback } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
- import { access, lstat, mkdir, readFile, stat } from "node:fs/promises";
3
+ import { access, lstat, mkdir, readdir, readFile, stat } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { isIP } from "node:net";
6
6
  import { join } from "node:path";
@@ -8,8 +8,8 @@ import { promisify } from "node:util";
8
8
  import { getBuiltinModels as getPiBuiltinModels, getBuiltinProviders as getPiBuiltinProviders, } from "@earendil-works/pi-ai/providers/all";
9
9
  import { listRecordedRuns } from "@mono-agent/observability";
10
10
  import { serializeTraceSpans } from "@mono-agent/observability/otel";
11
- import { loadToolPolicyFromJsonFile } from "@mono-agent/agent-harness";
12
- import { defaultExecutionModeForModel, describeMonoRuntimeSupport, modelReferenceKey, networkPolicyAllowsUrl, parseMonoRuntimeModelReference, resolveModelEffortLevels, } from "@mono-agent/runtime-adapter";
11
+ import { classifyContinuationMcpServerTransport, isStdioMcpServerSpec, loadToolPolicyFromJsonFile, } from "@mono-agent/agent-harness";
12
+ import { defaultExecutionModeForModel, describeMonoRuntimeSupport, isValidMcpServerName, modelReferenceKey, networkPolicyAllowsUrl, parseMonoRuntimeModelReference, resolveModelEffortLevels, } from "@mono-agent/runtime-adapter";
13
13
  import { buildMonoAgentConfigView, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, resolveSupermemoryContainer, } from "@mono-agent/config";
14
14
  import { describeSandboxEffectiveState, resolveSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
15
15
  import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, resolveAppArtifactDir, resolveAppObservabilityExporters, } from "./app-config.js";
@@ -18,8 +18,12 @@ import { canonicalToolName, isAllowAllTools, isKnownToolName, isMcpToolName, sug
18
18
  import { collectChannelConfigViews } from "./channel-config-view.js";
19
19
  import { resolveChannelDrivers } from "./channels.js";
20
20
  import { findUnknownAppConfigWarnings } from "./config-reference.js";
21
+ import { loadContinuationSettings } from "./continuation-config.js";
22
+ import { CONTINUATION_RECORD_STORE_SCHEMA_VERSION, CONTINUATION_STORE_SCHEMA_VERSION, } from "./continuation-store.js";
23
+ import { CONTINUATION_STATES } from "./continuations.js";
21
24
  import { formatInteractionBridgeUrl, loadInteractionSettings } from "./interaction-bridge.js";
22
25
  import { FIRST_RUN_MEMORY_INITIALIZING_MARKER } from "./first-run-managed-memory.js";
26
+ import { DEFAULT_MEMORY_EMBEDDING_ENDPOINTS, probeMemoryEmbeddingSelection, } from "./memory-embedding-service.js";
23
27
  import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
24
28
  import { piAuthRecoveryCommand } from "./provider-setup.js";
25
29
  import { inspectPiAuthStore } from "./pi-auth-store-inspection.js";
@@ -71,8 +75,9 @@ export async function validateMonoAgentFolder(options) {
71
75
  sections.push(runtimeSection(coreConfig));
72
76
  sections.push(await credentialsSection(coreConfig, options.env, options.cwd, liveness, options.verifiedCredentialModelRefs, options.sdkAuthStatusExecFile, staticTriggerCredentialRefs));
73
77
  sections.push(await contextSection(coreConfig, options.cwd));
74
- sections.push(await memorySection(coreConfig, options.cwd, liveness, allowFilesystemWrites));
78
+ sections.push(await memorySection(coreConfig, options.cwd, options.env, liveness, allowFilesystemWrites, options.preferAppPluginInstall === true));
75
79
  sections.push(await toolsSection(coreConfig, options));
80
+ sections.push(await continuationSection(coreConfig, options));
76
81
  sections.push(await sandboxSection(coreConfig, options.sandboxEngine));
77
82
  }
78
83
  sections.push(await exporterSection(options, liveness));
@@ -1038,7 +1043,7 @@ async function contextSection(config, cwd) {
1038
1043
  return { id: "context", label: "Context & skills", status, details };
1039
1044
  }
1040
1045
  const DEFAULT_CONSOLIDATION_CRON = "0 */2 * * *";
1041
- async function memorySection(config, cwd, liveness, allowFilesystemWrites) {
1046
+ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites, preferAppPluginInstall) {
1042
1047
  if (config.memory === undefined) {
1043
1048
  return { id: "memory", label: "Memory", status: "disabled", details: ["No memory configured."] };
1044
1049
  }
@@ -1056,7 +1061,7 @@ async function memorySection(config, cwd, liveness, allowFilesystemWrites) {
1056
1061
  };
1057
1062
  }
1058
1063
  try {
1059
- const plugin = await loadSupermemoryPlugin({ cwd });
1064
+ const plugin = await loadSupermemoryPlugin({ cwd, preferAppInstall: preferAppPluginInstall });
1060
1065
  const validation = plugin.validateSupermemoryConfig({
1061
1066
  baseUrl: sm.baseUrl,
1062
1067
  container: resolveSupermemoryContainer(config),
@@ -1146,7 +1151,7 @@ async function memorySection(config, cwd, liveness, allowFilesystemWrites) {
1146
1151
  };
1147
1152
  }
1148
1153
  if (config.memory.mode === "journal" || config.memory.mode === "bujo") {
1149
- const warns = await memoryLivenessWarnings(config.memory, liveness, allowFilesystemWrites);
1154
+ const warns = await memoryLivenessWarnings(config.memory, env, liveness, allowFilesystemWrites);
1150
1155
  if (warns.length > 0) {
1151
1156
  return {
1152
1157
  id: "memory",
@@ -1304,20 +1309,34 @@ async function fetchOllamaModels(endpoint) {
1304
1309
  clearTimeout(timer);
1305
1310
  }
1306
1311
  }
1307
- async function memoryLivenessWarnings(memory, liveness, allowFilesystemWrites) {
1312
+ async function memoryLivenessWarnings(memory, env, liveness, allowFilesystemWrites) {
1308
1313
  const warns = [];
1309
1314
  const mode = memory.mode;
1310
- const embeddingsUsesOllama = (memory.embeddings?.provider ?? "ollama") === "ollama";
1311
1315
  const llmUsesOllama = memory.llm?.provider === "ollama";
1312
1316
  const ollamaModelsByEndpoint = new Map();
1313
1317
  // 1. Memory root writable (every embedded tier) — local I/O, always checked.
1314
1318
  const rootWarns = await memoryRootWritableWarnings(mode, memory.path, allowFilesystemWrites);
1315
1319
  warns.push(...rootWarns);
1320
+ const embeddings = memory.embeddings;
1321
+ let embeddingApiKey = embeddings?.apiKey;
1322
+ if (embeddings?.apiKeyEnv !== undefined) {
1323
+ embeddingApiKey = env[embeddings.apiKeyEnv]?.trim();
1324
+ if (embeddingApiKey === undefined || embeddingApiKey.length === 0) {
1325
+ warns.push(`[WARN] Embeddings apiKeyEnv ${embeddings.apiKeyEnv} is declared, but the resolved environment has no ` +
1326
+ `non-empty value. Set ${embeddings.apiKeyEnv} before starting managed memory; no keyless probe was attempted.`);
1327
+ embeddingApiKey = undefined;
1328
+ }
1329
+ }
1316
1330
  // Network-dependent probes below only ever produce `waiting`, so the start
1317
1331
  // preflight skips them (liveness=false) without changing the pass/fail verdict.
1318
1332
  if (!liveness) {
1319
1333
  return warns;
1320
1334
  }
1335
+ if (embeddings !== undefined
1336
+ && (embeddings.provider === "ollama" || embeddings.provider === "lmstudio")
1337
+ && (embeddings.apiKeyEnv === undefined || embeddingApiKey !== undefined)) {
1338
+ warns.push(...await localEmbeddingLivenessWarnings(embeddings, embeddingApiKey));
1339
+ }
1321
1340
  async function modelsForOllamaEndpoint(endpoint) {
1322
1341
  const normalizedEndpoint = endpoint.replace(/\/$/u, "");
1323
1342
  if (ollamaModelsByEndpoint.has(normalizedEndpoint)) {
@@ -1334,18 +1353,9 @@ async function memoryLivenessWarnings(memory, liveness, allowFilesystemWrites) {
1334
1353
  return undefined;
1335
1354
  }
1336
1355
  }
1337
- // 2. Ollama liveness only probe components that actually use Ollama. OpenAI embeddings and
1338
- // agent-host chat LLMs have no local model list to validate here.
1339
- if (embeddingsUsesOllama) {
1340
- const endpoint = memory.embeddings?.endpoint ?? "http://localhost:11434";
1341
- const ollamaModels = await modelsForOllamaEndpoint(endpoint);
1342
- if (ollamaModels !== undefined) {
1343
- const embeddingsModel = memory.embeddings?.model ?? "nomic-embed-text:v1.5";
1344
- if (!ollamaModels.includes(embeddingsModel)) {
1345
- warns.push(`[WARN] Embeddings model ${embeddingsModel} not pulled — run \`ollama pull ${embeddingsModel}\`.`);
1346
- }
1347
- }
1348
- }
1356
+ // 2. Chat-LLM Ollama liveness remains independent from the selected
1357
+ // embeddings service. OpenAI embeddings and agent-host chat LLMs have no
1358
+ // local typed model catalog to validate here.
1349
1359
  if (llmUsesOllama && memory.llm !== undefined) {
1350
1360
  const endpoint = memory.llm.endpoint ?? "http://localhost:11434";
1351
1361
  const ollamaModels = await modelsForOllamaEndpoint(endpoint);
@@ -1358,6 +1368,52 @@ async function memoryLivenessWarnings(memory, liveness, allowFilesystemWrites) {
1358
1368
  }
1359
1369
  return warns;
1360
1370
  }
1371
+ async function localEmbeddingLivenessWarnings(embeddings, apiKey) {
1372
+ if (embeddings.provider !== "ollama" && embeddings.provider !== "lmstudio")
1373
+ return [];
1374
+ const provider = embeddings.provider;
1375
+ const label = provider === "ollama" ? "Ollama" : "LM Studio";
1376
+ const endpoint = (embeddings.endpoint ?? DEFAULT_MEMORY_EMBEDDING_ENDPOINTS[provider]).replace(/\/+$/u, "");
1377
+ // Runtime readiness is authoritative on the exact configured model. Typed
1378
+ // catalogs help the wizard offer choices, but an unavailable/incomplete
1379
+ // catalog must not make its explicit manual fallback permanently unready.
1380
+ try {
1381
+ await probeMemoryEmbeddingSelection({
1382
+ provider,
1383
+ endpoint,
1384
+ model: embeddings.model,
1385
+ expectedDimension: embeddings.dim ?? 768,
1386
+ timeoutMs: BUJO_PROBE_TIMEOUT_MS,
1387
+ ...(apiKey === undefined ? {} : { apiKey }),
1388
+ });
1389
+ return [];
1390
+ }
1391
+ catch (error) {
1392
+ const reason = error instanceof Error ? error.message : String(error);
1393
+ if (/HTTP 401/u.test(reason)) {
1394
+ return [
1395
+ `[WARN] ${label} authentication failed at ${endpoint} (HTTP 401). ` +
1396
+ "Set the configured embeddings apiKeyEnv to a valid bearer token and retry.",
1397
+ ];
1398
+ }
1399
+ if (/could not connect|timed out/iu.test(reason)) {
1400
+ return [
1401
+ `[WARN] ${label} not reachable at ${endpoint}; embeddings will fail at runtime (${reason}). ` +
1402
+ `Start ${label} or fix the endpoint.`,
1403
+ ];
1404
+ }
1405
+ if (provider === "ollama" && /HTTP 404/u.test(reason)) {
1406
+ return [
1407
+ `[WARN] Ollama embedding model ${embeddings.model} could not be proved at ${endpoint} (${reason}); ` +
1408
+ `run \`ollama pull ${embeddings.model}\` and verify its embedding capability.`,
1409
+ ];
1410
+ }
1411
+ return [
1412
+ `[WARN] ${label} embedding readiness failed for ${embeddings.model} at ${endpoint} (${reason}). ` +
1413
+ "Verify the selected model, authentication, and configured dimension.",
1414
+ ];
1415
+ }
1416
+ }
1361
1417
  async function memoryRootWritableWarnings(mode, memoryPath, allowFilesystemWrites) {
1362
1418
  if (allowFilesystemWrites) {
1363
1419
  try {
@@ -1519,17 +1575,39 @@ async function toolsSection(config, input) {
1519
1575
  }
1520
1576
  }
1521
1577
  for (const serverName of config.tools.mcpRequestContextServers ?? []) {
1578
+ if (!isValidMcpServerName(serverName)) {
1579
+ status = "error";
1580
+ details.push(`tools.mcpRequestContextServers entry "${serverName}" is not a runtime-valid MCP server name (letters, digits, underscores, and hyphens only).`);
1581
+ continue;
1582
+ }
1522
1583
  const spec = configuredMcpServers[serverName];
1523
1584
  if (spec === undefined) {
1524
1585
  status = "error";
1525
1586
  details.push(`tools.mcpRequestContextServers names unknown MCP server "${serverName}"; declare it in tools.mcpConfigPath.`);
1526
1587
  continue;
1527
1588
  }
1528
- if (!isStdioMcpSpec(spec)) {
1589
+ if (!isStdioMcpServerSpec(spec)) {
1529
1590
  status = "error";
1530
1591
  details.push(`tools.mcpRequestContextServers entry "${serverName}" must reference a stdio MCP server (command/type:stdio), not HTTP/SSE.`);
1531
1592
  }
1532
1593
  }
1594
+ for (const serverName of config.tools.continuationServers ?? []) {
1595
+ if (!isValidMcpServerName(serverName)) {
1596
+ status = "error";
1597
+ details.push(`tools.continuationServers entry "${serverName}" is not a runtime-valid MCP server name (letters, digits, underscores, and hyphens only).`);
1598
+ continue;
1599
+ }
1600
+ const spec = configuredMcpServers[serverName];
1601
+ if (spec === undefined) {
1602
+ status = "error";
1603
+ details.push(`tools.continuationServers names unknown MCP server "${serverName}"; declare it in tools.mcpConfigPath.`);
1604
+ continue;
1605
+ }
1606
+ if (classifyContinuationMcpServerTransport(spec) === "unsupported") {
1607
+ status = "error";
1608
+ details.push(`tools.continuationServers entry "${serverName}" must reference a stdio or loopback HTTP MCP server; remote HTTP and SSE are not supported.`);
1609
+ }
1610
+ }
1533
1611
  const adapterSendTools = await resolveAdapterSendToolsSettings(input, {
1534
1612
  allowedTools: config.tools.allowedTools,
1535
1613
  disallowedTools: config.tools.disallowedTools,
@@ -1556,12 +1634,333 @@ async function toolsSection(config, input) {
1556
1634
  }
1557
1635
  return { id: "tools", label: "Tools & MCP", status, details };
1558
1636
  }
1559
- function isStdioMcpSpec(value) {
1560
- if (typeof value !== "object" || value === null || Array.isArray(value))
1637
+ async function continuationSection(config, input) {
1638
+ let settings;
1639
+ try {
1640
+ settings = await loadContinuationSettings({
1641
+ cwd: input.cwd,
1642
+ configPath: input.configPath,
1643
+ env: input.env,
1644
+ });
1645
+ }
1646
+ catch (error) {
1647
+ return {
1648
+ id: "continuations",
1649
+ label: "Durable continuations",
1650
+ status: "error",
1651
+ details: [`Continuation configuration is invalid: ${continuationReason(error)}`],
1652
+ };
1653
+ }
1654
+ const continuationServers = config.tools.continuationServers ?? [];
1655
+ if (!settings.configured && continuationServers.length === 0) {
1656
+ return {
1657
+ id: "continuations",
1658
+ label: "Durable continuations",
1659
+ status: "disabled",
1660
+ details: ["No continuation-capable MCP servers or detached continuation routes are configured."],
1661
+ };
1662
+ }
1663
+ if (!settings.enabled) {
1664
+ return {
1665
+ id: "continuations",
1666
+ label: "Durable continuations",
1667
+ status: "error",
1668
+ details: ["Continuation service is disabled while continuation functionality is configured."],
1669
+ };
1670
+ }
1671
+ const details = [
1672
+ `Loopback service: http://${formatContinuationHost(settings.host)}:${String(settings.port)}.`,
1673
+ `Owner-only state: ${settings.stateDir}.`,
1674
+ continuationServers.length === 0
1675
+ ? "Run-scoped continuation MCP servers: none."
1676
+ : `Run-scoped continuation MCP servers: ${continuationServers.join(", ")}.`,
1677
+ ];
1678
+ const routeNames = Object.entries(settings.namedRoutes).map(([name, route]) => `${name} (${route.mode})`);
1679
+ details.push(routeNames.length === 0 ? "Named detached routes: none." : `Named detached routes: ${routeNames.join(", ")}.`);
1680
+ const detachedNames = Object.keys(settings.detachedServices);
1681
+ details.push(detachedNames.length === 0 ? "Detached services: none." : `Detached services: ${detachedNames.join(", ")}.`);
1682
+ const state = await inspectContinuationState(settings.stateDir);
1683
+ details.push(...state.details);
1684
+ return {
1685
+ id: "continuations",
1686
+ label: "Durable continuations",
1687
+ status: state.status,
1688
+ details,
1689
+ };
1690
+ }
1691
+ async function inspectContinuationState(stateDir) {
1692
+ const details = [];
1693
+ let directory;
1694
+ try {
1695
+ directory = await lstat(stateDir);
1696
+ }
1697
+ catch (error) {
1698
+ if (continuationFsCode(error) === "ENOENT") {
1699
+ return {
1700
+ status: "ok",
1701
+ details: ["State has not been initialized; the app will create it with owner-only permissions on first start."],
1702
+ };
1703
+ }
1704
+ return { status: "error", details: [`Continuation state cannot be inspected: ${continuationReason(error)}`] };
1705
+ }
1706
+ if (!directory.isDirectory() || directory.isSymbolicLink()) {
1707
+ return { status: "error", details: ["Continuation state path must be a real directory, not a file or symlink."] };
1708
+ }
1709
+ const directorySecurityError = continuationOwnershipError(directory, "state directory", 0o700);
1710
+ if (directorySecurityError !== undefined) {
1711
+ return { status: "error", details: [directorySecurityError] };
1712
+ }
1713
+ const secretError = await inspectContinuationSecret(join(stateDir, "continuation-secret"));
1714
+ if (secretError !== undefined) {
1715
+ return { status: "error", details: [secretError] };
1716
+ }
1717
+ const manifestPath = join(stateDir, "continuation-store-v2.json");
1718
+ let manifestInfo;
1719
+ try {
1720
+ manifestInfo = await lstat(manifestPath);
1721
+ }
1722
+ catch (error) {
1723
+ if (continuationFsCode(error) !== "ENOENT") {
1724
+ return { status: "error", details: [`Continuation store manifest cannot be inspected: ${continuationReason(error)}`] };
1725
+ }
1726
+ }
1727
+ if (manifestInfo !== undefined) {
1728
+ if (!manifestInfo.isFile() || manifestInfo.isSymbolicLink()) {
1729
+ return { status: "error", details: ["Continuation store manifest must be a regular file, not a symlink."] };
1730
+ }
1731
+ const manifestSecurityError = continuationOwnershipError(manifestInfo, "store manifest", 0o600);
1732
+ if (manifestSecurityError !== undefined)
1733
+ return { status: "error", details: [manifestSecurityError] };
1734
+ let manifest;
1735
+ try {
1736
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1737
+ }
1738
+ catch (error) {
1739
+ return { status: "error", details: [`Continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
1740
+ }
1741
+ if (!isContinuationStoreManifest(manifest)) {
1742
+ return { status: "error", details: ["Continuation store manifest has an unsupported or malformed schema."] };
1743
+ }
1744
+ const recordsDirectoryError = await inspectContinuationRecordsDirectory(join(stateDir, "records-v2"));
1745
+ if (recordsDirectoryError !== undefined)
1746
+ return { status: "error", details: [recordsDirectoryError] };
1747
+ const transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"));
1748
+ if (!transaction.valid)
1749
+ return { status: "error", details: [transaction.detail] };
1750
+ const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
1751
+ if (!owner.valid)
1752
+ return { status: "error", details: [owner.detail] };
1753
+ details.push(`Store v2: ${String(manifest.stats.records)} retained; ${String(manifest.stats.active)} active; ${String(manifest.stats.unresolvedDelivery)} delivery unknown; ${String(manifest.stats.deadLettered)} dead-lettered; ${String(manifest.stats.historyDegraded)} history-degraded deliveries; ${String(manifest.stats.terminalTombstones)} terminal tombstones; ${String(manifest.stats.capturedText)} captured answers.`, `Retention: at most ${String(manifest.stats.limits.terminalMaxRecords)} terminal tombstones and ${String(manifest.stats.limits.capturedTextMaxRecords)} captured answers.`, transaction.detail, owner.detail);
1754
+ return {
1755
+ status: transaction.pending
1756
+ || manifest.stats.unresolvedDelivery > 0
1757
+ || manifest.stats.deadLettered > 0
1758
+ || manifest.stats.historyDegraded > 0
1759
+ ? "waiting"
1760
+ : "ok",
1761
+ details,
1762
+ };
1763
+ }
1764
+ const storePath = join(stateDir, "continuations-v1.json");
1765
+ let storeInfo;
1766
+ try {
1767
+ storeInfo = await lstat(storePath);
1768
+ }
1769
+ catch (error) {
1770
+ if (continuationFsCode(error) === "ENOENT") {
1771
+ details.push("No continuation ledger has been written yet.");
1772
+ const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
1773
+ details.push(owner.detail);
1774
+ return { status: owner.valid ? "ok" : "error", details };
1775
+ }
1776
+ return { status: "error", details: [`Continuation ledger cannot be inspected: ${continuationReason(error)}`] };
1777
+ }
1778
+ if (!storeInfo.isFile() || storeInfo.isSymbolicLink()) {
1779
+ return { status: "error", details: ["Continuation ledger must be a regular file, not a symlink."] };
1780
+ }
1781
+ const storeSecurityError = continuationOwnershipError(storeInfo, "ledger", 0o600);
1782
+ if (storeSecurityError !== undefined) {
1783
+ return { status: "error", details: [storeSecurityError] };
1784
+ }
1785
+ let parsed;
1786
+ try {
1787
+ parsed = JSON.parse(await readFile(storePath, "utf8"));
1788
+ }
1789
+ catch (error) {
1790
+ return { status: "error", details: [`Continuation ledger contains invalid JSON: ${continuationReason(error)}`] };
1791
+ }
1792
+ if (!isContinuationLedger(parsed)) {
1793
+ return { status: "error", details: ["Continuation ledger has an unsupported or malformed schema."] };
1794
+ }
1795
+ const counts = Object.fromEntries(CONTINUATION_STATES.map((state) => [state, 0]));
1796
+ for (const record of Object.values(parsed.records)) {
1797
+ if (!isDoctorObject(record) || typeof record.state !== "string" || !CONTINUATION_STATES.includes(record.state)) {
1798
+ return { status: "error", details: ["Continuation ledger contains a record with an invalid lifecycle state."] };
1799
+ }
1800
+ counts[record.state] += 1;
1801
+ }
1802
+ const total = Object.values(counts).reduce((sum, count) => sum + count, 0);
1803
+ const pending = counts.claimed + counts.result_received + counts.synthesizing + counts.ready_to_deliver + counts.delivery_retry;
1804
+ details.push(`Legacy ledger awaiting v2 migration: ${String(total)} total; ${String(pending)} pending; ${String(counts.delivery_unknown)} delivery unknown; ${String(counts.dead_lettered)} dead-lettered.`);
1805
+ const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
1806
+ details.push(owner.detail);
1807
+ return {
1808
+ status: !owner.valid
1809
+ ? "error"
1810
+ : counts.delivery_unknown > 0 || counts.dead_lettered > 0
1811
+ ? "waiting"
1812
+ : "ok",
1813
+ details,
1814
+ };
1815
+ }
1816
+ async function inspectContinuationSecret(path) {
1817
+ let info;
1818
+ try {
1819
+ info = await lstat(path);
1820
+ }
1821
+ catch (error) {
1822
+ return continuationFsCode(error) === "ENOENT"
1823
+ ? undefined
1824
+ : `Continuation secret cannot be inspected: ${continuationReason(error)}`;
1825
+ }
1826
+ if (!info.isFile() || info.isSymbolicLink())
1827
+ return "Continuation secret must be a regular file, not a symlink.";
1828
+ const securityError = continuationOwnershipError(info, "secret", 0o600);
1829
+ if (securityError !== undefined)
1830
+ return securityError;
1831
+ try {
1832
+ const secret = Buffer.from((await readFile(path, "utf8")).trim(), "base64url");
1833
+ if (secret.length !== 32)
1834
+ return "Continuation secret contents are invalid.";
1835
+ }
1836
+ catch (error) {
1837
+ return `Continuation secret cannot be read: ${continuationReason(error)}`;
1838
+ }
1839
+ return undefined;
1840
+ }
1841
+ async function inspectContinuationOwnerDatabase(path) {
1842
+ let info;
1843
+ try {
1844
+ info = await lstat(path);
1845
+ }
1846
+ catch (error) {
1847
+ return continuationFsCode(error) === "ENOENT"
1848
+ ? { valid: true, detail: "The OS-backed continuation ownership database has not been initialized yet." }
1849
+ : { valid: false, detail: `Continuation ownership database cannot be inspected: ${continuationReason(error)}` };
1850
+ }
1851
+ if (!info.isFile() || info.isSymbolicLink()) {
1852
+ return { valid: false, detail: "Continuation ownership database is not a regular file." };
1853
+ }
1854
+ const securityError = continuationOwnershipError(info, "ownership database", 0o600);
1855
+ if (securityError !== undefined)
1856
+ return { valid: false, detail: securityError };
1857
+ return { valid: true, detail: "OS-backed exclusive ownership is released automatically on clean stop or process death." };
1858
+ }
1859
+ async function inspectContinuationRecordsDirectory(path) {
1860
+ let info;
1861
+ try {
1862
+ info = await lstat(path);
1863
+ }
1864
+ catch (error) {
1865
+ return continuationFsCode(error) === "ENOENT"
1866
+ ? "Continuation record directory is missing."
1867
+ : `Continuation record directory cannot be inspected: ${continuationReason(error)}`;
1868
+ }
1869
+ if (!info.isDirectory() || info.isSymbolicLink()) {
1870
+ return "Continuation record directory must be a real directory, not a file or symlink.";
1871
+ }
1872
+ const directorySecurityError = continuationOwnershipError(info, "record directory", 0o700);
1873
+ if (directorySecurityError !== undefined)
1874
+ return directorySecurityError;
1875
+ try {
1876
+ for (const entry of await readdir(path, { withFileTypes: true })) {
1877
+ if (!entry.name.endsWith(".json") && !(entry.name.startsWith(".") && entry.name.endsWith(".tmp"))) {
1878
+ return `Continuation record directory contains an unexpected entry: ${entry.name}.`;
1879
+ }
1880
+ const recordInfo = await lstat(join(path, entry.name));
1881
+ if (!recordInfo.isFile() || recordInfo.isSymbolicLink()) {
1882
+ return `Continuation record entry is not a regular file: ${entry.name}.`;
1883
+ }
1884
+ const securityError = continuationOwnershipError(recordInfo, "record", 0o600);
1885
+ if (securityError !== undefined)
1886
+ return securityError;
1887
+ }
1888
+ }
1889
+ catch (error) {
1890
+ return `Continuation record directory cannot be inspected: ${continuationReason(error)}`;
1891
+ }
1892
+ return undefined;
1893
+ }
1894
+ async function inspectContinuationTransaction(path) {
1895
+ let info;
1896
+ try {
1897
+ info = await lstat(path);
1898
+ }
1899
+ catch (error) {
1900
+ return continuationFsCode(error) === "ENOENT"
1901
+ ? { valid: true, pending: false, detail: "No interrupted durable transaction is awaiting recovery." }
1902
+ : { valid: false, pending: false, detail: `Continuation transaction cannot be inspected: ${continuationReason(error)}` };
1903
+ }
1904
+ if (!info.isFile() || info.isSymbolicLink()) {
1905
+ return { valid: false, pending: true, detail: "Continuation transaction must be a regular file, not a symlink." };
1906
+ }
1907
+ const securityError = continuationOwnershipError(info, "transaction", 0o600);
1908
+ if (securityError !== undefined)
1909
+ return { valid: false, pending: true, detail: securityError };
1910
+ return {
1911
+ valid: true,
1912
+ pending: true,
1913
+ detail: "An interrupted durable transaction is present and will be completed idempotently by the state owner.",
1914
+ };
1915
+ }
1916
+ function continuationOwnershipError(info, label, expectedMode) {
1917
+ if (typeof process.getuid === "function" && Number(info.uid) !== process.getuid()) {
1918
+ return `Continuation ${label} is not owned by the current user.`;
1919
+ }
1920
+ if (process.platform !== "win32" && (Number(info.mode) & 0o777) !== expectedMode) {
1921
+ return `Continuation ${label} permissions must be ${expectedMode.toString(8)}.`;
1922
+ }
1923
+ return undefined;
1924
+ }
1925
+ function isContinuationLedger(value) {
1926
+ return isDoctorObject(value)
1927
+ && value.schemaVersion === CONTINUATION_STORE_SCHEMA_VERSION
1928
+ && isDoctorObject(value.records);
1929
+ }
1930
+ function isContinuationStoreManifest(value) {
1931
+ if (!isDoctorObject(value)
1932
+ || value.schemaVersion !== CONTINUATION_RECORD_STORE_SCHEMA_VERSION
1933
+ || typeof value.generation !== "string"
1934
+ || !isDoctorObject(value.stats)
1935
+ || value.stats.format !== "per-record-v2"
1936
+ || !isDoctorObject(value.stats.limits))
1561
1937
  return false;
1562
- const spec = value;
1563
- return spec.type === "stdio"
1564
- || (typeof spec.command === "string" && spec.type !== "http" && spec.type !== "sse");
1938
+ return [
1939
+ value.stats.records,
1940
+ value.stats.active,
1941
+ value.stats.unresolvedDelivery,
1942
+ value.stats.deadLettered,
1943
+ value.stats.historyDegraded,
1944
+ value.stats.terminalTombstones,
1945
+ value.stats.compacted,
1946
+ value.stats.capturedText,
1947
+ value.stats.limits.terminalMaxRecords,
1948
+ value.stats.limits.terminalMaxAgeMs,
1949
+ value.stats.limits.capturedTextMaxRecords,
1950
+ value.stats.limits.capturedTextMaxAgeMs,
1951
+ ].every((entry) => Number.isSafeInteger(entry) && Number(entry) >= 0);
1952
+ }
1953
+ function isDoctorObject(value) {
1954
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1955
+ }
1956
+ function continuationFsCode(error) {
1957
+ return isDoctorObject(error) && typeof error.code === "string" ? error.code : undefined;
1958
+ }
1959
+ function continuationReason(error) {
1960
+ return error instanceof Error ? error.message : String(error);
1961
+ }
1962
+ function formatContinuationHost(host) {
1963
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
1565
1964
  }
1566
1965
  /**
1567
1966
  * Adapter-send tools run in a stdio MCP child governed by the native SRT