@zq-silk/yui 0.12.0 → 0.12.2

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 (53) hide show
  1. package/README.md +8 -9
  2. package/dist/cli/commandCatalog.js +11 -6
  3. package/dist/cli/interactionPolicy.js +5 -6
  4. package/dist/cli/updateCommand.js +3 -1
  5. package/dist/cli/updateOrchestrator.js +173 -28
  6. package/dist/cli/updatePorts.js +137 -8
  7. package/dist/cli/upgradeCommand.js +19 -9
  8. package/dist/cli.js +51 -14
  9. package/dist/commands/configCommands.js +1 -1
  10. package/dist/commands/executionAuditCommands.js +2 -1
  11. package/dist/commands/taskCommands.js +80 -29
  12. package/dist/commands/taskContextCommand.js +6 -2
  13. package/dist/commands/taskRoleRuntimeStatus.js +31 -7
  14. package/dist/config/configCatalog.js +1 -1
  15. package/dist/controller/clientRuntime.js +38 -2
  16. package/dist/controller/controller.js +23 -15
  17. package/dist/controller/fileSchedulerStoreAdapter.js +248 -138
  18. package/dist/controller/runtime.js +56 -1
  19. package/dist/controller/runtimeHookRunFence.js +19 -4
  20. package/dist/controller/structuredProviderObservation.js +20 -3
  21. package/dist/core/controllerClient.js +20 -2
  22. package/dist/core/controllerServer.js +1 -0
  23. package/dist/executor/agentExecutor.js +48 -46
  24. package/dist/executor/fileRoleLaunchPlanner.js +94 -30
  25. package/dist/lifecycle/exactRunTerminalization.js +68 -3
  26. package/dist/observability/executionAudit.js +5 -0
  27. package/dist/release/runtimeRelease.js +20 -0
  28. package/dist/run/recoveryProjection.js +45 -6
  29. package/dist/runtime/agentHost.js +159 -85
  30. package/dist/runtime/conversationSwitch.js +277 -0
  31. package/dist/runtime/index.js +1 -1
  32. package/dist/runtime/launchBroker.js +12 -0
  33. package/dist/runtime/processExitOutbox.js +88 -0
  34. package/dist/runtime/providerRuntimeIdentity.js +29 -1
  35. package/dist/runtime/runtimeHealthPolicy.js +5 -5
  36. package/dist/runtime/runtimeObservation.js +15 -0
  37. package/dist/runtime/runtimeProjection.js +6 -7
  38. package/dist/runtime/tmuxAdapters.js +4 -1
  39. package/dist/scheduler/activeRoleRunDelivery.js +31 -196
  40. package/dist/scheduler/leaderWakeupProcessor.js +39 -133
  41. package/dist/scheduler/roleRunStall.js +53 -17
  42. package/dist/storage/sqliteSchema.js +62 -26
  43. package/dist/storage/sqliteStore.js +23 -4
  44. package/dist/storage/upgrade/homeClassification.js +52 -0
  45. package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
  46. package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
  47. package/dist/task/nextAction.js +0 -34
  48. package/dist/web/webSnapshot.js +3 -1
  49. package/package.json +1 -1
  50. package/skills/yui-leader/SKILL.md +7 -4
  51. package/skills/yui-operator/SKILL.md +4 -4
  52. package/skills/yui-reviewer/SKILL.md +7 -4
  53. package/dist/lifecycle/taskRoleSessionReset.js +0 -118
package/README.md CHANGED
@@ -549,19 +549,18 @@ yui operator new
549
549
  yui operator enter
550
550
  ```
551
551
 
552
- When a Task Role's current native Session cannot continue, reset it by intent:
552
+ When a Task Role's current Provider Conversation cannot continue, request a
553
+ bounded switch with an audited reason:
553
554
 
554
555
  ```sh
