@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
package/dist/doctor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { isIP } from "node:net";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { getBuiltinModels as getPiBuiltinModels, getBuiltinProviders as getPiBuiltinProviders, } from "@earendil-works/pi-ai/providers/all";
|
|
9
|
+
import { validateCronExpression } from "@mono-agent/cron-adapter";
|
|
9
10
|
import { listRecordedRuns } from "@mono-agent/observability";
|
|
10
11
|
import { serializeTraceSpans } from "@mono-agent/observability/otel";
|
|
11
12
|
import { classifyContinuationMcpServerTransport, isStdioMcpServerSpec, loadToolPolicyFromJsonFile, } from "@mono-agent/agent-harness";
|
|
@@ -19,8 +20,13 @@ import { collectChannelConfigViews } from "./channel-config-view.js";
|
|
|
19
20
|
import { resolveChannelDrivers } from "./channels.js";
|
|
20
21
|
import { findUnknownAppConfigWarnings } from "./config-reference.js";
|
|
21
22
|
import { loadContinuationSettings } from "./continuation-config.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
23
|
+
import { applyOriginContextGroupCommit } from "./continuation-origin-store.js";
|
|
24
|
+
import { readBoundedOwnerOnlyFile } from "./continuation-store-fs.js";
|
|
25
|
+
import { loadLegacyStore, mergeMigrationRecords } from "./continuation-store-records.js";
|
|
26
|
+
import { CONTINUATION_RECORD_STORE_SCHEMA_VERSION, } from "./continuation-store.js";
|
|
27
|
+
import { applyRetention, isDurableGeneration, isOriginContextGroupCommit, isRecord, isRecordTransaction, isStoreFile, normalizeLegacyContinuationRecords, requiredDate, resolveRetention, } from "./continuation-store-policy.js";
|
|
28
|
+
import { MAX_LEGACY_STORE_BYTES, MAX_MANIFEST_BYTES, MAX_RECORD_BYTES, MAX_TRANSACTION_BYTES, } from "./continuation-store-types.js";
|
|
29
|
+
import { CONTINUATION_STATES, continuationDigest } from "./continuations.js";
|
|
24
30
|
import { formatInteractionBridgeUrl, loadInteractionSettings } from "./interaction-bridge.js";
|
|
25
31
|
import { FIRST_RUN_MEMORY_INITIALIZING_MARKER } from "./first-run-managed-memory.js";
|
|
26
32
|
import { DEFAULT_MEMORY_EMBEDDING_ENDPOINTS, probeMemoryEmbeddingSelection, } from "./memory-embedding-service.js";
|
|
@@ -29,7 +35,9 @@ import { piAuthRecoveryCommand } from "./provider-setup.js";
|
|
|
29
35
|
import { inspectPiAuthStore } from "./pi-auth-store-inspection.js";
|
|
30
36
|
import { checkManagedProjectSkills, managedProjectSkillsExist } from "./project-skills.js";
|
|
31
37
|
import { configuredRuntimeFallbackModels, configuredRuntimeModels } from "./runtime-routes.js";
|
|
38
|
+
import { runtimeProvenanceDetail } from "./runtime-provenance.js";
|
|
32
39
|
import { loadSupermemoryPlugin } from "./supermemory-plugin.js";
|
|
40
|
+
import { DEFAULT_LAUNCHD_LOG_POLICY, inspectLaunchdLogs, LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS, launchdLogPathsForConfig, } from "./launchd-logs.js";
|
|
33
41
|
const execFile = promisify(execFileCallback);
|
|
34
42
|
const CONTINUATION_V2_ROLLBACK_GUARD = "UPGRADED-TO-RECORDS-V3";
|
|
35
43
|
const CONTINUATION_V2_ROLLBACK_GUARD_CONTENT = "This state directory uses continuation records v3. Older runtimes must not open records-v2.\n";
|
|
@@ -54,6 +62,7 @@ export async function validateMonoAgentFolder(options) {
|
|
|
54
62
|
}
|
|
55
63
|
sections.push({ id: "core", label: "Core config", status: "error", details: [error.message] });
|
|
56
64
|
}
|
|
65
|
+
sections.push(await runtimeProvenanceSection());
|
|
57
66
|
if (coreConfig !== undefined) {
|
|
58
67
|
const staticTriggerCredentialRefs = await collectStaticTriggerCredentialRefs(drivers, options);
|
|
59
68
|
const jsonResult = await readMonoAgentConfigJson(options.configPath);
|
|
@@ -84,6 +93,7 @@ export async function validateMonoAgentFolder(options) {
|
|
|
84
93
|
}
|
|
85
94
|
sections.push(await exporterSection(options, liveness));
|
|
86
95
|
sections.push(await runsSection(options, coreConfig));
|
|
96
|
+
sections.push(await launchdLogsSection(options.configPath));
|
|
87
97
|
for (const driver of drivers) {
|
|
88
98
|
sections.push(await channelSection(driver, options));
|
|
89
99
|
}
|
|
@@ -102,6 +112,62 @@ export async function validateMonoAgentFolder(options) {
|
|
|
102
112
|
ok: sections.every((section) => section.status !== "error"),
|
|
103
113
|
};
|
|
104
114
|
}
|
|
115
|
+
/** Read-only launchd log inventory used by both `validate` and its `doctor` alias. */
|
|
116
|
+
export async function launchdLogsSection(configPath) {
|
|
117
|
+
let inspection;
|
|
118
|
+
try {
|
|
119
|
+
const paths = await launchdLogPathsForConfig(configPath);
|
|
120
|
+
inspection = await inspectLaunchdLogs(paths);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return {
|
|
124
|
+
id: "launchd-logs",
|
|
125
|
+
label: "Launchd logs",
|
|
126
|
+
status: "waiting",
|
|
127
|
+
details: ["[WARN] Managed launchd log metadata could not be inspected safely."],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return launchdLogsSectionFromInspection(inspection);
|
|
131
|
+
}
|
|
132
|
+
/** Pure renderer kept separate so exact byte accounting is deterministic in tests. */
|
|
133
|
+
export function launchdLogsSectionFromInspection(inspection) {
|
|
134
|
+
const policy = DEFAULT_LAUNCHD_LOG_POLICY;
|
|
135
|
+
const details = [
|
|
136
|
+
`Policy: ${policy.maxBytes} bytes per file, ${policy.rotationCount} retained generations, checked every ${LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS} seconds.`,
|
|
137
|
+
streamSizeDetail("stdout", inspection.stdout),
|
|
138
|
+
streamSizeDetail("stderr", inspection.stderr),
|
|
139
|
+
...inspection.issues.map((issue) => `[WARN] ${issue}.`),
|
|
140
|
+
...oversizedLogDetails("stdout", inspection.stdout, policy.maxBytes),
|
|
141
|
+
...oversizedLogDetails("stderr", inspection.stderr, policy.maxBytes),
|
|
142
|
+
];
|
|
143
|
+
if (!inspection.present && inspection.issues.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
id: "launchd-logs",
|
|
146
|
+
label: "Launchd logs",
|
|
147
|
+
status: "disabled",
|
|
148
|
+
details: [...details, "No managed launchd log files exist yet."],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
id: "launchd-logs",
|
|
153
|
+
label: "Launchd logs",
|
|
154
|
+
status: inspection.canMaintain && !inspection.needsMaintenance ? "ok" : "waiting",
|
|
155
|
+
details,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function streamSizeDetail(label, stream) {
|
|
159
|
+
if (!stream.byteAccountingComplete) {
|
|
160
|
+
return `${label}: byte inventory unavailable because one or more paths could not be inspected safely.`;
|
|
161
|
+
}
|
|
162
|
+
return `${label}: active=${stream.activeBytes} bytes, retained=${stream.retainedBytes} bytes, total=${stream.totalBytes} bytes.`;
|
|
163
|
+
}
|
|
164
|
+
function oversizedLogDetails(label, stream, maxBytes) {
|
|
165
|
+
if (!stream.byteAccountingComplete)
|
|
166
|
+
return [];
|
|
167
|
+
return stream.files.flatMap((file) => file.bytes <= maxBytes
|
|
168
|
+
? []
|
|
169
|
+
: [`[WARN] ${label}${file.generation === 0 ? "" : `.${file.generation}`} is ${file.bytes} bytes; maintenance limit is ${maxBytes} bytes.`]);
|
|
170
|
+
}
|
|
105
171
|
/** Enabled, statically configured webhook/cron entries that can actually execute. */
|
|
106
172
|
function staticTriggerConfigEntries(driverId, loaded) {
|
|
107
173
|
if (!isUnknownRecord(loaded))
|
|
@@ -392,6 +458,14 @@ function applyToolChannelCrossChecks(sections, allowedTools, disallowedTools) {
|
|
|
392
458
|
}
|
|
393
459
|
sections[toolsIndex] = { ...current, status, details: [...current.details, ...extraDetails] };
|
|
394
460
|
}
|
|
461
|
+
async function runtimeProvenanceSection() {
|
|
462
|
+
return {
|
|
463
|
+
id: "runtime-provenance",
|
|
464
|
+
label: "Runtime provenance",
|
|
465
|
+
status: "ok",
|
|
466
|
+
details: [await runtimeProvenanceDetail()],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
395
469
|
function runtimeSection(config) {
|
|
396
470
|
const details = [];
|
|
397
471
|
let status = "ok";
|
|
@@ -1045,13 +1119,25 @@ async function contextSection(config, cwd) {
|
|
|
1045
1119
|
return { id: "context", label: "Context & skills", status, details };
|
|
1046
1120
|
}
|
|
1047
1121
|
const DEFAULT_CONSOLIDATION_CRON = "0 */2 * * *";
|
|
1122
|
+
function memoryConsolidationCronIssue(expression) {
|
|
1123
|
+
const result = validateCronExpression(expression, { timezone: "UTC" });
|
|
1124
|
+
if (result.ok) {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
if (result.code === "required") {
|
|
1128
|
+
return "memory.consolidation.cron is required when consolidation is enabled.";
|
|
1129
|
+
}
|
|
1130
|
+
if (result.code === "field_count") {
|
|
1131
|
+
return `memory.consolidation.cron must use exactly five fields; received ${result.fieldCount}.`;
|
|
1132
|
+
}
|
|
1133
|
+
return `memory.consolidation.cron is invalid: ${result.reason}`;
|
|
1134
|
+
}
|
|
1048
1135
|
async function memorySection(config, cwd, env, liveness, allowFilesystemWrites, preferAppPluginInstall) {
|
|
1049
1136
|
if (config.memory === undefined) {
|
|
1050
1137
|
return { id: "memory", label: "Memory", status: "disabled", details: ["No memory configured."] };
|
|
1051
1138
|
}
|
|
1052
|
-
// External backend (e.g. supermemory): mode/embeddings/llm are bujo-only and
|
|
1053
|
-
//
|
|
1054
|
-
// starting the server.
|
|
1139
|
+
// External backend (e.g. supermemory): mode/embeddings/llm are bujo-only and
|
|
1140
|
+
// ignored, so validate the plugin-owned shape before any soft liveness probe.
|
|
1055
1141
|
if ((config.memory.backend ?? "bujo") === "supermemory") {
|
|
1056
1142
|
const sm = config.memory.supermemory;
|
|
1057
1143
|
if (sm === undefined) {
|
|
@@ -1088,19 +1174,26 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
|
|
|
1088
1174
|
details: [`[ERROR] ${error instanceof Error ? error.message : String(error)}`],
|
|
1089
1175
|
};
|
|
1090
1176
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1177
|
+
const details = [
|
|
1178
|
+
`Backend: supermemory, writeMode: ${config.memory.writeMode}.`,
|
|
1179
|
+
`Endpoint: ${sm.baseUrl} (container "${resolveSupermemoryContainer(config)}").`,
|
|
1180
|
+
sm.apiKey === undefined
|
|
1181
|
+
? "Auth: no API key configured (keyless — works only if the instance allows it)."
|
|
1182
|
+
: "Auth: API key configured.",
|
|
1183
|
+
];
|
|
1184
|
+
if (!liveness) {
|
|
1185
|
+
details.push("Supermemory liveness probe skipped; ingestion is async.");
|
|
1186
|
+
return { id: "memory", label: "Memory", status: "ok", details };
|
|
1187
|
+
}
|
|
1188
|
+
const probe = await probeSupermemoryEndpoint(sm.baseUrl);
|
|
1189
|
+
if (!probe.reachable) {
|
|
1190
|
+
details.push(`[WARN] Supermemory is not reachable at ${sm.baseUrl} (${probe.reason}). ` +
|
|
1191
|
+
"Start Supermemory or fix memory.supermemory.baseUrl, then re-run `mono-agent validate`; " +
|
|
1192
|
+
"capture and recall will degrade until it is reachable.");
|
|
1193
|
+
return { id: "memory", label: "Memory", status: "waiting", details };
|
|
1194
|
+
}
|
|
1195
|
+
details.push(`Supermemory transport reachable at ${sm.baseUrl} (HTTP ${probe.status}); ingestion is async.`);
|
|
1196
|
+
return { id: "memory", label: "Memory", status: "ok", details };
|
|
1104
1197
|
}
|
|
1105
1198
|
const details = [
|
|
1106
1199
|
`Mode: ${config.memory.mode}, path: ${config.memory.path}, writeMode: ${config.memory.writeMode}.`,
|
|
@@ -1133,6 +1226,11 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
|
|
|
1133
1226
|
else {
|
|
1134
1227
|
details.push("Consolidation: disabled.");
|
|
1135
1228
|
}
|
|
1229
|
+
const cronIssue = memoryConsolidationCronIssue(consolidationCron);
|
|
1230
|
+
if (cronIssue !== undefined) {
|
|
1231
|
+
status = "error";
|
|
1232
|
+
details.push(`[ERROR] ${cronIssue}`);
|
|
1233
|
+
}
|
|
1136
1234
|
}
|
|
1137
1235
|
const managedIdentity = await managedMemoryIdentityStatus(config.memory);
|
|
1138
1236
|
if (managedIdentity !== undefined) {
|
|
@@ -1294,11 +1392,39 @@ function isBuiltInMemoryNativeFailure(error) {
|
|
|
1294
1392
|
async function liteRootWritableWarning(memoryPath, allowFilesystemWrites) {
|
|
1295
1393
|
return await memoryRootWritableWarnings("lite", memoryPath, allowFilesystemWrites);
|
|
1296
1394
|
}
|
|
1297
|
-
const
|
|
1395
|
+
const LIVENESS_PROBE_TIMEOUT_MS = 3_000;
|
|
1396
|
+
/**
|
|
1397
|
+
* Read-only transport probe for a configured Supermemory service root. Neither
|
|
1398
|
+
* the hosted nor self-hosted base URL has a documented health response, so any
|
|
1399
|
+
* HTTP status proves reachability; only transport failure or timeout degrades
|
|
1400
|
+
* validation. Manual redirects and omitted auth keep the probe on the exact
|
|
1401
|
+
* configured endpoint without sending memory data or credentials.
|
|
1402
|
+
*/
|
|
1403
|
+
async function probeSupermemoryEndpoint(endpoint) {
|
|
1404
|
+
const ctrl = new AbortController();
|
|
1405
|
+
const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
|
|
1406
|
+
try {
|
|
1407
|
+
const response = await fetch(endpoint, {
|
|
1408
|
+
method: "HEAD",
|
|
1409
|
+
redirect: "manual",
|
|
1410
|
+
signal: ctrl.signal,
|
|
1411
|
+
});
|
|
1412
|
+
return { reachable: true, status: response.status };
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
return {
|
|
1416
|
+
reachable: false,
|
|
1417
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
finally {
|
|
1421
|
+
clearTimeout(timer);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1298
1424
|
/** Probes Ollama /api/tags and returns a sorted list of model names, or throws. */
|
|
1299
1425
|
async function fetchOllamaModels(endpoint) {
|
|
1300
1426
|
const ctrl = new AbortController();
|
|
1301
|
-
const timer = setTimeout(() => { ctrl.abort(); },
|
|
1427
|
+
const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
|
|
1302
1428
|
try {
|
|
1303
1429
|
const res = await fetch(`${endpoint}/api/tags`, { signal: ctrl.signal });
|
|
1304
1430
|
if (!res.ok) {
|
|
@@ -1385,7 +1511,7 @@ async function localEmbeddingLivenessWarnings(embeddings, apiKey) {
|
|
|
1385
1511
|
endpoint,
|
|
1386
1512
|
model: embeddings.model,
|
|
1387
1513
|
expectedDimension: embeddings.dim ?? 768,
|
|
1388
|
-
timeoutMs:
|
|
1514
|
+
timeoutMs: LIVENESS_PROBE_TIMEOUT_MS,
|
|
1389
1515
|
...(apiKey === undefined ? {} : { apiKey }),
|
|
1390
1516
|
});
|
|
1391
1517
|
return [];
|
|
@@ -1681,7 +1807,7 @@ async function continuationSection(config, input) {
|
|
|
1681
1807
|
details.push(routeNames.length === 0 ? "Named detached routes: none." : `Named detached routes: ${routeNames.join(", ")}.`);
|
|
1682
1808
|
const detachedNames = Object.keys(settings.detachedServices);
|
|
1683
1809
|
details.push(detachedNames.length === 0 ? "Detached services: none." : `Detached services: ${detachedNames.join(", ")}.`);
|
|
1684
|
-
const state = await inspectContinuationState(settings.stateDir);
|
|
1810
|
+
const state = await inspectContinuationState(settings.stateDir, settings.retention);
|
|
1685
1811
|
details.push(...state.details);
|
|
1686
1812
|
return {
|
|
1687
1813
|
id: "continuations",
|
|
@@ -1690,8 +1816,9 @@ async function continuationSection(config, input) {
|
|
|
1690
1816
|
details,
|
|
1691
1817
|
};
|
|
1692
1818
|
}
|
|
1693
|
-
async function inspectContinuationState(stateDir) {
|
|
1819
|
+
async function inspectContinuationState(stateDir, retention) {
|
|
1694
1820
|
const details = [];
|
|
1821
|
+
const retentionPolicy = resolveRetention(retention);
|
|
1695
1822
|
let directory;
|
|
1696
1823
|
try {
|
|
1697
1824
|
directory = await lstat(stateDir);
|
|
@@ -1735,7 +1862,7 @@ async function inspectContinuationState(stateDir) {
|
|
|
1735
1862
|
return { status: "error", details: [manifestSecurityError] };
|
|
1736
1863
|
let manifest;
|
|
1737
1864
|
try {
|
|
1738
|
-
manifest = JSON.parse(await
|
|
1865
|
+
manifest = JSON.parse(await readBoundedOwnerOnlyFile(manifestPath, MAX_MANIFEST_BYTES, "Continuation v3 manifest"));
|
|
1739
1866
|
}
|
|
1740
1867
|
catch (error) {
|
|
1741
1868
|
return { status: "error", details: [`Continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
|
|
@@ -1743,21 +1870,116 @@ async function inspectContinuationState(stateDir) {
|
|
|
1743
1870
|
if (!isContinuationStoreManifest(manifest, CONTINUATION_RECORD_STORE_SCHEMA_VERSION, "per-record-v3")) {
|
|
1744
1871
|
return { status: "error", details: ["Continuation store manifest has an unsupported or malformed schema."] };
|
|
1745
1872
|
}
|
|
1746
|
-
const
|
|
1873
|
+
const recordsDirectoryPath = join(stateDir, "records-v3");
|
|
1874
|
+
const recordsDirectoryError = await inspectContinuationRecordsDirectory(recordsDirectoryPath);
|
|
1747
1875
|
if (recordsDirectoryError !== undefined)
|
|
1748
1876
|
return { status: "error", details: [recordsDirectoryError] };
|
|
1749
|
-
|
|
1877
|
+
let recordsDirectoryHasEntries;
|
|
1878
|
+
try {
|
|
1879
|
+
recordsDirectoryHasEntries = (await readdir(recordsDirectoryPath))
|
|
1880
|
+
.some((entry) => !(entry.startsWith(".") && entry.endsWith(".tmp")));
|
|
1881
|
+
}
|
|
1882
|
+
catch (error) {
|
|
1883
|
+
return {
|
|
1884
|
+
status: "error",
|
|
1885
|
+
details: [`Continuation record directory cannot be inspected: ${continuationReason(error)}`],
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
const transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v3.json"), CONTINUATION_RECORD_STORE_SCHEMA_VERSION);
|
|
1750
1889
|
if (!transaction.valid)
|
|
1751
1890
|
return { status: "error", details: [transaction.detail] };
|
|
1752
|
-
const
|
|
1891
|
+
const legacyRecordsDirectoryPath = join(stateDir, "records-v2");
|
|
1892
|
+
let legacyRecordsDirectoryExists = false;
|
|
1893
|
+
try {
|
|
1894
|
+
const legacyRecordsDirectoryInfo = await lstat(legacyRecordsDirectoryPath);
|
|
1895
|
+
legacyRecordsDirectoryExists = true;
|
|
1896
|
+
if (!legacyRecordsDirectoryInfo.isDirectory() || legacyRecordsDirectoryInfo.isSymbolicLink()) {
|
|
1897
|
+
return { status: "error", details: ["Legacy continuation record directory must be a real directory."] };
|
|
1898
|
+
}
|
|
1899
|
+
const legacyRecordsDirectoryError = await inspectContinuationRecordsDirectory(legacyRecordsDirectoryPath, { allowV2RollbackGuard: true });
|
|
1900
|
+
if (legacyRecordsDirectoryError !== undefined) {
|
|
1901
|
+
return { status: "error", details: [legacyRecordsDirectoryError] };
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
catch (error) {
|
|
1905
|
+
if (continuationFsCode(error) !== "ENOENT") {
|
|
1906
|
+
return {
|
|
1907
|
+
status: "error",
|
|
1908
|
+
details: [`Legacy continuation record directory cannot be inspected: ${continuationReason(error)}`],
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
let legacyStateExists = legacyRecordsDirectoryExists;
|
|
1913
|
+
const legacyV1Path = join(stateDir, "continuations-v1.json");
|
|
1914
|
+
const legacyV1 = await inspectContinuationEvidenceFile(legacyV1Path, "v1 store");
|
|
1915
|
+
if (!legacyV1.valid)
|
|
1916
|
+
return { status: "error", details: [legacyV1.detail] };
|
|
1917
|
+
legacyStateExists ||= legacyV1.exists;
|
|
1918
|
+
const legacyV2Manifest = await inspectContinuationEvidenceFile(join(stateDir, "continuation-store-v2.json"), "v2 manifest");
|
|
1919
|
+
if (!legacyV2Manifest.valid)
|
|
1920
|
+
return { status: "error", details: [legacyV2Manifest.detail] };
|
|
1921
|
+
legacyStateExists ||= legacyV2Manifest.exists;
|
|
1922
|
+
const legacyTransaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"), 2);
|
|
1923
|
+
if (!legacyTransaction.valid)
|
|
1924
|
+
return { status: "error", details: [legacyTransaction.detail] };
|
|
1925
|
+
legacyStateExists ||= legacyTransaction.pending;
|
|
1926
|
+
const manifestRollbackGuardRequired = manifest.rollbackGuardRequired ?? true;
|
|
1927
|
+
const legacyMigrationPending = legacyStateExists && !manifestRollbackGuardRequired;
|
|
1928
|
+
let recoverableRecords;
|
|
1929
|
+
try {
|
|
1930
|
+
recoverableRecords = await loadContinuationRecordsForRecoveryInspection(recordsDirectoryPath);
|
|
1931
|
+
if (transaction.transaction !== undefined) {
|
|
1932
|
+
applyContinuationTransactionForInspection(recoverableRecords, transaction.transaction);
|
|
1933
|
+
}
|
|
1934
|
+
normalizeLegacyContinuationRecords(recoverableRecords);
|
|
1935
|
+
if (legacyMigrationPending) {
|
|
1936
|
+
const legacy = legacyRecordsDirectoryExists
|
|
1937
|
+
? await loadContinuationRecordsForRecoveryInspection(legacyRecordsDirectoryPath)
|
|
1938
|
+
: new Map();
|
|
1939
|
+
if (legacyTransaction.transaction !== undefined) {
|
|
1940
|
+
applyContinuationTransactionForInspection(legacy, legacyTransaction.transaction);
|
|
1941
|
+
}
|
|
1942
|
+
if (legacyV1.exists) {
|
|
1943
|
+
mergeMigrationRecords(legacy, await loadLegacyStore(legacyV1Path), "recoverable v1 and v2");
|
|
1944
|
+
}
|
|
1945
|
+
mergeMigrationRecords(recoverableRecords, legacy, "recoverable v2 and v3");
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
catch (error) {
|
|
1949
|
+
return {
|
|
1950
|
+
status: "error",
|
|
1951
|
+
details: [`Continuation recoverable records are malformed or conflicting: ${continuationReason(error)}`],
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
const originGroups = await inspectContinuationEvidenceDirectory(join(stateDir, "origin-context-groups-v1"), "origin-context activation directory", async () => recoverableRecords);
|
|
1955
|
+
if (!originGroups.valid)
|
|
1956
|
+
return { status: "error", details: [originGroups.detail] };
|
|
1957
|
+
try {
|
|
1958
|
+
applyRetention(recoverableRecords, retentionPolicy, new Date());
|
|
1959
|
+
}
|
|
1960
|
+
catch (error) {
|
|
1961
|
+
return {
|
|
1962
|
+
status: "error",
|
|
1963
|
+
details: [`Continuation retention projection cannot be recovered safely: ${continuationReason(error)}`],
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
const rollbackGuardRequired = manifestRollbackGuardRequired
|
|
1967
|
+
|| recordsDirectoryHasEntries
|
|
1968
|
+
|| transaction.pending
|
|
1969
|
+
|| originGroups.hasEntries;
|
|
1970
|
+
const rollbackGuard = await inspectContinuationRollbackGuard(join(legacyRecordsDirectoryPath, CONTINUATION_V2_ROLLBACK_GUARD), rollbackGuardRequired, legacyMigrationPending
|
|
1971
|
+
? "Legacy continuation state is awaiting v3 migration; the current runtime will fence it before v3 materialization."
|
|
1972
|
+
: "The empty v3 store has no v2 rollback guard; the current runtime will install one before its first v3 record becomes durable.");
|
|
1753
1973
|
if (!rollbackGuard.valid)
|
|
1754
1974
|
return { status: "error", details: [rollbackGuard.detail] };
|
|
1755
1975
|
const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
|
|
1756
1976
|
if (!owner.valid)
|
|
1757
1977
|
return { status: "error", details: [owner.detail] };
|
|
1758
|
-
details.push(`Store v3: ${String(manifest.stats.records)} retained; ${String(manifest.stats.active)} active; ${String(manifest.stats.unresolvedDelivery)} delivery unknown; ${String(manifest.stats.deadLettered)} dead-lettered; ${String(manifest.stats.historyDegraded)} history-degraded deliveries; ${String(manifest.stats.terminalTombstones)} terminal tombstones; ${String(manifest.stats.capturedText)} captured answers.`, `Retention: at most ${String(manifest.stats.limits.terminalMaxRecords)} terminal tombstones and ${String(manifest.stats.limits.capturedTextMaxRecords)} captured answers.`, transaction.detail, rollbackGuard.detail, owner.detail);
|
|
1978
|
+
details.push(`Store v3: ${String(manifest.stats.records)} retained; ${String(manifest.stats.active)} active; ${String(manifest.stats.unresolvedDelivery)} delivery unknown; ${String(manifest.stats.deadLettered)} dead-lettered; ${String(manifest.stats.historyDegraded)} history-degraded deliveries; ${String(manifest.stats.terminalTombstones)} terminal tombstones; ${String(manifest.stats.compacted)} compacted; ${String(manifest.stats.capturedText)} captured answers.`, `Retention: at most ${String(manifest.stats.limits.terminalMaxRecords)} terminal tombstones with a maximum age of ${String(manifest.stats.limits.terminalMaxAgeMs)} ms and ${String(manifest.stats.limits.capturedTextMaxRecords)} captured answers with a maximum age of ${String(manifest.stats.limits.capturedTextMaxAgeMs)} ms.`, transaction.detail, rollbackGuard.detail, originGroups.detail, ...(legacyMigrationPending ? ["Legacy continuation state is awaiting idempotent v3 migration."] : []), owner.detail);
|
|
1759
1979
|
return {
|
|
1760
1980
|
status: transaction.pending
|
|
1981
|
+
|| originGroups.hasEntries
|
|
1982
|
+
|| legacyMigrationPending
|
|
1761
1983
|
|| manifest.stats.unresolvedDelivery > 0
|
|
1762
1984
|
|| manifest.stats.deadLettered > 0
|
|
1763
1985
|
|| manifest.stats.historyDegraded > 0
|
|
@@ -1766,6 +1988,19 @@ async function inspectContinuationState(stateDir) {
|
|
|
1766
1988
|
details,
|
|
1767
1989
|
};
|
|
1768
1990
|
}
|
|
1991
|
+
// Without a v3 manifest, derive one complete read-only recovery projection
|
|
1992
|
+
// before choosing a status. A crash may leave a WAL, already-applied records
|
|
1993
|
+
// after WAL removal, legacy inputs, or an activation marker in any
|
|
1994
|
+
// combination; reporting one item as waiting must not mask another item that
|
|
1995
|
+
// runtime recovery would reject.
|
|
1996
|
+
const unmanifestedV3Transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v3.json"), CONTINUATION_RECORD_STORE_SCHEMA_VERSION);
|
|
1997
|
+
if (!unmanifestedV3Transaction.valid) {
|
|
1998
|
+
return { status: "error", details: [unmanifestedV3Transaction.detail] };
|
|
1999
|
+
}
|
|
2000
|
+
const unmanifestedV2Transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"), 2);
|
|
2001
|
+
if (!unmanifestedV2Transaction.valid) {
|
|
2002
|
+
return { status: "error", details: [unmanifestedV2Transaction.detail] };
|
|
2003
|
+
}
|
|
1769
2004
|
const legacyManifestPath = join(stateDir, "continuation-store-v2.json");
|
|
1770
2005
|
let legacyManifestInfo;
|
|
1771
2006
|
try {
|
|
@@ -1776,7 +2011,10 @@ async function inspectContinuationState(stateDir) {
|
|
|
1776
2011
|
return { status: "error", details: [`Legacy continuation store manifest cannot be inspected: ${continuationReason(error)}`] };
|
|
1777
2012
|
}
|
|
1778
2013
|
}
|
|
2014
|
+
let legacyManifestExists = false;
|
|
2015
|
+
let legacyManifestDetails;
|
|
1779
2016
|
if (legacyManifestInfo !== undefined) {
|
|
2017
|
+
legacyManifestExists = true;
|
|
1780
2018
|
if (!legacyManifestInfo.isFile() || legacyManifestInfo.isSymbolicLink()) {
|
|
1781
2019
|
return { status: "error", details: ["Legacy continuation store manifest must be a regular file, not a symlink."] };
|
|
1782
2020
|
}
|
|
@@ -1785,7 +2023,7 @@ async function inspectContinuationState(stateDir) {
|
|
|
1785
2023
|
return { status: "error", details: [manifestSecurityError] };
|
|
1786
2024
|
let legacyManifest;
|
|
1787
2025
|
try {
|
|
1788
|
-
legacyManifest = JSON.parse(await
|
|
2026
|
+
legacyManifest = JSON.parse(await readBoundedOwnerOnlyFile(legacyManifestPath, MAX_MANIFEST_BYTES, "Continuation v2 manifest"));
|
|
1789
2027
|
}
|
|
1790
2028
|
catch (error) {
|
|
1791
2029
|
return { status: "error", details: [`Legacy continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
|
|
@@ -1793,22 +2031,71 @@ async function inspectContinuationState(stateDir) {
|
|
|
1793
2031
|
if (!isContinuationStoreManifest(legacyManifest, 2, "per-record-v2")) {
|
|
1794
2032
|
return { status: "error", details: ["Legacy continuation store manifest has an unsupported or malformed schema."] };
|
|
1795
2033
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
2034
|
+
legacyManifestDetails = [
|
|
2035
|
+
`Legacy store v2 awaiting v3 migration: ${String(legacyManifest.stats.records)} retained; ${String(legacyManifest.stats.active)} active; ${String(legacyManifest.stats.unresolvedDelivery)} delivery unknown; ${String(legacyManifest.stats.deadLettered)} dead-lettered; ${String(legacyManifest.stats.historyDegraded)} history-degraded deliveries; ${String(legacyManifest.stats.terminalTombstones)} terminal tombstones; ${String(legacyManifest.stats.compacted)} compacted; ${String(legacyManifest.stats.capturedText)} captured answers.`,
|
|
2036
|
+
`Retention: at most ${String(legacyManifest.stats.limits.terminalMaxRecords)} terminal tombstones with a maximum age of ${String(legacyManifest.stats.limits.terminalMaxAgeMs)} ms and ${String(legacyManifest.stats.limits.capturedTextMaxRecords)} captured answers with a maximum age of ${String(legacyManifest.stats.limits.capturedTextMaxAgeMs)} ms.`,
|
|
2037
|
+
];
|
|
2038
|
+
}
|
|
2039
|
+
const legacyV1Path = join(stateDir, "continuations-v1.json");
|
|
2040
|
+
const legacyV1 = await inspectContinuationEvidenceFile(legacyV1Path, "v1 store");
|
|
2041
|
+
if (!legacyV1.valid)
|
|
2042
|
+
return { status: "error", details: [legacyV1.detail] };
|
|
2043
|
+
let v3Directory;
|
|
2044
|
+
let v2Directory;
|
|
2045
|
+
let projectedRecords;
|
|
2046
|
+
let hasCommittedV3Records = false;
|
|
2047
|
+
try {
|
|
2048
|
+
v3Directory = await loadOptionalContinuationRecordsForRecoveryInspection(join(stateDir, "records-v3"));
|
|
2049
|
+
hasCommittedV3Records = v3Directory.records.size > 0;
|
|
2050
|
+
v2Directory = await loadOptionalContinuationRecordsForRecoveryInspection(join(stateDir, "records-v2"), {
|
|
1798
2051
|
allowV2RollbackGuard: true,
|
|
1799
2052
|
});
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2053
|
+
const legacyRecords = v2Directory.records;
|
|
2054
|
+
if (unmanifestedV2Transaction.transaction !== undefined) {
|
|
2055
|
+
applyContinuationTransactionForInspection(legacyRecords, unmanifestedV2Transaction.transaction);
|
|
2056
|
+
}
|
|
2057
|
+
if (legacyV1.exists) {
|
|
2058
|
+
mergeMigrationRecords(legacyRecords, await loadLegacyStore(legacyV1Path), "recoverable v1 and v2");
|
|
2059
|
+
}
|
|
2060
|
+
projectedRecords = v3Directory.records;
|
|
2061
|
+
if (unmanifestedV3Transaction.transaction !== undefined) {
|
|
2062
|
+
applyContinuationTransactionForInspection(projectedRecords, unmanifestedV3Transaction.transaction);
|
|
2063
|
+
}
|
|
2064
|
+
normalizeLegacyContinuationRecords(projectedRecords);
|
|
2065
|
+
mergeMigrationRecords(projectedRecords, legacyRecords, "recoverable v2 and v3");
|
|
2066
|
+
}
|
|
2067
|
+
catch (error) {
|
|
2068
|
+
return {
|
|
2069
|
+
status: "error",
|
|
2070
|
+
details: [`Continuation recovery evidence is malformed or conflicting: ${continuationReason(error)}`],
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
const originGroups = await inspectContinuationEvidenceDirectory(join(stateDir, "origin-context-groups-v1"), "origin-context activation directory", async () => projectedRecords);
|
|
2074
|
+
if (!originGroups.valid)
|
|
2075
|
+
return { status: "error", details: [originGroups.detail] };
|
|
2076
|
+
try {
|
|
2077
|
+
applyRetention(projectedRecords, retentionPolicy, new Date());
|
|
2078
|
+
}
|
|
2079
|
+
catch (error) {
|
|
2080
|
+
return {
|
|
2081
|
+
status: "error",
|
|
2082
|
+
details: [`Continuation retention projection cannot be recovered safely: ${continuationReason(error)}`],
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
const rollbackGuard = await inspectContinuationRollbackGuard(join(stateDir, "records-v2", CONTINUATION_V2_ROLLBACK_GUARD), false, "The v2 rollback guard is not installed; the current runtime will install it during v3 migration before publishing v3 state.");
|
|
2086
|
+
if (!rollbackGuard.valid)
|
|
2087
|
+
return { status: "error", details: [rollbackGuard.detail] };
|
|
2088
|
+
const recoverableEvidenceExists = legacyManifestExists
|
|
2089
|
+
|| v2Directory.exists
|
|
2090
|
+
|| hasCommittedV3Records
|
|
2091
|
+
|| unmanifestedV2Transaction.pending
|
|
2092
|
+
|| unmanifestedV3Transaction.pending
|
|
2093
|
+
|| originGroups.hasEntries;
|
|
2094
|
+
if (recoverableEvidenceExists) {
|
|
1808
2095
|
const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
|
|
1809
2096
|
if (!owner.valid)
|
|
1810
2097
|
return { status: "error", details: [owner.detail] };
|
|
1811
|
-
details.push(
|
|
2098
|
+
details.push(...(legacyManifestDetails ?? ["Continuation records are awaiting completion of the v3 manifest."]), ...(unmanifestedV2Transaction.pending ? [unmanifestedV2Transaction.detail] : []), ...(unmanifestedV3Transaction.pending ? [unmanifestedV3Transaction.detail] : []), rollbackGuard.detail, originGroups.detail, owner.detail);
|
|
1812
2099
|
return { status: "waiting", details };
|
|
1813
2100
|
}
|
|
1814
2101
|
const storePath = join(stateDir, "continuations-v1.json");
|
|
@@ -1834,7 +2121,7 @@ async function inspectContinuationState(stateDir) {
|
|
|
1834
2121
|
}
|
|
1835
2122
|
let parsed;
|
|
1836
2123
|
try {
|
|
1837
|
-
parsed = JSON.parse(await
|
|
2124
|
+
parsed = JSON.parse(await readBoundedOwnerOnlyFile(storePath, MAX_LEGACY_STORE_BYTES, "Continuation legacy store"));
|
|
1838
2125
|
}
|
|
1839
2126
|
catch (error) {
|
|
1840
2127
|
return { status: "error", details: [`Continuation ledger contains invalid JSON: ${continuationReason(error)}`] };
|
|
@@ -1936,6 +2223,14 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
|
|
|
1936
2223
|
const securityError = continuationOwnershipError(recordInfo, "record", 0o600);
|
|
1937
2224
|
if (securityError !== undefined)
|
|
1938
2225
|
return securityError;
|
|
2226
|
+
if (entry.name.startsWith(".") && entry.name.endsWith(".tmp")) {
|
|
2227
|
+
try {
|
|
2228
|
+
await readBoundedOwnerOnlyFile(join(path, entry.name), MAX_RECORD_BYTES, "Continuation temporary record");
|
|
2229
|
+
}
|
|
2230
|
+
catch (error) {
|
|
2231
|
+
return `Continuation temporary record is unsafe: ${continuationReason(error)}`;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
1939
2234
|
}
|
|
1940
2235
|
}
|
|
1941
2236
|
catch (error) {
|
|
@@ -1943,7 +2238,247 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
|
|
|
1943
2238
|
}
|
|
1944
2239
|
return undefined;
|
|
1945
2240
|
}
|
|
1946
|
-
async function
|
|
2241
|
+
async function inspectContinuationEvidenceDirectory(path, label, loadRecoverableRecords) {
|
|
2242
|
+
let info;
|
|
2243
|
+
try {
|
|
2244
|
+
info = await lstat(path);
|
|
2245
|
+
}
|
|
2246
|
+
catch (error) {
|
|
2247
|
+
return continuationFsCode(error) === "ENOENT"
|
|
2248
|
+
? { valid: true, hasEntries: false, hasTemporaryDebris: false, detail: `${label} has not been created.` }
|
|
2249
|
+
: {
|
|
2250
|
+
valid: false,
|
|
2251
|
+
hasEntries: false,
|
|
2252
|
+
hasTemporaryDebris: false,
|
|
2253
|
+
detail: `${label} cannot be inspected: ${continuationReason(error)}`,
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
2257
|
+
return { valid: false, hasEntries: false, hasTemporaryDebris: false, detail: `${label} must be a real directory.` };
|
|
2258
|
+
}
|
|
2259
|
+
const securityError = continuationOwnershipError(info, label, 0o700);
|
|
2260
|
+
if (securityError !== undefined) {
|
|
2261
|
+
return { valid: false, hasEntries: false, hasTemporaryDebris: false, detail: securityError };
|
|
2262
|
+
}
|
|
2263
|
+
try {
|
|
2264
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
2265
|
+
const temporaryEntries = entries.filter((entry) => entry.name.startsWith(".") && entry.name.endsWith(".tmp"));
|
|
2266
|
+
let projectedRecords;
|
|
2267
|
+
const recordsForRecovery = async () => {
|
|
2268
|
+
if (projectedRecords !== undefined)
|
|
2269
|
+
return projectedRecords;
|
|
2270
|
+
projectedRecords = await loadRecoverableRecords();
|
|
2271
|
+
return projectedRecords;
|
|
2272
|
+
};
|
|
2273
|
+
for (const entry of entries) {
|
|
2274
|
+
const entryPath = join(path, entry.name);
|
|
2275
|
+
const temporary = entry.name.startsWith(".") && entry.name.endsWith(".tmp");
|
|
2276
|
+
if (temporary) {
|
|
2277
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
2278
|
+
return {
|
|
2279
|
+
valid: false,
|
|
2280
|
+
hasEntries: true,
|
|
2281
|
+
hasTemporaryDebris: true,
|
|
2282
|
+
detail: `${label} temporary entry must be a regular file: ${entry.name}.`,
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
await readBoundedOwnerOnlyFile(entryPath, 64 * 1024, "Continuation origin-context group temporary");
|
|
2287
|
+
}
|
|
2288
|
+
catch (error) {
|
|
2289
|
+
return {
|
|
2290
|
+
valid: false,
|
|
2291
|
+
hasEntries: false,
|
|
2292
|
+
hasTemporaryDebris: true,
|
|
2293
|
+
detail: `${label} temporary entry is unsafe: ${continuationReason(error)}`,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
if (!/^[a-f0-9]{64}\.json$/u.test(entry.name) || !entry.isFile() || entry.isSymbolicLink()) {
|
|
2299
|
+
return {
|
|
2300
|
+
valid: false,
|
|
2301
|
+
hasEntries: true,
|
|
2302
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2303
|
+
detail: `${label} contains an unexpected entry: ${entry.name}.`,
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
const entryInfo = await lstat(entryPath);
|
|
2307
|
+
if (!entryInfo.isFile() || entryInfo.isSymbolicLink() || entryInfo.nlink !== 1) {
|
|
2308
|
+
return {
|
|
2309
|
+
valid: false,
|
|
2310
|
+
hasEntries: true,
|
|
2311
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2312
|
+
detail: `${label} marker must be a single-link regular file: ${entry.name}.`,
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
const entrySecurityError = continuationOwnershipError(entryInfo, `${label} marker`, 0o600);
|
|
2316
|
+
if (entrySecurityError !== undefined) {
|
|
2317
|
+
return {
|
|
2318
|
+
valid: false,
|
|
2319
|
+
hasEntries: true,
|
|
2320
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2321
|
+
detail: entrySecurityError,
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
if (entryInfo.size > 64 * 1024) {
|
|
2325
|
+
return {
|
|
2326
|
+
valid: false,
|
|
2327
|
+
hasEntries: true,
|
|
2328
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2329
|
+
detail: `${label} marker exceeds its safety limit: ${entry.name}.`,
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
let markerBody;
|
|
2333
|
+
try {
|
|
2334
|
+
markerBody = await readBoundedOwnerOnlyFile(entryPath, 64 * 1024, "Continuation origin-context group commit");
|
|
2335
|
+
}
|
|
2336
|
+
catch (error) {
|
|
2337
|
+
return {
|
|
2338
|
+
valid: false,
|
|
2339
|
+
hasEntries: true,
|
|
2340
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2341
|
+
detail: `${label} marker is unsafe to read: ${continuationReason(error)}`,
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
let marker;
|
|
2345
|
+
try {
|
|
2346
|
+
marker = JSON.parse(markerBody);
|
|
2347
|
+
}
|
|
2348
|
+
catch (error) {
|
|
2349
|
+
return {
|
|
2350
|
+
valid: false,
|
|
2351
|
+
hasEntries: true,
|
|
2352
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2353
|
+
detail: `${label} marker contains invalid JSON: ${continuationReason(error)}`,
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
if (!isOriginContextGroupCommit(marker) || `${marker.groupKey}.json` !== entry.name) {
|
|
2357
|
+
return {
|
|
2358
|
+
valid: false,
|
|
2359
|
+
hasEntries: true,
|
|
2360
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2361
|
+
detail: `${label} marker has a malformed schema or filename: ${entry.name}.`,
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
try {
|
|
2365
|
+
applyOriginContextGroupCommit(await recordsForRecovery(), marker);
|
|
2366
|
+
}
|
|
2367
|
+
catch (error) {
|
|
2368
|
+
return {
|
|
2369
|
+
valid: false,
|
|
2370
|
+
hasEntries: true,
|
|
2371
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2372
|
+
detail: `${label} marker does not match the recoverable durable records: ${continuationReason(error)}`,
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
const markerCount = entries.length - temporaryEntries.length;
|
|
2377
|
+
const hasEntries = markerCount > 0;
|
|
2378
|
+
const detail = markerCount > 0
|
|
2379
|
+
? `${label} contains ${markerCount === 1 ? "a durable marker" : `${String(markerCount)} durable markers`} awaiting idempotent recovery${temporaryEntries.length > 0 ? ", plus incomplete temporary debris awaiting cleanup" : ""}.`
|
|
2380
|
+
: temporaryEntries.length > 0
|
|
2381
|
+
? `${label} contains only incomplete temporary debris awaiting cleanup.`
|
|
2382
|
+
: `${label} is owner-only and empty.`;
|
|
2383
|
+
return {
|
|
2384
|
+
valid: true,
|
|
2385
|
+
hasEntries,
|
|
2386
|
+
hasTemporaryDebris: temporaryEntries.length > 0,
|
|
2387
|
+
detail,
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
catch (error) {
|
|
2391
|
+
return {
|
|
2392
|
+
valid: false,
|
|
2393
|
+
hasEntries: false,
|
|
2394
|
+
hasTemporaryDebris: false,
|
|
2395
|
+
detail: `${label} cannot be inspected: ${continuationReason(error)}`,
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
async function loadContinuationRecordsForRecoveryInspection(path) {
|
|
2400
|
+
const records = new Map();
|
|
2401
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
2402
|
+
if (entry.name.startsWith(".") && entry.name.endsWith(".tmp"))
|
|
2403
|
+
continue;
|
|
2404
|
+
if (!entry.name.endsWith(".json"))
|
|
2405
|
+
continue;
|
|
2406
|
+
const entryPath = join(path, entry.name);
|
|
2407
|
+
let value;
|
|
2408
|
+
try {
|
|
2409
|
+
value = JSON.parse(await readBoundedOwnerOnlyFile(entryPath, MAX_RECORD_BYTES, "Continuation record"));
|
|
2410
|
+
}
|
|
2411
|
+
catch (error) {
|
|
2412
|
+
throw new Error(`Continuation record cannot be validated for activation recovery: ${entry.name}`, {
|
|
2413
|
+
cause: error,
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
if (!isDoctorObject(value)
|
|
2417
|
+
|| typeof value.continuationId !== "string"
|
|
2418
|
+
|| !isRecord(value, value.continuationId)
|
|
2419
|
+
|| `${continuationDigest(value.continuationId)}.json` !== entry.name
|
|
2420
|
+
|| records.has(value.continuationId)) {
|
|
2421
|
+
throw new Error(`Continuation record has a malformed schema, duplicate id, or mismatched filename: ${entry.name}`);
|
|
2422
|
+
}
|
|
2423
|
+
records.set(value.continuationId, structuredClone(value));
|
|
2424
|
+
}
|
|
2425
|
+
return records;
|
|
2426
|
+
}
|
|
2427
|
+
async function loadOptionalContinuationRecordsForRecoveryInspection(path, options = {}) {
|
|
2428
|
+
try {
|
|
2429
|
+
const info = await lstat(path);
|
|
2430
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
2431
|
+
throw new Error("Continuation record directory must be a real directory, not a file or symlink.");
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
catch (error) {
|
|
2435
|
+
if (continuationFsCode(error) === "ENOENT") {
|
|
2436
|
+
return { exists: false, records: new Map() };
|
|
2437
|
+
}
|
|
2438
|
+
throw error;
|
|
2439
|
+
}
|
|
2440
|
+
const inspectionError = await inspectContinuationRecordsDirectory(path, options);
|
|
2441
|
+
if (inspectionError !== undefined)
|
|
2442
|
+
throw new Error(inspectionError);
|
|
2443
|
+
return {
|
|
2444
|
+
exists: true,
|
|
2445
|
+
records: await loadContinuationRecordsForRecoveryInspection(path),
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function applyContinuationTransactionForInspection(records, transaction) {
|
|
2449
|
+
for (const record of transaction.writes) {
|
|
2450
|
+
records.set(record.continuationId, structuredClone(record));
|
|
2451
|
+
}
|
|
2452
|
+
for (const id of transaction.deletes)
|
|
2453
|
+
records.delete(id);
|
|
2454
|
+
}
|
|
2455
|
+
async function inspectContinuationEvidenceFile(path, label) {
|
|
2456
|
+
let info;
|
|
2457
|
+
try {
|
|
2458
|
+
info = await lstat(path);
|
|
2459
|
+
}
|
|
2460
|
+
catch (error) {
|
|
2461
|
+
return continuationFsCode(error) === "ENOENT"
|
|
2462
|
+
? { valid: true, exists: false, detail: `Legacy continuation ${label} is absent.` }
|
|
2463
|
+
: {
|
|
2464
|
+
valid: false,
|
|
2465
|
+
exists: false,
|
|
2466
|
+
detail: `Legacy continuation ${label} cannot be inspected: ${continuationReason(error)}`,
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
|
|
2470
|
+
return {
|
|
2471
|
+
valid: false,
|
|
2472
|
+
exists: true,
|
|
2473
|
+
detail: `Legacy continuation ${label} must be a single-link regular file.`,
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
const securityError = continuationOwnershipError(info, `legacy ${label}`, 0o600);
|
|
2477
|
+
return securityError === undefined
|
|
2478
|
+
? { valid: true, exists: true, detail: `Legacy continuation ${label} is owner-only.` }
|
|
2479
|
+
: { valid: false, exists: true, detail: securityError };
|
|
2480
|
+
}
|
|
2481
|
+
async function inspectContinuationRollbackGuard(path, required, missingDetail = "The v2 rollback guard is not installed; the current runtime will install it during v3 migration.") {
|
|
1947
2482
|
let info;
|
|
1948
2483
|
try {
|
|
1949
2484
|
info = await lstat(path);
|
|
@@ -1952,12 +2487,12 @@ async function inspectContinuationRollbackGuard(path, required) {
|
|
|
1952
2487
|
if (continuationFsCode(error) === "ENOENT") {
|
|
1953
2488
|
return required
|
|
1954
2489
|
? { valid: false, detail: "Continuation v2 rollback guard is missing from the v3 store." }
|
|
1955
|
-
: { valid: true, detail:
|
|
2490
|
+
: { valid: true, detail: missingDetail };
|
|
1956
2491
|
}
|
|
1957
2492
|
return { valid: false, detail: `Continuation v2 rollback guard cannot be inspected: ${continuationReason(error)}` };
|
|
1958
2493
|
}
|
|
1959
|
-
if (!info.isFile() || info.isSymbolicLink()) {
|
|
1960
|
-
return { valid: false, detail: "Continuation v2 rollback guard must be a regular file, not a symlink." };
|
|
2494
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
|
|
2495
|
+
return { valid: false, detail: "Continuation v2 rollback guard must be a single-link regular file, not a symlink." };
|
|
1961
2496
|
}
|
|
1962
2497
|
const securityError = continuationOwnershipError(info, "v2 rollback guard", 0o600);
|
|
1963
2498
|
if (securityError !== undefined)
|
|
@@ -1967,7 +2502,7 @@ async function inspectContinuationRollbackGuard(path, required) {
|
|
|
1967
2502
|
}
|
|
1968
2503
|
let contents;
|
|
1969
2504
|
try {
|
|
1970
|
-
contents = await
|
|
2505
|
+
contents = await readBoundedOwnerOnlyFile(path, 4 * 1024, "Continuation v2 rollback guard");
|
|
1971
2506
|
}
|
|
1972
2507
|
catch (error) {
|
|
1973
2508
|
return { valid: false, detail: `Continuation v2 rollback guard cannot be read: ${continuationReason(error)}` };
|
|
@@ -1980,26 +2515,62 @@ async function inspectContinuationRollbackGuard(path, required) {
|
|
|
1980
2515
|
detail: "The owner-only v2 rollback guard prevents older runtimes from opening stale continuation records.",
|
|
1981
2516
|
};
|
|
1982
2517
|
}
|
|
1983
|
-
async function inspectContinuationTransaction(path) {
|
|
2518
|
+
async function inspectContinuationTransaction(path, expectedSchemaVersion) {
|
|
2519
|
+
const versionLabel = `v${String(expectedSchemaVersion)}`;
|
|
1984
2520
|
let info;
|
|
1985
2521
|
try {
|
|
1986
2522
|
info = await lstat(path);
|
|
1987
2523
|
}
|
|
1988
2524
|
catch (error) {
|
|
1989
2525
|
return continuationFsCode(error) === "ENOENT"
|
|
1990
|
-
? { valid: true, pending: false, detail:
|
|
1991
|
-
: {
|
|
2526
|
+
? { valid: true, pending: false, detail: `No interrupted durable ${versionLabel} transaction is awaiting recovery.` }
|
|
2527
|
+
: {
|
|
2528
|
+
valid: false,
|
|
2529
|
+
pending: false,
|
|
2530
|
+
detail: `Continuation ${versionLabel} transaction cannot be inspected: ${continuationReason(error)}`,
|
|
2531
|
+
};
|
|
1992
2532
|
}
|
|
1993
|
-
if (!info.isFile() || info.isSymbolicLink()) {
|
|
1994
|
-
return {
|
|
2533
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
|
|
2534
|
+
return {
|
|
2535
|
+
valid: false,
|
|
2536
|
+
pending: true,
|
|
2537
|
+
detail: `Continuation ${versionLabel} transaction must be a single-link regular file, not a symlink.`,
|
|
2538
|
+
};
|
|
1995
2539
|
}
|
|
1996
|
-
const securityError = continuationOwnershipError(info,
|
|
2540
|
+
const securityError = continuationOwnershipError(info, `${versionLabel} transaction`, 0o600);
|
|
1997
2541
|
if (securityError !== undefined)
|
|
1998
2542
|
return { valid: false, pending: true, detail: securityError };
|
|
2543
|
+
let transaction;
|
|
2544
|
+
try {
|
|
2545
|
+
transaction = JSON.parse(await readBoundedOwnerOnlyFile(path, MAX_TRANSACTION_BYTES, `Continuation ${versionLabel} transaction`));
|
|
2546
|
+
}
|
|
2547
|
+
catch (error) {
|
|
2548
|
+
return {
|
|
2549
|
+
valid: false,
|
|
2550
|
+
pending: true,
|
|
2551
|
+
detail: `Continuation ${versionLabel} transaction cannot be read as bounded JSON: ${continuationReason(error)}`,
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
if (!isRecordTransaction(transaction, expectedSchemaVersion)) {
|
|
2555
|
+
return {
|
|
2556
|
+
valid: false,
|
|
2557
|
+
pending: true,
|
|
2558
|
+
detail: `Continuation ${versionLabel} transaction has an unsupported or malformed schema.`,
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
const oversizedRecord = transaction.writes.find((record) => Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`, "utf8") > MAX_RECORD_BYTES);
|
|
2562
|
+
if (oversizedRecord !== undefined) {
|
|
2563
|
+
return {
|
|
2564
|
+
valid: false,
|
|
2565
|
+
pending: true,
|
|
2566
|
+
detail: `Continuation ${versionLabel} transaction contains a record over its ${String(MAX_RECORD_BYTES)} byte safety limit: ${oversizedRecord.continuationId}.`,
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
1999
2569
|
return {
|
|
2000
2570
|
valid: true,
|
|
2001
2571
|
pending: true,
|
|
2002
|
-
|
|
2572
|
+
transaction,
|
|
2573
|
+
detail: `An interrupted durable ${versionLabel} transaction is present and will be completed idempotently by the state owner.`,
|
|
2003
2574
|
};
|
|
2004
2575
|
}
|
|
2005
2576
|
function continuationOwnershipError(info, label, expectedMode) {
|
|
@@ -2012,14 +2583,14 @@ function continuationOwnershipError(info, label, expectedMode) {
|
|
|
2012
2583
|
return undefined;
|
|
2013
2584
|
}
|
|
2014
2585
|
function isContinuationLedger(value) {
|
|
2015
|
-
return
|
|
2016
|
-
&& value.schemaVersion === CONTINUATION_STORE_SCHEMA_VERSION
|
|
2017
|
-
&& isDoctorObject(value.records);
|
|
2586
|
+
return isStoreFile(value);
|
|
2018
2587
|
}
|
|
2019
2588
|
function isContinuationStoreManifest(value, schemaVersion, format) {
|
|
2020
2589
|
if (!isDoctorObject(value)
|
|
2021
2590
|
|| value.schemaVersion !== schemaVersion
|
|
2022
|
-
||
|
|
2591
|
+
|| !isDurableGeneration(value.generation)
|
|
2592
|
+
|| !requiredDate(value.updatedAt)
|
|
2593
|
+
|| (value.rollbackGuardRequired !== undefined && typeof value.rollbackGuardRequired !== "boolean")
|
|
2023
2594
|
|| !isDoctorObject(value.stats)
|
|
2024
2595
|
|| value.stats.format !== format
|
|
2025
2596
|
|| !isDoctorObject(value.stats.limits))
|
|
@@ -2364,7 +2935,7 @@ async function exporterSection(input, liveness) {
|
|
|
2364
2935
|
*/
|
|
2365
2936
|
async function probeExporterEndpoint(endpoint) {
|
|
2366
2937
|
const ctrl = new AbortController();
|
|
2367
|
-
const timer = setTimeout(() => { ctrl.abort(); },
|
|
2938
|
+
const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
|
|
2368
2939
|
try {
|
|
2369
2940
|
const response = await fetch(endpoint, {
|
|
2370
2941
|
method: "POST",
|