@zq-silk/yui 0.13.9 → 0.14.0

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 (229) hide show
  1. package/ARCHITECTURE.md +69 -428
  2. package/README.md +108 -298
  3. package/dist/agent/managedRuntimeEnvironment.js +2 -2
  4. package/dist/cli/commandCatalog.js +39 -77
  5. package/dist/cli/interactionCandidates.js +4 -4
  6. package/dist/cli/interactionPolicy.js +7 -10
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/updateCommand.js +6 -27
  9. package/dist/cli/updateOrchestrator.js +63 -484
  10. package/dist/cli/updatePorts.js +15 -333
  11. package/dist/cli/upgradeCommand.js +8 -65
  12. package/dist/cli.js +57 -242
  13. package/dist/commands/configCommands.js +11 -62
  14. package/dist/commands/executionAuditCommands.js +23 -23
  15. package/dist/commands/globalRoleCommands.js +2 -7
  16. package/dist/commands/jobCommands.js +1 -4
  17. package/dist/commands/projectCommands.js +5 -5
  18. package/dist/commands/releaseCommands.js +3 -10
  19. package/dist/commands/resourcesCommands.js +2 -2
  20. package/dist/commands/sessionCommands.js +2 -2
  21. package/dist/commands/taskActor.js +31 -39
  22. package/dist/commands/taskChangeSetCommands.js +5 -12
  23. package/dist/commands/taskCommands.js +464 -1249
  24. package/dist/commands/taskContextCommand.js +35 -36
  25. package/dist/commands/taskExecutionCommands.js +33 -46
  26. package/dist/commands/taskInputCommands.js +20 -42
  27. package/dist/commands/taskNextActionCommand.js +9 -9
  28. package/dist/commands/taskOverviewCommand.js +56 -95
  29. package/dist/commands/taskRoleRuntimeStatus.js +77 -107
  30. package/dist/commands/taskUpstreamCommands.js +3 -3
  31. package/dist/commands/taskWorkspaceCommands.js +7 -79
  32. package/dist/commands/telemetryCommands.js +32 -124
  33. package/dist/commands/workflowCommands.js +3 -3
  34. package/dist/completion/fileCompletionManager.js +1 -1
  35. package/dist/config/configCatalog.js +4 -6
  36. package/dist/config/yuiConfig.js +6 -43
  37. package/dist/context/dispatchContext.js +14 -30
  38. package/dist/context/roleSessionContext.js +2 -2
  39. package/dist/context/sessionBootstrapManifest.js +2 -2
  40. package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
  41. package/dist/context/turnInputContract.js +204 -0
  42. package/dist/context/wakeNotification.js +2 -2
  43. package/dist/controller/agentRuntimeObserver.js +18 -18
  44. package/dist/controller/clientRuntime.js +13 -20
  45. package/dist/controller/controller.js +89 -116
  46. package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
  47. package/dist/controller/jobControl.js +17 -17
  48. package/dist/controller/resourceInventory.js +6 -6
  49. package/dist/controller/resourceInventoryLinux.js +38 -59
  50. package/dist/controller/resourceInventoryRpc.js +3 -3
  51. package/dist/controller/resourceInventoryWorker.js +1 -1
  52. package/dist/controller/runtime.js +46 -53
  53. package/dist/controller/runtimeEventInbox.js +23 -31
  54. package/dist/controller/runtimeEventProcessor.js +15 -15
  55. package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
  56. package/dist/controller/runtimeLaunchCoordinator.js +3 -3
  57. package/dist/controller/runtimeObservationHook.js +11 -8
  58. package/dist/controller/sessionNotify.js +11 -11
  59. package/dist/controller/structuredProviderObservation.js +89 -16
  60. package/dist/coordination/workMailbox.js +309 -579
  61. package/dist/coordination/workMailboxQueue.js +5 -6
  62. package/dist/core/controllerClient.js +1 -91
  63. package/dist/core/controllerServer.js +3 -6
  64. package/dist/doctor/doctor.js +51 -314
  65. package/dist/execution/candidateConvergence.js +3 -3
  66. package/dist/execution/executionGroup.js +32 -33
  67. package/dist/execution/executionHealth.js +36 -38
  68. package/dist/execution/resourceBroker.js +12 -12
  69. package/dist/executor/agentExecutor.js +30 -265
  70. package/dist/executor/effectiveLaunch.js +9 -19
  71. package/dist/executor/executorRegistry.js +43 -17
  72. package/dist/executor/fileRoleLaunchPlanner.js +56 -56
  73. package/dist/executor/launchPlan.js +3 -3
  74. package/dist/executor/workspacePreflightClassification.js +16 -16
  75. package/dist/grant/capabilityGrant.js +20 -25
  76. package/dist/input/inputRequest.js +12 -12
  77. package/dist/integration/changeSet.js +9 -13
  78. package/dist/integration/changeSetManifest.js +1 -1
  79. package/dist/integration/gitIntegrationService.js +2 -2
  80. package/dist/integration/integrationQueueService.js +5 -21
  81. package/dist/integration/overlapDiagnostics.js +2 -2
  82. package/dist/interaction/operatorPresentation.js +1 -1
  83. package/dist/job/jobRunner.js +1 -1
  84. package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
  85. package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
  86. package/dist/message/message.js +6 -6
  87. package/dist/observability/executionAudit.js +74 -86
  88. package/dist/observability/faultClassification.js +7 -24
  89. package/dist/observability/orchestrationMetrics.js +5 -5
  90. package/dist/observability/runtimeIdentity.js +34 -101
  91. package/dist/profile/agentProfile.js +1 -1
  92. package/dist/release/cliHomeReleaseFence.js +3 -3
  93. package/dist/release/releaseWorkflowEngine.js +6 -10
  94. package/dist/release/releaseWorkflowPorts.js +2 -2
  95. package/dist/release/workflowFileLock.js +1 -1
  96. package/dist/repository/checkoutSwap.js +1 -1
  97. package/dist/repository/gitWorkspace.js +0 -13
  98. package/dist/repository/project.js +1 -1
  99. package/dist/repository/projectMaintenanceLock.js +1 -2
  100. package/dist/repository/taskBaseFreshness.js +13 -13
  101. package/dist/repository/taskWorkspaceCoordinator.js +21 -25
  102. package/dist/repository/taskWorkspaceIdentity.js +8 -21
  103. package/dist/repository/taskWorkspacePreparer.js +63 -620
  104. package/dist/resources/autoResourceGc.js +3 -3
  105. package/dist/resources/liveReferences.js +1 -1
  106. package/dist/resources/resourceGc.js +1 -1
  107. package/dist/resources/sqliteResourceRegistry.js +1 -1
  108. package/dist/review/reviewDecision.js +7 -7
  109. package/dist/review/reviewFindingLedger.js +1 -1
  110. package/dist/review/reviewOutcomeClassifier.js +35 -50
  111. package/dist/review/reviewRound.js +14 -25
  112. package/dist/review/reviewerAvailability.js +4 -4
  113. package/dist/review/taskFinalReviewContractResolution.js +41 -0
  114. package/dist/role/role.js +6 -3
  115. package/dist/runtime/agentDriver.js +4 -0
  116. package/dist/runtime/agentHost.js +151 -36
  117. package/dist/runtime/builtinAgentDrivers.js +79 -2
  118. package/dist/runtime/codexAppServerRuntime.js +122 -0
  119. package/dist/runtime/continuationManager.js +3 -5
  120. package/dist/runtime/exactControlPlane.js +17 -42
  121. package/dist/runtime/firstProgressAdvisory.js +1 -1
  122. package/dist/runtime/index.js +2 -2
  123. package/dist/runtime/launchDiagnostics.js +2 -2
  124. package/dist/runtime/lifecycleReservation.js +4 -4
  125. package/dist/runtime/processExitObservation.js +1 -1
  126. package/dist/runtime/processExitOutbox.js +0 -32
  127. package/dist/runtime/promptEnvelope.js +9 -9
  128. package/dist/runtime/providerContinuation.js +2 -2
  129. package/dist/runtime/providerContinuationReconciliationService.js +2 -2
  130. package/dist/runtime/providerRuntimeIdentity.js +76 -24
  131. package/dist/runtime/recentTurnIds.js +38 -0
  132. package/dist/runtime/runtimeContinuationProjection.js +2 -2
  133. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  134. package/dist/runtime/runtimeObservation.js +58 -21
  135. package/dist/runtime/runtimeProjection.js +19 -27
  136. package/dist/runtime/sessionLaunchRequest.js +2 -2
  137. package/dist/runtime/structuredProviderHost.js +177 -12
  138. package/dist/runtime/tmuxAdapters.js +46 -11
  139. package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
  140. package/dist/scheduler/leaderWakeupProcessor.js +147 -445
  141. package/dist/scheduler/operatorEvent.js +4 -6
  142. package/dist/scheduler/resourceQueueProjection.js +71 -0
  143. package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
  144. package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
  145. package/dist/scheduler/taskExecutionProjection.js +99 -99
  146. package/dist/scheduler/taskObservabilityProjection.js +10 -10
  147. package/dist/scheduler/taskWake.js +4 -4
  148. package/dist/scheduler/wakeReason.js +6 -10
  149. package/dist/scheduler/wakeupQueue.js +0 -9
  150. package/dist/setup/setupCommand.js +2 -2
  151. package/dist/storage/currentTaskStore.js +46 -0
  152. package/dist/storage/persistenceWorker.js +1 -1
  153. package/dist/storage/sqliteSchema.js +81 -485
  154. package/dist/storage/sqliteStore.js +206 -365
  155. package/dist/storage/storageSchema.js +1 -15
  156. package/dist/storage/storageVersions.js +8 -9
  157. package/dist/storage/storeRpc.js +10 -34
  158. package/dist/storage/taskStore.js +63 -3415
  159. package/dist/storage/upgrade/recordVersions.js +55 -149
  160. package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
  161. package/dist/task/completionReadiness.js +5 -5
  162. package/dist/task/deliveryGuard.js +10 -10
  163. package/dist/task/nextAction.js +74 -77
  164. package/dist/task/task.js +2 -4
  165. package/dist/task/taskRecordReference.js +3 -3
  166. package/dist/task/taskRecordRetirement.js +1 -1
  167. package/dist/telemetry/sqliteTelemetryStore.js +53 -53
  168. package/dist/telemetry/telemetryConfig.js +14 -14
  169. package/dist/telemetry/telemetryWiring.js +8 -14
  170. package/dist/turn/turn.js +356 -0
  171. package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
  172. package/dist/verification/gateArtifactStore.js +2 -2
  173. package/dist/web/assets/client/app.js +1 -1
  174. package/dist/web/assets/client/components.js +29 -54
  175. package/dist/web/assets/client/i18n.js +17 -37
  176. package/dist/web/assets/client/view.js +37 -37
  177. package/dist/web/assets/shell.js +1 -1
  178. package/dist/web/assets/styles/cards.js +5 -5
  179. package/dist/web/assets/styles/widgets.js +1 -2
  180. package/dist/web/webSnapshot.js +65 -60
  181. package/dist/workItem/workItem.js +11 -11
  182. package/dist/workspace/workItemChangeSetManager.js +1 -1
  183. package/docs/task-local-identity.md +5 -5
  184. package/i18n/README.zh-CN.md +41 -54
  185. package/package.json +1 -1
  186. package/skills/yui-leader/SKILL.md +85 -83
  187. package/skills/yui-operator/SKILL.md +16 -16
  188. package/skills/yui-reviewer/SKILL.md +10 -10
  189. package/skills/yui-runtime/SKILL.md +17 -18
  190. package/skills/yui-worker/SKILL.md +16 -18
  191. package/dist/context/runContextContract.js +0 -162
  192. package/dist/executor/turnCompletion.js +0 -126
  193. package/dist/review/taskFinalReviewContractEvent.js +0 -1
  194. package/dist/review/taskFinalReviewContractRebind.js +0 -367
  195. package/dist/run/agentRun.js +0 -356
  196. package/dist/run/rejectedYieldAttempt.js +0 -221
  197. package/dist/run/runControlRequest.js +0 -50
  198. package/dist/run/yieldReceipt.js +0 -65
  199. package/dist/runtime/turnCompletion.js +0 -3
  200. package/dist/scheduler/actionability.js +0 -323
  201. package/dist/scheduler/activeRoleRunDelivery.js +0 -868
  202. package/dist/scheduler/activeTaskProgress.js +0 -126
  203. package/dist/storage/compatibleTaskStore.js +0 -248
  204. package/dist/storage/migration/baseline.js +0 -78
  205. package/dist/storage/migration/classifier.js +0 -51
  206. package/dist/storage/migration/compatibleCodec.js +0 -53
  207. package/dist/storage/migration/engine.js +0 -147
  208. package/dist/storage/migration/index.js +0 -33
  209. package/dist/storage/migration/planner.js +0 -154
  210. package/dist/storage/migration/productionRegistry.js +0 -3931
  211. package/dist/storage/migration/registry.js +0 -169
  212. package/dist/storage/migration/report.js +0 -54
  213. package/dist/storage/migration/types.js +0 -31
  214. package/dist/storage/upgrade/homeClassification.js +0 -353
  215. package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
  216. package/dist/storage/upgrade/migrationReceipt.js +0 -67
  217. package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
  218. package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
  219. package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
  220. package/dist/storage/upgrade/recordVersionScan.js +0 -176
  221. package/dist/storage/upgrade/sqliteFileSet.js +0 -50
  222. package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
  223. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
  224. package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
  225. package/dist/storage/upgrade/switchProgress.js +0 -80
  226. package/dist/storage/upgrade/upgradeReceipt.js +0 -161
  227. package/dist/storage/upgradeCoordination.js +0 -187
  228. package/dist/storage/upgradeFence.js +0 -366
  229. package/dist/telemetry/telemetryCompaction.js +0 -252
