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.
Files changed (548) hide show
  1. package/README.md +111 -96
  2. package/README.zh-CN.md +207 -86
  3. package/lib/adapters/agent-instructions.js +1 -1
  4. package/lib/adapters/file-collection.js +1 -1
  5. package/lib/adapters/index.d.ts +1 -1
  6. package/lib/adapters/index.js +1 -1
  7. package/lib/adapters/link-report.d.ts +1 -0
  8. package/lib/adapters/link-report.js +27 -3
  9. package/lib/adapters/messages.d.ts +18 -0
  10. package/lib/adapters/messages.js +50 -0
  11. package/lib/adapters/plugin-files.js +3 -2
  12. package/lib/adapters/plugins.js +4 -3
  13. package/lib/adapters/prompts.d.ts +1 -0
  14. package/lib/adapters/prompts.js +7 -1
  15. package/lib/adapters/self.d.ts +7 -1
  16. package/lib/adapters/self.js +1 -1
  17. package/lib/adapters/sessions.d.ts +1 -0
  18. package/lib/adapters/sessions.js +4 -2
  19. package/lib/adapters/ui.d.ts +2 -2
  20. package/lib/adapters/ui.js +25 -25
  21. package/lib/adapters/workspaces.d.ts +1 -0
  22. package/lib/adapters/workspaces.js +7 -3
  23. package/lib/cli/actions.d.ts +9 -1
  24. package/lib/cli/actions.js +5 -2
  25. package/lib/cli/help.js +23 -10
  26. package/lib/cli/index.d.ts +23 -0
  27. package/lib/cli/index.js +90 -32
  28. package/lib/cli/sessions-inspect.js +3 -1
  29. package/lib/cli/sessions-repair.js +25 -6
  30. package/lib/cli/web/page.d.ts +12 -0
  31. package/lib/cli/web/page.js +21 -12
  32. package/lib/cli/web/routes.js +2 -2
  33. package/lib/cli/web/server.js +4 -4
  34. package/lib/client.d.ts +1349 -490
  35. package/lib/client.js +43047 -36738
  36. package/lib/core/backup-plan.js +30 -17
  37. package/lib/core/backup-verify.js +24 -23
  38. package/lib/core/backup.d.ts +5 -0
  39. package/lib/core/backup.js +51 -25
  40. package/lib/core/boot-rescue.js +4 -3
  41. package/lib/core/boot-safety.js +2 -1
  42. package/lib/core/command-rollback.d.ts +178 -0
  43. package/lib/core/command-rollback.js +289 -0
  44. package/lib/core/consult-source.js +7 -6
  45. package/lib/core/disk-usage.d.ts +74 -10
  46. package/lib/core/disk-usage.js +67 -4
  47. package/lib/core/engine-locale.d.ts +87 -0
  48. package/lib/core/engine-locale.js +125 -0
  49. package/lib/core/exporter.js +3 -3
  50. package/lib/core/fork-reconcile.d.ts +128 -0
  51. package/lib/core/fork-reconcile.js +150 -0
  52. package/lib/core/journal.js +6 -5
  53. package/lib/core/local-plugin-host.js +3 -2
  54. package/lib/core/local-plugin-pack.js +11 -6
  55. package/lib/core/messages-core.d.ts +292 -0
  56. package/lib/core/messages-core.js +615 -0
  57. package/lib/core/messages.d.ts +60 -2
  58. package/lib/core/messages.js +116 -13
  59. package/lib/core/migration-consult.js +21 -20
  60. package/lib/core/migration-history.js +19 -5
  61. package/lib/core/model-tools.d.ts +0 -22
  62. package/lib/core/model-tools.js +50 -27
  63. package/lib/core/phase3-host.js +30 -5
  64. package/lib/core/plugin-cli.js +18 -19
  65. package/lib/core/reconcile.d.ts +2 -14
  66. package/lib/core/reconcile.js +87 -17
  67. package/lib/core/recovery-orchestrator.js +19 -18
  68. package/lib/core/reinstall.d.ts +4 -0
  69. package/lib/core/reinstall.js +26 -21
  70. package/lib/core/restore.js +20 -17
  71. package/lib/core/rollback-ignore-rules.d.ts +31 -0
  72. package/lib/core/rollback-ignore-rules.js +140 -0
  73. package/lib/core/rollback-point.d.ts +1208 -0
  74. package/lib/core/rollback-point.js +2993 -0
  75. package/lib/core/run-registry.js +6 -5
  76. package/lib/core/session-health.d.ts +15 -0
  77. package/lib/core/session-meta.d.ts +42 -2
  78. package/lib/core/session-meta.js +140 -1
  79. package/lib/core/session-title.d.ts +28 -0
  80. package/lib/core/session-title.js +185 -0
  81. package/lib/core/startup-barrier.d.ts +24 -0
  82. package/lib/core/startup-barrier.js +22 -0
  83. package/lib/core/transaction-coordinator.js +4 -3
  84. package/lib/core/types.d.ts +18 -1
  85. package/lib/core/validator.js +5 -4
  86. package/lib/core/verify-recovery.js +31 -30
  87. package/lib/foreign/bundle.js +3 -2
  88. package/lib/foreign/continue.js +2 -1
  89. package/lib/foreign/crush.js +2 -1
  90. package/lib/foreign/gemini.js +2 -1
  91. package/lib/foreign/goose.js +2 -1
  92. package/lib/foreign/kilocode.js +2 -1
  93. package/lib/foreign/kimi.js +2 -1
  94. package/lib/foreign/messages.d.ts +15 -0
  95. package/lib/foreign/messages.js +40 -0
  96. package/lib/foreign/mimocode.js +2 -1
  97. package/lib/foreign/openclaw.js +2 -1
  98. package/lib/foreign/opencode.js +2 -1
  99. package/lib/foreign/pi.js +2 -1
  100. package/lib/foreign/qoder.js +2 -1
  101. package/lib/foreign/qwen.js +2 -1
  102. package/lib/foreign/registry.js +7 -6
  103. package/lib/foreign/teleagent.js +2 -1
  104. package/lib/foreign/workbuddy.js +2 -1
  105. package/lib/foreign/zcode.js +2 -1
  106. package/lib/foreign/zed.js +2 -1
  107. package/lib/index.d.ts +223 -7
  108. package/lib/index.js +729 -54
  109. package/lib/market/git-file-writer.js +20 -19
  110. package/lib/market/github-repos.js +35 -19
  111. package/lib/market/index-parser.js +31 -30
  112. package/lib/market/messages-market.d.ts +138 -0
  113. package/lib/market/messages-market.js +305 -0
  114. package/lib/market/my-repo.d.ts +2 -2
  115. package/lib/market/my-repo.js +32 -29
  116. package/lib/market/prepare.js +18 -15
  117. package/lib/market/security.js +20 -19
  118. package/lib/market/types.js +2 -1
  119. package/lib/market/url.js +2 -1
  120. package/lib/migrations/index.js +6 -5
  121. package/lib/migrations/messages-migrations.d.ts +32 -0
  122. package/lib/migrations/messages-migrations.js +58 -0
  123. package/lib/profiles/dsh-profile-launcher.js +2 -1
  124. package/lib/profiles/dsh-profile-runtime.js +2 -1
  125. package/lib/profiles/messages.d.ts +7 -0
  126. package/lib/profiles/messages.js +27 -0
  127. package/lib/routes/backup.js +18 -2
  128. package/lib/routes/import.js +3 -3
  129. package/lib/routes/index.d.ts +1 -1
  130. package/lib/routes/kit.d.ts +3 -2
  131. package/lib/routes/kit.js +6 -1
  132. package/lib/routes/market.js +1 -1
  133. package/lib/routes/messages.d.ts +42 -0
  134. package/lib/routes/messages.js +100 -0
  135. package/lib/routes/profiles.js +12 -6
  136. package/lib/routes/recovery.js +378 -114
  137. package/lib/routes/snapshots.js +51 -4
  138. package/lib/routes/sync.js +68 -16
  139. package/lib/schema/config.d.ts +11 -1
  140. package/lib/schema/config.js +52 -20
  141. package/lib/schema/index.d.ts +8 -6
  142. package/lib/schema/index.js +8 -6
  143. package/lib/schema/manifest.js +19 -18
  144. package/lib/schema/messages.d.ts +47 -0
  145. package/lib/schema/messages.js +110 -0
  146. package/lib/schema/section-registry.d.ts +41 -14
  147. package/lib/schema/section-registry.js +35 -29
  148. package/lib/schema/versions.js +6 -5
  149. package/lib/security/encryption.js +16 -15
  150. package/lib/security/integrity.js +8 -7
  151. package/lib/security/messages.d.ts +30 -0
  152. package/lib/security/messages.js +77 -0
  153. package/lib/security/redaction.js +12 -0
  154. package/lib/security/secret-scanner.js +16 -13
  155. package/lib/security/vault.js +2 -1
  156. package/lib/security/zip-security.js +4 -3
  157. package/lib/sync/ancestor.js +2 -1
  158. package/lib/sync/autosync-config.d.ts +11 -0
  159. package/lib/sync/autosync-config.js +12 -0
  160. package/lib/sync/autosync-scheduler.d.ts +11 -1
  161. package/lib/sync/autosync-scheduler.js +115 -76
  162. package/lib/sync/backup-files.js +3 -2
  163. package/lib/sync/backup-scheduler.js +6 -5
  164. package/lib/sync/blob-store.js +2 -1
  165. package/lib/sync/git/git-transport.d.ts +55 -0
  166. package/lib/sync/git/git-transport.js +152 -2
  167. package/lib/sync/github-auth.js +10 -9
  168. package/lib/sync/layout.js +13 -12
  169. package/lib/sync/messages.d.ts +63 -0
  170. package/lib/sync/messages.js +149 -0
  171. package/lib/sync/review-queue.js +6 -5
  172. package/lib/sync/s3/messages.d.ts +7 -0
  173. package/lib/sync/s3/messages.js +16 -0
  174. package/lib/sync/s3/s3-providers.d.ts +4 -1
  175. package/lib/sync/s3/s3-providers.js +5 -5
  176. package/lib/sync/s3/s3-transport.js +1 -1
  177. package/lib/sync/s3/sigv4.js +2 -1
  178. package/lib/sync/snapshot-crypto.js +6 -5
  179. package/lib/sync/snapshot-json.js +3 -2
  180. package/lib/sync/sync-engine.d.ts +37 -15
  181. package/lib/sync/sync-engine.js +67 -37
  182. package/lib/sync/sync-history.js +7 -6
  183. package/lib/sync/sync-selection.d.ts +15 -4
  184. package/lib/sync/sync-selection.js +17 -5
  185. package/lib/sync/webdav/webdav-transport.d.ts +1 -0
  186. package/lib/sync/webdav/webdav-transport.js +4 -1
  187. package/lib/ui/artifact-view.js +4 -3
  188. package/lib/ui/commands.d.ts +1 -1
  189. package/lib/ui/commands.js +10 -0
  190. package/lib/ui/disk-usage-view.d.ts +33 -4
  191. package/lib/ui/disk-usage-view.js +29 -1
  192. package/lib/ui/errors.d.ts +1 -1
  193. package/lib/ui/errors.js +4 -3
  194. package/lib/ui/export-flow.js +24 -20
  195. package/lib/ui/foreign-view.d.ts +5 -1
  196. package/lib/ui/foreign-view.js +26 -2
  197. package/lib/ui/i18n.d.ts +65 -0
  198. package/lib/ui/i18n.js +114 -0
  199. package/lib/ui/import-wizard.d.ts +7 -0
  200. package/lib/ui/import-wizard.js +13 -10
  201. package/lib/ui/progress.d.ts +2 -2
  202. package/lib/ui/progress.js +1 -2
  203. package/lib/ui/report.d.ts +5 -5
  204. package/lib/ui/report.js +6 -6
  205. package/lib/ui/rollback-view.d.ts +756 -0
  206. package/lib/ui/rollback-view.js +724 -0
  207. package/lib/ui/selection-model.d.ts +41 -0
  208. package/lib/ui/selection-model.js +30 -0
  209. package/lib/ui/session-inventory-view.d.ts +97 -3
  210. package/lib/ui/session-inventory-view.js +76 -2
  211. package/lib/ui/session-issue-labels.d.ts +43 -0
  212. package/lib/ui/session-issue-labels.js +48 -0
  213. package/lib/ui/sync-settings-view.d.ts +33 -0
  214. package/lib/ui/sync-settings-view.js +40 -0
  215. package/lib/ui/types.d.ts +8 -3
  216. package/lib/ui/types.js +1 -1
  217. package/lib/utils/atomic-write.js +6 -5
  218. package/lib/utils/bundle-scan.d.ts +11 -1
  219. package/lib/utils/bundle-scan.js +12 -1
  220. package/lib/utils/env-lock.d.ts +10 -0
  221. package/lib/utils/env-lock.js +73 -46
  222. package/lib/utils/json.js +6 -5
  223. package/lib/utils/messages-utils.d.ts +96 -0
  224. package/lib/utils/messages-utils.js +216 -0
  225. package/lib/utils/proxy.js +18 -5
  226. package/lib/utils/recursive-walk.d.ts +22 -0
  227. package/lib/utils/recursive-walk.js +75 -1
  228. package/lib/utils/session-log-repair.js +10 -9
  229. package/lib/utils/zip.js +27 -26
  230. package/package.json +12 -2
  231. package/src/adapters/agent-instructions.ts +1 -1
  232. package/src/adapters/file-collection.test.ts +1 -0
  233. package/src/adapters/file-collection.ts +1 -1
  234. package/src/adapters/files.test.ts +27 -0
  235. package/src/adapters/index.ts +1 -1
  236. package/src/adapters/link-report.test.ts +22 -2
  237. package/src/adapters/link-report.ts +29 -4
  238. package/src/adapters/messages.ts +54 -0
  239. package/src/adapters/plugin-files.ts +3 -2
  240. package/src/adapters/plugins.ts +4 -3
  241. package/src/adapters/prompts.ts +7 -1
  242. package/src/adapters/self.test.ts +26 -0
  243. package/src/adapters/self.ts +8 -2
  244. package/src/adapters/sessions.ts +4 -2
  245. package/src/adapters/settings.test.ts +38 -1
  246. package/src/adapters/ui.test.ts +2 -2
  247. package/src/adapters/ui.ts +27 -26
  248. package/src/adapters/workspaces.ts +7 -3
  249. package/src/cli/actions.test.ts +28 -0
  250. package/src/cli/actions.ts +11 -2
  251. package/src/cli/help.ts +23 -10
  252. package/src/cli/index-guards.test.ts +193 -0
  253. package/src/cli/index.ts +106 -31
  254. package/src/cli/sessions-inspect.test.ts +15 -1
  255. package/src/cli/sessions-inspect.ts +3 -1
  256. package/src/cli/sessions-repair.test.ts +84 -0
  257. package/src/cli/sessions-repair.ts +33 -7
  258. package/src/cli/web/console-route-parity.test.ts +115 -0
  259. package/src/cli/web/page.ts +23 -12
  260. package/src/cli/web/routes.ts +2 -1
  261. package/src/cli/web/server.ts +4 -4
  262. package/src/cli/web/web.test.ts +2 -2
  263. package/src/client/ConfigManagerSection.tsx +30 -2
  264. package/src/client/about/AboutPanel.tsx +2 -2
  265. package/src/client/about/PluginUpdateDialog.tsx +1 -1
  266. package/src/client/about/ReleaseNotesDialog.tsx +2 -2
  267. package/src/client/about/about-view.test.ts +13 -3
  268. package/src/client/about/about-view.ts +10 -6
  269. package/src/client/about/release-notes-view.test.ts +14 -4
  270. package/src/client/about/release-notes-view.ts +4 -2
  271. package/src/client/api.ts +18 -6
  272. package/src/client/chat/ChatEntryView.tsx +391 -0
  273. package/src/client/chat/RollbackDiffTab.tsx +61 -0
  274. package/src/client/chat/RollbackFileIcon.tsx +96 -0
  275. package/src/client/chat/RollbackReviewDialog.tsx +591 -0
  276. package/src/client/chat/chat-entry-view.test.ts +1174 -0
  277. package/src/client/chat/chat-entry-view.ts +758 -0
  278. package/src/client/chat/chat-locales.ts +152 -0
  279. package/src/client/chat/chat-node-registration.ts +64 -0
  280. package/src/client/chat/chat-point-window.ts +173 -0
  281. package/src/client/chat/chat-slots.ts +107 -0
  282. package/src/client/chat/rollback-diff-tab.test.ts +105 -0
  283. package/src/client/chat/rollback-diff-tab.ts +164 -0
  284. package/src/client/chat/rollback-entry-view.test.ts +1469 -0
  285. package/src/client/chat/rollback-entry-view.ts +941 -0
  286. package/src/client/chat/sidebar-slots.ts +37 -0
  287. package/src/client/common/ConfirmDialog.tsx +7 -3
  288. package/src/client/common/ContentPicker.tsx +48 -10
  289. package/src/client/common/DiffTable.tsx +97 -0
  290. package/src/client/common/ErrorBanner.tsx +4 -4
  291. package/src/client/common/Icon.tsx +2 -0
  292. package/src/client/common/Modal.tsx +8 -1
  293. package/src/client/common/ProgressBar.tsx +8 -2
  294. package/src/client/common/ReportView.tsx +4 -4
  295. package/src/client/common/RunsCenter.tsx +1 -0
  296. package/src/client/common/client-route-parity.test.ts +78 -0
  297. package/src/client/common/info-hint-guard.test.ts +46 -13
  298. package/src/client/common/picker-lazy-units.test.ts +42 -0
  299. package/src/client/common/plan-text-redaction.test.ts +17 -6
  300. package/src/client/common/progress-view-edge.test.ts +8 -7
  301. package/src/client/common/progress-view.test.ts +9 -9
  302. package/src/client/common/progress-view.ts +2 -2
  303. package/src/client/common/routes.ts +28 -0
  304. package/src/client/common/ui.tsx +73 -10
  305. package/src/client/config-manager.module.css +996 -21
  306. package/src/client/css-scale-guard.test.ts +30 -0
  307. package/src/client/environment/EnvironmentPanel.tsx +11 -4
  308. package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
  309. package/src/client/export/ExportView.tsx +13 -8
  310. package/src/client/history/HistoryPanel.tsx +4 -3
  311. package/src/client/history/history-api.ts +2 -2
  312. package/src/client/history/history-locales.ts +5 -1
  313. package/src/client/home/HomePanel.tsx +29 -8
  314. package/src/client/import/ForeignImportView.tsx +4 -5
  315. package/src/client/import/ImportWizardView.tsx +5 -3
  316. package/src/client/import/import-wizard-split.test.ts +3 -1
  317. package/src/client/import/import-wizard-steps.tsx +9 -9
  318. package/src/client/index.ts +186 -3
  319. package/src/client/library/LibraryActions.tsx +32 -7
  320. package/src/client/library/LibraryPanel.tsx +23 -3
  321. package/src/client/library/RestorePlanView.tsx +9 -66
  322. package/src/client/locales.ts +54 -5
  323. package/src/client/lucide-icons.d.ts +1 -0
  324. package/src/client/market/MarketPanel.tsx +13 -2
  325. package/src/client/market/MyConfigsView.tsx +5 -4
  326. package/src/client/market/MyConfigsWizard.tsx +10 -8
  327. package/src/client/market/market-api.test.ts +9 -8
  328. package/src/client/market/market-api.ts +2 -2
  329. package/src/client/market/market-locales.ts +3 -0
  330. package/src/client/market/market-view.test.ts +6 -6
  331. package/src/client/market/market-view.ts +3 -3
  332. package/src/client/market/my-configs-api.ts +2 -2
  333. package/src/client/market/my-configs-view.test.ts +23 -22
  334. package/src/client/market/my-configs-view.ts +8 -8
  335. package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
  336. package/src/client/recovery/RecoveryPanel.tsx +442 -283
  337. package/src/client/recovery/RollbackCard.tsx +665 -0
  338. package/src/client/recovery/RollbackDiffPane.tsx +58 -0
  339. package/src/client/recovery/RollbackFileChanges.tsx +163 -0
  340. package/src/client/recovery/incident-api.ts +2 -2
  341. package/src/client/recovery/recovery-api.ts +116 -6
  342. package/src/client/recovery/recovery-locales.test.ts +299 -0
  343. package/src/client/recovery/recovery-locales.ts +488 -149
  344. package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
  345. package/src/client/recovery/rollback-file-changes.test.ts +73 -0
  346. package/src/client/recovery/session-dialog-mount.test.ts +54 -0
  347. package/src/client/run-store.ts +84 -21
  348. package/src/client/sync/AutosyncCard.tsx +25 -1
  349. package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
  350. package/src/client/sync/ChannelConfigDialog.tsx +10 -10
  351. package/src/client/sync/DecryptPasswordCard.tsx +2 -2
  352. package/src/client/sync/SecurityOptionsCard.tsx +27 -17
  353. package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
  354. package/src/client/sync/SyncConfirmView.tsx +46 -8
  355. package/src/client/sync/SyncLogList.tsx +1 -1
  356. package/src/client/sync/SyncPanel.tsx +31 -23
  357. package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
  358. package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
  359. package/src/client/sync/SyncSettingsView.test.ts +20 -1
  360. package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
  361. package/src/client/sync/WorkDirCard.tsx +112 -0
  362. package/src/client/sync/history-model.test.ts +42 -9
  363. package/src/client/sync/history-model.ts +30 -15
  364. package/src/client/sync/sync-api.test.ts +57 -33
  365. package/src/client/sync/sync-api.ts +80 -6
  366. package/src/client/sync/sync-locales.ts +104 -3
  367. package/src/client/sync/sync-view-v2.test.ts +65 -13
  368. package/src/client/sync/sync-view.test.ts +107 -52
  369. package/src/client/sync/sync-view.ts +124 -41
  370. package/src/core/backup-plan.test.ts +28 -0
  371. package/src/core/backup-plan.ts +29 -17
  372. package/src/core/backup-verify.ts +24 -23
  373. package/src/core/backup.test.ts +76 -0
  374. package/src/core/backup.ts +55 -25
  375. package/src/core/boot-rescue.ts +4 -3
  376. package/src/core/boot-safety.ts +2 -1
  377. package/src/core/command-rollback.test.ts +536 -0
  378. package/src/core/command-rollback.ts +402 -0
  379. package/src/core/consult-source.ts +7 -6
  380. package/src/core/disk-usage.test.ts +257 -1
  381. package/src/core/disk-usage.ts +134 -14
  382. package/src/core/engine-locale.test.ts +593 -0
  383. package/src/core/engine-locale.ts +175 -0
  384. package/src/core/exporter.ts +3 -3
  385. package/src/core/fork-reconcile.test.ts +279 -0
  386. package/src/core/fork-reconcile.ts +234 -0
  387. package/src/core/journal.ts +6 -5
  388. package/src/core/local-plugin-host.ts +3 -2
  389. package/src/core/local-plugin-pack.ts +11 -6
  390. package/src/core/messages-core.ts +657 -0
  391. package/src/core/messages.ts +123 -17
  392. package/src/core/migration-consult.ts +21 -20
  393. package/src/core/migration-history.ts +19 -5
  394. package/src/core/model-tools.ts +28 -27
  395. package/src/core/phase3-host.ts +21 -5
  396. package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
  397. package/src/core/plugin-cli.ts +18 -19
  398. package/src/core/reconcile.test.ts +117 -0
  399. package/src/core/reconcile.ts +85 -17
  400. package/src/core/recovery-orchestrator.ts +19 -18
  401. package/src/core/reinstall.ts +26 -21
  402. package/src/core/restore.ts +20 -17
  403. package/src/core/rollback-file-diff.test.ts +159 -0
  404. package/src/core/rollback-ignore-rules.ts +126 -0
  405. package/src/core/rollback-point.test.ts +2603 -0
  406. package/src/core/rollback-point.ts +3755 -0
  407. package/src/core/run-registry.ts +6 -5
  408. package/src/core/session-health.ts +15 -0
  409. package/src/core/session-meta.test.ts +117 -1
  410. package/src/core/session-meta.ts +149 -2
  411. package/src/core/session-title.test.ts +127 -0
  412. package/src/core/session-title.ts +189 -0
  413. package/src/core/startup-barrier.test.ts +69 -0
  414. package/src/core/startup-barrier.ts +27 -0
  415. package/src/core/transaction-coordinator.ts +4 -3
  416. package/src/core/types.ts +18 -1
  417. package/src/core/validator.ts +5 -4
  418. package/src/core/verify-recovery.ts +31 -30
  419. package/src/foreign/bundle.ts +3 -2
  420. package/src/foreign/continue.ts +2 -1
  421. package/src/foreign/crush.ts +2 -1
  422. package/src/foreign/file-budget.test.ts +2 -0
  423. package/src/foreign/gemini.ts +2 -1
  424. package/src/foreign/goose.ts +2 -1
  425. package/src/foreign/kilocode.ts +2 -1
  426. package/src/foreign/kimi.ts +2 -1
  427. package/src/foreign/messages.ts +44 -0
  428. package/src/foreign/mimocode.ts +2 -1
  429. package/src/foreign/openclaw.ts +2 -1
  430. package/src/foreign/opencode.ts +2 -1
  431. package/src/foreign/pi.ts +2 -1
  432. package/src/foreign/qoder.ts +2 -1
  433. package/src/foreign/qwen.ts +2 -1
  434. package/src/foreign/registry.ts +7 -6
  435. package/src/foreign/teleagent.ts +2 -1
  436. package/src/foreign/workbuddy.ts +2 -1
  437. package/src/foreign/zcode.ts +2 -1
  438. package/src/foreign/zed.ts +2 -1
  439. package/src/index.facade.test.ts +340 -1
  440. package/src/index.ts +813 -57
  441. package/src/market/git-file-writer.ts +20 -19
  442. package/src/market/github-repos.test.ts +23 -0
  443. package/src/market/github-repos.ts +37 -19
  444. package/src/market/index-parser.ts +31 -30
  445. package/src/market/messages-market.ts +325 -0
  446. package/src/market/my-repo.ts +32 -29
  447. package/src/market/prepare.ts +18 -15
  448. package/src/market/security.ts +20 -19
  449. package/src/market/types.ts +2 -1
  450. package/src/market/url.ts +2 -1
  451. package/src/migrations/index.ts +6 -5
  452. package/src/migrations/messages-migrations.ts +59 -0
  453. package/src/profiles/dsh-profile-launcher.ts +2 -1
  454. package/src/profiles/dsh-profile-runtime.ts +2 -1
  455. package/src/profiles/messages.ts +31 -0
  456. package/src/routes/backup.ts +18 -2
  457. package/src/routes/import.ts +3 -3
  458. package/src/routes/index.ts +1 -1
  459. package/src/routes/kit.ts +9 -3
  460. package/src/routes/market.ts +1 -1
  461. package/src/routes/messages.ts +104 -0
  462. package/src/routes/profiles.ts +12 -6
  463. package/src/routes/recovery.test.ts +405 -1
  464. package/src/routes/recovery.ts +377 -109
  465. package/src/routes/snapshots.ts +51 -4
  466. package/src/routes/sync.ts +67 -16
  467. package/src/schema/config.ts +53 -20
  468. package/src/schema/index.ts +7 -5
  469. package/src/schema/manifest.ts +19 -18
  470. package/src/schema/messages.ts +120 -0
  471. package/src/schema/registry.test.ts +19 -5
  472. package/src/schema/section-registry.ts +64 -39
  473. package/src/schema/versions.ts +6 -5
  474. package/src/security/encryption.ts +16 -15
  475. package/src/security/integrity.ts +8 -7
  476. package/src/security/messages.ts +87 -0
  477. package/src/security/redaction.test.ts +17 -1
  478. package/src/security/redaction.ts +13 -0
  479. package/src/security/secret-scanner.ts +19 -12
  480. package/src/security/security.test.ts +47 -0
  481. package/src/security/vault.ts +2 -1
  482. package/src/security/zip-security.ts +4 -3
  483. package/src/sync/ancestor.ts +2 -1
  484. package/src/sync/autosync-config.ts +21 -0
  485. package/src/sync/autosync-scheduler.test.ts +63 -7
  486. package/src/sync/autosync-scheduler.ts +118 -78
  487. package/src/sync/backup-files.ts +3 -2
  488. package/src/sync/backup-scheduler.ts +6 -5
  489. package/src/sync/blob-store.ts +2 -1
  490. package/src/sync/git/git-transport.test.ts +170 -8
  491. package/src/sync/git/git-transport.ts +176 -2
  492. package/src/sync/github-auth.ts +10 -9
  493. package/src/sync/layout.ts +13 -12
  494. package/src/sync/messages.ts +153 -0
  495. package/src/sync/review-queue.ts +6 -5
  496. package/src/sync/s3/messages.ts +16 -0
  497. package/src/sync/s3/s3-providers.ts +9 -6
  498. package/src/sync/s3/s3-transport.ts +1 -1
  499. package/src/sync/s3/sigv4.ts +3 -1
  500. package/src/sync/snapshot-crypto.ts +6 -5
  501. package/src/sync/snapshot-json.ts +3 -2
  502. package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
  503. package/src/sync/sync-engine.test.ts +226 -8
  504. package/src/sync/sync-engine.ts +103 -41
  505. package/src/sync/sync-history.ts +7 -6
  506. package/src/sync/sync-selection.test.ts +12 -2
  507. package/src/sync/sync-selection.ts +18 -5
  508. package/src/sync/webdav/webdav-transport.ts +4 -1
  509. package/src/ui/artifact-view.ts +4 -3
  510. package/src/ui/commands.test.ts +1 -0
  511. package/src/ui/commands.ts +11 -1
  512. package/src/ui/disk-usage-view.test.ts +113 -6
  513. package/src/ui/disk-usage-view.ts +63 -7
  514. package/src/ui/errors.ts +4 -3
  515. package/src/ui/export-flow.ts +24 -20
  516. package/src/ui/foreign-view.test.ts +43 -0
  517. package/src/ui/foreign-view.ts +30 -3
  518. package/src/ui/i18n.ts +125 -0
  519. package/src/ui/import-wizard.ts +18 -10
  520. package/src/ui/progress.ts +2 -2
  521. package/src/ui/report.ts +7 -6
  522. package/src/ui/rollback-view.test.ts +819 -0
  523. package/src/ui/rollback-view.ts +1339 -0
  524. package/src/ui/selection-model.test.ts +33 -1
  525. package/src/ui/selection-model.ts +53 -0
  526. package/src/ui/session-inventory-view.test.ts +109 -0
  527. package/src/ui/session-inventory-view.ts +138 -3
  528. package/src/ui/session-issue-labels.test.ts +51 -0
  529. package/src/ui/session-issue-labels.ts +70 -0
  530. package/src/ui/sync-settings-view.test.ts +36 -0
  531. package/src/ui/sync-settings-view.ts +49 -0
  532. package/src/ui/types.ts +10 -4
  533. package/src/utils/atomic-write.ts +6 -5
  534. package/src/utils/bundle-scan.test.ts +2 -2
  535. package/src/utils/bundle-scan.ts +12 -1
  536. package/src/utils/env-lock.ts +83 -49
  537. package/src/utils/json.ts +7 -5
  538. package/src/utils/messages-utils.ts +232 -0
  539. package/src/utils/proxy.test.ts +25 -0
  540. package/src/utils/proxy.ts +19 -5
  541. package/src/utils/recursive-walk.test.ts +43 -0
  542. package/src/utils/recursive-walk.ts +75 -1
  543. package/src/utils/session-log-repair.ts +10 -9
  544. package/src/utils/zip.ts +27 -26
  545. package/lib/core/checkpoint.d.ts +0 -362
  546. package/lib/core/checkpoint.js +0 -1309
  547. package/src/core/checkpoint.test.ts +0 -665
  548. package/src/core/checkpoint.ts +0 -1614
