@mono-agent/agent-app 0.11.3 → 0.11.4

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 (345) hide show
  1. package/README.md +222 -26
  2. package/dist/adapter-send-tools-main.js +7 -1
  3. package/dist/adapter-send-tools-main.js.map +1 -1
  4. package/dist/adapter-send-tools.d.ts +19 -1
  5. package/dist/adapter-send-tools.d.ts.map +1 -1
  6. package/dist/adapter-send-tools.js +127 -13
  7. package/dist/adapter-send-tools.js.map +1 -1
  8. package/dist/app-config.d.ts.map +1 -1
  9. package/dist/app-config.js +15 -2
  10. package/dist/app-config.js.map +1 -1
  11. package/dist/app-controller-channels.d.ts +8 -0
  12. package/dist/app-controller-channels.d.ts.map +1 -0
  13. package/dist/app-controller-channels.js +207 -0
  14. package/dist/app-controller-channels.js.map +1 -0
  15. package/dist/app-controller-continuation.d.ts +26 -0
  16. package/dist/app-controller-continuation.d.ts.map +1 -0
  17. package/dist/app-controller-continuation.js +198 -0
  18. package/dist/app-controller-continuation.js.map +1 -0
  19. package/dist/app-controller-lifecycle.d.ts +6 -0
  20. package/dist/app-controller-lifecycle.d.ts.map +1 -0
  21. package/dist/app-controller-lifecycle.js +81 -0
  22. package/dist/app-controller-lifecycle.js.map +1 -0
  23. package/dist/app-controller-maintenance.d.ts +13 -0
  24. package/dist/app-controller-maintenance.d.ts.map +1 -0
  25. package/dist/app-controller-maintenance.js +180 -0
  26. package/dist/app-controller-maintenance.js.map +1 -0
  27. package/dist/app-controller-memory-health.d.ts +12 -0
  28. package/dist/app-controller-memory-health.d.ts.map +1 -0
  29. package/dist/app-controller-memory-health.js +160 -0
  30. package/dist/app-controller-memory-health.js.map +1 -0
  31. package/dist/app-controller-memory.d.ts +9 -0
  32. package/dist/app-controller-memory.d.ts.map +1 -0
  33. package/dist/app-controller-memory.js +83 -0
  34. package/dist/app-controller-memory.js.map +1 -0
  35. package/dist/app-controller-responder.d.ts +20 -0
  36. package/dist/app-controller-responder.d.ts.map +1 -0
  37. package/dist/app-controller-responder.js +246 -0
  38. package/dist/app-controller-responder.js.map +1 -0
  39. package/dist/app-controller-traceability.d.ts +25 -0
  40. package/dist/app-controller-traceability.d.ts.map +1 -0
  41. package/dist/app-controller-traceability.js +418 -0
  42. package/dist/app-controller-traceability.js.map +1 -0
  43. package/dist/app-controller-utils.d.ts +38 -0
  44. package/dist/app-controller-utils.d.ts.map +1 -0
  45. package/dist/app-controller-utils.js +185 -0
  46. package/dist/app-controller-utils.js.map +1 -0
  47. package/dist/app-controller.d.ts +400 -0
  48. package/dist/app-controller.d.ts.map +1 -0
  49. package/dist/app-controller.js +336 -0
  50. package/dist/app-controller.js.map +1 -0
  51. package/dist/app.d.ts +7 -115
  52. package/dist/app.d.ts.map +1 -1
  53. package/dist/app.js +6 -1912
  54. package/dist/app.js.map +1 -1
  55. package/dist/artifact-retention.d.ts +4 -0
  56. package/dist/artifact-retention.d.ts.map +1 -1
  57. package/dist/artifact-retention.js +38 -0
  58. package/dist/artifact-retention.js.map +1 -1
  59. package/dist/backfill.d.ts +3 -1
  60. package/dist/backfill.d.ts.map +1 -1
  61. package/dist/backfill.js +5 -1
  62. package/dist/backfill.js.map +1 -1
  63. package/dist/background-runtime.d.ts +27 -0
  64. package/dist/background-runtime.d.ts.map +1 -1
  65. package/dist/background-runtime.js +383 -205
  66. package/dist/background-runtime.js.map +1 -1
  67. package/dist/background-worker-lease.d.ts.map +1 -1
  68. package/dist/background-worker-lease.js +41 -241
  69. package/dist/background-worker-lease.js.map +1 -1
  70. package/dist/background.d.ts +31 -9
  71. package/dist/background.d.ts.map +1 -1
  72. package/dist/background.js +543 -272
  73. package/dist/background.js.map +1 -1
  74. package/dist/channel-drivers/cron.d.ts +11 -0
  75. package/dist/channel-drivers/cron.d.ts.map +1 -0
  76. package/dist/channel-drivers/cron.js +168 -0
  77. package/dist/channel-drivers/cron.js.map +1 -0
  78. package/dist/channel-drivers/live.d.ts +11 -0
  79. package/dist/channel-drivers/live.d.ts.map +1 -0
  80. package/dist/channel-drivers/live.js +47 -0
  81. package/dist/channel-drivers/live.js.map +1 -0
  82. package/dist/channel-drivers/native-notify.d.ts +27 -0
  83. package/dist/channel-drivers/native-notify.d.ts.map +1 -0
  84. package/dist/channel-drivers/native-notify.js +94 -0
  85. package/dist/channel-drivers/native-notify.js.map +1 -0
  86. package/dist/channel-drivers/openai-api.d.ts +7 -0
  87. package/dist/channel-drivers/openai-api.d.ts.map +1 -0
  88. package/dist/channel-drivers/openai-api.js +63 -0
  89. package/dist/channel-drivers/openai-api.js.map +1 -0
  90. package/dist/channel-drivers/shared.d.ts +4 -0
  91. package/dist/channel-drivers/shared.d.ts.map +1 -0
  92. package/dist/channel-drivers/shared.js +5 -0
  93. package/dist/channel-drivers/shared.js.map +1 -0
  94. package/dist/channel-drivers/slack.d.ts +14 -0
  95. package/dist/channel-drivers/slack.d.ts.map +1 -0
  96. package/dist/channel-drivers/slack.js +227 -0
  97. package/dist/channel-drivers/slack.js.map +1 -0
  98. package/dist/channel-drivers/telegram.d.ts +11 -0
  99. package/dist/channel-drivers/telegram.d.ts.map +1 -0
  100. package/dist/channel-drivers/telegram.js +244 -0
  101. package/dist/channel-drivers/telegram.js.map +1 -0
  102. package/dist/channel-drivers/tui.d.ts +15 -0
  103. package/dist/channel-drivers/tui.d.ts.map +1 -0
  104. package/dist/channel-drivers/tui.js +108 -0
  105. package/dist/channel-drivers/tui.js.map +1 -0
  106. package/dist/channel-drivers/webhook.d.ts +7 -0
  107. package/dist/channel-drivers/webhook.d.ts.map +1 -0
  108. package/dist/channel-drivers/webhook.js +115 -0
  109. package/dist/channel-drivers/webhook.js.map +1 -0
  110. package/dist/channels.d.ts +21 -81
  111. package/dist/channels.d.ts.map +1 -1
  112. package/dist/channels.js +11 -1174
  113. package/dist/channels.js.map +1 -1
  114. package/dist/cli-args.d.ts +121 -0
  115. package/dist/cli-args.d.ts.map +1 -0
  116. package/dist/cli-args.js +532 -0
  117. package/dist/cli-args.js.map +1 -0
  118. package/dist/cli-background-command.d.ts +59 -0
  119. package/dist/cli-background-command.d.ts.map +1 -0
  120. package/dist/cli-background-command.js +475 -0
  121. package/dist/cli-background-command.js.map +1 -0
  122. package/dist/cli-cancellation.d.ts +6 -0
  123. package/dist/cli-cancellation.d.ts.map +1 -0
  124. package/dist/cli-cancellation.js +23 -0
  125. package/dist/cli-cancellation.js.map +1 -0
  126. package/dist/cli-commands.d.ts +12 -0
  127. package/dist/cli-commands.d.ts.map +1 -0
  128. package/dist/cli-commands.js +306 -0
  129. package/dist/cli-commands.js.map +1 -0
  130. package/dist/cli-help.d.ts +11 -0
  131. package/dist/cli-help.d.ts.map +1 -0
  132. package/dist/cli-help.js +262 -0
  133. package/dist/cli-help.js.map +1 -0
  134. package/dist/cli-init-command.d.ts +60 -0
  135. package/dist/cli-init-command.d.ts.map +1 -0
  136. package/dist/cli-init-command.js +1910 -0
  137. package/dist/cli-init-command.js.map +1 -0
  138. package/dist/cli-install-skill-command.d.ts +3 -0
  139. package/dist/cli-install-skill-command.d.ts.map +1 -0
  140. package/dist/cli-install-skill-command.js +51 -0
  141. package/dist/cli-install-skill-command.js.map +1 -0
  142. package/dist/cli-sandbox-command.d.ts +10 -0
  143. package/dist/cli-sandbox-command.d.ts.map +1 -0
  144. package/dist/cli-sandbox-command.js +100 -0
  145. package/dist/cli-sandbox-command.js.map +1 -0
  146. package/dist/cli-validate-config-command.d.ts +36 -0
  147. package/dist/cli-validate-config-command.d.ts.map +1 -0
  148. package/dist/cli-validate-config-command.js +317 -0
  149. package/dist/cli-validate-config-command.js.map +1 -0
  150. package/dist/cli.d.ts +6 -248
  151. package/dist/cli.d.ts.map +1 -1
  152. package/dist/cli.js +6 -3792
  153. package/dist/cli.js.map +1 -1
  154. package/dist/config-reference.d.ts +2 -1
  155. package/dist/config-reference.d.ts.map +1 -1
  156. package/dist/config-reference.js +14 -3
  157. package/dist/config-reference.js.map +1 -1
  158. package/dist/configured-agent.d.ts +23 -0
  159. package/dist/configured-agent.d.ts.map +1 -1
  160. package/dist/configured-agent.js +89 -12
  161. package/dist/configured-agent.js.map +1 -1
  162. package/dist/continuation-dispatch-worker.d.ts +21 -0
  163. package/dist/continuation-dispatch-worker.d.ts.map +1 -0
  164. package/dist/continuation-dispatch-worker.js +425 -0
  165. package/dist/continuation-dispatch-worker.js.map +1 -0
  166. package/dist/continuation-http-routes.d.ts +15 -0
  167. package/dist/continuation-http-routes.d.ts.map +1 -0
  168. package/dist/continuation-http-routes.js +265 -0
  169. package/dist/continuation-http-routes.js.map +1 -0
  170. package/dist/continuation-operator-api.d.ts +18 -0
  171. package/dist/continuation-operator-api.d.ts.map +1 -0
  172. package/dist/continuation-operator-api.js +164 -0
  173. package/dist/continuation-operator-api.js.map +1 -0
  174. package/dist/continuation-origin-store.d.ts +19 -0
  175. package/dist/continuation-origin-store.d.ts.map +1 -0
  176. package/dist/continuation-origin-store.js +273 -0
  177. package/dist/continuation-origin-store.js.map +1 -0
  178. package/dist/continuation-service-errors.d.ts +18 -0
  179. package/dist/continuation-service-errors.d.ts.map +1 -0
  180. package/dist/continuation-service-errors.js +35 -0
  181. package/dist/continuation-service-errors.js.map +1 -0
  182. package/dist/continuation-service-helpers.d.ts +39 -0
  183. package/dist/continuation-service-helpers.d.ts.map +1 -0
  184. package/dist/continuation-service-helpers.js +308 -0
  185. package/dist/continuation-service-helpers.js.map +1 -0
  186. package/dist/continuation-service-origin.d.ts +55 -0
  187. package/dist/continuation-service-origin.d.ts.map +1 -0
  188. package/dist/continuation-service-origin.js +325 -0
  189. package/dist/continuation-service-origin.js.map +1 -0
  190. package/dist/continuation-service-types.d.ts +110 -0
  191. package/dist/continuation-service-types.d.ts.map +1 -0
  192. package/dist/continuation-service-types.js +11 -0
  193. package/dist/continuation-service-types.js.map +1 -0
  194. package/dist/continuation-service.d.ts +4 -97
  195. package/dist/continuation-service.d.ts.map +1 -1
  196. package/dist/continuation-service.js +9 -1525
  197. package/dist/continuation-service.js.map +1 -1
  198. package/dist/continuation-store-fs.d.ts +15 -0
  199. package/dist/continuation-store-fs.d.ts.map +1 -0
  200. package/dist/continuation-store-fs.js +247 -0
  201. package/dist/continuation-store-fs.js.map +1 -0
  202. package/dist/continuation-store-policy.d.ts +21 -0
  203. package/dist/continuation-store-policy.d.ts.map +1 -0
  204. package/dist/continuation-store-policy.js +278 -0
  205. package/dist/continuation-store-policy.js.map +1 -0
  206. package/dist/continuation-store-records.d.ts +9 -0
  207. package/dist/continuation-store-records.d.ts.map +1 -0
  208. package/dist/continuation-store-records.js +221 -0
  209. package/dist/continuation-store-records.js.map +1 -0
  210. package/dist/continuation-store-types.d.ts +173 -0
  211. package/dist/continuation-store-types.d.ts.map +1 -0
  212. package/dist/continuation-store-types.js +26 -0
  213. package/dist/continuation-store-types.js.map +1 -0
  214. package/dist/continuation-store.d.ts +4 -138
  215. package/dist/continuation-store.d.ts.map +1 -1
  216. package/dist/continuation-store.js +145 -1049
  217. package/dist/continuation-store.js.map +1 -1
  218. package/dist/doctor.d.ts +5 -0
  219. package/dist/doctor.d.ts.map +1 -1
  220. package/dist/doctor.js +575 -38
  221. package/dist/doctor.js.map +1 -1
  222. package/dist/init.d.ts +2 -0
  223. package/dist/init.d.ts.map +1 -1
  224. package/dist/init.js +92 -239
  225. package/dist/init.js.map +1 -1
  226. package/dist/interaction-bridge.d.ts +19 -0
  227. package/dist/interaction-bridge.d.ts.map +1 -1
  228. package/dist/interaction-bridge.js +130 -0
  229. package/dist/interaction-bridge.js.map +1 -1
  230. package/dist/launchd-logs.d.ts +91 -0
  231. package/dist/launchd-logs.d.ts.map +1 -0
  232. package/dist/launchd-logs.js +1499 -0
  233. package/dist/launchd-logs.js.map +1 -0
  234. package/dist/launchd.d.ts +27 -0
  235. package/dist/launchd.d.ts.map +1 -1
  236. package/dist/launchd.js +89 -10
  237. package/dist/launchd.js.map +1 -1
  238. package/dist/memory-command.d.ts +2 -0
  239. package/dist/memory-command.d.ts.map +1 -1
  240. package/dist/memory-command.js +161 -48
  241. package/dist/memory-command.js.map +1 -1
  242. package/dist/memory-recall-settings.d.ts +64 -0
  243. package/dist/memory-recall-settings.d.ts.map +1 -0
  244. package/dist/memory-recall-settings.js +51 -0
  245. package/dist/memory-recall-settings.js.map +1 -0
  246. package/dist/memory-recall.d.ts +7 -95
  247. package/dist/memory-recall.d.ts.map +1 -1
  248. package/dist/memory-recall.js +8 -143
  249. package/dist/memory-recall.js.map +1 -1
  250. package/dist/memory-rituals.d.ts +3 -3
  251. package/dist/memory-rituals.d.ts.map +1 -1
  252. package/dist/memory-rituals.js +16 -111
  253. package/dist/memory-rituals.js.map +1 -1
  254. package/dist/modules/catalog.js +2 -2
  255. package/dist/modules/catalog.js.map +1 -1
  256. package/dist/modules/known-tools.d.ts +8 -6
  257. package/dist/modules/known-tools.d.ts.map +1 -1
  258. package/dist/modules/known-tools.js +8 -6
  259. package/dist/modules/known-tools.js.map +1 -1
  260. package/dist/notify-destinations.d.ts +5 -0
  261. package/dist/notify-destinations.d.ts.map +1 -1
  262. package/dist/notify-destinations.js +7 -1
  263. package/dist/notify-destinations.js.map +1 -1
  264. package/dist/owner-private-lock.d.ts +95 -0
  265. package/dist/owner-private-lock.d.ts.map +1 -0
  266. package/dist/owner-private-lock.js +566 -0
  267. package/dist/owner-private-lock.js.map +1 -0
  268. package/dist/pi-oauth-login.d.ts +2 -0
  269. package/dist/pi-oauth-login.d.ts.map +1 -1
  270. package/dist/pi-oauth-login.js +39 -5
  271. package/dist/pi-oauth-login.js.map +1 -1
  272. package/dist/posted-message-index.d.ts +35 -16
  273. package/dist/posted-message-index.d.ts.map +1 -1
  274. package/dist/posted-message-index.js +1068 -52
  275. package/dist/posted-message-index.js.map +1 -1
  276. package/dist/posted-reply-history.d.ts +17 -0
  277. package/dist/posted-reply-history.d.ts.map +1 -0
  278. package/dist/posted-reply-history.js +169 -0
  279. package/dist/posted-reply-history.js.map +1 -0
  280. package/dist/proactive-notify.d.ts.map +1 -1
  281. package/dist/proactive-notify.js +6 -4
  282. package/dist/proactive-notify.js.map +1 -1
  283. package/dist/project-skills.d.ts +4 -0
  284. package/dist/project-skills.d.ts.map +1 -1
  285. package/dist/project-skills.js +99 -130
  286. package/dist/project-skills.js.map +1 -1
  287. package/dist/provider-setup.d.ts +0 -1
  288. package/dist/provider-setup.d.ts.map +1 -1
  289. package/dist/provider-setup.js +0 -3
  290. package/dist/provider-setup.js.map +1 -1
  291. package/dist/readiness-probe-worker.d.ts +61 -1
  292. package/dist/readiness-probe-worker.d.ts.map +1 -1
  293. package/dist/readiness-probe-worker.js +49 -56
  294. package/dist/readiness-probe-worker.js.map +1 -1
  295. package/dist/readiness-probe.d.ts +6 -9
  296. package/dist/readiness-probe.d.ts.map +1 -1
  297. package/dist/readiness-probe.js +23 -56
  298. package/dist/readiness-probe.js.map +1 -1
  299. package/dist/redact-secrets.d.ts +15 -0
  300. package/dist/redact-secrets.d.ts.map +1 -0
  301. package/dist/redact-secrets.js +65 -0
  302. package/dist/redact-secrets.js.map +1 -0
  303. package/dist/run-history.js +34 -24
  304. package/dist/run-history.js.map +1 -1
  305. package/dist/runtime-provenance.d.ts +8 -0
  306. package/dist/runtime-provenance.d.ts.map +1 -0
  307. package/dist/runtime-provenance.js +225 -0
  308. package/dist/runtime-provenance.js.map +1 -0
  309. package/dist/sandbox-manager.d.ts +4 -1
  310. package/dist/sandbox-manager.d.ts.map +1 -1
  311. package/dist/sandbox-manager.js +114 -273
  312. package/dist/sandbox-manager.js.map +1 -1
  313. package/dist/secure-file-replace.d.ts +49 -0
  314. package/dist/secure-file-replace.d.ts.map +1 -0
  315. package/dist/secure-file-replace.js +270 -0
  316. package/dist/secure-file-replace.js.map +1 -0
  317. package/dist/seen-conversations.d.ts +32 -5
  318. package/dist/seen-conversations.d.ts.map +1 -1
  319. package/dist/seen-conversations.js +145 -15
  320. package/dist/seen-conversations.js.map +1 -1
  321. package/dist/sessions.d.ts +20 -0
  322. package/dist/sessions.d.ts.map +1 -1
  323. package/dist/sessions.js +41 -6
  324. package/dist/sessions.js.map +1 -1
  325. package/dist/wizard/index.d.ts +2 -2
  326. package/dist/wizard/index.d.ts.map +1 -1
  327. package/dist/wizard/index.js +1 -1
  328. package/dist/wizard/index.js.map +1 -1
  329. package/dist/wizard/presets.d.ts +2 -1
  330. package/dist/wizard/presets.d.ts.map +1 -1
  331. package/dist/wizard/presets.js +2 -1
  332. package/dist/wizard/presets.js.map +1 -1
  333. package/dist/wizard/run.d.ts +0 -26
  334. package/dist/wizard/run.d.ts.map +1 -1
  335. package/dist/wizard/run.js +0 -53
  336. package/dist/wizard/run.js.map +1 -1
  337. package/package.json +21 -16
  338. package/schema/mono-agent.config.schema.json +17 -6
  339. package/skills/mono-agent-composer/SKILL.md +3 -3
  340. package/skills/mono-agent-composer/references/config-blueprint.md +41 -22
  341. package/skills/mono-agent-composer/references/discovery-questions.md +4 -4
  342. package/skills/mono-agent-composer/references/feature-coverage.md +14 -14
  343. package/skills/mono-agent-composer/references/package-map.md +3 -3
  344. package/skills/mono-agent-composer/references/playbooks.md +38 -20
  345. package/skills/mono-agent-composer/references/validation.md +16 -3