@@ -1,55 +1,12 @@
1
1
  /**
2
- * The `yui update` orchestration.
2
+ * Side-by-side `yui update` orchestration for the current storage contract.
3
3
  *
4
- * The previous implementation ran `npm install --global` *first*, which is
5
- * self-contradictory with "on failure the old CLI and Home are both unchanged":
6
- * once the global package is replaced there is nothing to fall back to. This
7
- * orchestrator replaces that with a side-by-side staging flow:
8
- *
9
- * stage (side-by-side, never touches the live install)
10
- * -> preflight (the STAGED new binary classifies the target Home read-only)
11
- * -> current/compatible: exact Controller stop -> activate the same binary
12
- * -> migration-required: authoritative offline inventory -> recoverable
13
- * storage switch -> activate the same binary
14
- * -> post-verify through the activated binary, then replacement Controller
15
- *
16
- * ## Rollback boundary (NARROWED — no versioned binary pointer is introduced)
17
- *
18
- * The managed Session launcher is an in-place forwarder, not a versioned package
19
- * pointer, so this release does NOT claim binary+Home dual-resource atomicity.
20
- * The guarantees it DOES make, precisely:
21
- *
22
- * - **Staging is isolated.** `stage` installs side-by-side and never mutates the
23
- * live global install, so a stage/preflight failure leaves the old binary and
24
- * the Home byte-for-byte unchanged.
25
- * - **Compatible activation does not switch the Home.** The fast path stops the
26
- * exact Controller, promotes the staged artifact, and verifies a compatible
27
- * current-model loader without copy, backup, rename, or Session wait.
28
- * - **Storage activation is recoverable until writes resume.** The Home switch is
29
- * atomic with a timestamped backup (see the migration engine). Before the new
30
- * version resumes writes, recovery is a single restore of that backup.
31
- * - **No auto-downgrade after writes.** Once the new version has resumed writes
32
- * (a new Controller/first write against the migrated Home), the tool never
33
- * auto-reverts; the migrated Home is authoritative.
34
- *
35
- * The one window it CANNOT make atomic is: storage already switched to the new
36
- * schema, but binary promotion then fails. Because the axes are version-gated,
37
- * the old binary fail-closes on the new Home rather than misreading it; recovery
38
- * is to restore the timestamped Home backup (the exact command is printed), after
39
- * which the old binary works again. This is stated in the failure output, not
40
- * hidden behind a false atomicity claim.
41
- */
42
- import { isAbsolute } from "node:path";
43
- import { switchProgressPath } from "../storage/upgrade/switchProgress.js";
44
- import { upgradeReceiptPath } from "../storage/upgrade/upgradeReceipt.js";
45
- /**
46
- * Run the update orchestration. Never performs an irreversible step before the
47
- * read-only preflight has proven the Home is safe, and reports the precise phase
48
- * plus a recovery action on any failure.
4
+ * The staged binary may only prove that the target Home already implements the
5
+ * exact current contract. Updating never rewrites, normalizes, migrates, or
6
+ * repairs storage. An older Home stays untouched so an Operator can inspect it
7
+ * with its original Yui version and create a new current Task explicitly.
49
8
  */
