@mono-agent/agent-app 0.6.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/README.md +259 -19
  2. package/dist/account-home.d.ts +6 -0
  3. package/dist/account-home.d.ts.map +1 -0
  4. package/dist/account-home.js +9 -0
  5. package/dist/account-home.js.map +1 -0
  6. package/dist/adapter-send-proxy.d.ts +18 -0
  7. package/dist/adapter-send-proxy.d.ts.map +1 -0
  8. package/dist/adapter-send-proxy.js +229 -0
  9. package/dist/adapter-send-proxy.js.map +1 -0
  10. package/dist/adapter-send-tools-main.js +62 -4
  11. package/dist/adapter-send-tools-main.js.map +1 -1
  12. package/dist/adapter-send-tools.d.ts +33 -6
  13. package/dist/adapter-send-tools.d.ts.map +1 -1
  14. package/dist/adapter-send-tools.js +485 -87
  15. package/dist/adapter-send-tools.js.map +1 -1
  16. package/dist/app-config.d.ts +3 -1
  17. package/dist/app-config.d.ts.map +1 -1
  18. package/dist/app-config.js +16 -5
  19. package/dist/app-config.js.map +1 -1
  20. package/dist/app.d.ts +27 -0
  21. package/dist/app.d.ts.map +1 -1
  22. package/dist/app.js +761 -147
  23. package/dist/app.js.map +1 -1
  24. package/dist/background-environment.d.ts +16 -0
  25. package/dist/background-environment.d.ts.map +1 -0
  26. package/dist/background-environment.js +68 -0
  27. package/dist/background-environment.js.map +1 -0
  28. package/dist/background-runtime.d.ts +62 -0
  29. package/dist/background-runtime.d.ts.map +1 -0
  30. package/dist/background-runtime.js +1115 -0
  31. package/dist/background-runtime.js.map +1 -0
  32. package/dist/background-snapshot-key.d.ts +13 -0
  33. package/dist/background-snapshot-key.d.ts.map +1 -0
  34. package/dist/background-snapshot-key.js +194 -0
  35. package/dist/background-snapshot-key.js.map +1 -0
  36. package/dist/background-snapshot.d.ts +96 -0
  37. package/dist/background-snapshot.d.ts.map +1 -0
  38. package/dist/background-snapshot.js +450 -0
  39. package/dist/background-snapshot.js.map +1 -0
  40. package/dist/background-worker-lease.d.ts +39 -0
  41. package/dist/background-worker-lease.d.ts.map +1 -0
  42. package/dist/background-worker-lease.js +356 -0
  43. package/dist/background-worker-lease.js.map +1 -0
  44. package/dist/background.d.ts +74 -2
  45. package/dist/background.d.ts.map +1 -1
  46. package/dist/background.js +815 -73
  47. package/dist/background.js.map +1 -1
  48. package/dist/broadcast-recorder.d.ts.map +1 -1
  49. package/dist/broadcast-recorder.js +24 -2
  50. package/dist/broadcast-recorder.js.map +1 -1
  51. package/dist/channel-plugins.d.ts +2 -0
  52. package/dist/channel-plugins.d.ts.map +1 -1
  53. package/dist/channel-plugins.js +8 -0
  54. package/dist/channel-plugins.js.map +1 -1
  55. package/dist/channels.d.ts +10 -0
  56. package/dist/channels.d.ts.map +1 -1
  57. package/dist/channels.js +87 -9
  58. package/dist/channels.js.map +1 -1
  59. package/dist/cli.d.ts +86 -4
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +2421 -143
  62. package/dist/cli.js.map +1 -1
  63. package/dist/config-reference.d.ts.map +1 -1
  64. package/dist/config-reference.js +448 -6
  65. package/dist/config-reference.js.map +1 -1
  66. package/dist/configuration-proposal-main.d.ts +3 -0
  67. package/dist/configuration-proposal-main.d.ts.map +1 -0
  68. package/dist/configuration-proposal-main.js +13 -0
  69. package/dist/configuration-proposal-main.js.map +1 -0
  70. package/dist/configuration-proposal-tool.d.ts +33 -0
  71. package/dist/configuration-proposal-tool.d.ts.map +1 -0
  72. package/dist/configuration-proposal-tool.js +188 -0
  73. package/dist/configuration-proposal-tool.js.map +1 -0
  74. package/dist/configured-agent.d.ts +16 -0
  75. package/dist/configured-agent.d.ts.map +1 -1
  76. package/dist/configured-agent.js +165 -18
  77. package/dist/configured-agent.js.map +1 -1
  78. package/dist/consumer-contract.d.ts +3 -0
  79. package/dist/consumer-contract.d.ts.map +1 -1
  80. package/dist/consumer-contract.js +44 -1
  81. package/dist/consumer-contract.js.map +1 -1
  82. package/dist/continuation-command.d.ts +15 -0
  83. package/dist/continuation-command.d.ts.map +1 -0
  84. package/dist/continuation-command.js +170 -0
  85. package/dist/continuation-command.js.map +1 -0
  86. package/dist/continuation-config.d.ts +25 -0
  87. package/dist/continuation-config.d.ts.map +1 -0
  88. package/dist/continuation-config.js +236 -0
  89. package/dist/continuation-config.js.map +1 -0
  90. package/dist/continuation-service.d.ts +99 -0
  91. package/dist/continuation-service.d.ts.map +1 -0
  92. package/dist/continuation-service.js +1313 -0
  93. package/dist/continuation-service.js.map +1 -0
  94. package/dist/continuation-store.d.ts +104 -0
  95. package/dist/continuation-store.d.ts.map +1 -0
  96. package/dist/continuation-store.js +688 -0
  97. package/dist/continuation-store.js.map +1 -0
  98. package/dist/continuations.d.ts +167 -0
  99. package/dist/continuations.d.ts.map +1 -0
  100. package/dist/continuations.js +71 -0
  101. package/dist/continuations.js.map +1 -0
  102. package/dist/doctor.d.ts +29 -5
  103. package/dist/doctor.d.ts.map +1 -1
  104. package/dist/doctor.js +1611 -116
  105. package/dist/doctor.js.map +1 -1
  106. package/dist/first-run-managed-memory.d.ts +39 -0
  107. package/dist/first-run-managed-memory.d.ts.map +1 -0
  108. package/dist/first-run-managed-memory.js +584 -0
  109. package/dist/first-run-managed-memory.js.map +1 -0
  110. package/dist/first-run-readiness.d.ts +111 -0
  111. package/dist/first-run-readiness.d.ts.map +1 -0
  112. package/dist/first-run-readiness.js +728 -0
  113. package/dist/first-run-readiness.js.map +1 -0
  114. package/dist/index.d.ts +17 -3
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +8 -1
  117. package/dist/index.js.map +1 -1
  118. package/dist/init.d.ts +95 -3
  119. package/dist/init.d.ts.map +1 -1
  120. package/dist/init.js +1218 -24
  121. package/dist/init.js.map +1 -1
  122. package/dist/interaction-bridge.d.ts +25 -1
  123. package/dist/interaction-bridge.d.ts.map +1 -1
  124. package/dist/interaction-bridge.js +281 -19
  125. package/dist/interaction-bridge.js.map +1 -1
  126. package/dist/launchd.d.ts +16 -6
  127. package/dist/launchd.d.ts.map +1 -1
  128. package/dist/launchd.js +30 -15
  129. package/dist/launchd.js.map +1 -1
  130. package/dist/local-configuration.d.ts +99 -0
  131. package/dist/local-configuration.d.ts.map +1 -0
  132. package/dist/local-configuration.js +1299 -0
  133. package/dist/local-configuration.js.map +1 -0
  134. package/dist/managed-runtime-packages.d.ts +9 -0
  135. package/dist/managed-runtime-packages.d.ts.map +1 -0
  136. package/dist/managed-runtime-packages.js +56 -0
  137. package/dist/managed-runtime-packages.js.map +1 -0
  138. package/dist/memory-command.d.ts +4 -0
  139. package/dist/memory-command.d.ts.map +1 -1
  140. package/dist/memory-command.js +751 -27
  141. package/dist/memory-command.js.map +1 -1
  142. package/dist/memory-embedding-service.d.ts +33 -0
  143. package/dist/memory-embedding-service.d.ts.map +1 -0
  144. package/dist/memory-embedding-service.js +207 -0
  145. package/dist/memory-embedding-service.js.map +1 -0
  146. package/dist/memory-recall.d.ts +43 -14
  147. package/dist/memory-recall.d.ts.map +1 -1
  148. package/dist/memory-recall.js +103 -33
  149. package/dist/memory-recall.js.map +1 -1
  150. package/dist/memory-retrieval.d.ts +66 -0
  151. package/dist/memory-retrieval.d.ts.map +1 -0
  152. package/dist/memory-retrieval.js +332 -0
  153. package/dist/memory-retrieval.js.map +1 -0
  154. package/dist/modules/base.d.ts +6 -5
  155. package/dist/modules/base.d.ts.map +1 -1
  156. package/dist/modules/base.js +11 -7
  157. package/dist/modules/base.js.map +1 -1
  158. package/dist/modules/catalog.d.ts.map +1 -1
  159. package/dist/modules/catalog.js +141 -25
  160. package/dist/modules/catalog.js.map +1 -1
  161. package/dist/modules/index.d.ts +2 -2
  162. package/dist/modules/index.d.ts.map +1 -1
  163. package/dist/modules/index.js +1 -1
  164. package/dist/modules/index.js.map +1 -1
  165. package/dist/modules/known-tools.d.ts +3 -0
  166. package/dist/modules/known-tools.d.ts.map +1 -1
  167. package/dist/modules/known-tools.js +11 -1
  168. package/dist/modules/known-tools.js.map +1 -1
  169. package/dist/modules/types.d.ts +20 -6
  170. package/dist/modules/types.d.ts.map +1 -1
  171. package/dist/modules/types.js.map +1 -1
  172. package/dist/notify-runtime.d.ts +26 -0
  173. package/dist/notify-runtime.d.ts.map +1 -0
  174. package/dist/notify-runtime.js +27 -0
  175. package/dist/notify-runtime.js.map +1 -0
  176. package/dist/notify-tool.d.ts +51 -0
  177. package/dist/notify-tool.d.ts.map +1 -0
  178. package/dist/notify-tool.js +182 -0
  179. package/dist/notify-tool.js.map +1 -0
  180. package/dist/package-version.d.ts +3 -0
  181. package/dist/package-version.d.ts.map +1 -0
  182. package/dist/package-version.js +14 -0
  183. package/dist/package-version.js.map +1 -0
  184. package/dist/pi-auth-store-inspection.d.ts +21 -0
  185. package/dist/pi-auth-store-inspection.d.ts.map +1 -0
  186. package/dist/pi-auth-store-inspection.js +120 -0
  187. package/dist/pi-auth-store-inspection.js.map +1 -0
  188. package/dist/pi-oauth-login-main.d.ts +3 -0
  189. package/dist/pi-oauth-login-main.d.ts.map +1 -0
  190. package/dist/pi-oauth-login-main.js +14 -0
  191. package/dist/pi-oauth-login-main.js.map +1 -0
  192. package/dist/pi-oauth-login.d.ts +22 -0
  193. package/dist/pi-oauth-login.d.ts.map +1 -0
  194. package/dist/pi-oauth-login.js +79 -0
  195. package/dist/pi-oauth-login.js.map +1 -0
  196. package/dist/proactive-notify.d.ts +2 -0
  197. package/dist/proactive-notify.d.ts.map +1 -1
  198. package/dist/proactive-notify.js +1 -0
  199. package/dist/proactive-notify.js.map +1 -1
  200. package/dist/process-incarnation.d.ts +23 -0
  201. package/dist/process-incarnation.d.ts.map +1 -0
  202. package/dist/process-incarnation.js +208 -0
  203. package/dist/process-incarnation.js.map +1 -0
  204. package/dist/project-skills.d.ts +40 -0
  205. package/dist/project-skills.d.ts.map +1 -0
  206. package/dist/project-skills.js +573 -0
  207. package/dist/project-skills.js.map +1 -0
  208. package/dist/provider-setup.d.ts +117 -2
  209. package/dist/provider-setup.d.ts.map +1 -1
  210. package/dist/provider-setup.js +1505 -49
  211. package/dist/provider-setup.js.map +1 -1
  212. package/dist/readiness-probe-worker.d.ts +12 -0
  213. package/dist/readiness-probe-worker.d.ts.map +1 -0
  214. package/dist/readiness-probe-worker.js +276 -0
  215. package/dist/readiness-probe-worker.js.map +1 -0
  216. package/dist/readiness-probe.d.ts +100 -0
  217. package/dist/readiness-probe.d.ts.map +1 -0
  218. package/dist/readiness-probe.js +780 -0
  219. package/dist/readiness-probe.js.map +1 -0
  220. package/dist/recipes/base.d.ts +15 -0
  221. package/dist/recipes/base.d.ts.map +1 -0
  222. package/dist/recipes/base.js +51 -0
  223. package/dist/recipes/base.js.map +1 -0
  224. package/dist/recipes/catalog.d.ts +4 -0
  225. package/dist/recipes/catalog.d.ts.map +1 -0
  226. package/dist/recipes/catalog.js +525 -0
  227. package/dist/recipes/catalog.js.map +1 -0
  228. package/dist/recipes/index.d.ts +11 -0
  229. package/dist/recipes/index.d.ts.map +1 -0
  230. package/dist/recipes/index.js +14 -0
  231. package/dist/recipes/index.js.map +1 -0
  232. package/dist/recipes/types.d.ts +70 -0
  233. package/dist/recipes/types.d.ts.map +1 -0
  234. package/dist/recipes/types.js +15 -0
  235. package/dist/recipes/types.js.map +1 -0
  236. package/dist/request-model-override.d.ts +43 -1
  237. package/dist/request-model-override.d.ts.map +1 -1
  238. package/dist/request-model-override.js +178 -19
  239. package/dist/request-model-override.js.map +1 -1
  240. package/dist/run-history.d.ts +32 -0
  241. package/dist/run-history.d.ts.map +1 -0
  242. package/dist/run-history.js +859 -0
  243. package/dist/run-history.js.map +1 -0
  244. package/dist/runs-health.d.ts.map +1 -1
  245. package/dist/runs-health.js +12 -4
  246. package/dist/runs-health.js.map +1 -1
  247. package/dist/runtime-option-extensions.d.ts +14 -0
  248. package/dist/runtime-option-extensions.d.ts.map +1 -0
  249. package/dist/runtime-option-extensions.js +98 -0
  250. package/dist/runtime-option-extensions.js.map +1 -0
  251. package/dist/runtime-routes.d.ts +9 -0
  252. package/dist/runtime-routes.d.ts.map +1 -0
  253. package/dist/runtime-routes.js +13 -0
  254. package/dist/runtime-routes.js.map +1 -0
  255. package/dist/sandbox-manager.d.ts +86 -0
  256. package/dist/sandbox-manager.d.ts.map +1 -0
  257. package/dist/sandbox-manager.js +1402 -0
  258. package/dist/sandbox-manager.js.map +1 -0
  259. package/dist/setup.d.ts +29 -0
  260. package/dist/setup.d.ts.map +1 -0
  261. package/dist/setup.js +97 -0
  262. package/dist/setup.js.map +1 -0
  263. package/dist/supermemory-plugin.d.ts +56 -0
  264. package/dist/supermemory-plugin.d.ts.map +1 -0
  265. package/dist/supermemory-plugin.js +142 -0
  266. package/dist/supermemory-plugin.js.map +1 -0
  267. package/dist/tui-command.d.ts +56 -0
  268. package/dist/tui-command.d.ts.map +1 -1
  269. package/dist/tui-command.js +305 -31
  270. package/dist/tui-command.js.map +1 -1
  271. package/dist/web-command.d.ts +8 -0
  272. package/dist/web-command.d.ts.map +1 -1
  273. package/dist/web-command.js +174 -17
  274. package/dist/web-command.js.map +1 -1
  275. package/dist/wizard/answers.d.ts +31 -4
  276. package/dist/wizard/answers.d.ts.map +1 -1
  277. package/dist/wizard/answers.js +141 -29
  278. package/dist/wizard/answers.js.map +1 -1
  279. package/dist/wizard/from-flags.d.ts +7 -0
  280. package/dist/wizard/from-flags.d.ts.map +1 -1
  281. package/dist/wizard/from-flags.js +36 -2
  282. package/dist/wizard/from-flags.js.map +1 -1
  283. package/dist/wizard/index.d.ts +4 -4
  284. package/dist/wizard/index.d.ts.map +1 -1
  285. package/dist/wizard/index.js +2 -2
  286. package/dist/wizard/index.js.map +1 -1
  287. package/dist/wizard/model-discovery.d.ts +33 -2
  288. package/dist/wizard/model-discovery.d.ts.map +1 -1
  289. package/dist/wizard/model-discovery.js +826 -108
  290. package/dist/wizard/model-discovery.js.map +1 -1
  291. package/dist/wizard/presets.d.ts +1 -1
  292. package/dist/wizard/presets.d.ts.map +1 -1
  293. package/dist/wizard/presets.js +7 -12
  294. package/dist/wizard/presets.js.map +1 -1
  295. package/dist/wizard/prompts.d.ts +56 -9
  296. package/dist/wizard/prompts.d.ts.map +1 -1
  297. package/dist/wizard/prompts.js +172 -20
  298. package/dist/wizard/prompts.js.map +1 -1
  299. package/dist/wizard/run.d.ts +59 -3
  300. package/dist/wizard/run.d.ts.map +1 -1
  301. package/dist/wizard/run.js +1290 -191
  302. package/dist/wizard/run.js.map +1 -1
  303. package/package.json +21 -16
  304. package/resources/srt/package-lock.json +66 -0
  305. package/resources/srt/package.json +9 -0
  306. package/schema/mono-agent.config.schema.json +587 -20
  307. package/skills/mono-agent-composer/SKILL.md +6 -6
  308. package/skills/mono-agent-composer/references/config-blueprint.md +11 -10
  309. package/skills/mono-agent-composer/references/discovery-questions.md +33 -18
  310. package/skills/mono-agent-composer/references/feature-coverage.md +14 -12
  311. package/skills/mono-agent-composer/references/package-map.md +2 -2
  312. package/skills/mono-agent-composer/references/playbooks.md +8 -7
  313. package/skills/mono-agent-composer/references/validation.md +3 -3
