@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
@@ -1,106 +1,21 @@
1
- import { randomBytes, randomUUID } from "node:crypto";
2
- import { constants as fsConstants } from "node:fs";
3
- import { chmod, lstat, mkdir, open, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
5
- import { AGENT_CONTINUATION_ORIGIN_CONTEXT_MAX_BYTES, AGENT_CONTINUATION_ORIGIN_CONTEXT_MAX_MESSAGE_BYTES, AGENT_CONTINUATION_ORIGIN_CONTEXT_MAX_MESSAGES, assertAgentContinuationOriginContext, } from "@mono-agent/agent-contracts";
6
- import { canonicalContinuationJson, continuationDigest, isContinuationMode, isContinuationState, TERMINAL_CONTINUATION_STATES, } from "./continuations.js";
7
- export const CONTINUATION_STORE_SCHEMA_VERSION = 1;
8
- export const CONTINUATION_RECORD_STORE_SCHEMA_VERSION = 3;
9
- const RECORDS_DIRECTORY = "records-v3";
10
- const TRANSACTION_FILE = "continuation-transaction-v3.json";
11
- const MANIFEST_FILE = "continuation-store-v3.json";
12
- const LEGACY_RECORDS_DIRECTORY = "records-v2";
13
- const LEGACY_TRANSACTION_FILE = "continuation-transaction-v2.json";
14
- const V2_ROLLBACK_GUARD = "UPGRADED-TO-RECORDS-V3";
15
- const ORIGIN_CONTEXT_GROUPS_DIRECTORY = "origin-context-groups-v1";
16
- const OWNER_DATABASE_FILE = "continuations-owner.sqlite";
17
- const ORIGIN_CONTEXTS_DIRECTORY = "origin-context-v1";
18
- const MAX_RECORD_BYTES = 2 * 1024 * 1024;
19
- const MAX_TRANSACTION_BYTES = 16 * 1024 * 1024;
20
- const DEFAULT_TERMINAL_MAX_RECORDS = 50_000;
21
- const DEFAULT_TERMINAL_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1_000;
22
- const DEFAULT_CAPTURED_TEXT_MAX_RECORDS = 1_000;
23
- const DEFAULT_CAPTURED_TEXT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
24
- export const MAX_CONTINUATION_ORIGIN_CONTEXT_BYTES = AGENT_CONTINUATION_ORIGIN_CONTEXT_MAX_BYTES;
25
- export const MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGES = AGENT_CONTINUATION_ORIGIN_CONTEXT_MAX_MESSAGES;
26
- export const MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGE_BYTES = AGENT_CONTINUATION_ORIGIN_CONTEXT_MAX_MESSAGE_BYTES;
27
- export const MAX_CONTINUATION_ORIGIN_CONTEXT_STORE_BYTES = 256 * 1024 * 1024;
28
- /**
29
- * Claim exclusive ownership of a continuation state directory for this process.
30
- * SQLite holds an OS-backed exclusive transaction for the process lifetime.
31
- * There is no stale-path cleanup race: close or process death releases the OS
32
- * lock, and SQLite performs any required journal recovery for the next owner.
33
- */
34
- export async function acquireContinuationStoreLock(stateDir) {
35
- await ensureOwnerOnlyDirectory(stateDir);
36
- const path = join(stateDir, OWNER_DATABASE_FILE);
37
- let database;
38
- try {
39
- if (await continuationPathExists(path)) {
40
- await assertOwnerOnlyRegularFile(path, "Continuation owner database");
41
- }
42
- const { DatabaseSync } = await import("node:sqlite");
43
- database = new DatabaseSync(path, { timeout: 0 });
44
- if (process.platform !== "win32")
45
- await chmod(path, 0o600);
46
- await assertOwnerOnlyRegularFile(path, "Continuation owner database");
47
- database.exec("PRAGMA journal_mode=DELETE; PRAGMA locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE");
48
- database.exec(`
49
- CREATE TABLE IF NOT EXISTS ownership (
50
- id INTEGER PRIMARY KEY CHECK (id = 1),
51
- pid INTEGER NOT NULL,
52
- acquired_at TEXT NOT NULL
53
- );
54
- `);
55
- database.prepare(`
56
- INSERT INTO ownership (id, pid, acquired_at)
57
- VALUES (1, ?, ?)
58
- ON CONFLICT(id) DO UPDATE SET pid = excluded.pid, acquired_at = excluded.acquired_at
59
- `).run(process.pid, new Date().toISOString());
60
- const journalPath = `${path}-journal`;
61
- if (process.platform !== "win32" && await continuationPathExists(journalPath)) {
62
- await chmod(journalPath, 0o600);
63
- }
64
- }
65
- catch (error) {
66
- try {
67
- database?.close();
68
- }
69
- catch { /* best-effort close after failed acquisition */ }
70
- if (isObject(error) && error.code === "ERR_SQLITE_ERROR" && String(error.message).includes("database is locked")) {
71
- throw new Error(`Continuation state is already owned by another live process: ${stateDir}`, { cause: error });
72
- }
73
- throw error;
74
- }
75
- let released = false;
76
- const owner = database;
77
- return {
78
- async release() {
79
- if (released)
80
- return;
81
- released = true;
82
- try {
83
- if (owner.isTransaction)
84
- owner.exec("ROLLBACK");
85
- }
86
- finally {
87
- owner.close();
88
- }
89
- },
90
- };
91
- }
92
- /**
93
- * Open the bounded per-record store. Each continuation has its own atomic 0600
94
- * file, so a result mutates O(1) durable data instead of rewriting every prior
95
- * payload. A tiny write-ahead transaction makes multi-record maintenance
96
- * restart-safe. The legacy v1 monolith is migrated idempotently on first open.
97
- */
1
+ import { randomUUID } from "node:crypto";
2
+ import { readdir, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { assertAgentContinuationOriginContext, } from "@mono-agent/agent-contracts";
5
+ import { canonicalContinuationJson } from "./continuations.js";
6
+ import { OriginContextCorruptionError, applyOriginContextGroupCommit, applyOriginContextGroupCommits, cleanOriginContextGroupTemporaries, loadOriginContextGroupCommit, originContextDigest, originContextStoreBytes, prepareOriginContextGroupCommit, readOriginContextCanonical, referencedOriginContextDigests, releasePendingOriginPin, removeOriginContextGroupCommits, sweepOriginContextBlobs, } from "./continuation-origin-store.js";
7
+ import { continuationPathExists, ensureOwnerOnlyDirectory, readBoundedOwnerOnlyFile, syncDirectory, writeJsonAtomic, writeTextAtomic, } from "./continuation-store-fs.js";
8
+ import { applyRetention, cloneRecord, cloneRecords, continuationStoreStats, isMissing, isOriginContextReference, normalizeLegacyContinuationRecords, replaceRecords, requiredDate, requiredString, resolveRetention, } from "./continuation-store-policy.js";
9
+ import { assertV3Manifest, loadLegacyStore, loadRecordDirectory, mergeMigrationRecords, persistManifest, persistRecordChanges, recoverRecordTransaction, } from "./continuation-store-records.js";
10
+ import { CONTINUATION_RECORD_STORE_SCHEMA_VERSION, LEGACY_RECORDS_DIRECTORY, LEGACY_TRANSACTION_FILE, MANIFEST_FILE, MAX_CONTINUATION_ORIGIN_CONTEXT_BYTES, MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGES, MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGE_BYTES, MAX_CONTINUATION_ORIGIN_CONTEXT_STORE_BYTES, ORIGIN_CONTEXT_GROUPS_DIRECTORY, ORIGIN_CONTEXTS_DIRECTORY, RECORDS_DIRECTORY, TRANSACTION_FILE, V2_ROLLBACK_GUARD, } from "./continuation-store-types.js";
11
+ const V2_ROLLBACK_GUARD_CONTENT = "This state directory uses continuation records v3. Older runtimes must not open records-v2.\n";
12
+ export { acquireContinuationStoreLock, loadOrCreateContinuationSecret, } from "./continuation-store-fs.js";
13
+ export { CONTINUATION_RECORD_STORE_SCHEMA_VERSION, CONTINUATION_STORE_SCHEMA_VERSION, MAX_CONTINUATION_ORIGIN_CONTEXT_BYTES, MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGES, MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGE_BYTES, MAX_CONTINUATION_ORIGIN_CONTEXT_STORE_BYTES, } from "./continuation-store-types.js";
98
14
  export async function openContinuationStore(stateDir, options = {}) {
99
15
  await ensureOwnerOnlyDirectory(stateDir);
100
16
  const recordsDir = join(stateDir, RECORDS_DIRECTORY);
101
17
  await ensureOwnerOnlyDirectory(recordsDir);
102
18
  const legacyRecordsDir = join(stateDir, LEGACY_RECORDS_DIRECTORY);
103
- await ensureOwnerOnlyDirectory(legacyRecordsDir);
104
19
  const originContextsDir = join(stateDir, ORIGIN_CONTEXTS_DIRECTORY);
105
20
  await ensureOwnerOnlyDirectory(originContextsDir);
106
21
  const originContextGroupsDir = join(stateDir, ORIGIN_CONTEXT_GROUPS_DIRECTORY);
@@ -108,52 +23,140 @@ export async function openContinuationStore(stateDir, options = {}) {
108
23
  const transactionPath = join(stateDir, TRANSACTION_FILE);
109
24
  const manifestPath = join(stateDir, MANIFEST_FILE);
110
25
  const legacyPath = join(stateDir, "continuations-v1.json");
26
+ const legacyManifestPath = join(stateDir, "continuation-store-v2.json");
111
27
  const legacyTransactionPath = join(stateDir, LEGACY_TRANSACTION_FILE);
112
28
  const rollbackGuardPath = join(legacyRecordsDir, V2_ROLLBACK_GUARD);
113
29
  const policy = resolveRetention(options.retention);
114
30
  const now = options.now ?? (() => new Date());
115
31
  const manifestExists = await continuationPathExists(manifestPath);
116
- if (manifestExists)
117
- await assertV3Manifest(manifestPath);
118
- // Finish any v2 transaction before installing the rollback guard. Once the
119
- // guard exists, v0.10 fails closed while v3 deliberately leaves the v2/v1
120
- // evidence untouched for audit and manual recovery.
121
- if (!await continuationPathExists(rollbackGuardPath)) {
122
- await recoverRecordTransaction(legacyRecordsDir, legacyTransactionPath, 2);
123
- }
124
- const recoveredGeneration = await recoverRecordTransaction(recordsDir, transactionPath, CONTINUATION_RECORD_STORE_SCHEMA_VERSION);
125
- const records = await loadRecordDirectory(recordsDir);
126
- const beforeOpen = cloneRecords(records);
127
- normalizeLegacyContinuationRecords(records);
128
- if (!manifestExists) {
129
- const migrationSource = await loadRecordDirectory(legacyRecordsDir, new Set([V2_ROLLBACK_GUARD]));
32
+ const manifest = manifestExists ? await assertV3Manifest(manifestPath) : undefined;
33
+ // Fieldless manifests were written by the eager-guard implementation, so
34
+ // they remain permanently fenced. Only a new explicit false opens the one
35
+ // clean rollback window for an empty store.
36
+ const manifestRollbackGuardRequired = manifest === undefined
37
+ ? false
38
+ : manifest.rollbackGuardRequired ?? true;
39
+ let legacyRecordsDirectoryExists = await continuationPathExists(legacyRecordsDir);
40
+ if (legacyRecordsDirectoryExists)
41
+ await ensureOwnerOnlyDirectory(legacyRecordsDir);
42
+ const ensureLegacyRecordsDirectory = async () => {
43
+ if (legacyRecordsDirectoryExists)
44
+ return;
45
+ await ensureOwnerOnlyDirectory(legacyRecordsDir);
46
+ await syncDirectory(stateDir);
47
+ legacyRecordsDirectoryExists = true;
48
+ };
49
+ let rollbackGuardInstalled = legacyRecordsDirectoryExists
50
+ ? await continuationPathExists(rollbackGuardPath)
51
+ : false;
52
+ let rollbackGuardValidated = false;
53
+ const ensureV2RollbackGuard = async () => {
54
+ if (rollbackGuardValidated)
55
+ return;
56
+ await ensureLegacyRecordsDirectory();
57
+ if (!rollbackGuardInstalled) {
58
+ await writeTextAtomic(rollbackGuardPath, V2_ROLLBACK_GUARD_CONTENT, 4 * 1024);
59
+ rollbackGuardInstalled = true;
60
+ }
61
+ const contents = await readBoundedOwnerOnlyFile(rollbackGuardPath, 4 * 1024, "Continuation v2 rollback guard");
62
+ if (contents !== V2_ROLLBACK_GUARD_CONTENT) {
63
+ throw new Error(`Continuation v2 rollback guard contents are invalid: ${rollbackGuardPath}`);
64
+ }
65
+ rollbackGuardValidated = true;
66
+ };
67
+ const isCommittedEntry = (entry) => !(entry.startsWith(".") && entry.endsWith(".tmp"));
68
+ const v3StateExists = await continuationPathExists(transactionPath)
69
+ || (await readdir(recordsDir)).some(isCommittedEntry)
70
+ || (await readdir(originContextGroupsDir)).some(isCommittedEntry);
71
+ if (manifestRollbackGuardRequired) {
72
+ if (!rollbackGuardInstalled) {
73
+ throw new Error(`Continuation v2 rollback guard is missing from activated v3 state: ${rollbackGuardPath}`);
74
+ }
75
+ await ensureV2RollbackGuard();
76
+ }
77
+ else if (v3StateExists) {
78
+ // Preserve recovery of an interrupted first v3 transaction or activation:
79
+ // fence the legacy reader before inspecting or replaying that evidence.
80
+ await ensureV2RollbackGuard();
81
+ }
82
+ const legacyStateExists = legacyRecordsDirectoryExists
83
+ || await continuationPathExists(legacyPath)
84
+ || await continuationPathExists(legacyManifestPath)
85
+ || await continuationPathExists(legacyTransactionPath);
86
+ const migrationRequired = legacyStateExists && !manifestRollbackGuardRequired;
87
+ let migrationSource;
88
+ if (migrationRequired) {
89
+ // A guard can already exist after an interrupted migration. Validate it
90
+ // before touching state, but always finish any admitted v2 transaction:
91
+ // the guard does not itself prove that v2 replay completed.
92
+ if (rollbackGuardInstalled)
93
+ await ensureV2RollbackGuard();
94
+ if (await continuationPathExists(legacyTransactionPath)) {
95
+ await ensureLegacyRecordsDirectory();
96
+ await recoverRecordTransaction(legacyRecordsDir, legacyTransactionPath, 2);
97
+ }
98
+ migrationSource = legacyRecordsDirectoryExists
99
+ ? await loadRecordDirectory(legacyRecordsDir, new Set([V2_ROLLBACK_GUARD]))
100
+ : new Map();
130
101
  normalizeLegacyContinuationRecords(migrationSource);
131
102
  if (await continuationPathExists(legacyPath)) {
132
103
  const legacy = await loadLegacyStore(legacyPath);
133
104
  normalizeLegacyContinuationRecords(legacy);
134
105
  mergeMigrationRecords(migrationSource, legacy, "v1 and v2");
135
106
  }
136
- // Install the old-reader poison before the first v3 record becomes active.
137
- // A crash on either side is restart-safe: v3 repeats a semantic merge, and
138
- // v0.10 cannot start against a stale v2 snapshot.
139
- if (!await continuationPathExists(rollbackGuardPath)) {
140
- await writeTextAtomic(rollbackGuardPath, "This state directory uses continuation records v3. Older runtimes must not open records-v2.\n", 4 * 1024);
141
- }
107
+ }
108
+ const recoveredGeneration = await recoverRecordTransaction(recordsDir, transactionPath, CONTINUATION_RECORD_STORE_SCHEMA_VERSION);
109
+ const records = await loadRecordDirectory(recordsDir);
110
+ const beforeMigration = cloneRecords(records);
111
+ normalizeLegacyContinuationRecords(records);
112
+ if (migrationSource !== undefined) {
142
113
  mergeMigrationRecords(records, migrationSource, "v2 and v3");
143
114
  }
144
- const committedOriginGroups = await applyOriginContextGroupCommits(originContextGroupsDir, records);
145
- applyRetention(records, policy, now());
146
- const migrationGeneration = await persistRecordChanges(recordsDir, transactionPath, beforeOpen, records);
115
+ // Migration has its own durable phase. While the manifest remains false,
116
+ // legacy records are still authoritative and every partially materialized
117
+ // v3 record must remain semantically identical to that normalized source.
118
+ // Retention and activation are v3-only projections: publishing either one
119
+ // before the manifest closes migration would make crash replay compare a
120
+ // projected v3 record with its unprojected legacy source and reject it as a
121
+ // lossy conflict.
122
+ const migrationCompletionRequired = !manifestRollbackGuardRequired
123
+ && (migrationSource !== undefined || v3StateExists || rollbackGuardInstalled);
124
+ const projectionTime = now();
125
+ if (migrationCompletionRequired) {
126
+ // Validate the retention projection before closing migration. This is a
127
+ // read-only draft: normalized legacy records remain the only durable v3
128
+ // state until the manifest commits, but a growing compaction tombstone
129
+ // cannot strand the store behind that completion fence. Activation markers
130
+ // remain the later v3-only durable projection; their publisher preflights
131
+ // exact membership and record bounds before creating the marker.
132
+ const projectionDraft = cloneRecords(records);
133
+ // Temporary marker debris is cleanup-only and therefore excluded from the
134
+ // v3-evidence fence decision. Validate and clean it before publishing the
135
+ // migration fence so an unsafe linked/oversized temporary cannot poison
136
+ // every subsequent restart.
137
+ await cleanOriginContextGroupTemporaries(originContextGroupsDir);
138
+ applyRetention(projectionDraft, policy, projectionTime);
139
+ // The preflight above is non-durable. Fence the v2 reader only after the
140
+ // retention projection is known to be materializable, then publish
141
+ // normalized records as their own migration phase.
142
+ await ensureV2RollbackGuard();
143
+ }
144
+ const migrationGeneration = await persistRecordChanges(recordsDir, transactionPath, beforeMigration, records, ensureV2RollbackGuard);
147
145
  let generation = migrationGeneration ?? recoveredGeneration ?? randomUUID();
148
- await persistManifest(manifestPath, generation, continuationStoreStats(records, policy), now());
146
+ if (migrationCompletionRequired) {
147
+ await persistManifest(manifestPath, generation, continuationStoreStats(records, policy), now(), true);
148
+ }
149
+ const beforeProjections = cloneRecords(records);
150
+ const committedOriginGroups = await applyOriginContextGroupCommits(originContextGroupsDir, records);
151
+ applyRetention(records, policy, projectionTime);
152
+ const projectionGeneration = await persistRecordChanges(recordsDir, transactionPath, beforeProjections, records, ensureV2RollbackGuard);
153
+ if (projectionGeneration !== undefined)
154
+ generation = projectionGeneration;
155
+ await persistManifest(manifestPath, generation, continuationStoreStats(records, policy), now(), rollbackGuardInstalled);
149
156
  await removeOriginContextGroupCommits(originContextGroupsDir, committedOriginGroups);
150
157
  await sweepOriginContextBlobs(originContextsDir, referencedOriginContextDigests(records), new Set());
151
158
  let tail = Promise.resolve();
152
159
  let originTail = Promise.resolve();
153
- // Multiple capabilities from one origin run intentionally stage the same
154
- // content-addressed snapshot concurrently. Track leases, not just presence:
155
- // one failed/finalized caller must not sweep the blob while another caller
156
- // still owns an uncommitted pin for the same digest.
157
160
  const pendingOriginPins = new Map();
158
161
  let poisoned;
159
162
  async function withOriginLock(operation) {
@@ -189,11 +192,11 @@ export async function openContinuationStore(stateDir, options = {}) {
189
192
  }
190
193
  try {
191
194
  applyRetention(draft, policy, now());
192
- const committedGeneration = await persistRecordChanges(recordsDir, transactionPath, before, draft);
195
+ const committedGeneration = await persistRecordChanges(recordsDir, transactionPath, before, draft, ensureV2RollbackGuard);
193
196
  if (committedGeneration !== undefined)
194
197
  generation = committedGeneration;
195
198
  replaceRecords(records, draft);
196
- await persistManifest(manifestPath, generation, continuationStoreStats(records, policy), now());
199
+ await persistManifest(manifestPath, generation, continuationStoreStats(records, policy), now(), rollbackGuardInstalled);
197
200
  await withOriginLock(async () => {
198
201
  await sweepOriginContextBlobs(originContextsDir, referencedOriginContextDigests(records), new Set(pendingOriginPins.keys()));
199
202
  });
@@ -238,6 +241,10 @@ export async function openContinuationStore(stateDir, options = {}) {
238
241
  commit = prepareOriginContextGroupCommit(draft, input);
239
242
  if (commit === undefined)
240
243
  return;
244
+ // Preflight the exact projection, including v3 record-size bounds,
245
+ // before the durable marker becomes the recovery commit point.
246
+ applyOriginContextGroupCommit(draft, commit);
247
+ await ensureV2RollbackGuard();
241
248
  markerPath = join(originContextGroupsDir, `${commit.groupKey}.json`);
242
249
  if (await continuationPathExists(markerPath)) {
243
250
  const existing = await loadOriginContextGroupCommit(markerPath);
@@ -246,27 +253,19 @@ export async function openContinuationStore(stateDir, options = {}) {
246
253
  }
247
254
  }
248
255
  else {
249
- // The marker is the semantic commit point. It is deliberately compact:
250
- // the member-set digest makes one fsync atomic for groups whose record
251
- // materialization spans arbitrarily many bounded transaction batches.
252
256
  await writeJsonAtomic(markerPath, commit, true, 64 * 1024);
253
257
  }
254
258
  published = true;
255
- applyOriginContextGroupCommit(draft, commit);
256
259
  replaceRecords(records, draft);
257
260
  try {
258
- const committedGeneration = await persistRecordChanges(recordsDir, transactionPath, before, draft);
261
+ const committedGeneration = await persistRecordChanges(recordsDir, transactionPath, before, draft, ensureV2RollbackGuard);
259
262
  if (committedGeneration !== undefined)
260
263
  generation = committedGeneration;
261
- await persistManifest(manifestPath, generation, continuationStoreStats(records, policy), now());
264
+ await persistManifest(manifestPath, generation, continuationStoreStats(records, policy), now(), rollbackGuardInstalled);
262
265
  await rm(markerPath, { force: true });
263
266
  await syncDirectory(originContextGroupsDir);
264
267
  }
265
268
  catch (materializationError) {
266
- // Publication already committed. Recover the bounded materialization if
267
- // possible and keep the group marker as the restart-time source of
268
- // truth. Never report an ambiguous activation failure to a caller that
269
- // might then abandon an already-published group.
270
269
  try {
271
270
  const recovered = await recoverRecordTransaction(recordsDir, transactionPath, CONTINUATION_RECORD_STORE_SCHEMA_VERSION);
272
271
  if (recovered !== undefined)
@@ -288,9 +287,6 @@ export async function openContinuationStore(stateDir, options = {}) {
288
287
  catch (error) {
289
288
  if (!published)
290
289
  throw error;
291
- // The fsynced marker means activation is no longer ambiguous. Preserve
292
- // that success contract and force a restart for any unexpected local
293
- // maintenance failure after the commit point.
294
290
  poisoned ??= error;
295
291
  }
296
292
  finally {
@@ -332,15 +328,21 @@ export async function openContinuationStore(stateDir, options = {}) {
332
328
  throw new Error(`Continuation origin context exceeds its ${String(MAX_CONTINUATION_ORIGIN_CONTEXT_BYTES)} byte limit.`);
333
329
  }
334
330
  const digest = originContextDigest(canonical);
335
- const reference = { schemaVersion: 1, digest, bytes, messageCount: snapshot.messages.length };
331
+ const reference = {
332
+ schemaVersion: 1,
333
+ digest,
334
+ bytes,
335
+ messageCount: snapshot.messages.length,
336
+ };
336
337
  pendingOriginPins.set(digest, (pendingOriginPins.get(digest) ?? 0) + 1);
337
338
  try {
338
339
  await withOriginLock(async () => {
339
340
  const path = join(originContextsDir, `${digest}.json`);
340
341
  if (await continuationPathExists(path)) {
341
342
  const existing = await readOriginContextCanonical(path, reference);
342
- if (existing !== canonical)
343
+ if (existing !== canonical) {
343
344
  throw new Error("Continuation origin context digest collision or content conflict.");
345
+ }
344
346
  return;
345
347
  }
346
348
  const aggregate = await originContextStoreBytes(originContextsDir);
@@ -389,910 +391,4 @@ export async function openContinuationStore(stateDir, options = {}) {
389
391
  mutate: locked,
390
392
  };
391
393
  }
392
- /** Persistent owner-only key used to derive restart-stable callback capabilities. */
393
- export async function loadOrCreateContinuationSecret(stateDir) {
394
- await ensureOwnerOnlyDirectory(stateDir);
395
- const path = join(stateDir, "continuation-secret");
396
- try {
397
- const info = await lstat(path);
398
- if (!info.isFile() || info.isSymbolicLink())
399
- throw new Error(`Continuation secret is not a regular file: ${path}`);
400
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
401
- throw new Error(`Continuation secret is not owned by the current user: ${path}`);
402
- }
403
- if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
404
- throw new Error(`Continuation secret permissions are not owner-only: ${path}`);
405
- }
406
- const encoded = (await readFile(path, "utf8")).trim();
407
- const secret = Buffer.from(encoded, "base64url");
408
- if (secret.length !== 32)
409
- throw new Error(`Continuation secret has invalid contents: ${path}`);
410
- return secret;
411
- }
412
- catch (error) {
413
- if (!isMissing(error))
414
- throw error;
415
- }
416
- const secret = randomBytes(32);
417
- try {
418
- await writeFile(path, `${secret.toString("base64url")}\n`, { flag: "wx", mode: 0o600 });
419
- return secret;
420
- }
421
- catch (error) {
422
- if (!isObject(error) || error.code !== "EEXIST")
423
- throw error;
424
- return await loadOrCreateContinuationSecret(stateDir);
425
- }
426
- }
427
- async function ensureOwnerOnlyDirectory(path) {
428
- await mkdir(path, { recursive: true, mode: 0o700 });
429
- const info = await lstat(path);
430
- if (!info.isDirectory() || info.isSymbolicLink()) {
431
- throw new Error(`Continuation state path is not a real directory: ${path}`);
432
- }
433
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
434
- throw new Error(`Continuation state directory is not owned by the current user: ${path}`);
435
- }
436
- if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
437
- await chmod(path, 0o700);
438
- const repaired = await lstat(path);
439
- if ((repaired.mode & 0o077) !== 0) {
440
- throw new Error(`Continuation state directory permissions are not owner-only: ${path}`);
441
- }
442
- }
443
- }
444
- async function loadLegacyStore(path) {
445
- let raw;
446
- try {
447
- const info = await lstat(path);
448
- if (!info.isFile() || info.isSymbolicLink()) {
449
- throw new Error(`Continuation store is not a regular file: ${path}`);
450
- }
451
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
452
- throw new Error(`Continuation store is not owned by the current user: ${path}`);
453
- }
454
- if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
455
- throw new Error(`Continuation store permissions are not owner-only: ${path}`);
456
- }
457
- raw = await readFile(path, "utf8");
458
- }
459
- catch (error) {
460
- if (isMissing(error))
461
- return new Map();
462
- throw error;
463
- }
464
- let parsed;
465
- try {
466
- parsed = JSON.parse(raw);
467
- }
468
- catch (error) {
469
- throw new Error(`Continuation store contains invalid JSON: ${path}`, { cause: error });
470
- }
471
- if (!isStoreFile(parsed)) {
472
- throw new Error(`Continuation store has an unsupported or malformed schema: ${path}`);
473
- }
474
- return new Map(Object.entries(parsed.records).map(([id, record]) => [id, record]));
475
- }
476
- async function loadRecordDirectory(path, ignoredEntries = new Set()) {
477
- const records = new Map();
478
- let removedTemporary = false;
479
- for (const entry of await readdir(path, { withFileTypes: true })) {
480
- const filePath = join(path, entry.name);
481
- if (ignoredEntries.has(entry.name)) {
482
- await assertOwnerOnlyRegularFile(filePath, "Continuation migration guard");
483
- continue;
484
- }
485
- if (entry.name.startsWith(".") && entry.name.endsWith(".tmp")) {
486
- if (!entry.isFile() || entry.isSymbolicLink()) {
487
- throw new Error(`Continuation temporary record is not a regular file: ${filePath}`);
488
- }
489
- await rm(filePath, { force: true });
490
- removedTemporary = true;
491
- continue;
492
- }
493
- if (!entry.name.endsWith(".json")) {
494
- throw new Error(`Unexpected entry in continuation record directory: ${filePath}`);
495
- }
496
- await assertOwnerOnlyRegularFile(filePath, "Continuation record");
497
- const raw = await readBoundedOwnerOnlyFile(filePath, MAX_RECORD_BYTES, "Continuation record");
498
- let value;
499
- try {
500
- value = JSON.parse(raw);
501
- }
502
- catch (error) {
503
- throw new Error(`Continuation record contains invalid JSON: ${filePath}`, { cause: error });
504
- }
505
- if (!isObject(value) || !requiredString(value.continuationId) || !isRecord(value, value.continuationId)) {
506
- throw new Error(`Continuation record has a malformed schema: ${filePath}`);
507
- }
508
- const expectedName = continuationRecordFileName(value.continuationId);
509
- if (entry.name !== expectedName) {
510
- throw new Error(`Continuation record filename does not match its id: ${filePath}`);
511
- }
512
- if (records.has(value.continuationId)) {
513
- throw new Error(`Duplicate continuation record: ${value.continuationId}`);
514
- }
515
- records.set(value.continuationId, structuredClone(value));
516
- }
517
- if (removedTemporary)
518
- await syncDirectory(path);
519
- return records;
520
- }
521
- function mergeMigrationRecords(target, source, label) {
522
- // Both sides must be normalized before the semantic comparison. Otherwise a
523
- // crash after persisting defaults (for example synthesisDeferrals=0) makes a
524
- // restart falsely report a migration conflict against the equivalent v1/v2
525
- // representation that omitted those fields.
526
- normalizeLegacyContinuationRecords(target);
527
- normalizeLegacyContinuationRecords(source);
528
- for (const [id, record] of source) {
529
- const current = target.get(id);
530
- if (current === undefined) {
531
- target.set(id, structuredClone(record));
532
- }
533
- else if (canonicalContinuationJson(current) !== canonicalContinuationJson(record)) {
534
- throw new Error(`${label} continuation records conflict for id ${id}; refusing lossy migration.`);
535
- }
536
- }
537
- }
538
- async function assertV3Manifest(path) {
539
- const raw = await readBoundedOwnerOnlyFile(path, 1024 * 1024, "Continuation v3 manifest");
540
- let value;
541
- try {
542
- value = JSON.parse(raw);
543
- }
544
- catch (error) {
545
- throw new Error(`Continuation v3 manifest contains invalid JSON: ${path}`, { cause: error });
546
- }
547
- if (!isObject(value)
548
- || value.schemaVersion !== CONTINUATION_RECORD_STORE_SCHEMA_VERSION
549
- || !requiredString(value.generation)
550
- || !requiredDate(value.updatedAt)
551
- || !isObject(value.stats)) {
552
- throw new Error(`Continuation v3 manifest has a malformed schema: ${path}`);
553
- }
554
- }
555
- function prepareOriginContextGroupCommit(records, input) {
556
- const seeds = [...records.values()].filter((record) => record.claimFingerprint === input.claimFingerprint);
557
- if (seeds.length === 0)
558
- return undefined;
559
- const seed = seeds[0];
560
- if (seed.originContextState === "detached_latest")
561
- return undefined;
562
- if (seed.historyBoundary === undefined) {
563
- throw new Error("A pinned continuation origin group must have an immutable history boundary.");
564
- }
565
- const candidates = [...records.values()].filter((record) => record.originRunId === seed.originRunId
566
- && record.originConversationId === seed.originConversationId
567
- && record.historyBoundary === seed.historyBoundary
568
- && !TERMINAL_CONTINUATION_STATES.has(record.state));
569
- if (candidates.length === 0)
570
- return undefined;
571
- const digests = new Set();
572
- for (const record of candidates) {
573
- if ((record.originContextState !== "pending" && record.originContextState !== "pinned")
574
- || record.originContextRef === undefined
575
- || record.originContextDigest !== record.originContextRef.digest
576
- || record.originContextBindingMac === undefined) {
577
- throw new Error("Continuation origin context was not durably prepared for activation.");
578
- }
579
- digests.add(record.originContextRef.digest);
580
- }
581
- if (digests.size !== 1) {
582
- throw new Error("Continuation origin claims were prepared with conflicting snapshots.");
583
- }
584
- if (candidates.every((record) => record.originContextState === "pinned"))
585
- return undefined;
586
- const snapshotDigest = [...digests][0];
587
- if (snapshotDigest === undefined)
588
- throw new Error("Continuation origin group has no snapshot digest.");
589
- const memberIds = candidates.map((record) => record.continuationId).sort();
590
- const groupIdentity = {
591
- originRunId: seed.originRunId,
592
- originConversationId: seed.originConversationId,
593
- historyBoundary: seed.historyBoundary,
594
- };
595
- return {
596
- schemaVersion: 1,
597
- groupKey: continuationDigest(`mono-agent-origin-context-group-v1\0${canonicalContinuationJson(groupIdentity)}`),
598
- ...groupIdentity,
599
- snapshotDigest,
600
- memberCount: memberIds.length,
601
- memberSetDigest: continuationDigest(`mono-agent-origin-context-members-v1\0${canonicalContinuationJson(memberIds)}`),
602
- activatedAt: input.activatedAt,
603
- };
604
- }
605
- function applyOriginContextGroupCommit(records, commit) {
606
- const candidates = [...records.values()].filter((record) => record.originRunId === commit.originRunId
607
- && record.originConversationId === commit.originConversationId
608
- && record.historyBoundary === commit.historyBoundary
609
- && !TERMINAL_CONTINUATION_STATES.has(record.state));
610
- const memberIds = candidates.map((record) => record.continuationId).sort();
611
- const memberSetDigest = continuationDigest(`mono-agent-origin-context-members-v1\0${canonicalContinuationJson(memberIds)}`);
612
- if (candidates.length !== commit.memberCount || memberSetDigest !== commit.memberSetDigest) {
613
- throw new Error("Continuation origin-context group commit member set does not match durable records.");
614
- }
615
- for (const record of candidates) {
616
- if ((record.originContextState !== "pending" && record.originContextState !== "pinned")
617
- || record.originContextRef?.digest !== commit.snapshotDigest
618
- || record.originContextDigest !== commit.snapshotDigest
619
- || record.originContextBindingMac === undefined) {
620
- throw new Error("Continuation origin-context group commit does not match its prepared records.");
621
- }
622
- }
623
- for (const record of candidates) {
624
- if (record.originContextState === "pinned")
625
- continue;
626
- record.originContextState = "pinned";
627
- record.updatedAt = commit.activatedAt;
628
- if (record.lastError?.code === "origin_context_pending")
629
- delete record.lastError;
630
- delete record.nextAttemptAt;
631
- }
632
- }
633
- async function applyOriginContextGroupCommits(directory, records) {
634
- const applied = [];
635
- let removedTemporary = false;
636
- for (const entry of await readdir(directory, { withFileTypes: true })) {
637
- const path = join(directory, entry.name);
638
- if (entry.name.startsWith(".") && entry.name.endsWith(".tmp")) {
639
- if (!entry.isFile() || entry.isSymbolicLink()) {
640
- throw new Error(`Continuation origin-context group temporary is not a regular file: ${path}`);
641
- }
642
- await rm(path, { force: true });
643
- removedTemporary = true;
644
- continue;
645
- }
646
- if (!/^[a-f0-9]{64}\.json$/u.test(entry.name) || !entry.isFile() || entry.isSymbolicLink()) {
647
- throw new Error(`Unexpected entry in continuation origin-context group directory: ${path}`);
648
- }
649
- const commit = await loadOriginContextGroupCommit(path);
650
- if (`${commit.groupKey}.json` !== entry.name) {
651
- throw new Error(`Continuation origin-context group filename does not match its key: ${path}`);
652
- }
653
- applyOriginContextGroupCommit(records, commit);
654
- applied.push(path);
655
- }
656
- if (removedTemporary)
657
- await syncDirectory(directory);
658
- return applied;
659
- }
660
- async function loadOriginContextGroupCommit(path) {
661
- const raw = await readBoundedOwnerOnlyFile(path, 64 * 1024, "Continuation origin-context group commit");
662
- let value;
663
- try {
664
- value = JSON.parse(raw);
665
- }
666
- catch (error) {
667
- throw new Error(`Continuation origin-context group commit contains invalid JSON: ${path}`, { cause: error });
668
- }
669
- if (!isOriginContextGroupCommit(value)) {
670
- throw new Error(`Continuation origin-context group commit has a malformed schema: ${path}`);
671
- }
672
- return value;
673
- }
674
- async function removeOriginContextGroupCommits(directory, paths) {
675
- if (paths.length === 0)
676
- return;
677
- for (const path of paths)
678
- await rm(path, { force: true });
679
- await syncDirectory(directory);
680
- }
681
- async function persistRecordChanges(recordsDir, transactionPath, before, after) {
682
- const writes = [...after.values()].filter((record) => {
683
- const prior = before.get(record.continuationId);
684
- return prior === undefined || JSON.stringify(prior) !== JSON.stringify(record);
685
- }).map((record) => structuredClone(record));
686
- const deletes = [...before.keys()].filter((id) => !after.has(id));
687
- if (writes.length === 0 && deletes.length === 0)
688
- return undefined;
689
- let generation;
690
- for (const transaction of createTransactionBatches(writes, deletes)) {
691
- await writeJsonAtomic(transactionPath, transaction, true, MAX_TRANSACTION_BYTES);
692
- await applyRecordTransaction(recordsDir, transaction);
693
- await rm(transactionPath, { force: true });
694
- await syncDirectory(dirname(transactionPath));
695
- generation = transaction.generation;
696
- }
697
- return generation;
698
- }
699
- async function recoverRecordTransaction(recordsDir, transactionPath, expectedSchemaVersion) {
700
- if (!await continuationPathExists(transactionPath))
701
- return undefined;
702
- const raw = await readBoundedOwnerOnlyFile(transactionPath, MAX_TRANSACTION_BYTES, "Continuation transaction");
703
- let value;
704
- try {
705
- value = JSON.parse(raw);
706
- }
707
- catch (error) {
708
- throw new Error(`Continuation transaction contains invalid JSON: ${transactionPath}`, { cause: error });
709
- }
710
- if (!isRecordTransaction(value, expectedSchemaVersion)) {
711
- throw new Error(`Continuation transaction has a malformed schema: ${transactionPath}`);
712
- }
713
- await applyRecordTransaction(recordsDir, value);
714
- await rm(transactionPath, { force: true });
715
- await syncDirectory(dirname(transactionPath));
716
- return value.generation;
717
- }
718
- async function applyRecordTransaction(recordsDir, transaction) {
719
- for (const record of transaction.writes) {
720
- await writeJsonAtomic(join(recordsDir, continuationRecordFileName(record.continuationId)), record, false, MAX_RECORD_BYTES);
721
- }
722
- for (const id of transaction.deletes) {
723
- await rm(join(recordsDir, continuationRecordFileName(id)), { force: true });
724
- }
725
- await syncDirectory(recordsDir);
726
- }
727
- async function persistManifest(path, generation, stats, now) {
728
- await writeJsonAtomic(path, {
729
- schemaVersion: CONTINUATION_RECORD_STORE_SCHEMA_VERSION,
730
- generation,
731
- updatedAt: now.toISOString(),
732
- stats,
733
- });
734
- }
735
- async function writeJsonAtomic(path, value, syncParent = true, maxBytes = Number.MAX_SAFE_INTEGER) {
736
- const body = `${JSON.stringify(value, null, 2)}\n`;
737
- if (Buffer.byteLength(body, "utf8") > maxBytes) {
738
- throw new Error(`Durable continuation file exceeds its ${String(maxBytes)} byte safety limit: ${path}`);
739
- }
740
- const temporary = join(dirname(path), `.${continuationDigest(path).slice(0, 12)}-${process.pid}-${randomUUID()}.tmp`);
741
- let handle;
742
- try {
743
- handle = await open(temporary, "wx", 0o600);
744
- await handle.writeFile(body, "utf8");
745
- await handle.sync();
746
- await handle.close();
747
- handle = undefined;
748
- await rename(temporary, path);
749
- if (process.platform !== "win32")
750
- await chmod(path, 0o600);
751
- if (syncParent)
752
- await syncDirectory(dirname(path));
753
- }
754
- finally {
755
- await handle?.close().catch(() => undefined);
756
- await rm(temporary, { force: true }).catch(() => undefined);
757
- }
758
- }
759
- async function writeTextAtomic(path, body, maxBytes) {
760
- if (Buffer.byteLength(body, "utf8") > maxBytes) {
761
- throw new Error(`Durable continuation file exceeds its ${String(maxBytes)} byte safety limit: ${path}`);
762
- }
763
- const temporary = join(dirname(path), `.${continuationDigest(path).slice(0, 12)}-${process.pid}-${randomUUID()}.tmp`);
764
- let handle;
765
- try {
766
- handle = await open(temporary, "wx", 0o600);
767
- await handle.writeFile(body, "utf8");
768
- await handle.sync();
769
- await handle.close();
770
- handle = undefined;
771
- await rename(temporary, path);
772
- if (process.platform !== "win32")
773
- await chmod(path, 0o600);
774
- await syncDirectory(dirname(path));
775
- }
776
- finally {
777
- await handle?.close().catch(() => undefined);
778
- await rm(temporary, { force: true }).catch(() => undefined);
779
- }
780
- }
781
- class OriginContextCorruptionError extends Error {
782
- }
783
- function originContextDigest(canonical) {
784
- return continuationDigest(`mono-agent-origin-context-v1\0${canonical}`);
785
- }
786
- async function readOriginContextCanonical(path, reference) {
787
- let canonical;
788
- try {
789
- const loaded = await readBoundedOwnerOnlyFileWithStats(path, MAX_CONTINUATION_ORIGIN_CONTEXT_BYTES, "Continuation origin context");
790
- if (loaded.bytes !== reference.bytes) {
791
- throw new OriginContextCorruptionError("Continuation origin context size does not match its reference.");
792
- }
793
- canonical = loaded.text;
794
- }
795
- catch (error) {
796
- if (error instanceof OriginContextCorruptionError || isMissing(error))
797
- throw error;
798
- throw new OriginContextCorruptionError("Continuation origin context is not a safe owner-only file.", { cause: error });
799
- }
800
- if (Buffer.byteLength(canonical, "utf8") !== reference.bytes
801
- || originContextDigest(canonical) !== reference.digest) {
802
- throw new OriginContextCorruptionError("Continuation origin context digest does not match its reference.");
803
- }
804
- let parsed;
805
- try {
806
- parsed = JSON.parse(canonical);
807
- }
808
- catch (error) {
809
- throw new OriginContextCorruptionError("Continuation origin context is not valid JSON.", { cause: error });
810
- }
811
- if (canonicalContinuationJson(parsed) !== canonical) {
812
- throw new OriginContextCorruptionError("Continuation origin context is not canonically encoded.");
813
- }
814
- try {
815
- assertAgentContinuationOriginContext(parsed);
816
- }
817
- catch (error) {
818
- throw new OriginContextCorruptionError("Continuation origin context has an invalid schema.", { cause: error });
819
- }
820
- if (parsed.messages.length !== reference.messageCount) {
821
- throw new OriginContextCorruptionError("Continuation origin context message count does not match its reference.");
822
- }
823
- return canonical;
824
- }
825
- function referencedOriginContextDigests(records) {
826
- return new Set([...records.values()].flatMap((record) => record.originContextRef === undefined ? [] : [record.originContextRef.digest]));
827
- }
828
- function releasePendingOriginPin(pins, digest) {
829
- const count = pins.get(digest);
830
- if (count === undefined)
831
- return;
832
- if (count <= 1)
833
- pins.delete(digest);
834
- else
835
- pins.set(digest, count - 1);
836
- }
837
- async function sweepOriginContextBlobs(directory, referenced, pending) {
838
- let changed = false;
839
- for (const entry of await readdir(directory, { withFileTypes: true })) {
840
- const path = join(directory, entry.name);
841
- if (entry.name.startsWith(".") && entry.name.endsWith(".tmp")) {
842
- if (!entry.isFile() && !entry.isSymbolicLink()) {
843
- throw new Error(`Continuation origin-context temporary is not a regular file: ${path}`);
844
- }
845
- await rm(path, { force: true });
846
- changed = true;
847
- continue;
848
- }
849
- const match = /^([a-f0-9]{64})\.json$/u.exec(entry.name);
850
- if (match?.[1] !== undefined && !referenced.has(match[1]) && !pending.has(match[1])) {
851
- // Once no durable record references a blob, unlink it without opening or
852
- // following it. This also lets the safe-fallback path clean up a blob
853
- // whose mode/identity was corrupted instead of poisoning record storage.
854
- await rm(path, { force: true, recursive: true });
855
- changed = true;
856
- continue;
857
- }
858
- if (match?.[1] === undefined) {
859
- throw new Error(`Unexpected entry in continuation origin-context directory: ${path}`);
860
- }
861
- // A referenced blob is untrusted payload, not store metadata. Do not let a
862
- // corrupt mode, hard link, symlink, or non-file poison startup; the
863
- // descriptor-stable load path will classify it as unavailable and the
864
- // service can emit its deterministic zero-model fallback.
865
- if (!entry.isFile() || entry.isSymbolicLink())
866
- continue;
867
- try {
868
- await assertOwnerOnlyRegularFile(path, "Continuation origin context");
869
- }
870
- catch {
871
- continue;
872
- }
873
- }
874
- if (changed)
875
- await syncDirectory(directory);
876
- }
877
- async function originContextStoreBytes(directory) {
878
- let total = 0;
879
- for (const entry of await readdir(directory, { withFileTypes: true })) {
880
- if (entry.name.startsWith(".") && entry.name.endsWith(".tmp"))
881
- continue;
882
- if (!/^[a-f0-9]{64}\.json$/u.test(entry.name)) {
883
- throw new Error(`Unexpected entry in continuation origin-context directory: ${join(directory, entry.name)}`);
884
- }
885
- if (!entry.isFile() || entry.isSymbolicLink())
886
- continue;
887
- const info = await lstat(join(directory, entry.name));
888
- total += info.size;
889
- if (total > MAX_CONTINUATION_ORIGIN_CONTEXT_STORE_BYTES) {
890
- throw new Error("Continuation origin context store exceeds its aggregate byte quota.");
891
- }
892
- }
893
- return total;
894
- }
895
- async function syncDirectory(path) {
896
- let directory;
897
- try {
898
- directory = await open(path, "r");
899
- await directory.sync();
900
- }
901
- catch (error) {
902
- if (process.platform === "win32"
903
- && isObject(error)
904
- && (error.code === "EISDIR"
905
- || error.code === "EPERM"
906
- || error.code === "EACCES"
907
- || error.code === "EINVAL"
908
- || error.code === "EBADF")) {
909
- return;
910
- }
911
- throw error;
912
- }
913
- finally {
914
- await directory?.close().catch(() => undefined);
915
- }
916
- }
917
- function createTransactionBatches(writes, deletes) {
918
- const batches = [];
919
- const createdAt = new Date().toISOString();
920
- for (const record of writes) {
921
- const bytes = Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`, "utf8");
922
- if (bytes > MAX_RECORD_BYTES) {
923
- throw new Error(`Continuation record exceeds its ${String(MAX_RECORD_BYTES)} byte safety limit: ${record.continuationId}`);
924
- }
925
- }
926
- const changes = [
927
- ...writes.map((record) => ({ kind: "write", record: structuredClone(record) })),
928
- ...deletes.map((id) => ({ kind: "delete", id })),
929
- ];
930
- const makeTransaction = (entries) => ({
931
- schemaVersion: CONTINUATION_RECORD_STORE_SCHEMA_VERSION,
932
- generation: randomUUID(),
933
- createdAt,
934
- writes: entries.flatMap((entry) => entry.kind === "write" ? [entry.record] : []),
935
- deletes: entries.flatMap((entry) => entry.kind === "delete" ? [entry.id] : []),
936
- });
937
- const appendBounded = (entries) => {
938
- if (entries.length === 0)
939
- return;
940
- const candidate = makeTransaction(entries);
941
- const bytes = Buffer.byteLength(`${JSON.stringify(candidate, null, 2)}\n`, "utf8");
942
- if (bytes <= MAX_TRANSACTION_BYTES) {
943
- batches.push(candidate);
944
- return;
945
- }
946
- if (entries.length === 1) {
947
- const only = entries[0];
948
- throw new Error(only?.kind === "write"
949
- ? `Continuation record is too large for a bounded durable transaction: ${only.record.continuationId}`
950
- : `Continuation id is too large for a bounded durable transaction: ${only?.id ?? "unknown"}`);
951
- }
952
- const middle = Math.floor(entries.length / 2);
953
- appendBounded(entries.slice(0, middle));
954
- appendBounded(entries.slice(middle));
955
- };
956
- // Coarse compact-JSON estimates avoid repeatedly serializing an ever-growing
957
- // migration batch. Exact pretty-JSON size is checked at flush and recursively
958
- // split, so every persisted transaction remains within the hard bound.
959
- const targetBytes = Math.floor(MAX_TRANSACTION_BYTES / 2);
960
- let batch = [];
961
- let estimatedBytes = 512;
962
- for (const change of changes) {
963
- const value = change.kind === "write" ? change.record : change.id;
964
- const estimate = Buffer.byteLength(JSON.stringify(value), "utf8") + 64;
965
- if (batch.length > 0 && estimatedBytes + estimate > targetBytes) {
966
- appendBounded(batch);
967
- batch = [];
968
- estimatedBytes = 512;
969
- }
970
- batch.push(change);
971
- estimatedBytes += estimate;
972
- }
973
- appendBounded(batch);
974
- return batches;
975
- }
976
- function resolveRetention(options) {
977
- const policy = {
978
- terminalMaxRecords: options?.terminalMaxRecords ?? DEFAULT_TERMINAL_MAX_RECORDS,
979
- terminalMaxAgeMs: options?.terminalMaxAgeMs ?? DEFAULT_TERMINAL_MAX_AGE_MS,
980
- capturedTextMaxRecords: options?.capturedTextMaxRecords ?? DEFAULT_CAPTURED_TEXT_MAX_RECORDS,
981
- capturedTextMaxAgeMs: options?.capturedTextMaxAgeMs ?? DEFAULT_CAPTURED_TEXT_MAX_AGE_MS,
982
- };
983
- for (const [name, value] of Object.entries(policy)) {
984
- if (!Number.isSafeInteger(value) || value < 0) {
985
- throw new Error(`Continuation retention ${name} must be a non-negative safe integer.`);
986
- }
987
- }
988
- return policy;
989
- }
990
- const SETTLED_TERMINAL_STATES = new Set([
991
- "delivered",
992
- "expired",
993
- "cancelled",
994
- "dead_lettered",
995
- ]);
996
- const ORIGIN_CONTEXT_SCRUB_STATES = new Set([
997
- "delivery_unknown",
998
- "delivered",
999
- "expired",
1000
- "cancelled",
1001
- "dead_lettered",
1002
- ]);
1003
- function applyRetention(records, policy, now) {
1004
- const nowMs = now.getTime();
1005
- const captures = [...records.values()]
1006
- .filter((record) => SETTLED_TERMINAL_STATES.has(record.state)
1007
- && record.mode === "capture"
1008
- && record.synthesizedText !== undefined
1009
- && nowMs - Date.parse(record.updatedAt) <= policy.capturedTextMaxAgeMs)
1010
- .sort(newestFirst)
1011
- .slice(0, policy.capturedTextMaxRecords);
1012
- const retainedCaptureText = new Set(captures.map((record) => record.continuationId));
1013
- for (const record of records.values()) {
1014
- if (ORIGIN_CONTEXT_SCRUB_STATES.has(record.state) && record.originContextRef !== undefined) {
1015
- record.originContextDigest ??= record.originContextRef.digest;
1016
- record.originContextMessageCount ??= record.originContextRef.messageCount;
1017
- delete record.originContextRef;
1018
- record.originContextState = "scrubbed";
1019
- }
1020
- if (!SETTLED_TERMINAL_STATES.has(record.state))
1021
- continue;
1022
- if (record.resultPayload !== undefined) {
1023
- delete record.resultPayload;
1024
- }
1025
- if (record.synthesizedText !== undefined && !retainedCaptureText.has(record.continuationId)) {
1026
- delete record.synthesizedText;
1027
- }
1028
- if (record.compactedAt === undefined) {
1029
- record.compactedAt = now.toISOString();
1030
- }
1031
- }
1032
- const retainedTerminalIds = new Set([...records.values()]
1033
- .filter((record) => SETTLED_TERMINAL_STATES.has(record.state)
1034
- && nowMs - Date.parse(record.updatedAt) <= policy.terminalMaxAgeMs)
1035
- .sort(newestFirst)
1036
- .slice(0, policy.terminalMaxRecords)
1037
- .map((record) => record.continuationId));
1038
- for (const [id, record] of records) {
1039
- if (SETTLED_TERMINAL_STATES.has(record.state) && !retainedTerminalIds.has(id))
1040
- records.delete(id);
1041
- }
1042
- }
1043
- function newestFirst(left, right) {
1044
- const byUpdatedAt = right.updatedAt.localeCompare(left.updatedAt);
1045
- return byUpdatedAt === 0 ? right.continuationId.localeCompare(left.continuationId) : byUpdatedAt;
1046
- }
1047
- function continuationStoreStats(records, policy) {
1048
- const values = [...records.values()];
1049
- return {
1050
- format: "per-record-v3",
1051
- records: values.length,
1052
- active: values.filter((record) => !SETTLED_TERMINAL_STATES.has(record.state) && record.state !== "delivery_unknown").length,
1053
- unresolvedDelivery: values.filter((record) => record.state === "delivery_unknown").length,
1054
- deadLettered: values.filter((record) => record.state === "dead_lettered").length,
1055
- terminalTombstones: values.filter((record) => SETTLED_TERMINAL_STATES.has(record.state)).length,
1056
- compacted: values.filter((record) => record.compactedAt !== undefined).length,
1057
- capturedText: values.filter((record) => record.mode === "capture" && record.synthesizedText !== undefined).length,
1058
- historyDegraded: values.filter((record) => record.receipt?.historyRecorded === false).length,
1059
- limits: { ...policy },
1060
- };
1061
- }
1062
- function cloneRecords(records) {
1063
- return new Map([...records].map(([id, record]) => [id, structuredClone(record)]));
1064
- }
1065
- function replaceRecords(target, source) {
1066
- target.clear();
1067
- for (const [id, record] of source)
1068
- target.set(id, structuredClone(record));
1069
- }
1070
- function continuationRecordFileName(id) {
1071
- return `${continuationDigest(id)}.json`;
1072
- }
1073
- async function continuationPathExists(path) {
1074
- try {
1075
- await lstat(path);
1076
- return true;
1077
- }
1078
- catch (error) {
1079
- if (isMissing(error))
1080
- return false;
1081
- throw error;
1082
- }
1083
- }
1084
- async function assertOwnerOnlyRegularFile(path, label) {
1085
- const info = await lstat(path);
1086
- assertOwnerOnlySingleLinkStats(info, path, label);
1087
- }
1088
- function assertOwnerOnlySingleLinkStats(info, path, label) {
1089
- if (!info.isFile() || info.isSymbolicLink())
1090
- throw new Error(`${label} is not a regular file: ${path}`);
1091
- if (info.nlink !== 1)
1092
- throw new Error(`${label} must have exactly one filesystem link: ${path}`);
1093
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
1094
- throw new Error(`${label} is not owned by the current user: ${path}`);
1095
- }
1096
- if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
1097
- throw new Error(`${label} permissions are not owner-only: ${path}`);
1098
- }
1099
- }
1100
- async function readBoundedOwnerOnlyFile(path, maxBytes, label) {
1101
- return (await readBoundedOwnerOnlyFileWithStats(path, maxBytes, label)).text;
1102
- }
1103
- async function readBoundedOwnerOnlyFileWithStats(path, maxBytes, label) {
1104
- const pathInfo = await lstat(path);
1105
- assertOwnerOnlySingleLinkStats(pathInfo, path, label);
1106
- const flags = fsConstants.O_RDONLY
1107
- | (process.platform === "win32" ? 0 : fsConstants.O_NOFOLLOW);
1108
- let handle;
1109
- try {
1110
- handle = await open(path, flags);
1111
- const before = await handle.stat();
1112
- assertOwnerOnlySingleLinkStats(before, path, label);
1113
- if (before.dev !== pathInfo.dev || before.ino !== pathInfo.ino) {
1114
- throw new Error(`${label} changed identity while it was opened: ${path}`);
1115
- }
1116
- if (before.size > maxBytes) {
1117
- throw new Error(`${label} exceeds its ${String(maxBytes)} byte safety limit: ${path}`);
1118
- }
1119
- const body = await handle.readFile();
1120
- const after = await handle.stat();
1121
- assertOwnerOnlySingleLinkStats(after, path, label);
1122
- if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size
1123
- || body.byteLength !== before.size) {
1124
- throw new Error(`${label} changed while it was being read: ${path}`);
1125
- }
1126
- return { text: body.toString("utf8"), bytes: body.byteLength };
1127
- }
1128
- finally {
1129
- await handle?.close().catch(() => undefined);
1130
- }
1131
- }
1132
- function isRecordTransaction(value, expectedSchemaVersion) {
1133
- if (!isObject(value)
1134
- || value.schemaVersion !== expectedSchemaVersion
1135
- || !requiredString(value.generation)
1136
- || !requiredDate(value.createdAt)
1137
- || !Array.isArray(value.writes)
1138
- || !Array.isArray(value.deletes))
1139
- return false;
1140
- const writesValid = value.writes.every((record) => isObject(record)
1141
- && requiredString(record.continuationId)
1142
- && isRecord(record, record.continuationId));
1143
- if (!writesValid || !value.deletes.every(requiredString))
1144
- return false;
1145
- const writeIds = value.writes.map((record) => record.continuationId);
1146
- const deleteIds = value.deletes;
1147
- return new Set(writeIds).size === writeIds.length
1148
- && new Set(deleteIds).size === deleteIds.length
1149
- && !deleteIds.some((id) => writeIds.includes(id));
1150
- }
1151
- function isStoreFile(value) {
1152
- if (!isObject(value) || value.schemaVersion !== CONTINUATION_STORE_SCHEMA_VERSION || !isObject(value.records)) {
1153
- return false;
1154
- }
1155
- return Object.entries(value.records).every(([id, record]) => id.length > 0 && isRecord(record, id));
1156
- }
1157
- function normalizeLegacyContinuationRecords(records) {
1158
- for (const record of records.values()) {
1159
- if (record.originContextState === undefined) {
1160
- record.originContextState = record.historyBoundary === undefined
1161
- ? "detached_latest"
1162
- : "legacy_missing";
1163
- }
1164
- if (record.synthesisDeferrals === undefined)
1165
- record.synthesisDeferrals = 0;
1166
- }
1167
- }
1168
- function isRecord(value, id) {
1169
- if (!isObject(value))
1170
- return false;
1171
- return value.continuationId === id
1172
- && requiredString(value.serverName)
1173
- && requiredString(value.originRunId)
1174
- && requiredString(value.originConversationId)
1175
- && optionalString(value.replyToConversationId)
1176
- && optionalString(value.historyBoundary)
1177
- && (value.originContextState === undefined || isOriginContextState(value.originContextState))
1178
- && (value.originContextRef === undefined || isOriginContextReference(value.originContextRef))
1179
- && (value.originContextDigest === undefined || isSha256(value.originContextDigest))
1180
- && (value.originContextMessageCount === undefined
1181
- || (Number.isSafeInteger(value.originContextMessageCount)
1182
- && Number(value.originContextMessageCount) >= 0
1183
- && Number(value.originContextMessageCount) <= MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGES))
1184
- && (value.originContextFingerprint === undefined || isSha256(value.originContextFingerprint))
1185
- && (value.originContextBindingMac === undefined || isSha256(value.originContextBindingMac))
1186
- && (value.completionKind === undefined
1187
- || value.completionKind === "synthesized"
1188
- || value.completionKind === "origin_context_unavailable")
1189
- && isContinuationMode(value.mode)
1190
- && optionalString(value.routeName)
1191
- && requiredString(value.taskKey)
1192
- && requiredString(value.taskHash)
1193
- && requiredString(value.claimFingerprint)
1194
- && /^[a-f0-9]{64}$/u.test(String(value.resultTokenHash))
1195
- && requiredDate(value.createdAt)
1196
- && requiredDate(value.updatedAt)
1197
- && requiredDate(value.deadline)
1198
- && isContinuationState(value.state)
1199
- && Number.isInteger(value.synthesisAttempts)
1200
- && Number(value.synthesisAttempts) >= 0
1201
- && (value.synthesisDeferrals === undefined
1202
- || (Number.isSafeInteger(value.synthesisDeferrals) && Number(value.synthesisDeferrals) >= 0))
1203
- && Number.isInteger(value.deliveryAttempts)
1204
- && Number(value.deliveryAttempts) >= 0
1205
- && optionalString(value.resultIdempotencyKey)
1206
- && optionalString(value.resultPayloadHash)
1207
- && optionalDate(value.synthesisStartedAt)
1208
- && optionalString(value.synthesizedText)
1209
- && (value.actionable === undefined || typeof value.actionable === "boolean")
1210
- && optionalDate(value.deliveryStartedAt)
1211
- && optionalDate(value.nextAttemptAt)
1212
- && optionalString(value.leaseOwner)
1213
- && optionalDate(value.leaseUntil)
1214
- && optionalDate(value.compactedAt)
1215
- && (value.lastError === undefined || isLastError(value.lastError))
1216
- && (value.receipt === undefined || isReceipt(value.receipt));
1217
- }
1218
- function isOriginContextState(value) {
1219
- return value === "pending"
1220
- || value === "pinned"
1221
- || value === "abandoned"
1222
- || value === "detached_latest"
1223
- || value === "legacy_missing"
1224
- || value === "scrubbed";
1225
- }
1226
- function isOriginContextReference(value) {
1227
- return isObject(value)
1228
- && value.schemaVersion === 1
1229
- && isSha256(value.digest)
1230
- && Number.isSafeInteger(value.bytes)
1231
- && Number(value.bytes) > 0
1232
- && Number(value.bytes) <= MAX_CONTINUATION_ORIGIN_CONTEXT_BYTES
1233
- && Number.isSafeInteger(value.messageCount)
1234
- && Number(value.messageCount) >= 2
1235
- && Number(value.messageCount) <= MAX_CONTINUATION_ORIGIN_CONTEXT_MESSAGES;
1236
- }
1237
- function isOriginContextGroupCommit(value) {
1238
- if (!isObject(value)
1239
- || value.schemaVersion !== 1
1240
- || !isSha256(value.groupKey)
1241
- || !requiredString(value.originRunId)
1242
- || !requiredString(value.originConversationId)
1243
- || !requiredString(value.historyBoundary)
1244
- || !isSha256(value.snapshotDigest)
1245
- || !Number.isSafeInteger(value.memberCount)
1246
- || Number(value.memberCount) < 1
1247
- || !isSha256(value.memberSetDigest)
1248
- || !requiredDate(value.activatedAt))
1249
- return false;
1250
- const expectedKey = continuationDigest(`mono-agent-origin-context-group-v1\0${canonicalContinuationJson({
1251
- originRunId: value.originRunId,
1252
- originConversationId: value.originConversationId,
1253
- historyBoundary: value.historyBoundary,
1254
- })}`);
1255
- return value.groupKey === expectedKey;
1256
- }
1257
- function isSha256(value) {
1258
- return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
1259
- }
1260
- function isLastError(value) {
1261
- return isObject(value) && requiredString(value.code) && requiredString(value.reason) && requiredDate(value.at);
1262
- }
1263
- function isReceipt(value) {
1264
- if (!isObject(value)
1265
- || (value.kind !== "delivered" && value.kind !== "suppressed" && value.kind !== "captured" && value.kind !== "silent")) {
1266
- return false;
1267
- }
1268
- return requiredDate(value.deliveredAt)
1269
- && optionalString(value.deliveryId)
1270
- && optionalString(value.channelId)
1271
- && (value.historyRecorded === undefined || typeof value.historyRecorded === "boolean")
1272
- && (value.historyErrorCode === undefined
1273
- || (typeof value.historyErrorCode === "string" && value.historyErrorCode.length > 0 && value.historyErrorCode.length <= 128))
1274
- && (value.historyErrorCode === undefined || value.historyRecorded === false)
1275
- && (value.kind === "delivered" || (value.historyRecorded === undefined && value.historyErrorCode === undefined));
1276
- }
1277
- function isObject(value) {
1278
- return typeof value === "object" && value !== null && !Array.isArray(value);
1279
- }
1280
- function requiredString(value) {
1281
- return typeof value === "string" && value.length > 0;
1282
- }
1283
- function optionalString(value) {
1284
- return value === undefined || typeof value === "string";
1285
- }
1286
- function requiredDate(value) {
1287
- return requiredString(value) && Number.isFinite(Date.parse(value));
1288
- }
1289
- function optionalDate(value) {
1290
- return value === undefined || requiredDate(value);
1291
- }
1292
- function isMissing(error) {
1293
- return isObject(error) && error.code === "ENOENT";
1294
- }
1295
- function cloneRecord(record) {
1296
- return record === undefined ? undefined : structuredClone(record);
1297
- }
1298
394
  //# sourceMappingURL=continuation-store.js.map