555
- yui task role reset <task-id> <role> --reason "<why this generation cannot continue>"
556
+ yui task role session switch <task-id> <role> --reason "<why this conversation cannot continue>"
556
557
  ```
557
558
 
558
- Yui derives the current Run, Agent, launch, receipt, and native Session from its
559
- own records. It fails only that exact active Run (and its execution WorkItem),
560
- stores the current Session as broken history, and asks the Controller to stop
561
- only the Role-owned runtime. The command never creates a Candidate, accepts
562
- work, or completes the Task. While cleanup is pending, `task role status` and
563
- `task context` block a fresh launch. Existing messages, reviews, and delivery
564
- history remain durable.
559
+ The command records intent only. It does not fail active work, stop a live
560
+ runtime, or forget the current Conversation. Once the Role has ready work and
561
+ the current writer/Turn is settled, Yui creates and binds the replacement; a
562
+ failure before that bind leaves the old Conversation authoritative. Existing
563
+ messages, reviews, and delivery history remain durable.
565
564
 
566
565
  Without `--task`, `operator submit` creates a new Draft. Drafts accept planning changes but must be activated before Agent execution.
567
566
  Operator resolves every request against the Project catalog and existing Task
@@ -540,7 +540,7 @@ const taskChildren = [
540
540
  name: "role",
541
541
  summary: "Manage Roles within a Task.",
542
542
  sections: [{ id: "manage", title: "Commands", entries: [
543
- "add", "list", "status", "show", "update", "remove", "bind", "unbind", "reset",
543
+ "add", "list", "status", "show", "update", "remove", "bind", "unbind", "session",
544
544
  "view", "takeover", "release"
545
545
  ] }],
546
546
  children: [
@@ -570,10 +570,15 @@ const taskChildren = [
570
570
  { name: "bind", summary: "Bind and activate an Agent for a Task Role.", usage: "yui task role bind <task> <role> <agent-id>" },
571
571
  { name: "unbind", summary: "Unbind a dormant Agent from a Task Role.", usage: "yui task role unbind <task> <role> <agent-id>" },
572
572
  {
573
- name: "reset",
574
- summary: "Fail current work, forget the native Session, and request verified cleanup.",
575
- usage: "yui task role reset <task> <role> --reason <text>",
576
- options: ["--reason"]
573
+ name: "session",
574
+ summary: "Manage the Provider Conversation bound to a Task Role.",
575
+ sections: [{ id: "manage", title: "Commands", entries: ["switch"] }],
576
+ children: [{
577
+ name: "switch",
578
+ summary: "Request a fresh Conversation after the current one reaches a safe boundary.",
579
+ usage: "yui task role session switch <task> <role> --reason <text>",
580
+ options: ["--reason"]
581
+ }]
577
582
  },
578
583
  {
579
584
  name: "view",
@@ -736,7 +741,7 @@ const taskChildren = [
736
741
  {
737
742
  name: "recover",
738
743
  summary: "Record one exact Leader-controlled Run recovery decision.",
739
- usage: "yui task run recover <task>/<run> --action <diagnose|retry|replace-session|terminate> (--expected-progress-at <timestamp>|--from-next-action <fingerprint>) --provider-acceptance <accepted|rejected|ambiguous> --reason <text>",
744
+ usage: "yui task run recover <task>/<run> --action <diagnose|retry|terminate> (--expected-progress-at <timestamp>|--from-next-action <fingerprint>) --provider-acceptance <accepted|rejected|ambiguous> --reason <text>",
740
745
  options: ["--action", "--expected-progress-at", "--progress-at", "--from-next-action", "--provider-acceptance", "--reason", "--role", "--agent-id", "--adapter-id", "--native-session-id", "--launch-id"]
741
746
  },
742
747
  {
@@ -347,19 +347,18 @@ export const INTERACTION_POLICIES = Object.freeze([
347
347
  ]
348
348
  })),
349
349
  {
350
- commandPath: ["task", "role", "reset"],
350
+ commandPath: ["task", "role", "session", "switch"],
351
351
  selectors: [
352
- { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true, statuses: ["active"] },
352
+ { argumentIndex: 4, entity: "task", provider: "tasks", actionTarget: true, statuses: ["active"] },
353
353
  {
354
- argumentIndex: 4,
354
+ argumentIndex: 5,
355
355
  entity: "task-role",
356
356
  provider: "task-roles",
357
- dependsOn: 3,
357
+ dependsOn: 4,
358
358
  actionTarget: true
359
359
  }
360
360
  ],
361
- trailingOptions: { "--reason": "value" },
362
- confirmation: { action: "Reset Task Role Session", targetArgumentIndex: 4 }
361
+ trailingOptions: { "--reason": "value" }
363
362
  },
364
363
  ...["view", "takeover", "release"].map((command) => ({
365
364
  commandPath: ["task", "role", command],
@@ -38,7 +38,9 @@ export function renderUpdateResult(result) {
38
38
  return result.storageBackupPath === undefined
39
39
  ? result.path === "compatible-fast"
40
40
  ? `Updated Yui to ${result.version} via the compatible fast path; the Home was not migrated.`
41
- : `Updated Yui to ${result.version}; storage was already current.`
41
+ : result.path === "in-place-migration"
42
+ ? `Updated Yui to ${result.version}; SQLite was migrated in place without rebuilding the database.`
43
+ : `Updated Yui to ${result.version}; storage was already current.`
42
44
  : `Updated Yui to ${result.version}. Storage was migrated; original Home backed up at `
43
45
  + `${result.storageBackupPath}.`;
44
46
  case "aborted":
@@ -116,6 +116,64 @@ function runStagedUpdate(ports, staged, home) {
116
116
  ...(preflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
117
117
  };
118
118
  }
119
+ let releaseHandover;
120
+ try {
121
+ releaseHandover = ports.beginControllerHandover?.(home);
122
+ }
123
+ catch (error) {
124
+ return {
125
+ outcome: "aborted",
126
+ phase: "coordination",
127
+ 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.",
129
+ recoverable: true,
130
+ version: staged.version
131
+ };
132
+ }
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
+ try {
169
+ return runPreflightedUpdate(ports, staged, home, preflight, resumeWrites);
170
+ }
171
+ finally {
172
+ resumeWrites();
173
+ releaseHandover?.();
174
+ }
175
+ }
176
+ function runPreflightedUpdate(ports, staged, home, preflight, resumeWrites) {
119
177
  // Capture and stop the old Controller exactly once after preflight but
120
178
  // before either storage activation or binary promotion. This parent update
121
179
  // process remains the sole lifecycle owner for both binary-only and
@@ -127,7 +185,7 @@ function runStagedUpdate(ports, staged, home) {
127
185
  // Current and all-compatible chains share the lean path: no Home copy,
128
186
  // backup, rename, or command replay. The exact Controller handoff prevents
129
187
  // old/new writers from overlapping while the same staged artifact activates.
130
- return activateAndVerify(ports, staged, home, undefined, lifecycle.lifecycle, preflight.status === "compatible" ? "compatible-fast" : "current-fast");
188
+ return activateAndVerify(ports, staged, home, undefined, lifecycle.lifecycle, preflight.status === "compatible" ? "compatible-fast" : "current-fast", resumeWrites);
131
189
  }
132
190
  // 3) Activate storage — recoverable: atomic switch + timestamped backup. An
133
191
  // activation port that throws unexpectedly may have committed the switch
@@ -139,7 +197,9 @@ function runStagedUpdate(ports, staged, home) {
139
197
  activation = ports.activateStorage(staged, home);
140
198
  }
141
199
  catch (error) {
142
- return resolveAmbiguousActivation(ports, staged, home, `the activation step threw unexpectedly: ${messageOf(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);
143
203
  }
