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