@zq-silk/yui 0.13.9 → 0.14.0

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 (229) hide show
  1. package/ARCHITECTURE.md +69 -428
  2. package/README.md +108 -298
  3. package/dist/agent/managedRuntimeEnvironment.js +2 -2
  4. package/dist/cli/commandCatalog.js +39 -77
  5. package/dist/cli/interactionCandidates.js +4 -4
  6. package/dist/cli/interactionPolicy.js +7 -10
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/updateCommand.js +6 -27
  9. package/dist/cli/updateOrchestrator.js +63 -484
  10. package/dist/cli/updatePorts.js +15 -333
  11. package/dist/cli/upgradeCommand.js +8 -65
  12. package/dist/cli.js +57 -242
  13. package/dist/commands/configCommands.js +11 -62
  14. package/dist/commands/executionAuditCommands.js +23 -23
  15. package/dist/commands/globalRoleCommands.js +2 -7
  16. package/dist/commands/jobCommands.js +1 -4
  17. package/dist/commands/projectCommands.js +5 -5
  18. package/dist/commands/releaseCommands.js +3 -10
  19. package/dist/commands/resourcesCommands.js +2 -2
  20. package/dist/commands/sessionCommands.js +2 -2
  21. package/dist/commands/taskActor.js +31 -39
  22. package/dist/commands/taskChangeSetCommands.js +5 -12
  23. package/dist/commands/taskCommands.js +464 -1249
  24. package/dist/commands/taskContextCommand.js +35 -36
  25. package/dist/commands/taskExecutionCommands.js +33 -46
  26. package/dist/commands/taskInputCommands.js +20 -42
  27. package/dist/commands/taskNextActionCommand.js +9 -9
  28. package/dist/commands/taskOverviewCommand.js +56 -95
  29. package/dist/commands/taskRoleRuntimeStatus.js +77 -107
  30. package/dist/commands/taskUpstreamCommands.js +3 -3
  31. package/dist/commands/taskWorkspaceCommands.js +7 -79
  32. package/dist/commands/telemetryCommands.js +32 -124
  33. package/dist/commands/workflowCommands.js +3 -3
  34. package/dist/completion/fileCompletionManager.js +1 -1
  35. package/dist/config/configCatalog.js +4 -6
  36. package/dist/config/yuiConfig.js +6 -43
  37. package/dist/context/dispatchContext.js +14 -30
  38. package/dist/context/roleSessionContext.js +2 -2
  39. package/dist/context/sessionBootstrapManifest.js +2 -2
  40. package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
  41. package/dist/context/turnInputContract.js +204 -0
  42. package/dist/context/wakeNotification.js +2 -2
  43. package/dist/controller/agentRuntimeObserver.js +18 -18
  44. package/dist/controller/clientRuntime.js +13 -20
  45. package/dist/controller/controller.js +89 -116
  46. package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
  47. package/dist/controller/jobControl.js +17 -17
  48. package/dist/controller/resourceInventory.js +6 -6
  49. package/dist/controller/resourceInventoryLinux.js +38 -59
  50. package/dist/controller/resourceInventoryRpc.js +3 -3
  51. package/dist/controller/resourceInventoryWorker.js +1 -1
  52. package/dist/controller/runtime.js +46 -53
  53. package/dist/controller/runtimeEventInbox.js +23 -31
  54. package/dist/controller/runtimeEventProcessor.js +15 -15
  55. package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
  56. package/dist/controller/runtimeLaunchCoordinator.js +3 -3
  57. package/dist/controller/runtimeObservationHook.js +11 -8
  58. package/dist/controller/sessionNotify.js +11 -11
  59. package/dist/controller/structuredProviderObservation.js +89 -16
  60. package/dist/coordination/workMailbox.js +309 -579
  61. package/dist/coordination/workMailboxQueue.js +5 -6
  62. package/dist/core/controllerClient.js +1 -91
  63. package/dist/core/controllerServer.js +3 -6
  64. package/dist/doctor/doctor.js +51 -314
  65. package/dist/execution/candidateConvergence.js +3 -3
  66. package/dist/execution/executionGroup.js +32 -33
  67. package/dist/execution/executionHealth.js +36 -38
  68. package/dist/execution/resourceBroker.js +12 -12
  69. package/dist/executor/agentExecutor.js +30 -265
  70. package/dist/executor/effectiveLaunch.js +9 -19
  71. package/dist/executor/executorRegistry.js +43 -17
  72. package/dist/executor/fileRoleLaunchPlanner.js +56 -56
  73. package/dist/executor/launchPlan.js +3 -3
  74. package/dist/executor/workspacePreflightClassification.js +16 -16
  75. package/dist/grant/capabilityGrant.js +20 -25
  76. package/dist/input/inputRequest.js +12 -12
  77. package/dist/integration/changeSet.js +9 -13
  78. package/dist/integration/changeSetManifest.js +1 -1
  79. package/dist/integration/gitIntegrationService.js +2 -2
  80. package/dist/integration/integrationQueueService.js +5 -21
  81. package/dist/integration/overlapDiagnostics.js +2 -2
  82. package/dist/interaction/operatorPresentation.js +1 -1
  83. package/dist/job/jobRunner.js +1 -1
  84. package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
  85. package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
  86. package/dist/message/message.js +6 -6
  87. package/dist/observability/executionAudit.js +74 -86
  88. package/dist/observability/faultClassification.js +7 -24
  89. package/dist/observability/orchestrationMetrics.js +5 -5
  90. package/dist/observability/runtimeIdentity.js +34 -101
  91. package/dist/profile/agentProfile.js +1 -1
  92. package/dist/release/cliHomeReleaseFence.js +3 -3
  93. package/dist/release/releaseWorkflowEngine.js +6 -10
  94. package/dist/release/releaseWorkflowPorts.js +2 -2
  95. package/dist/release/workflowFileLock.js +1 -1
  96. package/dist/repository/checkoutSwap.js +1 -1
  97. package/dist/repository/gitWorkspace.js +0 -13
  98. package/dist/repository/project.js +1 -1
  99. package/dist/repository/projectMaintenanceLock.js +1 -2
  100. package/dist/repository/taskBaseFreshness.js +13 -13
  101. package/dist/repository/taskWorkspaceCoordinator.js +21 -25
  102. package/dist/repository/taskWorkspaceIdentity.js +8 -21
  103. package/dist/repository/taskWorkspacePreparer.js +63 -620
  104. package/dist/resources/autoResourceGc.js +3 -3
  105. package/dist/resources/liveReferences.js +1 -1
  106. package/dist/resources/resourceGc.js +1 -1
  107. package/dist/resources/sqliteResourceRegistry.js +1 -1
  108. package/dist/review/reviewDecision.js +7 -7
  109. package/dist/review/reviewFindingLedger.js +1 -1
  110. package/dist/review/reviewOutcomeClassifier.js +35 -50
  111. package/dist/review/reviewRound.js +14 -25
  112. package/dist/review/reviewerAvailability.js +4 -4
  113. package/dist/review/taskFinalReviewContractResolution.js +41 -0
  114. package/dist/role/role.js +6 -3
  115. package/dist/runtime/agentDriver.js +4 -0
  116. package/dist/runtime/agentHost.js +151 -36
  117. package/dist/runtime/builtinAgentDrivers.js +79 -2
  118. package/dist/runtime/codexAppServerRuntime.js +122 -0
  119. package/dist/runtime/continuationManager.js +3 -5
  120. package/dist/runtime/exactControlPlane.js +17 -42
  121. package/dist/runtime/firstProgressAdvisory.js +1 -1
  122. package/dist/runtime/index.js +2 -2
  123. package/dist/runtime/launchDiagnostics.js +2 -2
  124. package/dist/runtime/lifecycleReservation.js +4 -4
  125. package/dist/runtime/processExitObservation.js +1 -1
  126. package/dist/runtime/processExitOutbox.js +0 -32
  127. package/dist/runtime/promptEnvelope.js +9 -9
  128. package/dist/runtime/providerContinuation.js +2 -2
  129. package/dist/runtime/providerContinuationReconciliationService.js +2 -2
  130. package/dist/runtime/providerRuntimeIdentity.js +76 -24
  131. package/dist/runtime/recentTurnIds.js +38 -0
  132. package/dist/runtime/runtimeContinuationProjection.js +2 -2
  133. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  134. package/dist/runtime/runtimeObservation.js +58 -21
  135. package/dist/runtime/runtimeProjection.js +19 -27
  136. package/dist/runtime/sessionLaunchRequest.js +2 -2
  137. package/dist/runtime/structuredProviderHost.js +177 -12
  138. package/dist/runtime/tmuxAdapters.js +46 -11
  139. package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
  140. package/dist/scheduler/leaderWakeupProcessor.js +147 -445
  141. package/dist/scheduler/operatorEvent.js +4 -6
  142. package/dist/scheduler/resourceQueueProjection.js +71 -0
  143. package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
  144. package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
  145. package/dist/scheduler/taskExecutionProjection.js +99 -99
  146. package/dist/scheduler/taskObservabilityProjection.js +10 -10
  147. package/dist/scheduler/taskWake.js +4 -4
  148. package/dist/scheduler/wakeReason.js +6 -10
  149. package/dist/scheduler/wakeupQueue.js +0 -9
  150. package/dist/setup/setupCommand.js +2 -2
  151. package/dist/storage/currentTaskStore.js +46 -0
  152. package/dist/storage/persistenceWorker.js +1 -1
  153. package/dist/storage/sqliteSchema.js +81 -485
  154. package/dist/storage/sqliteStore.js +206 -365
  155. package/dist/storage/storageSchema.js +1 -15
  156. package/dist/storage/storageVersions.js +8 -9
  157. package/dist/storage/storeRpc.js +10 -34
  158. package/dist/storage/taskStore.js +63 -3415
  159. package/dist/storage/upgrade/recordVersions.js +55 -149
  160. package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
  161. package/dist/task/completionReadiness.js +5 -5
  162. package/dist/task/deliveryGuard.js +10 -10
  163. package/dist/task/nextAction.js +74 -77
  164. package/dist/task/task.js +2 -4
  165. package/dist/task/taskRecordReference.js +3 -3
  166. package/dist/task/taskRecordRetirement.js +1 -1
  167. package/dist/telemetry/sqliteTelemetryStore.js +53 -53
  168. package/dist/telemetry/telemetryConfig.js +14 -14
  169. package/dist/telemetry/telemetryWiring.js +8 -14
  170. package/dist/turn/turn.js +356 -0
  171. package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
  172. package/dist/verification/gateArtifactStore.js +2 -2
  173. package/dist/web/assets/client/app.js +1 -1
  174. package/dist/web/assets/client/components.js +29 -54
  175. package/dist/web/assets/client/i18n.js +17 -37
  176. package/dist/web/assets/client/view.js +37 -37
  177. package/dist/web/assets/shell.js +1 -1
  178. package/dist/web/assets/styles/cards.js +5 -5
  179. package/dist/web/assets/styles/widgets.js +1 -2
  180. package/dist/web/webSnapshot.js +65 -60
  181. package/dist/workItem/workItem.js +11 -11
  182. package/dist/workspace/workItemChangeSetManager.js +1 -1
  183. package/docs/task-local-identity.md +5 -5
  184. package/i18n/README.zh-CN.md +41 -54
  185. package/package.json +1 -1
  186. package/skills/yui-leader/SKILL.md +85 -83
  187. package/skills/yui-operator/SKILL.md +16 -16
  188. package/skills/yui-reviewer/SKILL.md +10 -10
  189. package/skills/yui-runtime/SKILL.md +17 -18
  190. package/skills/yui-worker/SKILL.md +16 -18
  191. package/dist/context/runContextContract.js +0 -162
  192. package/dist/executor/turnCompletion.js +0 -126
  193. package/dist/review/taskFinalReviewContractEvent.js +0 -1
  194. package/dist/review/taskFinalReviewContractRebind.js +0 -367
  195. package/dist/run/agentRun.js +0 -356
  196. package/dist/run/rejectedYieldAttempt.js +0 -221
  197. package/dist/run/runControlRequest.js +0 -50
  198. package/dist/run/yieldReceipt.js +0 -65
  199. package/dist/runtime/turnCompletion.js +0 -3
  200. package/dist/scheduler/actionability.js +0 -323
  201. package/dist/scheduler/activeRoleRunDelivery.js +0 -868
  202. package/dist/scheduler/activeTaskProgress.js +0 -126
  203. package/dist/storage/compatibleTaskStore.js +0 -248
  204. package/dist/storage/migration/baseline.js +0 -78
  205. package/dist/storage/migration/classifier.js +0 -51
  206. package/dist/storage/migration/compatibleCodec.js +0 -53
  207. package/dist/storage/migration/engine.js +0 -147
  208. package/dist/storage/migration/index.js +0 -33
  209. package/dist/storage/migration/planner.js +0 -154
  210. package/dist/storage/migration/productionRegistry.js +0 -3931
  211. package/dist/storage/migration/registry.js +0 -169
  212. package/dist/storage/migration/report.js +0 -54
  213. package/dist/storage/migration/types.js +0 -31
  214. package/dist/storage/upgrade/homeClassification.js +0 -353
  215. package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
  216. package/dist/storage/upgrade/migrationReceipt.js +0 -67
  217. package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
  218. package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
  219. package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
  220. package/dist/storage/upgrade/recordVersionScan.js +0 -176
  221. package/dist/storage/upgrade/sqliteFileSet.js +0 -50
  222. package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
  223. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
  224. package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
  225. package/dist/storage/upgrade/switchProgress.js +0 -80
  226. package/dist/storage/upgrade/upgradeReceipt.js +0 -161
  227. package/dist/storage/upgradeCoordination.js +0 -187
  228. package/dist/storage/upgradeFence.js +0 -366
  229. package/dist/telemetry/telemetryCompaction.js +0 -252
