@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.
Files changed (44) hide show
  1. package/ARCHITECTURE.md +27 -12
  2. package/README.md +85 -61
  3. package/dist/cli/commandCatalog.js +6 -6
  4. package/dist/cli/updateCommand.js +17 -9
  5. package/dist/cli/updateOrchestrator.js +81 -15
  6. package/dist/cli/updatePorts.js +72 -10
  7. package/dist/cli/upgradeCommand.js +104 -19
  8. package/dist/cli.js +2 -2
  9. package/dist/commands/agentCommands.js +13 -6
  10. package/dist/commands/controllerCommands.js +1 -1
  11. package/dist/commands/globalRoleCommands.js +11 -3
  12. package/dist/commands/roleConfiguration.js +7 -0
  13. package/dist/commands/roleRuntimeGuard.js +30 -0
  14. package/dist/commands/taskCommands.js +10 -3
  15. package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
  16. package/dist/controller/runtime.js +11 -25
  17. package/dist/controller/runtimeLaunchCoordinator.js +9 -30
  18. package/dist/controller/sessionNotify.js +5 -0
  19. package/dist/core/controllerServer.js +5 -5
  20. package/dist/doctor/doctor.js +37 -14
  21. package/dist/executor/agentExecutor.js +8 -11
  22. package/dist/executor/effectiveLaunch.js +34 -17
  23. package/dist/executor/fileRoleLaunchPlanner.js +11 -8
  24. package/dist/observability/runtimeIdentity.js +48 -50
  25. package/dist/release/runtimeRelease.js +9 -1
  26. package/dist/runtime/agentHost.js +7 -0
  27. package/dist/runtime/codexInteractiveHost.js +191 -0
  28. package/dist/runtime/exactControlPlane.js +20 -29
  29. package/dist/runtime/structuredProviderHost.js +35 -0
  30. package/dist/runtime/tmuxAdapters.js +51 -9
  31. package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
  32. package/dist/scheduler/leaderWakeupProcessor.js +3 -4
  33. package/dist/storage/currentTaskStore.js +6 -4
  34. package/dist/storage/sqliteSchema.js +134 -59
  35. package/dist/storage/sqliteStore.js +7 -5
  36. package/dist/storage/storageSchema.js +92 -223
  37. package/dist/storage/storageVersions.js +12 -16
  38. package/dist/storage/upgrade/upgradeOrchestrator.js +224 -62
  39. package/dist/tmux/tmuxManager.js +43 -28
  40. package/dist/version.js +3 -3
  41. package/docs/task-local-identity.md +9 -9
  42. package/i18n/README.zh-CN.md +33 -17
  43. package/package.json +1 -1
  44. 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 still records layout, aggregate, and record-family versions, but this