package/dist/doctor.js CHANGED
@@ -6,6 +6,7 @@ import { isIP } from "node:net";
6
6
  import { join } from "node:path";
7
7
  import { promisify } from "node:util";
8
8
  import { getBuiltinModels as getPiBuiltinModels, getBuiltinProviders as getPiBuiltinProviders, } from "@earendil-works/pi-ai/providers/all";
9
+ import { validateCronExpression } from "@mono-agent/cron-adapter";
9
10
  import { listRecordedRuns } from "@mono-agent/observability";
10
11
  import { serializeTraceSpans } from "@mono-agent/observability/otel";
11
12
  import { classifyContinuationMcpServerTransport, isStdioMcpServerSpec, loadToolPolicyFromJsonFile, } from "@mono-agent/agent-harness";
@@ -19,8 +20,13 @@ import { collectChannelConfigViews } from "./channel-config-view.js";
19
20
  import { resolveChannelDrivers } from "./channels.js";
20
21
  import { findUnknownAppConfigWarnings } from "./config-reference.js";
21
22
  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";
23
+ import { applyOriginContextGroupCommit } from "./continuation-origin-store.js";
24
+ import { readBoundedOwnerOnlyFile } from "./continuation-store-fs.js";
25
+ import { loadLegacyStore, mergeMigrationRecords } from "./continuation-store-records.js";
26
+ import { CONTINUATION_RECORD_STORE_SCHEMA_VERSION, } from "./continuation-store.js";
27
+ import { applyRetention, isDurableGeneration, isOriginContextGroupCommit, isRecord, isRecordTransaction, isStoreFile, normalizeLegacyContinuationRecords, requiredDate, resolveRetention, } from "./continuation-store-policy.js";
28
+ import { MAX_LEGACY_STORE_BYTES, MAX_MANIFEST_BYTES, MAX_RECORD_BYTES, MAX_TRANSACTION_BYTES, } from "./continuation-store-types.js";
29
+ import { CONTINUATION_STATES, continuationDigest } from "./continuations.js";
24
30
  import { formatInteractionBridgeUrl, loadInteractionSettings } from "./interaction-bridge.js";
