@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.
- package/LICENSE +674 -0
- package/README.md +222 -26
- package/dist/adapter-send-tools-main.js +7 -1
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +19 -1
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +127 -13
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +15 -2
- package/dist/app-config.js.map +1 -1
- package/dist/app-controller-channels.d.ts +8 -0
- package/dist/app-controller-channels.d.ts.map +1 -0
- package/dist/app-controller-channels.js +207 -0
- package/dist/app-controller-channels.js.map +1 -0
- package/dist/app-controller-continuation.d.ts +26 -0
- package/dist/app-controller-continuation.d.ts.map +1 -0
- package/dist/app-controller-continuation.js +198 -0
- package/dist/app-controller-continuation.js.map +1 -0
- package/dist/app-controller-lifecycle.d.ts +6 -0
- package/dist/app-controller-lifecycle.d.ts.map +1 -0
- package/dist/app-controller-lifecycle.js +81 -0
- package/dist/app-controller-lifecycle.js.map +1 -0
- package/dist/app-controller-maintenance.d.ts +13 -0
- package/dist/app-controller-maintenance.d.ts.map +1 -0
- package/dist/app-controller-maintenance.js +180 -0
- package/dist/app-controller-maintenance.js.map +1 -0
- package/dist/app-controller-memory-health.d.ts +12 -0
- package/dist/app-controller-memory-health.d.ts.map +1 -0
- package/dist/app-controller-memory-health.js +160 -0
- package/dist/app-controller-memory-health.js.map +1 -0
- package/dist/app-controller-memory.d.ts +9 -0
- package/dist/app-controller-memory.d.ts.map +1 -0
- package/dist/app-controller-memory.js +83 -0
- package/dist/app-controller-memory.js.map +1 -0
- package/dist/app-controller-responder.d.ts +20 -0
- package/dist/app-controller-responder.d.ts.map +1 -0
- package/dist/app-controller-responder.js +246 -0
- package/dist/app-controller-responder.js.map +1 -0
- package/dist/app-controller-traceability.d.ts +25 -0
- package/dist/app-controller-traceability.d.ts.map +1 -0
- package/dist/app-controller-traceability.js +418 -0
- package/dist/app-controller-traceability.js.map +1 -0
- package/dist/app-controller-utils.d.ts +38 -0
- package/dist/app-controller-utils.d.ts.map +1 -0
- package/dist/app-controller-utils.js +185 -0
- package/dist/app-controller-utils.js.map +1 -0
- package/dist/app-controller.d.ts +400 -0
- package/dist/app-controller.d.ts.map +1 -0
- package/dist/app-controller.js +336 -0
- package/dist/app-controller.js.map +1 -0
- package/dist/app.d.ts +7 -115
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +6 -1912
- package/dist/app.js.map +1 -1
- package/dist/artifact-retention.d.ts +4 -0
- package/dist/artifact-retention.d.ts.map +1 -1
- package/dist/artifact-retention.js +38 -0
- package/dist/artifact-retention.js.map +1 -1
- package/dist/backfill.d.ts +3 -1
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +5 -1
- package/dist/backfill.js.map +1 -1
- package/dist/background-runtime.d.ts +27 -0
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +383 -205
- package/dist/background-runtime.js.map +1 -1
- package/dist/background-worker-lease.d.ts.map +1 -1
- package/dist/background-worker-lease.js +41 -241
- package/dist/background-worker-lease.js.map +1 -1
- package/dist/background.d.ts +31 -9
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +543 -272
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.d.ts +11 -0
- package/dist/channel-drivers/cron.d.ts.map +1 -0
- package/dist/channel-drivers/cron.js +168 -0
- package/dist/channel-drivers/cron.js.map +1 -0
- package/dist/channel-drivers/live.d.ts +11 -0
- package/dist/channel-drivers/live.d.ts.map +1 -0
- package/dist/channel-drivers/live.js +47 -0
- package/dist/channel-drivers/live.js.map +1 -0
- package/dist/channel-drivers/native-notify.d.ts +27 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -0
- package/dist/channel-drivers/native-notify.js +94 -0
- package/dist/channel-drivers/native-notify.js.map +1 -0
- package/dist/channel-drivers/openai-api.d.ts +7 -0
- package/dist/channel-drivers/openai-api.d.ts.map +1 -0
- package/dist/channel-drivers/openai-api.js +63 -0
- package/dist/channel-drivers/openai-api.js.map +1 -0
- package/dist/channel-drivers/shared.d.ts +4 -0
- package/dist/channel-drivers/shared.d.ts.map +1 -0
- package/dist/channel-drivers/shared.js +5 -0
- package/dist/channel-drivers/shared.js.map +1 -0
- package/dist/channel-drivers/slack.d.ts +14 -0
- package/dist/channel-drivers/slack.d.ts.map +1 -0
- package/dist/channel-drivers/slack.js +227 -0
- package/dist/channel-drivers/slack.js.map +1 -0
- package/dist/channel-drivers/telegram.d.ts +11 -0
- package/dist/channel-drivers/telegram.d.ts.map +1 -0
- package/dist/channel-drivers/telegram.js +244 -0
- package/dist/channel-drivers/telegram.js.map +1 -0
- package/dist/channel-drivers/tui.d.ts +15 -0
- package/dist/channel-drivers/tui.d.ts.map +1 -0
- package/dist/channel-drivers/tui.js +108 -0
- package/dist/channel-drivers/tui.js.map +1 -0
- package/dist/channel-drivers/webhook.d.ts +7 -0
- package/dist/channel-drivers/webhook.d.ts.map +1 -0
- package/dist/channel-drivers/webhook.js +115 -0
- package/dist/channel-drivers/webhook.js.map +1 -0
- package/dist/channels.d.ts +21 -81
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +11 -1174
- package/dist/channels.js.map +1 -1
- package/dist/cli-args.d.ts +121 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +532 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-background-command.d.ts +59 -0
- package/dist/cli-background-command.d.ts.map +1 -0
- package/dist/cli-background-command.js +475 -0
- package/dist/cli-background-command.js.map +1 -0
- package/dist/cli-cancellation.d.ts +6 -0
- package/dist/cli-cancellation.d.ts.map +1 -0
- package/dist/cli-cancellation.js +23 -0
- package/dist/cli-cancellation.js.map +1 -0
- package/dist/cli-commands.d.ts +12 -0
- package/dist/cli-commands.d.ts.map +1 -0
- package/dist/cli-commands.js +306 -0
- package/dist/cli-commands.js.map +1 -0
- package/dist/cli-help.d.ts +11 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +262 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-init-command.d.ts +60 -0
- package/dist/cli-init-command.d.ts.map +1 -0
- package/dist/cli-init-command.js +1910 -0
- package/dist/cli-init-command.js.map +1 -0
- package/dist/cli-install-skill-command.d.ts +3 -0
- package/dist/cli-install-skill-command.d.ts.map +1 -0
- package/dist/cli-install-skill-command.js +51 -0
- package/dist/cli-install-skill-command.js.map +1 -0
- package/dist/cli-sandbox-command.d.ts +10 -0
- package/dist/cli-sandbox-command.d.ts.map +1 -0
- package/dist/cli-sandbox-command.js +100 -0
- package/dist/cli-sandbox-command.js.map +1 -0
- package/dist/cli-validate-config-command.d.ts +36 -0
- package/dist/cli-validate-config-command.d.ts.map +1 -0
- package/dist/cli-validate-config-command.js +317 -0
- package/dist/cli-validate-config-command.js.map +1 -0
- package/dist/cli.d.ts +6 -248
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -3792
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts +2 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +34 -8
- package/dist/config-reference.js.map +1 -1
- package/dist/configured-agent.d.ts +23 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +92 -13
- package/dist/configured-agent.js.map +1 -1
- package/dist/continuation-dispatch-worker.d.ts +21 -0
- package/dist/continuation-dispatch-worker.d.ts.map +1 -0
- package/dist/continuation-dispatch-worker.js +425 -0
- package/dist/continuation-dispatch-worker.js.map +1 -0
- package/dist/continuation-http-routes.d.ts +15 -0
- package/dist/continuation-http-routes.d.ts.map +1 -0
- package/dist/continuation-http-routes.js +265 -0
- package/dist/continuation-http-routes.js.map +1 -0
- package/dist/continuation-operator-api.d.ts +18 -0
- package/dist/continuation-operator-api.d.ts.map +1 -0
- package/dist/continuation-operator-api.js +164 -0
- package/dist/continuation-operator-api.js.map +1 -0
- package/dist/continuation-origin-store.d.ts +19 -0
- package/dist/continuation-origin-store.d.ts.map +1 -0
- package/dist/continuation-origin-store.js +273 -0
- package/dist/continuation-origin-store.js.map +1 -0
- package/dist/continuation-service-errors.d.ts +18 -0
- package/dist/continuation-service-errors.d.ts.map +1 -0
- package/dist/continuation-service-errors.js +35 -0
- package/dist/continuation-service-errors.js.map +1 -0
- package/dist/continuation-service-helpers.d.ts +39 -0
- package/dist/continuation-service-helpers.d.ts.map +1 -0
- package/dist/continuation-service-helpers.js +308 -0
- package/dist/continuation-service-helpers.js.map +1 -0
- package/dist/continuation-service-origin.d.ts +55 -0
- package/dist/continuation-service-origin.d.ts.map +1 -0
- package/dist/continuation-service-origin.js +325 -0
- package/dist/continuation-service-origin.js.map +1 -0
- package/dist/continuation-service-types.d.ts +110 -0
- package/dist/continuation-service-types.d.ts.map +1 -0
- package/dist/continuation-service-types.js +11 -0
- package/dist/continuation-service-types.js.map +1 -0
- package/dist/continuation-service.d.ts +4 -97
- package/dist/continuation-service.d.ts.map +1 -1
- package/dist/continuation-service.js +9 -1525
- package/dist/continuation-service.js.map +1 -1
- package/dist/continuation-store-fs.d.ts +15 -0
- package/dist/continuation-store-fs.d.ts.map +1 -0
- package/dist/continuation-store-fs.js +247 -0
- package/dist/continuation-store-fs.js.map +1 -0
- package/dist/continuation-store-policy.d.ts +21 -0
- package/dist/continuation-store-policy.d.ts.map +1 -0
- package/dist/continuation-store-policy.js +278 -0
- package/dist/continuation-store-policy.js.map +1 -0
- package/dist/continuation-store-records.d.ts +9 -0
- package/dist/continuation-store-records.d.ts.map +1 -0
- package/dist/continuation-store-records.js +221 -0
- package/dist/continuation-store-records.js.map +1 -0
- package/dist/continuation-store-types.d.ts +173 -0
- package/dist/continuation-store-types.d.ts.map +1 -0
- package/dist/continuation-store-types.js +26 -0
- package/dist/continuation-store-types.js.map +1 -0
- package/dist/continuation-store.d.ts +4 -138
- package/dist/continuation-store.d.ts.map +1 -1
- package/dist/continuation-store.js +145 -1049
- package/dist/continuation-store.js.map +1 -1
- package/dist/doctor.d.ts +10 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +629 -58
- package/dist/doctor.js.map +1 -1
- package/dist/init.d.ts +2 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +92 -239
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +19 -0
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +130 -0
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/launchd-logs.d.ts +91 -0
- package/dist/launchd-logs.d.ts.map +1 -0
- package/dist/launchd-logs.js +1499 -0
- package/dist/launchd-logs.js.map +1 -0
- package/dist/launchd.d.ts +27 -0
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +89 -10
- package/dist/launchd.js.map +1 -1
- package/dist/memory-command.d.ts +2 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +161 -48
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-recall-settings.d.ts +64 -0
- package/dist/memory-recall-settings.d.ts.map +1 -0
- package/dist/memory-recall-settings.js +51 -0
- package/dist/memory-recall-settings.js.map +1 -0
- package/dist/memory-recall.d.ts +7 -95
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +8 -143
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-rituals.d.ts +3 -3
- package/dist/memory-rituals.d.ts.map +1 -1
- package/dist/memory-rituals.js +16 -111
- package/dist/memory-rituals.js.map +1 -1
- package/dist/modules/catalog.js +2 -2
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/known-tools.d.ts +8 -6
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +8 -6
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/notify-destinations.d.ts +5 -0
- package/dist/notify-destinations.d.ts.map +1 -1
- package/dist/notify-destinations.js +7 -1
- package/dist/notify-destinations.js.map +1 -1
- package/dist/owner-private-lock.d.ts +95 -0
- package/dist/owner-private-lock.d.ts.map +1 -0
- package/dist/owner-private-lock.js +566 -0
- package/dist/owner-private-lock.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +2 -0
- package/dist/pi-oauth-login.d.ts.map +1 -1
- package/dist/pi-oauth-login.js +39 -5
- package/dist/pi-oauth-login.js.map +1 -1
- package/dist/posted-message-index.d.ts +35 -16
- package/dist/posted-message-index.d.ts.map +1 -1
- package/dist/posted-message-index.js +1068 -52
- package/dist/posted-message-index.js.map +1 -1
- package/dist/posted-reply-history.d.ts +17 -0
- package/dist/posted-reply-history.d.ts.map +1 -0
- package/dist/posted-reply-history.js +169 -0
- package/dist/posted-reply-history.js.map +1 -0
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +6 -4
- package/dist/proactive-notify.js.map +1 -1
- package/dist/project-skills.d.ts +4 -0
- package/dist/project-skills.d.ts.map +1 -1
- package/dist/project-skills.js +99 -130
- package/dist/project-skills.js.map +1 -1
- package/dist/provider-setup.d.ts +0 -1
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +0 -3
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +61 -1
- package/dist/readiness-probe-worker.d.ts.map +1 -1
- package/dist/readiness-probe-worker.js +54 -58
- package/dist/readiness-probe-worker.js.map +1 -1
- package/dist/readiness-probe.d.ts +6 -9
- package/dist/readiness-probe.d.ts.map +1 -1
- package/dist/readiness-probe.js +30 -56
- package/dist/readiness-probe.js.map +1 -1
- package/dist/redact-secrets.d.ts +15 -0
- package/dist/redact-secrets.d.ts.map +1 -0
- package/dist/redact-secrets.js +65 -0
- package/dist/redact-secrets.js.map +1 -0
- package/dist/run-history.js +34 -24
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-provenance.d.ts +8 -0
- package/dist/runtime-provenance.d.ts.map +1 -0
- package/dist/runtime-provenance.js +225 -0
- package/dist/runtime-provenance.js.map +1 -0
- package/dist/sandbox-manager.d.ts +4 -1
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox-manager.js +114 -273
- package/dist/sandbox-manager.js.map +1 -1
- package/dist/secure-file-replace.d.ts +49 -0
- package/dist/secure-file-replace.d.ts.map +1 -0
- package/dist/secure-file-replace.js +270 -0
- package/dist/secure-file-replace.js.map +1 -0
- package/dist/seen-conversations.d.ts +32 -5
- package/dist/seen-conversations.d.ts.map +1 -1
- package/dist/seen-conversations.js +145 -15
- package/dist/seen-conversations.js.map +1 -1
- package/dist/sessions.d.ts +20 -0
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +41 -6
- package/dist/sessions.js.map +1 -1
- package/dist/wizard/index.d.ts +2 -2
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +1 -1
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/presets.d.ts +2 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +2 -1
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/run.d.ts +0 -26
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +0 -53
- package/dist/wizard/run.js.map +1 -1
- package/package.json +22 -17
- package/schema/mono-agent.config.schema.json +45 -10
- package/skills/mono-agent-composer/SKILL.md +3 -3
- package/skills/mono-agent-composer/references/config-blueprint.md +123 -29
- package/skills/mono-agent-composer/references/discovery-questions.md +10 -8
- package/skills/mono-agent-composer/references/feature-coverage.md +90 -76
- package/skills/mono-agent-composer/references/package-map.md +6 -4
- package/skills/mono-agent-composer/references/playbooks.md +48 -27
- package/skills/mono-agent-composer/references/validation.md +16 -3
- package/dist/notify-runtime.d.ts +0 -26
- package/dist/notify-runtime.d.ts.map +0 -1
- package/dist/notify-runtime.js +0 -27
- package/dist/notify-runtime.js.map +0 -1
- package/dist/notify-tool.d.ts +0 -51
- package/dist/notify-tool.d.ts.map +0 -1
- package/dist/notify-tool.js +0 -182
- package/dist/notify-tool.js.map +0 -1
- package/dist/recipes/base.d.ts +0 -15
- package/dist/recipes/base.d.ts.map +0 -1
- package/dist/recipes/base.js +0 -51
- package/dist/recipes/base.js.map +0 -1
- package/dist/recipes/catalog.d.ts +0 -4
- package/dist/recipes/catalog.d.ts.map +0 -1
- package/dist/recipes/catalog.js +0 -525
- package/dist/recipes/catalog.js.map +0 -1
- package/dist/recipes/index.d.ts +0 -11
- package/dist/recipes/index.d.ts.map +0 -1
- package/dist/recipes/index.js +0 -14
- package/dist/recipes/index.js.map +0 -1
- package/dist/recipes/types.d.ts +0 -70
- package/dist/recipes/types.d.ts.map +0 -1
- package/dist/recipes/types.js +0 -15
- package/dist/recipes/types.js.map +0 -1
- package/dist/setup.d.ts +0 -29
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js +0 -97
- package/dist/setup.js.map +0 -1
|
@@ -1,58 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { ContinuationHttpRoutes } from "./continuation-http-routes.js";
|
|
5
5
|
import { acquireContinuationStoreLock, loadOrCreateContinuationSecret, openContinuationStore, } from "./continuation-store.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|