50
9
  export function runUpdate(ports, options) {
51
- const home = options.home;
52
- // 1) Stage side-by-side. A failure here leaves the live install untouched.
53
10
  let staged;
54
11
  try {
55
12
  staged = ports.stage();
@@ -66,27 +23,19 @@ export function runUpdate(ports, options) {
66
23
  let result;
67
24
  let cleanupWarning;
68
25
  try {
69
- result = runStagedUpdate(ports, staged, home);
26
+ result = runStagedUpdate(ports, staged, options.home);
70
27
  }
71
28
  finally {
72
29
  try {
73
30
  ports.cleanup(staged);
74
31
  }
75
32
  catch (error) {
76
- // Cleanup is deliberately best-effort. Once the orchestrator has
77
- // determined an update/abort/ambiguous result, a staging I/O failure must
78
- // not replace that authoritative outcome or turn it into a generic error.
79
33
  cleanupWarning = `Staging cleanup could not be completed: ${messageOf(error)}`;
80
34
  }
81
35
  }
82
36
  return cleanupWarning === undefined ? result : { ...result, cleanupWarning };
83
37
  }
84
- /** Run the staged flow; cleanup is owned by the caller so warnings are retained. */
85
38
  function runStagedUpdate(ports, staged, home) {
86
- // 2) Preflight — the staged binary inspects the Home read-only; no switch.
87
- // A preflight port that throws unexpectedly (e.g. an I/O fault) is not a safe
88
- // green light: treat it as a blocked preflight (recoverable, no switch) rather
89
- // than letting the exception escape (R4-F1).
90
39
  let preflight;
91
40
  try {
92
41
  preflight = ports.preflight(staged, home);
@@ -96,7 +45,7 @@ function runStagedUpdate(ports, staged, home) {
96
45
  outcome: "aborted",
97
46
  phase: "preflight",
98
47
  message: `Preflight failed unexpectedly: ${messageOf(error)}`,
99
- action: "The current install and Home are unchanged. Investigate the staged binary and retry.",
48
+ action: "The current install and Home are unchanged. Inspect the staged binary and retry.",
100
49
  recoverable: true,
101
50
  version: staged.version
102
51
  };
@@ -110,9 +59,7 @@ function runStagedUpdate(ports, staged, home) {
110
59
  recoverable: true,
111
60
  version: staged.version,
112
61
  ...(preflight.blockers === undefined ? {} : { blockers: preflight.blockers }),
113
- ...(preflight.retryCommand === undefined
114
- ? {}
115
- : { retryCommand: preflight.retryCommand }),
62
+ ...(preflight.retryCommand === undefined ? {} : { retryCommand: preflight.retryCommand }),
116
63
  ...(preflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
117
64
  };
118
65
  }
@@ -125,234 +72,76 @@ function runStagedUpdate(ports, staged, home) {
125
72
  outcome: "aborted",
126
73
  phase: "coordination",
127
74
  message: `Controller handover could not be acquired: ${messageOf(error)}`,
128
- action: "Another maintenance operation owns this Home. Wait for it to finish, then retry; no Controller, binary, or storage change was made.",
75
+ action: "Wait for the other maintenance operation to finish, then retry; nothing was changed.",
129
76
  recoverable: true,
130
77
  version: staged.version
131
78
  };
132
79
  }
133
- let releaseStorageWriteFence;
134
- const migrationPath = preflight.status === "in-place-migration"
135
- || preflight.status === "migration-required"
136
- || preflight.status === "migratable";
137
- if (migrationPath && ports.beginControllerHandover !== undefined) {
138
- if (ports.beginStorageWriteFence === undefined) {
139
- releaseHandover?.();
140
- return {
141
- outcome: "aborted",
142
- phase: "coordination",
143
- message: "The update lifecycle does not provide a storage write-admission fence.",
144
- action: "Use an updater that holds write admission through storage migration, binary activation, and loader verification; no Controller, binary, or storage change was made.",
145
- recoverable: true,
146
- version: staged.version
147
- };
148
- }
149
- try {
150
- releaseStorageWriteFence = ports.beginStorageWriteFence(home);
151
- }
152
- catch (error) {
153
- releaseHandover?.();
154
- return {
155
- outcome: "aborted",
156
- phase: "coordination",
157
- message: `Storage write admission could not be acquired: ${messageOf(error)}`,
158
- action: "Another maintenance operation owns this Home. Wait for it to finish, then retry; no Controller, binary, or storage change was made.",
159
- recoverable: true,
160
- version: staged.version
161
- };
162
- }
163
- }
164
- const resumeWrites = () => {
165
- releaseStorageWriteFence?.();
166
- releaseStorageWriteFence = undefined;
167
- };
168
80
  try {
169
- return runPreflightedUpdate(ports, staged, home, preflight, resumeWrites);
81
+ const captured = captureControllerLifecycle(ports, staged.version, home);
82
+ if ("outcome" in captured)
83
+ return captured;
84
+ return activateAndVerify(ports, staged, home, captured.lifecycle);
170
85
  }
171
86
  finally {
172
- resumeWrites();
173
87
  releaseHandover?.();
174
88
  }
175
89
  }
176
- function runPreflightedUpdate(ports, staged, home, preflight, resumeWrites) {
177
- // Capture and stop the old Controller exactly once after preflight but
178
- // before either storage activation or binary promotion. This parent update
179
- // process remains the sole lifecycle owner for both binary-only and
180
- // migratable updates.
181
- const lifecycle = captureControllerLifecycle(ports, staged.version, home);
182
- if ("outcome" in lifecycle)
183
- return lifecycle;
184
- if (preflight.status === "already-current" || preflight.status === "compatible") {
185
- // Current and all-compatible chains share the lean path: no Home copy,
186
- // backup, rename, or command replay. The exact Controller handoff prevents
187
- // old/new writers from overlapping while the same staged artifact activates.
188
- return activateAndVerify(ports, staged, home, undefined, lifecycle.lifecycle, preflight.status === "compatible" ? "compatible-fast" : "current-fast", resumeWrites);
189
- }
190
- // 3) Activate storage — recoverable: atomic switch + timestamped backup. An
191
- // activation port that throws unexpectedly may have committed the switch
192
- // before failing, so its state is UNKNOWN — resolve it as ambiguous from the
193
- // durable on-disk evidence, never let the exception escape as a false clean
194
- // failure (R4-F1 / P1-2).
195
- let activation;
196
- try {
197
- activation = ports.activateStorage(staged, home);
198
- }
199
- catch (error) {
200
- return resolveAmbiguousActivation(ports, staged, home, `the activation step threw unexpectedly: ${messageOf(error)}`, preflight.status === "in-place-migration"
201
- ? { kind: "in-place", migration: preflight.sqliteMigration }
202
- : { kind: "switch" }, lifecycle.lifecycle, resumeWrites);
203
- }
204
- if (activation.status === "blocked") {
205
- const failure = {
206
- outcome: "aborted",
207
- phase: "activate-storage",
208
- message: activation.message,
209
- action: activation.action,
210
- // The engine guarantees the source Home is unchanged on a blocked/failed
211
- // migration, and the binary has not been promoted, so this is recoverable.
212
- recoverable: true,
213
- version: staged.version,
214
- ...(activation.blockers === undefined ? {} : { blockers: activation.blockers }),
215
- ...(activation.retryCommand === undefined
216
- ? {}
217
- : { retryCommand: activation.retryCommand }),
218
- ...(activation.sceneUnchanged === true ? { sceneUnchanged: true } : {})
219
- };
220
- // The child was externally quiesced by this parent. A clean pre-switch
221
- // refusal therefore restores the exact captured identity; an ambiguous
222
- // activation is handled separately and never restores blindly.
223
- resumeWrites();
224
- return restoreBeforeSwitchOrReport(ports, home, lifecycle.lifecycle, undefined, false, failure);
225
- }
226
- if (activation.status === "ambiguous") {
227
- // The activation child left no parseable receipt: the switch may or may not
228
- // have committed. Resolve the true state from the durable on-disk evidence
229
- // and report an explicit manual recovery — never a false "recoverable".
230
- return resolveAmbiguousActivation(ports, staged, home, activation.detail, preflight.status === "in-place-migration"
231
- ? { kind: "in-place", migration: preflight.sqliteMigration }
232
- : { kind: "switch" }, lifecycle.lifecycle, resumeWrites);
233
- }
234
- if (activation.status === "migrated" && !isValidBackupPath(activation.backupPath)) {
235
- // A migrated/upgraded success without a concrete backup path violates
236
- // the recoverable storage-activation contract. Resolve it through the
237
- // existing durable receipt/schema probe instead of inferring that the
238
- // Home was untouched.
239
- return resolveAmbiguousActivation(ports, staged, home, "the activation reported migrated without a non-empty absolute backupPath", { kind: "switch" }, lifecycle.lifecycle, resumeWrites);
240
- }
241
- const backupPath = activation.status === "migrated" ? activation.backupPath : undefined;
242
- const path = activation.status === "migrated-in-place"
243
- ? "in-place-migration"
244
- : "offline-migration";
245
- // 4/5) Promote the binary, then post-verify with the new binary's loader.
246
- return activateAndVerify(ports, staged, home, backupPath, lifecycle.lifecycle, path, resumeWrites);
247
- }
248
- /**
249
- * Promote the staged binary and run the new-binary health check. This is the
250
- * last, non-atomic step: after storage has switched, binary promotion cannot be
251
- * made atomic with it, so a failure here is reported with the exact backup-based
252
- * recovery and is NOT auto-reverted once the new version would resume writes.
253
- */
254
- function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, path, resumeWrites) {
255
- const inPlaceCommitted = path === "in-place-migration";
90
+ function activateAndVerify(ports, staged, home, lifecycle) {
256
91
  try {
257
92
  ports.activateBinary(staged);
258
93
  }
259
94
  catch (error) {
260
- const failure = {
95
+ return restoreControllerOrReport(ports, home, lifecycle, {
261
96
  outcome: "aborted",
262
97
  phase: "activate-binary",
263
98
  message: `Failed to activate the new binary: ${messageOf(error)}`,
264
- action: inPlaceCommitted
265
- ? inPlaceMigrationRecoveryAction(home)
266
- : storageBackupPath === undefined
267
- ? binaryActivationUncertainAction()
268
- : postSwitchRecoveryAction(home, storageBackupPath),
269
- // Once binary activation begins, its outcome is not knowable from a
270
- // failed npm process. Home-not-switched is useful evidence, but it does
271
- // not prove the current installation remains usable.
99
+ action: binaryActivationUncertainAction(),
272
100
  recoverable: false,
273
- version: staged.version,
274
- ...(storageBackupPath === undefined ? {} : { storageBackupPath })
275
- };
276
- if (!inPlaceCommitted && storageBackupPath === undefined)
277
- resumeWrites();
278
- return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, inPlaceCommitted || storageBackupPath !== undefined, failure);
101
+ version: staged.version
102
+ });
279
103
  }
280
104
  try {
281
105
  ports.verify(staged, home);
282
106
  }
283
107
  catch (error) {
284
- const failure = {
108
+ return restoreControllerOrReport(ports, home, lifecycle, {
285
109
  outcome: "aborted",
286
110
  phase: "post-verify",
287
111
  message: `Post-update health check failed: ${messageOf(error)}`,
288
- action: inPlaceCommitted
289
- ? inPlaceMigrationRecoveryAction(home)
290
- : storageBackupPath === undefined
291
- ? binaryHealthUncertainAction()
292
- : postSwitchRecoveryAction(home, storageBackupPath),
112
+ action: binaryHealthUncertainAction(),
293
113
  recoverable: false,
294
- version: staged.version,
295
- ...(storageBackupPath === undefined ? {} : { storageBackupPath })
296
- };
297
- if (!inPlaceCommitted && storageBackupPath === undefined)
298
- resumeWrites();
299
- return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, inPlaceCommitted || storageBackupPath !== undefined, failure);
114
+ version: staged.version
115
+ });
300
116
  }
301
- // The migrated database has now been opened successfully by the activated
302
- // binary. Release direct-write admission before starting the replacement
303
- // Controller, whose startup scheduler is itself an authoritative writer.
304
- resumeWrites();
305
- if (lifecycle?.ensureRunning === true) {
117
+ if (lifecycle.ensureRunning) {
306
118
  try {
307
119
  ports.startController(home);
308
120
  }
309
121
  catch (error) {
310
122
  const unknownActive = isUnknownActiveControllerFailure(error);
311
- const startFailureAction = inPlaceCommitted
312
- ? inPlaceMigrationRecoveryAction(home)
313
- : lifecycle.wasRunning
314
- ? "The Home was not migrated. Keep writes quiesced and restore the previously running Controller identity before retrying."
315
- : "The Home was not migrated. Keep writes quiesced and start the replacement Controller after verifying the activated binary.";
316
123
  const failure = {
317
124
  outcome: "aborted",
318
125
  phase: "post-verify",
319
- message: `${unknownActive ? "Replacement Controller ownership could not be authenticated safely" : "The replacement Controller could not start after activation and health verification"}: `
320
- + `${messageOf(error)}.`,
126
+ message: `${unknownActive
127
+ ? "Replacement Controller ownership could not be authenticated safely"
128
+ : "The replacement Controller could not start after activation and verification"}: ${messageOf(error)}.`,
321
129
  action: unknownActive
322
- ? unknownActiveControllerAction(home, storageBackupPath, inPlaceCommitted)
323
- : storageBackupPath === undefined
324
- ? startFailureAction
325
- : postSwitchRecoveryAction(home, storageBackupPath),
130
+ ? unknownActiveControllerAction(home)
131
+ : lifecycle.wasRunning
132
+ ? "Keep the Home quiesced and restore the captured Controller identity before retrying."
133
+ : "Verify the activated binary, then start the Controller explicitly.",
326
134
  recoverable: false,
327
135
  version: staged.version,
328
- // P1-1 (rr22): ANY failure to start or authenticate the replacement
329
- // Controller leaves the lifecycle handoff unresolved — not just
330
- // UPDATE_CONTROLLER_UNKNOWN_ACTIVE. Even when the old identity is
331
- // restored below, the running Controller is not the replacement the
332
- // step promised, so the adapter must persist the step as unknown
333
- // rather than letting a controller-home query confirm it on binary
334
- // health alone.
335
- controllerOwnershipUnknown: true,
336
- ...(storageBackupPath === undefined ? {} : { storageBackupPath })
136
+ controllerOwnershipUnknown: true
337
137
  };
338
- // An ownership-unknown mismatch is a live-process blocker, not a
339
- // recoverable pre-switch failure. Restoring the captured identity here
340
- // could overwrite or race a foreign process, so leave the Home quiesced
341
- // and preserve the explicit manual blocker.
342
138
  return unknownActive
343
139
  ? failure
344
- : restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, inPlaceCommitted || storageBackupPath !== undefined, failure);
140
+ : restoreControllerOrReport(ports, home, lifecycle, failure);
345
141
  }
346
142
  }
347
- return storageBackupPath === undefined
348
- ? { outcome: "updated", version: staged.version, path }
349
- : { outcome: "updated", version: staged.version, path, storageBackupPath };
143
+ return { outcome: "updated", version: staged.version, path: "current-fast" };
350
144
  }