@@ -1,80 +0,0 @@
1
- /**
2
- * The atomic-switch progress marker — a durable, out-of-band record of where a
3
- * two-step storage switch is in its lifecycle.
4
- *
5
- * ## Why (the partial-switch problem, P1-4)
6
- *
7
- * `atomicSwitchWithBackup` promotes a staged Home with two atomic renames on the
8
- * same filesystem:
9
- *
10
- * 1. `home -> backup` (move the original aside)
11
- * 2. `staging -> home` (promote the migrated output into place)
12
- *
13
- * The only non-atomic window is *between* those renames. If step 2 fails after
14
- * step 1 committed, the original Home no longer exists at its path — so a naive
15
- * "the source Home is unchanged" claim is FALSE. The switch either rolled back
16
- * (original restored, truly unchanged) or is genuinely interrupted (original at
17
- * the backup, nothing promoted into `home`).
18
- *
19
- * This marker makes the phase durable and readable AFTER the fact, so the
20
- * orchestrator can tell:
21
- * - **not-started** — no marker (or cleared): nothing moved; source unchanged.
22
- * - **interrupted** — `phase: "interrupted"`: the original was moved aside and
23
- * neither promotion nor rollback completed; recover from the backup.
24
- * - **complete** — the switch cleared its marker after a fully-committed
25
- * promotion.
26
- *
27
- * It lives at a SIBLING path of the Home (`<home>.upgrade-switch.json`), never
28
- * inside it, so it survives the `home -> backup` / `staging -> home` renames —
29
- * exactly like the completion receipt it coordinates with (see
30
- * `upgradeReceipt.ts`).
31
- */
32
- import { existsSync, readFileSync, rmSync } from "node:fs";
33
- import { basename, dirname, join } from "node:path";
34
- import { writeTextFileAtomically } from "../durableFile.js";
35
- /** The sibling switch-progress marker path for a Home. */
36
- export function switchProgressPath(home) {
37
- return join(dirname(home), `${basename(home)}.upgrade-switch.json`);
38
- }
39
- /** Write the switch-progress marker atomically. */
40
- export function writeSwitchProgress(home, progress) {
41
- writeTextFileAtomically(switchProgressPath(home), `${JSON.stringify(progress, null, 2)}\n`);
42
- }
43
- /**
44
- * Read the switch-progress marker, or `null` when absent/unreadable/malformed.
45
- * A marker whose `phase` is not a recognized value reads as `null` — the caller
46
- * treats an unreadable marker conservatively (it separately inspects the backup
47
- * and the on-disk Home).
48
- */
49
- export function readSwitchProgress(home) {
50
- const path = switchProgressPath(home);
51
- if (!existsSync(path))
52
- return null;
53
- try {
54
- const value = JSON.parse(readFileSync(path, "utf8"));
55
- const phase = value.phase;
56
- if (phase !== "backing-up" && phase !== "promoting" && phase !== "interrupted") {
57
- return null;
58
- }
59
- if (typeof value.homePath !== "string"
60
- || typeof value.backupPath !== "string"
61
- || typeof value.stagingPath !== "string"
62
- || typeof value.updatedAt !== "string") {
63
- return null;
64
- }
65
- return {
66
- phase,
67
- homePath: value.homePath,
68
- backupPath: value.backupPath,
69
- stagingPath: value.stagingPath,
70
- updatedAt: value.updatedAt
71
- };
72
- }
73
- catch {
74
- return null;
75
- }
76
- }
77
- /** Best-effort removal of the switch-progress marker (on rollback / completion). */
78
- export function clearSwitchProgress(home) {
79
- rmSync(switchProgressPath(home), { force: true });
80
- }
@@ -1,161 +0,0 @@
1
- /**
2
- * The upgrade completion receipt — a verifiable, out-of-band marker that a
3
- * storage switch actually committed.
4
- *
5
- * ## Why (the activation-ambiguity problem)
6
- *
7
- * `yui update` activates storage by spawning the staged binary's `yui upgrade`.
8
- * If that child is killed (SIGTERM/OOM) *after* the atomic switch commits but
9
- * *before* it prints its success JSON, the parent sees empty stdout and cannot
10
- * tell "nothing happened" from "storage already switched". Treating that as
11
- * recoverable (source unchanged) is a false claim that can hide a completed
12
- * migration.
13
- *
14
- * The receipt closes that gap: `runStorageUpgrade` writes it the instant the
15
- * switch commits and clears it only on a clean, fully-verified return. So its
16
- * presence is a durable "the switch committed but clean completion was not
17
- * confirmed" signal that the parent can read even when stdout was lost.
18
- *
19
- * It lives at a SIBLING path of the Home (`<home>.upgrade-receipt.json`), never
20
- * inside it, so it survives the switch's `home -> backup` / `staging -> home`
21
- * renames and is readable by both the child (new binary) and the parent (old
22
- * binary), which share the same fixed `YUI_HOME` path.
23
- */
24
- import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
25
- import { basename, dirname, join } from "node:path";
26
- import { writeTextFileAtomically } from "../durableFile.js";
27
- /** The sibling receipt path for a Home. */
28
- export function upgradeReceiptPath(home) {
29
- return join(dirname(home), `${basename(home)}.upgrade-receipt.json`);
30
- }
31
- /** Write the completion receipt atomically, just after the switch commits. */
32
- export function writeUpgradeReceipt(home, receipt) {
33
- writeTextFileAtomically(upgradeReceiptPath(home), `${JSON.stringify(receipt, null, 2)}\n`);
34
- }
35
- /**
36
- * Read the completion receipt, or `null` when absent/unreadable. A malformed
37
- * receipt reads as `null`: it is only ever corroborating evidence, and the
38
- * caller separately inspects the on-disk schema and backup.
39
- */
40
- export function readUpgradeReceipt(home) {
41
- const path = upgradeReceiptPath(home);
42
- if (!existsSync(path))
43
- return null;
44
- try {
45
- const value = JSON.parse(readFileSync(path, "utf8"));
46
- if (value.switched !== true || typeof value.completedAt !== "string")
47
- return null;
48
- return {
49
- switched: true,
50
- completedAt: value.completedAt,
51
- ...(typeof value.homePath === "string" ? { homePath: value.homePath } : {}),
52
- ...(typeof value.backupPath === "string" ? { backupPath: value.backupPath } : {}),
53
- ...(Number.isInteger(value.targetLayoutVersion)
54
- ? { targetLayoutVersion: value.targetLayoutVersion }
55
- : {}),
56
- ...(Number.isInteger(value.targetAggregateVersion)
57
- ? { targetAggregateVersion: value.targetAggregateVersion }
58
- : {})
59
- };
60
- }
61
- catch {
62
- return null;
63
- }
64
- }
65
- /**
66
- * Validate that a receipt genuinely corresponds to the current Home AND its
67
- * backup before trusting it for a recovery decision (P2-6 / R3-F6). Existence
68
- * alone is not correspondence: a receipt is trusted only when it carries the
69
- * current protocol's correlating fields and its backup is a REAL directory at the
70
- * exact timestamped-sibling path this Home's switch would have produced. Each
71
- * rule closes a concrete staleness/impersonation gap:
72
- *
73
- * - **no receipt** — nothing to correlate.
74
- * - **missing/foreign homePath** — a legacy receipt without `homePath`, or one
75
- * naming a different Home, says nothing trustworthy about THIS Home; the
76
- * current protocol always stamps `homePath`, so its absence is a legacy/degraded
77
- * marker that must be re-probed, not reused.
78
- * - **missing backupPath** — a receipt with no backup path is a degraded/legacy
79
- * marker; without the exact backup to correlate against we cannot confirm it,
80
- * so we re-probe rather than assert a committed switch.
81
- * - **backup not the expected sibling** — a `backupPath` that is not
82
- * `<home>.backup-*` in the Home's own parent directory is unrelated evidence
83
- * (a copied/foreign receipt); reject it.
84
- * - **backup absent or not a directory** — the switch it recorded was already
85
- * rolled back/cleaned, or the path is not a real backup dir; re-probe.
86
- *
87
- * On any rejection the caller falls back to an explicit "uncertain → re-probe the
88
- * real on-disk state" rather than deriving a recovery instruction from stale or
89
- * unrelated evidence.
90
- */
91
- export function correlateUpgradeReceipt(home) {
92
- const receipt = readUpgradeReceipt(home);
93
- if (receipt === null) {
94
- return { corresponds: false, reason: "no receipt present", receipt: null };
95
- }
96
- // The current protocol always records the Home it is about; a receipt without
97
- // `homePath`, or naming a different Home, is legacy/foreign — never trusted.
98
- if (receipt.homePath === undefined) {
99
- return {
100
- corresponds: false,
101
- reason: "receipt has no homePath (legacy/degraded marker); re-probing current state",
102
- receipt
103
- };
104
- }
105
- if (receipt.homePath !== home) {
106
- return {
107
- corresponds: false,
108
- reason: `receipt names a different Home (${receipt.homePath} != ${home})`,
109
- receipt
110
- };
111
- }
112
- // A trustworthy receipt must carry the exact backup its switch created.
113
- if (receipt.backupPath === undefined) {
114
- return {
115
- corresponds: false,
116
- reason: "receipt has no backupPath; cannot correlate a committed switch — re-probing",
117
- receipt
118
- };
119
- }
120
- if (!isExpectedBackupSibling(home, receipt.backupPath)) {
121
- return {
122
- corresponds: false,
123
- reason: `receipt backup ${receipt.backupPath} is not this Home's expected timestamped sibling; unrelated evidence`,
124
- receipt
125
- };
126
- }
127
- if (!isRealDirectory(receipt.backupPath)) {
128
- return {
129
- corresponds: false,
130
- reason: `receipt backup ${receipt.backupPath} is absent or not a directory (already restored or cleaned)`,
131
- receipt
132
- };
133
- }
134
- return { corresponds: true, receipt };
135
- }
136
- /**
137
- * True when `backupPath` is the exact `<home>.backup-<stamp>` sibling in the
138
- * Home's own parent directory that this Home's atomic switch would produce (see
139
- * `homeMigrationTarget.atomicSwitchWithBackup`). Rejects any path in a different
140
- * directory or without the `<basename>.backup-` prefix — i.e. unrelated evidence.
141
- */
142
- function isExpectedBackupSibling(home, backupPath) {
143
- if (dirname(backupPath) !== dirname(home))
144
- return false;
145
- const prefix = `${basename(home)}.backup-`;
146
- const name = basename(backupPath);
147
- return name.startsWith(prefix) && name.length > prefix.length;
148
- }
149
- /** True when `path` exists and is a real directory (not a file/symlink target miss). */
150
- function isRealDirectory(path) {
151
- try {
152
- return statSync(path).isDirectory();
153
- }
154
- catch {
155
- return false;
156
- }
157
- }
158
- /** Best-effort removal of the receipt (on clean success or before a fresh run). */
159
- export function clearUpgradeReceipt(home) {
160
- rmSync(upgradeReceiptPath(home), { force: true });
161
- }
@@ -1,187 +0,0 @@
1
- /**
2
- * Shared coordination boundary for durable runtime-hook admission and the
3
- * final Home snapshot/switch.
4
- *
5
- * The upgrade fence answers "may a new writer start?" but a writer that has
6
- * already passed that check can still be in the middle of its durable inbox
7
- * write. This sibling lock supplies the missing linearization point:
8
- *
9
- * inbox publish: acquire lock -> check fence/progress -> write -> release
10
- * upgrade cutover: acquire lock -> check drain -> copy -> switch -> release
11
- *
12
- * The lock lives beside (not inside) the Home, so the home -> backup and
13
- * staging -> home renames cannot move it out from under a live holder. It is
14
- * a short, non-reentrant-on-disk critical section with a bounded wait. A
15
- * crashed holder leaves its directory behind; a later entrant may atomically
16
- * rename it aside only after the owner is provably dead (or an owner-less
17
- * directory has exceeded the conservative age bound). No TTL is used to
18
- * evict a live owner, and a live/undeterminable holder fails closed.
19
- */
20
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
21
- import { randomUUID } from "node:crypto";
22
- import { dirname, join } from "node:path";
23
- import { assertHomeWritable, UpgradeFenceError } from "./upgradeFence.js";
24
- import { STORAGE_SCHEMA_FILE } from "./storageSchema.js";
25
- import { switchProgressPath } from "./upgrade/switchProgress.js";
26
- /** Sibling directory used for the shared inbox/cutover critical section. */
27
- export function upgradeCoordinationLockPath(home) {
28
- return `${home}.upgrade-coordination.lock`;
29
- }
30
- const COORDINATION_LOCK_TIMEOUT_MS = 5_000;
31
- const COORDINATION_LOCK_RETRY_MS = 10;
32
- const COORDINATION_LOCK_MIN_AGE_MS = 1_000;
33
- /**
34
- * Run a synchronous operation at the shared admission/cutover boundary.
35
- *
36
- * The admission check is intentionally performed *after* acquiring the lock.
37
- * A hook that passed a check before an upgrade placed its fence either owns the
38
- * lock and finishes before cutover, or waits and receives an explicit
39
- * UpgradeFenceError after the cutover holder releases it. There is no second
40
- * scan or retry protocol hidden in this helper. A staged updater child passes
41
- * its already-authenticated parent fence owner through this same boundary.
42
- */
43
- export function withUpgradeCoordinationLock(home, execute, fenceOwnerPid = process.pid) {
44
- const release = acquireUpgradeCoordinationLock(home);
45
- try {
46
- assertUpgradeAdmission(home, fenceOwnerPid);
47
- return execute();
48
- }
49
- finally {
50
- release();
51
- }
52
- }
53
- /**
54
- * Refuse admission when a foreign upgrade fence or an unresolved durable
55
- * switch-progress marker is present. A marker is actionable only while the
56
- * filesystem corroborates a missing/uninitialized Home; a stale marker beside
57
- * an intact Home is safe to ignore, matching update's recovery probe and
58
- * avoiding a permanent post-promote hook deadlock. A malformed marker with a
59
- * missing Home still fails closed because its recovery phase cannot be trusted.
60
- */
61
- export function assertUpgradeAdmission(home, fenceOwnerPid = process.pid) {
62
- assertHomeWritable(home, fenceOwnerPid);
63
- const progressPath = switchProgressPath(home);
64
- if (!existsSync(progressPath))
65
- return;
66
- const homeInitialized = existsSync(join(home, STORAGE_SCHEMA_FILE));
67
- if (!homeInitialized) {
68
- throw new UpgradeFenceError("storage switch recovery is in progress; the Home is not yet safe for a new write");
69
- }
70
- // A marker with an intact Home is stale relative to the filesystem (for
71
- // example, a best-effort marker-clear failure after promotion). Reads and
72
- // normal hook writes may proceed; update reports the marker only when
73
- // backup+missing-Home evidence corroborates an interrupted switch.
74
- }
75
- // A same-process nested call is safe because the critical section is synchronous
76
- // and cannot interleave with another turn. Cross-process ownership remains
77
- // governed by the on-disk mkdir boundary.
78
- const ownedLocks = new Map();
79
- function acquireUpgradeCoordinationLock(home) {
80
- const lock = upgradeCoordinationLockPath(home);
81
- const nested = ownedLocks.get(lock);
82
- if (nested !== undefined) {
83
- nested.depth += 1;
84
- return () => releaseNested(lock, nested);
85
- }
86
- mkdirSync(dirname(lock), { recursive: true, mode: 0o700 });
87
- const deadline = Date.now() + COORDINATION_LOCK_TIMEOUT_MS;
88
- while (true) {
89
- try {
90
- mkdirSync(lock, { mode: 0o700 });
91
- try {
92
- writeFileSync(`${lock}/owner`, `${process.pid}\n`, { mode: 0o600 });
93
- }
94
- catch (error) {
95
- rmSync(lock, { recursive: true, force: true });
96
- throw error;
97
- }
98
- const release = () => releaseOwnedLock(lock, process.pid);
99
- ownedLocks.set(lock, { depth: 1, release });
100
- return release;
101
- }
102
- catch (error) {
103
- if (!isEexist(error))
104
- throw error;
105
- if (reclaimStaleCoordinationLock(lock))
106
- continue;
107
- if (Date.now() >= deadline) {
108
- throw new UpgradeFenceError("shared inbox/cutover coordination is held by another process; retry after it exits");
109
- }
110
- // Match the bounded wait used by the existing storage lock. This is
111
- // contention backoff, not a correctness delay or a blind retry of a
112
- // publish/switch operation.
113
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, COORDINATION_LOCK_RETRY_MS);
114
- }
115
- }
116
- }
117
- function releaseNested(lock, owned) {
118
- owned.depth -= 1;
119
- if (owned.depth === 0) {
120
- ownedLocks.delete(lock);
121
- owned.release();
122
- }
123
- }
124
- function releaseOwnedLock(lock, ownerPid) {
125
- const current = readOwnerPid(lock);
126
- // Never remove a successor's lock if a stale release races with recovery.
127
- if (current === ownerPid)
128
- rmSync(lock, { recursive: true, force: true });
129
- }
130
- /**
131
- * Move an orphaned lock aside atomically. The directory is never deleted in
132
- * place while another process could be acquiring it: rename-aside gives the
133
- * next O_EXCL mkdir a clean path and preserves a live replacement.
134
- */
135
- function reclaimStaleCoordinationLock(lock) {
136
- let age;
137
- try {
138
- age = Date.now() - statSync(lock).mtimeMs;
139
- }
140
- catch (error) {
141
- return isEnoent(error);
142
- }
143
- if (age < COORDINATION_LOCK_MIN_AGE_MS)
144
- return false;
145
- const ownerPid = readOwnerPid(lock);
146
- if (ownerPid !== null && processIsAlive(ownerPid))
147
- return false;
148
- const abandoned = `${lock}.reclaim-${process.pid}-${randomUUID()}`;
149
- try {
150
- renameSync(lock, abandoned);
151
- rmSync(abandoned, { recursive: true, force: true });
152
- return true;
153
- }
154
- catch (error) {
155
- // ENOENT means the holder released it (or another reclaimer won); retry the
156
- // mkdir. Any other failure is undeterminable and therefore fail-closed.
157
- return isEnoent(error);
158
- }
159
- }
160
- function readOwnerPid(lock) {
161
- let raw;
162
- try {
163
- raw = readFileSync(`${lock}/owner`, "utf8").trim();
164
- }
165
- catch {
166
- return null;
167
- }
168
- if (!/^\d+$/.test(raw))
169
- return null;
170
- const pid = Number(raw);
171
- return Number.isInteger(pid) && pid > 0 ? pid : null;
172
- }
173
- function processIsAlive(pid) {
174
- try {
175
- process.kill(pid, 0);
176
- return true;
177
- }
178
- catch (error) {
179
- return error instanceof Error && "code" in error && error.code === "EPERM";
180
- }
181
- }
182
- function isEexist(error) {
183
- return error instanceof Error && "code" in error && error.code === "EEXIST";
184
- }
185
- function isEnoent(error) {
186
- return error instanceof Error && "code" in error && error.code === "ENOENT";
187
- }