@mono-agent/agent-app 0.11.2 → 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 (374) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +222 -26
  3. package/dist/adapter-send-tools-main.js +7 -1
  4. package/dist/adapter-send-tools-main.js.map +1 -1
  5. package/dist/adapter-send-tools.d.ts +19 -1
  6. package/dist/adapter-send-tools.d.ts.map +1 -1
  7. package/dist/adapter-send-tools.js +127 -13
  8. package/dist/adapter-send-tools.js.map +1 -1
  9. package/dist/app-config.d.ts.map +1 -1
  10. package/dist/app-config.js +15 -2
  11. package/dist/app-config.js.map +1 -1
  12. package/dist/app-controller-channels.d.ts +8 -0
  13. package/dist/app-controller-channels.d.ts.map +1 -0
  14. package/dist/app-controller-channels.js +207 -0
  15. package/dist/app-controller-channels.js.map +1 -0
  16. package/dist/app-controller-continuation.d.ts +26 -0
  17. package/dist/app-controller-continuation.d.ts.map +1 -0
  18. package/dist/app-controller-continuation.js +198 -0
  19. package/dist/app-controller-continuation.js.map +1 -0
  20. package/dist/app-controller-lifecycle.d.ts +6 -0
  21. package/dist/app-controller-lifecycle.d.ts.map +1 -0
  22. package/dist/app-controller-lifecycle.js +81 -0
  23. package/dist/app-controller-lifecycle.js.map +1 -0
  24. package/dist/app-controller-maintenance.d.ts +13 -0
  25. package/dist/app-controller-maintenance.d.ts.map +1 -0
  26. package/dist/app-controller-maintenance.js +180 -0
  27. package/dist/app-controller-maintenance.js.map +1 -0
  28. package/dist/app-controller-memory-health.d.ts +12 -0
  29. package/dist/app-controller-memory-health.d.ts.map +1 -0
  30. package/dist/app-controller-memory-health.js +160 -0
  31. package/dist/app-controller-memory-health.js.map +1 -0
  32. package/dist/app-controller-memory.d.ts +9 -0
  33. package/dist/app-controller-memory.d.ts.map +1 -0
  34. package/dist/app-controller-memory.js +83 -0
  35. package/dist/app-controller-memory.js.map +1 -0
  36. package/dist/app-controller-responder.d.ts +20 -0
  37. package/dist/app-controller-responder.d.ts.map +1 -0
  38. package/dist/app-controller-responder.js +246 -0
  39. package/dist/app-controller-responder.js.map +1 -0
  40. package/dist/app-controller-traceability.d.ts +25 -0
  41. package/dist/app-controller-traceability.d.ts.map +1 -0
  42. package/dist/app-controller-traceability.js +418 -0
  43. package/dist/app-controller-traceability.js.map +1 -0
  44. package/dist/app-controller-utils.d.ts +38 -0
  45. package/dist/app-controller-utils.d.ts.map +1 -0
  46. package/dist/app-controller-utils.js +185 -0
  47. package/dist/app-controller-utils.js.map +1 -0
  48. package/dist/app-controller.d.ts +400 -0
  49. package/dist/app-controller.d.ts.map +1 -0
  50. package/dist/app-controller.js +336 -0
  51. package/dist/app-controller.js.map +1 -0
  52. package/dist/app.d.ts +7 -115
  53. package/dist/app.d.ts.map +1 -1
  54. package/dist/app.js +6 -1912
  55. package/dist/app.js.map +1 -1
  56. package/dist/artifact-retention.d.ts +4 -0
  57. package/dist/artifact-retention.d.ts.map +1 -1
  58. package/dist/artifact-retention.js +38 -0
  59. package/dist/artifact-retention.js.map +1 -1
  60. package/dist/backfill.d.ts +3 -1
  61. package/dist/backfill.d.ts.map +1 -1
  62. package/dist/backfill.js +5 -1
  63. package/dist/backfill.js.map +1 -1
  64. package/dist/background-runtime.d.ts +27 -0
  65. package/dist/background-runtime.d.ts.map +1 -1
  66. package/dist/background-runtime.js +383 -205
  67. package/dist/background-runtime.js.map +1 -1
  68. package/dist/background-worker-lease.d.ts.map +1 -1
  69. package/dist/background-worker-lease.js +41 -241
  70. package/dist/background-worker-lease.js.map +1 -1
  71. package/dist/background.d.ts +31 -9
  72. package/dist/background.d.ts.map +1 -1
  73. package/dist/background.js +543 -272
  74. package/dist/background.js.map +1 -1
  75. package/dist/channel-drivers/cron.d.ts +11 -0
  76. package/dist/channel-drivers/cron.d.ts.map +1 -0
  77. package/dist/channel-drivers/cron.js +168 -0
  78. package/dist/channel-drivers/cron.js.map +1 -0
  79. package/dist/channel-drivers/live.d.ts +11 -0
  80. package/dist/channel-drivers/live.d.ts.map +1 -0
  81. package/dist/channel-drivers/live.js +47 -0
  82. package/dist/channel-drivers/live.js.map +1 -0
  83. package/dist/channel-drivers/native-notify.d.ts +27 -0
  84. package/dist/channel-drivers/native-notify.d.ts.map +1 -0
  85. package/dist/channel-drivers/native-notify.js +94 -0
  86. package/dist/channel-drivers/native-notify.js.map +1 -0
  87. package/dist/channel-drivers/openai-api.d.ts +7 -0
  88. package/dist/channel-drivers/openai-api.d.ts.map +1 -0
  89. package/dist/channel-drivers/openai-api.js +63 -0
  90. package/dist/channel-drivers/openai-api.js.map +1 -0
  91. package/dist/channel-drivers/shared.d.ts +4 -0
  92. package/dist/channel-drivers/shared.d.ts.map +1 -0
  93. package/dist/channel-drivers/shared.js +5 -0
  94. package/dist/channel-drivers/shared.js.map +1 -0
  95. package/dist/channel-drivers/slack.d.ts +14 -0
  96. package/dist/channel-drivers/slack.d.ts.map +1 -0
  97. package/dist/channel-drivers/slack.js +227 -0
  98. package/dist/channel-drivers/slack.js.map +1 -0
  99. package/dist/channel-drivers/telegram.d.ts +11 -0
  100. package/dist/channel-drivers/telegram.d.ts.map +1 -0
  101. package/dist/channel-drivers/telegram.js +244 -0
  102. package/dist/channel-drivers/telegram.js.map +1 -0
  103. package/dist/channel-drivers/tui.d.ts +15 -0
  104. package/dist/channel-drivers/tui.d.ts.map +1 -0
  105. package/dist/channel-drivers/tui.js +108 -0
  106. package/dist/channel-drivers/tui.js.map +1 -0
  107. package/dist/channel-drivers/webhook.d.ts +7 -0
  108. package/dist/channel-drivers/webhook.d.ts.map +1 -0
  109. package/dist/channel-drivers/webhook.js +115 -0
  110. package/dist/channel-drivers/webhook.js.map +1 -0
  111. package/dist/channels.d.ts +21 -81
  112. package/dist/channels.d.ts.map +1 -1
  113. package/dist/channels.js +11 -1174
  114. package/dist/channels.js.map +1 -1
  115. package/dist/cli-args.d.ts +121 -0
  116. package/dist/cli-args.d.ts.map +1 -0
  117. package/dist/cli-args.js +532 -0
  118. package/dist/cli-args.js.map +1 -0
  119. package/dist/cli-background-command.d.ts +59 -0
  120. package/dist/cli-background-command.d.ts.map +1 -0
  121. package/dist/cli-background-command.js +475 -0
  122. package/dist/cli-background-command.js.map +1 -0
  123. package/dist/cli-cancellation.d.ts +6 -0
  124. package/dist/cli-cancellation.d.ts.map +1 -0
  125. package/dist/cli-cancellation.js +23 -0
  126. package/dist/cli-cancellation.js.map +1 -0
  127. package/dist/cli-commands.d.ts +12 -0
  128. package/dist/cli-commands.d.ts.map +1 -0
  129. package/dist/cli-commands.js +306 -0
  130. package/dist/cli-commands.js.map +1 -0
  131. package/dist/cli-help.d.ts +11 -0
  132. package/dist/cli-help.d.ts.map +1 -0
  133. package/dist/cli-help.js +262 -0
  134. package/dist/cli-help.js.map +1 -0
  135. package/dist/cli-init-command.d.ts +60 -0
  136. package/dist/cli-init-command.d.ts.map +1 -0
  137. package/dist/cli-init-command.js +1910 -0
  138. package/dist/cli-init-command.js.map +1 -0
  139. package/dist/cli-install-skill-command.d.ts +3 -0
  140. package/dist/cli-install-skill-command.d.ts.map +1 -0
  141. package/dist/cli-install-skill-command.js +51 -0
  142. package/dist/cli-install-skill-command.js.map +1 -0
  143. package/dist/cli-sandbox-command.d.ts +10 -0
  144. package/dist/cli-sandbox-command.d.ts.map +1 -0
  145. package/dist/cli-sandbox-command.js +100 -0
  146. package/dist/cli-sandbox-command.js.map +1 -0
  147. package/dist/cli-validate-config-command.d.ts +36 -0
  148. package/dist/cli-validate-config-command.d.ts.map +1 -0
  149. package/dist/cli-validate-config-command.js +317 -0
  150. package/dist/cli-validate-config-command.js.map +1 -0
  151. package/dist/cli.d.ts +6 -248
  152. package/dist/cli.d.ts.map +1 -1
  153. package/dist/cli.js +6 -3792
  154. package/dist/cli.js.map +1 -1
  155. package/dist/config-reference.d.ts +2 -1
  156. package/dist/config-reference.d.ts.map +1 -1
  157. package/dist/config-reference.js +34 -8
  158. package/dist/config-reference.js.map +1 -1
  159. package/dist/configured-agent.d.ts +23 -0
  160. package/dist/configured-agent.d.ts.map +1 -1
  161. package/dist/configured-agent.js +92 -13
  162. package/dist/configured-agent.js.map +1 -1
  163. package/dist/continuation-dispatch-worker.d.ts +21 -0
  164. package/dist/continuation-dispatch-worker.d.ts.map +1 -0
  165. package/dist/continuation-dispatch-worker.js +425 -0
  166. package/dist/continuation-dispatch-worker.js.map +1 -0
  167. package/dist/continuation-http-routes.d.ts +15 -0
  168. package/dist/continuation-http-routes.d.ts.map +1 -0
  169. package/dist/continuation-http-routes.js +265 -0
  170. package/dist/continuation-http-routes.js.map +1 -0
  171. package/dist/continuation-operator-api.d.ts +18 -0
  172. package/dist/continuation-operator-api.d.ts.map +1 -0
  173. package/dist/continuation-operator-api.js +164 -0
  174. package/dist/continuation-operator-api.js.map +1 -0
  175. package/dist/continuation-origin-store.d.ts +19 -0
  176. package/dist/continuation-origin-store.d.ts.map +1 -0
  177. package/dist/continuation-origin-store.js +273 -0
  178. package/dist/continuation-origin-store.js.map +1 -0
  179. package/dist/continuation-service-errors.d.ts +18 -0
  180. package/dist/continuation-service-errors.d.ts.map +1 -0
  181. package/dist/continuation-service-errors.js +35 -0
  182. package/dist/continuation-service-errors.js.map +1 -0
  183. package/dist/continuation-service-helpers.d.ts +39 -0
  184. package/dist/continuation-service-helpers.d.ts.map +1 -0
  185. package/dist/continuation-service-helpers.js +308 -0
  186. package/dist/continuation-service-helpers.js.map +1 -0
  187. package/dist/continuation-service-origin.d.ts +55 -0
  188. package/dist/continuation-service-origin.d.ts.map +1 -0
  189. package/dist/continuation-service-origin.js +325 -0
  190. package/dist/continuation-service-origin.js.map +1 -0
  191. package/dist/continuation-service-types.d.ts +110 -0
  192. package/dist/continuation-service-types.d.ts.map +1 -0
  193. package/dist/continuation-service-types.js +11 -0
  194. package/dist/continuation-service-types.js.map +1 -0
  195. package/dist/continuation-service.d.ts +4 -97
  196. package/dist/continuation-service.d.ts.map +1 -1
  197. package/dist/continuation-service.js +9 -1525
  198. package/dist/continuation-service.js.map +1 -1
  199. package/dist/continuation-store-fs.d.ts +15 -0
  200. package/dist/continuation-store-fs.d.ts.map +1 -0
  201. package/dist/continuation-store-fs.js +247 -0
  202. package/dist/continuation-store-fs.js.map +1 -0
  203. package/dist/continuation-store-policy.d.ts +21 -0
  204. package/dist/continuation-store-policy.d.ts.map +1 -0
  205. package/dist/continuation-store-policy.js +278 -0
  206. package/dist/continuation-store-policy.js.map +1 -0
  207. package/dist/continuation-store-records.d.ts +9 -0
  208. package/dist/continuation-store-records.d.ts.map +1 -0
  209. package/dist/continuation-store-records.js +221 -0
  210. package/dist/continuation-store-records.js.map +1 -0
  211. package/dist/continuation-store-types.d.ts +173 -0
  212. package/dist/continuation-store-types.d.ts.map +1 -0
  213. package/dist/continuation-store-types.js +26 -0
  214. package/dist/continuation-store-types.js.map +1 -0
  215. package/dist/continuation-store.d.ts +4 -138
  216. package/dist/continuation-store.d.ts.map +1 -1
  217. package/dist/continuation-store.js +145 -1049
  218. package/dist/continuation-store.js.map +1 -1
  219. package/dist/doctor.d.ts +10 -5
  220. package/dist/doctor.d.ts.map +1 -1
  221. package/dist/doctor.js +629 -58
  222. package/dist/doctor.js.map +1 -1
  223. package/dist/init.d.ts +2 -0
  224. package/dist/init.d.ts.map +1 -1
  225. package/dist/init.js +92 -239
  226. package/dist/init.js.map +1 -1
  227. package/dist/interaction-bridge.d.ts +19 -0
  228. package/dist/interaction-bridge.d.ts.map +1 -1
  229. package/dist/interaction-bridge.js +130 -0
  230. package/dist/interaction-bridge.js.map +1 -1
  231. package/dist/launchd-logs.d.ts +91 -0
  232. package/dist/launchd-logs.d.ts.map +1 -0
  233. package/dist/launchd-logs.js +1499 -0
  234. package/dist/launchd-logs.js.map +1 -0
  235. package/dist/launchd.d.ts +27 -0
  236. package/dist/launchd.d.ts.map +1 -1
  237. package/dist/launchd.js +89 -10
  238. package/dist/launchd.js.map +1 -1
  239. package/dist/memory-command.d.ts +2 -0
  240. package/dist/memory-command.d.ts.map +1 -1
  241. package/dist/memory-command.js +161 -48
  242. package/dist/memory-command.js.map +1 -1
  243. package/dist/memory-recall-settings.d.ts +64 -0
  244. package/dist/memory-recall-settings.d.ts.map +1 -0
  245. package/dist/memory-recall-settings.js +51 -0
  246. package/dist/memory-recall-settings.js.map +1 -0
  247. package/dist/memory-recall.d.ts +7 -95
  248. package/dist/memory-recall.d.ts.map +1 -1
  249. package/dist/memory-recall.js +8 -143
  250. package/dist/memory-recall.js.map +1 -1
  251. package/dist/memory-rituals.d.ts +3 -3
  252. package/dist/memory-rituals.d.ts.map +1 -1
  253. package/dist/memory-rituals.js +16 -111
  254. package/dist/memory-rituals.js.map +1 -1
  255. package/dist/modules/catalog.js +2 -2
  256. package/dist/modules/catalog.js.map +1 -1
  257. package/dist/modules/known-tools.d.ts +8 -6
  258. package/dist/modules/known-tools.d.ts.map +1 -1
  259. package/dist/modules/known-tools.js +8 -6
  260. package/dist/modules/known-tools.js.map +1 -1
  261. package/dist/notify-destinations.d.ts +5 -0
  262. package/dist/notify-destinations.d.ts.map +1 -1
  263. package/dist/notify-destinations.js +7 -1
  264. package/dist/notify-destinations.js.map +1 -1
  265. package/dist/owner-private-lock.d.ts +95 -0
  266. package/dist/owner-private-lock.d.ts.map +1 -0
  267. package/dist/owner-private-lock.js +566 -0
  268. package/dist/owner-private-lock.js.map +1 -0
  269. package/dist/pi-oauth-login.d.ts +2 -0
  270. package/dist/pi-oauth-login.d.ts.map +1 -1
  271. package/dist/pi-oauth-login.js +39 -5
  272. package/dist/pi-oauth-login.js.map +1 -1
  273. package/dist/posted-message-index.d.ts +35 -16
  274. package/dist/posted-message-index.d.ts.map +1 -1
  275. package/dist/posted-message-index.js +1068 -52
  276. package/dist/posted-message-index.js.map +1 -1
  277. package/dist/posted-reply-history.d.ts +17 -0
  278. package/dist/posted-reply-history.d.ts.map +1 -0
  279. package/dist/posted-reply-history.js +169 -0
  280. package/dist/posted-reply-history.js.map +1 -0
  281. package/dist/proactive-notify.d.ts.map +1 -1
  282. package/dist/proactive-notify.js +6 -4
  283. package/dist/proactive-notify.js.map +1 -1
  284. package/dist/project-skills.d.ts +4 -0
  285. package/dist/project-skills.d.ts.map +1 -1
  286. package/dist/project-skills.js +99 -130
  287. package/dist/project-skills.js.map +1 -1
  288. package/dist/provider-setup.d.ts +0 -1
  289. package/dist/provider-setup.d.ts.map +1 -1
  290. package/dist/provider-setup.js +0 -3
  291. package/dist/provider-setup.js.map +1 -1
  292. package/dist/readiness-probe-worker.d.ts +61 -1
  293. package/dist/readiness-probe-worker.d.ts.map +1 -1
  294. package/dist/readiness-probe-worker.js +54 -58
  295. package/dist/readiness-probe-worker.js.map +1 -1
  296. package/dist/readiness-probe.d.ts +6 -9
  297. package/dist/readiness-probe.d.ts.map +1 -1
  298. package/dist/readiness-probe.js +30 -56
  299. package/dist/readiness-probe.js.map +1 -1
  300. package/dist/redact-secrets.d.ts +15 -0
  301. package/dist/redact-secrets.d.ts.map +1 -0
  302. package/dist/redact-secrets.js +65 -0
  303. package/dist/redact-secrets.js.map +1 -0
  304. package/dist/run-history.js +34 -24
  305. package/dist/run-history.js.map +1 -1
  306. package/dist/runtime-provenance.d.ts +8 -0
  307. package/dist/runtime-provenance.d.ts.map +1 -0
  308. package/dist/runtime-provenance.js +225 -0
  309. package/dist/runtime-provenance.js.map +1 -0
  310. package/dist/sandbox-manager.d.ts +4 -1
  311. package/dist/sandbox-manager.d.ts.map +1 -1
  312. package/dist/sandbox-manager.js +114 -273
  313. package/dist/sandbox-manager.js.map +1 -1
  314. package/dist/secure-file-replace.d.ts +49 -0
  315. package/dist/secure-file-replace.d.ts.map +1 -0
  316. package/dist/secure-file-replace.js +270 -0
  317. package/dist/secure-file-replace.js.map +1 -0
  318. package/dist/seen-conversations.d.ts +32 -5
  319. package/dist/seen-conversations.d.ts.map +1 -1
  320. package/dist/seen-conversations.js +145 -15
  321. package/dist/seen-conversations.js.map +1 -1
  322. package/dist/sessions.d.ts +20 -0
  323. package/dist/sessions.d.ts.map +1 -1
  324. package/dist/sessions.js +41 -6
  325. package/dist/sessions.js.map +1 -1
  326. package/dist/wizard/index.d.ts +2 -2
  327. package/dist/wizard/index.d.ts.map +1 -1
  328. package/dist/wizard/index.js +1 -1
  329. package/dist/wizard/index.js.map +1 -1
  330. package/dist/wizard/presets.d.ts +2 -1
  331. package/dist/wizard/presets.d.ts.map +1 -1
  332. package/dist/wizard/presets.js +2 -1
  333. package/dist/wizard/presets.js.map +1 -1
  334. package/dist/wizard/run.d.ts +0 -26
  335. package/dist/wizard/run.d.ts.map +1 -1
  336. package/dist/wizard/run.js +0 -53
  337. package/dist/wizard/run.js.map +1 -1
  338. package/package.json +22 -17
  339. package/schema/mono-agent.config.schema.json +45 -10
  340. package/skills/mono-agent-composer/SKILL.md +3 -3
  341. package/skills/mono-agent-composer/references/config-blueprint.md +123 -29
  342. package/skills/mono-agent-composer/references/discovery-questions.md +10 -8
  343. package/skills/mono-agent-composer/references/feature-coverage.md +90 -76
  344. package/skills/mono-agent-composer/references/package-map.md +6 -4
  345. package/skills/mono-agent-composer/references/playbooks.md +48 -27
  346. package/skills/mono-agent-composer/references/validation.md +16 -3
  347. package/dist/notify-runtime.d.ts +0 -26
  348. package/dist/notify-runtime.d.ts.map +0 -1
  349. package/dist/notify-runtime.js +0 -27
  350. package/dist/notify-runtime.js.map +0 -1
  351. package/dist/notify-tool.d.ts +0 -51
  352. package/dist/notify-tool.d.ts.map +0 -1
  353. package/dist/notify-tool.js +0 -182
  354. package/dist/notify-tool.js.map +0 -1
  355. package/dist/recipes/base.d.ts +0 -15
  356. package/dist/recipes/base.d.ts.map +0 -1
  357. package/dist/recipes/base.js +0 -51
  358. package/dist/recipes/base.js.map +0 -1
  359. package/dist/recipes/catalog.d.ts +0 -4
  360. package/dist/recipes/catalog.d.ts.map +0 -1
  361. package/dist/recipes/catalog.js +0 -525
  362. package/dist/recipes/catalog.js.map +0 -1
  363. package/dist/recipes/index.d.ts +0 -11
  364. package/dist/recipes/index.d.ts.map +0 -1
  365. package/dist/recipes/index.js +0 -14
  366. package/dist/recipes/index.js.map +0 -1
  367. package/dist/recipes/types.d.ts +0 -70
  368. package/dist/recipes/types.d.ts.map +0 -1
  369. package/dist/recipes/types.js +0 -15
  370. package/dist/recipes/types.js.map +0 -1
  371. package/dist/setup.d.ts +0 -29
  372. package/dist/setup.d.ts.map +0 -1
  373. package/dist/setup.js +0 -97
  374. package/dist/setup.js.map +0 -1
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,13 +1119,25 @@ 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."] };
1051
1138
  }