370
- release deliberately re-baselines all three axes at the current contract. The
371
- production migration registry is empty. Ordinary opening, Controller startup,
372
- doctor, update preflight, and the storage upgrade entry point therefore accept
373
- only an exact current manifest and current record shapes. An older Home is
374
- unsupported and must not be normalized, rewritten, or switched in place.
375
-
376
- SQLite bootstrap DDL is an implementation detail for initializing a fresh Home.
377
- Its ledger must be complete on every later open; a partial or older ledger is
378
- rejected rather than advanced. This keeps one durable model for Turn,
379
- TaskRoleSessionSet, WorkMailbox, and Provider Runtime Binding and prevents an
380
- old writer or migration transform from recreating removed delivery state.
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 `schema.json`, the authoritative SQLite database `yui.db`,
76
- Project Catalog and knowledge, and Controller discovery files. Stable Project
77
- checkouts and managed worktrees live under the configured workspace, outside
78
- Yui home. Runtime storage accepts only the exact current contract; it never
79
- falls back to `state.json`, normalizes an older shape, or repairs a historical
80
- Home in place.
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 records layout, aggregate, and per-record-family versions in `schema.json`.
93
- Runtime admission has only two outcomes: exact current, or rejected; a
94
- Controller never migrates storage while serving work. `yui upgrade --dry-run`
95
- is read-only, while `yui upgrade` validates only the release's exact current
96
- contract. Older, newer, incomplete, and malformed Homes fail closed.
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
- verifies the installed binary and current Home, and starts the replacement
101
- Controller. If the staged binary does not support the current Home exactly,
102
- the update stops before activation and leaves both the Home and current
103
- installation unchanged.
104
-
105
- To retain an old Home, keep it byte-for-byte and open it only with its original
106
- Yui version for read-only inspection. For unfinished work, initialize a new
107
- Home and let the Operator create a new Task from the old Task's objective,
108
- relevant WorkItems, current repository state, and available exact Turn results.
109
- The Operator creates new identities and may consult available exact Turn
110
- results; it does not import old runtime/session state or pretend that the old
111
- Task continued.
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 advance a version axis
117
- (`layout`, `aggregate`, or a `record` family) on its own isolated branch without
118
- waiting for another Task's schema change to land. The later-integrating branch
119
- owns the reconciliation — rebasing onto the latest project head, resolving schema
120
- and code conflicts, re-advancing the schema versions and record-version-map
121
- entries the rebase requires, rebuilding and re-validating the wiring, and
122
- re-running the isolated E2E and docs. This is a deliberate scheduling trade-off
123
- that avoids cross-Task blocking, not an accident to repair ad hoc. The
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. `task context`, Role views, Turn history,
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 only the current Home contract. Unsupported older Homes
921
- remain untouched; inspect those with a compatible Yui version and let the
922
- current Operator recreate unfinished intent as new Tasks in a newly initialized
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
- The Role's active binding is desired state for the next compatible launch. A
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 allowed
943
- only when the complete effective snapshot and workspace remain compatible;
944
- otherwise Yui starts a new Session after the old process has stopped and keeps
945
- the terminal Session's immutable effective snapshot in history. Managed
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 interactive Codex sessions may
956
- still use its `notify` callback for conversation presentation.
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. `upgrade`
1007
- is read-only and does not start a Controller. `update` replaces an
1008
- already-running Controller only after the new binary passes its health checks.
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 verify the current Home. Only then does it stop the exact old
1101
- Controller, activate the same concrete package version, validate the actually
1102
- installed binary and Home, and start the replacement Controller. Unsupported
1103
- Homes block preflight and remain untouched.
1104
-
1105
- `yui upgrade --dry-run` validates an exact current Home without writing.
1106
- `yui upgrade` performs the same validation. This release provides no migration
1107
- from older aggregate contracts; use their matching Yui version or initialize a
1108
- new Home.
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: "Validate this Home against the exact current storage contract.",
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 require affected Sessions to be stopped.",
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 compatible Session.",
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. Only the exact current storage contract is
9
- * accepted; the exact Controller handoff then promotes and verifies the binary
10
- * without changing the Home. Older Homes remain untouched.
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 `Updated Yui to ${result.version}; the current Home was not modified.`;
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.recoverable
35
- ? "The current install and Home remain usable."
36
- : result.phase === "activate-binary"
37
- ? "The Home was unchanged, but binary health is unknown; do not assume the current install is usable."
38
- : "Manual recovery is required (see below).",
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 current storage contract.
2
+ * Side-by-side `yui update` orchestration for the supported storage range.
3
3
  *
4
- * The staged binary proves an exact-current Home before the exact Controller
5
- * is stopped and the replacement is activated, verified, and restarted.
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: false,
112
- version: staged.version
155
+ recoverable: storageMigrated,
156
+ version: staged.version,
157
+ ...(migration?.backupPath === undefined
158
+ ? {}
159
+ : { backupPath: migration.backupPath })
113
160
  };
114
- return restoreControllerOrReport(ports, home, lifecycle, failure);
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
- : lifecycle.wasRunning
131
- ? "Keep the Home quiesced and restore the captured Controller identity before retrying."
132
- : "Verify the activated binary, then start the Controller explicitly.",
133
- recoverable: false,
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
- controllerOwnershipUnknown: true
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
  }