144
204
  if (activation.status === "blocked") {
145
205
  const failure = {
@@ -160,24 +220,30 @@ function runStagedUpdate(ports, staged, home) {
160
220
  // The child was externally quiesced by this parent. A clean pre-switch
161
221
  // refusal therefore restores the exact captured identity; an ambiguous
162
222
  // activation is handled separately and never restores blindly.
163
- return restoreBeforeSwitchOrReport(ports, home, lifecycle.lifecycle, undefined, failure);
223
+ resumeWrites();
224
+ return restoreBeforeSwitchOrReport(ports, home, lifecycle.lifecycle, undefined, false, failure);
164
225
  }
165
226
  if (activation.status === "ambiguous") {
166
227
  // The activation child left no parseable receipt: the switch may or may not
167
228
  // have committed. Resolve the true state from the durable on-disk evidence
168
229
  // and report an explicit manual recovery — never a false "recoverable".
169
- return resolveAmbiguousActivation(ports, staged, home, activation.detail);
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);
170
233
  }
171
234
  if (activation.status === "migrated" && !isValidBackupPath(activation.backupPath)) {
172
235
  // A migrated/upgraded success without a concrete backup path violates
173
236
  // the recoverable storage-activation contract. Resolve it through the
174
237
  // existing durable receipt/schema probe instead of inferring that the
175
238
  // Home was untouched.
176
- return resolveAmbiguousActivation(ports, staged, home, "the activation reported migrated without a non-empty absolute backupPath");
239
+ return resolveAmbiguousActivation(ports, staged, home, "the activation reported migrated without a non-empty absolute backupPath", { kind: "switch" }, lifecycle.lifecycle, resumeWrites);
177
240
  }
178
241
  const backupPath = activation.status === "migrated" ? activation.backupPath : undefined;
242
+ const path = activation.status === "migrated-in-place"
243
+ ? "in-place-migration"
244
+ : "offline-migration";
179
245
  // 4/5) Promote the binary, then post-verify with the new binary's loader.
180
- return activateAndVerify(ports, staged, home, backupPath, lifecycle.lifecycle, "offline-migration");
246
+ return activateAndVerify(ports, staged, home, backupPath, lifecycle.lifecycle, path, resumeWrites);
181
247
  }