1052
- // External backend (e.g. supermemory): mode/embeddings/llm are bujo-only and ignored, so report the
1053
- // backend's own shape. We do not ping the instance here (config-shape check); the playbook covers
1054
- // starting the server.
1139
+ // External backend (e.g. supermemory): mode/embeddings/llm are bujo-only and
1140
+ // ignored, so validate the plugin-owned shape before any soft liveness probe.
1055
1141
  if ((config.memory.backend ?? "bujo") === "supermemory") {
1056
1142
  const sm = config.memory.supermemory;
1057
1143
  if (sm === undefined) {
@@ -1088,19 +1174,26 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
1088
1174
  details: [`[ERROR] ${error instanceof Error ? error.message : String(error)}`],
1089
1175
  };
1090
1176
  }
1091
- return {
1092
- id: "memory",
1093
- label: "Memory",
1094
- status: "ok",
1095
- details: [
1096
- `Backend: supermemory, writeMode: ${config.memory.writeMode}.`,
1097
- `Endpoint: ${sm.baseUrl} (container "${resolveSupermemoryContainer(config)}").`,
1098
- sm.apiKey === undefined
1099
- ? "Auth: no API key configured (keyless — works only if the instance allows it)."
1100
- : "Auth: API key configured.",
1101
- "Start the Supermemory instance (e.g. `supermemory-server`) before sending turns; ingestion is async.",
1102
- ],
1103
- };
1177
+ const details = [
1178
+ `Backend: supermemory, writeMode: ${config.memory.writeMode}.`,
1179
+ `Endpoint: ${sm.baseUrl} (container "${resolveSupermemoryContainer(config)}").`,
1180
+ sm.apiKey === undefined
1181
+ ? "Auth: no API key configured (keyless — works only if the instance allows it)."
1182
+ : "Auth: API key configured.",
1183
+ ];
1184
+ if (!liveness) {
1185
+ details.push("Supermemory liveness probe skipped; ingestion is async.");
1186
+ return { id: "memory", label: "Memory", status: "ok", details };
1187
+ }
1188
+ const probe = await probeSupermemoryEndpoint(sm.baseUrl);
1189
+ if (!probe.reachable) {
1190
+ details.push(`[WARN] Supermemory is not reachable at ${sm.baseUrl} (${probe.reason}). ` +
1191
+ "Start Supermemory or fix memory.supermemory.baseUrl, then re-run `mono-agent validate`; " +
1192
+ "capture and recall will degrade until it is reachable.");
1193
+ return { id: "memory", label: "Memory", status: "waiting", details };
1194
+ }
1195
+ details.push(`Supermemory transport reachable at ${sm.baseUrl} (HTTP ${probe.status}); ingestion is async.`);
1196
+ return { id: "memory", label: "Memory", status: "ok", details };
1104
1197
  }