25
31
  import { FIRST_RUN_MEMORY_INITIALIZING_MARKER } from "./first-run-managed-memory.js";
26
32
  import { DEFAULT_MEMORY_EMBEDDING_ENDPOINTS, probeMemoryEmbeddingSelection, } from "./memory-embedding-service.js";
@@ -29,7 +35,9 @@ import { piAuthRecoveryCommand } from "./provider-setup.js";
29
35
  import { inspectPiAuthStore } from "./pi-auth-store-inspection.js";
30
36
  import { checkManagedProjectSkills, managedProjectSkillsExist } from "./project-skills.js";
31
37
  import { configuredRuntimeFallbackModels, configuredRuntimeModels } from "./runtime-routes.js";
38
+ import { runtimeProvenanceDetail } from "./runtime-provenance.js";
32
39
  import { loadSupermemoryPlugin } from "./supermemory-plugin.js";
40
+ import { DEFAULT_LAUNCHD_LOG_POLICY, inspectLaunchdLogs, LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS, launchdLogPathsForConfig, } from "./launchd-logs.js";
33
41
  const execFile = promisify(execFileCallback);
34
42
  const CONTINUATION_V2_ROLLBACK_GUARD = "UPGRADED-TO-RECORDS-V3";
35
43
  const CONTINUATION_V2_ROLLBACK_GUARD_CONTENT = "This state directory uses continuation records v3. Older runtimes must not open records-v2.\n";
@@ -54,6 +62,7 @@ export async function validateMonoAgentFolder(options) {
54
62
  }
55
63
  sections.push({ id: "core", label: "Core config", status: "error", details: [error.message] });
56
64
  }
65
+ sections.push(await runtimeProvenanceSection());
57
66
  if (coreConfig !== undefined) {
58
67
  const staticTriggerCredentialRefs = await collectStaticTriggerCredentialRefs(drivers, options);
59
68
  const jsonResult = await readMonoAgentConfigJson(options.configPath);
@@ -84,6 +93,7 @@ export async function validateMonoAgentFolder(options) {
84
93
  }
85
94
  sections.push(await exporterSection(options, liveness));
86
95
  sections.push(await runsSection(options, coreConfig));
96
+ sections.push(await launchdLogsSection(options.configPath));
87
97
  for (const driver of drivers) {
88
98
  sections.push(await channelSection(driver, options));
89
99
  }
@@ -102,6 +112,62 @@ export async function validateMonoAgentFolder(options) {
102
112
  ok: sections.every((section) => section.status !== "error"),
103
113
  };
104
114
  }
