@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
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,6 +1119,19 @@ 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."] };
|
|
@@ -1139,6 +1226,11 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
|
|
|
1139
1226
|
else {
|
|
1140
1227
|
details.push("Consolidation: disabled.");
|
|
1141
1228
|
}
|
|
1229
|
+
const cronIssue = memoryConsolidationCronIssue(consolidationCron);
|
|
1230
|
+
if (cronIssue !== undefined) {
|
|
1231
|
+
status = "error";
|
|
1232
|
+
details.push(`[ERROR] ${cronIssue}`);
|
|
1233
|
+
}
|
|
1142
1234
|
}
|
|
1143
1235
|
const managedIdentity = await managedMemoryIdentityStatus(config.memory);
|
|
1144
1236
|
if (managedIdentity !== undefined) {
|
|
@@ -1715,7 +1807,7 @@ async function continuationSection(config, input) {
|
|
|
1715
1807
|
details.push(routeNames.length === 0 ? "Named detached routes: none." : `Named detached routes: ${routeNames.join(", ")}.`);
|
|
1716
1808
|
const detachedNames = Object.keys(settings.detachedServices);
|
|
1717
1809
|
details.push(detachedNames.length === 0 ? "Detached services: none." : `Detached services: ${detachedNames.join(", ")}.`);
|
|
1718
|
-
const state = await inspectContinuationState(settings.stateDir);
|
|
1810
|
+
const state = await inspectContinuationState(settings.stateDir, settings.retention);
|
|
1719
1811
|
details.push(...state.details);
|
|
1720
1812
|
return {
|
|
1721
1813
|
id: "continuations",
|
|
@@ -1724,8 +1816,9 @@ async function continuationSection(config, input) {
|
|
|
1724
1816
|
details,
|
|
1725
1817
|
};
|
|
1726
1818
|
}
|
|
1727
|
-
async function inspectContinuationState(stateDir) {
|
|
1819
|
+
async function inspectContinuationState(stateDir, retention) {
|
|
1728
1820
|
const details = [];
|
|
1821
|
+
const retentionPolicy = resolveRetention(retention);
|
|
1729
1822
|
let directory;
|
|
1730
1823
|
try {
|
|
1731
1824
|
directory = await lstat(stateDir);
|
|
@@ -1769,7 +1862,7 @@ async function inspectContinuationState(stateDir) {
|
|
|
1769
1862
|
return { status: "error", details: [manifestSecurityError] };
|
|
1770
1863
|
let manifest;
|
|
1771
1864
|
try {
|
|
1772
|
-
manifest = JSON.parse(await
|
|
1865
|
+
manifest = JSON.parse(await readBoundedOwnerOnlyFile(manifestPath, MAX_MANIFEST_BYTES, "Continuation v3 manifest"));
|
|
1773
1866
|
}
|
|
1774
1867
|
catch (error) {
|
|
1775
1868
|
return { status: "error", details: [`Continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
|
|
@@ -1777,21 +1870,116 @@ async function inspectContinuationState(stateDir) {
|
|
|
1777
1870
|
if (!isContinuationStoreManifest(manifest, CONTINUATION_RECORD_STORE_SCHEMA_VERSION, "per-record-v3")) {
|
|
1778
1871
|
return { status: "error", details: ["Continuation store manifest has an unsupported or malformed schema."] };
|
|
1779
1872
|
}
|
|
1780
|
-
const
|
|
1873
|
+
const recordsDirectoryPath = join(stateDir, "records-v3");
|
|
1874
|
+
const recordsDirectoryError = await inspectContinuationRecordsDirectory(recordsDirectoryPath);
|
|
1781
1875
|
if (recordsDirectoryError !== undefined)
|
|
1782
1876
|
return { status: "error", details: [recordsDirectoryError] };
|
|
1783
|
-
|
|
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);
|
|
1784
1889
|
if (!transaction.valid)
|
|
1785
1890
|
return { status: "error", details: [transaction.detail] };
|
|
1786
|
-
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.");
|
|
1787
1973
|
if (!rollbackGuard.valid)
|
|
1788
1974
|
return { status: "error", details: [rollbackGuard.detail] };
|
|
1789
1975
|
const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
|
|
1790
1976
|
if (!owner.valid)
|
|
1791
1977
|
return { status: "error", details: [owner.detail] };
|
|
1792
|
-
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);
|
|
1793
1979
|
return {
|
|
1794
1980
|
status: transaction.pending
|
|
1981
|
+
|| originGroups.hasEntries
|
|
1982
|
+
|| legacyMigrationPending
|
|
1795
1983
|
|| manifest.stats.unresolvedDelivery > 0
|
|
1796
1984
|
|| manifest.stats.deadLettered > 0
|
|
1797
1985
|
|| manifest.stats.historyDegraded > 0
|
|
@@ -1800,6 +1988,19 @@ async function inspectContinuationState(stateDir) {
|
|
|
1800
1988
|
details,
|
|
1801
1989
|
};
|
|
1802
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
|
+
}
|
|
1803
2004
|
const legacyManifestPath = join(stateDir, "continuation-store-v2.json");
|
|
1804
2005
|
let legacyManifestInfo;
|
|
1805
2006
|
try {
|
|
@@ -1810,7 +2011,10 @@ async function inspectContinuationState(stateDir) {
|
|
|
1810
2011
|
return { status: "error", details: [`Legacy continuation store manifest cannot be inspected: ${continuationReason(error)}`] };
|
|
1811
2012
|
}
|
|
1812
2013
|
}
|
|
2014
|
+
let legacyManifestExists = false;
|
|
2015
|
+
let legacyManifestDetails;
|
|
1813
2016
|
if (legacyManifestInfo !== undefined) {
|
|
2017
|
+
legacyManifestExists = true;
|
|
1814
2018
|
if (!legacyManifestInfo.isFile() || legacyManifestInfo.isSymbolicLink()) {
|
|
1815
2019
|
return { status: "error", details: ["Legacy continuation store manifest must be a regular file, not a symlink."] };
|
|
1816
2020
|
}
|
|
@@ -1819,7 +2023,7 @@ async function inspectContinuationState(stateDir) {
|
|
|
1819
2023
|
return { status: "error", details: [manifestSecurityError] };
|
|
1820
2024
|
let legacyManifest;
|
|
1821
2025
|
try {
|
|
1822
|
-
legacyManifest = JSON.parse(await
|
|
2026
|
+
legacyManifest = JSON.parse(await readBoundedOwnerOnlyFile(legacyManifestPath, MAX_MANIFEST_BYTES, "Continuation v2 manifest"));
|
|
1823
2027
|
}
|
|
1824
2028
|
catch (error) {
|
|
1825
2029
|
return { status: "error", details: [`Legacy continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
|
|
@@ -1827,22 +2031,71 @@ async function inspectContinuationState(stateDir) {
|
|
|
1827
2031
|
if (!isContinuationStoreManifest(legacyManifest, 2, "per-record-v2")) {
|
|
1828
2032
|
return { status: "error", details: ["Legacy continuation store manifest has an unsupported or malformed schema."] };
|
|
1829
2033
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
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"), {
|
|
1832
2051
|
allowV2RollbackGuard: true,
|
|
1833
2052
|
});
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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) {
|
|
1842
2095
|
const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
|
|
1843
2096
|
if (!owner.valid)
|
|
1844
2097
|
return { status: "error", details: [owner.detail] };
|
|
1845
|
-
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);
|
|
1846
2099
|
return { status: "waiting", details };
|
|
1847
2100
|
}
|
|
1848
2101
|
const storePath = join(stateDir, "continuations-v1.json");
|
|
@@ -1868,7 +2121,7 @@ async function inspectContinuationState(stateDir) {
|
|
|
1868
2121
|
}
|
|
1869
2122
|
let parsed;
|
|
1870
2123
|
try {
|
|
1871
|
-
parsed = JSON.parse(await
|
|
2124
|
+
parsed = JSON.parse(await readBoundedOwnerOnlyFile(storePath, MAX_LEGACY_STORE_BYTES, "Continuation legacy store"));
|
|
1872
2125
|
}
|
|
1873
2126
|
catch (error) {
|
|
1874
2127
|
return { status: "error", details: [`Continuation ledger contains invalid JSON: ${continuationReason(error)}`] };
|
|
@@ -1970,6 +2223,14 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
|
|
|
1970
2223
|
const securityError = continuationOwnershipError(recordInfo, "record", 0o600);
|
|
1971
2224
|
if (securityError !== undefined)
|
|
1972
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
|
+
}
|
|
1973
2234
|
}
|
|
1974
2235
|
}
|
|
1975
2236
|
catch (error) {
|
|
@@ -1977,7 +2238,247 @@ async function inspectContinuationRecordsDirectory(path, options = {}) {
|
|
|
1977
2238
|
}
|
|
1978
2239
|
return undefined;
|
|
1979
2240
|
}
|
|
1980
|
-
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.") {
|
|
1981
2482
|
let info;
|
|
1982
2483
|
try {
|
|
1983
2484
|
info = await lstat(path);
|
|
@@ -1986,12 +2487,12 @@ async function inspectContinuationRollbackGuard(path, required) {
|
|
|
1986
2487
|
if (continuationFsCode(error) === "ENOENT") {
|
|
1987
2488
|
return required
|
|
1988
2489
|
? { valid: false, detail: "Continuation v2 rollback guard is missing from the v3 store." }
|
|
1989
|
-
: { valid: true, detail:
|
|
2490
|
+
: { valid: true, detail: missingDetail };
|
|
1990
2491
|
}
|
|
1991
2492
|
return { valid: false, detail: `Continuation v2 rollback guard cannot be inspected: ${continuationReason(error)}` };
|
|
1992
2493
|
}
|
|
1993
|
-
if (!info.isFile() || info.isSymbolicLink()) {
|
|
1994
|
-
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." };
|
|
1995
2496
|
}
|
|
1996
2497
|
const securityError = continuationOwnershipError(info, "v2 rollback guard", 0o600);
|
|
1997
2498
|
if (securityError !== undefined)
|
|
@@ -2001,7 +2502,7 @@ async function inspectContinuationRollbackGuard(path, required) {
|
|
|
2001
2502
|
}
|
|
2002
2503
|
let contents;
|
|
2003
2504
|
try {
|
|
2004
|
-
contents = await
|
|
2505
|
+
contents = await readBoundedOwnerOnlyFile(path, 4 * 1024, "Continuation v2 rollback guard");
|
|
2005
2506
|
}
|
|
2006
2507
|
catch (error) {
|
|
2007
2508
|
return { valid: false, detail: `Continuation v2 rollback guard cannot be read: ${continuationReason(error)}` };
|
|
@@ -2014,26 +2515,62 @@ async function inspectContinuationRollbackGuard(path, required) {
|
|
|
2014
2515
|
detail: "The owner-only v2 rollback guard prevents older runtimes from opening stale continuation records.",
|
|
2015
2516
|
};
|
|
2016
2517
|
}
|
|
2017
|
-
async function inspectContinuationTransaction(path) {
|
|
2518
|
+
async function inspectContinuationTransaction(path, expectedSchemaVersion) {
|
|
2519
|
+
const versionLabel = `v${String(expectedSchemaVersion)}`;
|
|
2018
2520
|
let info;
|
|
2019
2521
|
try {
|
|
2020
2522
|
info = await lstat(path);
|
|
2021
2523
|
}
|
|
2022
2524
|
catch (error) {
|
|
2023
2525
|
return continuationFsCode(error) === "ENOENT"
|
|
2024
|
-
? { valid: true, pending: false, detail:
|
|
2025
|
-
: {
|
|
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
|
+
};
|
|
2026
2532
|
}
|
|
2027
|
-
if (!info.isFile() || info.isSymbolicLink()) {
|
|
2028
|
-
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
|
+
};
|
|
2029
2539
|
}
|
|
2030
|
-
const securityError = continuationOwnershipError(info,
|
|
2540
|
+
const securityError = continuationOwnershipError(info, `${versionLabel} transaction`, 0o600);
|
|
2031
2541
|
if (securityError !== undefined)
|
|
2032
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
|
+
}
|
|
2033
2569
|
return {
|
|
2034
2570
|
valid: true,
|
|
2035
2571
|
pending: true,
|
|
2036
|
-
|
|
2572
|
+
transaction,
|
|
2573
|
+
detail: `An interrupted durable ${versionLabel} transaction is present and will be completed idempotently by the state owner.`,
|
|
2037
2574
|
};
|
|
2038
2575
|
}
|
|
2039
2576
|
function continuationOwnershipError(info, label, expectedMode) {
|
|
@@ -2046,14 +2583,14 @@ function continuationOwnershipError(info, label, expectedMode) {
|
|
|
2046
2583
|
return undefined;
|
|
2047
2584
|
}
|
|
2048
2585
|
function isContinuationLedger(value) {
|
|
2049
|
-
return
|
|
2050
|
-
&& value.schemaVersion === CONTINUATION_STORE_SCHEMA_VERSION
|
|
2051
|
-
&& isDoctorObject(value.records);
|
|
2586
|
+
return isStoreFile(value);
|
|
2052
2587
|
}
|
|
2053
2588
|
function isContinuationStoreManifest(value, schemaVersion, format) {
|
|
2054
2589
|
if (!isDoctorObject(value)
|
|
2055
2590
|
|| value.schemaVersion !== schemaVersion
|
|
2056
|
-
||
|
|
2591
|
+
|| !isDurableGeneration(value.generation)
|
|
2592
|
+
|| !requiredDate(value.updatedAt)
|
|
2593
|
+
|| (value.rollbackGuardRequired !== undefined && typeof value.rollbackGuardRequired !== "boolean")
|
|
2057
2594
|
|| !isDoctorObject(value.stats)
|
|
2058
2595
|
|| value.stats.format !== format
|
|
2059
2596
|
|| !isDoctorObject(value.stats.limits))
|