1105
1198
  const details = [
1106
1199
  `Mode: ${config.memory.mode}, path: ${config.memory.path}, writeMode: ${config.memory.writeMode}.`,
@@ -1133,6 +1226,11 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
1133
1226
  else {
1134
1227
  details.push("Consolidation: disabled.");
1135
1228
  }
1229
+ const cronIssue = memoryConsolidationCronIssue(consolidationCron);
1230
+ if (cronIssue !== undefined) {
1231
+ status = "error";
1232
+ details.push(`[ERROR] ${cronIssue}`);
1233
+ }
1136
1234
  }
1137
1235
  const managedIdentity = await managedMemoryIdentityStatus(config.memory);
1138
1236
  if (managedIdentity !== undefined) {
@@ -1294,11 +1392,39 @@ function isBuiltInMemoryNativeFailure(error) {
1294
1392
  async function liteRootWritableWarning(memoryPath, allowFilesystemWrites) {
1295
1393
  return await memoryRootWritableWarnings("lite", memoryPath, allowFilesystemWrites);
1296
1394
  }
1297
- const BUJO_PROBE_TIMEOUT_MS = 3_000;
1395
+ const LIVENESS_PROBE_TIMEOUT_MS = 3_000;
1396
+ /**
1397
+ * Read-only transport probe for a configured Supermemory service root. Neither
1398
+ * the hosted nor self-hosted base URL has a documented health response, so any
1399
+ * HTTP status proves reachability; only transport failure or timeout degrades
1400
+ * validation. Manual redirects and omitted auth keep the probe on the exact
1401
+ * configured endpoint without sending memory data or credentials.
1402
+ */
1403
+ async function probeSupermemoryEndpoint(endpoint) {
1404
+ const ctrl = new AbortController();
1405
+ const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
1406
+ try {
1407
+ const response = await fetch(endpoint, {
1408
+ method: "HEAD",
1409
+ redirect: "manual",
1410
+ signal: ctrl.signal,
1411
+ });
1412
+ return { reachable: true, status: response.status };
1413
+ }
1414
+ catch (error) {
1415
+ return {
1416
+ reachable: false,
1417
+ reason: error instanceof Error ? error.message : String(error),
1418
+ };
1419
+ }
1420
+ finally {
1421
+ clearTimeout(timer);
1422
+ }
1423
+ }
1298
1424
  /** Probes Ollama /api/tags and returns a sorted list of model names, or throws. */
1299
1425
  async function fetchOllamaModels(endpoint) {
1300
1426
  const ctrl = new AbortController();
1301
- const timer = setTimeout(() => { ctrl.abort(); }, BUJO_PROBE_TIMEOUT_MS);
1427
+ const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
1302
1428
  try {
1303
1429
  const res = await fetch(`${endpoint}/api/tags`, { signal: ctrl.signal });
1304
1430
  if (!res.ok) {
@@ -1385,7 +1511,7 @@ async function localEmbeddingLivenessWarnings(embeddings, apiKey) {
1385
1511
  endpoint,
1386
1512
  model: embeddings.model,
1387
1513
  expectedDimension: embeddings.dim ?? 768,
1388
- timeoutMs: BUJO_PROBE_TIMEOUT_MS,
1514
+ timeoutMs: LIVENESS_PROBE_TIMEOUT_MS,
1389
1515
  ...(apiKey === undefined ? {} : { apiKey }),
1390
1516
  });
1391
1517
  return [];
@@ -1681,7 +1807,7 @@ async function continuationSection(config, input) {
1681
1807
  details.push(routeNames.length === 0 ? "Named detached routes: none." : `Named detached routes: ${routeNames.join(", ")}.`);
1682
1808
  const detachedNames = Object.keys(settings.detachedServices);
1683
1809
  details.push(detachedNames.length === 0 ? "Detached services: none." : `Detached services: ${detachedNames.join(", ")}.`);
1684
- const state = await inspectContinuationState(settings.stateDir);
1810
+ const state = await inspectContinuationState(settings.stateDir, settings.retention);
1685
1811
  details.push(...state.details);
1686
1812
  return {
1687
1813
  id: "continuations",
@@ -1690,8 +1816,9 @@ async function continuationSection(config, input) {
1690
1816
  details,
1691
1817
  };
1692
1818
  }
1693
- async function inspectContinuationState(stateDir) {
1819
+ async function inspectContinuationState(stateDir, retention) {
1694
1820
  const details = [];
1821
+ const retentionPolicy = resolveRetention(retention);
1695
1822
  let directory;
1696
1823
  try {
1697
1824
  directory = await lstat(stateDir);
@@ -1735,7 +1862,7 @@ async function inspectContinuationState(stateDir) {
1735
1862
  return { status: "error", details: [manifestSecurityError] };
1736
1863
  let manifest;
1737
1864
  try {
1738
- manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1865
+ manifest = JSON.parse(await readBoundedOwnerOnlyFile(manifestPath, MAX_MANIFEST_BYTES, "Continuation v3 manifest"));
1739
1866
  }
1740
1867
  catch (error) {
1741
1868
  return { status: "error", details: [`Continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
@@ -1743,21 +1870,116 @@ async function inspectContinuationState(stateDir) {
1743
1870
  if (!isContinuationStoreManifest(manifest, CONTINUATION_RECORD_STORE_SCHEMA_VERSION, "per-record-v3")) {
1744
1871
  return { status: "error", details: ["Continuation store manifest has an unsupported or malformed schema."] };
1745
1872
  }
1746
- const recordsDirectoryError = await inspectContinuationRecordsDirectory(join(stateDir, "records-v3"));
1873
+ const recordsDirectoryPath = join(stateDir, "records-v3");
1874
+ const recordsDirectoryError = await inspectContinuationRecordsDirectory(recordsDirectoryPath);
1747
1875
  if (recordsDirectoryError !== undefined)
1748
1876
  return { status: "error", details: [recordsDirectoryError] };
1749
- 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);
1750
1889
  if (!transaction.valid)
1751
1890
  return { status: "error", details: [transaction.detail] };
1752
- 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.");
1753
1973
  if (!rollbackGuard.valid)
1754
1974
  return { status: "error", details: [rollbackGuard.detail] };
1755
1975
  const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
1756
1976
  if (!owner.valid)
1757
1977
  return { status: "error", details: [owner.detail] };
1758
- 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);
1759
1979
  return {
1760
1980
  status: transaction.pending
1981
+ || originGroups.hasEntries
1982
+ || legacyMigrationPending
1761
1983
  || manifest.stats.unresolvedDelivery > 0
1762
1984
  || manifest.stats.deadLettered > 0
1763
1985
  || manifest.stats.historyDegraded > 0
@@ -1766,6 +1988,19 @@ async function inspectContinuationState(stateDir) {
1766
1988
  details,
1767
1989
  };
1768
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
+ }
1769
2004
  const legacyManifestPath = join(stateDir, "continuation-store-v2.json");
1770
2005
  let legacyManifestInfo;
1771
2006
  try {
@@ -1776,7 +2011,10 @@ async function inspectContinuationState(stateDir) {
1776
2011
  return { status: "error", details: [`Legacy continuation store manifest cannot be inspected: ${continuationReason(error)}`] };
1777
2012
  }
1778
2013
  }
2014
+ let legacyManifestExists = false;
2015
+ let legacyManifestDetails;
1779
2016
  if (legacyManifestInfo !== undefined) {
2017
+ legacyManifestExists = true;
1780
2018
  if (!legacyManifestInfo.isFile() || legacyManifestInfo.isSymbolicLink()) {
1781
2019
  return { status: "error", details: ["Legacy continuation store manifest must be a regular file, not a symlink."] };
1782
2020
  }
@@ -1785,7 +2023,7 @@ async function inspectContinuationState(stateDir) {
1785
2023
  return { status: "error", details: [manifestSecurityError] };
1786
2024
  let legacyManifest;
1787
2025
  try {
1788
- legacyManifest = JSON.parse(await readFile(legacyManifestPath, "utf8"));
2026
+ legacyManifest = JSON.parse(await readBoundedOwnerOnlyFile(legacyManifestPath, MAX_MANIFEST_BYTES, "Continuation v2 manifest"));
1789
2027
  }
1790
2028
  catch (error) {
1791
2029
  return { status: "error", details: [`Legacy continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
@@ -1793,22 +2031,71 @@ async function inspectContinuationState(stateDir) {
1793
2031
  if (!isContinuationStoreManifest(legacyManifest, 2, "per-record-v2")) {
1794
2032
  return { status: "error", details: ["Legacy continuation store manifest has an unsupported or malformed schema."] };
1795
2033
  }
1796
- const legacyRecordsDirectory = join(stateDir, "records-v2");
1797
- 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"), {
1798
2051
  allowV2RollbackGuard: true,
1799
2052
  });
1800
- if (recordsDirectoryError !== undefined)
1801
- return { status: "error", details: [recordsDirectoryError] };
1802
- const transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"));
1803
- if (!transaction.valid)
1804
- return { status: "error", details: [transaction.detail] };
1805
- const rollbackGuard = await inspectContinuationRollbackGuard(join(legacyRecordsDirectory, CONTINUATION_V2_ROLLBACK_GUARD), false);
1806
- if (!rollbackGuard.valid)
1807
- 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) {
1808
2095
  const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
1809
2096
  if (!owner.valid)
1810
2097
  return { status: "error", details: [owner.detail] };
1811
- 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);
1812
2099
  return { status: "waiting", details };
1813
2100
  }
1814
2101
  const storePath = join(stateDir, "continuations-v1.json");
@@ -1834,7 +2121,7 @@ async function inspectContinuationState(stateDir) {
1834
2121
  }
1835
2122
  let parsed;
1836
2123
  try {
1837
- parsed = JSON.parse(await readFile(storePath, "utf8"));
2124
+ parsed = JSON.parse(await readBoundedOwnerOnlyFile(storePath, MAX_LEGACY_STORE_BYTES, "Continuation legacy store"));
1838
2125
  }
1839
2126
  catch (error) {
1840
2127
  return { status: "error", details: [`Continuation ledger contains invalid JSON: ${continuationReason(error)}`] };
@@ -1936,6 +2223,14 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
1936
2223
  const securityError = continuationOwnershipError(recordInfo, "record", 0o600);
1937
2224
  if (securityError !== undefined)
1938
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
+ }
1939
2234
  }
1940
2235
  }
1941
2236
  catch (error) {
@@ -1943,7 +2238,247 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
1943
2238
  }
1944
2239
  return undefined;
1945
2240
  }
1946
- 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.") {
1947
2482
  let info;
1948
2483
  try {
1949
2484
  info = await lstat(path);
@@ -1952,12 +2487,12 @@ async function inspectContinuationRollbackGuard(path, required) {
1952
2487
  if (continuationFsCode(error) === "ENOENT") {
1953
2488
  return required
1954
2489
  ? { valid: false, detail: "Continuation v2 rollback guard is missing from the v3 store." }
1955
- : { valid: true, detail: "The v2 rollback guard is not installed; the current runtime will install it during v3 migration." };
2490
+ : { valid: true, detail: missingDetail };
1956
2491
  }
1957
2492
  return { valid: false, detail: `Continuation v2 rollback guard cannot be inspected: ${continuationReason(error)}` };
1958
2493
  }
1959
- if (!info.isFile() || info.isSymbolicLink()) {
1960
- 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." };
1961
2496
  }
1962
2497
  const securityError = continuationOwnershipError(info, "v2 rollback guard", 0o600);
1963
2498
  if (securityError !== undefined)
@@ -1967,7 +2502,7 @@ async function inspectContinuationRollbackGuard(path, required) {
1967
2502
  }
1968
2503
  let contents;
1969
2504
  try {
1970
- contents = await readFile(path, "utf8");
2505
+ contents = await readBoundedOwnerOnlyFile(path, 4 * 1024, "Continuation v2 rollback guard");
1971
2506
  }
1972
2507
  catch (error) {
1973
2508
  return { valid: false, detail: `Continuation v2 rollback guard cannot be read: ${continuationReason(error)}` };
@@ -1980,26 +2515,62 @@ async function inspectContinuationRollbackGuard(path, required) {
1980
2515
  detail: "The owner-only v2 rollback guard prevents older runtimes from opening stale continuation records.",
1981
2516
  };
1982
2517
  }
1983
- async function inspectContinuationTransaction(path) {
2518
+ async function inspectContinuationTransaction(path, expectedSchemaVersion) {
2519
+ const versionLabel = `v${String(expectedSchemaVersion)}`;
1984
2520
  let info;
1985
2521
  try {
1986
2522
  info = await lstat(path);
1987
2523
  }
1988
2524
  catch (error) {
1989
2525
  return continuationFsCode(error) === "ENOENT"
1990
- ? { valid: true, pending: false, detail: "No interrupted durable transaction is awaiting recovery." }
1991
- : { 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
+ };
1992
2532
  }
1993
- if (!info.isFile() || info.isSymbolicLink()) {
1994
- 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
+ };
1995
2539
  }
1996
- const securityError = continuationOwnershipError(info, "transaction", 0o600);
2540
+ const securityError = continuationOwnershipError(info, `${versionLabel} transaction`, 0o600);
1997
2541
  if (securityError !== undefined)
1998
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
+ }
1999
2569
  return {
2000
2570
  valid: true,
2001
2571
  pending: true,
2002
- 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.`,
2003
2574
  };
2004
2575
  }
2005
2576
  function continuationOwnershipError(info, label, expectedMode) {
@@ -2012,14 +2583,14 @@ function continuationOwnershipError(info, label, expectedMode) {
2012
2583
  return undefined;
2013
2584
  }
2014
2585
  function isContinuationLedger(value) {
2015
- return isDoctorObject(value)
2016
- && value.schemaVersion === CONTINUATION_STORE_SCHEMA_VERSION
2017
- && isDoctorObject(value.records);
2586
+ return isStoreFile(value);
2018
2587
  }
2019
2588
  function isContinuationStoreManifest(value, schemaVersion, format) {
2020
2589
  if (!isDoctorObject(value)
2021
2590
  || value.schemaVersion !== schemaVersion
2022
- || typeof value.generation !== "string"
2591
+ || !isDurableGeneration(value.generation)
2592
+ || !requiredDate(value.updatedAt)
2593
+ || (value.rollbackGuardRequired !== undefined && typeof value.rollbackGuardRequired !== "boolean")
2023
2594
  || !isDoctorObject(value.stats)
2024
2595
  || value.stats.format !== format
2025
2596
  || !isDoctorObject(value.stats.limits))
@@ -2364,7 +2935,7 @@ async function exporterSection(input, liveness) {
2364
2935
  */
2365
2936
  async function probeExporterEndpoint(endpoint) {
2366
2937
  const ctrl = new AbortController();
2367
- const timer = setTimeout(() => { ctrl.abort(); }, BUJO_PROBE_TIMEOUT_MS);
2938
+ const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
2368
2939
  try {
2369
2940
  const response = await fetch(endpoint, {
2370
2941
  method: "POST",