182
248
  /**
183
249
  * Promote the staged binary and run the new-binary health check. This is the
@@ -185,7 +251,8 @@ function runStagedUpdate(ports, staged, home) {
185
251
  * made atomic with it, so a failure here is reported with the exact backup-based
186
252
  * recovery and is NOT auto-reverted once the new version would resume writes.
187
253
  */
188
- function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, path) {
254
+ function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, path, resumeWrites) {
255
+ const inPlaceCommitted = path === "in-place-migration";
189
256
  try {
190
257
  ports.activateBinary(staged);
191
258
  }
@@ -194,9 +261,11 @@ function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, pa
194
261
  outcome: "aborted",
195
262
  phase: "activate-binary",
196
263
  message: `Failed to activate the new binary: ${messageOf(error)}`,
197
- action: storageBackupPath === undefined
198
- ? binaryActivationUncertainAction()
199
- : postSwitchRecoveryAction(home, storageBackupPath),
264
+ action: inPlaceCommitted
265
+ ? inPlaceMigrationRecoveryAction(home)
266
+ : storageBackupPath === undefined
267
+ ? binaryActivationUncertainAction()
268
+ : postSwitchRecoveryAction(home, storageBackupPath),
200
269
  // Once binary activation begins, its outcome is not knowable from a
201
270
  // failed npm process. Home-not-switched is useful evidence, but it does
202
271
  // not prove the current installation remains usable.
@@ -204,7 +273,9 @@ function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, pa
204
273
  version: staged.version,
205
274
  ...(storageBackupPath === undefined ? {} : { storageBackupPath })
206
275
  };
207
- return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure);
276
+ if (!inPlaceCommitted && storageBackupPath === undefined)
277
+ resumeWrites();
278
+ return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, inPlaceCommitted || storageBackupPath !== undefined, failure);
208
279
  }
209
280
  try {
210
281
  ports.verify(staged, home);
@@ -214,31 +285,41 @@ function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, pa
214
285
  outcome: "aborted",
215
286
  phase: "post-verify",
216
287
  message: `Post-update health check failed: ${messageOf(error)}`,
217
- action: storageBackupPath === undefined
218
- ? binaryHealthUncertainAction()
219
- : postSwitchRecoveryAction(home, storageBackupPath),
288
+ action: inPlaceCommitted
289
+ ? inPlaceMigrationRecoveryAction(home)
290
+ : storageBackupPath === undefined
291
+ ? binaryHealthUncertainAction()
292
+ : postSwitchRecoveryAction(home, storageBackupPath),
220
293
  recoverable: false,
221
294
  version: staged.version,
222
295
  ...(storageBackupPath === undefined ? {} : { storageBackupPath })
223
296
  };
