@zq-silk/yui 0.11.3 → 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/ARCHITECTURE.md +23 -4
- package/README.md +44 -22
- package/dist/cli/commandCatalog.js +13 -10
- 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 +156 -57
- package/dist/commands/configCommands.js +13 -46
- package/dist/commands/executionAuditCommands.js +3 -2
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/taskCommands.js +234 -200
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +61 -13
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +55 -7
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +3 -3
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +39 -3
- package/dist/controller/controller.js +31 -27
- package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
- package/dist/controller/runtime.js +58 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +52 -50
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +129 -45
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +69 -5
- package/dist/observability/executionAudit.js +5 -0
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +2 -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 +28 -0
- package/dist/runtime/runtimeProjection.js +22 -32
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +34 -199
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +42 -138
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +53 -31
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +379 -0
- package/dist/storage/sqliteSchema.js +66 -23
- package/dist/storage/sqliteStore.js +46 -23
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -58
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +60 -20
- package/skills/yui-operator/SKILL.md +13 -4
- package/skills/yui-reviewer/SKILL.md +35 -11
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/lifecycle/taskRoleSessionReset.js +0 -126
- package/dist/scheduler/operatorNotification.js +0 -59
package/ARCHITECTURE.md
CHANGED
|
@@ -19,6 +19,16 @@ units. For a feature, the Leader judges whether the whole result is small
|
|
|
19
19
|
enough to own in the same way or large enough to need independently owned
|
|
20
20
|
delivery units.
|
|
21
21
|
|
|
22
|
+
Operator updates have one durable path. Domain and runtime transitions first
|
|
23
|
+
append an immutable TaskEvent; user-owned questions append an InputRequest.
|
|
24
|
+
Only those record references enter the global Operator mailbox. The Controller
|
|
25
|
+
batches pending references into one receipt-backed synthetic user message for
|
|
26
|
+
the existing interactive Operator and defers the complete batch while that
|
|
27
|
+
Operator is busy or unavailable. The message carries CLI read pointers rather
|
|
28
|
+
than copied Task or Provider narrative. No lower layer calls the Operator, no
|
|
29
|
+
mutable notification projection duplicates the event history, and no Goal or
|
|
30
|
+
polling protocol is required to follow work.
|
|
31
|
+
|
|
22
32
|
`WorkItem` means one substantial, independently acceptable requirement with a
|
|
23
33
|
clear owner. Create multiple WorkItems only when multiple Workers can own and
|
|
24
34
|
advance those requirements independently, normally in parallel. Internal
|
|
@@ -128,10 +138,14 @@ late results until explicitly reopened.
|
|
|
128
138
|
|
|
129
139
|
Stable Project checkouts are read-only references. Task identity follows one
|
|
130
140
|
bounded outcome rather than Project count. A Task binds zero or more Projects,
|
|
131
|
-
records an independent base ref for each binding, and
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
records an independent base ref for each binding, and adopts one workspace root
|
|
142
|
+
only when it becomes active. A Draft owns planning state and Project bindings,
|
|
143
|
+
not a writable Workspace. Activation prepares physical worktrees first and then
|
|
144
|
+
commits status, workspace identity, cwd, and durable Workspace ownership in one
|
|
145
|
+
TaskStore transaction; failure discards unadopted resources and leaves the Task
|
|
146
|
+
Draft. The active Workspace contains a managed main worktree for each binding.
|
|
147
|
+
The `<workspace>/tasks/<task>/main` root is a logical multi-Project container,
|
|
148
|
+
not a Git repository. Each Project child (for example
|
|
135
149
|
`<workspace>/tasks/<task>/main/yui`) is the supported Git cwd and points to
|
|
136
150
|
`<workspace>/worktree/<project>/<task>/main`; Git commands run in that child.
|
|
137
151
|
For a single-Project workspace, the native Agent starts in that Project's
|
|
@@ -142,6 +156,11 @@ additional-directory mechanism. The active Leader may append a Project when the
|
|
|
142
156
|
same outcome expands; replacing an existing binding is not a scope-repair
|
|
143
157
|
mechanism.
|
|
144
158
|
|
|
159
|
+
Before a Role launch, Yui verifies that every physical Project HEAD still
|
|
160
|
+
descends from its Workspace's recorded base. Normal committed progress is
|
|
161
|
+
allowed; a reset or repoint outside that lineage is reported as
|
|
162
|
+
`physical-drift` and fails closed before Provider launch.
|
|
163
|
+
|
|
145
164
|
A WorkItem can read the full Task workspace but has an explicit Project write
|
|
146
165
|
scope. Isolation creates a second root with independent worktrees for writable
|
|
147
166
|
Projects and Task-main context for the rest. The managed dispatch and
|
package/README.md
CHANGED
|
@@ -274,6 +274,12 @@ yui task context <task-id>
|
|
|
274
274
|
yui task activate <task-id>
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
+
A Draft stores planning state and Project bindings only; it does not adopt a
|
|
278
|
+
writable managed Workspace. `task activate` prepares every bound Project first,
|
|
279
|
+
then commits the Task's `active` status and Task-owned Workspace together. A
|
|
280
|
+
preparation or consistency failure leaves the Task Draft and reports the
|
|
281
|
+
workspace diagnosis instead of exposing a partially adopted execution root.
|
|
282
|
+
|
|
277
283
|
Task type describes intent rather than selecting an execution protocol.
|
|
278
284
|
Software Projects use `bugfix` or `feature`: a bugfix is Leader-owned; if it
|
|
279
285
|
grows into independently owned delivery requirements, reclassify it as a
|
|
@@ -390,14 +396,16 @@ acceptance so the Leader can accept it directly or run
|
|
|
390
396
|
Leader-managed candidates awaiting a decision instead of marking them done.
|
|
391
397
|
`final` does not create WorkItem ReviewRounds or decide Task topology. The
|
|
392
398
|
Leader explicitly requests a Task-scoped Review, unless an immutable Task
|
|
393
|
-
contract requires one. The Round
|
|
394
|
-
Leader-owned Task with no WorkItem can be reviewed
|
|
395
|
-
|
|
399
|
+
contract requires one. The Round snapshots the exact Task-main Project heads
|
|
400
|
+
directly, so even a Leader-owned Task with no WorkItem can be reviewed without
|
|
401
|
+
locking the mutable Task workspace. A changed frozen head needs a new semantic
|
|
402
|
+
Round; the same Reviewer Session continues in its
|
|
396
403
|
stable workspace, while every Run remains bound to its exact Round and head.
|
|
397
404
|
The Reviewer follows Project Policy/Knowledge and reports reachable, material,
|
|
398
405
|
actionable findings across the complete Task.
|
|
399
|
-
A ReviewRound freezes the Candidate's exact Git commit and
|
|
400
|
-
|
|
406
|
+
A ReviewRound freezes the Candidate's exact Git commit and updates the
|
|
407
|
+
Reviewer Role's stable writable workspace to that head while recording exact
|
|
408
|
+
Round-owned workspace evidence. Its AgentRun may edit,
|
|
401
409
|
test, and optionally commit diagnostic evidence there, but never changes the
|
|
402
410
|
Candidate or Worker workspace and never creates another WorkItem, Candidate,
|
|
403
411
|
ChangeSet, or recursive review. The result wakes the Leader, who decides whether
|
|
@@ -405,6 +413,17 @@ to route evidence to the original Worker, accept, reject and redispatch that
|
|
|
405
413
|
Worker in its existing Session, review again, or request user input.
|
|
406
414
|
A failed review remains visible evidence and wakes the Leader, but does not
|
|
407
415
|
take that decision away from the Leader.
|
|
416
|
+
Task context and next-action expose direct Review facts: every frozen Project
|
|
417
|
+
commit, its relation to the current candidate, the active Run, and the Reviewer
|
|
418
|
+
workspace. A request that fails after Round creation retains the ReviewRound
|
|
419
|
+
and reports its exact reason; the Leader opens that Round and decides whether
|
|
420
|
+
to retry, inspect or clean the workspace, use another Reviewer, or continue
|
|
421
|
+
other work.
|
|
422
|
+
An active Task-final Review freezes only its own candidate; it does not prevent
|
|
423
|
+
the Leader from processing new input or advancing a later candidate. Delta
|
|
424
|
+
Recheck is always available when Yui can prove an accepted contiguous baseline
|
|
425
|
+
and exact diff. Yui does not select a mode from generic size thresholds;
|
|
426
|
+
`requires-full-review` returns to the Leader without creating another Round.
|
|
408
427
|
Candidate history, every ReviewRound, and the Leader decision remain grouped
|
|
409
428
|
under the original WorkItem. A rejected result creates a new Candidate on the
|
|
410
429
|
next dispatch while reusing the original execution Role, Session, and
|
|
@@ -530,19 +549,18 @@ yui operator new
|
|
|
530
549
|
yui operator enter
|
|
531
550
|
```
|
|
532
551
|
|
|
533
|
-
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:
|
|
534
554
|
|
|
535
555
|
```sh
|
|
536
|
-
yui task role
|
|
556
|
+
yui task role session switch <task-id> <role> --reason "<why this conversation cannot continue>"
|
|
537
557
|
```
|
|
538
558
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
`task context` block a fresh launch. Existing messages, reviews, and delivery
|
|
545
|
-
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.
|
|
546
564
|
|
|
547
565
|
Without `--task`, `operator submit` creates a new Draft. Drafts accept planning changes but must be activated before Agent execution.
|
|
548
566
|
Operator resolves every request against the Project catalog and existing Task
|
|
@@ -594,11 +612,14 @@ Every stage is a new immutable ExecutionGroup. Its ContextSnapshot and selected
|
|
|
594
612
|
parent Lane results are durable references; `retry` repeats a stage within its
|
|
595
613
|
attempt budget, while `retry` at Resolve begins the next bounded round.
|
|
596
614
|
|
|
597
|
-
Each new stage also freezes one Resource Broker contract:
|
|
598
|
-
wall-clock budgets; quorum and deadline; a
|
|
599
|
-
marginal value for more Lane spend. Omitted
|
|
600
|
-
budget and runtime-health windows; stage
|
|
601
|
-
|
|
615
|
+
Each new stage also freezes one Resource Broker contract: behavioral tool-call
|
|
616
|
+
and wall-clock budgets; a display-only token threshold; quorum and deadline; a
|
|
617
|
+
straggler window; and the minimum marginal value for more Lane spend. Omitted
|
|
618
|
+
values reuse the existing context budget and runtime-health windows; stage
|
|
619
|
+
retries share the original cumulative observations and absolute deadline.
|
|
620
|
+
Observed token totals and the configured token threshold are cost context only:
|
|
621
|
+
they never close spend, block admission, suppress scheduling, or stop a Lane.
|
|
622
|
+
Execution, Lane retry, and Reviewer-panel
|
|
602
623
|
admission all count active Lanes at Home, Task, WorkItem, Group, Provider,
|
|
603
624
|
Agent, and model scopes. Capacity pressure keeps the excess Lane durably
|
|
604
625
|
pending instead of failing the Group. Capacity release or deadline arrival
|
|
@@ -613,8 +634,9 @@ The Leader may add `--early-stop <0-100>` to an accepting Group resolution.
|
|
|
613
634
|
Yui permits it only after quorum and T5's passed Verify/Resolve evidence prove
|
|
614
635
|
sufficiency. It may skip Lanes that never started; active stragglers are
|
|
615
636
|
reported and retained, never killed automatically for cost. If evidence is
|
|
616
|
-
insufficient, budget or deadline exhaustion
|
|
617
|
-
judgment instead of turning thin evidence into
|
|
637
|
+
insufficient, behavioral tool-call/wall-clock budget or deadline exhaustion
|
|
638
|
+
blocks the stage for Leader judgment instead of turning thin evidence into
|
|
639
|
+
success.
|
|
618
640
|
|
|
619
641
|
New exploration histories also freeze the structured candidate-convergence
|
|
620
642
|
contract. Yui appends the exact stage-local JSON shape to every Lane assignment
|
|
@@ -792,7 +814,7 @@ yui task input request <task-id> --question "Which format should be the default?
|
|
|
792
814
|
|
|
793
815
|
The recommendation is shown to the user. If no answer arrives, the nearest-deadline timer wakes the Controller to atomically apply that exact choice and queue the fixed Leader session to resume. Free-text and user-required requests never auto-resolve.
|
|
794
816
|
|
|
795
|
-
`task input list` is the authoritative global open-input Inbox; add a Task ID to scope it, or `--all` to include answered and cancelled requests. The Controller
|
|
817
|
+
`task input list` is the authoritative global open-input Inbox; add a Task ID to scope it, or `--all` to include answered and cancelled requests. Task completion, retirement, Leader attention, stalls, and open input are queued to the global Operator mailbox only as immutable TaskEvent or InputRequest references. The Controller merges one pending mailbox batch into one receipt-backed `[Yui updates]` user message for an existing ready Operator; the Operator reads the referenced records through the CLI and decides what is worth presenting. A running or unavailable Operator is never started or interrupted: the whole batch remains durable and is retried after native turn completion or a later Controller pass. This path is a user message, not a tool call, and it never inspects or classifies Agent terminal text. Answers may be submitted by the user or Operator. An open request prevents unrelated pending wakes and Task completion or archival. The originating Leader may instead run `yui task input cancel <task-id> <input-id> --reason "..."`; cancellation queues that fixed Leader session to resume.
|
|
796
818
|
|
|
797
819
|
Inspect the result:
|
|
798
820
|
|
|
@@ -59,8 +59,7 @@ function durableConfigDomainNode(domain) {
|
|
|
59
59
|
const values = CONFIG_KEY_VALUES.filter(({ name }) => keys.includes(name));
|
|
60
60
|
const options = domain === "workflow"
|
|
61
61
|
? [
|
|
62
|
-
"--soft-tokens", "--hard-tokens", "--role", "--trigger", "--finding-ledger"
|
|
63
|
-
"--delta-recheck", "--delta-recheck-max-lines", "--delta-recheck-max-files"
|
|
62
|
+
"--soft-tokens", "--hard-tokens", "--role", "--trigger", "--finding-ledger"
|
|
64
63
|
]
|
|
65
64
|
: domain === "runtime"
|
|
66
65
|
? ["--quiet-after-seconds", "--diagnostic-after-seconds", "--stall-after-seconds"]
|
|
@@ -86,8 +85,7 @@ function durableConfigDomainNode(domain) {
|
|
|
86
85
|
optionValues: domain === "workflow"
|
|
87
86
|
? {
|
|
88
87
|
"--trigger": ["always", "leader", "final"],
|
|
89
|
-
"--finding-ledger": ["shadow", "enforce"]
|
|
90
|
-
"--delta-recheck": ["enabled", "disabled"]
|
|
88
|
+
"--finding-ledger": ["shadow", "enforce"]
|
|
91
89
|
}
|
|
92
90
|
: {}
|
|
93
91
|
},
|
|
@@ -542,7 +540,7 @@ const taskChildren = [
|
|
|
542
540
|
name: "role",
|
|
543
541
|
summary: "Manage Roles within a Task.",
|
|
544
542
|
sections: [{ id: "manage", title: "Commands", entries: [
|
|
545
|
-
"add", "list", "status", "show", "update", "remove", "bind", "unbind", "
|
|
543
|
+
"add", "list", "status", "show", "update", "remove", "bind", "unbind", "session",
|
|
546
544
|
"view", "takeover", "release"
|
|
547
545
|
] }],
|
|
548
546
|
children: [
|
|
@@ -572,10 +570,15 @@ const taskChildren = [
|
|
|
572
570
|
{ name: "bind", summary: "Bind and activate an Agent for a Task Role.", usage: "yui task role bind <task> <role> <agent-id>" },
|
|
573
571
|
{ name: "unbind", summary: "Unbind a dormant Agent from a Task Role.", usage: "yui task role unbind <task> <role> <agent-id>" },
|
|
574
572
|
{
|
|
575
|
-
name: "
|
|
576
|
-
summary: "
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
}]
|
|
579
582
|
},
|
|
580
583
|
{
|
|
581
584
|
name: "view",
|
|
@@ -738,7 +741,7 @@ const taskChildren = [
|
|
|
738
741
|
{
|
|
739
742
|
name: "recover",
|
|
740
743
|
summary: "Record one exact Leader-controlled Run recovery decision.",
|
|
741
|
-
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>",
|
|
742
745
|
options: ["--action", "--expected-progress-at", "--progress-at", "--from-next-action", "--provider-acceptance", "--reason", "--role", "--agent-id", "--adapter-id", "--native-session-id", "--launch-id"]
|
|
743
746
|
},
|
|
744
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
|