@zq-silk/yui 0.14.2 → 0.15.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/ARCHITECTURE.md +27 -12
- package/README.md +85 -61
- package/dist/cli/commandCatalog.js +6 -6
- package/dist/cli/updateCommand.js +17 -9
- package/dist/cli/updateOrchestrator.js +81 -15
- package/dist/cli/updatePorts.js +72 -10
- package/dist/cli/upgradeCommand.js +104 -19
- package/dist/cli.js +2 -2
- package/dist/commands/agentCommands.js +13 -6
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/globalRoleCommands.js +11 -3
- package/dist/commands/roleConfiguration.js +7 -0
- package/dist/commands/roleRuntimeGuard.js +30 -0
- package/dist/commands/taskCommands.js +10 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
- package/dist/controller/runtime.js +11 -25
- package/dist/controller/runtimeLaunchCoordinator.js +9 -30
- package/dist/controller/sessionNotify.js +5 -0
- package/dist/core/controllerServer.js +5 -5
- package/dist/doctor/doctor.js +37 -14
- package/dist/executor/agentExecutor.js +8 -11
- package/dist/executor/effectiveLaunch.js +34 -17
- package/dist/executor/fileRoleLaunchPlanner.js +11 -8
- package/dist/observability/runtimeIdentity.js +48 -50
- package/dist/release/runtimeRelease.js +9 -1
- package/dist/runtime/agentHost.js +7 -0
- package/dist/runtime/codexInteractiveHost.js +191 -0
- package/dist/runtime/exactControlPlane.js +20 -29
- package/dist/runtime/structuredProviderHost.js +35 -0
- package/dist/runtime/tmuxAdapters.js +51 -9
- package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -4
- package/dist/storage/currentTaskStore.js +6 -4
- package/dist/storage/sqliteSchema.js +134 -59
- package/dist/storage/sqliteStore.js +7 -5
- package/dist/storage/storageSchema.js +92 -223
- package/dist/storage/storageVersions.js +12 -16
- package/dist/storage/upgrade/upgradeOrchestrator.js +224 -62
- package/dist/tmux/tmuxManager.js +43 -28
- package/dist/version.js +3 -3
- package/docs/task-local-identity.md +9 -9
- package/i18n/README.zh-CN.md +33 -17
- package/package.json +1 -1
- package/dist/storage/upgrade/recordVersions.js +0 -82
package/ARCHITECTURE.md
CHANGED
|
@@ -366,18 +366,33 @@ cleanup revalidates ownership and fails safely when concurrent state changes;
|
|
|
366
366
|
manual retry is the recovery boundary rather than another durable state
|
|
367
367
|
machine.
|
|
368
368
|
|
|
369
|
-
Storage
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
369
|
+
Storage has one compatibility authority: the highest contiguous, checksummed
|
|
370
|
+
version in SQLite's append-only migration ledger. The CLI exposes that release's
|
|
371
|
+
current version and minimum supported migration version. Ordinary opening and
|
|
372
|
+
Controller startup accept only the exact current version; historical decoding
|
|
373
|
+
and rewriting exist only inside `yui upgrade` and the migration phase of
|
|
374
|
+
`yui update`.
|
|
375
|
+
|
|
376
|
+
Yui 0.15.0 establishes storage version 1 as the clean single-version baseline:
|
|
377
|
+
`schema.json` has no authority, and layout/aggregate/record-version axes are
|
|
378
|
+
absent from Home metadata. Pre-0.15.0 Homes are outside the migration floor.
|
|
379
|
+
Future releases append one immutable migration per storage version and retain
|
|
380
|
+
the complete chain from version 1. A fresh Home replays that same chain, so
|
|
381
|
+
fresh and upgraded Homes converge on one current schema.
|
|
382
|
+
|
|
383
|
+
The staged `upgrade --update-preflight` / `--update-apply` machine handshake is
|
|
384
|
+
also a compatibility contract: target releases keep its established outcomes
|
|
385
|
+
and required fields, plus the parent-owned handover-lock proof, readable by
|
|
386
|
+
every updater released since version 1. Additive detail is allowed; renaming or
|
|
387
|
+
removing consumed fields would strand an otherwise valid cross-version
|
|
388
|
+
migration path.
|
|
389
|
+
|
|
390
|
+
An upgrade runs only while the Controller is quiesced, creates a consistent
|
|
391
|
+
database backup, applies all missing migrations in one transaction, and
|
|
392
|
+
validates the current record model before writes resume. A missing or changed
|
|
393
|
+
ledger row, a migration gap, a Home below the migration floor, a future Home,
|
|
394
|
+
or malformed data fails closed. Runtime code contains no dual readers,
|
|
395
|
+
historical normalizers, or independently writable compatibility state.
|
|
381
396
|
|
|
382
397
|
The Web control room is loopback-only and never receives Controller socket
|
|
383
398
|
credentials. It presents durable records and native terminal access without
|
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@ execution and continuity, but they are not competing sources of Task truth.
|
|
|
15
15
|
|
|
16
16
|
The current implementation restores the useful Role/Agent/session and CLI framework without restoring the later data-maintenance, lease, schedule, and recovery-ledger systems.
|
|
17
17
|
|
|
18
|
+
The [target architecture handbook](docs/architecture/README.md) preserves the
|
|
19
|
+
2026-09-06 design baseline for the upcoming refactor; it does not describe
|
|
20
|
+
already implemented behavior.
|
|
21
|
+
|
|
18
22
|
## Requirements
|
|
19
23
|
|
|
20
24
|
- Node.js 20.17+, 22.9+, or 24.x
|
|
@@ -72,12 +76,12 @@ export YUI_HOME=/absolute/path/to/yui-home
|
|
|
72
76
|
yui setup
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
The home contains
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
The home contains the authoritative SQLite database `yui.db`, Project Catalog
|
|
80
|
+
and knowledge, and Controller discovery files. Stable Project checkouts and
|
|
81
|
+
managed worktrees live under the configured workspace, outside Yui home.
|
|
82
|
+
Legacy `schema.json` and `state.json` files are evidence only. Runtime storage
|
|
83
|
+
accepts only the exact current contract; supported earlier storage versions
|
|
84
|
+
enter only through the explicit upgrade boundary.
|
|
81
85
|
|
|
82
86
|
Every Task-owned record family allocates a monotonically increasing local ID
|
|
83
87
|
inside its Task. Different Tasks may therefore both contain `work-item-1`,
|
|
@@ -89,43 +93,41 @@ and `task integration start`, keep their subordinate IDs local to that Task.
|
|
|
89
93
|
Candidate IDs are local to their WorkItem and carry both Task and WorkItem
|
|
90
94
|
provenance.
|
|
91
95
|
|
|
92
|
-
Yui
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
Yui has one Home storage version, recorded by the append-only SQLite migration
|
|
97
|
+
ledger. Every CLI reports both its current storage version and its minimum
|
|
98
|
+
supported migration version. Runtime admission still has only two outcomes:
|
|
99
|
+
exact current, or rejected; a Controller never migrates storage while serving
|
|
100
|
+
work. `yui upgrade --dry-run` is read-only. `yui upgrade` quiesces a running
|
|
101
|
+
Controller, creates a consistent backup, applies every missing migration in
|
|
102
|
+
one transaction, validates the current contract, and then restarts the
|
|
103
|
+
Controller when it was running before the upgrade.
|
|
97
104
|
|
|
98
105
|
`yui update` stages and pins one exact package, runs that staged binary's
|
|
99
106
|
storage preflight, stops the exact old Controller, activates the same artifact,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
applies the staged release's migration chain when required, verifies the
|
|
108
|
+
installed binary and current Home, and starts the replacement Controller. A
|
|
109
|
+
Home inside the staged release's supported range can upgrade directly across
|
|
110
|
+
multiple versions without installing intermediate releases. A newer Home, a
|
|
111
|
+
Home below the migration floor, an incomplete migration ledger, or malformed
|
|
112
|
+
data fails closed without a guessed repair.
|
|
113
|
+
|
|
114
|
+
Yui 0.15.0 establishes storage version 1 and the migration floor. Homes created
|
|
115
|
+
by earlier releases, including 0.14.2, are not on this compatibility line:
|
|
116
|
+
preserve them for inspection with their matching Yui release or initialize a
|
|
117
|
+
new Home. From 0.15.0 onward, each release retains the complete chain, so later
|
|
118
|
+
`yui update` invocations can cross versions directly.
|
|
112
119
|
|
|
113
120
|
See [Task-local identity](docs/task-local-identity.md) for the current reference
|
|
114
121
|
contract.
|
|
115
122
|
|
|
116
|
-
Schema work across Tasks is not serialized: any Task may
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
current manifest descriptor map is re-derived against the newest head, while the
|
|
125
|
-
post-baseline descriptor snapshot remains frozen. If another Task later lands a
|
|
126
|
-
record-schema change, the integrating branch must rebase, allocate the next
|
|
127
|
-
current contract without collision, and re-test to convergence. It does not
|
|
128
|
-
preserve an upgrade path for the displaced intermediate contract.
|
|
123
|
+
Schema work across Tasks is not serialized: any Task may propose the next
|
|
124
|
+
storage migration on its own isolated branch without waiting for another Task's
|
|
125
|
+
schema change to land. The later-integrating branch owns the reconciliation:
|
|
126
|
+
rebase onto the latest project head, preserve every already released migration
|
|
127
|
+
unchanged, allocate the next contiguous storage version, resolve schema and code
|
|
128
|
+
conflicts, and re-run the bounded validation. A migration may update physical
|
|
129
|
+
tables and current record payloads together; it must not introduce another
|
|
130
|
+
writable compatibility axis.
|
|
129
131
|
|
|
130
132
|
Yui provides four reusable Worker Profile definitions through
|
|
131
133
|
`yui config profile reset`; minimum setup makes each one inherit the current
|
|
@@ -601,7 +603,11 @@ future launch. Each Turn and native Role Session stores the complete actual
|
|
|
601
603
|
agent, adapter, model, effort, Profile access intent, exact writable Projects,
|
|
602
604
|
permission strategy and native options, workspace, context, and source desired revision. Updating,
|
|
603
605
|
switching, or clearing Role overrides never
|
|
604
|
-
hot-mutates an existing process.
|
|
606
|
+
hot-mutates an existing process. When the Role has a live Session,
|
|
607
|
+
`task role update`, `config role update`, and `config agent update` report that
|
|
608
|
+
Session once and require `--yes`, so the change is recorded in the knowledge
|
|
609
|
+
that it applies to the next activation; stopping the Session applies it
|
|
610
|
+
immediately instead. `task context`, Role views, Turn history,
|
|
605
611
|
Events, and Web show desired/effective revisions, Profile intent, permission, and
|
|
606
612
|
pending next-launch drift.
|
|
607
613
|
|
|
@@ -911,16 +917,19 @@ Session/Driver lifecycle facts.
|
|
|
911
917
|
Global Operator and global Role sessions remain native interactive CLIs. Codex
|
|
912
918
|
connects that TUI to the default shared App Server, so the same thread can move
|
|
913
919
|
between Yui and Desktop without transferring a rollout writer or losing its
|
|
914
|
-
Global Context entry
|
|
920
|
+
Global Context entry. A thin Host in the same pane transparently forwards the
|
|
921
|
+
native TUI's App Server connection and acknowledges its exact `thread/start`
|
|
922
|
+
or `thread/resume` response. Yui records that Thread ID before the first user
|
|
923
|
+
Turn, without depending on `notify`, scanning history, or creating a bootstrap
|
|
924
|
+
message. The attachment outlives Controller restarts but exits with the TUI:
|
|
915
925
|
|
|
916
926
|
```sh
|
|
917
927
|
yui session enter <global-role>
|
|
918
928
|
```
|
|
919
929
|
|
|
920
|
-
`yui update` accepts
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
Home.
|
|
930
|
+
`yui update` accepts either the current Home contract or any valid historical
|
|
931
|
+
contract at or above the staged CLI's minimum supported migration version.
|
|
932
|
+
Unsupported newer Homes and Homes below that floor remain untouched.
|
|
924
933
|
|
|
925
934
|
tmux fixes a pane's history capacity when that pane is created. Existing panes
|
|
926
935
|
retain their configured capacity; managed runtime output remains observable in
|
|
@@ -937,12 +946,23 @@ running, Yui asks before stopping it and switching the conversation. On a
|
|
|
937
946
|
cross-Agent switch, the saved model and effort are reused unless the user
|
|
938
947
|
explicitly chooses to update them.
|
|
939
948
|
|
|
940
|
-
|
|
949
|
+
Window existence does not imply a running Agent: `pane_dead=0` is live and
|
|
950
|
+
`pane_dead=1` is an exited, retained diagnostic pane. Status reads never delete
|
|
951
|
+
that evidence, and unreadable state is an error rather than proof of exit.
|
|
952
|
+
An explicit launch can rebuild the exact dead window; tmux refuses to replace
|
|
953
|
+
a live pane. An unidentified live Operator still cannot be overwritten.
|
|
954
|
+
|
|
955
|
+
The Role's active binding is desired state for the next launch. A
|
|
941
956
|
running Turn and its native Session continue under their immutable
|
|
942
|
-
effective snapshot even if the Role is edited or switched. Resume is
|
|
943
|
-
only when
|
|
944
|
-
|
|
945
|
-
|
|
957
|
+
effective snapshot even if the Role is edited or switched. Resume is refused
|
|
958
|
+
only when continuation is impossible: no recoverable native Session, a
|
|
959
|
+
different Agent or adapter, or a different physical workspace. Desired launch
|
|
960
|
+
configuration such as model, effort, permission, Role context, Skills, or
|
|
961
|
+
declared write scope shapes the next activation instead of ending the Session,
|
|
962
|
+
and Turn-scoped facts such as ReviewRound identity or candidate commits never
|
|
963
|
+
affect reuse. When continuation is impossible Yui starts a new Session after
|
|
964
|
+
the old process has stopped and keeps the terminal Session's immutable
|
|
965
|
+
effective snapshot in history. Managed
|
|
946
966
|
Sessions invoke the ordinary `yui` command; their Manifest and durable
|
|
947
967
|
Role/Turn fences authenticate scope while protocol and storage compatibility
|
|
948
968
|
allow a CLI package or Controller upgrade in place. Exact internal callbacks
|
|
@@ -952,8 +972,9 @@ Use `yui config role unbind <global-role> <agent-id>` or `yui task role unbind <
|
|
|
952
972
|
|
|
953
973
|
Claude session IDs are preallocated at launch. Codex discovers its native
|
|
954
974
|
thread identity from App Server responses. Managed Task Turns use structured
|
|
955
|
-
Provider observations for both CLIs. Global
|
|
956
|
-
|
|
975
|
+
Provider observations for both CLIs. Global Codex uses the native TUI's exact
|
|
976
|
+
App Server startup response and the existing Host acknowledgement. Legacy
|
|
977
|
+
global `notify` callbacks cannot register a Session or change its lifecycle.
|
|
957
978
|
|
|
958
979
|
Automated lifecycle and delivery decisions use structured Provider events or
|
|
959
980
|
supported Hook payloads, persisted identities, usage snapshots, tmux process state, receipts, and pane
|
|
@@ -1003,9 +1024,10 @@ hiding the resources that remain. Use `--all` to include discovered Yui homes.
|
|
|
1003
1024
|
`controller restart` replaces the Controller process and its scheduler/socket services with the currently installed Yui version. It can recover a lost discovery record only when the old process still matches the current UID, Controller entrypoint, physical Home, PID, and process-start identity. It does not stop or restart managed tmux/Agent sessions.
|
|
1004
1025
|
|
|
1005
1026
|
Successful `setup` and `update` commands ensure that the current Home has a
|
|
1006
|
-
running Controller, starting one when the Home was previously idle.
|
|
1007
|
-
|
|
1008
|
-
|
|
1027
|
+
running Controller, starting one when the Home was previously idle. A
|
|
1028
|
+
successful `upgrade` restores a Controller only when it stopped one for the
|
|
1029
|
+
migration. `update` starts the replacement only after migration and health
|
|
1030
|
+
checks pass.
|
|
1009
1031
|
|
|
1010
1032
|
Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. Structured Agent Driver observations, whether received from native provider events or supported Hooks, are exact-fenced before they reach the durable runtime inbox. A terminal Turn observation atomically records the exact Turn result. Durable mailboxes freeze the current batch while new signals merge into the next batch. Task-orchestration failures retain the exact Controller-owned processing batch for two bounded fast retries and later periodic recovery; a successful retry completes that batch before newer pending work is claimed. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
|
|
1011
1033
|
|
|
@@ -1097,15 +1119,17 @@ yui project reset|replace|retire|delete
|
|
|
1097
1119
|
```
|
|
1098
1120
|
|
|
1099
1121
|
`yui update` stages the newly published package side by side and asks that exact
|
|
1100
|
-
binary to
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
Homes block preflight and
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
`yui upgrade`
|
|
1107
|
-
|
|
1108
|
-
|
|
1122
|
+
binary to classify the Home. Only then does it stop the exact old Controller,
|
|
1123
|
+
activate the same concrete package version, apply the complete missing
|
|
1124
|
+
migration chain when needed, validate the actually installed binary and Home,
|
|
1125
|
+
and start the replacement Controller. Unsupported Homes block preflight and
|
|
1126
|
+
remain untouched.
|
|
1127
|
+
|
|
1128
|
+
`yui upgrade --dry-run` prints the ordered migration plan without writing.
|
|
1129
|
+
`yui upgrade` creates a timestamped SQLite backup and upgrades any valid Home
|
|
1130
|
+
from the CLI's minimum supported storage version to its current version. Yui
|
|
1131
|
+
0.15.0 is storage version 1; pre-0.15.0 Homes remain outside that migration
|
|
1132
|
+
line and are never rewritten.
|
|
1109
1133
|
|
|
1110
1134
|
Agent environment bindings store process-environment variable names, never secret values. Adapter-owned lifecycle arguments cannot be overridden through raw arguments.
|
|
1111
1135
|
|
|
@@ -118,7 +118,7 @@ const agentChildren = [
|
|
|
118
118
|
name: "update",
|
|
119
119
|
summary: "Update a configured Agent.",
|
|
120
120
|
usage: "yui config agent update <id> [--adapter <adapter>] [--command <command>] [--arg <arg> ... | --clear-args] [--env TARGET=PROCESS_NAME ... | --clear-env]",
|
|
121
|
-
options: ["--adapter", "--command", "--arg", "--clear-args", "--env", "--clear-env"],
|
|
121
|
+
options: ["--adapter", "--command", "--arg", "--clear-args", "--env", "--clear-env", "--yes"],
|
|
122
122
|
optionValues: { "--adapter": supportedAgentAdapterIds() },
|
|
123
123
|
executableOptions: ["--command"]
|
|
124
124
|
},
|
|
@@ -170,7 +170,7 @@ const roleChildren = [
|
|
|
170
170
|
summary: "Update a global Role.",
|
|
171
171
|
usage: "yui config role update <name> [profile options] [clear options]",
|
|
172
172
|
options: ["--agent", "--workspace", ...roleProfileOptions, ...roleAgentOptions,
|
|
173
|
-
...roleProfileClearOptions, ...roleAgentClearOptions],
|
|
173
|
+
...roleProfileClearOptions, ...roleAgentClearOptions, "--yes"],
|
|
174
174
|
optionValues: roleAgentOptionValues,
|
|
175
175
|
fileOptions: ["--workspace"]
|
|
176
176
|
},
|
|
@@ -578,7 +578,7 @@ const taskChildren = [
|
|
|
578
578
|
summary: "Update a Task Role; Agent settings target the named or active binding without switching it.",
|
|
579
579
|
usage: "yui task role update <task> <role> [--profile <id>] [--agent <id>] [Role and Agent settings]",
|
|
580
580
|
options: ["--profile", "--agent", ...roleProfileOptions, ...roleAgentOptions,
|
|
581
|
-
...roleProfileClearOptions, ...roleAgentClearOptions],
|
|
581
|
+
...roleProfileClearOptions, ...roleAgentClearOptions, "--yes"],
|
|
582
582
|
optionValues: roleAgentOptionValues
|
|
583
583
|
},
|
|
584
584
|
{ name: "remove", summary: "Remove a Task Role.", usage: "yui task role remove <task> <role>" },
|
|
@@ -995,7 +995,7 @@ export const ROOT_COMMAND = buildNode({
|
|
|
995
995
|
{ name: "update", summary: "Install the latest published Yui package globally." },
|
|
996
996
|
{
|
|
997
997
|
name: "upgrade",
|
|
998
|
-
summary: "
|
|
998
|
+
summary: "Plan or apply supported storage migrations for this Home.",
|
|
999
999
|
usage: "yui upgrade [--dry-run]",
|
|
1000
1000
|
options: ["--dry-run"]
|
|
1001
1001
|
},
|
|
@@ -1132,7 +1132,7 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1132
1132
|
...CONFIG_DOMAINS.map(durableConfigDomainNode),
|
|
1133
1133
|
{
|
|
1134
1134
|
name: "agent",
|
|
1135
|
-
summary: "Manage configured native Agent CLIs; launch-setting changes
|
|
1135
|
+
summary: "Manage configured native Agent CLIs; launch-setting changes apply to the next Session activation.",
|
|
1136
1136
|
examples: ["yui config agent list", "yui config agent capabilities codex"],
|
|
1137
1137
|
sections: [
|
|
1138
1138
|
{ id: "inspect", title: "Inspect", entries: ["list", "show", "capabilities"] },
|
|
@@ -1152,7 +1152,7 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1152
1152
|
},
|
|
1153
1153
|
{
|
|
1154
1154
|
name: "role",
|
|
1155
|
-
summary: "Manage reusable global Roles and desired Agent launch configuration for the next
|
|
1155
|
+
summary: "Manage reusable global Roles and desired Agent launch configuration for the next Host activation.",
|
|
1156
1156
|
examples: ["yui config role list", "yui config role show operator"],
|
|
1157
1157
|
sections: [
|
|
1158
1158
|
{ id: "inspect", title: "Inspect", entries: ["list", "show"] },
|
|
@@ -5,9 +5,9 @@ import { runUpdate } from "./updateOrchestrator.js";
|
|
|
5
5
|
* Run `yui update` as a side-by-side, recoverable orchestration.
|
|
6
6
|
*
|
|
7
7
|
* The new package is staged beside the live install and used to run a read-only
|
|
8
|
-
* preflight against the target Home.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* preflight against the target Home. A current Home proceeds directly; a Home
|
|
9
|
+
* inside the staged release's supported range is migrated after the exact
|
|
10
|
+
* Controller handoff and before post-update verification.
|
|
11
11
|
*
|
|
12
12
|
* Returns a process exit code: 0 on success or already-current, 5 on abort.
|
|
13
13
|
*/
|
|
@@ -27,15 +27,23 @@ export function renderUpdateResult(result) {
|
|
|
27
27
|
case "already-current":
|
|
28
28
|
return "Yui is already up to date; nothing to install.";
|
|
29
29
|
case "updated":
|
|
30
|
-
return
|
|
30
|
+
return result.backupPath === undefined
|
|
31
|
+
? `Updated Yui to ${result.version}; storage was already current.`
|
|
32
|
+
: `Updated Yui to ${result.version} and migrated storage. Backup: ${result.backupPath}`;
|
|
31
33
|
case "aborted":
|
|
32
34
|
return [
|
|
33
35
|
`Update aborted during ${result.phase}: ${result.message}`,
|
|
34
|
-
result.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
result.phase === "migrate-storage"
|
|
37
|
+
|| (result.phase === "post-verify" && result.recoverable)
|
|
38
|
+
? "The target binary is installed; the Home remains quiesced pending successful verification."
|
|
39
|
+
: result.recoverable
|
|
40
|
+
? "The current install and Home remain usable."
|
|
41
|
+
: result.phase === "activate-binary"
|
|
42
|
+
? "The Home was unchanged, but binary health is unknown; do not assume the current install is usable."
|
|
43
|
+
: "Manual recovery is required (see below).",
|
|
44
|
+
...(result.backupPath === undefined
|
|
45
|
+
? []
|
|
46
|
+
: [`Storage backup: ${result.backupPath}`]),
|
|
39
47
|
`Action: ${result.action}`
|
|
40
48
|
].join("\n");
|
|
41
49
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Side-by-side `yui update` orchestration for the
|
|
2
|
+
* Side-by-side `yui update` orchestration for the supported storage range.
|
|
3
3
|
*
|
|
4
|
-
* The staged binary proves an exact-current Home
|
|
5
|
-
*
|
|
4
|
+
* The staged binary proves either an exact-current Home or a complete
|
|
5
|
+
* migration path before the exact Controller is stopped. The replacement is
|
|
6
|
+
* then activated, any required storage migration is applied, and the result is
|
|
7
|
+
* verified before the Controller is restarted.
|
|
6
8
|
*/
|
|
7
9
|
export function runUpdate(ports, options) {
|
|
8
10
|
let staged;
|
|
@@ -61,6 +63,26 @@ function runStagedUpdate(ports, staged, home) {
|
|
|
61
63
|
...(preflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
62
64
|
};
|
|
63
65
|
}
|
|
66
|
+
if (preflight.status === "migration-ready" && ports.migrateStorage === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
outcome: "aborted",
|
|
69
|
+
phase: "preflight",
|
|
70
|
+
message: "The staged binary requires a storage migration, but no migration operation is available.",
|
|
71
|
+
action: "Use the complete Yui updater or run the staged release's `yui upgrade` explicitly.",
|
|
72
|
+
recoverable: true,
|
|
73
|
+
version: staged.version
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (preflight.status === "migration-ready" && !hasCompleteControllerLifecycle(ports)) {
|
|
77
|
+
return {
|
|
78
|
+
outcome: "aborted",
|
|
79
|
+
phase: "preflight",
|
|
80
|
+
message: "Storage migration requires a complete, fenced Controller handoff.",
|
|
81
|
+
action: "Provide status, exact stop, replacement start, and exact restore operations.",
|
|
82
|
+
recoverable: true,
|
|
83
|
+
version: staged.version
|
|
84
|
+
};
|
|
85
|
+
}
|
|
64
86
|
let releaseHandover;
|
|
65
87
|
try {
|
|
66
88
|
releaseHandover = ports.beginControllerHandover?.(home);
|
|
@@ -79,13 +101,13 @@ function runStagedUpdate(ports, staged, home) {
|
|
|
79
101
|
const captured = captureControllerLifecycle(ports, staged.version, home);
|
|
80
102
|
if ("outcome" in captured)
|
|
81
103
|
return captured;
|
|
82
|
-
return activateAndVerify(ports, staged, home, captured.lifecycle);
|
|
104
|
+
return activateAndVerify(ports, staged, home, captured.lifecycle, preflight);
|
|
83
105
|
}
|
|
84
106
|
finally {
|
|
85
107
|
releaseHandover?.();
|
|
86
108
|
}
|
|
87
109
|
}
|
|
88
|
-
function activateAndVerify(ports, staged, home, lifecycle) {
|
|
110
|
+
function activateAndVerify(ports, staged, home, lifecycle, preflight) {
|
|
89
111
|
try {
|
|
90
112
|
ports.activateBinary(staged);
|
|
91
113
|
}
|
|
@@ -99,19 +121,46 @@ function activateAndVerify(ports, staged, home, lifecycle) {
|
|
|
99
121
|
version: staged.version
|
|
100
122
|
});
|
|
101
123
|
}
|
|
124
|
+
let migration;
|
|
125
|
+
if (preflight.status === "migration-ready") {
|
|
126
|
+
try {
|
|
127
|
+
migration = ports.migrateStorage(staged, home);
|
|
128
|
+
if (!isStorageMigrationResult(migration)) {
|
|
129
|
+
throw new Error("Storage migration returned a malformed result.");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
outcome: "aborted",
|
|
135
|
+
phase: "migrate-storage",
|
|
136
|
+
message: `Storage migration failed: ${messageOf(error)}`,
|
|
137
|
+
action: "The target Yui binary is installed and the Home remains quiesced. "
|
|
138
|
+
+ "Resolve the reported problem, rerun `yui upgrade`, then verify "
|
|
139
|
+
+ "`yui doctor` before starting the Controller.",
|
|
140
|
+
recoverable: true,
|
|
141
|
+
version: staged.version
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
102
145
|
try {
|
|
103
146
|
ports.verify(staged, home);
|
|
104
147
|
}
|
|
105
148
|
catch (error) {
|
|
149
|
+
const storageMigrated = preflight.status === "migration-ready";
|
|
106
150
|
const failure = {
|
|
107
151
|
outcome: "aborted",
|
|
108
152
|
phase: "post-verify",
|
|
109
153
|
message: `Post-update health check failed: ${messageOf(error)}`,
|
|
110
154
|
action: binaryHealthUncertainAction(),
|
|
111
|
-
recoverable:
|
|
112
|
-
version: staged.version
|
|
155
|
+
recoverable: storageMigrated,
|
|
156
|
+
version: staged.version,
|
|
157
|
+
...(migration?.backupPath === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { backupPath: migration.backupPath })
|
|
113
160
|
};
|
|
114
|
-
return
|
|
161
|
+
return storageMigrated
|
|
162
|
+
? failure
|
|
163
|
+
: restoreControllerOrReport(ports, home, lifecycle, failure);
|
|
115
164
|
}
|
|
116
165
|
if (lifecycle.ensureRunning) {
|
|
117
166
|
try {
|
|
@@ -119,6 +168,7 @@ function activateAndVerify(ports, staged, home, lifecycle) {
|
|
|
119
168
|
}
|
|
120
169
|
catch (error) {
|
|
121
170
|
const unknownActive = isUnknownActiveControllerFailure(error);
|
|
171
|
+
const storageMigrated = preflight.status === "migration-ready";
|
|
122
172
|
const failure = {
|
|
123
173
|
outcome: "aborted",
|
|
124
174
|
phase: "post-verify",
|
|
@@ -127,21 +177,27 @@ function activateAndVerify(ports, staged, home, lifecycle) {
|
|
|
127
177
|
: "The replacement Controller could not start after activation and verification"}: ${messageOf(error)}.`,
|
|
128
178
|
action: unknownActive
|
|
129
179
|
? unknownActiveControllerAction(home)
|
|
130
|
-
:
|
|
131
|
-
? "Keep the Home quiesced
|
|
132
|
-
:
|
|
133
|
-
|
|
180
|
+
: storageMigrated
|
|
181
|
+
? "Keep the Home quiesced, verify the activated binary, then start the current Controller explicitly."
|
|
182
|
+
: lifecycle.wasRunning
|
|
183
|
+
? "Keep the Home quiesced and restore the captured Controller identity before retrying."
|
|
184
|
+
: "Verify the activated binary, then start the Controller explicitly.",
|
|
185
|
+
recoverable: storageMigrated && !unknownActive,
|
|
134
186
|
version: staged.version,
|
|
135
|
-
|
|
187
|
+
...(migration?.backupPath === undefined
|
|
188
|
+
? {}
|
|
189
|
+
: { backupPath: migration.backupPath }),
|
|
190
|
+
...(unknownActive ? { controllerOwnershipUnknown: true } : {})
|
|
136
191
|
};
|
|
137
|
-
return unknownActive
|
|
192
|
+
return unknownActive || storageMigrated
|
|
138
193
|
? failure
|
|
139
194
|
: restoreControllerOrReport(ports, home, lifecycle, failure);
|
|
140
195
|
}
|
|
141
196
|
}
|
|
142
197
|
return {
|
|
143
198
|
outcome: "updated",
|
|
144
|
-
version: staged.version
|
|
199
|
+
version: staged.version,
|
|
200
|
+
...(migration?.backupPath === undefined ? {} : { backupPath: migration.backupPath })
|
|
145
201
|
};
|
|
146
202
|
}
|
|
147
203
|
function captureControllerLifecycle(ports, version, home) {
|
|
@@ -259,6 +315,16 @@ function isControllerLifecycleStatus(value) {
|
|
|
259
315
|
function isControllerStopResult(value) {
|
|
260
316
|
return isRecord(value) && typeof value.stopped === "boolean";
|
|
261
317
|
}
|
|
318
|
+
function hasCompleteControllerLifecycle(ports) {
|
|
319
|
+
return ports.controllerStatus !== undefined
|
|
320
|
+
&& ports.stopController !== undefined
|
|
321
|
+
&& ports.startController !== undefined
|
|
322
|
+
&& ports.restoreController !== undefined;
|
|
323
|
+
}
|
|
324
|
+
function isStorageMigrationResult(value) {
|
|
325
|
+
return isRecord(value)
|
|
326
|
+
&& (value.backupPath === undefined || typeof value.backupPath === "string");
|
|
327
|
+
}
|
|
262
328
|
function isPositivePid(value) {
|
|
263
329
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
264
330
|
}
|