224
- return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure);
225
- }
297
+ if (!inPlaceCommitted && storageBackupPath === undefined)
298
+ resumeWrites();
299
+ return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, inPlaceCommitted || storageBackupPath !== undefined, failure);
300
+ }
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();
226
305
  if (lifecycle?.ensureRunning === true) {
227
306
  try {
228
307
  ports.startController(home);
229
308
  }
230
309
  catch (error) {
231
310
  const unknownActive = isUnknownActiveControllerFailure(error);
232
- const startFailureAction = lifecycle.wasRunning
233
- ? "The Home was not migrated. Keep writes quiesced and restore the previously running Controller identity before retrying."
234
- : "The Home was not migrated. Keep writes quiesced and start the replacement Controller after verifying the activated binary.";
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.";
235
316
  const failure = {
236
317
  outcome: "aborted",
237
318
  phase: "post-verify",
238
319
  message: `${unknownActive ? "Replacement Controller ownership could not be authenticated safely" : "The replacement Controller could not start after activation and health verification"}: `
239
320
  + `${messageOf(error)}.`,
240
321
  action: unknownActive
241
- ? unknownActiveControllerAction(home, storageBackupPath)
322
+ ? unknownActiveControllerAction(home, storageBackupPath, inPlaceCommitted)
242
323
  : storageBackupPath === undefined
243
324
  ? startFailureAction
244
325
  : postSwitchRecoveryAction(home, storageBackupPath),
@@ -260,7 +341,7 @@ function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, pa
260
341
  // and preserve the explicit manual blocker.
261
342
  return unknownActive
262
343
  ? failure
263
- : restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure);
344
+ : restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, inPlaceCommitted || storageBackupPath !== undefined, failure);
264
345
  }
265
346
  }
266
347
  return storageBackupPath === undefined
@@ -377,10 +458,10 @@ function captureControllerLifecycle(ports, version, home) {
377
458
  }
378
459
  };
379
460
  }
380
- function restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure) {
461
+ function restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, storageCommitted, failure) {
381
462
  // Once storage switched, the old Controller is never safe to restore. Keep
382
463
  // the failure structured and point at all durable recovery evidence instead.
383
- if (storageBackupPath !== undefined || lifecycle?.wasRunning !== true)
464
+ if (storageCommitted || lifecycle?.wasRunning !== true)
384
465
  return failure;
385
466
  try {
386
467
  ports.restoreController(home, lifecycle.identity);
@@ -416,10 +497,16 @@ function binaryHealthUncertainAction() {
416
497
  + "assume the current install is usable. Reinstall Yui, verify `yui version` and `yui doctor`, "
417
498
  + "then retry `yui update` before resuming writes.";
418
499
  }
419
- function unknownActiveControllerAction(home, backupPath) {
420
- const storageEvidence = backupPath === undefined
421
- ? "The Home was not migrated."
422
- : `The storage switch is committed (backup at ${backupPath}); do not restore the old Controller. `;
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.";
503
+ }
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. `;
423
510
  return `${storageEvidence} A replacement Controller may still be active under unknown ownership. `
424
511
  + `Keep writes quiesced and do not claim recovery or resume writes. Inspect the authenticated `
425
512
  + `Controller status for ${home}, stop only the PID proven to belong to this update, then `
@@ -463,13 +550,26 @@ function isControllerIdentity(value) {
463
550
  * - no receipt, schema not current -> genuinely indeterminate; give the
464
551
  * operator the exact files to inspect.
465
552
  */
466
- function resolveAmbiguousActivation(ports, staged, home, detail) {
553
+ function resolveAmbiguousActivation(ports, staged, home, detail, expected, lifecycle, resumeWrites) {
467
554
  let probe;
468
555
  try {
469
556
  probe = ports.probeStorage(home);
470
557
  }
471
558
  catch (error) {
472
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
+ }
473
573
  return {
474
574
  outcome: "ambiguous",
475
575
  phase: "activate-storage",
@@ -484,6 +584,48 @@ function resolveAmbiguousActivation(ports, staged, home, detail) {
484
584
  switched: false
485
585
  };
486
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
+ }
487
629
  if (probe.interrupted === true) {
488
630
  // A partially-applied, interrupted switch: the original was moved to the
489
631
  // backup and neither promotion nor rollback completed, so the Home path may
@@ -541,6 +683,9 @@ function resolveAmbiguousActivation(ports, staged, home, detail) {
541
683
  function messageOf(error) {
542
684
  return error instanceof Error ? error.message : String(error);
543
685
  }
686
+ function sameSqliteLedgerHead(actual, expected) {
687
+ return actual?.version === expected.version && actual.checksum === expected.checksum;
688
+ }
544
689
  function isValidBackupPath(value) {
545
690
  return typeof value === "string"
546
691
  && value.length > 0