@@ -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(`active transaction ${opId} 未收敛(待 recover / reconcile),拒绝创建新 transaction`)
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
- probeExternal: async () => 'unknown',
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(`snapshotProvider 返回 null(快照创建失败),abort operation ${operationType}`);
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('bindSnapshot: snapshotId 为空');
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(`bindSnapshot 非法 state: ${next.state}`);
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) => {
@@ -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: 'profile 的 node_modules 由旧版 pnpm 创建,与当前 pnpm 的默认配置不兼容;在 profile 目录执行一次 pnpm install 重建后重试 / this profile\'s node_modules was created by a different pnpm major; rebuild it (pnpm install) before retrying',
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: 'profile 是 pnpm workspace,当前 dsh CLI 转发未带 -w 被 pnpm 拒绝;可手动执行 dsh plugin --profile <name> add -w <pkg> / pnpm refused to add at a workspace root (missing -w); run dsh plugin --profile <name> add -w <pkg> by hand',
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: 'profile 目录不是 pnpm workspace,却传入了 -w,因此被 pnpm 拒绝 / -w was passed but the profile is not a pnpm workspace, so pnpm refused the operation',
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: 'profile 里有刚发布不久的插件版本,pnpm 的安全等待期检查拒绝了本次改动(即使改的是别的插件);等待发布期满,或临时以 --config.minimumReleaseAge=0 重试 / a recently-published plugin version in this profile trips pnpm\'s fresh-release safety check, blocking any change (even to other plugins); wait for the release to mature, or retry once with --config.minimumReleaseAge=0',
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: '该 git 插件需要在安装时执行构建脚本,被 pnpm 的 allowBuilds 白名单拦截;把 pnpm 提示的包名加入 profile 的 pnpm-workspace.yaml 的 allowBuilds 后重试 / this git-hosted plugin needs to run its build script at install time, which pnpm blocks until allowed — add the exact package pnpm printed to allowBuilds in the profile\'s pnpm-workspace.yaml, then re-run',
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 zh = file === undefined ? '' : `(${file})`
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: `profile 的 pnpm-workspace.yaml 声明了 patchedDependencies,但对应的 patch 文件不存在${zh};pnpm 因此拒绝任何安装(含与补丁无关的插件)。把缺失的 patch 文件放回 profile 的 patches/ 目录,或从 pnpm-workspace.yaml 删除该条目后重试 / the profile's pnpm-workspace.yaml declares patchedDependencies but the patch file is missing${en}; pnpm then refuses every install (even unrelated plugins). Put the patch file back under the profile's patches/ directory, or remove that entry from pnpm-workspace.yaml and retry`,
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 zh = pkg === undefined ? '' : `(${pkg})`
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: `有一个依赖在 registry 上不存在${zh},pnpm 拒绝任何安装操作;它可能是之前失败操作残留在 profile package.json 里的幽灵依赖(手动删除该行),也可能是需要登录的私有包 / a dependency cannot be resolved from the registry${en}; pnpm refuses every install while it is present — it may be a ghost entry left in the profile's package.json by an earlier failed operation (remove that line by hand), or a private package needing registry credentials`,
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: '拉取依赖时网络临时失败(安装会重放整个依赖树,任何一个既有依赖抖动都会中断本次操作);稍后直接重试通常即可 / a transient network failure while fetching dependencies (installs replay the whole dependency tree, so any existing dependency can hiccup); a plain retry usually succeeds',
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: 'desktop 是 DeepSeek Harness Desktop 独占管理的保留档案,普通 dsh CLI 拒绝操作它;请在桌面端应用内重试(新版本插件会自动改用桌面端自带的 CLI 载体),或改用其它档案 / the "desktop" profile is managed exclusively by the DeepSeek Harness Desktop application and the plain dsh CLI refuses to touch it; retry from inside the Desktop app (a current version of this plugin switches to the Desktop-bundled CLI carrier) or target another profile',
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: '找不到 pnpm(dsh 转发 pnpm 失败):请先安装 pnpm(npm install -g pnpm 或 corepack enable pnpm),然后重试 / pnpm is not on PATH (dsh forwards to pnpm): install pnpm (npm install -g pnpm or corepack enable pnpm) and retry',
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(`无法启动 dsh CLI(${result.spawnError}):请确认 dsh 已安装且在 PATH 上,然后重试安装 ${pkg} / cannot start dsh CLI (${result.spawnError}): make sure dsh is installed and on PATH, then retry installing ${pkg}`)
698
+ return new Error(engineMsg('core.pluginCli.dshCliStartFailed', { spawnError: result.spawnError, pkg }))
700
699
  }
