@logictan/dsh-config-manager 0.1.60
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 +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 子进程 crash 注入 harness(真实 process death 模拟;由 phase3-consistency.test.ts spawn)。
|
|
3
|
+
*
|
|
4
|
+
* 用法:node phase3-child-crash.ts <transactionsDir> <sideEffectFile|null> <mode>
|
|
5
|
+
* mode:after-lock / after-journal-create / after-snapshot / before-step / after-side-effect /
|
|
6
|
+
* after-step-done / before-commit / after-commit / during-rollback
|
|
7
|
+
* kill 用 SIGKILL,finally 不执行,真实模拟 crash。
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { JournalStore, createJournalEntry } from './journal.js';
|
|
12
|
+
import { sha256Hex } from '../utils/hashing.js';
|
|
13
|
+
const [txDirRaw, sideEffectRaw, modeRaw] = process.argv.slice(2);
|
|
14
|
+
const txDir = txDirRaw;
|
|
15
|
+
const sideEffectFile = sideEffectRaw === 'null' || sideEffectRaw === undefined ? null : sideEffectRaw;
|
|
16
|
+
const modeName = modeRaw ?? '';
|
|
17
|
+
const OP_ID = '00000000-0000-4000-8000-00000000dead';
|
|
18
|
+
const OWNER = 'child-instance';
|
|
19
|
+
const FP = 'fp-child';
|
|
20
|
+
async function main() {
|
|
21
|
+
const store = new JournalStore({ transactionsDir: txDir });
|
|
22
|
+
await store.ensureDirs();
|
|
23
|
+
const base = {
|
|
24
|
+
operationId: OP_ID, ownerInstanceId: OWNER, lockId: 'child-lock', packageVersion: '0.1.54', environmentFingerprint: FP,
|
|
25
|
+
};
|
|
26
|
+
if (modeName === 'after-lock') {
|
|
27
|
+
await fs.writeFile(path.join(txDir, '..', 'environment.lock'), JSON.stringify({ owner: { instanceId: OWNER }, op: 'import-apply' }));
|
|
28
|
+
process.kill(process.pid, 'SIGKILL');
|
|
29
|
+
}
|
|
30
|
+
if (modeName === 'after-journal-create') {
|
|
31
|
+
await store.create(createJournalEntry('import-apply', base, new Date().toISOString()));
|
|
32
|
+
process.kill(process.pid, 'SIGKILL');
|
|
33
|
+
}
|
|
34
|
+
if (modeName === 'after-snapshot') {
|
|
35
|
+
const j = createJournalEntry('import-apply', base, new Date().toISOString());
|
|
36
|
+
j.snapshotId = 'snap-child';
|
|
37
|
+
await store.persist(OP_ID, j);
|
|
38
|
+
process.kill(process.pid, 'SIGKILL');
|
|
39
|
+
}
|
|
40
|
+
if (modeName === 'before-step' || modeName === 'after-side-effect' || modeName === 'after-step-done') {
|
|
41
|
+
const j = createJournalEntry('import-apply', base, new Date().toISOString());
|
|
42
|
+
j.plannedSteps = ['s1'];
|
|
43
|
+
j.steps = {
|
|
44
|
+
s1: { adapter: 'skills', ref: 'child.md', kind: 'Update', external: false, beforeFp: 'before-child', afterFp: null, status: 'planned', appliedAt: null },
|
|
45
|
+
};
|
|
46
|
+
j.state = 'APPLYING';
|
|
47
|
+
await store.persist(OP_ID, j);
|
|
48
|
+
if (modeName === 'before-step')
|
|
49
|
+
process.kill(process.pid, 'SIGKILL');
|
|
50
|
+
if (sideEffectFile !== null) {
|
|
51
|
+
await fs.writeFile(sideEffectFile, 'side-effect-content');
|
|
52
|
+
}
|
|
53
|
+
if (modeName === 'after-side-effect')
|
|
54
|
+
process.kill(process.pid, 'SIGKILL');
|
|
55
|
+
j.steps.s1.afterFp = sha256Hex(new TextEncoder().encode('side-effect-content'));
|
|
56
|
+
j.steps.s1.status = 'done';
|
|
57
|
+
j.steps.s1.appliedAt = new Date().toISOString();
|
|
58
|
+
await store.persist(OP_ID, j);
|
|
59
|
+
if (modeName === 'after-step-done')
|
|
60
|
+
process.kill(process.pid, 'SIGKILL');
|
|
61
|
+
}
|
|
62
|
+
if (modeName === 'before-commit') {
|
|
63
|
+
const j = createJournalEntry('import-apply', base, new Date().toISOString());
|
|
64
|
+
j.state = 'VALIDATING';
|
|
65
|
+
j.plannedSteps = ['s1'];
|
|
66
|
+
j.steps = { s1: { adapter: 'skills', ref: 'c.md', kind: 'Update', external: false, beforeFp: 'b', afterFp: 'a', status: 'done', appliedAt: null } };
|
|
67
|
+
await store.persist(OP_ID, j);
|
|
68
|
+
process.kill(process.pid, 'SIGKILL');
|
|
69
|
+
}
|
|
70
|
+
if (modeName === 'after-commit') {
|
|
71
|
+
const j = createJournalEntry('import-apply', base, new Date().toISOString());
|
|
72
|
+
j.state = 'COMMITTED';
|
|
73
|
+
j.commit = { at: new Date().toISOString(), validated: true, validationWarnings: [] };
|
|
74
|
+
await store.persist(OP_ID, j);
|
|
75
|
+
process.kill(process.pid, 'SIGKILL');
|
|
76
|
+
}
|
|
77
|
+
if (modeName === 'during-rollback') {
|
|
78
|
+
const j = createJournalEntry('import-apply', base, new Date().toISOString());
|
|
79
|
+
j.state = 'ROLLING_BACK';
|
|
80
|
+
j.rollback = { attemptedAt: new Date().toISOString(), full: false, failed: [], entryDone: { 0: true } };
|
|
81
|
+
await store.persist(OP_ID, j);
|
|
82
|
+
process.kill(process.pid, 'SIGKILL');
|
|
83
|
+
}
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
main().catch((err) => { process.stderr.write(String(err)); process.exit(2); });
|
|
87
|
+
//# sourceMappingURL=phase3-child-crash.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { JournalStore } from './journal.ts';
|
|
2
|
+
import { type ReconcileProbeHooks } from './reconcile.ts';
|
|
3
|
+
import { type LockState, type MutationLockContext } from '../utils/env-lock.ts';
|
|
4
|
+
import type { JournalStepRecord } from './types.ts';
|
|
5
|
+
/** 存在未收敛的 active transaction → 拒绝创建第二个(active≤1)。 */
|
|
6
|
+
export declare class TransactionRecoveryRequiredError extends Error {
|
|
7
|
+
constructor(opId: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Phase 4 生产 journal↔snapshot 绑定 API(deferred 模式):
|
|
11
|
+
* fn 收到此 ctx,在【首个 destructive side effect 前】:
|
|
12
|
+
* - 用 plan 创建 op-bound snapshot(operationId/operationType/environmentFingerprint/ownerInstanceId)
|
|
13
|
+
* - `bindSnapshot(id)`:journal 记录 snapshotId,CREATED→SNAPSHOT_CREATED
|
|
14
|
+
* - 首个 mutation 前 `markApplying()`:SNAPSHOT_CREATED→APPLYING
|
|
15
|
+
* 保证「快照 durable+verified 且 journal 已知」先于任何写。显式 ctx,无 process-global reentrancy。
|
|
16
|
+
*/
|
|
17
|
+
export interface JournalRunContext {
|
|
18
|
+
operationId: string;
|
|
19
|
+
operationType: string;
|
|
20
|
+
environmentFingerprint: string;
|
|
21
|
+
ownerInstanceId: string;
|
|
22
|
+
/** 记录 journal.snapshotId 并推进 CREATED→SNAPSHOT_CREATED(或幂等)。 */
|
|
23
|
+
bindSnapshot: (snapshotId: string) => Promise<void>;
|
|
24
|
+
/** 首个 destructive side effect 前调用:SNAPSHOT_CREATED/CREATED→APPLYING。 */
|
|
25
|
+
markApplying: () => Promise<void>;
|
|
26
|
+
/** 逐计划项 WAL step 记录(P2-B,Phase 8):文件类带 fp,非文件类 external=true。 */
|
|
27
|
+
recordStep: (step: JournalStepRecord) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** 把环境锁分类映射到 startup 判定:ACQUIRED 视同 LOCKED(有活跃 owner),IO/PERMISSION 保守 LOCKED。 */
|
|
30
|
+
export declare function mapLockStateForStartup(state: LockState): 'LOCKED' | 'STALE_LOCK_DETECTED' | 'UNKNOWN_STATE' | 'FREE';
|
|
31
|
+
export interface Phase3RecoveryOptions {
|
|
32
|
+
dataDir: string;
|
|
33
|
+
packageVersion: string;
|
|
34
|
+
/** 覆盖环境指纹(测试) */
|
|
35
|
+
environmentFingerprint?: string;
|
|
36
|
+
/** 环境指纹持久化 token 目录(缺省 dataDir) */
|
|
37
|
+
fingerprintDataDir?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Phase 4 F21/F11 生产正向校验:校验 journal 引用的 snapshot 是否「存在 + READY + verified +
|
|
40
|
+
* op/env/owner binding 匹配」。缺省 = 保守 false(无法证明存在 → 不强推回滚)。
|
|
41
|
+
* 宿主注入 FileSnapshotStore + verifySnapshot + manifest binding 校验实现。
|
|
42
|
+
*/
|
|
43
|
+
snapshotExists?: (snapshotId: string | null, binding?: SnapshotBindingRef) => Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
/** journal↔snapshot binding 引用(与 reconcile ReconcileProbeHooks.snapshotExists 的 binding 一致)。 */
|
|
46
|
+
export interface SnapshotBindingRef {
|
|
47
|
+
operationId?: string;
|
|
48
|
+
ownerInstanceId?: string;
|
|
49
|
+
environmentFingerprint?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare class Phase3Recovery {
|
|
52
|
+
readonly store: JournalStore;
|
|
53
|
+
readonly packageVersion: string;
|
|
54
|
+
private readonly dataDir;
|
|
55
|
+
private readonly fingerprintDataDir;
|
|
56
|
+
private environmentFingerprint;
|
|
57
|
+
private readonly snapshotExistsFn;
|
|
58
|
+
/** 内存 SAFE MODE 标志(isBlocked 同步谓词用) */
|
|
59
|
+
safeModeActive: boolean;
|
|
60
|
+
constructor(opts: Phase3RecoveryOptions);
|
|
61
|
+
/** 计算环境指纹(持久化 token;跨启动稳定)。在 startup 前调用一次。 */
|
|
62
|
+
initFingerprint(): Promise<string>;
|
|
63
|
+
/** isBlocked 同步谓词(供 withMutationLock / runWithMutationLock 注入;env-lock 只问 blocked?) */
|
|
64
|
+
isBlocked(): boolean;
|
|
65
|
+
/** 刷新 SAFE MODE 标志(读 durable 标记) */
|
|
66
|
+
refreshSafeMode(): Promise<void>;
|
|
67
|
+
/** 同步探测 durable SAFE MODE 标记(宿主 apply() 同步阶段、scheduler.start() 前调用,保证先阻断)。 */
|
|
68
|
+
probeSafeModeSync(): boolean;
|
|
69
|
+
/** 保守 hooks:无法证明默认 needs-attention(绝不自动恢复/回滚)。snapshotExists 用宿主注入的正向校验(若提供)。 */
|
|
70
|
+
private conservativeHooks;
|
|
71
|
+
/**
|
|
72
|
+
* 启动只读 reconcile:返回是否需 SAFE MODE / RECOVERY_REQUIRED,并写 durable 标记。
|
|
73
|
+
* @param lockState 宿主 EnvironmentLockManager 的 inspectLockState 结果(只分类,不自动 recover)
|
|
74
|
+
* @param expectedOwnershipInstanceId 若在显式 recovery 前已捕获 stale ownership 的 owner.instanceId,传入以做 P1-A binding 校验
|
|
75
|
+
*/
|
|
76
|
+
startup(lockState: LockState, expectedOwnershipInstanceId?: string | null): Promise<{
|
|
77
|
+
safeModeRequired: boolean;
|
|
78
|
+
recoveryRequired: boolean;
|
|
79
|
+
}>;
|
|
80
|
+
/** P1-A:读取 crashed stale ownership 的 owner.instanceId(environment.lock 的 owner 证据;不可用返回 null)。 */
|
|
81
|
+
captureStaleOwnershipInstanceId(): Promise<string | null>;
|
|
82
|
+
/** 用户确认恢复后清除 SAFE MODE(清空 durable 标记 + 内存标志) */
|
|
83
|
+
clearSafeMode(): Promise<void>;
|
|
84
|
+
/** 保守 reconcile hooks(供启动 barrier / inspect 用) */
|
|
85
|
+
get recoveryHooks(): ReconcileProbeHooks;
|
|
86
|
+
/** 环境指纹(供启动 barrier 用) */
|
|
87
|
+
get recoveryEnvFingerprint(): string;
|
|
88
|
+
/**
|
|
89
|
+
* 生产 journal 包装(关闭 P0-A):在【已持 GLOBAL 锁】下,为该 destructive operation
|
|
90
|
+
* 创建 durable journal(CREATED → snapshot → APPLYING → 执行真实引擎 → COMMITTED → 规整)。
|
|
91
|
+
*
|
|
92
|
+
* - active≤1(§14):创建前扫描 active/;存在非 terminal 残留(非当前 live owner)→ 抛错阻断,不建第二个 journal。
|
|
93
|
+
* - Journal→Lock 绑定(§15/P1-A):ownerInstanceId = lockCtx.token.instanceId(= Phase 2 activeInstanceId,
|
|
94
|
+
* acquisition-specific);lockId = 同一 ownership epoch identity(ownerInstanceId)。recovery 时强制校验。
|
|
95
|
+
* - 不 double-acquire(§6):调用方(host gate)已持锁,本方法只负责 journal 生命周期,不 re-acquire、不 release(release 由 gate 负责)。
|
|
96
|
+
* - 异常 → NEEDS_ATTENTION + durable SAFE MODE + rethrow(不破坏既有错误/响应流)。
|
|
97
|
+
* - 返回 { operationId, result }。
|
|
98
|
+
*
|
|
99
|
+
* Phase 4 snapshot 两种模式:
|
|
100
|
+
* - `snapshotProvider`(pre-fn):journal CREATED → 调用 provider 创建并 verify snapshot → 绑定 snapshotId
|
|
101
|
+
* → SNAPSHOT_CREATED → APPLYING → fn(适合 provider 不依赖请求体 plan 的场景)。
|
|
102
|
+
* - `deferredSnapshot`(推荐,F20 生产接线):plan 只在 handler 解析请求体后可用,因此 journal 停留在 CREATED,
|
|
103
|
+
* fn 收到 `ctx`(含 `bindSnapshot` / `markApplying`)。引擎在【首个 destructive side effect 前】用 plan 创建
|
|
104
|
+
* op-bound snapshot → ctx.bindSnapshot(id)(CREATED→SNAPSHOT_CREATED,记录 snapshotId)→ 首个 mutation 前
|
|
105
|
+
* ctx.markApplying()(SNAPSHOT_CREATED→APPLYING)。保证「快照 durable+verified 且 journal 已知」先于任何写。
|
|
106
|
+
*/
|
|
107
|
+
runJournaled<T>(opts: {
|
|
108
|
+
operationType: string;
|
|
109
|
+
lockCtx: MutationLockContext;
|
|
110
|
+
/** 可选:真实 pre-operation snapshot(回滚点),返回 snapshotId(pre-fn 模式)。 */
|
|
111
|
+
snapshotProvider?(): Promise<string | null>;
|
|
112
|
+
/** 可选:deferred 绑定模式——journal 停留 CREATED,fn 收到 ctx 自行 bindSnapshot/markApplying。 */
|
|
113
|
+
deferredSnapshot?: boolean;
|
|
114
|
+
fn: (ctx?: JournalRunContext) => Promise<T>;
|
|
115
|
+
}): Promise<{
|
|
116
|
+
operationId: string;
|
|
117
|
+
result: T;
|
|
118
|
+
}>;
|
|
119
|
+
/**
|
|
120
|
+
* 轻量 operation 包装(意图 journal,供外部/不可证明操作如 sync-push / reinstall 用):
|
|
121
|
+
* 创建 CREATED → APPLYING(含 external intent step)→ 执行 → COMMITTED;异常 → NEEDS_ATTENTION + SAFE MODE。
|
|
122
|
+
*/
|
|
123
|
+
runExternalIntent<T>(opts: {
|
|
124
|
+
operationType: string;
|
|
125
|
+
lockCtx: MutationLockContext;
|
|
126
|
+
intent: {
|
|
127
|
+
adapter: string;
|
|
128
|
+
ref: string;
|
|
129
|
+
kind: string;
|
|
130
|
+
};
|
|
131
|
+
fn: (ctx?: {
|
|
132
|
+
operationId: string;
|
|
133
|
+
}) => Promise<T>;
|
|
134
|
+
}): Promise<{
|
|
135
|
+
operationId: string;
|
|
136
|
+
result: T;
|
|
137
|
+
}>;
|
|
138
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 Host Recovery 集成助手。
|
|
3
|
+
*
|
|
4
|
+
* 职责(最小、保守):
|
|
5
|
+
* - 持有 JournalStore + 内存 SAFE MODE 标志(isBlocked 同步谓词,供 env-lock 注入)。
|
|
6
|
+
* - `startup(lockState)`:启动时**只读**扫描 + 锁状态判定 → 设 SAFE MODE / RECOVERY_REQUIRED(durable)。
|
|
7
|
+
* **不自动 recover stale lock**(Rev 3 P1-NEW-2)。
|
|
8
|
+
* - 保守 reconcile hooks:任何无法证明的 step → needs-attention → SAFE MODE(绝不自动恢复/回滚)。
|
|
9
|
+
*
|
|
10
|
+
* 引擎级 WAL / 指纹(import/restore 逐 step 插桩)为 Phase 3 v1 之外(§33 不要临时扩大)——
|
|
11
|
+
* 本助手提供保守的「operation intent + crash 判定」基础,供宿主在各 destructive 入口包 Coordinator。
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import fssync from 'node:fs';
|
|
16
|
+
import { JournalStore, environmentFingerprint as computeFingerprint, SAFE_MODE_MARKER, generateOperationId, createJournalEntry, transitionJournalState, isTerminalState, } from './journal.js';
|
|
17
|
+
import { inspectStartup } from './reconcile.js';
|
|
18
|
+
import { OWNERSHIP_FILE } from '../utils/env-lock.js';
|
|
19
|
+
import { sha256Hex } from '../utils/hashing.js';
|
|
20
|
+
/** 存在未收敛的 active transaction → 拒绝创建第二个(active≤1)。 */
|
|
21
|
+
export class TransactionRecoveryRequiredError extends Error {
|
|
22
|
+
constructor(opId) {
|
|
23
|
+
super(`active transaction ${opId} 未收敛(待 recover / reconcile),拒绝创建新 transaction`);
|
|
24
|
+
this.name = 'TransactionRecoveryRequiredError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** 把环境锁分类映射到 startup 判定:ACQUIRED 视同 LOCKED(有活跃 owner),IO/PERMISSION 保守 LOCKED。 */
|
|
28
|
+
export function mapLockStateForStartup(state) {
|
|
29
|
+
if (state === 'STALE_LOCK_DETECTED')
|
|
30
|
+
return 'STALE_LOCK_DETECTED';
|
|
31
|
+
if (state === 'UNKNOWN_STATE')
|
|
32
|
+
return 'UNKNOWN_STATE';
|
|
33
|
+
return 'LOCKED'; // ACQUIRED / LOCKED / LOCK_IO_ERROR / PERMISSION_ERROR → 保守 LOCKED
|
|
34
|
+
}
|
|
35
|
+
export class Phase3Recovery {
|
|
36
|
+
store;
|
|
37
|
+
packageVersion;
|
|
38
|
+
dataDir;
|
|
39
|
+
fingerprintDataDir;
|
|
40
|
+
environmentFingerprint;
|
|
41
|
+
snapshotExistsFn;
|
|
42
|
+
/** 内存 SAFE MODE 标志(isBlocked 同步谓词用) */
|
|
43
|
+
safeModeActive = false;
|
|
44
|
+
constructor(opts) {
|
|
45
|
+
this.dataDir = opts.dataDir;
|
|
46
|
+
this.fingerprintDataDir = opts.fingerprintDataDir ?? opts.dataDir;
|
|
47
|
+
this.store = new JournalStore({ transactionsDir: path.join(opts.dataDir, 'transactions') });
|
|
48
|
+
this.packageVersion = opts.packageVersion;
|
|
49
|
+
this.environmentFingerprint = opts.environmentFingerprint ?? 'unknown';
|
|
50
|
+
this.snapshotExistsFn = opts.snapshotExists;
|
|
51
|
+
}
|
|
52
|
+
/** 计算环境指纹(持久化 token;跨启动稳定)。在 startup 前调用一次。 */
|
|
53
|
+
async initFingerprint() {
|
|
54
|
+
try {
|
|
55
|
+
await fs.mkdir(this.fingerprintDataDir, { recursive: true });
|
|
56
|
+
this.environmentFingerprint = await computeFingerprint(this.fingerprintDataDir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
this.environmentFingerprint = 'unknown';
|
|
60
|
+
}
|
|
61
|
+
return this.environmentFingerprint;
|
|
62
|
+
}
|
|
63
|
+
/** isBlocked 同步谓词(供 withMutationLock / runWithMutationLock 注入;env-lock 只问 blocked?) */
|
|
64
|
+
isBlocked() {
|
|
65
|
+
return this.safeModeActive;
|
|
66
|
+
}
|
|
67
|
+
/** 刷新 SAFE MODE 标志(读 durable 标记) */
|
|
68
|
+
async refreshSafeMode() {
|
|
69
|
+
this.safeModeActive = await this.store.readSafeMode().catch(() => false);
|
|
70
|
+
}
|
|
71
|
+
/** 同步探测 durable SAFE MODE 标记(宿主 apply() 同步阶段、scheduler.start() 前调用,保证先阻断)。 */
|
|
72
|
+
probeSafeModeSync() {
|
|
73
|
+
const p = path.join(this.dataDir, 'transactions', SAFE_MODE_MARKER);
|
|
74
|
+
try {
|
|
75
|
+
const text = fssync.existsSync(p) ? fssync.readFileSync(p, 'utf8') : '';
|
|
76
|
+
const blocked = /blocked|true/i.test(text);
|
|
77
|
+
this.safeModeActive = blocked;
|
|
78
|
+
return blocked;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** 保守 hooks:无法证明默认 needs-attention(绝不自动恢复/回滚)。snapshotExists 用宿主注入的正向校验(若提供)。 */
|
|
85
|
+
conservativeHooks() {
|
|
86
|
+
const snapshotExists = this.snapshotExistsFn;
|
|
87
|
+
return {
|
|
88
|
+
// P2-B(Phase 8):对带 afterFp/beforeFp 的本地文件 step 做真实磁盘指纹判定。
|
|
89
|
+
// 这使「可安全判定已应用」的整文件项在 crash 后判 recovered;无指纹项(external /
|
|
90
|
+
// before/after 均 null)→ unable(保守)。**不放松**「不可信/不可指纹 → needs-attention」边界。
|
|
91
|
+
verifyStepFingerprint: async (step) => {
|
|
92
|
+
if (step.external === true)
|
|
93
|
+
return 'unable';
|
|
94
|
+
if (step.beforeFp === null && step.afterFp === null)
|
|
95
|
+
return 'unable';
|
|
96
|
+
if (step.ref === '')
|
|
97
|
+
return 'unable';
|
|
98
|
+
let data;
|
|
99
|
+
try {
|
|
100
|
+
data = await fs.readFile(step.ref);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// 目标不存在:若已记录 afterFp(期望存在)→ 未应用;否则无法判定。
|
|
104
|
+
return step.afterFp !== null && step.beforeFp === null ? 'before-match' : 'none';
|
|
105
|
+
}
|
|
106
|
+
const fp = sha256Hex(data);
|
|
107
|
+
if (step.afterFp !== null && fp === step.afterFp)
|
|
108
|
+
return 'after-match';
|
|
109
|
+
if (step.beforeFp !== null && fp === step.beforeFp)
|
|
110
|
+
return 'before-match';
|
|
111
|
+
return 'none';
|
|
112
|
+
},
|
|
113
|
+
probeExternal: async () => 'unknown',
|
|
114
|
+
snapshotExists: async (snapshotId, binding) => {
|
|
115
|
+
if (snapshotExists === undefined)
|
|
116
|
+
return false; // 未注入 → 保守 false
|
|
117
|
+
return snapshotExists(snapshotId, binding);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 启动只读 reconcile:返回是否需 SAFE MODE / RECOVERY_REQUIRED,并写 durable 标记。
|
|
123
|
+
* @param lockState 宿主 EnvironmentLockManager 的 inspectLockState 结果(只分类,不自动 recover)
|
|
124
|
+
* @param expectedOwnershipInstanceId 若在显式 recovery 前已捕获 stale ownership 的 owner.instanceId,传入以做 P1-A binding 校验
|
|
125
|
+
*/
|
|
126
|
+
async startup(lockState, expectedOwnershipInstanceId) {
|
|
127
|
+
const env = {
|
|
128
|
+
environmentFingerprint: this.environmentFingerprint || 'unknown',
|
|
129
|
+
isLiveOwner: async () => false,
|
|
130
|
+
...(expectedOwnershipInstanceId ? { expectedOwnershipInstanceId } : {}),
|
|
131
|
+
};
|
|
132
|
+
const insp = await inspectStartup(this.store, this.conservativeHooks(), env, {}, mapLockStateForStartup(lockState));
|
|
133
|
+
if (insp.safeModeRequired)
|
|
134
|
+
this.safeModeActive = true;
|
|
135
|
+
return { safeModeRequired: insp.safeModeRequired, recoveryRequired: insp.recoveryRequired };
|
|
136
|
+
}
|
|
137
|
+
/** P1-A:读取 crashed stale ownership 的 owner.instanceId(environment.lock 的 owner 证据;不可用返回 null)。 */
|
|
138
|
+
async captureStaleOwnershipInstanceId() {
|
|
139
|
+
try {
|
|
140
|
+
const p = path.join(this.dataDir, 'locks', OWNERSHIP_FILE);
|
|
141
|
+
const text = await fs.readFile(p, 'utf8');
|
|
142
|
+
const rec = JSON.parse(text);
|
|
143
|
+
return typeof rec?.owner?.instanceId === 'string' && rec.owner.instanceId !== ''
|
|
144
|
+
? rec.owner.instanceId
|
|
145
|
+
: null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** 用户确认恢复后清除 SAFE MODE(清空 durable 标记 + 内存标志) */
|
|
152
|
+
async clearSafeMode() {
|
|
153
|
+
this.safeModeActive = false;
|
|
154
|
+
await this.store.writeSafeMode(false);
|
|
155
|
+
}
|
|
156
|
+
/** 保守 reconcile hooks(供启动 barrier / inspect 用) */
|
|
157
|
+
get recoveryHooks() { return this.conservativeHooks(); }
|
|
158
|
+
/** 环境指纹(供启动 barrier 用) */
|
|
159
|
+
get recoveryEnvFingerprint() { return this.environmentFingerprint || 'unknown'; }
|
|
160
|
+
/**
|
|
161
|
+
* 生产 journal 包装(关闭 P0-A):在【已持 GLOBAL 锁】下,为该 destructive operation
|
|
162
|
+
* 创建 durable journal(CREATED → snapshot → APPLYING → 执行真实引擎 → COMMITTED → 规整)。
|
|
163
|
+
*
|
|
164
|
+
* - active≤1(§14):创建前扫描 active/;存在非 terminal 残留(非当前 live owner)→ 抛错阻断,不建第二个 journal。
|
|
165
|
+
* - Journal→Lock 绑定(§15/P1-A):ownerInstanceId = lockCtx.token.instanceId(= Phase 2 activeInstanceId,
|
|
166
|
+
* acquisition-specific);lockId = 同一 ownership epoch identity(ownerInstanceId)。recovery 时强制校验。
|
|
167
|
+
* - 不 double-acquire(§6):调用方(host gate)已持锁,本方法只负责 journal 生命周期,不 re-acquire、不 release(release 由 gate 负责)。
|
|
168
|
+
* - 异常 → NEEDS_ATTENTION + durable SAFE MODE + rethrow(不破坏既有错误/响应流)。
|
|
169
|
+
* - 返回 { operationId, result }。
|
|
170
|
+
*
|
|
171
|
+
* Phase 4 snapshot 两种模式:
|
|
172
|
+
* - `snapshotProvider`(pre-fn):journal CREATED → 调用 provider 创建并 verify snapshot → 绑定 snapshotId
|
|
173
|
+
* → SNAPSHOT_CREATED → APPLYING → fn(适合 provider 不依赖请求体 plan 的场景)。
|
|
174
|
+
* - `deferredSnapshot`(推荐,F20 生产接线):plan 只在 handler 解析请求体后可用,因此 journal 停留在 CREATED,
|
|
175
|
+
* fn 收到 `ctx`(含 `bindSnapshot` / `markApplying`)。引擎在【首个 destructive side effect 前】用 plan 创建
|
|
176
|
+
* op-bound snapshot → ctx.bindSnapshot(id)(CREATED→SNAPSHOT_CREATED,记录 snapshotId)→ 首个 mutation 前
|
|
177
|
+
* ctx.markApplying()(SNAPSHOT_CREATED→APPLYING)。保证「快照 durable+verified 且 journal 已知」先于任何写。
|
|
178
|
+
*/
|
|
179
|
+
async runJournaled(opts) {
|
|
180
|
+
const { operationType, lockCtx, snapshotProvider, deferredSnapshot, fn } = opts;
|
|
181
|
+
const ownerInstanceId = (lockCtx?.token?.instanceId ?? 'unknown').toString();
|
|
182
|
+
// P1-A:ownership epoch identity = 真实 acquisition-specific ownerInstanceId(来自 lockCtx,
|
|
183
|
+
// 即 Phase 2 activeInstanceId;跨进程/跨持有不同)。不再用环境稳定合成串。
|
|
184
|
+
const ownershipIdentity = ownerInstanceId;
|
|
185
|
+
// active≤1:存在非 terminal 残留 → 阻断(不创建第二个 journal)
|
|
186
|
+
const activeIds = await this.store.scanActive();
|
|
187
|
+
for (const opId of activeIds) {
|
|
188
|
+
const j = await this.store.loadActive(opId);
|
|
189
|
+
if (j === null)
|
|
190
|
+
continue;
|
|
191
|
+
const ts = ['COMMITTED', 'ROLLED_BACK', 'RECOVERED', 'NEEDS_ATTENTION'];
|
|
192
|
+
if (!ts.includes(j.state)) {
|
|
193
|
+
throw new TransactionRecoveryRequiredError(opId);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const opId = generateOperationId();
|
|
197
|
+
const base = {
|
|
198
|
+
operationId: opId, ownerInstanceId, lockId: ownershipIdentity,
|
|
199
|
+
packageVersion: this.packageVersion, environmentFingerprint: this.environmentFingerprint || 'unknown',
|
|
200
|
+
};
|
|
201
|
+
await this.store.create(createJournalEntry(operationType, base, new Date().toISOString()));
|
|
202
|
+
let fnCompleted = false;
|
|
203
|
+
let terminalPersistAttempted = false;
|
|
204
|
+
// Phase 4 deferred 模式:把 journal 绑定 API 暴露给 fn(引擎在第一个 destructive side effect 前调用)。
|
|
205
|
+
// bindSnapshot:记录 snapshotId 并 CREATED→SNAPSHOT_CREATED;markApplying:SNAPSHOT_CREATED→APPLYING。
|
|
206
|
+
// 这些只在该 op 的同一 journal 上生效,杜绝 process-global reentrancy。
|
|
207
|
+
const journalCtx = {
|
|
208
|
+
operationId: opId,
|
|
209
|
+
operationType,
|
|
210
|
+
environmentFingerprint: this.environmentFingerprint || 'unknown',
|
|
211
|
+
ownerInstanceId,
|
|
212
|
+
bindSnapshot: async (snapshotId) => {
|
|
213
|
+
if (snapshotId === null || snapshotId === '')
|
|
214
|
+
throw new Error('bindSnapshot: snapshotId 为空');
|
|
215
|
+
// Reviewer A P2①:持久化失败必须传播(fail-closed)——若 journal 无法 durable 绑定 SNAPSHOT_CREATED,
|
|
216
|
+
// 引擎的 await bindSnapshot 抛错 → fn 抛错 → runJournaled catch(NEEDS_ATTENTION),mutation 绝不在无绑定下继续。
|
|
217
|
+
await this.store.update(opId, (j) => {
|
|
218
|
+
let next = { ...j, snapshotId };
|
|
219
|
+
if (next.state === 'CREATED')
|
|
220
|
+
next = transitionJournalState(next, 'SNAPSHOT_CREATED');
|
|
221
|
+
else if (next.state === 'SNAPSHOT_CREATED') { /* 幂等:已绑定 */ }
|
|
222
|
+
else if (next.state === 'APPLYING') { /* 允许:绑定已晚但幂等记录 */ }
|
|
223
|
+
else
|
|
224
|
+
throw new Error(`bindSnapshot 非法 state: ${next.state}`);
|
|
225
|
+
return next;
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
markApplying: async () => {
|
|
229
|
+
// Reviewer A P2①:同 fail-closed——SNAPSHOT_CREATED→APPLYING 持久化失败须传播,不得在未 APPLYING durable 下 mutation。
|
|
230
|
+
await this.store.update(opId, (j) => {
|
|
231
|
+
let next = j;
|
|
232
|
+
if (next.state === 'SNAPSHOT_CREATED')
|
|
233
|
+
next = transitionJournalState(next, 'APPLYING');
|
|
234
|
+
else if (next.state === 'CREATED')
|
|
235
|
+
next = transitionJournalState(next, 'APPLYING');
|
|
236
|
+
return next;
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
// P2-B(Phase 8):逐计划项 WAL step 记录(文件类带 fp;非文件类 external=true)。
|
|
240
|
+
// 与 bindSnapshot/markApplying 同一 fail-closed:持久化失败传播 → fn 抛错 → NEEDS_ATTENTION。
|
|
241
|
+
recordStep: async (rec) => {
|
|
242
|
+
const step = {
|
|
243
|
+
adapter: rec.adapter,
|
|
244
|
+
ref: rec.ref,
|
|
245
|
+
kind: rec.kind,
|
|
246
|
+
external: rec.external === true,
|
|
247
|
+
beforeFp: rec.beforeFp ?? null,
|
|
248
|
+
afterFp: rec.afterFp ?? null,
|
|
249
|
+
status: rec.status ?? (rec.external === true ? 'attention' : 'planned'),
|
|
250
|
+
appliedAt: new Date().toISOString(),
|
|
251
|
+
message: rec.message ?? null,
|
|
252
|
+
};
|
|
253
|
+
await this.store.update(opId, (j) => {
|
|
254
|
+
const plannedSteps = j.plannedSteps.includes(rec.id)
|
|
255
|
+
? j.plannedSteps
|
|
256
|
+
: [...j.plannedSteps, rec.id];
|
|
257
|
+
return { ...j, plannedSteps, steps: { ...j.steps, [rec.id]: step } };
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
try {
|
|
262
|
+
if (deferredSnapshot !== true && snapshotProvider !== undefined) {
|
|
263
|
+
const snapId = await snapshotProvider();
|
|
264
|
+
// P1-2 修复:snapshotProvider 返回 null 表示快照创建失败 → 显式 abort(不得在无快照下继续 mutation)
|
|
265
|
+
if (snapId === null) {
|
|
266
|
+
throw new Error(`snapshotProvider 返回 null(快照创建失败),abort operation ${operationType}`);
|
|
267
|
+
}
|
|
268
|
+
await journalCtx.bindSnapshot(snapId);
|
|
269
|
+
}
|
|
270
|
+
else if (deferredSnapshot !== true) {
|
|
271
|
+
await this.store.update(opId, (j) => transitionJournalState(j, 'APPLYING'));
|
|
272
|
+
}
|
|
273
|
+
// deferred 模式:保持 CREATED,fn 自行 bindSnapshot + markApplying(首个 destructive side effect 前)
|
|
274
|
+
const result = await fn(journalCtx);
|
|
275
|
+
fnCompleted = true;
|
|
276
|
+
// 尾操作:从任意 pre-commit 状态推进到 COMMITTED(合法链 CREATED/SNAPSHOT_CREATED→APPLYING→VALIDATING→COMMITTED)
|
|
277
|
+
await this.store.update(opId, (j) => {
|
|
278
|
+
let next = j;
|
|
279
|
+
if (next.state === 'CREATED')
|
|
280
|
+
next = transitionJournalState(next, 'APPLYING');
|
|
281
|
+
if (next.state === 'SNAPSHOT_CREATED')
|
|
282
|
+
next = transitionJournalState(next, 'APPLYING');
|
|
283
|
+
if (next.state === 'APPLYING')
|
|
284
|
+
next = transitionJournalState(next, 'VALIDATING');
|
|
285
|
+
next = transitionJournalState(next, 'COMMITTED');
|
|
286
|
+
return { ...next, commit: { at: new Date().toISOString(), validated: true, validationWarnings: [] } };
|
|
287
|
+
});
|
|
288
|
+
terminalPersistAttempted = true;
|
|
289
|
+
await this.store.moveToCompleted(opId).catch(() => undefined);
|
|
290
|
+
return { operationId: opId, result };
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
this.safeModeActive = true;
|
|
294
|
+
await this.store.writeSafeMode(true).catch(() => undefined);
|
|
295
|
+
if (fnCompleted && !terminalPersistAttempted) {
|
|
296
|
+
// 真实事务副作用已完成,但 terminal 持久化失败 → RECOVERY_REQUIRED(不伪造终态,保持非终态 journal)
|
|
297
|
+
// (此处保持 journal 非终态:apply 已成功、COMMITTED 未 durable —— 由下一轮显式 recovery 决定。
|
|
298
|
+
// SAFE MODE 已设;不额外写 NEEDS_ATTENTION,避免把「无法 durable 记录 outcome」误报成「已分类 NEEDS_ATTENTION」。)
|
|
299
|
+
}
|
|
300
|
+
else if (!fnCompleted) {
|
|
301
|
+
// fn / side effect 阶段失败:op 已开始但 outcome 不确定 → NEEDS_ATTENTION(durable 分类)
|
|
302
|
+
const j = await this.store.loadActive(opId);
|
|
303
|
+
if (j !== null && !isTerminalState(j.state)) {
|
|
304
|
+
await this.store.update(opId, (cur) => transitionJournalState(cur, 'NEEDS_ATTENTION')).catch(() => undefined);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 轻量 operation 包装(意图 journal,供外部/不可证明操作如 sync-push / reinstall 用):
|
|
312
|
+
* 创建 CREATED → APPLYING(含 external intent step)→ 执行 → COMMITTED;异常 → NEEDS_ATTENTION + SAFE MODE。
|
|
313
|
+
*/
|
|
314
|
+
async runExternalIntent(opts) {
|
|
315
|
+
const { operationType, lockCtx, intent, fn } = opts;
|
|
316
|
+
const ownerInstanceId = (lockCtx?.token?.instanceId ?? 'unknown').toString();
|
|
317
|
+
// P1-A:ownership epoch identity = 真实 acquisition-specific ownerInstanceId(非环境稳定串)
|
|
318
|
+
const ownershipIdentity = ownerInstanceId;
|
|
319
|
+
const opId = generateOperationId();
|
|
320
|
+
const base = {
|
|
321
|
+
operationId: opId, ownerInstanceId, lockId: ownershipIdentity,
|
|
322
|
+
packageVersion: this.packageVersion, environmentFingerprint: this.environmentFingerprint || 'unknown',
|
|
323
|
+
};
|
|
324
|
+
await this.store.create(createJournalEntry(operationType, base, new Date().toISOString()));
|
|
325
|
+
try {
|
|
326
|
+
await this.store.update(opId, (j) => {
|
|
327
|
+
const extStep = { adapter: intent.adapter, ref: intent.ref, kind: intent.kind, external: true, beforeFp: null, afterFp: null, status: 'planned', appliedAt: null };
|
|
328
|
+
const withStep = {
|
|
329
|
+
...j, plannedSteps: ['ext'], steps: { ext: extStep },
|
|
330
|
+
};
|
|
331
|
+
return transitionJournalState(withStep, 'APPLYING');
|
|
332
|
+
});
|
|
333
|
+
// Phase 4 F29/F30:把 operationId 暴露给 fn,使调用方(如 CLI reinstall)能在首个 destructive
|
|
334
|
+
// side effect 前写 durably-bound recovery point(operationId + 环境/版本元数据)。
|
|
335
|
+
const result = await fn({ operationId: opId });
|
|
336
|
+
await this.store.update(opId, (j) => {
|
|
337
|
+
let next = j;
|
|
338
|
+
if (next.state === 'SNAPSHOT_CREATED')
|
|
339
|
+
next = transitionJournalState(next, 'APPLYING');
|
|
340
|
+
if (next.state === 'APPLYING')
|
|
341
|
+
next = transitionJournalState(next, 'VALIDATING');
|
|
342
|
+
next = transitionJournalState(next, 'COMMITTED');
|
|
343
|
+
return { ...next, commit: { at: new Date().toISOString(), validated: true, validationWarnings: [] } };
|
|
344
|
+
});
|
|
345
|
+
await this.store.moveToCompleted(opId).catch(() => undefined);
|
|
346
|
+
return { operationId: opId, result };
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
const j = await this.store.loadActive(opId);
|
|
350
|
+
if (j !== null && !isTerminalState(j.state)) {
|
|
351
|
+
await this.store.update(opId, (cur) => transitionJournalState(cur, 'NEEDS_ATTENTION')).catch(() => undefined);
|
|
352
|
+
}
|
|
353
|
+
this.safeModeActive = true;
|
|
354
|
+
await this.store.writeSafeMode(true).catch(() => undefined);
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=phase3-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生产路径 child crash harness:用真实 Phase3Recovery.runJournaled(生产接线所用的助手)
|
|
3
|
+
* 执行一个 operation;在 fn(真实 side effect 写磁盘)后、COMMITTED 前 SIGKILL。
|
|
4
|
+
* 用于 phase3-production-integration.test.ts 验证「真实操作 crash → 残留非终态 journal → 可 reconcile」。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { Phase3Recovery } from './phase3-host.js';
|
|
9
|
+
const txDir = process.argv[2];
|
|
10
|
+
const dataDir = path.dirname(txDir);
|
|
11
|
+
const sideEffect = path.join(dataDir, 'side-effect.txt');
|
|
12
|
+
const FP = 'fp-prod';
|
|
13
|
+
async function main() {
|
|
14
|
+
const recovery = new Phase3Recovery({ dataDir, packageVersion: '0.1.54', environmentFingerprint: FP });
|
|
15
|
+
await recovery.store.ensureDirs();
|
|
16
|
+
const lockCtx = { token: { tokenId: 't', managerId: 'm', instanceId: 'child-prod', acquiredAt: Date.now() } };
|
|
17
|
+
try {
|
|
18
|
+
await recovery.runJournaled({
|
|
19
|
+
operationType: 'import-apply',
|
|
20
|
+
lockCtx,
|
|
21
|
+
fn: async () => {
|
|
22
|
+
// 真实 side effect:写磁盘文件
|
|
23
|
+
await fs.writeFile(sideEffect, 'side-effect-content');
|
|
24
|
+
// side effect 已完成、step done 尚未落 journal 前 crash
|
|
25
|
+
process.kill(process.pid, 'SIGKILL');
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// 被杀不会到这;若执行完则正常退出(测试用 before-step 场景)
|
|
31
|
+
}
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
main().catch((err) => { process.stderr.write(String(err)); process.exit(2); });
|
|
35
|
+
//# sourceMappingURL=phase3-prod-child.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|