@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
@@ -1,58 +1,12 @@
1
- import { createHmac, randomBytes, randomUUID } from "node:crypto";
1
+ import { randomBytes } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { resolve } from "node:path";
4
- import { assertAgentContinuationOriginContext, } from "@mono-agent/agent-contracts";
4
+ import { ContinuationHttpRoutes } from "./continuation-http-routes.js";
5
5
  import { acquireContinuationStoreLock, loadOrCreateContinuationSecret, openContinuationStore, } from "./continuation-store.js";
6
- import { CONTINUATION_CLAIM_TOKEN_ENV, CONTINUATION_CLAIM_TOKEN_HEADER, CONTINUATION_CLAIM_URL_ENV, CONTINUATION_CLAIM_URL_HEADER, CONTINUATION_FINGERPRINT_ENV, CONTINUATION_FINGERPRINT_HEADER, CONTINUATION_MODE_ENV, CONTINUATION_MODE_HEADER, CONTINUATION_STATES, DEFAULT_CONTINUATION_LIMITS, TERMINAL_CONTINUATION_STATES, canonicalContinuationJson, continuationDigest, continuationTokenMatches, normalizeContinuationReplyTarget, } from "./continuations.js";
7
- const DEFAULT_MAX_RESULT_BYTES = 256 * 1024;
8
- const DEFAULT_MAX_DEADLINE_MS = 30 * 24 * 60 * 60 * 1_000;
9
- const DEFAULT_LEASE_MS = 30 * 60 * 1_000;
10
- const DEFAULT_WORKER_INTERVAL_MS = 1_000;
11
- const DEFAULT_DELIVERY_ATTEMPTS = 20;
12
- const MAX_OPERATOR_PAGE_SIZE = 500;
13
- const MAX_CLAIM_BODY_BYTES = 16 * 1024;
14
- const MAX_TASK_KEY_CHARS = 256;
15
- const MAX_TEXT_CHARS = 200_000;
16
- const ORIGIN_CONTEXT_UNAVAILABLE_TEXT = "The background task finished, but I could not safely restore the original conversation context. Please ask me to check the result again.";
17
- /**
18
- * Trusted host signal for the narrow race where readiness disappears after
19
- * preflight but before the model call begins. It is safe to requeue because the
20
- * host must throw it before invoking the responder/model.
21
- */
22
- export class ContinuationSynthesisUnavailableError extends Error {
23
- code;
24
- retryAfterMs;
25
- constructor(code, message, retryAfterMs) {
26
- super(message);
27
- this.name = "ContinuationSynthesisUnavailableError";
28
- this.code = code;
29
- this.retryAfterMs = retryAfterMs;
30
- }
31
- }
32
- class ContinuationServiceStoppingError extends Error {
33
- constructor() {
34
- super("Continuation service is stopping.");
35
- this.name = "ContinuationServiceStoppingError";
36
- }
37
- }
38
- class ContinuationOperationTimeoutError extends Error {
39
- phase;
40
- constructor(phase, timeoutMs) {
41
- super(`Continuation ${phase} exceeded its ${String(timeoutMs)}ms timeout.`);
42
- this.phase = phase;
43
- this.name = "ContinuationOperationTimeoutError";
44
- }
45
- }
46
- export class ContinuationProtocolError extends Error {
47
- status;
48
- code;
49
- constructor(status, code, message) {
50
- super(message);
51
- this.name = "ContinuationProtocolError";
52
- this.status = status;
53
- this.code = code;
54
- }
55
- }
6
+ import { clearLease, closeContinuationServer, errorRecord, formatHost, isLoopbackHost, safeReason, statusOf, statusOfRequired, validateNamedRoutes, } from "./continuation-service-helpers.js";
7
+ import { CONTINUATION_CLAIM_TOKEN_ENV, CONTINUATION_CLAIM_TOKEN_HEADER, CONTINUATION_CLAIM_URL_ENV, CONTINUATION_CLAIM_URL_HEADER, CONTINUATION_FINGERPRINT_ENV, CONTINUATION_FINGERPRINT_HEADER, CONTINUATION_MODE_ENV, CONTINUATION_MODE_HEADER, continuationDigest, normalizeContinuationReplyTarget, } from "./continuations.js";
8
+ export { ContinuationProtocolError, ContinuationSynthesisUnavailableError, } from "./continuation-service-errors.js";
9
+ export { continuationOperatorToken } from "./continuation-service-helpers.js";
56
10
  export async function startContinuationService(options) {
57
11
  const host = options.host ?? "127.0.0.1";
58
12
  if (!isLoopbackHost(host)) {
@@ -78,55 +32,7 @@ export async function startContinuationService(options) {
78
32
  throw error;
79
33
  }
80
34
  }
81
- class ContinuationService {
82
- store;
83
- secret;
84
- options;
85
- releaseStoreLock;
86
- instanceId = randomUUID();
87
- operatorToken;
88
- claimBindings = new Map();
89
- activeClaimBindings = new Set();
90
- namedRoutes;
91
- detachedServiceTokenHashes;
92
- maxResultBytes;
93
- maxDeadlineMs;
94
- leaseMs;
95
- workerIntervalMs;
96
- autoProcess;
97
- limits;
98
- now;
99
- logger;
100
- lifecycleAbort = new AbortController();
101
- inFlight = new Map();
102
- activeHttpRequests = new Set();
103
- activeHandleOperations = 0;
104
- handleDrainPromise;
105
- resolveHandleDrain;
106
- resolvingUnknown = new Set();
107
- dispatchTail = Promise.resolve();
108
- server;
109
- baseUrl = "";
110
- worker;
111
- stopped = false;
112
- stopPromise;
113
- constructor(store, secret, options, releaseStoreLock) {
114
- this.store = store;
115
- this.secret = secret;
116
- this.options = options;
117
- this.releaseStoreLock = releaseStoreLock;
118
- this.namedRoutes = options.namedRoutes ?? {};
119
- this.operatorToken = continuationOperatorToken(secret);
120
- this.detachedServiceTokenHashes = new Map(Object.entries(options.detachedServices ?? {}).map(([name, token]) => [name, continuationDigest(token)]));
121
- this.maxResultBytes = options.maxResultBytes ?? DEFAULT_MAX_RESULT_BYTES;
122
- this.maxDeadlineMs = options.maxDeadlineMs ?? DEFAULT_MAX_DEADLINE_MS;
123
- this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
124
- this.workerIntervalMs = options.workerIntervalMs ?? DEFAULT_WORKER_INTERVAL_MS;
125
- this.autoProcess = options.autoProcess !== false;
126
- this.limits = resolveContinuationLimits(options.limits);
127
- this.now = options.now ?? (() => new Date());
128
- this.logger = options.logger;
129
- }
35
+ class ContinuationService extends ContinuationHttpRoutes {
130
36
  async start(host, port) {
131
37
  const server = createServer((request, response) => {
132
38
  const operation = this.handleRequest(request, response).catch((error) => {
@@ -144,8 +50,9 @@ class ContinuationService {
144
50
  });
145
51
  });
146
52
  const address = server.address();
147
- if (address === null || typeof address === "string")
53
+ if (address === null || typeof address === "string") {
148
54
  throw new Error("Continuation service has no TCP address.");
55
+ }
149
56
  this.baseUrl = `http://${formatHost(host)}:${String(address.port)}`;
150
57
  if (this.autoProcess) {
151
58
  this.worker = setInterval(() => { void this.processDue().catch(() => undefined); }, this.workerIntervalMs);
@@ -182,9 +89,6 @@ class ContinuationService {
182
89
  if (this.stopped)
183
90
  throw new Error("Continuation service is stopped.");
184
91
  const mode = input.mode ?? "reply";
185
- // Request-bound capabilities are always interactive and therefore always
186
- // pin an origin boundary. Context-free detached work has a separate,
187
- // host-authenticated named-route endpoint.
188
92
  const historyBoundary = input.historyBoundary ?? input.runId;
189
93
  const replyToConversationId = input.replyToConversationId
190
94
  ?? (mode === "reply" || mode === "notify_if_actionable"
@@ -248,1113 +152,6 @@ class ContinuationService {
248
152
  },
249
153
  };
250
154
  }
251
- async runHandleOperation(operation) {
252
- if (this.stopped)
253
- throw new Error("Continuation service is stopped.");
254
- this.activeHandleOperations += 1;
255
- try {
256
- return await operation();
257
- }
258
- finally {
259
- this.activeHandleOperations -= 1;
260
- if (this.stopped && this.activeHandleOperations === 0) {
261
- this.resolveHandleDrain?.();
262
- this.resolveHandleDrain = undefined;
263
- this.handleDrainPromise = undefined;
264
- }
265
- }
266
- }
267
- async drainHandleOperations() {
268
- if (this.activeHandleOperations === 0)
269
- return;
270
- this.handleDrainPromise ??= new Promise((resolve) => { this.resolveHandleDrain = resolve; });
271
- await this.handleDrainPromise;
272
- }
273
- async closeClaimBinding(token, binding) {
274
- binding.closed = true;
275
- if (this.claimBindings.get(token) === binding)
276
- this.claimBindings.delete(token);
277
- if (binding.inFlightOperations === 0)
278
- return;
279
- binding.drainPromise ??= new Promise((resolve) => { binding.resolveDrain = resolve; });
280
- await binding.drainPromise;
281
- }
282
- beginClaim(binding) {
283
- if (binding.closed) {
284
- throw new ContinuationProtocolError(401, "invalid_claim_capability", "Invalid or expired claim capability.");
285
- }
286
- binding.inFlightOperations += 1;
287
- let finished = false;
288
- return () => {
289
- if (finished)
290
- return;
291
- finished = true;
292
- binding.inFlightOperations -= 1;
293
- if (binding.closed && binding.inFlightOperations === 0) {
294
- binding.resolveDrain?.();
295
- delete binding.resolveDrain;
296
- delete binding.drainPromise;
297
- }
298
- };
299
- }
300
- async requiresOriginContext(binding) {
301
- if (this.stopped || binding.settled)
302
- return false;
303
- if (!binding.closed) {
304
- throw new Error("Continuation claims must be revoked before origin settlement is inspected.");
305
- }
306
- if (binding.inFlightOperations > 0) {
307
- binding.drainPromise ??= new Promise((resolve) => { binding.resolveDrain = resolve; });
308
- await binding.drainPromise;
309
- }
310
- const finishOperation = this.beginOriginSettlementOperation(binding);
311
- if (finishOperation === undefined)
312
- return false;
313
- try {
314
- if (binding.historyBoundary === undefined) {
315
- this.settleClaimBinding(binding);
316
- return false;
317
- }
318
- const required = (await this.store.list()).some((record) => record.claimFingerprint === binding.fingerprint
319
- && !TERMINAL_CONTINUATION_STATES.has(record.state));
320
- if (!required)
321
- this.settleClaimBinding(binding);
322
- return required;
323
- }
324
- finally {
325
- finishOperation();
326
- }
327
- }
328
- beginOriginSettlementOperation(binding) {
329
- if (this.stopped || binding.settled)
330
- return undefined;
331
- if (!binding.closed) {
332
- throw new Error("Continuation claims must be revoked before origin settlement begins.");
333
- }
334
- if (binding.inFlightOperations !== 0) {
335
- throw new Error("Continuation capability release has not finished draining admitted work.");
336
- }
337
- binding.inFlightOperations += 1;
338
- let finished = false;
339
- return () => {
340
- if (finished)
341
- return;
342
- finished = true;
343
- binding.inFlightOperations -= 1;
344
- if (binding.closed && binding.inFlightOperations === 0) {
345
- binding.resolveDrain?.();
346
- delete binding.resolveDrain;
347
- delete binding.drainPromise;
348
- }
349
- };
350
- }
351
- settleClaimBinding(binding) {
352
- binding.settled = true;
353
- this.activeClaimBindings.delete(binding);
354
- }
355
- async finalizeOriginContext(binding, snapshot) {
356
- const finishOperation = this.beginOriginSettlementOperation(binding);
357
- if (finishOperation === undefined)
358
- return;
359
- try {
360
- if (binding.historyBoundary === undefined)
361
- return;
362
- assertAgentContinuationOriginContext(snapshot);
363
- if (snapshot.conversationId !== binding.originConversationId
364
- || snapshot.originRunId !== binding.originRunId
365
- || snapshot.historyBoundary !== binding.historyBoundary) {
366
- throw new Error("Continuation origin context does not match its immutable claim binding.");
367
- }
368
- const matching = (await this.store.list()).filter((record) => record.claimFingerprint === binding.fingerprint
369
- && !TERMINAL_CONTINUATION_STATES.has(record.state));
370
- if (matching.length === 0)
371
- return;
372
- const pin = await this.store.stageOriginContext(snapshot);
373
- try {
374
- await this.store.mutate((records) => {
375
- const candidates = [...records.values()].filter((record) => record.claimFingerprint === binding.fingerprint
376
- && !TERMINAL_CONTINUATION_STATES.has(record.state));
377
- for (const record of candidates) {
378
- if (record.originContextState === "pinned"
379
- || (record.originContextState === "pending" && record.originContextRef !== undefined)) {
380
- if (record.originContextDigest !== pin.reference.digest
381
- || record.originContextRef?.digest !== pin.reference.digest) {
382
- throw new Error("Continuation origin context conflicts with an existing pinned snapshot.");
383
- }
384
- continue;
385
- }
386
- if (record.originContextState !== "pending") {
387
- throw new Error(`Continuation origin context cannot be finalized from ${record.originContextState}.`);
388
- }
389
- record.originContextRef = pin.reference;
390
- record.originContextDigest = pin.reference.digest;
391
- record.originContextMessageCount = pin.reference.messageCount;
392
- record.originContextFingerprint = continuationDigest(`mono-agent-origin-context-binding-v2\0${record.claimFingerprint}\0${pin.reference.digest}`);
393
- record.originContextBindingMac = originContextBindingMac(this.secret, record, pin.reference);
394
- record.updatedAt = this.now().toISOString();
395
- }
396
- });
397
- }
398
- finally {
399
- await pin.release();
400
- }
401
- }
402
- finally {
403
- finishOperation();
404
- }
405
- }
406
- async activateOriginContext(binding) {
407
- const finishOperation = this.beginOriginSettlementOperation(binding);
408
- if (finishOperation === undefined)
409
- return;
410
- try {
411
- await this.store.activateOriginContextGroup({
412
- claimFingerprint: binding.fingerprint,
413
- activatedAt: this.now().toISOString(),
414
- });
415
- this.settleClaimBinding(binding);
416
- }
417
- finally {
418
- finishOperation();
419
- }
420
- }
421
- async abandonOriginContext(binding) {
422
- const finishOperation = this.beginOriginSettlementOperation(binding);
423
- if (finishOperation === undefined)
424
- return;
425
- const at = this.now().toISOString();
426
- try {
427
- await this.store.mutate((records) => {
428
- for (const record of records.values()) {
429
- if (record.claimFingerprint !== binding.fingerprint
430
- || record.originContextState !== "pending")
431
- continue;
432
- record.originContextState = "abandoned";
433
- delete record.originContextRef;
434
- record.updatedAt = at;
435
- record.lastError = errorRecord("origin_context_unavailable", "The origin run did not commit its pinned continuation context.", at);
436
- delete record.nextAttemptAt;
437
- }
438
- });
439
- this.settleClaimBinding(binding);
440
- if (this.autoProcess)
441
- void this.processDue().catch(() => undefined);
442
- }
443
- finally {
444
- finishOperation();
445
- }
446
- }
447
- async recover(startup = false) {
448
- const now = this.now().getTime();
449
- const current = await this.store.list();
450
- const needsRecovery = current.some((record) => !TERMINAL_CONTINUATION_STATES.has(record.state)
451
- && (startup || !this.inFlight.has(record.continuationId))
452
- && ((startup && record.originContextState === "pending")
453
- ||
454
- (startup && record.leaseOwner !== undefined)
455
- || Date.parse(record.deadline) <= now
456
- || (record.leaseUntil !== undefined && Date.parse(record.leaseUntil) <= now)));
457
- if (!needsRecovery)
458
- return;
459
- await this.store.mutate((records) => {
460
- for (const record of records.values()) {
461
- if (TERMINAL_CONTINUATION_STATES.has(record.state))
462
- continue;
463
- if (startup && record.originContextState === "pending") {
464
- record.originContextState = "abandoned";
465
- delete record.originContextRef;
466
- record.updatedAt = this.now().toISOString();
467
- record.lastError = errorRecord("origin_context_unavailable", "The service restarted before the origin context was pinned.", record.updatedAt);
468
- clearLease(record);
469
- }
470
- // A configured operation timeout may intentionally exceed the lease.
471
- // The process-lifetime store lock prevents another owner, while this
472
- // map proves the current owner is still supervising the attempt.
473
- if (!startup && this.inFlight.has(record.continuationId))
474
- continue;
475
- const abandonedAtStartup = startup && record.leaseOwner !== undefined;
476
- const leaseExpired = record.leaseUntil !== undefined && Date.parse(record.leaseUntil) <= now;
477
- // Once a native send began, absence of a receipt is ambiguous even if
478
- // the wall-clock deadline passed while the process was down.
479
- if (record.deliveryStartedAt !== undefined && (abandonedAtStartup || leaseExpired || Date.parse(record.deadline) <= now)) {
480
- record.state = "delivery_unknown";
481
- record.lastError = errorRecord("delivery_outcome_unknown", "Process stopped after native delivery began; automatic replay is unsafe.", this.now().toISOString());
482
- clearLease(record);
483
- continue;
484
- }
485
- if (Date.parse(record.deadline) <= now) {
486
- expire(record, this.now().toISOString());
487
- continue;
488
- }
489
- if (!abandonedAtStartup && !leaseExpired)
490
- continue;
491
- if (record.state === "synthesizing") {
492
- if (record.synthesizedText !== undefined) {
493
- record.state = "ready_to_deliver";
494
- clearLease(record);
495
- }
496
- else {
497
- record.state = "dead_lettered";
498
- record.lastError = errorRecord("synthesis_outcome_unknown", "Process stopped after synthesis began; synthesis was not repeated.", this.now().toISOString());
499
- clearLease(record);
500
- }
501
- }
502
- else {
503
- clearLease(record);
504
- }
505
- }
506
- });
507
- }
508
- async processDue(limit = this.limits.maxConcurrent) {
509
- if (this.stopped)
510
- return 0;
511
- if (!Number.isSafeInteger(limit) || limit < 0)
512
- throw new Error("Continuation process limit must be a non-negative safe integer.");
513
- const jobs = await this.startDueJobs(limit);
514
- const outcomes = await Promise.all(jobs);
515
- return outcomes.filter(Boolean).length;
516
- }
517
- /**
518
- * Keep dispatch serialized, but never serialize the work itself. A hung
519
- * provider occupies only one bounded worker slot and cannot block unrelated
520
- * continuations from being leased and processed.
521
- */
522
- async startDueJobs(limit) {
523
- const dispatch = this.dispatchTail.then(async () => {
524
- if (this.stopped)
525
- return [];
526
- await this.recover();
527
- if (this.stopped)
528
- return [];
529
- const capacity = Math.max(0, Math.min(limit, this.limits.maxConcurrent - this.inFlight.size));
530
- if (capacity === 0)
531
- return [];
532
- const nowMs = this.now().getTime();
533
- const candidates = (await this.store.list())
534
- .filter((record) => !this.inFlight.has(record.continuationId)
535
- && ((record.state === "result_received" && Date.parse(record.nextAttemptAt ?? record.updatedAt) <= nowMs)
536
- || record.state === "ready_to_deliver"
537
- || (record.state === "delivery_retry" && Date.parse(record.nextAttemptAt ?? record.updatedAt) <= nowMs)))
538
- .sort((left, right) => Date.parse(left.updatedAt) - Date.parse(right.updatedAt))
539
- .slice(0, capacity);
540
- if (this.stopped)
541
- return [];
542
- return candidates.map((candidate) => this.startOne(candidate.continuationId));
543
- });
544
- this.dispatchTail = dispatch.then(() => undefined, () => undefined);
545
- return await dispatch;
546
- }
547
- startOne(id) {
548
- const existing = this.inFlight.get(id);
549
- if (existing !== undefined)
550
- return existing;
551
- const job = this.processOne(id);
552
- this.inFlight.set(id, job);
553
- void job.then(() => { if (this.inFlight.get(id) === job)
554
- this.inFlight.delete(id); }, () => { if (this.inFlight.get(id) === job)
555
- this.inFlight.delete(id); });
556
- return job;
557
- }
558
- async processOne(id) {
559
- this.throwIfStopping();
560
- const leased = await this.acquireLease(id);
561
- if (leased === undefined)
562
- return false;
563
- this.throwIfStopping();
564
- let record = leased;
565
- // Even modes that need no model or channel text must not settle before the
566
- // originating run publishes (or abandons) its context boundary. Otherwise
567
- // a fast silent result can become terminal and poison origin settlement.
568
- if (record.originContextState === "pending") {
569
- await this.deferOriginContext(record.continuationId);
570
- return true;
571
- }
572
- if (record.synthesizedText === undefined && record.mode !== "silent") {
573
- const originContext = await this.resolveOriginContext(record);
574
- if (originContext.kind === "pending") {
575
- await this.deferOriginContext(record.continuationId);
576
- return true;
577
- }
578
- if (originContext.kind === "invalid") {
579
- await this.deadLetter(record.continuationId, originContext.code, "Pinned continuation binding failed integrity verification; native delivery was blocked.");
580
- return true;
581
- }
582
- if (originContext.kind === "unavailable") {
583
- record = await this.prepareOriginContextFallback(record.continuationId);
584
- }
585
- else {
586
- const available = await this.synthesisAvailable(record, originContext);
587
- if (!available.ready) {
588
- await this.deferSynthesis(record.continuationId, available.code, available.reason, available.retryAfterMs);
589
- return true;
590
- }
591
- record = await this.synthesize(record, originContext);
592
- }
593
- if (record.state !== "ready_to_deliver")
594
- return true;
595
- }
596
- else if (record.state !== "ready_to_deliver") {
597
- record = await this.markReady(record.continuationId);
598
- }
599
- // Synthesis can outlive the task deadline. Persist the expiry decision
600
- // after the model result is stored and immediately before any native side
601
- // effect so a late answer is never posted merely because the original
602
- // lease was acquired in time.
603
- if (await this.expireBeforeDeliveryIfNeeded(record.continuationId)) {
604
- return true;
605
- }
606
- this.throwIfStopping();
607
- if (record.mode === "silent") {
608
- await this.markDelivered(record.continuationId, { kind: "silent", deliveredAt: this.now().toISOString() });
609
- return true;
610
- }
611
- if (record.mode === "capture") {
612
- await this.markDelivered(record.continuationId, { kind: "captured", deliveredAt: this.now().toISOString() });
613
- return true;
614
- }
615
- if (record.mode === "notify_if_actionable" && record.actionable === false) {
616
- await this.markDelivered(record.continuationId, { kind: "suppressed", deliveredAt: this.now().toISOString() });
617
- return true;
618
- }
619
- await this.deliver(record);
620
- return true;
621
- }
622
- async synthesisAvailable(record, originContext) {
623
- if (this.options.synthesisPreflight === undefined)
624
- return { ready: true };
625
- try {
626
- return await this.runBoundedOperation("synthesis", Math.min(this.limits.synthesisTimeoutMs, 30_000), async () => await this.options.synthesisPreflight?.(synthesisInput(record, originContext)) ?? { ready: true });
627
- }
628
- catch (error) {
629
- if (error instanceof ContinuationServiceStoppingError)
630
- throw error;
631
- return {
632
- ready: false,
633
- code: error instanceof ContinuationOperationTimeoutError
634
- ? "synthesis_preflight_timeout"
635
- : "synthesis_preflight_failed",
636
- reason: safeReason(error),
637
- };
638
- }
639
- }
640
- async resolveOriginContext(record) {
641
- if (record.originContextState === "detached_latest") {
642
- return { kind: "ready", policy: "detached_latest" };
643
- }
644
- if (record.originContextState === "pending")
645
- return { kind: "pending" };
646
- if (record.originContextState !== "pinned" || record.originContextRef === undefined) {
647
- return { kind: "unavailable" };
648
- }
649
- const expectedFingerprint = continuationDigest(`mono-agent-origin-context-binding-v2\0${record.claimFingerprint}\0${record.originContextRef.digest}`);
650
- const expectedMac = originContextBindingMac(this.secret, record, record.originContextRef);
651
- if (record.originContextDigest !== record.originContextRef.digest
652
- || record.originContextFingerprint !== expectedFingerprint
653
- || record.originContextBindingMac !== expectedMac) {
654
- return { kind: "invalid", code: "origin_context_binding_invalid" };
655
- }
656
- let snapshot;
657
- try {
658
- snapshot = await this.store.loadOriginContext(record.originContextRef);
659
- }
660
- catch {
661
- // Filesystem identity/permission failures are indistinguishable from a
662
- // corrupt snapshot at this trust boundary. Fail closed into the same
663
- // deterministic no-model delivery instead of retrying or dead-lettering.
664
- await this.markOriginContextUnavailable(record.continuationId, "origin_context_unreadable");
665
- return { kind: "unavailable" };
666
- }
667
- if (snapshot === undefined
668
- || snapshot.conversationId !== record.originConversationId
669
- || snapshot.originRunId !== record.originRunId
670
- || snapshot.historyBoundary !== record.historyBoundary) {
671
- await this.markOriginContextUnavailable(record.continuationId, "origin_context_missing_or_corrupt");
672
- return { kind: "unavailable" };
673
- }
674
- return { kind: "ready", policy: "pinned", snapshot };
675
- }
676
- async markOriginContextUnavailable(id, code) {
677
- await this.store.mutate((records) => {
678
- const current = requireRecord(records, id);
679
- requireLease(current, this.instanceId);
680
- current.originContextState = "abandoned";
681
- delete current.originContextRef;
682
- current.updatedAt = this.now().toISOString();
683
- current.lastError = errorRecord(code, "Pinned origin context is unavailable.", current.updatedAt);
684
- });
685
- }
686
- async deferOriginContext(id) {
687
- return await this.store.mutate((records) => {
688
- const current = requireRecord(records, id);
689
- requireLease(current, this.instanceId);
690
- current.synthesisDeferrals += 1;
691
- const at = this.now();
692
- current.state = "result_received";
693
- current.updatedAt = at.toISOString();
694
- current.nextAttemptAt = new Date(at.getTime() + backoffMs(current.synthesisDeferrals, 1_000, 5 * 60 * 1_000)).toISOString();
695
- current.lastError = errorRecord("origin_context_pending", "The origin run has not committed its pinned context yet.", current.updatedAt);
696
- clearLease(current);
697
- return structuredClone(current);
698
- });
699
- }
700
- async prepareOriginContextFallback(id) {
701
- return await this.store.mutate((records) => {
702
- const current = requireRecord(records, id);
703
- requireLease(current, this.instanceId);
704
- current.synthesizedText = ORIGIN_CONTEXT_UNAVAILABLE_TEXT;
705
- current.completionKind = "origin_context_unavailable";
706
- current.state = "ready_to_deliver";
707
- current.updatedAt = this.now().toISOString();
708
- delete current.nextAttemptAt;
709
- delete current.synthesisStartedAt;
710
- return structuredClone(current);
711
- });
712
- }
713
- async deferSynthesis(id, code, reason, retryAfterMs = 1_000, consumedAttempt = false) {
714
- return await this.store.mutate((records) => {
715
- const current = requireRecord(records, id);
716
- requireLease(current, this.instanceId);
717
- const at = this.now();
718
- current.state = "result_received";
719
- current.updatedAt = at.toISOString();
720
- current.nextAttemptAt = new Date(at.getTime() + Math.max(0, retryAfterMs)).toISOString();
721
- current.lastError = errorRecord(code, reason, current.updatedAt);
722
- delete current.synthesisStartedAt;
723
- if (consumedAttempt)
724
- current.synthesisAttempts = Math.max(0, current.synthesisAttempts - 1);
725
- clearLease(current);
726
- return structuredClone(current);
727
- });
728
- }
729
- async expireBeforeDeliveryIfNeeded(id) {
730
- const now = this.now();
731
- return await this.store.mutate((records) => {
732
- const current = requireRecord(records, id);
733
- requireLease(current, this.instanceId);
734
- if (Date.parse(current.deadline) > now.getTime())
735
- return false;
736
- expire(current, now.toISOString());
737
- return true;
738
- });
739
- }
740
- async acquireLease(id) {
741
- const now = this.now();
742
- return await this.store.mutate((records) => {
743
- const record = records.get(id);
744
- if (record === undefined || TERMINAL_CONTINUATION_STATES.has(record.state))
745
- return undefined;
746
- if (record.leaseUntil !== undefined && Date.parse(record.leaseUntil) > now.getTime())
747
- return undefined;
748
- if (Date.parse(record.deadline) <= now.getTime()) {
749
- expire(record, now.toISOString());
750
- return undefined;
751
- }
752
- record.leaseOwner = this.instanceId;
753
- record.leaseUntil = new Date(now.getTime() + this.leaseMs).toISOString();
754
- record.updatedAt = now.toISOString();
755
- return structuredClone(record);
756
- });
757
- }
758
- async synthesize(record, originContext) {
759
- const startedAt = this.now().toISOString();
760
- const prepared = await this.store.mutate((records) => {
761
- this.throwIfStopping();
762
- const current = requireRecord(records, record.continuationId);
763
- requireLease(current, this.instanceId);
764
- current.state = "synthesizing";
765
- current.synthesisAttempts += 1;
766
- current.synthesisStartedAt = startedAt;
767
- current.updatedAt = startedAt;
768
- delete current.nextAttemptAt;
769
- return structuredClone(current);
770
- });
771
- try {
772
- const result = await this.runBoundedOperation("synthesis", this.limits.synthesisTimeoutMs, async () => await this.options.synthesize(synthesisInput(prepared, originContext)));
773
- this.throwIfStopping();
774
- const text = result.text.trim();
775
- if (text.length === 0 || text.length > MAX_TEXT_CHARS) {
776
- throw new Error(text.length === 0 ? "Synthesis returned empty text." : "Synthesis exceeded the 200,000 character limit.");
777
- }
778
- return await this.store.mutate((records) => {
779
- const current = requireRecord(records, prepared.continuationId);
780
- requireLease(current, this.instanceId);
781
- current.synthesizedText = text;
782
- current.completionKind = "synthesized";
783
- if (result.actionable !== undefined)
784
- current.actionable = result.actionable;
785
- current.state = "ready_to_deliver";
786
- current.updatedAt = this.now().toISOString();
787
- delete current.lastError;
788
- delete current.synthesisStartedAt;
789
- return structuredClone(current);
790
- });
791
- }
792
- catch (error) {
793
- if (error instanceof ContinuationServiceStoppingError)
794
- throw error;
795
- if (error instanceof ContinuationSynthesisUnavailableError) {
796
- return await this.deferSynthesis(prepared.continuationId, error.code, error.message, error.retryAfterMs, true);
797
- }
798
- return await this.store.mutate((records) => {
799
- const current = requireRecord(records, prepared.continuationId);
800
- requireLease(current, this.instanceId);
801
- const at = this.now().toISOString();
802
- current.lastError = errorRecord(error instanceof ContinuationOperationTimeoutError
803
- ? "synthesis_timeout_outcome_unknown"
804
- : "synthesis_failed", safeReason(error), at);
805
- current.updatedAt = at;
806
- clearLease(current);
807
- // At-most-once model execution: a thrown/ambiguous synthesis is terminal.
808
- // Only delivery may retry, and only after text has been durably persisted.
809
- current.state = "dead_lettered";
810
- return structuredClone(current);
811
- });
812
- }
813
- }
814
- async markReady(id) {
815
- return await this.store.mutate((records) => {
816
- const record = requireRecord(records, id);
817
- requireLease(record, this.instanceId);
818
- record.state = "ready_to_deliver";
819
- record.updatedAt = this.now().toISOString();
820
- return structuredClone(record);
821
- });
822
- }
823
- async deliver(record) {
824
- if (record.replyToConversationId === undefined || record.synthesizedText === undefined) {
825
- await this.deadLetter(record.continuationId, "missing_delivery_binding", "Continuation has no bound native destination or synthesized text.");
826
- return;
827
- }
828
- const started = await this.store.mutate((records) => {
829
- this.throwIfStopping();
830
- const current = requireRecord(records, record.continuationId);
831
- requireLease(current, this.instanceId);
832
- current.deliveryAttempts += 1;
833
- const at = this.now().toISOString();
834
- current.deliveryStartedAt = at;
835
- current.updatedAt = at;
836
- return structuredClone(current);
837
- });
838
- const conversationId = started.replyToConversationId;
839
- const text = started.synthesizedText;
840
- if (conversationId === undefined || text === undefined) {
841
- await this.deadLetter(started.continuationId, "missing_delivery_binding", "Continuation delivery binding disappeared before native delivery.");
842
- return;
843
- }
844
- let result;
845
- try {
846
- result = await this.runBoundedOperation("delivery", this.limits.deliveryTimeoutMs, async () => await this.options.deliver({
847
- continuationId: started.continuationId,
848
- conversationId,
849
- text,
850
- deliveryKey: `continuation:${started.continuationId}`,
851
- }));
852
- this.throwIfStopping();
853
- }
854
- catch (error) {
855
- if (error instanceof ContinuationServiceStoppingError)
856
- throw error;
857
- result = {
858
- kind: "unknown",
859
- code: error instanceof ContinuationOperationTimeoutError
860
- ? "delivery_timeout_outcome_unknown"
861
- : "delivery_threw",
862
- reason: safeReason(error),
863
- };
864
- }
865
- await this.store.mutate((records) => {
866
- const current = requireRecord(records, started.continuationId);
867
- requireLease(current, this.instanceId);
868
- const at = this.now().toISOString();
869
- current.updatedAt = at;
870
- clearLease(current);
871
- if (result.kind === "delivered") {
872
- current.state = "delivered";
873
- delete current.deliveryStartedAt;
874
- delete current.nextAttemptAt;
875
- delete current.lastError;
876
- current.receipt = {
877
- kind: "delivered",
878
- deliveredAt: at,
879
- ...(result.deliveryId === undefined ? {} : { deliveryId: result.deliveryId }),
880
- ...(result.channelId === undefined ? {} : { channelId: result.channelId }),
881
- ...(result.historyRecorded === undefined ? {} : { historyRecorded: result.historyRecorded }),
882
- ...(result.historyRecorded !== false || result.historyErrorCode === undefined
883
- ? {}
884
- : { historyErrorCode: boundedHistoryErrorCode(result.historyErrorCode) }),
885
- };
886
- }
887
- else if (result.kind === "retryable" && current.deliveryAttempts < DEFAULT_DELIVERY_ATTEMPTS) {
888
- current.state = "delivery_retry";
889
- delete current.deliveryStartedAt;
890
- current.lastError = errorRecord(result.code, result.reason, at);
891
- const delay = result.retryAfterMs ?? backoffMs(current.deliveryAttempts, 1_000, 60 * 60 * 1_000);
892
- current.nextAttemptAt = new Date(this.now().getTime() + delay).toISOString();
893
- }
894
- else if (result.kind === "unknown") {
895
- current.state = "delivery_unknown";
896
- current.lastError = errorRecord(result.code, result.reason, at);
897
- }
898
- else {
899
- current.state = "dead_lettered";
900
- delete current.deliveryStartedAt;
901
- const code = result.kind === "retryable" ? "delivery_attempts_exhausted" : result.code;
902
- current.lastError = errorRecord(code, result.reason, at);
903
- }
904
- });
905
- }
906
- async markDelivered(id, receipt) {
907
- await this.store.mutate((records) => {
908
- const record = requireRecord(records, id);
909
- requireLease(record, this.instanceId);
910
- record.state = "delivered";
911
- record.receipt = receipt;
912
- record.updatedAt = receipt.deliveredAt;
913
- delete record.nextAttemptAt;
914
- delete record.lastError;
915
- clearLease(record);
916
- });
917
- }
918
- async deadLetter(id, code, reason) {
919
- await this.store.mutate((records) => {
920
- const record = requireRecord(records, id);
921
- record.state = "dead_lettered";
922
- record.updatedAt = this.now().toISOString();
923
- record.lastError = errorRecord(code, reason, record.updatedAt);
924
- clearLease(record);
925
- });
926
- }
927
- async retry(id, options) {
928
- const record = await this.store.mutate((records) => {
929
- const current = requireRecord(records, id);
930
- if (current.state === "delivery_unknown" && options?.allowUnknown !== true) {
931
- throw new ContinuationProtocolError(409, "delivery_unknown", "Resolve the ambiguous delivery before retrying.");
932
- }
933
- if (current.state !== "delivery_retry" && current.state !== "dead_lettered" && current.state !== "delivery_unknown") {
934
- throw new ContinuationProtocolError(409, "not_retryable", `Continuation state ${current.state} is not retryable.`);
935
- }
936
- if (current.synthesizedText === undefined && current.mode !== "silent") {
937
- throw new ContinuationProtocolError(409, "synthesis_not_retryable", "Continuation synthesis has no persisted output and cannot be repeated.");
938
- }
939
- current.state = "ready_to_deliver";
940
- current.nextAttemptAt = this.now().toISOString();
941
- delete current.deliveryStartedAt;
942
- delete current.lastError;
943
- current.updatedAt = this.now().toISOString();
944
- clearLease(current);
945
- return structuredClone(current);
946
- });
947
- if (this.autoProcess)
948
- void this.processDue().catch(() => undefined);
949
- return statusOfRequired(record);
950
- }
951
- async cancel(id) {
952
- const record = await this.store.mutate((records) => {
953
- const current = requireRecord(records, id);
954
- if (TERMINAL_CONTINUATION_STATES.has(current.state)) {
955
- if (current.state === "cancelled")
956
- return structuredClone(current);
957
- throw new ContinuationProtocolError(409, "already_terminal", `Continuation is already ${current.state}.`);
958
- }
959
- if (current.leaseOwner !== undefined || current.state === "synthesizing" || current.deliveryStartedAt !== undefined) {
960
- throw new ContinuationProtocolError(409, "continuation_in_flight", "Continuation has an active synthesis or native delivery; cancellation cannot make that side effect disappear.");
961
- }
962
- current.state = "cancelled";
963
- current.updatedAt = this.now().toISOString();
964
- current.lastError = errorRecord("cancelled_by_operator", "Continuation cancelled by the operator.", current.updatedAt);
965
- clearLease(current);
966
- return structuredClone(current);
967
- });
968
- return statusOfRequired(record);
969
- }
970
- async resolveUnknown(id, outcome) {
971
- if (this.resolvingUnknown.has(id)) {
972
- throw new ContinuationProtocolError(409, "resolution_in_progress", "This ambiguous delivery is already being resolved.");
973
- }
974
- this.resolvingUnknown.add(id);
975
- try {
976
- const current = await this.store.get(id);
977
- if (current === undefined)
978
- throw new ContinuationProtocolError(404, "not_found", "Continuation not found.");
979
- if (current.state !== "delivery_unknown") {
980
- throw new ContinuationProtocolError(409, "not_delivery_unknown", "Only delivery_unknown continuations can be resolved.");
981
- }
982
- let history;
983
- if (outcome.kind === "delivered") {
984
- if (this.options.recordHistory === undefined
985
- || current.replyToConversationId === undefined
986
- || current.synthesizedText === undefined) {
987
- history = { recorded: false, code: "history_record_unavailable_after_ambiguous_delivery" };
988
- }
989
- else {
990
- try {
991
- history = await this.runBoundedOperation("delivery", this.limits.deliveryTimeoutMs, async () => await this.options.recordHistory?.({
992
- continuationId: current.continuationId,
993
- conversationId: current.replyToConversationId,
994
- text: current.synthesizedText,
995
- deliveryKey: `continuation:${current.continuationId}`,
996
- }) ?? { recorded: false, code: "history_record_unavailable_after_ambiguous_delivery" });
997
- }
998
- catch (error) {
999
- if (error instanceof ContinuationServiceStoppingError)
1000
- throw error;
1001
- history = {
1002
- recorded: false,
1003
- code: error instanceof ContinuationOperationTimeoutError
1004
- ? "history_record_timeout_after_ambiguous_delivery"
1005
- : "history_record_failed_after_ambiguous_delivery",
1006
- };
1007
- }
1008
- }
1009
- }
1010
- const record = await this.store.mutate((records) => {
1011
- const mutable = requireRecord(records, id);
1012
- if (mutable.state !== "delivery_unknown") {
1013
- throw new ContinuationProtocolError(409, "not_delivery_unknown", "Only delivery_unknown continuations can be resolved.");
1014
- }
1015
- const at = this.now().toISOString();
1016
- mutable.updatedAt = at;
1017
- delete mutable.deliveryStartedAt;
1018
- if (outcome.kind === "delivered") {
1019
- const recorded = history?.recorded === true;
1020
- mutable.state = "delivered";
1021
- mutable.receipt = {
1022
- kind: "delivered",
1023
- deliveredAt: at,
1024
- ...(outcome.deliveryId === undefined ? {} : { deliveryId: outcome.deliveryId }),
1025
- historyRecorded: recorded,
1026
- ...(recorded ? {} : {
1027
- historyErrorCode: boundedHistoryErrorCode(history?.recorded === false
1028
- ? history.code
1029
- : "history_record_unavailable_after_ambiguous_delivery"),
1030
- }),
1031
- };
1032
- delete mutable.lastError;
1033
- }
1034
- else if (outcome.kind === "not_delivered") {
1035
- mutable.state = "ready_to_deliver";
1036
- mutable.nextAttemptAt = at;
1037
- delete mutable.lastError;
1038
- }
1039
- else {
1040
- mutable.state = "dead_lettered";
1041
- mutable.lastError = errorRecord("operator_dead_lettered", "Ambiguous delivery was dead-lettered by the operator.", at);
1042
- }
1043
- return structuredClone(mutable);
1044
- });
1045
- if (outcome.kind === "not_delivered" && this.autoProcess)
1046
- void this.processDue().catch(() => undefined);
1047
- return statusOfRequired(record);
1048
- }
1049
- finally {
1050
- this.resolvingUnknown.delete(id);
1051
- }
1052
- }
1053
- async health() {
1054
- const [records, storage] = await Promise.all([this.store.list(), this.store.stats()]);
1055
- const counts = Object.fromEntries(CONTINUATION_STATES.map((state) => [state, 0]));
1056
- let oldestPendingAt;
1057
- let due = 0;
1058
- const now = this.now().getTime();
1059
- for (const record of records) {
1060
- counts[record.state] += 1;
1061
- if (!TERMINAL_CONTINUATION_STATES.has(record.state)) {
1062
- if (oldestPendingAt === undefined || record.createdAt < oldestPendingAt)
1063
- oldestPendingAt = record.createdAt;
1064
- if (record.nextAttemptAt === undefined || Date.parse(record.nextAttemptAt) <= now)
1065
- due += 1;
1066
- }
1067
- }
1068
- const pending = records.length - [...TERMINAL_CONTINUATION_STATES].reduce((sum, state) => sum + counts[state], 0);
1069
- const status = counts.delivery_unknown > 0 || counts.dead_lettered > 0
1070
- ? "unhealthy"
1071
- : pending > 0 || counts.expired > 0 || storage.historyDegraded > 0
1072
- ? "degraded"
1073
- : "healthy";
1074
- return {
1075
- status,
1076
- checkedAt: this.now().toISOString(),
1077
- counts,
1078
- pending,
1079
- due,
1080
- storage,
1081
- ...(oldestPendingAt === undefined ? {} : { oldestPendingAt }),
1082
- };
1083
- }
1084
- async handleRequest(request, response) {
1085
- response.setHeader("cache-control", "no-store");
1086
- response.setHeader("x-content-type-options", "nosniff");
1087
- if (this.stopped) {
1088
- throw new ContinuationProtocolError(503, "service_stopping", "Continuation service is stopping.");
1089
- }
1090
- const url = new URL(request.url ?? "/", this.baseUrl);
1091
- if (request.method === "POST" && url.pathname === "/v1/continuations/claim") {
1092
- const token = requireBearer(request);
1093
- const binding = this.claimBindings.get(token);
1094
- if (binding === undefined)
1095
- throw new ContinuationProtocolError(401, "invalid_claim_capability", "Invalid or expired claim capability.");
1096
- const body = await readJson(request, MAX_CLAIM_BODY_BYTES);
1097
- // Do not count a slow request body as admitted work. Recheck and enter
1098
- // the drain only after the complete bounded body exists; release() then
1099
- // either revokes it or waits for its durable mutation to finish.
1100
- const finishClaim = this.beginClaim(binding);
1101
- try {
1102
- sendJson(response, 200, await this.claim(binding, body));
1103
- }
1104
- finally {
1105
- finishClaim();
1106
- }
1107
- return;
1108
- }
1109
- if (request.method === "POST" && url.pathname === "/v1/continuations/detached/claim") {
1110
- const serviceName = requireHeader(request, "x-mono-agent-service-name");
1111
- this.authorizeDetached(serviceName, requireBearer(request));
1112
- const body = await readJson(request, MAX_CLAIM_BODY_BYTES);
1113
- sendJson(response, 200, await this.claimDetached(serviceName, body));
1114
- return;
1115
- }
1116
- const resultMatch = /^\/v1\/continuations\/([^/]+)\/result$/u.exec(url.pathname);
1117
- if ((request.method === "PUT" || request.method === "POST") && resultMatch?.[1] !== undefined) {
1118
- const body = await readJson(request, this.maxResultBytes);
1119
- sendJson(response, 202, await this.acceptResult(resultMatch[1], requireBearer(request), body));
1120
- return;
1121
- }
1122
- const statusMatch = /^\/v1\/continuations\/([^/]+)\/status$/u.exec(url.pathname);
1123
- if (request.method === "GET" && statusMatch?.[1] !== undefined) {
1124
- const record = await this.authorizedRecord(statusMatch[1], requireBearer(request));
1125
- sendJson(response, 200, statusOfRequired(record));
1126
- return;
1127
- }
1128
- if (url.pathname === "/v1/operator/continuations") {
1129
- this.authorizeOperator(request);
1130
- if (request.method !== "GET")
1131
- throw new ContinuationProtocolError(405, "method_not_allowed", "Method not allowed.");
1132
- sendJson(response, 200, operatorContinuationPage(await this.store.list(), url, this.limits.operatorPageSize));
1133
- return;
1134
- }
1135
- if (url.pathname === "/v1/operator/health") {
1136
- this.authorizeOperator(request);
1137
- if (request.method !== "GET")
1138
- throw new ContinuationProtocolError(405, "method_not_allowed", "Method not allowed.");
1139
- sendJson(response, 200, await this.health());
1140
- return;
1141
- }
1142
- const operatorMatch = /^\/v1\/operator\/continuations\/([^/]+)\/(retry|cancel|resolve)$/u.exec(url.pathname);
1143
- if (request.method === "POST" && operatorMatch?.[1] !== undefined && operatorMatch[2] !== undefined) {
1144
- this.authorizeOperator(request);
1145
- const body = await readJson(request, MAX_CLAIM_BODY_BYTES);
1146
- if (operatorMatch[2] === "retry") {
1147
- sendJson(response, 200, await this.retry(operatorMatch[1], { allowUnknown: asObject(body).allowUnknown === true }));
1148
- }
1149
- else if (operatorMatch[2] === "cancel") {
1150
- sendJson(response, 200, await this.cancel(operatorMatch[1]));
1151
- }
1152
- else {
1153
- const value = asObject(body);
1154
- const kind = value.kind;
1155
- if (kind !== "delivered" && kind !== "not_delivered" && kind !== "dead_lettered") {
1156
- throw new ContinuationProtocolError(400, "invalid_resolution", "Resolution kind is invalid.");
1157
- }
1158
- sendJson(response, 200, await this.resolveUnknown(operatorMatch[1], {
1159
- kind,
1160
- ...(kind === "delivered" && typeof value.deliveryId === "string" ? { deliveryId: value.deliveryId } : {}),
1161
- }));
1162
- }
1163
- return;
1164
- }
1165
- throw new ContinuationProtocolError(404, "not_found", "Continuation endpoint not found.");
1166
- }
1167
- async claim(binding, body) {
1168
- const claim = parseClaim(body, this.now(), this.maxDeadlineMs);
1169
- return await this.claimBound(binding, claim);
1170
- }
1171
- async claimDetached(serviceName, body) {
1172
- const object = asObject(body);
1173
- const routeName = requiredStringField(object, "route");
1174
- const route = this.namedRoutes[routeName];
1175
- if (route === undefined)
1176
- throw new ContinuationProtocolError(403, "unknown_named_route", "Detached claim route is not configured.");
1177
- const claim = parseClaim(body, this.now(), this.maxDeadlineMs);
1178
- const originConversationId = route.conversationId ?? `continuation-route:${routeName}`;
1179
- const fingerprint = continuationDigest([
1180
- "v1-detached",
1181
- serviceName,
1182
- routeName,
1183
- route.mode,
1184
- route.conversationId ?? "",
1185
- ].join("\0"));
1186
- return await this.claimBound({
1187
- serverName: `detached:${serviceName}`,
1188
- originRunId: `detached:${serviceName}:${claim.taskKey}`,
1189
- originConversationId,
1190
- ...(route.conversationId === undefined ? {} : { replyToConversationId: route.conversationId }),
1191
- mode: route.mode,
1192
- fingerprint,
1193
- closed: false,
1194
- settled: true,
1195
- inFlightOperations: 0,
1196
- }, claim, routeName);
1197
- }
1198
- async claimBound(binding, claim, routeName) {
1199
- let created = false;
1200
- const record = await this.store.mutate((records) => {
1201
- const existing = [...records.values()].find((candidate) => candidate.serverName === binding.serverName
1202
- && candidate.originRunId === binding.originRunId
1203
- && candidate.taskKey === claim.taskKey);
1204
- if (existing !== undefined) {
1205
- if (existing.taskHash !== claim.taskHash
1206
- || existing.claimFingerprint !== binding.fingerprint
1207
- || existing.deadline !== claim.deadline) {
1208
- throw new ContinuationProtocolError(409, "claim_conflict", "taskKey was already claimed with different immutable inputs.");
1209
- }
1210
- return structuredClone(existing);
1211
- }
1212
- const active = [...records.values()].filter((candidate) => !TERMINAL_CONTINUATION_STATES.has(candidate.state));
1213
- if (active.length >= this.limits.maxActiveRecords) {
1214
- throw new ContinuationProtocolError(429, "active_continuation_limit", `The service already has its maximum ${String(this.limits.maxActiveRecords)} active continuations.`);
1215
- }
1216
- const activeForOrigin = active.filter((candidate) => candidate.claimFingerprint === binding.fingerprint).length;
1217
- if (activeForOrigin >= this.limits.maxActivePerOrigin) {
1218
- throw new ContinuationProtocolError(429, "active_origin_limit", `This claim origin already has its maximum ${String(this.limits.maxActivePerOrigin)} active continuations.`);
1219
- }
1220
- const now = this.now().toISOString();
1221
- const continuationId = randomUUID();
1222
- const token = this.deriveResultToken(continuationId, claim.taskHash);
1223
- const next = {
1224
- continuationId,
1225
- serverName: binding.serverName,
1226
- originRunId: binding.originRunId,
1227
- originConversationId: binding.originConversationId,
1228
- ...(binding.replyToConversationId === undefined ? {} : { replyToConversationId: binding.replyToConversationId }),
1229
- ...(binding.historyBoundary === undefined ? {} : { historyBoundary: binding.historyBoundary }),
1230
- originContextState: binding.historyBoundary === undefined ? "detached_latest" : "pending",
1231
- mode: binding.mode,
1232
- ...(routeName === undefined ? {} : { routeName }),
1233
- taskKey: claim.taskKey,
1234
- taskHash: claim.taskHash,
1235
- claimFingerprint: binding.fingerprint,
1236
- resultTokenHash: continuationDigest(token),
1237
- createdAt: now,
1238
- updatedAt: now,
1239
- deadline: claim.deadline,
1240
- state: "claimed",
1241
- synthesisAttempts: 0,
1242
- synthesisDeferrals: 0,
1243
- deliveryAttempts: 0,
1244
- };
1245
- records.set(continuationId, next);
1246
- created = true;
1247
- return structuredClone(next);
1248
- });
1249
- const token = this.deriveResultToken(record.continuationId, record.taskHash);
1250
- return {
1251
- continuationId: record.continuationId,
1252
- resultUrl: `${this.baseUrl}/v1/continuations/${encodeURIComponent(record.continuationId)}/result`,
1253
- statusUrl: `${this.baseUrl}/v1/continuations/${encodeURIComponent(record.continuationId)}/status`,
1254
- token,
1255
- expiresAt: record.deadline,
1256
- fingerprint: record.claimFingerprint,
1257
- ...(created ? {} : { replayed: true }),
1258
- };
1259
- }
1260
- async acceptResult(id, token, body) {
1261
- const object = asObject(body);
1262
- const idempotencyKey = requiredStringField(object, "idempotencyKey", 256);
1263
- if (!("payload" in object))
1264
- throw new ContinuationProtocolError(400, "missing_payload", "Result payload is required.");
1265
- const serialized = canonicalContinuationJson(object.payload);
1266
- const payloadHash = continuationDigest(serialized);
1267
- const providedHash = typeof object.payloadHash === "string" ? object.payloadHash : undefined;
1268
- if (providedHash !== undefined && providedHash !== payloadHash) {
1269
- throw new ContinuationProtocolError(400, "payload_hash_mismatch", "Result payload hash does not match the payload.");
1270
- }
1271
- const now = this.now();
1272
- const record = await this.store.mutate((records) => {
1273
- const current = requireRecord(records, id);
1274
- authorizeRecordToken(current, token);
1275
- if (current.resultIdempotencyKey !== undefined) {
1276
- if (current.resultIdempotencyKey !== idempotencyKey || current.resultPayloadHash !== payloadHash) {
1277
- throw new ContinuationProtocolError(409, "result_conflict", "Continuation already has a different immutable result.");
1278
- }
1279
- return structuredClone(current);
1280
- }
1281
- if (current.state !== "claimed") {
1282
- throw new ContinuationProtocolError(409, "result_not_accepted", `Continuation is ${current.state}.`);
1283
- }
1284
- if (Date.parse(current.deadline) <= now.getTime()) {
1285
- expire(current, now.toISOString());
1286
- throw new ContinuationProtocolError(410, "continuation_expired", "Continuation deadline has passed.");
1287
- }
1288
- current.resultIdempotencyKey = idempotencyKey;
1289
- current.resultPayloadHash = payloadHash;
1290
- current.resultPayload = structuredClone(object.payload);
1291
- current.state = "result_received";
1292
- current.updatedAt = now.toISOString();
1293
- return structuredClone(current);
1294
- });
1295
- if (this.autoProcess)
1296
- void this.processDue().catch(() => undefined);
1297
- return { continuationId: id, state: record.state, accepted: true };
1298
- }
1299
- async authorizedRecord(id, token) {
1300
- const record = await this.store.get(id);
1301
- if (record === undefined)
1302
- throw new ContinuationProtocolError(404, "not_found", "Continuation not found.");
1303
- authorizeRecordToken(record, token);
1304
- return record;
1305
- }
1306
- authorizeDetached(serviceName, token) {
1307
- const expected = this.detachedServiceTokenHashes.get(serviceName);
1308
- if (expected === undefined || !continuationTokenMatches(token, expected)) {
1309
- throw new ContinuationProtocolError(401, "invalid_service_capability", "Invalid detached-service capability.");
1310
- }
1311
- }
1312
- authorizeOperator(request) {
1313
- if (!continuationTokenMatches(requireBearer(request), continuationDigest(this.operatorToken))) {
1314
- throw new ContinuationProtocolError(401, "invalid_operator_capability", "Invalid operator capability.");
1315
- }
1316
- }
1317
- deriveResultToken(id, taskHash) {
1318
- return createHmac("sha256", this.secret).update(`continuation-result\0${id}\0${taskHash}`).digest("base64url");
1319
- }
1320
- handleRequestError(response, error) {
1321
- if (response.headersSent || response.writableEnded)
1322
- return;
1323
- const protocol = error instanceof ContinuationProtocolError
1324
- ? error
1325
- : new ContinuationProtocolError(500, "internal_error", "Continuation request failed.");
1326
- if (!(error instanceof ContinuationProtocolError)) {
1327
- this.logger?.error?.("Continuation request failed.", { reason: safeReason(error) });
1328
- }
1329
- sendJson(response, protocol.status, { error: { code: protocol.code, message: protocol.message } });
1330
- }
1331
- throwIfStopping() {
1332
- if (this.stopped || this.lifecycleAbort.signal.aborted) {
1333
- throw new ContinuationServiceStoppingError();
1334
- }
1335
- }
1336
- async runBoundedOperation(phase, timeoutMs, operation) {
1337
- this.throwIfStopping();
1338
- let timeout;
1339
- let onAbort;
1340
- const timedOut = new Promise((_resolve, reject) => {
1341
- timeout = setTimeout(() => reject(new ContinuationOperationTimeoutError(phase, timeoutMs)), timeoutMs);
1342
- timeout.unref?.();
1343
- });
1344
- const stopped = new Promise((_resolve, reject) => {
1345
- onAbort = () => reject(new ContinuationServiceStoppingError());
1346
- this.lifecycleAbort.signal.addEventListener("abort", onAbort, { once: true });
1347
- });
1348
- try {
1349
- return await Promise.race([Promise.resolve().then(operation), timedOut, stopped]);
1350
- }
1351
- finally {
1352
- if (timeout !== undefined)
1353
- clearTimeout(timeout);
1354
- if (onAbort !== undefined)
1355
- this.lifecycleAbort.signal.removeEventListener("abort", onAbort);
1356
- }
1357
- }
1358
155
  async stop() {
1359
156
  this.stopPromise ??= this.stopOnce();
1360
157
  await this.stopPromise;
@@ -1405,10 +202,6 @@ class ContinuationService {
1405
202
  });
1406
203
  }
1407
204
  catch (error) {
1408
- // A prior durable transaction may already have poisoned this store.
1409
- // Shutdown must still revoke stale closures and release OS ownership;
1410
- // restart recovery deterministically abandons any remaining pending
1411
- // origin records before processing them.
1412
205
  this.logger?.warn?.("Continuation origin settlement will finish during restart recovery.", {
1413
206
  reason: safeReason(error),
1414
207
  });
@@ -1422,313 +215,4 @@ class ContinuationService {
1422
215
  }
1423
216
  }
1424
217
  }
1425
- /** Derive the restart-stable local operator capability from the owner-only service secret. */
1426
- export function continuationOperatorToken(secret) {
1427
- return createHmac("sha256", secret).update("mono-agent-continuation-operator-v1").digest("base64url");
1428
- }
1429
- function parseClaim(body, now, maxDeadlineMs) {
1430
- const object = asObject(body);
1431
- const taskKey = requiredStringField(object, "taskKey", MAX_TASK_KEY_CHARS);
1432
- const taskHash = requiredStringField(object, "taskHash", 256);
1433
- const deadline = requiredStringField(object, "deadline", 64);
1434
- const deadlineMs = Date.parse(deadline);
1435
- if (!Number.isFinite(deadlineMs) || deadlineMs <= now.getTime()) {
1436
- throw new ContinuationProtocolError(400, "invalid_deadline", "Claim deadline must be a future ISO timestamp.");
1437
- }
1438
- if (deadlineMs - now.getTime() > maxDeadlineMs) {
1439
- throw new ContinuationProtocolError(400, "deadline_too_far", "Claim deadline exceeds the configured maximum.");
1440
- }
1441
- return { taskKey, taskHash, deadline: new Date(deadlineMs).toISOString() };
1442
- }
1443
- function statusOf(record) {
1444
- return record === undefined ? undefined : statusOfRequired(record);
1445
- }
1446
- function statusOfRequired(record) {
1447
- return {
1448
- continuationId: record.continuationId,
1449
- state: record.state,
1450
- mode: record.mode,
1451
- taskKey: record.taskKey,
1452
- createdAt: record.createdAt,
1453
- updatedAt: record.updatedAt,
1454
- deadline: record.deadline,
1455
- attempts: { synthesis: record.synthesisAttempts, delivery: record.deliveryAttempts },
1456
- synthesisDeferrals: record.synthesisDeferrals,
1457
- originContext: {
1458
- state: record.originContextState,
1459
- ...(record.originContextDigest === undefined ? {} : { digest: record.originContextDigest }),
1460
- ...(record.originContextMessageCount === undefined ? {} : { messageCount: record.originContextMessageCount }),
1461
- },
1462
- ...(record.completionKind === undefined ? {} : { completionKind: record.completionKind }),
1463
- ...(record.nextAttemptAt === undefined ? {} : { nextAttemptAt: record.nextAttemptAt }),
1464
- ...(record.lastError === undefined ? {} : { lastError: record.lastError }),
1465
- ...(record.receipt === undefined ? {} : { receipt: record.receipt }),
1466
- };
1467
- }
1468
- function requireRecord(records, id) {
1469
- const record = records.get(id);
1470
- if (record === undefined)
1471
- throw new ContinuationProtocolError(404, "not_found", "Continuation not found.");
1472
- return record;
1473
- }
1474
- function synthesisInput(record, originContext) {
1475
- const common = {
1476
- continuationId: record.continuationId,
1477
- originConversationId: record.originConversationId,
1478
- originRunId: record.originRunId,
1479
- ...(record.replyToConversationId === undefined ? {} : { replyToConversationId: record.replyToConversationId }),
1480
- mode: record.mode,
1481
- payload: record.resultPayload,
1482
- };
1483
- if (originContext.policy === "detached_latest") {
1484
- return { ...common, originContextPolicy: "detached_latest" };
1485
- }
1486
- if (record.historyBoundary === undefined) {
1487
- throw new Error(`Pinned continuation ${record.continuationId} is missing its history boundary.`);
1488
- }
1489
- return {
1490
- ...common,
1491
- historyBoundary: record.historyBoundary,
1492
- originContextPolicy: "pinned",
1493
- originContext: originContext.snapshot,
1494
- };
1495
- }
1496
- function originContextBindingMac(secret, record, reference) {
1497
- return createHmac("sha256", secret).update(canonicalContinuationJson({
1498
- version: 2,
1499
- continuationId: record.continuationId,
1500
- claimFingerprint: record.claimFingerprint,
1501
- serverName: record.serverName,
1502
- originRunId: record.originRunId,
1503
- originConversationId: record.originConversationId,
1504
- replyToConversationId: record.replyToConversationId ?? null,
1505
- historyBoundary: record.historyBoundary ?? null,
1506
- mode: record.mode,
1507
- routeName: record.routeName ?? null,
1508
- taskKey: record.taskKey,
1509
- taskHash: record.taskHash,
1510
- resultTokenHash: record.resultTokenHash,
1511
- createdAt: record.createdAt,
1512
- deadline: record.deadline,
1513
- originContextDigest: reference.digest,
1514
- originContextBytes: reference.bytes,
1515
- originContextMessageCount: reference.messageCount,
1516
- })).digest("hex");
1517
- }
1518
- function requireLease(record, owner) {
1519
- if (record.leaseOwner !== owner)
1520
- throw new ContinuationProtocolError(409, "lease_lost", "Continuation processing lease was lost.");
1521
- }
1522
- function clearLease(record) {
1523
- delete record.leaseOwner;
1524
- delete record.leaseUntil;
1525
- }
1526
- function expire(record, at) {
1527
- record.state = "expired";
1528
- record.updatedAt = at;
1529
- record.lastError = errorRecord("deadline_expired", "Continuation deadline passed before delivery completed.", at);
1530
- clearLease(record);
1531
- }
1532
- function errorRecord(code, reason, at) {
1533
- return { code: bounded(code, 128), reason: bounded(reason, 1_000), at };
1534
- }
1535
- function authorizeRecordToken(record, token) {
1536
- if (!continuationTokenMatches(token, record.resultTokenHash)) {
1537
- throw new ContinuationProtocolError(401, "invalid_result_capability", "Invalid continuation result capability.");
1538
- }
1539
- }
1540
- function validateNamedRoutes(routes) {
1541
- for (const [name, route] of Object.entries(routes)) {
1542
- if (name.trim().length === 0 || name.length > 128)
1543
- throw new Error("Continuation route names must be 1-128 characters.");
1544
- if ((route.mode === "notify_if_actionable" || route.mode === "capture") && !route.conversationId?.trim()) {
1545
- throw new Error(`Continuation route ${name} requires conversationId.`);
1546
- }
1547
- if (route.mode === "silent" && route.conversationId !== undefined) {
1548
- throw new Error(`Continuation route ${name} cannot set conversationId for mode ${route.mode}.`);
1549
- }
1550
- }
1551
- }
1552
- function resolveContinuationLimits(input) {
1553
- const maxActiveRecords = input?.maxActiveRecords ?? DEFAULT_CONTINUATION_LIMITS.maxActiveRecords;
1554
- const limits = {
1555
- ...DEFAULT_CONTINUATION_LIMITS,
1556
- ...input,
1557
- maxActiveRecords,
1558
- maxActivePerOrigin: input?.maxActivePerOrigin
1559
- ?? Math.min(DEFAULT_CONTINUATION_LIMITS.maxActivePerOrigin, maxActiveRecords),
1560
- };
1561
- for (const [name, value] of Object.entries(limits)) {
1562
- if (!Number.isSafeInteger(value) || value <= 0) {
1563
- throw new Error(`Continuation limit ${name} must be a positive safe integer.`);
1564
- }
1565
- }
1566
- if (limits.maxActiveRecords > 1_000_000)
1567
- throw new Error("Continuation maxActiveRecords cannot exceed 1000000.");
1568
- if (limits.maxActivePerOrigin > limits.maxActiveRecords) {
1569
- throw new Error("Continuation maxActivePerOrigin cannot exceed maxActiveRecords.");
1570
- }
1571
- if (limits.maxConcurrent > 256)
1572
- throw new Error("Continuation maxConcurrent cannot exceed 256.");
1573
- if (limits.synthesisTimeoutMs > 24 * 60 * 60 * 1_000 || limits.deliveryTimeoutMs > 24 * 60 * 60 * 1_000) {
1574
- throw new Error("Continuation operation timeouts cannot exceed 24 hours.");
1575
- }
1576
- if (limits.operatorPageSize > MAX_OPERATOR_PAGE_SIZE) {
1577
- throw new Error(`Continuation operatorPageSize cannot exceed ${String(MAX_OPERATOR_PAGE_SIZE)}.`);
1578
- }
1579
- return limits;
1580
- }
1581
- function operatorContinuationPage(records, url, configuredPageSize) {
1582
- const rawLimit = url.searchParams.get("limit");
1583
- let limit = configuredPageSize;
1584
- if (rawLimit !== null) {
1585
- if (!/^[1-9][0-9]*$/u.test(rawLimit)) {
1586
- throw new ContinuationProtocolError(400, "invalid_page_limit", "Operator list limit must be a positive integer.");
1587
- }
1588
- limit = Number(rawLimit);
1589
- if (!Number.isSafeInteger(limit) || limit > configuredPageSize || limit > MAX_OPERATOR_PAGE_SIZE) {
1590
- throw new ContinuationProtocolError(400, "invalid_page_limit", `Operator list limit cannot exceed ${String(Math.min(configuredPageSize, MAX_OPERATOR_PAGE_SIZE))}.`);
1591
- }
1592
- }
1593
- const cursor = parseOperatorCursor(url.searchParams.get("cursor"));
1594
- const ordered = [...records].sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.continuationId.localeCompare(left.continuationId));
1595
- const eligible = cursor === undefined
1596
- ? ordered
1597
- : ordered.filter((record) => record.createdAt < cursor.createdAt
1598
- || (record.createdAt === cursor.createdAt && record.continuationId < cursor.continuationId));
1599
- const selected = eligible.slice(0, limit);
1600
- const last = selected.at(-1);
1601
- return {
1602
- continuations: selected.map(statusOfRequired),
1603
- pageSize: limit,
1604
- ...(eligible.length <= limit || last === undefined
1605
- ? {}
1606
- : { nextCursor: encodeOperatorCursor(last) }),
1607
- };
1608
- }
1609
- function encodeOperatorCursor(record) {
1610
- return Buffer.from(JSON.stringify([record.createdAt, record.continuationId]), "utf8").toString("base64url");
1611
- }
1612
- function parseOperatorCursor(value) {
1613
- if (value === null)
1614
- return undefined;
1615
- try {
1616
- if (value.length === 0 || value.length > 512)
1617
- throw new Error("invalid");
1618
- const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
1619
- if (!Array.isArray(decoded)
1620
- || decoded.length !== 2
1621
- || typeof decoded[0] !== "string"
1622
- || !Number.isFinite(Date.parse(decoded[0]))
1623
- || typeof decoded[1] !== "string"
1624
- || decoded[1].length === 0
1625
- || decoded[1].length > 128) {
1626
- throw new Error("invalid");
1627
- }
1628
- return { createdAt: decoded[0], continuationId: decoded[1] };
1629
- }
1630
- catch {
1631
- throw new ContinuationProtocolError(400, "invalid_page_cursor", "Operator list cursor is invalid.");
1632
- }
1633
- }
1634
- async function closeContinuationServer(server) {
1635
- await new Promise((resolveClose, reject) => {
1636
- let forced;
1637
- const finish = (error) => {
1638
- if (forced !== undefined)
1639
- clearTimeout(forced);
1640
- if (error === undefined)
1641
- resolveClose();
1642
- else
1643
- reject(error);
1644
- };
1645
- server.close((error) => finish(error));
1646
- server.closeIdleConnections?.();
1647
- forced = setTimeout(() => {
1648
- // An incomplete/slow request must not hold config reload or process
1649
- // shutdown forever. Result and claim writes are idempotent, so callers
1650
- // can safely retry a connection terminated at this boundary.
1651
- server.closeAllConnections?.();
1652
- }, 5_000);
1653
- });
1654
- }
1655
- function requireBearer(request) {
1656
- const authorization = request.headers.authorization;
1657
- if (typeof authorization !== "string" || !authorization.startsWith("Bearer ")) {
1658
- throw new ContinuationProtocolError(401, "missing_capability", "Bearer capability is required.");
1659
- }
1660
- const token = authorization.slice("Bearer ".length).trim();
1661
- if (token.length === 0 || token.length > 512) {
1662
- throw new ContinuationProtocolError(401, "invalid_capability", "Bearer capability is invalid.");
1663
- }
1664
- return token;
1665
- }
1666
- function requireHeader(request, name) {
1667
- const value = request.headers[name];
1668
- if (typeof value !== "string" || value.trim().length === 0 || value.length > 256) {
1669
- throw new ContinuationProtocolError(400, "missing_service_name", "Detached service name header is required.");
1670
- }
1671
- return value;
1672
- }
1673
- async function readJson(request, maxBytes) {
1674
- const chunks = [];
1675
- let bytes = 0;
1676
- for await (const chunk of request) {
1677
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1678
- bytes += buffer.length;
1679
- if (bytes > maxBytes)
1680
- throw new ContinuationProtocolError(413, "payload_too_large", "Request body exceeds the configured limit.");
1681
- chunks.push(buffer);
1682
- }
1683
- try {
1684
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1685
- }
1686
- catch {
1687
- throw new ContinuationProtocolError(400, "invalid_json", "Request body must be valid JSON.");
1688
- }
1689
- }
1690
- function asObject(value) {
1691
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1692
- throw new ContinuationProtocolError(400, "invalid_body", "Request body must be a JSON object.");
1693
- }
1694
- return value;
1695
- }
1696
- function requiredStringField(object, key, max = 512) {
1697
- const value = object[key];
1698
- if (typeof value !== "string" || value.trim().length === 0 || value.length > max) {
1699
- throw new ContinuationProtocolError(400, `invalid_${key}`, `${key} must be a non-empty string up to ${String(max)} characters.`);
1700
- }
1701
- return value;
1702
- }
1703
- function sendJson(response, status, body) {
1704
- if (response.writableEnded)
1705
- return;
1706
- const encoded = JSON.stringify(body);
1707
- response.statusCode = status;
1708
- response.setHeader("content-type", "application/json; charset=utf-8");
1709
- response.setHeader("content-length", Buffer.byteLength(encoded));
1710
- response.end(encoded);
1711
- }
1712
- function isLoopbackHost(host) {
1713
- const normalized = host.trim().toLowerCase();
1714
- return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
1715
- }
1716
- function formatHost(host) {
1717
- return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
1718
- }
1719
- function backoffMs(attempt, base, cap) {
1720
- const exponential = Math.min(cap, base * (2 ** Math.max(0, attempt - 1)));
1721
- const deterministicJitter = 0.75 + ((attempt * 1103515245 + 12345) % 500) / 1_000;
1722
- return Math.max(base, Math.floor(exponential * deterministicJitter));
1723
- }
1724
- function bounded(value, max) {
1725
- return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
1726
- }
1727
- function safeReason(error) {
1728
- return bounded(error instanceof Error ? error.message : String(error), 1_000);
1729
- }
1730
- function boundedHistoryErrorCode(value) {
1731
- const normalized = value.trim().slice(0, 128);
1732
- return normalized.length === 0 ? "history_record_failed" : normalized;
1733
- }
1734
218
  //# sourceMappingURL=continuation-service.js.map