351
- /**
352
- * Capture the old Controller before a binary-only update mutates the install.
353
- * A partial lifecycle port set is rejected rather than silently selecting a
354
- * staged/new `ensureFileTaskController` for restoration.
355
- */
356
145
  function captureControllerLifecycle(ports, version, home) {
357
146
  const supplied = [
358
147
  ports.controllerStatus,
@@ -361,7 +150,7 @@ function captureControllerLifecycle(ports, version, home) {
361
150
  ports.restoreController
362
151
  ].some((port) => port !== undefined);
363
152
  if (!supplied) {
364
- return { lifecycle: { ensureRunning: false, wasRunning: false, stopped: false } };
153
+ return { lifecycle: { ensureRunning: false, wasRunning: false } };
365
154
  }
366
155
  if (ports.controllerStatus === undefined
367
156
  || ports.stopController === undefined
@@ -371,7 +160,7 @@ function captureControllerLifecycle(ports, version, home) {
371
160
  outcome: "aborted",
372
161
  phase: "preflight",
373
162
  message: "Controller lifecycle ownership is incomplete for this update.",
374
- action: "Provide status, stop, replacement-start, and exact-identity restore seams; refusing to activate a binary without a complete lifecycle owner.",
163
+ action: "Provide status, exact stop, replacement start, and exact restore operations.",
375
164
  recoverable: true,
376
165
  version
377
166
  };
@@ -385,43 +174,19 @@ function captureControllerLifecycle(ports, version, home) {
385
174
  outcome: "aborted",
386
175
  phase: "preflight",
387
176
  message: `Controller status could not be verified: ${messageOf(error)}`,
388
- action: "Do not activate the binary while Controller ownership is unknown; inspect the Controller and retry once status is verified.",
177
+ action: "Inspect Controller ownership and retry after its status is known.",
389
178
  recoverable: true,
390
179
  version
391
180
  };
392
181
  }
393
182
  if (!isControllerLifecycleStatus(status)) {
394
- return {
395
- outcome: "aborted",
396
- phase: "preflight",
397
- message: "Controller status was malformed; expected a boolean running field.",
398
- action: "Inspect the Controller status provider and retry; no binary activation was attempted.",
399
- recoverable: true,
400
- version
401
- };
183
+ return malformedControllerResult(version, "Controller status was malformed.");
402
184
  }
403
185
  if (!status.running) {
404
- return { lifecycle: { ensureRunning: true, wasRunning: false, stopped: false } };
405
- }
406
- if (!isPositivePid(status.pid)) {
407
- return {
408
- outcome: "aborted",
409
- phase: "preflight",
410
- message: "The Controller is running but its exact PID could not be captured.",
411
- action: "Refusing an unfenced stop; inspect Controller ownership and retry only after its PID is authenticated.",
412
- recoverable: true,
413
- version
414
- };
186
+ return { lifecycle: { ensureRunning: true, wasRunning: false } };
415
187
  }
416
- if (!isControllerIdentity(status.identity)) {
417
- return {
418
- outcome: "aborted",
419
- phase: "preflight",
420
- message: "The Controller is running but its executable/version identity could not be captured.",
421
- action: "Refusing to stop a Controller that cannot be restored exactly; provide its executable path, arguments, and version, then retry.",
422
- recoverable: true,
423
- version
424
- };
188
+ if (!isPositivePid(status.pid) || !isControllerIdentity(status.identity)) {
189
+ return malformedControllerResult(version, "The running Controller did not expose an exact PID and launch identity.");
425
190
  }
426
191
  let stopped;
427
192
  try {
@@ -432,85 +197,56 @@ function captureControllerLifecycle(ports, version, home) {
432
197
  outcome: "aborted",
433
198
  phase: "preflight",
434
199
  message: `Controller stop/drain failed: ${messageOf(error)}`,
435
- action: "The Controller may still be active or draining. Do not retry stop blindly; inspect it and retry once the Home is quiesced.",
200
+ action: "Inspect the captured Controller before retrying; binary activation was not attempted.",
436
201
  recoverable: true,
437
202
  version
438
203
  };
439
204
  }
440
- if (!isControllerStopResult(stopped)
441
- || stopped.stopped !== true
442
- || stopped.pid !== status.pid) {
443
- return {
444
- outcome: "aborted",
445
- phase: "preflight",
446
- message: `Controller stop did not confirm the captured PID ${status.pid} with stopped:true.`,
447
- action: "Keep the Home quiesced and inspect the Controller; no binary activation was attempted and no stop retry was issued.",
448
- recoverable: true,
449
- version
450
- };
205
+ if (!isControllerStopResult(stopped) || stopped.stopped !== true || stopped.pid !== status.pid) {
206
+ return malformedControllerResult(version, `Controller stop did not confirm captured PID ${status.pid}.`);
451
207
  }
452
208
  return {
453
209
  lifecycle: {
454
210
  ensureRunning: true,
455
211
  wasRunning: true,
456
- stopped: true,
457
212
  identity: status.identity
458
213
  }
459
214
  };
460
215
  }
461
- function restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, storageCommitted, failure) {
462
- // Once storage switched, the old Controller is never safe to restore. Keep
463
- // the failure structured and point at all durable recovery evidence instead.
464
- if (storageCommitted || lifecycle?.wasRunning !== true)
216
+ function malformedControllerResult(version, message) {
217
+ return {
218
+ outcome: "aborted",
219
+ phase: "preflight",
220
+ message,
221
+ action: "Refusing an unfenced Controller handoff; inspect ownership and retry.",
222
+ recoverable: true,
223
+ version
224
+ };
225
+ }
226
+ function restoreControllerOrReport(ports, home, lifecycle, failure) {
227
+ if (!lifecycle.wasRunning)
465
228
  return failure;
466
229
  try {
467
230
  ports.restoreController(home, lifecycle.identity);
468
231
  return failure;
469
232
  }
470
233
  catch (error) {
471
- // Preserve the original phase, Home-switch evidence, and binary-health
472
- // uncertainty guidance. A restore failure is an appended blocker, not a
473
- // replacement generic post-verify result.
474
234
  return {
475
235
  ...failure,
476
- message: `${failure.message} The previously running Controller identity could not be restored: `
477
- + `${messageOf(error)}.`,
478
- action: `${failure.action} The old Controller restore also failed: ${messageOf(error)}. `
479
- + "Keep the Home quiesced; inspect the restore blocker and resolve it before any bounded retry. ",
236
+ message: `${failure.message} Captured Controller restore failed: ${messageOf(error)}.`,
237
+ action: `${failure.action} Keep the Home quiesced and resolve the restore failure.`,
480
238
  recoverable: false
481
239
  };
482
240
  }
483
241
  }
484
- function postSwitchRecoveryAction(home, backupPath) {
485
- return `The storage switch is committed (backup at ${backupPath}); do not restore the old Controller. `
486
- + `Inspect the backup, receipt marker "${upgradeReceiptPath(home)}", and switch-progress marker `
487
- + `"${switchProgressPath(home)}". Verify the migrated Home, then either finish the update or `
488
- + `restore the backup explicitly with mv "${backupPath}" "${home}".`;
489
- }
490
242
  function binaryActivationUncertainAction() {
491
- return "The Home was not migrated, but binary activation began and its outcome is unknown; "
492
- + "do not assume the current install is usable. Reinstall Yui, verify `yui version` and "
493
- + "`yui doctor`, then retry `yui update` before resuming writes.";
243
+ return "Binary activation began and its result is unknown. Reinstall Yui, then verify `yui version` and `yui doctor` before resuming the Controller.";
494
244
  }
495
245
  function binaryHealthUncertainAction() {
496
- return "The Home was not migrated, but the activated binary failed health verification; do not "
497
- + "assume the current install is usable. Reinstall Yui, verify `yui version` and `yui doctor`, "
498
- + "then retry `yui update` before resuming writes.";
499
- }
500
- function inPlaceMigrationRecoveryAction(home) {
501
- return `The SQLite migration for ${home} committed in place and has no rollback backup. `
502
- + "Do not restore the old Controller; finish installing this or a newer Yui version, run `yui doctor`, then start the Controller.";
246
+ return "The activated binary failed health verification. Reinstall Yui and verify `yui version` and `yui doctor` before resuming the Controller.";
503
247
  }
504
- function unknownActiveControllerAction(home, backupPath, inPlaceCommitted) {
505
- const storageEvidence = inPlaceCommitted
506
- ? "The SQLite migration committed in place; do not restore the old Controller. "
507
- : backupPath === undefined
508
- ? "The Home was not migrated."
509
- : `The storage switch is committed (backup at ${backupPath}); do not restore the old Controller. `;
510
- return `${storageEvidence} A replacement Controller may still be active under unknown ownership. `
511
- + `Keep writes quiesced and do not claim recovery or resume writes. Inspect the authenticated `
512
- + `Controller status for ${home}, stop only the PID proven to belong to this update, then `
513
- + "verify `yui version` and `yui doctor` before a bounded retry.";
248
+ function unknownActiveControllerAction(home) {
249
+ return `A replacement Controller may be active under unknown ownership for ${home}. Keep it quiesced, authenticate its PID, and stop only that proven owner before retrying.`;
514
250
  }
515
251
  function isUnknownActiveControllerFailure(error) {
516
252
  return isRecord(error) && error.code === "UPDATE_CONTROLLER_UNKNOWN_ACTIVE";
@@ -533,166 +269,9 @@ function isControllerIdentity(value) {
533
269
  && typeof value.version === "string"
534
270
  && value.version.length > 0;
535
271
  }
536
- /**
537
- * Resolve an ambiguous storage activation (P1-2): the activation child left no
538
- * parseable receipt, so the switch may or may not have committed. Probe the
539
- * durable on-disk evidence and report an explicit manual recovery — this is
540
- * NEVER reported as recoverable-and-unchanged.
541
- *
542
- * The verdict is driven by the completion receipt first (the switch writes it the
543
- * instant it commits), then corroborated by the on-disk schema:
544
- * - receipt present -> the switch committed; point at the backup and require
545
- * the operator to verify the migrated Home before resuming (the binary was
546
- * never promoted, so the old binary fail-closes on the new schema).
547
- * - no receipt, schema already current -> most likely the switch never ran
548
- * (or fully reverted); still require an explicit re-run rather than asserting
549
- * "unchanged", because stdout was lost.
550
- * - no receipt, schema not current -> genuinely indeterminate; give the
551
- * operator the exact files to inspect.
552
- */
553
- function resolveAmbiguousActivation(ports, staged, home, detail, expected, lifecycle, resumeWrites) {
554
- let probe;
555
- try {
556
- probe = ports.probeStorage(home);
557
- }
558
- catch (error) {
559
- // Even the probe failed: report maximum uncertainty with the raw evidence.
560
- if (expected.kind === "in-place") {
561
- return {
562
- outcome: "ambiguous",
563
- phase: "activate-storage",
564
- message: `SQLite activation result is unknown (${detail}); reading its migration ledger also failed: ${messageOf(error)}.`,
565
- action: `Do not restore the old Controller or assume the migration committed. Inspect the `
566
- + `schema_migrations ledger in "${home}/yui.db" with the staged/current Yui version, `
567
- + `then re-run "yui update" only after the ledger state is known.`,
568
- version: staged.version,
569
- schemaCurrent: false,
570
- switched: false
571
- };
572
- }
573
- return {
574
- outcome: "ambiguous",
575
- phase: "activate-storage",
576
- message: `Storage activation result is unknown (${detail}); probing the Home also failed: ${messageOf(error)}.`,
577
- action: `Do NOT assume the update succeeded or was a no-op. Manually inspect ${home} and any `
578
- + `"${home}.backup-*" sibling, "${upgradeReceiptPath(home)}", and `
579
- + `"${switchProgressPath(home)}"; if a receipt exists the `
580
- + `switch committed — verify the migrated Home with "yui doctor" before resuming, otherwise `
581
- + `restore the newest backup with mv before re-running "yui update".`,
582
- version: staged.version,
583
- schemaCurrent: false,
584
- switched: false
585
- };
586
- }
587
- if (expected.kind === "in-place"
588
- && sameSqliteLedgerHead(probe.sqliteSchemaHead, expected.migration.target)) {
589
- return {
590
- outcome: "ambiguous",
591
- phase: "activate-storage",
592
- message: `The SQLite transaction committed according to its migration ledger, but the activation process did not confirm success (${detail}). The new binary was NOT promoted.`,
593
- action: "Do not restore the old Controller. Re-run `yui update` with this or a newer version; the current ledger will make the storage step a no-op, then the binary and Controller handoff can finish.",
594
- version: staged.version,
595
- schemaCurrent: probe.schemaCurrent,
596
- switched: false
597
- };
598
- }
599
- if (expected.kind === "in-place"
600
- && sameSqliteLedgerHead(probe.sqliteSchemaHead, expected.migration.current)) {
601
- const failure = {
602
- outcome: "aborted",
603
- phase: "activate-storage",
604
- message: `The SQLite activation process did not confirm success (${detail}), but the atomic migration ledger proves that no schema transaction committed.`,
605
- action: "The database remains at its previous schema. The captured Controller identity was restored when possible; retry the update after diagnosing the activation child.",
606
- recoverable: true,
607
- version: staged.version
608
- };
609
- resumeWrites();
610
- return restoreBeforeSwitchOrReport(ports, home, lifecycle, undefined, false, failure);
611
- }
612
- if (expected.kind === "in-place") {
613
- const actual = probe.sqliteSchemaHead === undefined
614
- ? "unavailable"
615
- : `${probe.sqliteSchemaHead.version}:${probe.sqliteSchemaHead.checksum}`;
616
- return {
617
- outcome: "ambiguous",
618
- phase: "activate-storage",
619
- message: `SQLite activation did not confirm a result (${detail}), and its migration ledger head `
620
- + `(${actual}) matches neither the validated source nor target boundary.`,
621
- action: `Do not restore the old Controller or resume writes. Inspect the schema_migrations ledger `
622
- + `in "${home}/yui.db" with the staged/current Yui version and resolve the unexpected `
623
- + `ledger state before re-running "yui update".`,
624
- version: staged.version,
625
- schemaCurrent: probe.schemaCurrent,
626
- switched: false
627
- };
628
- }
629
- if (probe.interrupted === true) {
630
- // A partially-applied, interrupted switch: the original was moved to the
631
- // backup and neither promotion nor rollback completed, so the Home path may
632
- // be missing. This is the strongest "restore the backup now" signal — never a
633
- // "verify the migrated Home" or a "recoverable no-op".
634
- return {
635
- outcome: "ambiguous",
636
- phase: "activate-storage",
637
- message: `Storage switch was INTERRUPTED mid-rename (${detail}); the Home may be missing and was `
638
- + `NOT left intact. The new binary was NOT promoted.`,
639
- action: `Restore the timestamped backup to recover the original Home: `
640
- + `mv "${probe.backupPath ?? "<home>.backup-*"}" "${home}". Do NOT resume writes until it is `
641
- + `restored; inspect "${switchProgressPath(home)}" for the interrupted phase, then re-run "yui update".`,
642
- version: staged.version,
643
- schemaCurrent: probe.schemaCurrent,
644
- switched: false,
645
- ...(probe.backupPath === undefined ? {} : { storageBackupPath: probe.backupPath })
646
- };
647
- }
648
- if (probe.switched) {
649
- // The switch provably committed; the binary was not promoted.
650
- return {
651
- outcome: "ambiguous",
652
- phase: "activate-storage",
653
- message: `Storage was switched (a completion receipt is present) but the activation process `
654
- + `did not confirm success (${detail}). The new binary was NOT promoted.`,
655
- action: `Verify the migrated Home before resuming writes: run "yui doctor"`
656
- + `${probe.backupPath === undefined ? "" : ` (its timestamped backup is ${probe.backupPath})`}. `
657
- + `Inspect "${upgradeReceiptPath(home)}" and "${switchProgressPath(home)}" before deciding. `
658
- + `If it is healthy, finish by re-running "yui update" to promote the binary; if not, restore `
659
- + `the backup with mv "${probe.backupPath ?? "<home>.backup-*"}" "${home}". Do NOT resume `
660
- + `writes with the old binary against the migrated Home.`,
661
- version: staged.version,
662
- schemaCurrent: probe.schemaCurrent,
663
- switched: true,
664
- ...(probe.backupPath === undefined ? {} : { storageBackupPath: probe.backupPath })
665
- };
666
- }
667
- // No receipt: the switch most likely never committed, but stdout was lost so we
668
- // cannot assert "unchanged". Require an explicit, verified re-run.
669
- return {
670
- outcome: "ambiguous",
671
- phase: "activate-storage",
672
- message: `Storage activation did not confirm a result (${detail}) and no completion receipt was found`
673
- + `${probe.schemaCurrent ? "; the Home currently loads at the current schema" : ""}.`,
674
- action: `The switch most likely did not commit, but this was not confirmed. Verify with "yui doctor" `
675
- + `and check for any "${home}.backup-*" sibling, "${upgradeReceiptPath(home)}", and `
676
- + `"${switchProgressPath(home)}" before retrying; then re-run "yui update". Do `
677
- + `NOT assume the update completed.`,
678
- version: staged.version,
679
- schemaCurrent: probe.schemaCurrent,
680
- switched: false
681
- };
272
+ function isRecord(value) {
273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
682
274
  }
683
275
  function messageOf(error) {
684
276
  return error instanceof Error ? error.message : String(error);
685
277
  }
686
- function sameSqliteLedgerHead(actual, expected) {
687
- return actual?.version === expected.version && actual.checksum === expected.checksum;
688
- }
689
- function isValidBackupPath(value) {
690
- return typeof value === "string"
691
- && value.length > 0
692
- && value.trim() === value
693
- && !value.includes("\0")
694
- && isAbsolute(value);
695
- }
696
- function isRecord(value) {
697
- return typeof value === "object" && value !== null && !Array.isArray(value);
698
- }