701
700
  if (result.timedOut) {
702
- return new Error(`安装 ${pkg} 超时(进程已终止,默认 15 分钟):请检查网络或稍后重试 / install ${pkg} timed out (process killed, default 15 min): check your network and retry later`)
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(`安装 ${pkg} 失败(${failure.code}):${failure.message}`)
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(`dsh plugin 安装 ${pkg} 失败(exit ${String(result.exitCode)}):${tail}`)
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
+
@@ -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 = `journal ${operationId} 无法解析或 schema 非法(隔离处理)`;
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: 'owner 存活,live operation 进行中', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
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 = `ownership binding 不匹配(journal.ownerInstanceId/lockId 非 crashed ownership ${env.expectedOwnershipInstanceId}),不作 trusted 恢复`;
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 = `environment fingerprint 不匹配(跨机/环境变化),不自动恢复`;
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: 'NEEDS_ATTENTION 待用户确认', snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
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: `terminal(${j.state}) journal 规整到 completed`, snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
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 reason = `回滚中断(${j.state === 'ROLLING_BACK' ? 'ROLLING_BACK' : `entryDone ${Object.keys(j.rollback.entryDone).join(',')}`}),需显式续跑回滚(用户确认)`;
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 = 'RECOVERING 且 recovery 未验证(无 recoveryVerification 或 verdict 非 MATCH/PARTIAL_MATCH),绝不自动 RECOVERED';
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(`planned step ${stepId} 缺少 steps 条目(损坏/篡改 journal)`);
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(`step ${step.ref} 未可靠完成(磁盘==before)`); allDoneConfirmed = false; anyUnprovable = true; continue; }
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(`step ${step.ref} 指纹 ${verdict}`);
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: 'mutation 未开始,安全完成', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
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 = 'opaque APPLYING(空 steps)且 mutation 可能已开始,有 trusted snapshot 可回滚(需用户确认)';
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 = 'opaque APPLYING(空 steps)且 mutation 可能已开始,无 trusted snapshot 可回滚';
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: '无副作用可恢复,安全完成', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
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 = `不可证明步骤: ${details.join('; ') || '外部副作用未知'}` + (snapshotExists ? '' : ';快照缺失');
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 = '部分 step 已应用,可安全回滚(需用户确认)';
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