115
+ /** Read-only launchd log inventory used by both `validate` and its `doctor` alias. */
116
+ export async function launchdLogsSection(configPath) {
117
+ let inspection;
118
+ try {
119
+ const paths = await launchdLogPathsForConfig(configPath);
120
+ inspection = await inspectLaunchdLogs(paths);
121
+ }
122
+ catch {
123
+ return {
124
+ id: "launchd-logs",
125
+ label: "Launchd logs",
126
+ status: "waiting",
127
+ details: ["[WARN] Managed launchd log metadata could not be inspected safely."],
128
+ };
129
+ }
130
+ return launchdLogsSectionFromInspection(inspection);
131
+ }
132
+ /** Pure renderer kept separate so exact byte accounting is deterministic in tests. */
133
+ export function launchdLogsSectionFromInspection(inspection) {
134
+ const policy = DEFAULT_LAUNCHD_LOG_POLICY;
135
+ const details = [
136
+ `Policy: ${policy.maxBytes} bytes per file, ${policy.rotationCount} retained generations, checked every ${LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS} seconds.`,
137
+ streamSizeDetail("stdout", inspection.stdout),
138
+ streamSizeDetail("stderr", inspection.stderr),
139
+ ...inspection.issues.map((issue) => `[WARN] ${issue}.`),
140
+ ...oversizedLogDetails("stdout", inspection.stdout, policy.maxBytes),
141
+ ...oversizedLogDetails("stderr", inspection.stderr, policy.maxBytes),
142
+ ];
143
+ if (!inspection.present && inspection.issues.length === 0) {
144
+ return {
145
+ id: "launchd-logs",
146
+ label: "Launchd logs",
147
+ status: "disabled",
148
+ details: [...details, "No managed launchd log files exist yet."],
149
+ };
150
+ }
151
+ return {
152
+ id: "launchd-logs",
153
+ label: "Launchd logs",
154
+ status: inspection.canMaintain && !inspection.needsMaintenance ? "ok" : "waiting",
155
+ details,
156
+ };
157
+ }
158
+ function streamSizeDetail(label, stream) {
159
+ if (!stream.byteAccountingComplete) {
160
+ return `${label}: byte inventory unavailable because one or more paths could not be inspected safely.`;
161
+ }
162
+ return `${label}: active=${stream.activeBytes} bytes, retained=${stream.retainedBytes} bytes, total=${stream.totalBytes} bytes.`;
163
+ }
164
+ function oversizedLogDetails(label, stream, maxBytes) {
165
+ if (!stream.byteAccountingComplete)
166
+ return [];
167
+ return stream.files.flatMap((file) => file.bytes <= maxBytes
168
+ ? []
169
+ : [`[WARN] ${label}${file.generation === 0 ? "" : `.${file.generation}`} is ${file.bytes} bytes; maintenance limit is ${maxBytes} bytes.`]);
170
+ }
105
171
  /** Enabled, statically configured webhook/cron entries that can actually execute. */
106
172
  function staticTriggerConfigEntries(driverId, loaded) {
107
173
  if (!isUnknownRecord(loaded))
@@ -392,6 +458,14 @@ function applyToolChannelCrossChecks(sections, allowedTools, disallowedTools) {
392
458
  }
393
459
  sections[toolsIndex] = { ...current, status, details: [...current.details, ...extraDetails] };
394
460
  }
