dsh-config-manager 0.1.71 → 0.1.72
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 +111 -96
- package/README.zh-CN.md +207 -86
- package/lib/adapters/agent-instructions.js +1 -1
- package/lib/adapters/file-collection.js +1 -1
- package/lib/adapters/index.d.ts +1 -1
- package/lib/adapters/index.js +1 -1
- package/lib/adapters/link-report.d.ts +1 -0
- package/lib/adapters/link-report.js +27 -3
- package/lib/adapters/messages.d.ts +18 -0
- package/lib/adapters/messages.js +50 -0
- package/lib/adapters/plugin-files.js +3 -2
- package/lib/adapters/plugins.js +4 -3
- package/lib/adapters/prompts.d.ts +1 -0
- package/lib/adapters/prompts.js +7 -1
- package/lib/adapters/self.d.ts +7 -1
- package/lib/adapters/self.js +1 -1
- package/lib/adapters/sessions.d.ts +1 -0
- package/lib/adapters/sessions.js +4 -2
- package/lib/adapters/ui.d.ts +2 -2
- package/lib/adapters/ui.js +25 -25
- package/lib/adapters/workspaces.d.ts +1 -0
- package/lib/adapters/workspaces.js +7 -3
- package/lib/cli/actions.d.ts +9 -1
- package/lib/cli/actions.js +5 -2
- package/lib/cli/help.js +23 -10
- package/lib/cli/index.d.ts +23 -0
- package/lib/cli/index.js +90 -32
- package/lib/cli/sessions-inspect.js +3 -1
- package/lib/cli/sessions-repair.js +25 -6
- package/lib/cli/web/page.d.ts +12 -0
- package/lib/cli/web/page.js +21 -12
- package/lib/cli/web/routes.js +2 -2
- package/lib/cli/web/server.js +4 -4
- package/lib/client.d.ts +1349 -490
- package/lib/client.js +43047 -36738
- package/lib/core/backup-plan.js +30 -17
- package/lib/core/backup-verify.js +24 -23
- package/lib/core/backup.d.ts +5 -0
- package/lib/core/backup.js +51 -25
- package/lib/core/boot-rescue.js +4 -3
- package/lib/core/boot-safety.js +2 -1
- package/lib/core/command-rollback.d.ts +178 -0
- package/lib/core/command-rollback.js +289 -0
- package/lib/core/consult-source.js +7 -6
- package/lib/core/disk-usage.d.ts +74 -10
- package/lib/core/disk-usage.js +67 -4
- package/lib/core/engine-locale.d.ts +87 -0
- package/lib/core/engine-locale.js +125 -0
- package/lib/core/exporter.js +3 -3
- package/lib/core/fork-reconcile.d.ts +128 -0
- package/lib/core/fork-reconcile.js +150 -0
- package/lib/core/journal.js +6 -5
- package/lib/core/local-plugin-host.js +3 -2
- package/lib/core/local-plugin-pack.js +11 -6
- package/lib/core/messages-core.d.ts +292 -0
- package/lib/core/messages-core.js +615 -0
- package/lib/core/messages.d.ts +60 -2
- package/lib/core/messages.js +116 -13
- package/lib/core/migration-consult.js +21 -20
- package/lib/core/migration-history.js +19 -5
- package/lib/core/model-tools.d.ts +0 -22
- package/lib/core/model-tools.js +50 -27
- package/lib/core/phase3-host.js +30 -5
- package/lib/core/plugin-cli.js +18 -19
- package/lib/core/reconcile.d.ts +2 -14
- package/lib/core/reconcile.js +87 -17
- package/lib/core/recovery-orchestrator.js +19 -18
- package/lib/core/reinstall.d.ts +4 -0
- package/lib/core/reinstall.js +26 -21
- package/lib/core/restore.js +20 -17
- package/lib/core/rollback-ignore-rules.d.ts +31 -0
- package/lib/core/rollback-ignore-rules.js +140 -0
- package/lib/core/rollback-point.d.ts +1208 -0
- package/lib/core/rollback-point.js +2993 -0
- package/lib/core/run-registry.js +6 -5
- package/lib/core/session-health.d.ts +15 -0
- package/lib/core/session-meta.d.ts +42 -2
- package/lib/core/session-meta.js +140 -1
- package/lib/core/session-title.d.ts +28 -0
- package/lib/core/session-title.js +185 -0
- package/lib/core/startup-barrier.d.ts +24 -0
- package/lib/core/startup-barrier.js +22 -0
- package/lib/core/transaction-coordinator.js +4 -3
- package/lib/core/types.d.ts +18 -1
- package/lib/core/validator.js +5 -4
- package/lib/core/verify-recovery.js +31 -30
- package/lib/foreign/bundle.js +3 -2
- package/lib/foreign/continue.js +2 -1
- package/lib/foreign/crush.js +2 -1
- package/lib/foreign/gemini.js +2 -1
- package/lib/foreign/goose.js +2 -1
- package/lib/foreign/kilocode.js +2 -1
- package/lib/foreign/kimi.js +2 -1
- package/lib/foreign/messages.d.ts +15 -0
- package/lib/foreign/messages.js +40 -0
- package/lib/foreign/mimocode.js +2 -1
- package/lib/foreign/openclaw.js +2 -1
- package/lib/foreign/opencode.js +2 -1
- package/lib/foreign/pi.js +2 -1
- package/lib/foreign/qoder.js +2 -1
- package/lib/foreign/qwen.js +2 -1
- package/lib/foreign/registry.js +7 -6
- package/lib/foreign/teleagent.js +2 -1
- package/lib/foreign/workbuddy.js +2 -1
- package/lib/foreign/zcode.js +2 -1
- package/lib/foreign/zed.js +2 -1
- package/lib/index.d.ts +223 -7
- package/lib/index.js +729 -54
- package/lib/market/git-file-writer.js +20 -19
- package/lib/market/github-repos.js +35 -19
- package/lib/market/index-parser.js +31 -30
- package/lib/market/messages-market.d.ts +138 -0
- package/lib/market/messages-market.js +305 -0
- package/lib/market/my-repo.d.ts +2 -2
- package/lib/market/my-repo.js +32 -29
- package/lib/market/prepare.js +18 -15
- package/lib/market/security.js +20 -19
- package/lib/market/types.js +2 -1
- package/lib/market/url.js +2 -1
- package/lib/migrations/index.js +6 -5
- package/lib/migrations/messages-migrations.d.ts +32 -0
- package/lib/migrations/messages-migrations.js +58 -0
- package/lib/profiles/dsh-profile-launcher.js +2 -1
- package/lib/profiles/dsh-profile-runtime.js +2 -1
- package/lib/profiles/messages.d.ts +7 -0
- package/lib/profiles/messages.js +27 -0
- package/lib/routes/backup.js +18 -2
- package/lib/routes/import.js +3 -3
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +3 -2
- package/lib/routes/kit.js +6 -1
- package/lib/routes/market.js +1 -1
- package/lib/routes/messages.d.ts +42 -0
- package/lib/routes/messages.js +100 -0
- package/lib/routes/profiles.js +12 -6
- package/lib/routes/recovery.js +378 -114
- package/lib/routes/snapshots.js +51 -4
- package/lib/routes/sync.js +68 -16
- package/lib/schema/config.d.ts +11 -1
- package/lib/schema/config.js +52 -20
- package/lib/schema/index.d.ts +8 -6
- package/lib/schema/index.js +8 -6
- package/lib/schema/manifest.js +19 -18
- package/lib/schema/messages.d.ts +47 -0
- package/lib/schema/messages.js +110 -0
- package/lib/schema/section-registry.d.ts +41 -14
- package/lib/schema/section-registry.js +35 -29
- package/lib/schema/versions.js +6 -5
- package/lib/security/encryption.js +16 -15
- package/lib/security/integrity.js +8 -7
- package/lib/security/messages.d.ts +30 -0
- package/lib/security/messages.js +77 -0
- package/lib/security/redaction.js +12 -0
- package/lib/security/secret-scanner.js +16 -13
- package/lib/security/vault.js +2 -1
- package/lib/security/zip-security.js +4 -3
- package/lib/sync/ancestor.js +2 -1
- package/lib/sync/autosync-config.d.ts +11 -0
- package/lib/sync/autosync-config.js +12 -0
- package/lib/sync/autosync-scheduler.d.ts +11 -1
- package/lib/sync/autosync-scheduler.js +115 -76
- package/lib/sync/backup-files.js +3 -2
- package/lib/sync/backup-scheduler.js +6 -5
- package/lib/sync/blob-store.js +2 -1
- package/lib/sync/git/git-transport.d.ts +55 -0
- package/lib/sync/git/git-transport.js +152 -2
- package/lib/sync/github-auth.js +10 -9
- package/lib/sync/layout.js +13 -12
- package/lib/sync/messages.d.ts +63 -0
- package/lib/sync/messages.js +149 -0
- package/lib/sync/review-queue.js +6 -5
- package/lib/sync/s3/messages.d.ts +7 -0
- package/lib/sync/s3/messages.js +16 -0
- package/lib/sync/s3/s3-providers.d.ts +4 -1
- package/lib/sync/s3/s3-providers.js +5 -5
- package/lib/sync/s3/s3-transport.js +1 -1
- package/lib/sync/s3/sigv4.js +2 -1
- package/lib/sync/snapshot-crypto.js +6 -5
- package/lib/sync/snapshot-json.js +3 -2
- package/lib/sync/sync-engine.d.ts +37 -15
- package/lib/sync/sync-engine.js +67 -37
- package/lib/sync/sync-history.js +7 -6
- package/lib/sync/sync-selection.d.ts +15 -4
- package/lib/sync/sync-selection.js +17 -5
- package/lib/sync/webdav/webdav-transport.d.ts +1 -0
- package/lib/sync/webdav/webdav-transport.js +4 -1
- package/lib/ui/artifact-view.js +4 -3
- package/lib/ui/commands.d.ts +1 -1
- package/lib/ui/commands.js +10 -0
- package/lib/ui/disk-usage-view.d.ts +33 -4
- package/lib/ui/disk-usage-view.js +29 -1
- package/lib/ui/errors.d.ts +1 -1
- package/lib/ui/errors.js +4 -3
- package/lib/ui/export-flow.js +24 -20
- package/lib/ui/foreign-view.d.ts +5 -1
- package/lib/ui/foreign-view.js +26 -2
- package/lib/ui/i18n.d.ts +65 -0
- package/lib/ui/i18n.js +114 -0
- package/lib/ui/import-wizard.d.ts +7 -0
- package/lib/ui/import-wizard.js +13 -10
- package/lib/ui/progress.d.ts +2 -2
- package/lib/ui/progress.js +1 -2
- package/lib/ui/report.d.ts +5 -5
- package/lib/ui/report.js +6 -6
- package/lib/ui/rollback-view.d.ts +756 -0
- package/lib/ui/rollback-view.js +724 -0
- package/lib/ui/selection-model.d.ts +41 -0
- package/lib/ui/selection-model.js +30 -0
- package/lib/ui/session-inventory-view.d.ts +97 -3
- package/lib/ui/session-inventory-view.js +76 -2
- package/lib/ui/session-issue-labels.d.ts +43 -0
- package/lib/ui/session-issue-labels.js +48 -0
- package/lib/ui/sync-settings-view.d.ts +33 -0
- package/lib/ui/sync-settings-view.js +40 -0
- package/lib/ui/types.d.ts +8 -3
- package/lib/ui/types.js +1 -1
- package/lib/utils/atomic-write.js +6 -5
- package/lib/utils/bundle-scan.d.ts +11 -1
- package/lib/utils/bundle-scan.js +12 -1
- package/lib/utils/env-lock.d.ts +10 -0
- package/lib/utils/env-lock.js +73 -46
- package/lib/utils/json.js +6 -5
- package/lib/utils/messages-utils.d.ts +96 -0
- package/lib/utils/messages-utils.js +216 -0
- package/lib/utils/proxy.js +18 -5
- package/lib/utils/recursive-walk.d.ts +22 -0
- package/lib/utils/recursive-walk.js +75 -1
- package/lib/utils/session-log-repair.js +10 -9
- package/lib/utils/zip.js +27 -26
- package/package.json +12 -2
- package/src/adapters/agent-instructions.ts +1 -1
- package/src/adapters/file-collection.test.ts +1 -0
- package/src/adapters/file-collection.ts +1 -1
- package/src/adapters/files.test.ts +27 -0
- package/src/adapters/index.ts +1 -1
- package/src/adapters/link-report.test.ts +22 -2
- package/src/adapters/link-report.ts +29 -4
- package/src/adapters/messages.ts +54 -0
- package/src/adapters/plugin-files.ts +3 -2
- package/src/adapters/plugins.ts +4 -3
- package/src/adapters/prompts.ts +7 -1
- package/src/adapters/self.test.ts +26 -0
- package/src/adapters/self.ts +8 -2
- package/src/adapters/sessions.ts +4 -2
- package/src/adapters/settings.test.ts +38 -1
- package/src/adapters/ui.test.ts +2 -2
- package/src/adapters/ui.ts +27 -26
- package/src/adapters/workspaces.ts +7 -3
- package/src/cli/actions.test.ts +28 -0
- package/src/cli/actions.ts +11 -2
- package/src/cli/help.ts +23 -10
- package/src/cli/index-guards.test.ts +193 -0
- package/src/cli/index.ts +106 -31
- package/src/cli/sessions-inspect.test.ts +15 -1
- package/src/cli/sessions-inspect.ts +3 -1
- package/src/cli/sessions-repair.test.ts +84 -0
- package/src/cli/sessions-repair.ts +33 -7
- package/src/cli/web/console-route-parity.test.ts +115 -0
- package/src/cli/web/page.ts +23 -12
- package/src/cli/web/routes.ts +2 -1
- package/src/cli/web/server.ts +4 -4
- package/src/cli/web/web.test.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +30 -2
- package/src/client/about/AboutPanel.tsx +2 -2
- package/src/client/about/PluginUpdateDialog.tsx +1 -1
- package/src/client/about/ReleaseNotesDialog.tsx +2 -2
- package/src/client/about/about-view.test.ts +13 -3
- package/src/client/about/about-view.ts +10 -6
- package/src/client/about/release-notes-view.test.ts +14 -4
- package/src/client/about/release-notes-view.ts +4 -2
- package/src/client/api.ts +18 -6
- package/src/client/chat/ChatEntryView.tsx +391 -0
- package/src/client/chat/RollbackDiffTab.tsx +61 -0
- package/src/client/chat/RollbackFileIcon.tsx +96 -0
- package/src/client/chat/RollbackReviewDialog.tsx +591 -0
- package/src/client/chat/chat-entry-view.test.ts +1174 -0
- package/src/client/chat/chat-entry-view.ts +758 -0
- package/src/client/chat/chat-locales.ts +152 -0
- package/src/client/chat/chat-node-registration.ts +64 -0
- package/src/client/chat/chat-point-window.ts +173 -0
- package/src/client/chat/chat-slots.ts +107 -0
- package/src/client/chat/rollback-diff-tab.test.ts +105 -0
- package/src/client/chat/rollback-diff-tab.ts +164 -0
- package/src/client/chat/rollback-entry-view.test.ts +1469 -0
- package/src/client/chat/rollback-entry-view.ts +941 -0
- package/src/client/chat/sidebar-slots.ts +37 -0
- package/src/client/common/ConfirmDialog.tsx +7 -3
- package/src/client/common/ContentPicker.tsx +48 -10
- package/src/client/common/DiffTable.tsx +97 -0
- package/src/client/common/ErrorBanner.tsx +4 -4
- package/src/client/common/Icon.tsx +2 -0
- package/src/client/common/Modal.tsx +8 -1
- package/src/client/common/ProgressBar.tsx +8 -2
- package/src/client/common/ReportView.tsx +4 -4
- package/src/client/common/RunsCenter.tsx +1 -0
- package/src/client/common/client-route-parity.test.ts +78 -0
- package/src/client/common/info-hint-guard.test.ts +46 -13
- package/src/client/common/picker-lazy-units.test.ts +42 -0
- package/src/client/common/plan-text-redaction.test.ts +17 -6
- package/src/client/common/progress-view-edge.test.ts +8 -7
- package/src/client/common/progress-view.test.ts +9 -9
- package/src/client/common/progress-view.ts +2 -2
- package/src/client/common/routes.ts +28 -0
- package/src/client/common/ui.tsx +73 -10
- package/src/client/config-manager.module.css +996 -21
- package/src/client/css-scale-guard.test.ts +30 -0
- package/src/client/environment/EnvironmentPanel.tsx +11 -4
- package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
- package/src/client/export/ExportView.tsx +13 -8
- package/src/client/history/HistoryPanel.tsx +4 -3
- package/src/client/history/history-api.ts +2 -2
- package/src/client/history/history-locales.ts +5 -1
- package/src/client/home/HomePanel.tsx +29 -8
- package/src/client/import/ForeignImportView.tsx +4 -5
- package/src/client/import/ImportWizardView.tsx +5 -3
- package/src/client/import/import-wizard-split.test.ts +3 -1
- package/src/client/import/import-wizard-steps.tsx +9 -9
- package/src/client/index.ts +186 -3
- package/src/client/library/LibraryActions.tsx +32 -7
- package/src/client/library/LibraryPanel.tsx +23 -3
- package/src/client/library/RestorePlanView.tsx +9 -66
- package/src/client/locales.ts +54 -5
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +13 -2
- package/src/client/market/MyConfigsView.tsx +5 -4
- package/src/client/market/MyConfigsWizard.tsx +10 -8
- package/src/client/market/market-api.test.ts +9 -8
- package/src/client/market/market-api.ts +2 -2
- package/src/client/market/market-locales.ts +3 -0
- package/src/client/market/market-view.test.ts +6 -6
- package/src/client/market/market-view.ts +3 -3
- package/src/client/market/my-configs-api.ts +2 -2
- package/src/client/market/my-configs-view.test.ts +23 -22
- package/src/client/market/my-configs-view.ts +8 -8
- package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
- package/src/client/recovery/RecoveryPanel.tsx +442 -283
- package/src/client/recovery/RollbackCard.tsx +665 -0
- package/src/client/recovery/RollbackDiffPane.tsx +58 -0
- package/src/client/recovery/RollbackFileChanges.tsx +163 -0
- package/src/client/recovery/incident-api.ts +2 -2
- package/src/client/recovery/recovery-api.ts +116 -6
- package/src/client/recovery/recovery-locales.test.ts +299 -0
- package/src/client/recovery/recovery-locales.ts +488 -149
- package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
- package/src/client/recovery/rollback-file-changes.test.ts +73 -0
- package/src/client/recovery/session-dialog-mount.test.ts +54 -0
- package/src/client/run-store.ts +84 -21
- package/src/client/sync/AutosyncCard.tsx +25 -1
- package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
- package/src/client/sync/ChannelConfigDialog.tsx +10 -10
- package/src/client/sync/DecryptPasswordCard.tsx +2 -2
- package/src/client/sync/SecurityOptionsCard.tsx +27 -17
- package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
- package/src/client/sync/SyncConfirmView.tsx +46 -8
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +31 -23
- package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
- package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
- package/src/client/sync/SyncSettingsView.test.ts +20 -1
- package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
- package/src/client/sync/WorkDirCard.tsx +112 -0
- package/src/client/sync/history-model.test.ts +42 -9
- package/src/client/sync/history-model.ts +30 -15
- package/src/client/sync/sync-api.test.ts +57 -33
- package/src/client/sync/sync-api.ts +80 -6
- package/src/client/sync/sync-locales.ts +104 -3
- package/src/client/sync/sync-view-v2.test.ts +65 -13
- package/src/client/sync/sync-view.test.ts +107 -52
- package/src/client/sync/sync-view.ts +124 -41
- package/src/core/backup-plan.test.ts +28 -0
- package/src/core/backup-plan.ts +29 -17
- package/src/core/backup-verify.ts +24 -23
- package/src/core/backup.test.ts +76 -0
- package/src/core/backup.ts +55 -25
- package/src/core/boot-rescue.ts +4 -3
- package/src/core/boot-safety.ts +2 -1
- package/src/core/command-rollback.test.ts +536 -0
- package/src/core/command-rollback.ts +402 -0
- package/src/core/consult-source.ts +7 -6
- package/src/core/disk-usage.test.ts +257 -1
- package/src/core/disk-usage.ts +134 -14
- package/src/core/engine-locale.test.ts +593 -0
- package/src/core/engine-locale.ts +175 -0
- package/src/core/exporter.ts +3 -3
- package/src/core/fork-reconcile.test.ts +279 -0
- package/src/core/fork-reconcile.ts +234 -0
- package/src/core/journal.ts +6 -5
- package/src/core/local-plugin-host.ts +3 -2
- package/src/core/local-plugin-pack.ts +11 -6
- package/src/core/messages-core.ts +657 -0
- package/src/core/messages.ts +123 -17
- package/src/core/migration-consult.ts +21 -20
- package/src/core/migration-history.ts +19 -5
- package/src/core/model-tools.ts +28 -27
- package/src/core/phase3-host.ts +21 -5
- package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
- package/src/core/plugin-cli.ts +18 -19
- package/src/core/reconcile.test.ts +117 -0
- package/src/core/reconcile.ts +85 -17
- package/src/core/recovery-orchestrator.ts +19 -18
- package/src/core/reinstall.ts +26 -21
- package/src/core/restore.ts +20 -17
- package/src/core/rollback-file-diff.test.ts +159 -0
- package/src/core/rollback-ignore-rules.ts +126 -0
- package/src/core/rollback-point.test.ts +2603 -0
- package/src/core/rollback-point.ts +3755 -0
- package/src/core/run-registry.ts +6 -5
- package/src/core/session-health.ts +15 -0
- package/src/core/session-meta.test.ts +117 -1
- package/src/core/session-meta.ts +149 -2
- package/src/core/session-title.test.ts +127 -0
- package/src/core/session-title.ts +189 -0
- package/src/core/startup-barrier.test.ts +69 -0
- package/src/core/startup-barrier.ts +27 -0
- package/src/core/transaction-coordinator.ts +4 -3
- package/src/core/types.ts +18 -1
- package/src/core/validator.ts +5 -4
- package/src/core/verify-recovery.ts +31 -30
- package/src/foreign/bundle.ts +3 -2
- package/src/foreign/continue.ts +2 -1
- package/src/foreign/crush.ts +2 -1
- package/src/foreign/file-budget.test.ts +2 -0
- package/src/foreign/gemini.ts +2 -1
- package/src/foreign/goose.ts +2 -1
- package/src/foreign/kilocode.ts +2 -1
- package/src/foreign/kimi.ts +2 -1
- package/src/foreign/messages.ts +44 -0
- package/src/foreign/mimocode.ts +2 -1
- package/src/foreign/openclaw.ts +2 -1
- package/src/foreign/opencode.ts +2 -1
- package/src/foreign/pi.ts +2 -1
- package/src/foreign/qoder.ts +2 -1
- package/src/foreign/qwen.ts +2 -1
- package/src/foreign/registry.ts +7 -6
- package/src/foreign/teleagent.ts +2 -1
- package/src/foreign/workbuddy.ts +2 -1
- package/src/foreign/zcode.ts +2 -1
- package/src/foreign/zed.ts +2 -1
- package/src/index.facade.test.ts +340 -1
- package/src/index.ts +813 -57
- package/src/market/git-file-writer.ts +20 -19
- package/src/market/github-repos.test.ts +23 -0
- package/src/market/github-repos.ts +37 -19
- package/src/market/index-parser.ts +31 -30
- package/src/market/messages-market.ts +325 -0
- package/src/market/my-repo.ts +32 -29
- package/src/market/prepare.ts +18 -15
- package/src/market/security.ts +20 -19
- package/src/market/types.ts +2 -1
- package/src/market/url.ts +2 -1
- package/src/migrations/index.ts +6 -5
- package/src/migrations/messages-migrations.ts +59 -0
- package/src/profiles/dsh-profile-launcher.ts +2 -1
- package/src/profiles/dsh-profile-runtime.ts +2 -1
- package/src/profiles/messages.ts +31 -0
- package/src/routes/backup.ts +18 -2
- package/src/routes/import.ts +3 -3
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +9 -3
- package/src/routes/market.ts +1 -1
- package/src/routes/messages.ts +104 -0
- package/src/routes/profiles.ts +12 -6
- package/src/routes/recovery.test.ts +405 -1
- package/src/routes/recovery.ts +377 -109
- package/src/routes/snapshots.ts +51 -4
- package/src/routes/sync.ts +67 -16
- package/src/schema/config.ts +53 -20
- package/src/schema/index.ts +7 -5
- package/src/schema/manifest.ts +19 -18
- package/src/schema/messages.ts +120 -0
- package/src/schema/registry.test.ts +19 -5
- package/src/schema/section-registry.ts +64 -39
- package/src/schema/versions.ts +6 -5
- package/src/security/encryption.ts +16 -15
- package/src/security/integrity.ts +8 -7
- package/src/security/messages.ts +87 -0
- package/src/security/redaction.test.ts +17 -1
- package/src/security/redaction.ts +13 -0
- package/src/security/secret-scanner.ts +19 -12
- package/src/security/security.test.ts +47 -0
- package/src/security/vault.ts +2 -1
- package/src/security/zip-security.ts +4 -3
- package/src/sync/ancestor.ts +2 -1
- package/src/sync/autosync-config.ts +21 -0
- package/src/sync/autosync-scheduler.test.ts +63 -7
- package/src/sync/autosync-scheduler.ts +118 -78
- package/src/sync/backup-files.ts +3 -2
- package/src/sync/backup-scheduler.ts +6 -5
- package/src/sync/blob-store.ts +2 -1
- package/src/sync/git/git-transport.test.ts +170 -8
- package/src/sync/git/git-transport.ts +176 -2
- package/src/sync/github-auth.ts +10 -9
- package/src/sync/layout.ts +13 -12
- package/src/sync/messages.ts +153 -0
- package/src/sync/review-queue.ts +6 -5
- package/src/sync/s3/messages.ts +16 -0
- package/src/sync/s3/s3-providers.ts +9 -6
- package/src/sync/s3/s3-transport.ts +1 -1
- package/src/sync/s3/sigv4.ts +3 -1
- package/src/sync/snapshot-crypto.ts +6 -5
- package/src/sync/snapshot-json.ts +3 -2
- package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
- package/src/sync/sync-engine.test.ts +226 -8
- package/src/sync/sync-engine.ts +103 -41
- package/src/sync/sync-history.ts +7 -6
- package/src/sync/sync-selection.test.ts +12 -2
- package/src/sync/sync-selection.ts +18 -5
- package/src/sync/webdav/webdav-transport.ts +4 -1
- package/src/ui/artifact-view.ts +4 -3
- package/src/ui/commands.test.ts +1 -0
- package/src/ui/commands.ts +11 -1
- package/src/ui/disk-usage-view.test.ts +113 -6
- package/src/ui/disk-usage-view.ts +63 -7
- package/src/ui/errors.ts +4 -3
- package/src/ui/export-flow.ts +24 -20
- package/src/ui/foreign-view.test.ts +43 -0
- package/src/ui/foreign-view.ts +30 -3
- package/src/ui/i18n.ts +125 -0
- package/src/ui/import-wizard.ts +18 -10
- package/src/ui/progress.ts +2 -2
- package/src/ui/report.ts +7 -6
- package/src/ui/rollback-view.test.ts +819 -0
- package/src/ui/rollback-view.ts +1339 -0
- package/src/ui/selection-model.test.ts +33 -1
- package/src/ui/selection-model.ts +53 -0
- package/src/ui/session-inventory-view.test.ts +109 -0
- package/src/ui/session-inventory-view.ts +138 -3
- package/src/ui/session-issue-labels.test.ts +51 -0
- package/src/ui/session-issue-labels.ts +70 -0
- package/src/ui/sync-settings-view.test.ts +36 -0
- package/src/ui/sync-settings-view.ts +49 -0
- package/src/ui/types.ts +10 -4
- package/src/utils/atomic-write.ts +6 -5
- package/src/utils/bundle-scan.test.ts +2 -2
- package/src/utils/bundle-scan.ts +12 -1
- package/src/utils/env-lock.ts +83 -49
- package/src/utils/json.ts +7 -5
- package/src/utils/messages-utils.ts +232 -0
- package/src/utils/proxy.test.ts +25 -0
- package/src/utils/proxy.ts +19 -5
- package/src/utils/recursive-walk.test.ts +43 -0
- package/src/utils/recursive-walk.ts +75 -1
- package/src/utils/session-log-repair.ts +10 -9
- package/src/utils/zip.ts +27 -26
- package/lib/core/checkpoint.d.ts +0 -362
- package/lib/core/checkpoint.js +0 -1309
- package/src/core/checkpoint.test.ts +0 -665
- package/src/core/checkpoint.ts +0 -1614
package/src/core/phase3-host.ts
CHANGED
|
@@ -22,11 +22,12 @@ import { inspectStartup, type ReconcileProbeHooks, type ReconcileEnv } from './r
|
|
|
22
22
|
import { OWNERSHIP_FILE, type LockState, type MutationLockContext } from '../utils/env-lock.ts';
|
|
23
23
|
import { sha256Hex } from '../utils/hashing.ts';
|
|
24
24
|
import type { JournalStepRecord } from './types.ts';
|
|
25
|
+
import { engineMsg } from './messages-core.ts';
|
|
25
26
|
|
|
26
27
|
/** 存在未收敛的 active transaction → 拒绝创建第二个(active≤1)。 */
|
|
27
28
|
export class TransactionRecoveryRequiredError extends Error {
|
|
28
29
|
constructor(opId: string) {
|
|
29
|
-
super(
|
|
30
|
+
super(engineMsg('core.phase3.transactionNotConverged', { opId }))
|
|
30
31
|
this.name = 'TransactionRecoveryRequiredError'
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -75,6 +76,9 @@ export interface JournalRunContext {
|
|
|
75
76
|
export function mapLockStateForStartup(state: LockState): 'LOCKED' | 'STALE_LOCK_DETECTED' | 'UNKNOWN_STATE' | 'FREE' {
|
|
76
77
|
if (state === 'STALE_LOCK_DETECTED') return 'STALE_LOCK_DETECTED';
|
|
77
78
|
if (state === 'UNKNOWN_STATE') return 'UNKNOWN_STATE';
|
|
79
|
+
// issue #79:FREE(ownership 确实不存在)如实透传。此前它落到下面的兜底被映射成 LOCKED ——
|
|
80
|
+
// 与 inspectLockState 的 missing→LOCKED 一起,把「没有锁」的系统性误报成「有活跃锁」。
|
|
81
|
+
if (state === 'FREE') return 'FREE';
|
|
78
82
|
return 'LOCKED'; // ACQUIRED / LOCKED / LOCK_IO_ERROR / PERMISSION_ERROR → 保守 LOCKED
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -209,7 +213,19 @@ export class Phase3Recovery {
|
|
|
209
213
|
if (step.beforeFp !== null && fp === step.beforeFp) return 'before-match';
|
|
210
214
|
return 'none';
|
|
211
215
|
},
|
|
212
|
-
|
|
216
|
+
// issue #87:external step 的**真实只读探测**(仅在 journal 里确实记录了指纹时才可能得出结论)。
|
|
217
|
+
// 语义仍是「证不出来就说 unknown」:ref 为空 / 无指纹 / 读失败 / 与两个指纹都不符 → unknown。
|
|
218
|
+
// 注意:runExternalIntent 目前写入的 external step 恒无指纹(beforeFp/afterFp 皆 null),
|
|
219
|
+
// 对它们本探测必然返回 unknown —— 那是刻意的,放行判断在 reconcile 的「可自证残留」门(独立判据)。
|
|
220
|
+
probeExternal: async (step) => {
|
|
221
|
+
if (step.ref === '' || step.beforeFp === null || step.afterFp === null) return 'unknown'
|
|
222
|
+
let data: Buffer
|
|
223
|
+
try { data = await fs.readFile(step.ref) } catch { return 'unknown' }
|
|
224
|
+
const fp = sha256Hex(data)
|
|
225
|
+
if (fp === step.afterFp) return 'installed'
|
|
226
|
+
if (fp === step.beforeFp) return 'not-installed'
|
|
227
|
+
return 'unknown'
|
|
228
|
+
},
|
|
213
229
|
snapshotExists: async (snapshotId, binding) => {
|
|
214
230
|
if (snapshotExists === undefined) return false; // 未注入 → 保守 false
|
|
215
231
|
return snapshotExists(snapshotId, binding);
|
|
@@ -330,7 +346,7 @@ export class Phase3Recovery {
|
|
|
330
346
|
const snapId = await snapshotProvider();
|
|
331
347
|
// P1-2 修复:snapshotProvider 返回 null 表示快照创建失败 → 显式 abort(不得在无快照下继续 mutation)
|
|
332
348
|
if (snapId === null) {
|
|
333
|
-
throw new Error(
|
|
349
|
+
throw new Error(engineMsg('core.phase3.snapshotProviderNull', { operationType }));
|
|
334
350
|
}
|
|
335
351
|
await journalCtx.bindSnapshot(snapId);
|
|
336
352
|
} else if (deferredSnapshot !== true) {
|
|
@@ -446,7 +462,7 @@ export class Phase3Recovery {
|
|
|
446
462
|
environmentFingerprint: this.environmentFingerprint || 'unknown',
|
|
447
463
|
ownerInstanceId,
|
|
448
464
|
bindSnapshot: async (snapshotId: string) => {
|
|
449
|
-
if (snapshotId === null || snapshotId === '') throw new Error('
|
|
465
|
+
if (snapshotId === null || snapshotId === '') throw new Error(engineMsg('core.phase3.bindSnapshotEmptyId'));
|
|
450
466
|
// Reviewer A P2①:持久化失败必须传播(fail-closed)——若 journal 无法 durable 绑定 SNAPSHOT_CREATED,
|
|
451
467
|
// 引擎的 await bindSnapshot 抛错 → fn 抛错 → runJournaled catch(NEEDS_ATTENTION),mutation 绝不在无绑定下继续。
|
|
452
468
|
await this.store.update(opId, (j) => {
|
|
@@ -454,7 +470,7 @@ export class Phase3Recovery {
|
|
|
454
470
|
if (next.state === 'CREATED') next = transitionJournalState(next, 'SNAPSHOT_CREATED');
|
|
455
471
|
else if (next.state === 'SNAPSHOT_CREATED') { /* 幂等:已绑定 */ }
|
|
456
472
|
else if (next.state === 'APPLYING') { /* 允许:绑定已晚但幂等记录 */ }
|
|
457
|
-
else throw new Error(
|
|
473
|
+
else throw new Error(engineMsg('core.phase3.bindSnapshotIllegalState', { state: next.state }));
|
|
458
474
|
return next;
|
|
459
475
|
});
|
|
460
476
|
},
|
|
@@ -68,6 +68,28 @@ test('verifyStepFingerprint:本地文件 step 真实磁盘指纹判定', async
|
|
|
68
68
|
assert.equal(await hooks.verifyStepFingerprint({ adapter: 'plugins', ref: 'plugin:@x', kind: 'Install', external: true, beforeFp: null, afterFp: null, status: 'planned', appliedAt: null }), 'unable');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
test('issue #87 probeExternal:有指纹时做真实只读探测;证不出来一律 unknown(默认保守语义不变)', async (t) => {
|
|
72
|
+
const dir = tmp(t);
|
|
73
|
+
const recovery = await makeRecovery(dir);
|
|
74
|
+
const target = path.join(dir, 'artifact.bin');
|
|
75
|
+
const fpA = sha256Hex(Buffer.from('A'));
|
|
76
|
+
const fpB = sha256Hex(Buffer.from('B'));
|
|
77
|
+
const step = (beforeFp: string | null, afterFp: string | null, ref = target): import('./journal.ts').JournalStep => ({
|
|
78
|
+
adapter: 'plugins', ref, kind: 'Install', external: true, beforeFp, afterFp, status: 'planned', appliedAt: null,
|
|
79
|
+
});
|
|
80
|
+
const hooks = recovery.recoveryHooks;
|
|
81
|
+
|
|
82
|
+
fssync.writeFileSync(target, 'A');
|
|
83
|
+
assert.equal(await hooks.probeExternal(step(fpA, fpB)), 'not-installed', '磁盘仍等于 beforeFp → 可自证未装');
|
|
84
|
+
fssync.writeFileSync(target, 'B');
|
|
85
|
+
assert.equal(await hooks.probeExternal(step(fpA, fpB)), 'installed', '磁盘等于 afterFp → 已装');
|
|
86
|
+
fssync.writeFileSync(target, 'C');
|
|
87
|
+
assert.equal(await hooks.probeExternal(step(fpA, fpB)), 'unknown', '与两个指纹都不符 → unknown(绝不猜)');
|
|
88
|
+
assert.equal(await hooks.probeExternal(step(null, null)), 'unknown', '无指纹(runExternalIntent 的真实形态)→ unknown');
|
|
89
|
+
assert.equal(await hooks.probeExternal(step(fpA, fpB, '')), 'unknown', 'ref 为空 → unknown');
|
|
90
|
+
assert.equal(await hooks.probeExternal(step(fpA, fpB, path.join(dir, 'missing.bin'))), 'unknown', '读失败 → unknown');
|
|
91
|
+
});
|
|
92
|
+
|
|
71
93
|
// ---------- 2. recordStep 持久化 ----------
|
|
72
94
|
|
|
73
95
|
test('runJournaled journalCtx.recordStep:文件/外部 step 落 journal', async (t) => {
|
package/src/core/plugin-cli.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 只依赖 node 内置模块,零 DSH 运行时依赖(M5 的 TDD 可直接单测本模块)。
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { engineMsg } from './messages-core.ts';
|
|
18
19
|
import { spawn } from 'node:child_process'
|
|
19
20
|
import type { ChildProcess, SpawnOptions } from 'node:child_process'
|
|
20
21
|
import { readFileSync, rmSync } from 'node:fs'
|
|
@@ -609,35 +610,35 @@ export function classifyDshPluginFailure(output: string): DshPluginFailure | nul
|
|
|
609
610
|
return {
|
|
610
611
|
code: 'hoist-pattern-diff',
|
|
611
612
|
recoverable: true,
|
|
612
|
-
message: '
|
|
613
|
+
message: engineMsg('core.pluginCli.hoistPatternDiff'),
|
|
613
614
|
}
|
|
614
615
|
}
|
|
615
616
|
if (output.includes('ERR_PNPM_ADDING_TO_ROOT')) {
|
|
616
617
|
return {
|
|
617
618
|
code: 'adding-to-root',
|
|
618
619
|
recoverable: true,
|
|
619
|
-
message: '
|
|
620
|
+
message: engineMsg('core.pluginCli.addingToRoot'),
|
|
620
621
|
}
|
|
621
622
|
}
|
|
622
623
|
if (/--workspace-root may only be used inside a workspace/i.test(output)) {
|
|
623
624
|
return {
|
|
624
625
|
code: 'not-a-workspace',
|
|
625
626
|
recoverable: false,
|
|
626
|
-
message: '
|
|
627
|
+
message: engineMsg('core.pluginCli.notAWorkspace'),
|
|
627
628
|
}
|
|
628
629
|
}
|
|
629
630
|
if (output.includes('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION') || output.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')) {
|
|
630
631
|
return {
|
|
631
632
|
code: 'release-age-violation',
|
|
632
633
|
recoverable: true,
|
|
633
|
-
message: '
|
|
634
|
+
message: engineMsg('core.pluginCli.releaseAgeViolation'),
|
|
634
635
|
}
|
|
635
636
|
}
|
|
636
637
|
if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED') || /git-hosted package "[^"]+" needs to execute build scripts/i.test(output)) {
|
|
637
638
|
return {
|
|
638
639
|
code: 'git-build-blocked',
|
|
639
640
|
recoverable: false,
|
|
640
|
-
message: '
|
|
641
|
+
message: engineMsg('core.pluginCli.gitBuildBlocked'),
|
|
641
642
|
}
|
|
642
643
|
}
|
|
643
644
|
// issue #35:pnpm-workspace.yaml 的 patchedDependencies 指向不存在的 patch 文件时,
|
|
@@ -647,29 +648,27 @@ export function classifyDshPluginFailure(output: string): DshPluginFailure | nul
|
|
|
647
648
|
// 路径可能含盘符冒号(C:\...),故按整行取、再切掉尾随的 ':' 或 ': <错误文本>'
|
|
648
649
|
const raw = /Failed to read patch file\s+([^\r\n]+)/i.exec(output)?.[1]?.trim()
|
|
649
650
|
const file = raw === undefined ? undefined : raw.split(/:\s/)[0]?.replace(/:\s*$/, '').trim()
|
|
650
|
-
const
|
|
651
|
-
const en = file === undefined ? '' : ` (${file})`
|
|
651
|
+
const patchSuffix = file === undefined ? '' : engineMsg('core.pluginCli.parenValue', { value: file })
|
|
652
652
|
return {
|
|
653
653
|
code: 'patch-file-missing',
|
|
654
654
|
recoverable: false,
|
|
655
|
-
message:
|
|
655
|
+
message: engineMsg('core.pluginCli.patchFileMissing', { patch: patchSuffix }),
|
|
656
656
|
}
|
|
657
657
|
}
|
|
658
658
|
if (output.includes('ERR_PNPM_FETCH_404')) {
|
|
659
659
|
const pkg = /GET\s+\S*\/([^/\s]+):/.exec(output)?.[1]?.replace(/%2[Ff]/g, '/')
|
|
660
|
-
const
|
|
661
|
-
const en = pkg === undefined ? '' : ` (${pkg})`
|
|
660
|
+
const pkgSuffix = pkg === undefined ? '' : engineMsg('core.pluginCli.parenValue', { value: pkg })
|
|
662
661
|
return {
|
|
663
662
|
code: 'fetch-404',
|
|
664
663
|
recoverable: false,
|
|
665
|
-
message:
|
|
664
|
+
message: engineMsg('core.pluginCli.fetch404', { pkg: pkgSuffix }),
|
|
666
665
|
}
|
|
667
666
|
}
|
|
668
667
|
if (isTransientDshPluginFailure(output)) {
|
|
669
668
|
return {
|
|
670
669
|
code: 'transient-network',
|
|
671
670
|
recoverable: true,
|
|
672
|
-
message: '
|
|
671
|
+
message: engineMsg('core.pluginCli.transientNetwork'),
|
|
673
672
|
}
|
|
674
673
|
}
|
|
675
674
|
// 桌面端保留档案:普通 dsh CLI(< 0.2 的旧版尤其)对 `--profile desktop` 直接报
|
|
@@ -680,14 +679,14 @@ export function classifyDshPluginFailure(output: string): DshPluginFailure | nul
|
|
|
680
679
|
return {
|
|
681
680
|
code: 'desktop-profile-reserved',
|
|
682
681
|
recoverable: false,
|
|
683
|
-
message: '
|
|
682
|
+
message: engineMsg('core.pluginCli.desktopProfileReserved'),
|
|
684
683
|
}
|
|
685
684
|
}
|
|
686
685
|
if (output.includes('pnpm not found on PATH')) {
|
|
687
686
|
return {
|
|
688
687
|
code: 'pnpm-missing',
|
|
689
688
|
recoverable: false,
|
|
690
|
-
message: '
|
|
689
|
+
message: engineMsg('core.pluginCli.pnpmMissing'),
|
|
691
690
|
}
|
|
692
691
|
}
|
|
693
692
|
return null
|
|
@@ -696,18 +695,18 @@ export function classifyDshPluginFailure(output: string): DshPluginFailure | nul
|
|
|
696
695
|
/** 安装失败 → 分类后的双语可读 Error;未识别则保留 stderr 尾部摘要。 */
|
|
697
696
|
export function installErrorFor(pkg: string, result: DshPluginResult): Error {
|
|
698
697
|
if (result.spawnError !== undefined && /ENOENT|EINVAL/.test(result.spawnError)) {
|
|
699
|
-
return new Error(
|
|
698
|
+
return new Error(engineMsg('core.pluginCli.dshCliStartFailed', { spawnError: result.spawnError, pkg }))
|
|
700
699
|
}
|
|
701
700
|
if (result.timedOut) {
|
|
702
|
-
return new Error(
|
|
701
|
+
return new Error(engineMsg('core.pluginCli.installTimeout', { pkg }))
|
|
703
702
|
}
|
|
704
703
|
const output = `${result.stderr}\n${result.stdout}`
|
|
705
704
|
const failure = classifyDshPluginFailure(output)
|
|
706
705
|
if (failure !== null) {
|
|
707
|
-
return new Error(
|
|
706
|
+
return new Error(engineMsg('core.pluginCli.installFailed', { pkg, code: failure.code, message: failure.message }))
|
|
708
707
|
}
|
|
709
708
|
const tail = (result.stderr.trim().split('\n').slice(-8).join('\n')
|
|
710
709
|
|| result.stdout.trim().split('\n').slice(-8).join('\n')
|
|
711
|
-
|| '
|
|
712
|
-
return new Error(
|
|
710
|
+
|| engineMsg('core.pluginCli.noOutput'))
|
|
711
|
+
return new Error(engineMsg('core.pluginCli.installFailedWithTail', { pkg, exitCode: String(result.exitCode), tail }))
|
|
713
712
|
}
|
|
@@ -452,3 +452,120 @@ test('t37:Phase3Recovery.refreshSafeMode 消费三态(unknown → isBlocked
|
|
|
452
452
|
assert.equal(recovery.lastSafeModeState, 'clear', '标记已清除 → 刷新后仍为 clear');
|
|
453
453
|
});
|
|
454
454
|
|
|
455
|
+
// ---------- issue #87:可自证的崩溃残留自动放行 ----------
|
|
456
|
+
|
|
457
|
+
function backupOp(id: string, state: OperationJournal['state'] = 'APPLYING'): OperationJournal {
|
|
458
|
+
const j = createJournalEntry('backup-schedule', { operationId: id, ownerInstanceId: 'o1', lockId: 'l1', packageVersion: '0.1.71', environmentFingerprint: FP }, '2026-01-01T00:00:00.000Z');
|
|
459
|
+
j.state = state;
|
|
460
|
+
return j;
|
|
461
|
+
}
|
|
462
|
+
function backupExtStep(status: JournalStep['status'] = 'planned'): JournalStep {
|
|
463
|
+
return { adapter: 'backup', ref: 'exports', kind: 'Backup', external: true, beforeFp: null, afterFp: null, status, appliedAt: null };
|
|
464
|
+
}
|
|
465
|
+
const unknownProbe: Partial<ReconcileProbeHooks> = { probeExternal: async () => 'unknown' };
|
|
466
|
+
|
|
467
|
+
test('issue #87:定时备份残留(planned external backup step + 无快照)→ noop 自动放行,不再永久占 SAFE MODE', async (t) => {
|
|
468
|
+
const dir = tmp(t);
|
|
469
|
+
const store = mkStore(dir);
|
|
470
|
+
const id = '00000000-0000-4000-8000-0000000000d1';
|
|
471
|
+
const j = backupOp(id);
|
|
472
|
+
j.plannedSteps = ['ext'];
|
|
473
|
+
j.steps = { ext: backupExtStep() };
|
|
474
|
+
await store.create(j);
|
|
475
|
+
// 生产形态:external step 无指纹 ⇒ probe 恒 unknown(修复前正是这一条把它顶成 needs-attention)
|
|
476
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, env);
|
|
477
|
+
assert.equal(out.decisions[0]!.kind, 'noop');
|
|
478
|
+
assert.equal(out.safeModeRequired, false);
|
|
479
|
+
assert.equal(await store.readSafeMode(), false, '不得写 durable SAFE MODE');
|
|
480
|
+
assert.equal((await store.load(id))?.state, 'RECOVERED');
|
|
481
|
+
assert.deepEqual(await store.scanActive(), [], '必须规整出 active');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('issue #87:定时备份残留(一个 step 都没登记)→ noop(该通道先落 step 再调 fn,无 step ⇒ fn 未开始)', async (t) => {
|
|
485
|
+
const dir = tmp(t);
|
|
486
|
+
const store = mkStore(dir);
|
|
487
|
+
const id = '00000000-0000-4000-8000-0000000000d2';
|
|
488
|
+
await store.create(backupOp(id));
|
|
489
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, env);
|
|
490
|
+
assert.equal(out.decisions[0]!.kind, 'noop');
|
|
491
|
+
assert.equal(out.safeModeRequired, false);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test('issue #87 对照:同样 external 形状但 operationType 会改用户配置(import-apply)→ 仍 NEEDS_ATTENTION', async (t) => {
|
|
495
|
+
const dir = tmp(t);
|
|
496
|
+
const store = mkStore(dir);
|
|
497
|
+
const id = '00000000-0000-4000-8000-0000000000d3';
|
|
498
|
+
const j = op(id, 'APPLYING');
|
|
499
|
+
j.plannedSteps = ['ext'];
|
|
500
|
+
j.steps = { ext: backupExtStep() };
|
|
501
|
+
await store.create(j);
|
|
502
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, env);
|
|
503
|
+
assert.equal(out.decisions[0]!.kind, 'needs-attention', '只放行 artifact-only 通道,绝不按形状普放');
|
|
504
|
+
assert.equal(out.safeModeRequired, true);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('issue #87 对照:定时备份但 step 已 applied/非 planned → 保持保守', async (t) => {
|
|
508
|
+
const dir = tmp(t);
|
|
509
|
+
const store = mkStore(dir);
|
|
510
|
+
const id = '00000000-0000-4000-8000-0000000000d4';
|
|
511
|
+
const j = backupOp(id);
|
|
512
|
+
j.plannedSteps = ['ext'];
|
|
513
|
+
j.steps = { ext: { ...backupExtStep('done'), appliedAt: '2026-01-01T00:00:01.000Z' } };
|
|
514
|
+
await store.create(j);
|
|
515
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, env);
|
|
516
|
+
assert.equal(out.decisions[0]!.kind, 'needs-attention');
|
|
517
|
+
assert.equal(out.safeModeRequired, true);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test('issue #87 对照:定时备份但 step 不是 backup 适配器(可能装插件)→ 保持保守', async (t) => {
|
|
521
|
+
const dir = tmp(t);
|
|
522
|
+
const store = mkStore(dir);
|
|
523
|
+
const id = '00000000-0000-4000-8000-0000000000d5';
|
|
524
|
+
const j = backupOp(id);
|
|
525
|
+
j.plannedSteps = ['p1'];
|
|
526
|
+
j.steps = { p1: extStep('planned') };
|
|
527
|
+
await store.create(j);
|
|
528
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, env);
|
|
529
|
+
assert.equal(out.decisions[0]!.kind, 'needs-attention');
|
|
530
|
+
assert.equal(out.safeModeRequired, true);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('issue #87 对照:定时备份但已有 snapshotId(可回滚)→ 走原 rollback 路径,不判 noop', async (t) => {
|
|
534
|
+
const dir = tmp(t);
|
|
535
|
+
const store = mkStore(dir);
|
|
536
|
+
const id = '00000000-0000-4000-8000-0000000000d6';
|
|
537
|
+
const j = backupOp(id);
|
|
538
|
+
j.snapshotId = '00000000-0000-4000-8000-0000000000aa';
|
|
539
|
+
j.plannedSteps = ['ext'];
|
|
540
|
+
j.steps = { ext: backupExtStep() };
|
|
541
|
+
await store.create(j);
|
|
542
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, env);
|
|
543
|
+
assert.equal(out.decisions[0]!.kind, 'needs-attention');
|
|
544
|
+
assert.equal(out.safeModeRequired, true);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('issue #87 对照:ownership binding 不匹配 → 早于自证门的信任门先拦(不越权接管)', async (t) => {
|
|
548
|
+
const dir = tmp(t);
|
|
549
|
+
const store = mkStore(dir);
|
|
550
|
+
const id = '00000000-0000-4000-8000-0000000000d7';
|
|
551
|
+
const j = backupOp(id);
|
|
552
|
+
j.plannedSteps = ['ext'];
|
|
553
|
+
j.steps = { ext: backupExtStep() };
|
|
554
|
+
await store.create(j);
|
|
555
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, { ...env, expectedOwnershipInstanceId: 'someone-else' });
|
|
556
|
+
assert.equal(out.decisions[0]!.kind, 'needs-attention');
|
|
557
|
+
assert.match(out.decisions[0]!.reason, /binding|ownerInstanceId/i);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('issue #87 对照:零步骤的 sync-push(非 artifact-only 通道)→ 保持 opaque NEEDS_ATTENTION', async (t) => {
|
|
561
|
+
const dir = tmp(t);
|
|
562
|
+
const store = mkStore(dir);
|
|
563
|
+
const id = '00000000-0000-4000-8000-0000000000d8';
|
|
564
|
+
const j = createJournalEntry('sync-push', { operationId: id, ownerInstanceId: 'o1', lockId: 'l1', packageVersion: '0.1.71', environmentFingerprint: FP }, '2026-01-01T00:00:00.000Z');
|
|
565
|
+
j.state = 'APPLYING';
|
|
566
|
+
await store.create(j);
|
|
567
|
+
const out = await reconcileActive(store, { ...hooks(), ...unknownProbe }, env);
|
|
568
|
+
assert.equal(out.decisions[0]!.kind, 'needs-attention');
|
|
569
|
+
assert.equal(out.safeModeRequired, true);
|
|
570
|
+
});
|
|
571
|
+
|
package/src/core/reconcile.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - 若发现 stale lock + incomplete journal → RECOVERY_REQUIRED / SAFE MODE,不自动 recoverStaleLock;
|
|
13
13
|
* - 用户显式确认后才 prove stale → recover → acquire → reconcile。
|
|
14
14
|
*/
|
|
15
|
+
import { engineMsg } from './messages-core.ts';
|
|
15
16
|
import {
|
|
16
17
|
JournalStore, isTerminalState, transitionJournalState,
|
|
17
18
|
type OperationJournal, type JournalStep, type SafeModeMarkerState,
|
|
@@ -90,6 +91,55 @@ export interface ReconcileOutcome {
|
|
|
90
91
|
unresolved: string[];
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* **唯一**被放行的 operationType(issue #87):定时备份。
|
|
96
|
+
*
|
|
97
|
+
* 为什么只放行它:它整条链路**不可能改用户配置** —— backup-scheduler 的 `doExport` 只做三件事:
|
|
98
|
+
* 往 `<dataDir>/exports/` 写一个归档 zip、写插件自身的调度状态文件、按保留策略删旧 auto 归档
|
|
99
|
+
* (src/sync/backup-scheduler.ts)。因此「崩溃前到底跑没跑到」这个不可判定问题,对**用户配置是否半成品**
|
|
100
|
+
* 完全无关紧要 —— 那才是 SAFE MODE 要防的东西。
|
|
101
|
+
*
|
|
102
|
+
* **其它 operationType 一律不得套用**:`autosync-apply` / `import-apply` 之类会真的改用户配置,
|
|
103
|
+
* 它们「已开始但未登记 step」永远不可证明,必须留在原保守路径(NEEDS_ATTENTION + SAFE MODE)。
|
|
104
|
+
*/
|
|
105
|
+
const ARTIFACT_ONLY_OPERATION_TYPES: ReadonlySet<string> = new Set<string>(['backup-schedule'])
|
|
106
|
+
|
|
107
|
+
/** 可自证「不可能掩盖半成品配置改动」的残留类型(issue #87)。 */
|
|
108
|
+
export type SelfProvableResidualClass = 'artifact-only' | 'no-steps'
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 判定崩溃残留是否**可自证**「没发生」(issue #87);不可自证 → null,走原保守路径。
|
|
112
|
+
*
|
|
113
|
+
* 只回答一个问题:这次残留有没有可能掩盖「半成品用户配置改动」?只放行 artifact-only 通道的两类形状 ——
|
|
114
|
+
* - `no-steps`:一个 step 都没登记。该通道(core/phase3-host.ts runExternalIntent)在调用 `fn` **之前**
|
|
115
|
+
* 就把 external step 落进 journal,没有 step ⇒ `fn` 从未开始 ⇒ 连归档都没产出;
|
|
116
|
+
* - `artifact-only`:有 step 且全是 external backup(形状与 runExternalIntent 写的 ext step 逐字一致)。
|
|
117
|
+
*
|
|
118
|
+
* 形状门缺一不可:state ∈ {APPLYING, NEEDS_ATTENTION}、无 snapshotId、`commit.at` 为空、
|
|
119
|
+
* rollback 未尝试且无已补偿条目、recovery 未尝试、所有 step 都是 `planned` 且 `appliedAt` 为空。
|
|
120
|
+
* ownership / environmentFingerprint 绑定校验由调用方在**更早**的信任门完成,这里不重复也不放松。
|
|
121
|
+
*/
|
|
122
|
+
function selfProvableResidual(j: OperationJournal): { cls: SelfProvableResidualClass; reason: string } | null {
|
|
123
|
+
if (!ARTIFACT_ONLY_OPERATION_TYPES.has(j.operationType)) return null
|
|
124
|
+
if (j.state !== 'APPLYING' && j.state !== 'NEEDS_ATTENTION') return null
|
|
125
|
+
if (j.snapshotId !== null && j.snapshotId !== '') return null
|
|
126
|
+
if (j.commit.at !== null) return null
|
|
127
|
+
if (j.rollback.attemptedAt !== null) return null
|
|
128
|
+
if (Object.keys(j.rollback.entryDone ?? {}).length > 0) return null
|
|
129
|
+
if (j.recovery.attemptedAt !== null) return null
|
|
130
|
+
const steps = Object.values(j.steps)
|
|
131
|
+
if (steps.some((s) => s.status !== 'planned' || s.appliedAt !== null)) return null
|
|
132
|
+
|
|
133
|
+
if (steps.length === 0) {
|
|
134
|
+
if (j.plannedSteps.length > 0) return null // 悬空 plannedSteps:损坏/篡改 → 保守
|
|
135
|
+
return { cls: 'no-steps', reason: engineMsg('core.reconcile.selfProvenNoSteps') }
|
|
136
|
+
}
|
|
137
|
+
if (steps.every((s) => s.external === true && s.adapter === 'backup')) {
|
|
138
|
+
return { cls: 'artifact-only', reason: engineMsg('core.reconcile.selfProvenArtifactOnly') }
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
93
143
|
// ---------- Reconcile ----------
|
|
94
144
|
|
|
95
145
|
/**
|
|
@@ -126,7 +176,7 @@ async function reconcileOne(
|
|
|
126
176
|
const j = await store.loadActive(operationId);
|
|
127
177
|
if (j === null) {
|
|
128
178
|
// 存在但 parse/校验失败 → corrupt → quarantine
|
|
129
|
-
const reason =
|
|
179
|
+
const reason = engineMsg('core.reconcile.journalUnparsable', { opId: operationId });
|
|
130
180
|
if (opts.autoQuarantine !== false) {
|
|
131
181
|
try {
|
|
132
182
|
await store.quarantine(operationId, reason);
|
|
@@ -138,7 +188,7 @@ async function reconcileOne(
|
|
|
138
188
|
|
|
139
189
|
// live owner 检查
|
|
140
190
|
if (await env.isLiveOwner(j)) {
|
|
141
|
-
return { decision: { operationId, kind: 'live', reason: '
|
|
191
|
+
return { decision: { operationId, kind: 'live', reason: engineMsg('core.reconcile.ownerLive'), snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
142
192
|
}
|
|
143
193
|
|
|
144
194
|
// P1-A:ownership binding 强制校验(显式 recovery 捕获的 crashed ownership 证据)。
|
|
@@ -147,7 +197,7 @@ async function reconcileOne(
|
|
|
147
197
|
const bindingOk = j.ownerInstanceId === env.expectedOwnershipInstanceId
|
|
148
198
|
&& j.lockId === env.expectedOwnershipInstanceId; // lockId = 同 epoch identity(ownerInstanceId)
|
|
149
199
|
if (!bindingOk) {
|
|
150
|
-
const reason =
|
|
200
|
+
const reason = engineMsg('core.reconcile.bindingMismatch', { instanceId: env.expectedOwnershipInstanceId });
|
|
151
201
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
152
202
|
await store.appendRecoveryHistory('binding-mismatch', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
153
203
|
return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
@@ -156,29 +206,46 @@ async function reconcileOne(
|
|
|
156
206
|
|
|
157
207
|
// env fingerprint
|
|
158
208
|
if (env.environmentFingerprint && j.environmentFingerprint && env.environmentFingerprint !== j.environmentFingerprint) {
|
|
159
|
-
const reason =
|
|
209
|
+
const reason = engineMsg('core.reconcile.envMismatch');
|
|
160
210
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
161
211
|
return { decision: { operationId, kind: 'env-mismatch', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
162
212
|
}
|
|
163
213
|
|
|
214
|
+
// issue #87:自证「没发生」的残留自动放行(APPLYING/NEEDS_ATTENTION → RECOVERED,history = noop)。
|
|
215
|
+
// 位置刻意放在 live-owner / ownership-binding / env-fingerprint 三道信任门**之后**、terminal 与步态判定
|
|
216
|
+
// **之前**:只有已通过全部绑定校验、且形状证明副作用不可能已开始的残留才会走到这里;其余一律落回原保守路径。
|
|
217
|
+
const residual = selfProvableResidual(j)
|
|
218
|
+
if (residual !== null) {
|
|
219
|
+
const reason = residual.reason
|
|
220
|
+
await store.update(operationId, (cur) => {
|
|
221
|
+
let next = transitionJournalState(cur, 'RECOVERED')
|
|
222
|
+
next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason, attempts: next.recovery.attempts + 1 } }
|
|
223
|
+
return next
|
|
224
|
+
})
|
|
225
|
+
await store.moveToCompleted(operationId).catch(() => undefined)
|
|
226
|
+
await store.appendRecoveryHistory('noop', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined)
|
|
227
|
+
return { decision: { operationId, kind: 'noop', reason, snapshotId: j.snapshotId }, safeMode: false, unresolved: false }
|
|
228
|
+
}
|
|
229
|
+
|
|
164
230
|
// terminal 遗留 active → 规整到 completed(无实质 recovery)
|
|
165
231
|
if (isTerminalState(j.state)) {
|
|
166
232
|
if (j.state === 'NEEDS_ATTENTION') {
|
|
167
233
|
// NEEDS_ATTENTION 是「待用户确认」状态,保持 active 可见(不静默规整),SAFE MODE
|
|
168
234
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
169
|
-
return { decision: { operationId, kind: 'needs-attention', reason: '
|
|
235
|
+
return { decision: { operationId, kind: 'needs-attention', reason: engineMsg('core.reconcile.needsAttentionConfirm'), snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
170
236
|
}
|
|
171
237
|
// COMMITTED / ROLLED_BACK / RECOVERED 遗留 → 规整
|
|
172
238
|
if (opts.organizeTerminal !== false) {
|
|
173
239
|
try { await store.moveToCompleted(operationId); } catch { /* move 失败不影响 */ }
|
|
174
240
|
}
|
|
175
|
-
return { decision: { operationId, kind: 'organized', reason:
|
|
241
|
+
return { decision: { operationId, kind: 'organized', reason: engineMsg('core.reconcile.terminalOrganized', { state: j.state }), snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
176
242
|
}
|
|
177
243
|
|
|
178
244
|
// 回滚进行中(ROLLING_BACK)或已部分补偿(entryDone 非空):**绝不判 RECOVERED**
|
|
179
245
|
// (P0-B:崩溃在回滚中 = 半回滚态,判 recovered 会把破坏状态误报为成功)。
|
|
180
246
|
if (j.state === 'ROLLING_BACK' || Object.keys(j.rollback.entryDone ?? {}).length > 0) {
|
|
181
|
-
const
|
|
247
|
+
const rollbackDetail = j.state === 'ROLLING_BACK' ? 'ROLLING_BACK' : `entryDone ${Object.keys(j.rollback.entryDone).join(',')}`;
|
|
248
|
+
const reason = engineMsg('core.reconcile.rollbackInterrupted', { detail: rollbackDetail });
|
|
182
249
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
183
250
|
await store.appendRecoveryHistory('rollback-interrupted', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
184
251
|
return { decision: { operationId, kind: 'rollback-continue', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
@@ -194,7 +261,7 @@ async function reconcileOne(
|
|
|
194
261
|
const v = j.recoveryVerification;
|
|
195
262
|
const verified = v !== undefined && (v.verdict === 'MATCH' || v.verdict === 'PARTIAL_MATCH');
|
|
196
263
|
if (!verified) {
|
|
197
|
-
const reason = '
|
|
264
|
+
const reason = engineMsg('core.reconcile.recoveringUnverified');
|
|
198
265
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
199
266
|
await store.update(operationId, (cur) => {
|
|
200
267
|
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
@@ -221,7 +288,7 @@ async function reconcileOne(
|
|
|
221
288
|
// Review D P2:plannedSteps 引用悬空(steps 缺条目)→ 不可证明,绝不能经 noop 判 RECOVERED。
|
|
222
289
|
anyUnprovable = true;
|
|
223
290
|
allDoneConfirmed = false;
|
|
224
|
-
details.push(
|
|
291
|
+
details.push(engineMsg('core.reconcile.danglingPlannedStep', { stepId }));
|
|
225
292
|
continue;
|
|
226
293
|
}
|
|
227
294
|
|
|
@@ -244,9 +311,9 @@ async function reconcileOne(
|
|
|
244
311
|
let verdict: StepFingerprintVerdict;
|
|
245
312
|
try { verdict = await hooks.verifyStepFingerprint(step); } catch { verdict = 'unable'; }
|
|
246
313
|
if (verdict === 'after-match') { anyApplied = true; continue; }
|
|
247
|
-
if (verdict === 'before-match') { anyApplied = false; details.push(
|
|
314
|
+
if (verdict === 'before-match') { anyApplied = false; details.push(engineMsg('core.reconcile.stepNotReliablyDone', { ref: step.ref })); allDoneConfirmed = false; anyUnprovable = true; continue; }
|
|
248
315
|
anyUnprovable = true; allDoneConfirmed = false;
|
|
249
|
-
details.push(
|
|
316
|
+
details.push(engineMsg('core.reconcile.stepFingerprint', { ref: step.ref, verdict }));
|
|
250
317
|
} else {
|
|
251
318
|
// planned/attention/failed → 未完成
|
|
252
319
|
allDoneConfirmed = false;
|
|
@@ -279,13 +346,13 @@ async function reconcileOne(
|
|
|
279
346
|
});
|
|
280
347
|
await store.moveToCompleted(operationId).catch(() => undefined);
|
|
281
348
|
await store.appendRecoveryHistory('noop', { operationId, at: new Date().toISOString(), reason: 'no mutation started' }).catch(() => undefined);
|
|
282
|
-
return { decision: { operationId, kind: 'noop', reason: '
|
|
349
|
+
return { decision: { operationId, kind: 'noop', reason: engineMsg('core.reconcile.mutationNotStarted'), snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
283
350
|
}
|
|
284
351
|
// APPLYING(或其它非 terminal):mutation 可能已开始,无法证明
|
|
285
352
|
if (j.snapshotId !== null && snapshotExists) {
|
|
286
353
|
// 有 trusted bound snapshot → rollback-recommended(需用户确认)
|
|
287
354
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
288
|
-
const reason = '
|
|
355
|
+
const reason = engineMsg('core.reconcile.opaqueRollbackAvailable');
|
|
289
356
|
await store.update(operationId, (cur) => {
|
|
290
357
|
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
291
358
|
next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
|
|
@@ -296,7 +363,7 @@ async function reconcileOne(
|
|
|
296
363
|
}
|
|
297
364
|
// 无 trusted snapshot → NEEDS_ATTENTION(SAFE MODE)
|
|
298
365
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
299
|
-
const reason = '
|
|
366
|
+
const reason = engineMsg('core.reconcile.opaqueNoSnapshot');
|
|
300
367
|
await store.update(operationId, (cur) => {
|
|
301
368
|
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
302
369
|
next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
|
|
@@ -328,13 +395,14 @@ async function reconcileOne(
|
|
|
328
395
|
});
|
|
329
396
|
await store.moveToCompleted(operationId).catch(() => undefined);
|
|
330
397
|
await store.appendRecoveryHistory('noop', { operationId, at: new Date().toISOString(), reason: 'no step applied' }).catch(() => undefined);
|
|
331
|
-
return { decision: { operationId, kind: 'noop', reason: '
|
|
398
|
+
return { decision: { operationId, kind: 'noop', reason: engineMsg('core.reconcile.noSideEffects'), snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
332
399
|
}
|
|
333
400
|
|
|
334
401
|
if (anyUnprovable || anyExternalUnknown || !snapshotExists) {
|
|
335
402
|
// 不可证明 / 外部未知 / 快照缺失 → NEEDS_ATTENTION(SAFE MODE)
|
|
336
403
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
337
|
-
const reason =
|
|
404
|
+
const reason = engineMsg('core.reconcile.unprovableSteps', { details: details.join('; ') || engineMsg('core.reconcile.externalSideEffectsUnknown') })
|
|
405
|
+
+ (snapshotExists ? '' : engineMsg('core.reconcile.snapshotMissingSuffix'));
|
|
338
406
|
await store.update(operationId, (cur) => {
|
|
339
407
|
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
340
408
|
next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
|
|
@@ -346,7 +414,7 @@ async function reconcileOne(
|
|
|
346
414
|
|
|
347
415
|
// 快照有效 + 已应用部分 + 无不可证明 → rollback 可安全执行(需用户确认)
|
|
348
416
|
await store.writeSafeMode(true).catch(() => undefined);
|
|
349
|
-
const reason = '
|
|
417
|
+
const reason = engineMsg('core.reconcile.partialApplied');
|
|
350
418
|
return { decision: { operationId, kind: 'rollback-recommended', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
351
419
|
}
|
|
352
420
|
|