@zq-silk/yui 0.2.0 → 0.4.2

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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -0,0 +1,161 @@
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
+ }
@@ -0,0 +1,186 @@
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.
41
+ */
42
+ export function withUpgradeCoordinationLock(home, execute) {
43
+ const release = acquireUpgradeCoordinationLock(home);
44
+ try {
45
+ assertUpgradeAdmission(home);
46
+ return execute();
47
+ }
48
+ finally {
49
+ release();
50
+ }
51
+ }
52
+ /**
53
+ * Refuse admission when a foreign upgrade fence or an unresolved durable
54
+ * switch-progress marker is present. A marker is actionable only while the
55
+ * filesystem corroborates a missing/uninitialized Home; a stale marker beside
56
+ * an intact Home is safe to ignore, matching update's recovery probe and
57
+ * avoiding a permanent post-promote hook deadlock. A malformed marker with a
58
+ * missing Home still fails closed because its recovery phase cannot be trusted.
59
+ */
60
+ export function assertUpgradeAdmission(home) {
61
+ assertHomeWritable(home);
62
+ const progressPath = switchProgressPath(home);
63
+ if (!existsSync(progressPath))
64
+ return;
65
+ const homeInitialized = existsSync(join(home, STORAGE_SCHEMA_FILE));
66
+ if (!homeInitialized) {
67
+ throw new UpgradeFenceError("storage switch recovery is in progress; the Home is not yet safe for a new write");
68
+ }
69
+ // A marker with an intact Home is stale relative to the filesystem (for
70
+ // example, a best-effort marker-clear failure after promotion). Reads and
71
+ // normal hook writes may proceed; update reports the marker only when
72
+ // backup+missing-Home evidence corroborates an interrupted switch.
73
+ }
74
+ // A same-process nested call is safe because the critical section is synchronous
75
+ // and cannot interleave with another turn. Cross-process ownership remains
76
+ // governed by the on-disk mkdir boundary.
77
+ const ownedLocks = new Map();
78
+ function acquireUpgradeCoordinationLock(home) {
79
+ const lock = upgradeCoordinationLockPath(home);
80
+ const nested = ownedLocks.get(lock);
81
+ if (nested !== undefined) {
82
+ nested.depth += 1;
83
+ return () => releaseNested(lock, nested);
84
+ }
85
+ mkdirSync(dirname(lock), { recursive: true, mode: 0o700 });
86
+ const deadline = Date.now() + COORDINATION_LOCK_TIMEOUT_MS;
87
+ while (true) {
88
+ try {
89
+ mkdirSync(lock, { mode: 0o700 });
90
+ try {
91
+ writeFileSync(`${lock}/owner`, `${process.pid}\n`, { mode: 0o600 });
92
+ }
93
+ catch (error) {
94
+ rmSync(lock, { recursive: true, force: true });
95
+ throw error;
96
+ }
97
+ const release = () => releaseOwnedLock(lock, process.pid);
98
+ ownedLocks.set(lock, { depth: 1, release });
99
+ return release;
100
+ }
101
+ catch (error) {
102
+ if (!isEexist(error))
103
+ throw error;
104
+ if (reclaimStaleCoordinationLock(lock))
105
+ continue;
106
+ if (Date.now() >= deadline) {
107
+ throw new UpgradeFenceError("shared inbox/cutover coordination is held by another process; retry after it exits");
108
+ }
109
+ // Match the bounded wait used by the existing storage lock. This is
110
+ // contention backoff, not a correctness delay or a blind retry of a
111
+ // publish/switch operation.
112
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, COORDINATION_LOCK_RETRY_MS);
113
+ }
114
+ }
115
+ }
116
+ function releaseNested(lock, owned) {
117
+ owned.depth -= 1;
118
+ if (owned.depth === 0) {
119
+ ownedLocks.delete(lock);
120
+ owned.release();
121
+ }
122
+ }
123
+ function releaseOwnedLock(lock, ownerPid) {
124
+ const current = readOwnerPid(lock);
125
+ // Never remove a successor's lock if a stale release races with recovery.
126
+ if (current === ownerPid)
127
+ rmSync(lock, { recursive: true, force: true });
128
+ }
129
+ /**
130
+ * Move an orphaned lock aside atomically. The directory is never deleted in
131
+ * place while another process could be acquiring it: rename-aside gives the
132
+ * next O_EXCL mkdir a clean path and preserves a live replacement.
133
+ */
134
+ function reclaimStaleCoordinationLock(lock) {
135
+ let age;
136
+ try {
137
+ age = Date.now() - statSync(lock).mtimeMs;
138
+ }
139
+ catch (error) {
140
+ return isEnoent(error);
141
+ }
142
+ if (age < COORDINATION_LOCK_MIN_AGE_MS)
143
+ return false;
144
+ const ownerPid = readOwnerPid(lock);
145
+ if (ownerPid !== null && processIsAlive(ownerPid))
146
+ return false;
147
+ const abandoned = `${lock}.reclaim-${process.pid}-${randomUUID()}`;
148
+ try {
149
+ renameSync(lock, abandoned);
150
+ rmSync(abandoned, { recursive: true, force: true });
151
+ return true;
152
+ }
153
+ catch (error) {
154
+ // ENOENT means the holder released it (or another reclaimer won); retry the
155
+ // mkdir. Any other failure is undeterminable and therefore fail-closed.
156
+ return isEnoent(error);
157
+ }
158
+ }
159
+ function readOwnerPid(lock) {
160
+ let raw;
161
+ try {
162
+ raw = readFileSync(`${lock}/owner`, "utf8").trim();
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ if (!/^\d+$/.test(raw))
168
+ return null;
169
+ const pid = Number(raw);
170
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
171
+ }
172
+ function processIsAlive(pid) {
173
+ try {
174
+ process.kill(pid, 0);
175
+ return true;
176
+ }
177
+ catch (error) {
178
+ return error instanceof Error && "code" in error && error.code === "EPERM";
179
+ }
180
+ }
181
+ function isEexist(error) {
182
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
183
+ }
184
+ function isEnoent(error) {
185
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
186
+ }