@zq-silk/yui 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/dist/cli/commandCatalog.js +11 -6
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +51 -14
- package/dist/commands/configCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -1
- package/dist/commands/taskCommands.js +80 -29
- package/dist/commands/taskContextCommand.js +6 -2
- package/dist/commands/taskRoleRuntimeStatus.js +31 -7
- package/dist/config/configCatalog.js +1 -1
- package/dist/controller/clientRuntime.js +38 -2
- package/dist/controller/controller.js +23 -15
- package/dist/controller/fileSchedulerStoreAdapter.js +248 -138
- package/dist/controller/runtime.js +56 -1
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/executor/agentExecutor.js +48 -46
- package/dist/executor/fileRoleLaunchPlanner.js +94 -30
- package/dist/lifecycle/exactRunTerminalization.js +68 -3
- package/dist/observability/executionAudit.js +5 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +15 -0
- package/dist/runtime/runtimeProjection.js +6 -7
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +31 -196
- package/dist/scheduler/leaderWakeupProcessor.js +39 -133
- package/dist/scheduler/roleRunStall.js +53 -17
- package/dist/storage/sqliteSchema.js +57 -24
- package/dist/storage/sqliteStore.js +23 -4
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/nextAction.js +0 -34
- package/dist/web/webSnapshot.js +3 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +7 -4
- package/skills/yui-operator/SKILL.md +4 -4
- package/skills/yui-reviewer/SKILL.md +7 -4
- 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
|
|
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
|
|
556
|
+
yui task role session switch <task-id> <role> --reason "<why this conversation cannot continue>"
|
|
556
557
|
```
|
|
557
558
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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", "
|
|
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: "
|
|
574
|
-
summary: "
|
|
575
|
-
|
|
576
|
-
|
|
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|
|
|
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", "
|
|
350
|
+
commandPath: ["task", "role", "session", "switch"],
|
|
351
351
|
selectors: [
|
|
352
|
-
{ argumentIndex:
|
|
352
|
+
{ argumentIndex: 4, entity: "task", provider: "tasks", actionTarget: true, statuses: ["active"] },
|
|
353
353
|
{
|
|
354
|
-
argumentIndex:
|
|
354
|
+
argumentIndex: 5,
|
|
355
355
|
entity: "task-role",
|
|
356
356
|
provider: "task-roles",
|
|
357
|
-
dependsOn:
|
|
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
|
-
:
|
|
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
|
-
|
|
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,
|
|
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:
|
|
198
|
-
?
|
|
199
|
-
:
|
|
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
|
-
|
|
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:
|
|
218
|
-
?
|
|
219
|
-
:
|
|
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
|
-
|
|
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 =
|
|
233
|
-
?
|
|
234
|
-
:
|
|
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 (
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|