461
+ async function runtimeProvenanceSection() {
462
+ return {
463
+ id: "runtime-provenance",
464
+ label: "Runtime provenance",
465
+ status: "ok",
466
+ details: [await runtimeProvenanceDetail()],
467
+ };
468
+ }
395
469
  function runtimeSection(config) {
396
470
  const details = [];
397
471
  let status = "ok";
@@ -1045,6 +1119,19 @@ async function contextSection(config, cwd) {
1045
1119
  return { id: "context", label: "Context & skills", status, details };
1046
1120
  }
1047
1121
  const DEFAULT_CONSOLIDATION_CRON = "0 */2 * * *";
1122
+ function memoryConsolidationCronIssue(expression) {
1123
+ const result = validateCronExpression(expression, { timezone: "UTC" });
1124
+ if (result.ok) {
1125
+ return undefined;
1126
+ }
1127
+ if (result.code === "required") {
1128
+ return "memory.consolidation.cron is required when consolidation is enabled.";
1129
+ }
1130
+ if (result.code === "field_count") {
1131
+ return `memory.consolidation.cron must use exactly five fields; received ${result.fieldCount}.`;
1132
+ }
1133
+ return `memory.consolidation.cron is invalid: ${result.reason}`;
1134
+ }
1048
1135
  async function memorySection(config, cwd, env, liveness, allowFilesystemWrites, preferAppPluginInstall) {
1049
1136
  if (config.memory === undefined) {
1050
1137
  return { id: "memory", label: "Memory", status: "disabled", details: ["No memory configured."] };
@@ -1139,6 +1226,11 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
1139
1226
  else {
1140
1227
  details.push("Consolidation: disabled.");
1141
1228
  }
1229
+ const cronIssue = memoryConsolidationCronIssue(consolidationCron);
1230
+ if (cronIssue !== undefined) {
1231
+ status = "error";
1232
+ details.push(`[ERROR] ${cronIssue}`);
1233
+ }
1142
1234
  }
1143
1235
  const managedIdentity = await managedMemoryIdentityStatus(config.memory);
1144
1236
  if (managedIdentity !== undefined) {
@@ -1715,7 +1807,7 @@ async function continuationSection(config, input) {
1715
1807
  details.push(routeNames.length === 0 ? "Named detached routes: none." : `Named detached routes: ${routeNames.join(", ")}.`);
1716
1808
  const detachedNames = Object.keys(settings.detachedServices);
1717
1809
  details.push(detachedNames.length === 0 ? "Detached services: none." : `Detached services: ${detachedNames.join(", ")}.`);
1718
- const state = await inspectContinuationState(settings.stateDir);
1810
+ const state = await inspectContinuationState(settings.stateDir, settings.retention);
1719
1811
  details.push(...state.details);
1720
1812
  return {
1721
1813
  id: "continuations",
@@ -1724,8 +1816,9 @@ async function continuationSection(config, input) {
1724
1816
  details,
1725
1817
  };
1726
1818
  }
1727
- async function inspectContinuationState(stateDir) {
1819
+ async function inspectContinuationState(stateDir, retention) {
1728
1820
  const details = [];
1821
+ const retentionPolicy = resolveRetention(retention);
1729
1822
  let directory;
1730
1823
  try {
1731
1824
  directory = await lstat(stateDir);
@@ -1769,7 +1862,7 @@ async function inspectContinuationState(stateDir) {
1769
1862
  return { status: "error", details: [manifestSecurityError] };
1770
1863
  let manifest;
1771
1864
  try {
1772
- manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1865
+ manifest = JSON.parse(await readBoundedOwnerOnlyFile(manifestPath, MAX_MANIFEST_BYTES, "Continuation v3 manifest"));
1773
1866
  }
1774
1867
  catch (error) {
1775
1868
  return { status: "error", details: [`Continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
@@ -1777,21 +1870,116 @@ async function inspectContinuationState(stateDir) {
1777
1870
  if (!isContinuationStoreManifest(manifest, CONTINUATION_RECORD_STORE_SCHEMA_VERSION, "per-record-v3")) {
1778
1871
  return { status: "error", details: ["Continuation store manifest has an unsupported or malformed schema."] };
1779
1872
  }
1780
- const recordsDirectoryError = await inspectContinuationRecordsDirectory(join(stateDir, "records-v3"));
1873
+ const recordsDirectoryPath = join(stateDir, "records-v3");
1874
+ const recordsDirectoryError = await inspectContinuationRecordsDirectory(recordsDirectoryPath);
1781
1875
  if (recordsDirectoryError !== undefined)
1782
1876
  return { status: "error", details: [recordsDirectoryError] };
1783
- const transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v3.json"));
1877
+ let recordsDirectoryHasEntries;
1878
+ try {
1879
+ recordsDirectoryHasEntries = (await readdir(recordsDirectoryPath))
1880
+ .some((entry) => !(entry.startsWith(".") && entry.endsWith(".tmp")));
1881
+ }
1882
+ catch (error) {
1883
+ return {
1884
+ status: "error",
1885
+ details: [`Continuation record directory cannot be inspected: ${continuationReason(error)}`],
1886
+ };
1887
+ }
1888
+ const transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v3.json"), CONTINUATION_RECORD_STORE_SCHEMA_VERSION);
1784
1889
  if (!transaction.valid)
1785
1890
  return { status: "error", details: [transaction.detail] };
1786
- const rollbackGuard = await inspectContinuationRollbackGuard(join(stateDir, "records-v2", CONTINUATION_V2_ROLLBACK_GUARD), true);
1891
+ const legacyRecordsDirectoryPath = join(stateDir, "records-v2");
1892
+ let legacyRecordsDirectoryExists = false;
1893
+ try {
1894
+ const legacyRecordsDirectoryInfo = await lstat(legacyRecordsDirectoryPath);
1895
+ legacyRecordsDirectoryExists = true;
1896
+ if (!legacyRecordsDirectoryInfo.isDirectory() || legacyRecordsDirectoryInfo.isSymbolicLink()) {
1897
+ return { status: "error", details: ["Legacy continuation record directory must be a real directory."] };
1898
+ }
1899
+ const legacyRecordsDirectoryError = await inspectContinuationRecordsDirectory(legacyRecordsDirectoryPath, { allowV2RollbackGuard: true });
1900
+ if (legacyRecordsDirectoryError !== undefined) {
1901
+ return { status: "error", details: [legacyRecordsDirectoryError] };
1902
+ }
1903
+ }
1904
+ catch (error) {
1905
+ if (continuationFsCode(error) !== "ENOENT") {
1906
+ return {
1907
+ status: "error",
1908
+ details: [`Legacy continuation record directory cannot be inspected: ${continuationReason(error)}`],
1909
+ };
1910
+ }
1911
+ }
1912
+ let legacyStateExists = legacyRecordsDirectoryExists;
1913
+ const legacyV1Path = join(stateDir, "continuations-v1.json");
1914
+ const legacyV1 = await inspectContinuationEvidenceFile(legacyV1Path, "v1 store");
1915
+ if (!legacyV1.valid)
1916
+ return { status: "error", details: [legacyV1.detail] };
1917
+ legacyStateExists ||= legacyV1.exists;
1918
+ const legacyV2Manifest = await inspectContinuationEvidenceFile(join(stateDir, "continuation-store-v2.json"), "v2 manifest");
1919
+ if (!legacyV2Manifest.valid)
1920
+ return { status: "error", details: [legacyV2Manifest.detail] };
1921
+ legacyStateExists ||= legacyV2Manifest.exists;
1922
+ const legacyTransaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"), 2);
1923
+ if (!legacyTransaction.valid)
1924
+ return { status: "error", details: [legacyTransaction.detail] };
1925
+ legacyStateExists ||= legacyTransaction.pending;
1926
+ const manifestRollbackGuardRequired = manifest.rollbackGuardRequired ?? true;
1927
+ const legacyMigrationPending = legacyStateExists && !manifestRollbackGuardRequired;
1928
+ let recoverableRecords;
1929
+ try {
1930
+ recoverableRecords = await loadContinuationRecordsForRecoveryInspection(recordsDirectoryPath);
1931
+ if (transaction.transaction !== undefined) {
1932
+ applyContinuationTransactionForInspection(recoverableRecords, transaction.transaction);
1933
+ }
1934
+ normalizeLegacyContinuationRecords(recoverableRecords);
1935
+ if (legacyMigrationPending) {
1936
+ const legacy = legacyRecordsDirectoryExists
1937
+ ? await loadContinuationRecordsForRecoveryInspection(legacyRecordsDirectoryPath)
1938
+ : new Map();
1939
+ if (legacyTransaction.transaction !== undefined) {
1940
+ applyContinuationTransactionForInspection(legacy, legacyTransaction.transaction);
1941
+ }
1942
+ if (legacyV1.exists) {
1943
+ mergeMigrationRecords(legacy, await loadLegacyStore(legacyV1Path), "recoverable v1 and v2");
1944
+ }
1945
+ mergeMigrationRecords(recoverableRecords, legacy, "recoverable v2 and v3");
1946
+ }
1947
+ }
1948
+ catch (error) {
1949
+ return {
1950
+ status: "error",
1951
+ details: [`Continuation recoverable records are malformed or conflicting: ${continuationReason(error)}`],
1952
+ };
1953
+ }
1954
+ const originGroups = await inspectContinuationEvidenceDirectory(join(stateDir, "origin-context-groups-v1"), "origin-context activation directory", async () => recoverableRecords);
1955
+ if (!originGroups.valid)
1956
+ return { status: "error", details: [originGroups.detail] };
1957
+ try {
1958
+ applyRetention(recoverableRecords, retentionPolicy, new Date());
1959
+ }
1960
+ catch (error) {
1961
+ return {
1962
+ status: "error",
1963
+ details: [`Continuation retention projection cannot be recovered safely: ${continuationReason(error)}`],
1964
+ };
1965
+ }
1966
+ const rollbackGuardRequired = manifestRollbackGuardRequired
1967
+ || recordsDirectoryHasEntries
1968
+ || transaction.pending
1969
+ || originGroups.hasEntries;
1970
+ const rollbackGuard = await inspectContinuationRollbackGuard(join(legacyRecordsDirectoryPath, CONTINUATION_V2_ROLLBACK_GUARD), rollbackGuardRequired, legacyMigrationPending
1971
+ ? "Legacy continuation state is awaiting v3 migration; the current runtime will fence it before v3 materialization."
1972
+ : "The empty v3 store has no v2 rollback guard; the current runtime will install one before its first v3 record becomes durable.");
1787
1973
  if (!rollbackGuard.valid)
1788
1974
  return { status: "error", details: [rollbackGuard.detail] };
1789
1975
  const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
1790
1976
  if (!owner.valid)
1791
1977
  return { status: "error", details: [owner.detail] };
1792
- details.push(`Store v3: ${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, rollbackGuard.detail, owner.detail);
1978
+ details.push(`Store v3: ${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.compacted)} compacted; ${String(manifest.stats.capturedText)} captured answers.`, `Retention: at most ${String(manifest.stats.limits.terminalMaxRecords)} terminal tombstones with a maximum age of ${String(manifest.stats.limits.terminalMaxAgeMs)} ms and ${String(manifest.stats.limits.capturedTextMaxRecords)} captured answers with a maximum age of ${String(manifest.stats.limits.capturedTextMaxAgeMs)} ms.`, transaction.detail, rollbackGuard.detail, originGroups.detail, ...(legacyMigrationPending ? ["Legacy continuation state is awaiting idempotent v3 migration."] : []), owner.detail);
1793
1979
  return {
1794
1980
  status: transaction.pending
1981
+ || originGroups.hasEntries
1982
+ || legacyMigrationPending
1795
1983
  || manifest.stats.unresolvedDelivery > 0
1796
1984
  || manifest.stats.deadLettered > 0
1797
1985
  || manifest.stats.historyDegraded > 0
@@ -1800,6 +1988,19 @@ async function inspectContinuationState(stateDir) {
1800
1988
  details,
1801
1989
  };
1802
1990
  }
1991
+ // Without a v3 manifest, derive one complete read-only recovery projection
1992
+ // before choosing a status. A crash may leave a WAL, already-applied records
1993
+ // after WAL removal, legacy inputs, or an activation marker in any
1994
+ // combination; reporting one item as waiting must not mask another item that
1995
+ // runtime recovery would reject.
1996
+ const unmanifestedV3Transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v3.json"), CONTINUATION_RECORD_STORE_SCHEMA_VERSION);
1997
+ if (!unmanifestedV3Transaction.valid) {
1998
+ return { status: "error", details: [unmanifestedV3Transaction.detail] };
1999
+ }
2000
+ const unmanifestedV2Transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"), 2);
2001
+ if (!unmanifestedV2Transaction.valid) {
2002
+ return { status: "error", details: [unmanifestedV2Transaction.detail] };
2003
+ }
1803
2004
  const legacyManifestPath = join(stateDir, "continuation-store-v2.json");
1804
2005
  let legacyManifestInfo;
1805
2006
  try {
@@ -1810,7 +2011,10 @@ async function inspectContinuationState(stateDir) {
1810
2011
  return { status: "error", details: [`Legacy continuation store manifest cannot be inspected: ${continuationReason(error)}`] };
1811
2012
  }
1812
2013
  }
2014
+ let legacyManifestExists = false;
2015
+ let legacyManifestDetails;
1813
2016
  if (legacyManifestInfo !== undefined) {
2017
+ legacyManifestExists = true;
1814
2018
  if (!legacyManifestInfo.isFile() || legacyManifestInfo.isSymbolicLink()) {
1815
2019
  return { status: "error", details: ["Legacy continuation store manifest must be a regular file, not a symlink."] };
1816
2020
  }
@@ -1819,7 +2023,7 @@ async function inspectContinuationState(stateDir) {
1819
2023
  return { status: "error", details: [manifestSecurityError] };
1820
2024
  let legacyManifest;
1821
2025
  try {
1822
- legacyManifest = JSON.parse(await readFile(legacyManifestPath, "utf8"));
2026
+ legacyManifest = JSON.parse(await readBoundedOwnerOnlyFile(legacyManifestPath, MAX_MANIFEST_BYTES, "Continuation v2 manifest"));
1823
2027
  }
1824
2028
  catch (error) {
1825
2029
  return { status: "error", details: [`Legacy continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
@@ -1827,22 +2031,71 @@ async function inspectContinuationState(stateDir) {
1827
2031
  if (!isContinuationStoreManifest(legacyManifest, 2, "per-record-v2")) {
1828
2032
  return { status: "error", details: ["Legacy continuation store manifest has an unsupported or malformed schema."] };
1829
2033
  }
1830
- const legacyRecordsDirectory = join(stateDir, "records-v2");
1831
- const recordsDirectoryError = await inspectContinuationRecordsDirectory(legacyRecordsDirectory, {
2034
+ legacyManifestDetails = [
2035
+ `Legacy store v2 awaiting v3 migration: ${String(legacyManifest.stats.records)} retained; ${String(legacyManifest.stats.active)} active; ${String(legacyManifest.stats.unresolvedDelivery)} delivery unknown; ${String(legacyManifest.stats.deadLettered)} dead-lettered; ${String(legacyManifest.stats.historyDegraded)} history-degraded deliveries; ${String(legacyManifest.stats.terminalTombstones)} terminal tombstones; ${String(legacyManifest.stats.compacted)} compacted; ${String(legacyManifest.stats.capturedText)} captured answers.`,
2036
+ `Retention: at most ${String(legacyManifest.stats.limits.terminalMaxRecords)} terminal tombstones with a maximum age of ${String(legacyManifest.stats.limits.terminalMaxAgeMs)} ms and ${String(legacyManifest.stats.limits.capturedTextMaxRecords)} captured answers with a maximum age of ${String(legacyManifest.stats.limits.capturedTextMaxAgeMs)} ms.`,
2037
+ ];
2038
+ }
2039
+ const legacyV1Path = join(stateDir, "continuations-v1.json");
2040
+ const legacyV1 = await inspectContinuationEvidenceFile(legacyV1Path, "v1 store");
2041
+ if (!legacyV1.valid)
2042
+ return { status: "error", details: [legacyV1.detail] };
2043
+ let v3Directory;
2044
+ let v2Directory;
2045
+ let projectedRecords;
2046
+ let hasCommittedV3Records = false;
2047
+ try {
2048
+ v3Directory = await loadOptionalContinuationRecordsForRecoveryInspection(join(stateDir, "records-v3"));
2049
+ hasCommittedV3Records = v3Directory.records.size > 0;
2050
+ v2Directory = await loadOptionalContinuationRecordsForRecoveryInspection(join(stateDir, "records-v2"), {
1832
2051
  allowV2RollbackGuard: true,
1833
2052
  });
1834
- if (recordsDirectoryError !== undefined)
1835
- return { status: "error", details: [recordsDirectoryError] };
1836
- const transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"));
1837
- if (!transaction.valid)
1838
- return { status: "error", details: [transaction.detail] };
1839
- const rollbackGuard = await inspectContinuationRollbackGuard(join(legacyRecordsDirectory, CONTINUATION_V2_ROLLBACK_GUARD), false);
1840
- if (!rollbackGuard.valid)
1841
- return { status: "error", details: [rollbackGuard.detail] };
2053
+ const legacyRecords = v2Directory.records;
2054
+ if (unmanifestedV2Transaction.transaction !== undefined) {
2055
+ applyContinuationTransactionForInspection(legacyRecords, unmanifestedV2Transaction.transaction);
2056
+ }
2057
+ if (legacyV1.exists) {
2058
+ mergeMigrationRecords(legacyRecords, await loadLegacyStore(legacyV1Path), "recoverable v1 and v2");
2059
+ }
2060
+ projectedRecords = v3Directory.records;
2061
+ if (unmanifestedV3Transaction.transaction !== undefined) {
2062
+ applyContinuationTransactionForInspection(projectedRecords, unmanifestedV3Transaction.transaction);
2063
+ }
2064
+ normalizeLegacyContinuationRecords(projectedRecords);
2065
+ mergeMigrationRecords(projectedRecords, legacyRecords, "recoverable v2 and v3");
2066
+ }
2067
+ catch (error) {
2068
+ return {
2069
+ status: "error",
2070
+ details: [`Continuation recovery evidence is malformed or conflicting: ${continuationReason(error)}`],
2071
+ };
2072
+ }
2073
+ const originGroups = await inspectContinuationEvidenceDirectory(join(stateDir, "origin-context-groups-v1"), "origin-context activation directory", async () => projectedRecords);
2074
+ if (!originGroups.valid)
2075
+ return { status: "error", details: [originGroups.detail] };
2076
+ try {
2077
+ applyRetention(projectedRecords, retentionPolicy, new Date());
2078
+ }
2079
+ catch (error) {
2080
+ return {
2081
+ status: "error",
2082
+ details: [`Continuation retention projection cannot be recovered safely: ${continuationReason(error)}`],
2083
+ };
2084
+ }
2085
+ const rollbackGuard = await inspectContinuationRollbackGuard(join(stateDir, "records-v2", CONTINUATION_V2_ROLLBACK_GUARD), false, "The v2 rollback guard is not installed; the current runtime will install it during v3 migration before publishing v3 state.");
2086
+ if (!rollbackGuard.valid)
2087
+ return { status: "error", details: [rollbackGuard.detail] };
2088
+ const recoverableEvidenceExists = legacyManifestExists
2089
+ || v2Directory.exists
2090
+ || hasCommittedV3Records
2091
+ || unmanifestedV2Transaction.pending
2092
+ || unmanifestedV3Transaction.pending
2093
+ || originGroups.hasEntries;
2094
+ if (recoverableEvidenceExists) {
1842
2095
  const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
1843
2096
  if (!owner.valid)
1844
2097
  return { status: "error", details: [owner.detail] };
1845
- details.push(`Legacy store v2 awaiting v3 migration: ${String(legacyManifest.stats.records)} retained; ${String(legacyManifest.stats.active)} active; ${String(legacyManifest.stats.unresolvedDelivery)} delivery unknown; ${String(legacyManifest.stats.deadLettered)} dead-lettered; ${String(legacyManifest.stats.historyDegraded)} history-degraded deliveries; ${String(legacyManifest.stats.terminalTombstones)} terminal tombstones; ${String(legacyManifest.stats.capturedText)} captured answers.`, `Retention: at most ${String(legacyManifest.stats.limits.terminalMaxRecords)} terminal tombstones and ${String(legacyManifest.stats.limits.capturedTextMaxRecords)} captured answers.`, transaction.detail, rollbackGuard.detail, owner.detail);
2098
+ details.push(...(legacyManifestDetails ?? ["Continuation records are awaiting completion of the v3 manifest."]), ...(unmanifestedV2Transaction.pending ? [unmanifestedV2Transaction.detail] : []), ...(unmanifestedV3Transaction.pending ? [unmanifestedV3Transaction.detail] : []), rollbackGuard.detail, originGroups.detail, owner.detail);
1846
2099
  return { status: "waiting", details };
1847
2100
  }
1848
2101
  const storePath = join(stateDir, "continuations-v1.json");
@@ -1868,7 +2121,7 @@ async function inspectContinuationState(stateDir) {
1868
2121
  }
1869
2122
  let parsed;
1870
2123
  try {
1871
- parsed = JSON.parse(await readFile(storePath, "utf8"));
2124
+ parsed = JSON.parse(await readBoundedOwnerOnlyFile(storePath, MAX_LEGACY_STORE_BYTES, "Continuation legacy store"));
1872
2125
  }
1873
2126
  catch (error) {
1874
2127
  return { status: "error", details: [`Continuation ledger contains invalid JSON: ${continuationReason(error)}`] };
@@ -1970,6 +2223,14 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
1970
2223
  const securityError = continuationOwnershipError(recordInfo, "record", 0o600);
1971
2224
  if (securityError !== undefined)
1972
2225
  return securityError;
2226
+ if (entry.name.startsWith(".") && entry.name.endsWith(".tmp")) {
2227
+ try {
2228
+ await readBoundedOwnerOnlyFile(join(path, entry.name), MAX_RECORD_BYTES, "Continuation temporary record");
2229
+ }
2230
+ catch (error) {
2231
+ return `Continuation temporary record is unsafe: ${continuationReason(error)}`;
2232
+ }
2233
+ }
1973
2234
  }
1974
2235
  }
1975
2236
  catch (error) {
@@ -1977,7 +2238,247 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
1977
2238
  }
1978
2239
  return undefined;
1979
2240
  }
1980
- async function inspectContinuationRollbackGuard(path, required) {
2241
+ async function inspectContinuationEvidenceDirectory(path, label, loadRecoverableRecords) {
2242
+ let info;
2243
+ try {
2244
+ info = await lstat(path);
2245
+ }
2246
+ catch (error) {
2247
+ return continuationFsCode(error) === "ENOENT"
2248
+ ? { valid: true, hasEntries: false, hasTemporaryDebris: false, detail: `${label} has not been created.` }
2249
+ : {
2250
+ valid: false,
2251
+ hasEntries: false,
2252
+ hasTemporaryDebris: false,
2253
+ detail: `${label} cannot be inspected: ${continuationReason(error)}`,
2254
+ };
2255
+ }
2256
+ if (!info.isDirectory() || info.isSymbolicLink()) {
2257
+ return { valid: false, hasEntries: false, hasTemporaryDebris: false, detail: `${label} must be a real directory.` };
2258
+ }
2259
+ const securityError = continuationOwnershipError(info, label, 0o700);
2260
+ if (securityError !== undefined) {
2261
+ return { valid: false, hasEntries: false, hasTemporaryDebris: false, detail: securityError };
2262
+ }
2263
+ try {
2264
+ const entries = await readdir(path, { withFileTypes: true });
2265
+ const temporaryEntries = entries.filter((entry) => entry.name.startsWith(".") && entry.name.endsWith(".tmp"));
2266
+ let projectedRecords;
2267
+ const recordsForRecovery = async () => {
2268
+ if (projectedRecords !== undefined)
2269
+ return projectedRecords;
2270
+ projectedRecords = await loadRecoverableRecords();
2271
+ return projectedRecords;
2272
+ };
2273
+ for (const entry of entries) {
2274
+ const entryPath = join(path, entry.name);
2275
+ const temporary = entry.name.startsWith(".") && entry.name.endsWith(".tmp");
2276
+ if (temporary) {
2277
+ if (!entry.isFile() || entry.isSymbolicLink()) {
2278
+ return {
2279
+ valid: false,
2280
+ hasEntries: true,
2281
+ hasTemporaryDebris: true,
2282
+ detail: `${label} temporary entry must be a regular file: ${entry.name}.`,
2283
+ };
2284
+ }
2285
+ try {
2286
+ await readBoundedOwnerOnlyFile(entryPath, 64 * 1024, "Continuation origin-context group temporary");
2287
+ }
2288
+ catch (error) {
2289
+ return {
2290
+ valid: false,
2291
+ hasEntries: false,
2292
+ hasTemporaryDebris: true,
2293
+ detail: `${label} temporary entry is unsafe: ${continuationReason(error)}`,
2294
+ };
2295
+ }
2296
+ continue;
2297
+ }
2298
+ if (!/^[a-f0-9]{64}\.json$/u.test(entry.name) || !entry.isFile() || entry.isSymbolicLink()) {
2299
+ return {
2300
+ valid: false,
2301
+ hasEntries: true,
2302
+ hasTemporaryDebris: temporaryEntries.length > 0,
2303
+ detail: `${label} contains an unexpected entry: ${entry.name}.`,
2304
+ };
2305
+ }
2306
+ const entryInfo = await lstat(entryPath);
2307
+ if (!entryInfo.isFile() || entryInfo.isSymbolicLink() || entryInfo.nlink !== 1) {
2308
+ return {
2309
+ valid: false,
2310
+ hasEntries: true,
2311
+ hasTemporaryDebris: temporaryEntries.length > 0,
2312
+ detail: `${label} marker must be a single-link regular file: ${entry.name}.`,
2313
+ };
2314
+ }
2315
+ const entrySecurityError = continuationOwnershipError(entryInfo, `${label} marker`, 0o600);
2316
+ if (entrySecurityError !== undefined) {
2317
+ return {
2318
+ valid: false,
2319
+ hasEntries: true,
2320
+ hasTemporaryDebris: temporaryEntries.length > 0,
2321
+ detail: entrySecurityError,
2322
+ };
2323
+ }
2324
+ if (entryInfo.size > 64 * 1024) {
2325
+ return {
2326
+ valid: false,
2327
+ hasEntries: true,
2328
+ hasTemporaryDebris: temporaryEntries.length > 0,
2329
+ detail: `${label} marker exceeds its safety limit: ${entry.name}.`,
2330
+ };
2331
+ }
2332
+ let markerBody;
2333
+ try {
2334
+ markerBody = await readBoundedOwnerOnlyFile(entryPath, 64 * 1024, "Continuation origin-context group commit");
2335
+ }
2336
+ catch (error) {
2337
+ return {
2338
+ valid: false,
2339
+ hasEntries: true,
2340
+ hasTemporaryDebris: temporaryEntries.length > 0,
2341
+ detail: `${label} marker is unsafe to read: ${continuationReason(error)}`,
2342
+ };
2343
+ }
2344
+ let marker;
2345
+ try {
2346
+ marker = JSON.parse(markerBody);
2347
+ }
2348
+ catch (error) {
2349
+ return {
2350
+ valid: false,
2351
+ hasEntries: true,
2352
+ hasTemporaryDebris: temporaryEntries.length > 0,
2353
+ detail: `${label} marker contains invalid JSON: ${continuationReason(error)}`,
2354
+ };
2355
+ }
2356
+ if (!isOriginContextGroupCommit(marker) || `${marker.groupKey}.json` !== entry.name) {
2357
+ return {
2358
+ valid: false,
2359
+ hasEntries: true,
2360
+ hasTemporaryDebris: temporaryEntries.length > 0,
2361
+ detail: `${label} marker has a malformed schema or filename: ${entry.name}.`,
2362
+ };
2363
+ }
2364
+ try {
2365
+ applyOriginContextGroupCommit(await recordsForRecovery(), marker);
2366
+ }
2367
+ catch (error) {
2368
+ return {
2369
+ valid: false,
2370
+ hasEntries: true,
2371
+ hasTemporaryDebris: temporaryEntries.length > 0,
2372
+ detail: `${label} marker does not match the recoverable durable records: ${continuationReason(error)}`,
2373
+ };
2374
+ }
2375
+ }
2376
+ const markerCount = entries.length - temporaryEntries.length;
2377
+ const hasEntries = markerCount > 0;
2378
+ const detail = markerCount > 0
2379
+ ? `${label} contains ${markerCount === 1 ? "a durable marker" : `${String(markerCount)} durable markers`} awaiting idempotent recovery${temporaryEntries.length > 0 ? ", plus incomplete temporary debris awaiting cleanup" : ""}.`
2380
+ : temporaryEntries.length > 0
2381
+ ? `${label} contains only incomplete temporary debris awaiting cleanup.`
2382
+ : `${label} is owner-only and empty.`;
2383
+ return {
2384
+ valid: true,
2385
+ hasEntries,
2386
+ hasTemporaryDebris: temporaryEntries.length > 0,
2387
+ detail,
2388
+ };
2389
+ }
2390
+ catch (error) {
2391
+ return {
2392
+ valid: false,
2393
+ hasEntries: false,
2394
+ hasTemporaryDebris: false,
2395
+ detail: `${label} cannot be inspected: ${continuationReason(error)}`,
2396
+ };
2397
+ }
2398
+ }
2399
+ async function loadContinuationRecordsForRecoveryInspection(path) {
2400
+ const records = new Map();
2401
+ for (const entry of await readdir(path, { withFileTypes: true })) {
2402
+ if (entry.name.startsWith(".") && entry.name.endsWith(".tmp"))
2403
+ continue;
2404
+ if (!entry.name.endsWith(".json"))
2405
+ continue;
2406
+ const entryPath = join(path, entry.name);
2407
+ let value;
2408
+ try {
2409
+ value = JSON.parse(await readBoundedOwnerOnlyFile(entryPath, MAX_RECORD_BYTES, "Continuation record"));
2410
+ }
2411
+ catch (error) {
2412
+ throw new Error(`Continuation record cannot be validated for activation recovery: ${entry.name}`, {
2413
+ cause: error,
2414
+ });
2415
+ }
2416
+ if (!isDoctorObject(value)
2417
+ || typeof value.continuationId !== "string"
2418
+ || !isRecord(value, value.continuationId)
2419
+ || `${continuationDigest(value.continuationId)}.json` !== entry.name
2420
+ || records.has(value.continuationId)) {
2421
+ throw new Error(`Continuation record has a malformed schema, duplicate id, or mismatched filename: ${entry.name}`);
2422
+ }
2423
+ records.set(value.continuationId, structuredClone(value));
2424
+ }
2425
+ return records;
2426
+ }
2427
+ async function loadOptionalContinuationRecordsForRecoveryInspection(path, options = {}) {
2428
+ try {
2429
+ const info = await lstat(path);
2430
+ if (!info.isDirectory() || info.isSymbolicLink()) {
2431
+ throw new Error("Continuation record directory must be a real directory, not a file or symlink.");
2432
+ }
2433
+ }
2434
+ catch (error) {
2435
+ if (continuationFsCode(error) === "ENOENT") {
2436
+ return { exists: false, records: new Map() };
2437
+ }
2438
+ throw error;
2439
+ }
2440
+ const inspectionError = await inspectContinuationRecordsDirectory(path, options);
2441
+ if (inspectionError !== undefined)
2442
+ throw new Error(inspectionError);
2443
+ return {
2444
+ exists: true,
2445
+ records: await loadContinuationRecordsForRecoveryInspection(path),
2446
+ };
2447
+ }
2448
+ function applyContinuationTransactionForInspection(records, transaction) {
2449
+ for (const record of transaction.writes) {
2450
+ records.set(record.continuationId, structuredClone(record));
2451
+ }
2452
+ for (const id of transaction.deletes)
2453
+ records.delete(id);
2454
+ }
2455
+ async function inspectContinuationEvidenceFile(path, label) {
2456
+ let info;
2457
+ try {
2458
+ info = await lstat(path);
2459
+ }
2460
+ catch (error) {
2461
+ return continuationFsCode(error) === "ENOENT"
2462
+ ? { valid: true, exists: false, detail: `Legacy continuation ${label} is absent.` }
2463
+ : {
2464
+ valid: false,
2465
+ exists: false,
2466
+ detail: `Legacy continuation ${label} cannot be inspected: ${continuationReason(error)}`,
2467
+ };
2468
+ }
2469
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
2470
+ return {
2471
+ valid: false,
2472
+ exists: true,
2473
+ detail: `Legacy continuation ${label} must be a single-link regular file.`,
2474
+ };
2475
+ }
2476
+ const securityError = continuationOwnershipError(info, `legacy ${label}`, 0o600);
2477
+ return securityError === undefined
2478
+ ? { valid: true, exists: true, detail: `Legacy continuation ${label} is owner-only.` }
2479
+ : { valid: false, exists: true, detail: securityError };
2480
+ }
2481
+ async function inspectContinuationRollbackGuard(path, required, missingDetail = "The v2 rollback guard is not installed; the current runtime will install it during v3 migration.") {
1981
2482
  let info;
1982
2483
  try {
1983
2484
  info = await lstat(path);
@@ -1986,12 +2487,12 @@ async function inspectContinuationRollbackGuard(path, required) {
1986
2487
  if (continuationFsCode(error) === "ENOENT") {
1987
2488
  return required
1988
2489
  ? { valid: false, detail: "Continuation v2 rollback guard is missing from the v3 store." }
1989
- : { valid: true, detail: "The v2 rollback guard is not installed; the current runtime will install it during v3 migration." };
2490
+ : { valid: true, detail: missingDetail };
1990
2491
  }
1991
2492
  return { valid: false, detail: `Continuation v2 rollback guard cannot be inspected: ${continuationReason(error)}` };
1992
2493
  }
1993
- if (!info.isFile() || info.isSymbolicLink()) {
1994
- return { valid: false, detail: "Continuation v2 rollback guard must be a regular file, not a symlink." };
2494
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
2495
+ return { valid: false, detail: "Continuation v2 rollback guard must be a single-link regular file, not a symlink." };
1995
2496
  }
1996
2497
  const securityError = continuationOwnershipError(info, "v2 rollback guard", 0o600);
1997
2498
  if (securityError !== undefined)
@@ -2001,7 +2502,7 @@ async function inspectContinuationRollbackGuard(path, required) {
2001
2502
  }
2002
2503
  let contents;
2003
2504
  try {
2004
- contents = await readFile(path, "utf8");
2505
+ contents = await readBoundedOwnerOnlyFile(path, 4 * 1024, "Continuation v2 rollback guard");
2005
2506
  }
2006
2507
  catch (error) {
2007
2508
  return { valid: false, detail: `Continuation v2 rollback guard cannot be read: ${continuationReason(error)}` };
@@ -2014,26 +2515,62 @@ async function inspectContinuationRollbackGuard(path, required) {
2014
2515
  detail: "The owner-only v2 rollback guard prevents older runtimes from opening stale continuation records.",
2015
2516
  };
2016
2517
  }
2017
- async function inspectContinuationTransaction(path) {
2518
+ async function inspectContinuationTransaction(path, expectedSchemaVersion) {
2519
+ const versionLabel = `v${String(expectedSchemaVersion)}`;
2018
2520
  let info;
2019
2521
  try {
2020
2522
  info = await lstat(path);
2021
2523
  }
2022
2524
  catch (error) {
2023
2525
  return continuationFsCode(error) === "ENOENT"
2024
- ? { valid: true, pending: false, detail: "No interrupted durable transaction is awaiting recovery." }
2025
- : { valid: false, pending: false, detail: `Continuation transaction cannot be inspected: ${continuationReason(error)}` };
2526
+ ? { valid: true, pending: false, detail: `No interrupted durable ${versionLabel} transaction is awaiting recovery.` }
2527
+ : {
2528
+ valid: false,
2529
+ pending: false,
2530
+ detail: `Continuation ${versionLabel} transaction cannot be inspected: ${continuationReason(error)}`,
2531
+ };
2026
2532
  }
2027
- if (!info.isFile() || info.isSymbolicLink()) {
2028
- return { valid: false, pending: true, detail: "Continuation transaction must be a regular file, not a symlink." };
2533
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
2534
+ return {
2535
+ valid: false,
2536
+ pending: true,
2537
+ detail: `Continuation ${versionLabel} transaction must be a single-link regular file, not a symlink.`,
2538
+ };
2029
2539
  }
2030
- const securityError = continuationOwnershipError(info, "transaction", 0o600);
2540
+ const securityError = continuationOwnershipError(info, `${versionLabel} transaction`, 0o600);
2031
2541
  if (securityError !== undefined)
2032
2542
  return { valid: false, pending: true, detail: securityError };
2543
+ let transaction;
2544
+ try {
2545
+ transaction = JSON.parse(await readBoundedOwnerOnlyFile(path, MAX_TRANSACTION_BYTES, `Continuation ${versionLabel} transaction`));
2546
+ }
2547
+ catch (error) {
2548
+ return {
2549
+ valid: false,
2550
+ pending: true,
2551
+ detail: `Continuation ${versionLabel} transaction cannot be read as bounded JSON: ${continuationReason(error)}`,
2552
+ };
2553
+ }
2554
+ if (!isRecordTransaction(transaction, expectedSchemaVersion)) {
2555
+ return {
2556
+ valid: false,
2557
+ pending: true,
2558
+ detail: `Continuation ${versionLabel} transaction has an unsupported or malformed schema.`,
2559
+ };
2560
+ }
2561
+ const oversizedRecord = transaction.writes.find((record) => Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`, "utf8") > MAX_RECORD_BYTES);
2562
+ if (oversizedRecord !== undefined) {
2563
+ return {
2564
+ valid: false,
2565
+ pending: true,
2566
+ detail: `Continuation ${versionLabel} transaction contains a record over its ${String(MAX_RECORD_BYTES)} byte safety limit: ${oversizedRecord.continuationId}.`,
2567
+ };
2568
+ }
2033
2569
  return {
2034
2570
  valid: true,
2035
2571
  pending: true,
2036
- detail: "An interrupted durable transaction is present and will be completed idempotently by the state owner.",
2572
+ transaction,
2573
+ detail: `An interrupted durable ${versionLabel} transaction is present and will be completed idempotently by the state owner.`,
2037
2574
  };
2038
2575
  }
2039
2576
  function continuationOwnershipError(info, label, expectedMode) {
@@ -2046,14 +2583,14 @@ function continuationOwnershipError(info, label, expectedMode) {
2046
2583
  return undefined;
2047
2584
  }
2048
2585
  function isContinuationLedger(value) {
2049
- return isDoctorObject(value)
2050
- && value.schemaVersion === CONTINUATION_STORE_SCHEMA_VERSION
2051
- && isDoctorObject(value.records);
2586
+ return isStoreFile(value);
2052
2587
  }
2053
2588
  function isContinuationStoreManifest(value, schemaVersion, format) {
2054
2589
  if (!isDoctorObject(value)
2055
2590
  || value.schemaVersion !== schemaVersion
2056
- || typeof value.generation !== "string"
2591
+ || !isDurableGeneration(value.generation)
2592
+ || !requiredDate(value.updatedAt)
2593
+ || (value.rollbackGuardRequired !== undefined && typeof value.rollbackGuardRequired !== "boolean")
2057
2594
  || !isDoctorObject(value.stats)
2058
2595
  || value.stats.format !== format
2059
2596
  || !isDoctorObject(value.stats.limits))