@@ -0,0 +1,1313 @@
1
+ import { createHmac, randomBytes, randomUUID } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { resolve } from "node:path";
4
+ import { acquireContinuationStoreLock, loadOrCreateContinuationSecret, openContinuationStore, } from "./continuation-store.js";
5
+ 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";
6
+ const DEFAULT_MAX_RESULT_BYTES = 256 * 1024;
7
+ const DEFAULT_MAX_DEADLINE_MS = 30 * 24 * 60 * 60 * 1_000;
8
+ const DEFAULT_LEASE_MS = 30 * 60 * 1_000;
9
+ const DEFAULT_WORKER_INTERVAL_MS = 1_000;
10
+ const DEFAULT_DELIVERY_ATTEMPTS = 20;
11
+ const MAX_OPERATOR_PAGE_SIZE = 500;
12
+ const MAX_CLAIM_BODY_BYTES = 16 * 1024;
13
+ const MAX_TASK_KEY_CHARS = 256;
14
+ const MAX_TEXT_CHARS = 200_000;
15
+ /**
16
+ * Trusted host signal for the narrow race where readiness disappears after
17
+ * preflight but before the model call begins. It is safe to requeue because the
18
+ * host must throw it before invoking the responder/model.
19
+ */
20
+ export class ContinuationSynthesisUnavailableError extends Error {
21
+ code;
22
+ retryAfterMs;
23
+ constructor(code, message, retryAfterMs) {
24
+ super(message);
25
+ this.name = "ContinuationSynthesisUnavailableError";
26
+ this.code = code;
27
+ this.retryAfterMs = retryAfterMs;
28
+ }
29
+ }
30
+ class ContinuationServiceStoppingError extends Error {
31
+ constructor() {
32
+ super("Continuation service is stopping.");
33
+ this.name = "ContinuationServiceStoppingError";
34
+ }
35
+ }
36
+ class ContinuationOperationTimeoutError extends Error {
37
+ phase;
38
+ constructor(phase, timeoutMs) {
39
+ super(`Continuation ${phase} exceeded its ${String(timeoutMs)}ms timeout.`);
40
+ this.phase = phase;
41
+ this.name = "ContinuationOperationTimeoutError";
42
+ }
43
+ }
44
+ export class ContinuationProtocolError extends Error {
45
+ status;
46
+ code;
47
+ constructor(status, code, message) {
48
+ super(message);
49
+ this.name = "ContinuationProtocolError";
50
+ this.status = status;
51
+ this.code = code;
52
+ }
53
+ }
54
+ export async function startContinuationService(options) {
55
+ const host = options.host ?? "127.0.0.1";
56
+ if (!isLoopbackHost(host)) {
57
+ throw new Error(`Continuation service must bind to loopback, received ${host}.`);
58
+ }
59
+ validateNamedRoutes(options.namedRoutes ?? {});
60
+ const stateDir = resolve(options.stateDir ?? resolve(options.cwd ?? process.cwd(), ".mono-agent", "continuations"));
61
+ const lock = await acquireContinuationStoreLock(stateDir);
62
+ try {
63
+ const [store, secret] = await Promise.all([
64
+ openContinuationStore(stateDir, {
65
+ ...(options.retention === undefined ? {} : { retention: options.retention }),
66
+ ...(options.now === undefined ? {} : { now: options.now }),
67
+ }),
68
+ loadOrCreateContinuationSecret(stateDir),
69
+ ]);
70
+ const service = new ContinuationService(store, secret, options, () => lock.release());
71
+ await service.recover(true);
72
+ return await service.start(host, options.port ?? 0);
73
+ }
74
+ catch (error) {
75
+ await lock.release().catch(() => undefined);
76
+ throw error;
77
+ }
78
+ }
79
+ class ContinuationService {
80
+ store;
81
+ secret;
82
+ options;
83
+ releaseStoreLock;
84
+ instanceId = randomUUID();
85
+ operatorToken;
86
+ claimBindings = new Map();
87
+ namedRoutes;
88
+ detachedServiceTokenHashes;
89
+ maxResultBytes;
90
+ maxDeadlineMs;
91
+ leaseMs;
92
+ workerIntervalMs;
93
+ autoProcess;
94
+ limits;
95
+ now;
96
+ logger;
97
+ lifecycleAbort = new AbortController();
98
+ inFlight = new Map();
99
+ resolvingUnknown = new Set();
100
+ dispatchTail = Promise.resolve();
101
+ server;
102
+ baseUrl = "";
103
+ worker;
104
+ stopped = false;
105
+ constructor(store, secret, options, releaseStoreLock) {
106
+ this.store = store;
107
+ this.secret = secret;
108
+ this.options = options;
109
+ this.releaseStoreLock = releaseStoreLock;
110
+ this.namedRoutes = options.namedRoutes ?? {};
111
+ this.operatorToken = continuationOperatorToken(secret);
112
+ this.detachedServiceTokenHashes = new Map(Object.entries(options.detachedServices ?? {}).map(([name, token]) => [name, continuationDigest(token)]));
113
+ this.maxResultBytes = options.maxResultBytes ?? DEFAULT_MAX_RESULT_BYTES;
114
+ this.maxDeadlineMs = options.maxDeadlineMs ?? DEFAULT_MAX_DEADLINE_MS;
115
+ this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
116
+ this.workerIntervalMs = options.workerIntervalMs ?? DEFAULT_WORKER_INTERVAL_MS;
117
+ this.autoProcess = options.autoProcess !== false;
118
+ this.limits = resolveContinuationLimits(options.limits);
119
+ this.now = options.now ?? (() => new Date());
120
+ this.logger = options.logger;
121
+ }
122
+ async start(host, port) {
123
+ const server = createServer((request, response) => {
124
+ void this.handleRequest(request, response).catch((error) => {
125
+ this.handleRequestError(response, error);
126
+ });
127
+ });
128
+ this.server = server;
129
+ await new Promise((resolve, reject) => {
130
+ server.once("error", reject);
131
+ server.listen(port, host, () => {
132
+ server.off("error", reject);
133
+ resolve();
134
+ });
135
+ });
136
+ const address = server.address();
137
+ if (address === null || typeof address === "string")
138
+ throw new Error("Continuation service has no TCP address.");
139
+ this.baseUrl = `http://${formatHost(host)}:${String(address.port)}`;
140
+ if (this.autoProcess) {
141
+ this.worker = setInterval(() => { void this.processDue().catch(() => undefined); }, this.workerIntervalMs);
142
+ this.worker.unref?.();
143
+ }
144
+ this.logger?.info?.("Continuation service started.", { url: this.baseUrl, store: this.store.path });
145
+ return {
146
+ url: this.baseUrl,
147
+ operatorToken: this.operatorToken,
148
+ issueContinuationClaimCapability: (input) => this.issueRunClaimCapability({
149
+ serverName: input.serverName,
150
+ runId: input.runId,
151
+ originConversationId: input.conversationId,
152
+ ...(input.replyTo === undefined ? {} : { replyToConversationId: input.replyTo.conversationId }),
153
+ ...(input.historyBoundary === undefined ? {} : { historyBoundary: input.historyBoundary }),
154
+ mode: "reply",
155
+ }),
156
+ issueRunClaimCapability: (input) => this.issueRunClaimCapability(input),
157
+ status: async (id) => statusOf(await this.store.get(id)),
158
+ list: async () => (await this.store.list()).map(statusOfRequired),
159
+ health: async () => await this.health(),
160
+ processDue: async (limit) => await this.processDue(limit),
161
+ retry: async (id, retryOptions) => await this.retry(id, retryOptions),
162
+ cancel: async (id) => await this.cancel(id),
163
+ resolveUnknown: async (id, outcome) => await this.resolveUnknown(id, outcome),
164
+ capturedText: async (id) => {
165
+ const record = await this.store.get(id);
166
+ return record?.mode === "capture" && record.state === "delivered" ? record.synthesizedText : undefined;
167
+ },
168
+ stop: async () => await this.stop(),
169
+ };
170
+ }
171
+ issueRunClaimCapability(input) {
172
+ if (this.stopped)
173
+ throw new Error("Continuation service is stopped.");
174
+ const mode = input.mode ?? "reply";
175
+ const replyToConversationId = input.replyToConversationId
176
+ ?? (mode === "reply" || mode === "notify_if_actionable"
177
+ ? normalizeContinuationReplyTarget(input.originConversationId)
178
+ : undefined);
179
+ if ((mode === "reply" || mode === "notify_if_actionable") && replyToConversationId === undefined) {
180
+ throw new Error(`Continuation mode ${mode} requires a bound reply target.`);
181
+ }
182
+ const fingerprint = continuationDigest([
183
+ "v1",
184
+ input.serverName,
185
+ input.runId,
186
+ input.originConversationId,
187
+ replyToConversationId ?? "",
188
+ input.historyBoundary ?? "",
189
+ mode,
190
+ ].join("\0"));
191
+ const token = randomBytes(24).toString("base64url");
192
+ this.claimBindings.set(token, {
193
+ serverName: input.serverName,
194
+ originRunId: input.runId,
195
+ originConversationId: input.originConversationId,
196
+ ...(replyToConversationId === undefined ? {} : { replyToConversationId }),
197
+ ...(input.historyBoundary === undefined ? {} : { historyBoundary: input.historyBoundary }),
198
+ mode,
199
+ fingerprint,
200
+ });
201
+ const url = `${this.baseUrl}/v1/continuations/claim`;
202
+ let released = false;
203
+ return {
204
+ url,
205
+ token,
206
+ fingerprint,
207
+ mode,
208
+ headers: () => ({
209
+ [CONTINUATION_CLAIM_URL_HEADER]: url,
210
+ [CONTINUATION_CLAIM_TOKEN_HEADER]: token,
211
+ [CONTINUATION_FINGERPRINT_HEADER]: fingerprint,
212
+ [CONTINUATION_MODE_HEADER]: mode,
213
+ }),
214
+ env: () => ({
215
+ [CONTINUATION_CLAIM_URL_ENV]: url,
216
+ [CONTINUATION_CLAIM_TOKEN_ENV]: token,
217
+ [CONTINUATION_FINGERPRINT_ENV]: fingerprint,
218
+ [CONTINUATION_MODE_ENV]: mode,
219
+ }),
220
+ release: () => {
221
+ if (released)
222
+ return;
223
+ released = true;
224
+ this.claimBindings.delete(token);
225
+ },
226
+ };
227
+ }
228
+ async recover(startup = false) {
229
+ const now = this.now().getTime();
230
+ const current = await this.store.list();
231
+ const needsRecovery = current.some((record) => !TERMINAL_CONTINUATION_STATES.has(record.state)
232
+ && (startup || !this.inFlight.has(record.continuationId))
233
+ && ((startup && record.leaseOwner !== undefined)
234
+ || Date.parse(record.deadline) <= now
235
+ || (record.leaseUntil !== undefined && Date.parse(record.leaseUntil) <= now)));
236
+ if (!needsRecovery)
237
+ return;
238
+ await this.store.mutate((records) => {
239
+ for (const record of records.values()) {
240
+ if (TERMINAL_CONTINUATION_STATES.has(record.state))
241
+ continue;
242
+ // A configured operation timeout may intentionally exceed the lease.
243
+ // The process-lifetime store lock prevents another owner, while this
244
+ // map proves the current owner is still supervising the attempt.
245
+ if (!startup && this.inFlight.has(record.continuationId))
246
+ continue;
247
+ const abandonedAtStartup = startup && record.leaseOwner !== undefined;
248
+ const leaseExpired = record.leaseUntil !== undefined && Date.parse(record.leaseUntil) <= now;
249
+ // Once a native send began, absence of a receipt is ambiguous even if
250
+ // the wall-clock deadline passed while the process was down.
251
+ if (record.deliveryStartedAt !== undefined && (abandonedAtStartup || leaseExpired || Date.parse(record.deadline) <= now)) {
252
+ record.state = "delivery_unknown";
253
+ record.lastError = errorRecord("delivery_outcome_unknown", "Process stopped after native delivery began; automatic replay is unsafe.", this.now().toISOString());
254
+ clearLease(record);
255
+ continue;
256
+ }
257
+ if (Date.parse(record.deadline) <= now) {
258
+ expire(record, this.now().toISOString());
259
+ continue;
260
+ }
261
+ if (!abandonedAtStartup && !leaseExpired)
262
+ continue;
263
+ if (record.state === "synthesizing") {
264
+ if (record.synthesizedText !== undefined) {
265
+ record.state = "ready_to_deliver";
266
+ clearLease(record);
267
+ }
268
+ else {
269
+ record.state = "dead_lettered";
270
+ record.lastError = errorRecord("synthesis_outcome_unknown", "Process stopped after synthesis began; synthesis was not repeated.", this.now().toISOString());
271
+ clearLease(record);
272
+ }
273
+ }
274
+ else {
275
+ clearLease(record);
276
+ }
277
+ }
278
+ });
279
+ }
280
+ async processDue(limit = this.limits.maxConcurrent) {
281
+ if (this.stopped)
282
+ return 0;
283
+ if (!Number.isSafeInteger(limit) || limit < 0)
284
+ throw new Error("Continuation process limit must be a non-negative safe integer.");
285
+ const jobs = await this.startDueJobs(limit);
286
+ const outcomes = await Promise.all(jobs);
287
+ return outcomes.filter(Boolean).length;
288
+ }
289
+ /**
290
+ * Keep dispatch serialized, but never serialize the work itself. A hung
291
+ * provider occupies only one bounded worker slot and cannot block unrelated
292
+ * continuations from being leased and processed.
293
+ */
294
+ async startDueJobs(limit) {
295
+ const dispatch = this.dispatchTail.then(async () => {
296
+ if (this.stopped)
297
+ return [];
298
+ await this.recover();
299
+ if (this.stopped)
300
+ return [];
301
+ const capacity = Math.max(0, Math.min(limit, this.limits.maxConcurrent - this.inFlight.size));
302
+ if (capacity === 0)
303
+ return [];
304
+ const nowMs = this.now().getTime();
305
+ const candidates = (await this.store.list())
306
+ .filter((record) => !this.inFlight.has(record.continuationId)
307
+ && ((record.state === "result_received" && Date.parse(record.nextAttemptAt ?? record.updatedAt) <= nowMs)
308
+ || record.state === "ready_to_deliver"
309
+ || (record.state === "delivery_retry" && Date.parse(record.nextAttemptAt ?? record.updatedAt) <= nowMs)))
310
+ .sort((left, right) => Date.parse(left.updatedAt) - Date.parse(right.updatedAt))
311
+ .slice(0, capacity);
312
+ if (this.stopped)
313
+ return [];
314
+ return candidates.map((candidate) => this.startOne(candidate.continuationId));
315
+ });
316
+ this.dispatchTail = dispatch.then(() => undefined, () => undefined);
317
+ return await dispatch;
318
+ }
319
+ startOne(id) {
320
+ const existing = this.inFlight.get(id);
321
+ if (existing !== undefined)
322
+ return existing;
323
+ const job = this.processOne(id);
324
+ this.inFlight.set(id, job);
325
+ void job.then(() => { if (this.inFlight.get(id) === job)
326
+ this.inFlight.delete(id); }, () => { if (this.inFlight.get(id) === job)
327
+ this.inFlight.delete(id); });
328
+ return job;
329
+ }
330
+ async processOne(id) {
331
+ this.throwIfStopping();
332
+ const leased = await this.acquireLease(id);
333
+ if (leased === undefined)
334
+ return false;
335
+ this.throwIfStopping();
336
+ let record = leased;
337
+ if (record.synthesizedText === undefined && record.mode !== "silent") {
338
+ const available = await this.synthesisAvailable(record);
339
+ if (!available.ready) {
340
+ await this.deferSynthesis(record.continuationId, available.code, available.reason, available.retryAfterMs);
341
+ return true;
342
+ }
343
+ record = await this.synthesize(record);
344
+ if (record.state !== "ready_to_deliver")
345
+ return true;
346
+ }
347
+ else if (record.state !== "ready_to_deliver") {
348
+ record = await this.markReady(record.continuationId);
349
+ }
350
+ // Synthesis can outlive the task deadline. Persist the expiry decision
351
+ // after the model result is stored and immediately before any native side
352
+ // effect so a late answer is never posted merely because the original
353
+ // lease was acquired in time.
354
+ if (await this.expireBeforeDeliveryIfNeeded(record.continuationId)) {
355
+ return true;
356
+ }
357
+ this.throwIfStopping();
358
+ if (record.mode === "silent") {
359
+ await this.markDelivered(record.continuationId, { kind: "silent", deliveredAt: this.now().toISOString() });
360
+ return true;
361
+ }
362
+ if (record.mode === "capture") {
363
+ await this.markDelivered(record.continuationId, { kind: "captured", deliveredAt: this.now().toISOString() });
364
+ return true;
365
+ }
366
+ if (record.mode === "notify_if_actionable" && record.actionable === false) {
367
+ await this.markDelivered(record.continuationId, { kind: "suppressed", deliveredAt: this.now().toISOString() });
368
+ return true;
369
+ }
370
+ await this.deliver(record);
371
+ return true;
372
+ }
373
+ async synthesisAvailable(record) {
374
+ if (this.options.synthesisPreflight === undefined)
375
+ return { ready: true };
376
+ try {
377
+ return await this.runBoundedOperation("synthesis", Math.min(this.limits.synthesisTimeoutMs, 30_000), async () => await this.options.synthesisPreflight?.(synthesisInput(record)) ?? { ready: true });
378
+ }
379
+ catch (error) {
380
+ if (error instanceof ContinuationServiceStoppingError)
381
+ throw error;
382
+ return {
383
+ ready: false,
384
+ code: error instanceof ContinuationOperationTimeoutError
385
+ ? "synthesis_preflight_timeout"
386
+ : "synthesis_preflight_failed",
387
+ reason: safeReason(error),
388
+ };
389
+ }
390
+ }
391
+ async deferSynthesis(id, code, reason, retryAfterMs = 1_000, consumedAttempt = false) {
392
+ return await this.store.mutate((records) => {
393
+ const current = requireRecord(records, id);
394
+ requireLease(current, this.instanceId);
395
+ const at = this.now();
396
+ current.state = "result_received";
397
+ current.updatedAt = at.toISOString();
398
+ current.nextAttemptAt = new Date(at.getTime() + Math.max(0, retryAfterMs)).toISOString();
399
+ current.lastError = errorRecord(code, reason, current.updatedAt);
400
+ delete current.synthesisStartedAt;
401
+ if (consumedAttempt)
402
+ current.synthesisAttempts = Math.max(0, current.synthesisAttempts - 1);
403
+ clearLease(current);
404
+ return structuredClone(current);
405
+ });
406
+ }
407
+ async expireBeforeDeliveryIfNeeded(id) {
408
+ const now = this.now();
409
+ return await this.store.mutate((records) => {
410
+ const current = requireRecord(records, id);
411
+ requireLease(current, this.instanceId);
412
+ if (Date.parse(current.deadline) > now.getTime())
413
+ return false;
414
+ expire(current, now.toISOString());
415
+ return true;
416
+ });
417
+ }
418
+ async acquireLease(id) {
419
+ const now = this.now();
420
+ return await this.store.mutate((records) => {
421
+ const record = records.get(id);
422
+ if (record === undefined || TERMINAL_CONTINUATION_STATES.has(record.state))
423
+ return undefined;
424
+ if (record.leaseUntil !== undefined && Date.parse(record.leaseUntil) > now.getTime())
425
+ return undefined;
426
+ if (Date.parse(record.deadline) <= now.getTime()) {
427
+ expire(record, now.toISOString());
428
+ return undefined;
429
+ }
430
+ record.leaseOwner = this.instanceId;
431
+ record.leaseUntil = new Date(now.getTime() + this.leaseMs).toISOString();
432
+ record.updatedAt = now.toISOString();
433
+ return structuredClone(record);
434
+ });
435
+ }
436
+ async synthesize(record) {
437
+ const startedAt = this.now().toISOString();
438
+ const prepared = await this.store.mutate((records) => {
439
+ this.throwIfStopping();
440
+ const current = requireRecord(records, record.continuationId);
441
+ requireLease(current, this.instanceId);
442
+ current.state = "synthesizing";
443
+ current.synthesisAttempts += 1;
444
+ current.synthesisStartedAt = startedAt;
445
+ current.updatedAt = startedAt;
446
+ delete current.nextAttemptAt;
447
+ return structuredClone(current);
448
+ });
449
+ try {
450
+ const result = await this.runBoundedOperation("synthesis", this.limits.synthesisTimeoutMs, async () => await this.options.synthesize(synthesisInput(prepared)));
451
+ this.throwIfStopping();
452
+ const text = result.text.trim();
453
+ if (text.length === 0 || text.length > MAX_TEXT_CHARS) {
454
+ throw new Error(text.length === 0 ? "Synthesis returned empty text." : "Synthesis exceeded the 200,000 character limit.");
455
+ }
456
+ return await this.store.mutate((records) => {
457
+ const current = requireRecord(records, prepared.continuationId);
458
+ requireLease(current, this.instanceId);
459
+ current.synthesizedText = text;
460
+ if (result.actionable !== undefined)
461
+ current.actionable = result.actionable;
462
+ current.state = "ready_to_deliver";
463
+ current.updatedAt = this.now().toISOString();
464
+ delete current.lastError;
465
+ delete current.synthesisStartedAt;
466
+ return structuredClone(current);
467
+ });
468
+ }
469
+ catch (error) {
470
+ if (error instanceof ContinuationServiceStoppingError)
471
+ throw error;
472
+ if (error instanceof ContinuationSynthesisUnavailableError) {
473
+ return await this.deferSynthesis(prepared.continuationId, error.code, error.message, error.retryAfterMs, true);
474
+ }
475
+ return await this.store.mutate((records) => {
476
+ const current = requireRecord(records, prepared.continuationId);
477
+ requireLease(current, this.instanceId);
478
+ const at = this.now().toISOString();
479
+ current.lastError = errorRecord(error instanceof ContinuationOperationTimeoutError
480
+ ? "synthesis_timeout_outcome_unknown"
481
+ : "synthesis_failed", safeReason(error), at);
482
+ current.updatedAt = at;
483
+ clearLease(current);
484
+ // At-most-once model execution: a thrown/ambiguous synthesis is terminal.
485
+ // Only delivery may retry, and only after text has been durably persisted.
486
+ current.state = "dead_lettered";
487
+ return structuredClone(current);
488
+ });
489
+ }
490
+ }
491
+ async markReady(id) {
492
+ return await this.store.mutate((records) => {
493
+ const record = requireRecord(records, id);
494
+ requireLease(record, this.instanceId);
495
+ record.state = "ready_to_deliver";
496
+ record.updatedAt = this.now().toISOString();
497
+ return structuredClone(record);
498
+ });
499
+ }
500
+ async deliver(record) {
501
+ if (record.replyToConversationId === undefined || record.synthesizedText === undefined) {
502
+ await this.deadLetter(record.continuationId, "missing_delivery_binding", "Continuation has no bound native destination or synthesized text.");
503
+ return;
504
+ }
505
+ const started = await this.store.mutate((records) => {
506
+ this.throwIfStopping();
507
+ const current = requireRecord(records, record.continuationId);
508
+ requireLease(current, this.instanceId);
509
+ current.deliveryAttempts += 1;
510
+ const at = this.now().toISOString();
511
+ current.deliveryStartedAt = at;
512
+ current.updatedAt = at;
513
+ return structuredClone(current);
514
+ });
515
+ const conversationId = started.replyToConversationId;
516
+ const text = started.synthesizedText;
517
+ if (conversationId === undefined || text === undefined) {
518
+ await this.deadLetter(started.continuationId, "missing_delivery_binding", "Continuation delivery binding disappeared before native delivery.");
519
+ return;
520
+ }
521
+ let result;
522
+ try {
523
+ result = await this.runBoundedOperation("delivery", this.limits.deliveryTimeoutMs, async () => await this.options.deliver({
524
+ continuationId: started.continuationId,
525
+ conversationId,
526
+ text,
527
+ deliveryKey: `continuation:${started.continuationId}`,
528
+ }));
529
+ this.throwIfStopping();
530
+ }
531
+ catch (error) {
532
+ if (error instanceof ContinuationServiceStoppingError)
533
+ throw error;
534
+ result = {
535
+ kind: "unknown",
536
+ code: error instanceof ContinuationOperationTimeoutError
537
+ ? "delivery_timeout_outcome_unknown"
538
+ : "delivery_threw",
539
+ reason: safeReason(error),
540
+ };
541
+ }
542
+ await this.store.mutate((records) => {
543
+ const current = requireRecord(records, started.continuationId);
544
+ requireLease(current, this.instanceId);
545
+ const at = this.now().toISOString();
546
+ current.updatedAt = at;
547
+ clearLease(current);
548
+ if (result.kind === "delivered") {
549
+ current.state = "delivered";
550
+ delete current.deliveryStartedAt;
551
+ delete current.nextAttemptAt;
552
+ delete current.lastError;
553
+ current.receipt = {
554
+ kind: "delivered",
555
+ deliveredAt: at,
556
+ ...(result.deliveryId === undefined ? {} : { deliveryId: result.deliveryId }),
557
+ ...(result.channelId === undefined ? {} : { channelId: result.channelId }),
558
+ ...(result.historyRecorded === undefined ? {} : { historyRecorded: result.historyRecorded }),
559
+ ...(result.historyRecorded !== false || result.historyErrorCode === undefined
560
+ ? {}
561
+ : { historyErrorCode: boundedHistoryErrorCode(result.historyErrorCode) }),
562
+ };
563
+ }
564
+ else if (result.kind === "retryable" && current.deliveryAttempts < DEFAULT_DELIVERY_ATTEMPTS) {
565
+ current.state = "delivery_retry";
566
+ delete current.deliveryStartedAt;
567
+ current.lastError = errorRecord(result.code, result.reason, at);
568
+ const delay = result.retryAfterMs ?? backoffMs(current.deliveryAttempts, 1_000, 60 * 60 * 1_000);
569
+ current.nextAttemptAt = new Date(this.now().getTime() + delay).toISOString();
570
+ }
571
+ else if (result.kind === "unknown") {
572
+ current.state = "delivery_unknown";
573
+ current.lastError = errorRecord(result.code, result.reason, at);
574
+ }
575
+ else {
576
+ current.state = "dead_lettered";
577
+ delete current.deliveryStartedAt;
578
+ const code = result.kind === "retryable" ? "delivery_attempts_exhausted" : result.code;
579
+ current.lastError = errorRecord(code, result.reason, at);
580
+ }
581
+ });
582
+ }
583
+ async markDelivered(id, receipt) {
584
+ await this.store.mutate((records) => {
585
+ const record = requireRecord(records, id);
586
+ requireLease(record, this.instanceId);
587
+ record.state = "delivered";
588
+ record.receipt = receipt;
589
+ record.updatedAt = receipt.deliveredAt;
590
+ delete record.nextAttemptAt;
591
+ delete record.lastError;
592
+ clearLease(record);
593
+ });
594
+ }
595
+ async deadLetter(id, code, reason) {
596
+ await this.store.mutate((records) => {
597
+ const record = requireRecord(records, id);
598
+ record.state = "dead_lettered";
599
+ record.updatedAt = this.now().toISOString();
600
+ record.lastError = errorRecord(code, reason, record.updatedAt);
601
+ clearLease(record);
602
+ });
603
+ }
604
+ async retry(id, options) {
605
+ const record = await this.store.mutate((records) => {
606
+ const current = requireRecord(records, id);
607
+ if (current.state === "delivery_unknown" && options?.allowUnknown !== true) {
608
+ throw new ContinuationProtocolError(409, "delivery_unknown", "Resolve the ambiguous delivery before retrying.");
609
+ }
610
+ if (current.state !== "delivery_retry" && current.state !== "dead_lettered" && current.state !== "delivery_unknown") {
611
+ throw new ContinuationProtocolError(409, "not_retryable", `Continuation state ${current.state} is not retryable.`);
612
+ }
613
+ if (current.synthesizedText === undefined && current.mode !== "silent") {
614
+ throw new ContinuationProtocolError(409, "synthesis_not_retryable", "Continuation synthesis has no persisted output and cannot be repeated.");
615
+ }
616
+ current.state = "ready_to_deliver";
617
+ current.nextAttemptAt = this.now().toISOString();
618
+ delete current.deliveryStartedAt;
619
+ delete current.lastError;
620
+ current.updatedAt = this.now().toISOString();
621
+ clearLease(current);
622
+ return structuredClone(current);
623
+ });
624
+ if (this.autoProcess)
625
+ void this.processDue().catch(() => undefined);
626
+ return statusOfRequired(record);
627
+ }
628
+ async cancel(id) {
629
+ const record = await this.store.mutate((records) => {
630
+ const current = requireRecord(records, id);
631
+ if (TERMINAL_CONTINUATION_STATES.has(current.state)) {
632
+ if (current.state === "cancelled")
633
+ return structuredClone(current);
634
+ throw new ContinuationProtocolError(409, "already_terminal", `Continuation is already ${current.state}.`);
635
+ }
636
+ if (current.leaseOwner !== undefined || current.state === "synthesizing" || current.deliveryStartedAt !== undefined) {
637
+ throw new ContinuationProtocolError(409, "continuation_in_flight", "Continuation has an active synthesis or native delivery; cancellation cannot make that side effect disappear.");
638
+ }
639
+ current.state = "cancelled";
640
+ current.updatedAt = this.now().toISOString();
641
+ current.lastError = errorRecord("cancelled_by_operator", "Continuation cancelled by the operator.", current.updatedAt);
642
+ clearLease(current);
643
+ return structuredClone(current);
644
+ });
645
+ return statusOfRequired(record);
646
+ }
647
+ async resolveUnknown(id, outcome) {
648
+ if (this.resolvingUnknown.has(id)) {
649
+ throw new ContinuationProtocolError(409, "resolution_in_progress", "This ambiguous delivery is already being resolved.");
650
+ }
651
+ this.resolvingUnknown.add(id);
652
+ try {
653
+ const current = await this.store.get(id);
654
+ if (current === undefined)
655
+ throw new ContinuationProtocolError(404, "not_found", "Continuation not found.");
656
+ if (current.state !== "delivery_unknown") {
657
+ throw new ContinuationProtocolError(409, "not_delivery_unknown", "Only delivery_unknown continuations can be resolved.");
658
+ }
659
+ let history;
660
+ if (outcome.kind === "delivered") {
661
+ if (this.options.recordHistory === undefined
662
+ || current.replyToConversationId === undefined
663
+ || current.synthesizedText === undefined) {
664
+ history = { recorded: false, code: "history_record_unavailable_after_ambiguous_delivery" };
665
+ }
666
+ else {
667
+ try {
668
+ history = await this.runBoundedOperation("delivery", this.limits.deliveryTimeoutMs, async () => await this.options.recordHistory?.({
669
+ continuationId: current.continuationId,
670
+ conversationId: current.replyToConversationId,
671
+ text: current.synthesizedText,
672
+ deliveryKey: `continuation:${current.continuationId}`,
673
+ }) ?? { recorded: false, code: "history_record_unavailable_after_ambiguous_delivery" });
674
+ }
675
+ catch (error) {
676
+ if (error instanceof ContinuationServiceStoppingError)
677
+ throw error;
678
+ history = {
679
+ recorded: false,
680
+ code: error instanceof ContinuationOperationTimeoutError
681
+ ? "history_record_timeout_after_ambiguous_delivery"
682
+ : "history_record_failed_after_ambiguous_delivery",
683
+ };
684
+ }
685
+ }
686
+ }
687
+ const record = await this.store.mutate((records) => {
688
+ const mutable = requireRecord(records, id);
689
+ if (mutable.state !== "delivery_unknown") {
690
+ throw new ContinuationProtocolError(409, "not_delivery_unknown", "Only delivery_unknown continuations can be resolved.");
691
+ }
692
+ const at = this.now().toISOString();
693
+ mutable.updatedAt = at;
694
+ delete mutable.deliveryStartedAt;
695
+ if (outcome.kind === "delivered") {
696
+ const recorded = history?.recorded === true;
697
+ mutable.state = "delivered";
698
+ mutable.receipt = {
699
+ kind: "delivered",
700
+ deliveredAt: at,
701
+ ...(outcome.deliveryId === undefined ? {} : { deliveryId: outcome.deliveryId }),
702
+ historyRecorded: recorded,
703
+ ...(recorded ? {} : {
704
+ historyErrorCode: boundedHistoryErrorCode(history?.recorded === false
705
+ ? history.code
706
+ : "history_record_unavailable_after_ambiguous_delivery"),
707
+ }),
708
+ };
709
+ delete mutable.lastError;
710
+ }
711
+ else if (outcome.kind === "not_delivered") {
712
+ mutable.state = "ready_to_deliver";
713
+ mutable.nextAttemptAt = at;
714
+ delete mutable.lastError;
715
+ }
716
+ else {
717
+ mutable.state = "dead_lettered";
718
+ mutable.lastError = errorRecord("operator_dead_lettered", "Ambiguous delivery was dead-lettered by the operator.", at);
719
+ }
720
+ return structuredClone(mutable);
721
+ });
722
+ if (outcome.kind === "not_delivered" && this.autoProcess)
723
+ void this.processDue().catch(() => undefined);
724
+ return statusOfRequired(record);
725
+ }
726
+ finally {
727
+ this.resolvingUnknown.delete(id);
728
+ }
729
+ }
730
+ async health() {
731
+ const [records, storage] = await Promise.all([this.store.list(), this.store.stats()]);
732
+ const counts = Object.fromEntries(CONTINUATION_STATES.map((state) => [state, 0]));
733
+ let oldestPendingAt;
734
+ let due = 0;
735
+ const now = this.now().getTime();
736
+ for (const record of records) {
737
+ counts[record.state] += 1;
738
+ if (!TERMINAL_CONTINUATION_STATES.has(record.state)) {
739
+ if (oldestPendingAt === undefined || record.createdAt < oldestPendingAt)
740
+ oldestPendingAt = record.createdAt;
741
+ if (record.nextAttemptAt === undefined || Date.parse(record.nextAttemptAt) <= now)
742
+ due += 1;
743
+ }
744
+ }
745
+ const pending = records.length - [...TERMINAL_CONTINUATION_STATES].reduce((sum, state) => sum + counts[state], 0);
746
+ const status = counts.delivery_unknown > 0 || counts.dead_lettered > 0
747
+ ? "unhealthy"
748
+ : pending > 0 || counts.expired > 0 || storage.historyDegraded > 0
749
+ ? "degraded"
750
+ : "healthy";
751
+ return {
752
+ status,
753
+ checkedAt: this.now().toISOString(),
754
+ counts,
755
+ pending,
756
+ due,
757
+ storage,
758
+ ...(oldestPendingAt === undefined ? {} : { oldestPendingAt }),
759
+ };
760
+ }
761
+ async handleRequest(request, response) {
762
+ response.setHeader("cache-control", "no-store");
763
+ response.setHeader("x-content-type-options", "nosniff");
764
+ if (this.stopped) {
765
+ throw new ContinuationProtocolError(503, "service_stopping", "Continuation service is stopping.");
766
+ }
767
+ const url = new URL(request.url ?? "/", this.baseUrl);
768
+ if (request.method === "POST" && url.pathname === "/v1/continuations/claim") {
769
+ const token = requireBearer(request);
770
+ const binding = this.claimBindings.get(token);
771
+ if (binding === undefined)
772
+ throw new ContinuationProtocolError(401, "invalid_claim_capability", "Invalid or expired claim capability.");
773
+ const body = await readJson(request, MAX_CLAIM_BODY_BYTES);
774
+ sendJson(response, 200, await this.claim(binding, body));
775
+ return;
776
+ }
777
+ if (request.method === "POST" && url.pathname === "/v1/continuations/detached/claim") {
778
+ const serviceName = requireHeader(request, "x-mono-agent-service-name");
779
+ this.authorizeDetached(serviceName, requireBearer(request));
780
+ const body = await readJson(request, MAX_CLAIM_BODY_BYTES);
781
+ sendJson(response, 200, await this.claimDetached(serviceName, body));
782
+ return;
783
+ }
784
+ const resultMatch = /^\/v1\/continuations\/([^/]+)\/result$/u.exec(url.pathname);
785
+ if ((request.method === "PUT" || request.method === "POST") && resultMatch?.[1] !== undefined) {
786
+ const body = await readJson(request, this.maxResultBytes);
787
+ sendJson(response, 202, await this.acceptResult(resultMatch[1], requireBearer(request), body));
788
+ return;
789
+ }
790
+ const statusMatch = /^\/v1\/continuations\/([^/]+)\/status$/u.exec(url.pathname);
791
+ if (request.method === "GET" && statusMatch?.[1] !== undefined) {
792
+ const record = await this.authorizedRecord(statusMatch[1], requireBearer(request));
793
+ sendJson(response, 200, statusOfRequired(record));
794
+ return;
795
+ }
796
+ if (url.pathname === "/v1/operator/continuations") {
797
+ this.authorizeOperator(request);
798
+ if (request.method !== "GET")
799
+ throw new ContinuationProtocolError(405, "method_not_allowed", "Method not allowed.");
800
+ sendJson(response, 200, operatorContinuationPage(await this.store.list(), url, this.limits.operatorPageSize));
801
+ return;
802
+ }
803
+ if (url.pathname === "/v1/operator/health") {
804
+ this.authorizeOperator(request);
805
+ if (request.method !== "GET")
806
+ throw new ContinuationProtocolError(405, "method_not_allowed", "Method not allowed.");
807
+ sendJson(response, 200, await this.health());
808
+ return;
809
+ }
810
+ const operatorMatch = /^\/v1\/operator\/continuations\/([^/]+)\/(retry|cancel|resolve)$/u.exec(url.pathname);
811
+ if (request.method === "POST" && operatorMatch?.[1] !== undefined && operatorMatch[2] !== undefined) {
812
+ this.authorizeOperator(request);
813
+ const body = await readJson(request, MAX_CLAIM_BODY_BYTES);
814
+ if (operatorMatch[2] === "retry") {
815
+ sendJson(response, 200, await this.retry(operatorMatch[1], { allowUnknown: asObject(body).allowUnknown === true }));
816
+ }
817
+ else if (operatorMatch[2] === "cancel") {
818
+ sendJson(response, 200, await this.cancel(operatorMatch[1]));
819
+ }
820
+ else {
821
+ const value = asObject(body);
822
+ const kind = value.kind;
823
+ if (kind !== "delivered" && kind !== "not_delivered" && kind !== "dead_lettered") {
824
+ throw new ContinuationProtocolError(400, "invalid_resolution", "Resolution kind is invalid.");
825
+ }
826
+ sendJson(response, 200, await this.resolveUnknown(operatorMatch[1], {
827
+ kind,
828
+ ...(kind === "delivered" && typeof value.deliveryId === "string" ? { deliveryId: value.deliveryId } : {}),
829
+ }));
830
+ }
831
+ return;
832
+ }
833
+ throw new ContinuationProtocolError(404, "not_found", "Continuation endpoint not found.");
834
+ }
835
+ async claim(binding, body) {
836
+ const claim = parseClaim(body, this.now(), this.maxDeadlineMs);
837
+ return await this.claimBound(binding, claim);
838
+ }
839
+ async claimDetached(serviceName, body) {
840
+ const object = asObject(body);
841
+ const routeName = requiredStringField(object, "route");
842
+ const route = this.namedRoutes[routeName];
843
+ if (route === undefined)
844
+ throw new ContinuationProtocolError(403, "unknown_named_route", "Detached claim route is not configured.");
845
+ const claim = parseClaim(body, this.now(), this.maxDeadlineMs);
846
+ const originConversationId = route.conversationId ?? `continuation-route:${routeName}`;
847
+ const fingerprint = continuationDigest([
848
+ "v1-detached",
849
+ serviceName,
850
+ routeName,
851
+ route.mode,
852
+ route.conversationId ?? "",
853
+ ].join("\0"));
854
+ return await this.claimBound({
855
+ serverName: `detached:${serviceName}`,
856
+ originRunId: `detached:${serviceName}:${claim.taskKey}`,
857
+ originConversationId,
858
+ ...(route.conversationId === undefined ? {} : { replyToConversationId: route.conversationId }),
859
+ mode: route.mode,
860
+ fingerprint,
861
+ }, claim, routeName);
862
+ }
863
+ async claimBound(binding, claim, routeName) {
864
+ let created = false;
865
+ const record = await this.store.mutate((records) => {
866
+ const existing = [...records.values()].find((candidate) => candidate.serverName === binding.serverName
867
+ && candidate.originRunId === binding.originRunId
868
+ && candidate.taskKey === claim.taskKey);
869
+ if (existing !== undefined) {
870
+ if (existing.taskHash !== claim.taskHash
871
+ || existing.claimFingerprint !== binding.fingerprint
872
+ || existing.deadline !== claim.deadline) {
873
+ throw new ContinuationProtocolError(409, "claim_conflict", "taskKey was already claimed with different immutable inputs.");
874
+ }
875
+ return structuredClone(existing);
876
+ }
877
+ const active = [...records.values()].filter((candidate) => !TERMINAL_CONTINUATION_STATES.has(candidate.state));
878
+ if (active.length >= this.limits.maxActiveRecords) {
879
+ throw new ContinuationProtocolError(429, "active_continuation_limit", `The service already has its maximum ${String(this.limits.maxActiveRecords)} active continuations.`);
880
+ }
881
+ const activeForOrigin = active.filter((candidate) => candidate.claimFingerprint === binding.fingerprint).length;
882
+ if (activeForOrigin >= this.limits.maxActivePerOrigin) {
883
+ throw new ContinuationProtocolError(429, "active_origin_limit", `This claim origin already has its maximum ${String(this.limits.maxActivePerOrigin)} active continuations.`);
884
+ }
885
+ const now = this.now().toISOString();
886
+ const continuationId = randomUUID();
887
+ const token = this.deriveResultToken(continuationId, claim.taskHash);
888
+ const next = {
889
+ continuationId,
890
+ serverName: binding.serverName,
891
+ originRunId: binding.originRunId,
892
+ originConversationId: binding.originConversationId,
893
+ ...(binding.replyToConversationId === undefined ? {} : { replyToConversationId: binding.replyToConversationId }),
894
+ ...(binding.historyBoundary === undefined ? {} : { historyBoundary: binding.historyBoundary }),
895
+ mode: binding.mode,
896
+ ...(routeName === undefined ? {} : { routeName }),
897
+ taskKey: claim.taskKey,
898
+ taskHash: claim.taskHash,
899
+ claimFingerprint: binding.fingerprint,
900
+ resultTokenHash: continuationDigest(token),
901
+ createdAt: now,
902
+ updatedAt: now,
903
+ deadline: claim.deadline,
904
+ state: "claimed",
905
+ synthesisAttempts: 0,
906
+ deliveryAttempts: 0,
907
+ };
908
+ records.set(continuationId, next);
909
+ created = true;
910
+ return structuredClone(next);
911
+ });
912
+ const token = this.deriveResultToken(record.continuationId, record.taskHash);
913
+ return {
914
+ continuationId: record.continuationId,
915
+ resultUrl: `${this.baseUrl}/v1/continuations/${encodeURIComponent(record.continuationId)}/result`,
916
+ statusUrl: `${this.baseUrl}/v1/continuations/${encodeURIComponent(record.continuationId)}/status`,
917
+ token,
918
+ expiresAt: record.deadline,
919
+ fingerprint: record.claimFingerprint,
920
+ ...(created ? {} : { replayed: true }),
921
+ };
922
+ }
923
+ async acceptResult(id, token, body) {
924
+ const object = asObject(body);
925
+ const idempotencyKey = requiredStringField(object, "idempotencyKey", 256);
926
+ if (!("payload" in object))
927
+ throw new ContinuationProtocolError(400, "missing_payload", "Result payload is required.");
928
+ const serialized = canonicalContinuationJson(object.payload);
929
+ const payloadHash = continuationDigest(serialized);
930
+ const providedHash = typeof object.payloadHash === "string" ? object.payloadHash : undefined;
931
+ if (providedHash !== undefined && providedHash !== payloadHash) {
932
+ throw new ContinuationProtocolError(400, "payload_hash_mismatch", "Result payload hash does not match the payload.");
933
+ }
934
+ const now = this.now();
935
+ const record = await this.store.mutate((records) => {
936
+ const current = requireRecord(records, id);
937
+ authorizeRecordToken(current, token);
938
+ if (current.resultIdempotencyKey !== undefined) {
939
+ if (current.resultIdempotencyKey !== idempotencyKey || current.resultPayloadHash !== payloadHash) {
940
+ throw new ContinuationProtocolError(409, "result_conflict", "Continuation already has a different immutable result.");
941
+ }
942
+ return structuredClone(current);
943
+ }
944
+ if (current.state !== "claimed") {
945
+ throw new ContinuationProtocolError(409, "result_not_accepted", `Continuation is ${current.state}.`);
946
+ }
947
+ if (Date.parse(current.deadline) <= now.getTime()) {
948
+ expire(current, now.toISOString());
949
+ throw new ContinuationProtocolError(410, "continuation_expired", "Continuation deadline has passed.");
950
+ }
951
+ current.resultIdempotencyKey = idempotencyKey;
952
+ current.resultPayloadHash = payloadHash;
953
+ current.resultPayload = structuredClone(object.payload);
954
+ current.state = "result_received";
955
+ current.updatedAt = now.toISOString();
956
+ return structuredClone(current);
957
+ });
958
+ if (this.autoProcess)
959
+ void this.processDue().catch(() => undefined);
960
+ return { continuationId: id, state: record.state, accepted: true };
961
+ }
962
+ async authorizedRecord(id, token) {
963
+ const record = await this.store.get(id);
964
+ if (record === undefined)
965
+ throw new ContinuationProtocolError(404, "not_found", "Continuation not found.");
966
+ authorizeRecordToken(record, token);
967
+ return record;
968
+ }
969
+ authorizeDetached(serviceName, token) {
970
+ const expected = this.detachedServiceTokenHashes.get(serviceName);
971
+ if (expected === undefined || !continuationTokenMatches(token, expected)) {
972
+ throw new ContinuationProtocolError(401, "invalid_service_capability", "Invalid detached-service capability.");
973
+ }
974
+ }
975
+ authorizeOperator(request) {
976
+ if (!continuationTokenMatches(requireBearer(request), continuationDigest(this.operatorToken))) {
977
+ throw new ContinuationProtocolError(401, "invalid_operator_capability", "Invalid operator capability.");
978
+ }
979
+ }
980
+ deriveResultToken(id, taskHash) {
981
+ return createHmac("sha256", this.secret).update(`continuation-result\0${id}\0${taskHash}`).digest("base64url");
982
+ }
983
+ handleRequestError(response, error) {
984
+ if (response.headersSent || response.writableEnded)
985
+ return;
986
+ const protocol = error instanceof ContinuationProtocolError
987
+ ? error
988
+ : new ContinuationProtocolError(500, "internal_error", "Continuation request failed.");
989
+ if (!(error instanceof ContinuationProtocolError)) {
990
+ this.logger?.error?.("Continuation request failed.", { reason: safeReason(error) });
991
+ }
992
+ sendJson(response, protocol.status, { error: { code: protocol.code, message: protocol.message } });
993
+ }
994
+ throwIfStopping() {
995
+ if (this.stopped || this.lifecycleAbort.signal.aborted) {
996
+ throw new ContinuationServiceStoppingError();
997
+ }
998
+ }
999
+ async runBoundedOperation(phase, timeoutMs, operation) {
1000
+ this.throwIfStopping();
1001
+ let timeout;
1002
+ let onAbort;
1003
+ const timedOut = new Promise((_resolve, reject) => {
1004
+ timeout = setTimeout(() => reject(new ContinuationOperationTimeoutError(phase, timeoutMs)), timeoutMs);
1005
+ timeout.unref?.();
1006
+ });
1007
+ const stopped = new Promise((_resolve, reject) => {
1008
+ onAbort = () => reject(new ContinuationServiceStoppingError());
1009
+ this.lifecycleAbort.signal.addEventListener("abort", onAbort, { once: true });
1010
+ });
1011
+ try {
1012
+ return await Promise.race([Promise.resolve().then(operation), timedOut, stopped]);
1013
+ }
1014
+ finally {
1015
+ if (timeout !== undefined)
1016
+ clearTimeout(timeout);
1017
+ if (onAbort !== undefined)
1018
+ this.lifecycleAbort.signal.removeEventListener("abort", onAbort);
1019
+ }
1020
+ }
1021
+ async stop() {
1022
+ if (this.stopped)
1023
+ return;
1024
+ this.stopped = true;
1025
+ this.lifecycleAbort.abort();
1026
+ if (this.worker !== undefined)
1027
+ clearInterval(this.worker);
1028
+ this.worker = undefined;
1029
+ this.claimBindings.clear();
1030
+ const server = this.server;
1031
+ this.server = undefined;
1032
+ try {
1033
+ if (server !== undefined) {
1034
+ await closeContinuationServer(server);
1035
+ }
1036
+ await this.dispatchTail.catch(() => undefined);
1037
+ await Promise.allSettled([...this.inFlight.values()]);
1038
+ }
1039
+ finally {
1040
+ await this.releaseStoreLock();
1041
+ }
1042
+ }
1043
+ }
1044
+ /** Derive the restart-stable local operator capability from the owner-only service secret. */
1045
+ export function continuationOperatorToken(secret) {
1046
+ return createHmac("sha256", secret).update("mono-agent-continuation-operator-v1").digest("base64url");
1047
+ }
1048
+ function parseClaim(body, now, maxDeadlineMs) {
1049
+ const object = asObject(body);
1050
+ const taskKey = requiredStringField(object, "taskKey", MAX_TASK_KEY_CHARS);
1051
+ const taskHash = requiredStringField(object, "taskHash", 256);
1052
+ const deadline = requiredStringField(object, "deadline", 64);
1053
+ const deadlineMs = Date.parse(deadline);
1054
+ if (!Number.isFinite(deadlineMs) || deadlineMs <= now.getTime()) {
1055
+ throw new ContinuationProtocolError(400, "invalid_deadline", "Claim deadline must be a future ISO timestamp.");
1056
+ }
1057
+ if (deadlineMs - now.getTime() > maxDeadlineMs) {
1058
+ throw new ContinuationProtocolError(400, "deadline_too_far", "Claim deadline exceeds the configured maximum.");
1059
+ }
1060
+ return { taskKey, taskHash, deadline: new Date(deadlineMs).toISOString() };
1061
+ }
1062
+ function statusOf(record) {
1063
+ return record === undefined ? undefined : statusOfRequired(record);
1064
+ }
1065
+ function statusOfRequired(record) {
1066
+ return {
1067
+ continuationId: record.continuationId,
1068
+ state: record.state,
1069
+ mode: record.mode,
1070
+ taskKey: record.taskKey,
1071
+ createdAt: record.createdAt,
1072
+ updatedAt: record.updatedAt,
1073
+ deadline: record.deadline,
1074
+ attempts: { synthesis: record.synthesisAttempts, delivery: record.deliveryAttempts },
1075
+ ...(record.nextAttemptAt === undefined ? {} : { nextAttemptAt: record.nextAttemptAt }),
1076
+ ...(record.lastError === undefined ? {} : { lastError: record.lastError }),
1077
+ ...(record.receipt === undefined ? {} : { receipt: record.receipt }),
1078
+ };
1079
+ }
1080
+ function requireRecord(records, id) {
1081
+ const record = records.get(id);
1082
+ if (record === undefined)
1083
+ throw new ContinuationProtocolError(404, "not_found", "Continuation not found.");
1084
+ return record;
1085
+ }
1086
+ function synthesisInput(record) {
1087
+ return {
1088
+ continuationId: record.continuationId,
1089
+ originConversationId: record.originConversationId,
1090
+ originRunId: record.originRunId,
1091
+ ...(record.replyToConversationId === undefined ? {} : { replyToConversationId: record.replyToConversationId }),
1092
+ ...(record.historyBoundary === undefined ? {} : { historyBoundary: record.historyBoundary }),
1093
+ mode: record.mode,
1094
+ payload: record.resultPayload,
1095
+ };
1096
+ }
1097
+ function requireLease(record, owner) {
1098
+ if (record.leaseOwner !== owner)
1099
+ throw new ContinuationProtocolError(409, "lease_lost", "Continuation processing lease was lost.");
1100
+ }
1101
+ function clearLease(record) {
1102
+ delete record.leaseOwner;
1103
+ delete record.leaseUntil;
1104
+ }
1105
+ function expire(record, at) {
1106
+ record.state = "expired";
1107
+ record.updatedAt = at;
1108
+ record.lastError = errorRecord("deadline_expired", "Continuation deadline passed before delivery completed.", at);
1109
+ clearLease(record);
1110
+ }
1111
+ function errorRecord(code, reason, at) {
1112
+ return { code: bounded(code, 128), reason: bounded(reason, 1_000), at };
1113
+ }
1114
+ function authorizeRecordToken(record, token) {
1115
+ if (!continuationTokenMatches(token, record.resultTokenHash)) {
1116
+ throw new ContinuationProtocolError(401, "invalid_result_capability", "Invalid continuation result capability.");
1117
+ }
1118
+ }
1119
+ function validateNamedRoutes(routes) {
1120
+ for (const [name, route] of Object.entries(routes)) {
1121
+ if (name.trim().length === 0 || name.length > 128)
1122
+ throw new Error("Continuation route names must be 1-128 characters.");
1123
+ if ((route.mode === "notify_if_actionable" || route.mode === "capture") && !route.conversationId?.trim()) {
1124
+ throw new Error(`Continuation route ${name} requires conversationId.`);
1125
+ }
1126
+ if (route.mode === "silent" && route.conversationId !== undefined) {
1127
+ throw new Error(`Continuation route ${name} cannot set conversationId for mode ${route.mode}.`);
1128
+ }
1129
+ }
1130
+ }
1131
+ function resolveContinuationLimits(input) {
1132
+ const maxActiveRecords = input?.maxActiveRecords ?? DEFAULT_CONTINUATION_LIMITS.maxActiveRecords;
1133
+ const limits = {
1134
+ ...DEFAULT_CONTINUATION_LIMITS,
1135
+ ...input,
1136
+ maxActiveRecords,
1137
+ maxActivePerOrigin: input?.maxActivePerOrigin
1138
+ ?? Math.min(DEFAULT_CONTINUATION_LIMITS.maxActivePerOrigin, maxActiveRecords),
1139
+ };
1140
+ for (const [name, value] of Object.entries(limits)) {
1141
+ if (!Number.isSafeInteger(value) || value <= 0) {
1142
+ throw new Error(`Continuation limit ${name} must be a positive safe integer.`);
1143
+ }
1144
+ }
1145
+ if (limits.maxActiveRecords > 1_000_000)
1146
+ throw new Error("Continuation maxActiveRecords cannot exceed 1000000.");
1147
+ if (limits.maxActivePerOrigin > limits.maxActiveRecords) {
1148
+ throw new Error("Continuation maxActivePerOrigin cannot exceed maxActiveRecords.");
1149
+ }
1150
+ if (limits.maxConcurrent > 256)
1151
+ throw new Error("Continuation maxConcurrent cannot exceed 256.");
1152
+ if (limits.synthesisTimeoutMs > 24 * 60 * 60 * 1_000 || limits.deliveryTimeoutMs > 24 * 60 * 60 * 1_000) {
1153
+ throw new Error("Continuation operation timeouts cannot exceed 24 hours.");
1154
+ }
1155
+ if (limits.operatorPageSize > MAX_OPERATOR_PAGE_SIZE) {
1156
+ throw new Error(`Continuation operatorPageSize cannot exceed ${String(MAX_OPERATOR_PAGE_SIZE)}.`);
1157
+ }
1158
+ return limits;
1159
+ }
1160
+ function operatorContinuationPage(records, url, configuredPageSize) {
1161
+ const rawLimit = url.searchParams.get("limit");
1162
+ let limit = configuredPageSize;
1163
+ if (rawLimit !== null) {
1164
+ if (!/^[1-9][0-9]*$/u.test(rawLimit)) {
1165
+ throw new ContinuationProtocolError(400, "invalid_page_limit", "Operator list limit must be a positive integer.");
1166
+ }
1167
+ limit = Number(rawLimit);
1168
+ if (!Number.isSafeInteger(limit) || limit > configuredPageSize || limit > MAX_OPERATOR_PAGE_SIZE) {
1169
+ throw new ContinuationProtocolError(400, "invalid_page_limit", `Operator list limit cannot exceed ${String(Math.min(configuredPageSize, MAX_OPERATOR_PAGE_SIZE))}.`);
1170
+ }
1171
+ }
1172
+ const cursor = parseOperatorCursor(url.searchParams.get("cursor"));
1173
+ const ordered = [...records].sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.continuationId.localeCompare(left.continuationId));
1174
+ const eligible = cursor === undefined
1175
+ ? ordered
1176
+ : ordered.filter((record) => record.createdAt < cursor.createdAt
1177
+ || (record.createdAt === cursor.createdAt && record.continuationId < cursor.continuationId));
1178
+ const selected = eligible.slice(0, limit);
1179
+ const last = selected.at(-1);
1180
+ return {
1181
+ continuations: selected.map(statusOfRequired),
1182
+ pageSize: limit,
1183
+ ...(eligible.length <= limit || last === undefined
1184
+ ? {}
1185
+ : { nextCursor: encodeOperatorCursor(last) }),
1186
+ };
1187
+ }
1188
+ function encodeOperatorCursor(record) {
1189
+ return Buffer.from(JSON.stringify([record.createdAt, record.continuationId]), "utf8").toString("base64url");
1190
+ }
1191
+ function parseOperatorCursor(value) {
1192
+ if (value === null)
1193
+ return undefined;
1194
+ try {
1195
+ if (value.length === 0 || value.length > 512)
1196
+ throw new Error("invalid");
1197
+ const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
1198
+ if (!Array.isArray(decoded)
1199
+ || decoded.length !== 2
1200
+ || typeof decoded[0] !== "string"
1201
+ || !Number.isFinite(Date.parse(decoded[0]))
1202
+ || typeof decoded[1] !== "string"
1203
+ || decoded[1].length === 0
1204
+ || decoded[1].length > 128) {
1205
+ throw new Error("invalid");
1206
+ }
1207
+ return { createdAt: decoded[0], continuationId: decoded[1] };
1208
+ }
1209
+ catch {
1210
+ throw new ContinuationProtocolError(400, "invalid_page_cursor", "Operator list cursor is invalid.");
1211
+ }
1212
+ }
1213
+ async function closeContinuationServer(server) {
1214
+ await new Promise((resolveClose, reject) => {
1215
+ let forced;
1216
+ const finish = (error) => {
1217
+ if (forced !== undefined)
1218
+ clearTimeout(forced);
1219
+ if (error === undefined)
1220
+ resolveClose();
1221
+ else
1222
+ reject(error);
1223
+ };
1224
+ server.close((error) => finish(error));
1225
+ server.closeIdleConnections?.();
1226
+ forced = setTimeout(() => {
1227
+ // An incomplete/slow request must not hold config reload or process
1228
+ // shutdown forever. Result and claim writes are idempotent, so callers
1229
+ // can safely retry a connection terminated at this boundary.
1230
+ server.closeAllConnections?.();
1231
+ }, 5_000);
1232
+ });
1233
+ }
1234
+ function requireBearer(request) {
1235
+ const authorization = request.headers.authorization;
1236
+ if (typeof authorization !== "string" || !authorization.startsWith("Bearer ")) {
1237
+ throw new ContinuationProtocolError(401, "missing_capability", "Bearer capability is required.");
1238
+ }
1239
+ const token = authorization.slice("Bearer ".length).trim();
1240
+ if (token.length === 0 || token.length > 512) {
1241
+ throw new ContinuationProtocolError(401, "invalid_capability", "Bearer capability is invalid.");
1242
+ }
1243
+ return token;
1244
+ }
1245
+ function requireHeader(request, name) {
1246
+ const value = request.headers[name];
1247
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 256) {
1248
+ throw new ContinuationProtocolError(400, "missing_service_name", "Detached service name header is required.");
1249
+ }
1250
+ return value;
1251
+ }
1252
+ async function readJson(request, maxBytes) {
1253
+ const chunks = [];
1254
+ let bytes = 0;
1255
+ for await (const chunk of request) {
1256
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1257
+ bytes += buffer.length;
1258
+ if (bytes > maxBytes)
1259
+ throw new ContinuationProtocolError(413, "payload_too_large", "Request body exceeds the configured limit.");
1260
+ chunks.push(buffer);
1261
+ }
1262
+ try {
1263
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1264
+ }
1265
+ catch {
1266
+ throw new ContinuationProtocolError(400, "invalid_json", "Request body must be valid JSON.");
1267
+ }
1268
+ }
1269
+ function asObject(value) {
1270
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1271
+ throw new ContinuationProtocolError(400, "invalid_body", "Request body must be a JSON object.");
1272
+ }
1273
+ return value;
1274
+ }
1275
+ function requiredStringField(object, key, max = 512) {
1276
+ const value = object[key];
1277
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > max) {
1278
+ throw new ContinuationProtocolError(400, `invalid_${key}`, `${key} must be a non-empty string up to ${String(max)} characters.`);
1279
+ }
1280
+ return value;
1281
+ }
1282
+ function sendJson(response, status, body) {
1283
+ if (response.writableEnded)
1284
+ return;
1285
+ const encoded = JSON.stringify(body);
1286
+ response.statusCode = status;
1287
+ response.setHeader("content-type", "application/json; charset=utf-8");
1288
+ response.setHeader("content-length", Buffer.byteLength(encoded));
1289
+ response.end(encoded);
1290
+ }
1291
+ function isLoopbackHost(host) {
1292
+ const normalized = host.trim().toLowerCase();
1293
+ return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
1294
+ }
1295
+ function formatHost(host) {
1296
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
1297
+ }
1298
+ function backoffMs(attempt, base, cap) {
1299
+ const exponential = Math.min(cap, base * (2 ** Math.max(0, attempt - 1)));
1300
+ const deterministicJitter = 0.75 + ((attempt * 1103515245 + 12345) % 500) / 1_000;
1301
+ return Math.max(base, Math.floor(exponential * deterministicJitter));
1302
+ }
1303
+ function bounded(value, max) {
1304
+ return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
1305
+ }
1306
+ function safeReason(error) {
1307
+ return bounded(error instanceof Error ? error.message : String(error), 1_000);
1308
+ }
1309
+ function boundedHistoryErrorCode(value) {
1310
+ const normalized = value.trim().slice(0, 128);
1311
+ return normalized.length === 0 ? "history_record_failed" : normalized;
1312
+ }
1313
+ //# sourceMappingURL=continuation-service.js.map