@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,366 +0,0 @@
1
- /**
2
- * The upgrade admission fence.
3
- *
4
- * A fence is a small marker file inside a Yui Home. While it exists and is owned
5
- * by a live process other than the caller, every authoritative writer — a
6
- * baseline `yui` CLI invocation *and* the per-home Controller alike — refuses to
7
- * begin a new write. This is the mechanism by which `yui upgrade`/`yui update`
8
- * stop new writes before quiescing and switching the store, without a broad
9
- * process kill or a TTL/idle heuristic.
10
- *
11
- * The fence is honored at the single storage write choke point
12
- * (`FileTaskStore` commit), so both the CLI and the Controller — which mutate
13
- * through the same store — observe it. Because the check lives in that shared
14
- * path, every writer built from this release forward enforces it; it cannot
15
- * retroactively bind an already-installed older binary, so the upgrade
16
- * orchestrator additionally drains the Controller and fails closed on any live
17
- * writer rather than relying on the fence alone. It never blocks reads, and it
18
- * never blocks the process that placed it (so the upgrade orchestrator can
19
- * re-pin the revision under the write lock). A fence whose owner process is gone
20
- * is stale and is reclaimed, mirroring the storage lock's dead-owner reclaim.
21
- */
22
- import { closeSync, constants, fsyncSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeSync } from "node:fs";
23
- import { dirname, join } from "node:path";
24
- /** Fence marker location, beside the Controller discovery file under `runtime/`. */
25
- export const UPGRADE_FENCE_FILE = "runtime/upgrade.fence";
26
- /** Thrown when a writer is refused because an upgrade fence is in place. */
27
- export class UpgradeFenceError extends Error {
28
- reason;
29
- constructor(reason) {
30
- super("Yui storage is fenced for an in-progress upgrade; new writes are refused "
31
- + `(${reason}). Wait for the upgrade to finish, or if no upgrade is `
32
- + "running remove the stale fence and retry.");
33
- this.name = "UpgradeFenceError";
34
- this.reason = reason;
35
- }
36
- }
37
- function fencePath(home) {
38
- return join(home, UPGRADE_FENCE_FILE);
39
- }
40
- /** Read the fence file's raw bytes at an exact path, or `null` when absent. */
41
- function readFenceRaw(path) {
42
- try {
43
- return readFileSync(path, "utf8");
44
- }
45
- catch (error) {
46
- if (isEnoent(error))
47
- return null;
48
- throw error;
49
- }
50
- }
51
- /**
52
- * Parse raw fence bytes into a record. A well-formed fence yields its fields; a
53
- * malformed/unparseable fence yields a sentinel `ownerPid: -1` "malformed fence"
54
- * so callers treat it as an undeterminable (fail-closed) live fence, never as
55
- * absent or reclaimable.
56
- */
57
- function parseFence(raw) {
58
- try {
59
- const value = JSON.parse(raw);
60
- if (value.schemaVersion === 1
61
- && Number.isInteger(value.ownerPid)
62
- && typeof value.reason === "string"
63
- && typeof value.createdAt === "string") {
64
- return {
65
- schemaVersion: 1,
66
- ownerPid: value.ownerPid,
67
- reason: value.reason,
68
- createdAt: value.createdAt
69
- };
70
- }
71
- }
72
- catch {
73
- // fall through to the malformed sentinel.
74
- }
75
- return { schemaVersion: 1, ownerPid: -1, reason: "malformed fence", createdAt: "" };
76
- }
77
- /** Read the current fence record, or `null` when none is present or it is unreadable. */
78
- export function readUpgradeFence(home) {
79
- const raw = readFenceRaw(fencePath(home));
80
- if (raw === null)
81
- return null;
82
- return parseFence(raw);
83
- }
84
- /**
85
- * Refuse a write when a live upgrade fence owned by another process is present.
86
- * A fence owned by the current process, or by a process that is gone, never
87
- * blocks: the placer may re-pin under the lock, and a stale (dead-owner) fence is
88
- * reclaimed. A stale fence is only cleared for the caller once the reclaim is
89
- * PROVEN complete — if it cannot be cleared, this fails closed rather than
90
- * falsely reporting the Home writable (R2-F4).
91
- */
92
- export function assertHomeWritable(home, callerPid = process.pid) {
93
- const path = fencePath(home);
94
- const raw = readFenceRaw(path);
95
- if (raw === null)
96
- return;
97
- const fence = parseFence(raw);
98
- if (fence.ownerPid === callerPid)
99
- return;
100
- if (fence.ownerPid > 0 && !processIsAlive(fence.ownerPid)) {
101
- // Stale fence from a crashed upgrade: reclaim it with the crash-safe atomic
102
- // rename-aside (F4/R2-F4), then RE-VERIFY. We only permit the write if the
103
- // admission path is now clear of a blocking fence:
104
- // - absent / owned by us / owned by a still-dead pid -> writable, and
105
- // - a LIVE foreign owner that took the freed slot -> refuse.
106
- // If the stale fence is somehow still present (reclaim could not move it), we
107
- // do NOT return writable — fail closed.
108
- reclaimStaleFence(path, raw);
109
- const afterRaw = readFenceRaw(path);
110
- if (afterRaw === null)
111
- return; // reclaimed: path is free.
112
- const after = parseFence(afterRaw);
113
- if (after.ownerPid === callerPid)
114
- return;
115
- if (after.ownerPid > 0 && !processIsAlive(after.ownerPid) && afterRaw !== raw) {
116
- // A different dead-owner fence appeared (a racer's crashed attempt); it is
117
- // itself reclaimable, so the Home is not live-fenced. Permit.
118
- return;
119
- }
120
- // Either the SAME stale fence is still there (reclaim did not complete) or a
121
- // live/undeterminable owner now holds it: fail closed, never falsely writable.
122
- throw new UpgradeFenceError(after.reason);
123
- }
124
- throw new UpgradeFenceError(fence.reason);
125
- }
126
- /**
127
- * Place an upgrade fence owned by the current process and return a release
128
- * handle. The handle is idempotent and only removes a fence this process owns.
129
- *
130
- * Acquisition is a SINGLE atomic `O_CREAT | O_EXCL` create (P2-5): the kernel
131
- * guarantees exactly one of any number of concurrent entrants wins that create,
132
- * so there is no check-then-write window in which two upgraders both believe
133
- * they acquired. A loser reads the existing fence and either:
134
- * - re-enters (it already owns the fence — idempotent), or
135
- * - reclaims a PROVABLY-DEAD owner's stale fence and retries the atomic create
136
- * once (a dead owner cannot be a live competitor), or
137
- * - fails closed with {@link UpgradeFenceError} for a live foreign owner or an
138
- * undeterminable/malformed fence.
139
- * There is no lease, heartbeat, or multi-round negotiation — just the one atomic
140
- * create plus a bounded dead-owner reclaim.
141
- */
142
- export function placeUpgradeFence(home, options) {
143
- const ownerPid = options.ownerPid ?? process.pid;
144
- const fence = {
145
- schemaVersion: 1,
146
- ownerPid,
147
- reason: options.reason,
148
- createdAt: options.createdAt
149
- };
150
- const content = `${JSON.stringify(fence, null, 2)}\n`;
151
- const path = fencePath(home);
152
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
153
- // At most a bounded number of attempts: each reclaim of a dead owner's stale
154
- // fence is followed by one more atomic-create try. A fresh race is settled by
155
- // the first atomic create; a live foreign owner fails immediately.
156
- for (let attempt = 0; attempt < 3; attempt += 1) {
157
- if (tryCreateFenceExclusive(path, content)) {
158
- fsyncDirectory(dirname(path));
159
- return makeFenceRelease(home, ownerPid);
160
- }
161
- // The atomic create lost: a fence already exists. Decide purely from its
162
- // owner — never by overwriting, which would reintroduce the race.
163
- const rawExisting = readFenceRaw(path);
164
- if (rawExisting === null)
165
- continue; // vanished between create and read; retry.
166
- const existing = parseFence(rawExisting);
167
- if (existing.ownerPid === ownerPid) {
168
- // This process already owns the fence: idempotent re-entry.
169
- return makeFenceRelease(home, ownerPid);
170
- }
171
- if (existing.ownerPid > 0 && !processIsAlive(existing.ownerPid)) {
172
- // Provably-dead owner: reclaim the stale fence with a crash-safe atomic
173
- // rename-aside (F4). It moves ONLY the exact stale inode out of the
174
- // admission path — a racer that replaced it with a live fence is preserved,
175
- // and no persistent coordination lock is left to orphan and block future
176
- // reclaims. Then retry the atomic create against the freed path.
177
- reclaimStaleFence(path, rawExisting);
178
- continue;
179
- }
180
- // A live foreign owner, or an undeterminable/malformed fence (ownerPid <= 0):
181
- // another upgrade holds this Home. Fail closed with no coordination protocol.
182
- throw new UpgradeFenceError(existing.reason);
183
- }
184
- // Still contended after the bounded reclaim retries: fail closed rather than spin.
185
- throw new UpgradeFenceError(readUpgradeFence(home)?.reason ?? "upgrade fence contended");
186
- }
187
- /**
188
- * Reclaim a stale (dead-owner) fence: delete the fence file at `path` only if its
189
- * bytes still exactly equal `expectedRaw`, under a short critical section that is
190
- * itself CRASH-SAFE (R2-F4).
191
- *
192
- * The exclusion is a `mkdir` lock — but, mirroring the storage lock's
193
- * `reclaimDeadLock`, the lock records its owner pid and is itself reclaimable, so
194
- * a holder that crashes mid-critical-section cannot permanently orphan it and
195
- * strand admission. On contention we first try to reclaim an orphaned lock (dead
196
- * owner, or older than a small age bound), then retry; if the lock is genuinely
197
- * held by a live process we simply return (that live reclaimer will finish, and
198
- * the caller's next O_EXCL create settles the race).
199
- *
200
- * Correctness of the delete: inside the lock we RE-READ the fence and delete only
201
- * if the bytes are still the exact stale ones, so a racer that O_EXCL-created a
202
- * fresh live fence at `path` between our observe and the delete is never
203
- * clobbered. The single O_EXCL create in `placeUpgradeFence` remains the sole
204
- * winner-decider, so a reclaim never grants ownership by itself.
205
- */
206
- function reclaimStaleFence(path, expectedRaw) {
207
- const reclaimLock = `${path}.reclaim.lock`;
208
- const ownerFile = join(reclaimLock, "owner");
209
- for (let attempt = 0; attempt < 2; attempt += 1) {
210
- try {
211
- mkdirSync(reclaimLock); // atomic; EEXIST if another reclaimer holds it.
212
- }
213
- catch (error) {
214
- if (!isEexist(error))
215
- throw error;
216
- // Contended: reclaim the lock only if it is orphaned (dead owner or too
217
- // old), then retry once. A live-held lock means another reclaimer is
218
- // actively working — leave it and let the O_EXCL create settle the race.
219
- if (reclaimOrphanedLock(reclaimLock, ownerFile))
220
- continue;
221
- return;
222
- }
223
- try {
224
- writeFenceLockOwner(ownerFile);
225
- // Re-read UNDER the lock; delete only if still the exact stale bytes.
226
- const current = readFenceRaw(path);
227
- if (current !== null && current === expectedRaw) {
228
- rmSync(path, { force: true });
229
- fsyncDirectory(dirname(path));
230
- }
231
- }
232
- finally {
233
- rmSync(reclaimLock, { recursive: true, force: true });
234
- }
235
- return;
236
- }
237
- }
238
- /** Small age bound below which an orphaned-looking lock is given the benefit of the doubt. */
239
- const RECLAIM_LOCK_MIN_AGE_MS = 1_000;
240
- /**
241
- * Reclaim a reclaim-lock directory ONLY when it is provably orphaned: its owner
242
- * pid is dead (or unreadable) AND it is older than a small age bound (so a lock
243
- * whose owner just `mkdir`ed but has not yet written its pid is not stolen).
244
- * Returns true when it removed the lock (caller should retry), false otherwise.
245
- */
246
- function reclaimOrphanedLock(reclaimLock, ownerFile) {
247
- try {
248
- const age = Date.now() - statSync(reclaimLock).mtimeMs;
249
- if (age < RECLAIM_LOCK_MIN_AGE_MS)
250
- return false; // too fresh; owner may be mid-acquire.
251
- let pid;
252
- try {
253
- pid = Number.parseInt(readFileSync(ownerFile, "utf8"), 10);
254
- }
255
- catch {
256
- pid = null; // no/unreadable owner on an old lock: treat as orphaned.
257
- }
258
- if (pid !== null && Number.isInteger(pid) && processIsAlive(pid))
259
- return false; // live holder.
260
- rmSync(reclaimLock, { recursive: true, force: true });
261
- return true;
262
- }
263
- catch (error) {
264
- // ENOENT: the lock vanished (holder released it) — retry the mkdir.
265
- return isEnoent(error);
266
- }
267
- }
268
- function writeFenceLockOwner(ownerFile) {
269
- let descriptor;
270
- try {
271
- descriptor = openSync(ownerFile, constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC, 0o600);
272
- }
273
- catch {
274
- // Best effort: without the owner pid the lock is still age-reclaimable, so a
275
- // failure to record it degrades to the age bound rather than orphaning.
276
- return;
277
- }
278
- // Keep the descriptor so it can be closed in `finally` — never leak an fd per
279
- // reclaim (R3-F5).
280
- try {
281
- writeSync(descriptor, `${process.pid}\n`);
282
- }
283
- catch {
284
- // Recording the pid is best-effort; the age bound still bounds reclaim.
285
- }
286
- finally {
287
- closeSync(descriptor);
288
- }
289
- }
290
- /**
291
- * Atomically create the fence file and write its content, failing (returning
292
- * `false`) iff the file already exists. The `O_EXCL` create is the single point
293
- * that decides a concurrent race; the owner bytes are written into the same
294
- * descriptor and fsynced before it is closed.
295
- */
296
- function tryCreateFenceExclusive(path, content) {
297
- let descriptor;
298
- try {
299
- descriptor = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
300
- }
301
- catch (error) {
302
- if (isEexist(error))
303
- return false;
304
- throw error;
305
- }
306
- try {
307
- writeSync(descriptor, content);
308
- fsyncSync(descriptor);
309
- }
310
- finally {
311
- closeSync(descriptor);
312
- }
313
- return true;
314
- }
315
- /** Build the idempotent release handle that only clears a fence this owner holds. */
316
- function makeFenceRelease(home, ownerPid) {
317
- return () => {
318
- const current = readUpgradeFence(home);
319
- if (current !== null && current.ownerPid === ownerPid)
320
- clearUpgradeFence(home);
321
- };
322
- }
323
- /** Unconditionally remove any fence marker (used by release and stale reclaim). */
324
- export function clearUpgradeFence(home) {
325
- rmSync(fencePath(home), { force: true });
326
- }
327
- function processIsAlive(pid) {
328
- try {
329
- process.kill(pid, 0);
330
- return true;
331
- }
332
- catch (error) {
333
- return error instanceof Error && "code" in error && error.code === "EPERM";
334
- }
335
- }
336
- function isEnoent(error) {
337
- return error instanceof Error && "code" in error && error.code === "ENOENT";
338
- }
339
- function isEexist(error) {
340
- return error instanceof Error && "code" in error && error.code === "EEXIST";
341
- }
342
- /**
343
- * fsync the directory holding the fence so a crash right after acquisition still
344
- * observes the fence on the next boot (the marker's whole purpose is durability
345
- * across an interrupted upgrade). Best-effort: a platform that cannot open a
346
- * directory for fsync must not fail the acquisition.
347
- */
348
- function fsyncDirectory(directory) {
349
- let descriptor;
350
- try {
351
- descriptor = openSync(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
352
- }
353
- catch {
354
- return;
355
- }
356
- try {
357
- fsyncSync(descriptor);
358
- }
359
- catch {
360
- // Some filesystems reject fsync on a directory fd; the O_EXCL create already
361
- // committed the inode, so this is not fatal to correctness.
362
- }
363
- finally {
364
- closeSync(descriptor);
365
- }
366
- }
@@ -1,252 +0,0 @@
1
- import { DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
2
- /**
3
- * Issue 09 — historical compaction.
4
- *
5
- * Progress events that were appended to semantic Task event history (legacy
6
- * behavior) are folded into the telemetry tables inside the Home's `yui.db`:
7
- * the newest `keep` rows per Run/generation become the retained window, and
8
- * every Run/generation gets an accurate aggregate
9
- * (count/first/last/maxSequence/errorCount). The semantic progress events are
10
- * then removed through the store's public transaction adapter. Semantic
11
- * evidence (Run dispatch/delivery/terminal receipts, Session lifecycle,
12
- * Review, Decision, Integration, ...) is never touched.
13
- *
14
- * Compaction runs on a staged copy of a Home. The caller promotes the staged
15
- * copy only after this module validates the result; a validation failure
16
- * throws and leaves the original history unchanged.
17
- */
18
- /** Historical pre-Driver event type accepted only by the offline migration. */
19
- export const PROGRESS_EVENT_TYPE = "runtime.provider-turn-progress";
20
- /**
21
- * Scan a store and build the compaction plan. Read-only: nothing is modified.
22
- */
23
- export function planTelemetryCompaction(store, options = {}) {
24
- const keep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
25
- const tasks = [];
26
- let progressEvents = 0;
27
- let generations = 0;
28
- let telemetryRows = 0;
29
- for (const task of store.listTasks()) {
30
- const taskPlan = planTaskCompaction(task.id, store.listEvents(task.id), keep);
31
- if (taskPlan === null)
32
- continue;
33
- tasks.push(taskPlan);
34
- progressEvents += taskPlan.progressEventIds.length;
35
- generations += taskPlan.generations.length;
36
- for (const generation of taskPlan.generations)
37
- telemetryRows += generation.window.length;
38
- }
39
- return {
40
- tasks,
41
- totals: { tasks: tasks.length, progressEvents, generations, telemetryRows }
42
- };
43
- }
44
- function planTaskCompaction(taskId, events, keep) {
45
- const byGeneration = new Map();
46
- const progressEventIds = [];
47
- let malformedKept = 0;
48
- for (const event of events) {
49
- if (event.type !== PROGRESS_EVENT_TYPE)
50
- continue;
51
- const runId = event.payload.runId;
52
- const progressId = event.payload.progressId;
53
- if (typeof runId !== "string" || typeof progressId !== "string") {
54
- // Fail closed: an anomalous progress row stays semantic and is reported.
55
- malformedKept++;
56
- continue;
57
- }
58
- const roleName = typeof event.payload.roleName === "string" ? event.payload.roleName : "unknown";
59
- const generation = typeof event.payload.launchId === "string" && event.payload.launchId !== ""
60
- ? event.payload.launchId
61
- : typeof event.payload.nativeSessionId === "string" && event.payload.nativeSessionId !== ""
62
- ? event.payload.nativeSessionId
63
- : "unbound";
64
- const key = `${roleName}\u0000${runId}\u0000${generation}`;
65
- const group = byGeneration.get(key);
66
- if (group === undefined)
67
- byGeneration.set(key, [event]);
68
- else
69
- group.push(event);
70
- progressEventIds.push(event.id);
71
- }
72
- if (progressEventIds.length === 0 && malformedKept === 0)
73
- return null;
74
- const generations = [];
75
- for (const group of byGeneration.values()) {
76
- generations.push(planGenerationCompaction(taskId, group, keep));
77
- }
78
- return { taskId, progressEventIds, malformedKept, generations };
79
- }
80
- function planGenerationCompaction(taskId, events, keep) {
81
- const first = events[0];
82
- const roleName = typeof first.payload.roleName === "string" ? first.payload.roleName : "unknown";
83
- const runId = first.payload.runId;
84
- const generation = typeof first.payload.launchId === "string" && first.payload.launchId !== ""
85
- ? first.payload.launchId
86
- : typeof first.payload.nativeSessionId === "string" && first.payload.nativeSessionId !== ""
87
- ? first.payload.nativeSessionId
88
- : "unbound";
89
- const ordered = [...events].sort(compareProgressEvents);
90
- let firstAt = progressAt(ordered[0]);
91
- let lastAt = firstAt;
92
- let maxSequence = null;
93
- let errorCount = 0;
94
- for (const event of ordered) {
95
- const at = progressAt(event);
96
- if (at < firstAt)
97
- firstAt = at;
98
- if (at > lastAt)
99
- lastAt = at;
100
- const sequence = event.payload.sequence;
101
- if (typeof sequence === "string") {
102
- const value = Number(sequence);
103
- if (Number.isSafeInteger(value) && (maxSequence === null || value > maxSequence)) {
104
- maxSequence = value;
105
- }
106
- }
107
- if ((typeof event.payload.error === "string" && event.payload.error !== "")
108
- || (typeof event.payload.errorKind === "string" && event.payload.errorKind !== "")) {
109
- errorCount++;
110
- }
111
- }
112
- const window = ordered.slice(-keep).map((event) => ({
113
- taskId,
114
- roleName,
115
- runId,
116
- generation,
117
- progressId: event.payload.progressId,
118
- ...(typeof event.payload.sequence === "string"
119
- && Number.isSafeInteger(Number(event.payload.sequence))
120
- ? { sequence: Number(event.payload.sequence) }
121
- : {}),
122
- payload: { ...event.payload },
123
- receivedAt: progressAt(event)
124
- }));
125
- return {
126
- roleName,
127
- runId,
128
- generation,
129
- aggregate: {
130
- taskId,
131
- roleName,
132
- runId,
133
- generation,
134
- firstAt,
135
- lastAt,
136
- count: ordered.length,
137
- maxSequence,
138
- errorCount
139
- },
140
- window
141
- };
142
- }
143
- function compareProgressEvents(a, b) {
144
- const seqA = numericSequence(a);
145
- const seqB = numericSequence(b);
146
- if (seqA !== null && seqB !== null && seqA !== seqB)
147
- return seqA - seqB;
148
- const atA = Date.parse(progressAt(a));
149
- const atB = Date.parse(progressAt(b));
150
- if (atA !== atB)
151
- return atA - atB;
152
- return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
153
- }
154
- function numericSequence(event) {
155
- const raw = event.payload.sequence;
156
- if (typeof raw !== "string")
157
- return null;
158
- const value = Number(raw);
159
- return Number.isSafeInteger(value) ? value : null;
160
- }
161
- function progressAt(event) {
162
- const raw = event.payload.progressAt;
163
- if (typeof raw === "string" && Number.isFinite(Date.parse(raw)))
164
- return raw;
165
- return event.createdAt;
166
- }
167
- /**
168
- * Apply a plan to a staged Home. The telemetry windows/aggregates are imported
169
- * first and validated, then the semantic progress events are removed inside
170
- * one store transaction and validated again. Throws on any validation
171
- * failure; the caller must discard the staged Home in that case. A crash
172
- * between the two phases leaves the staged Home in the equivalent of
173
- * dual-write state (telemetry imported, semantic progress still present),
174
- * which is harmless and re-runnable.
175
- */
176
- export function applyTelemetryCompaction(store, telemetry, plan, options) {
177
- const now = options.now ?? new Date();
178
- const keep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
179
- if (!options.dryRun) {
180
- // Import first and validate the sidecar before touching semantic history:
181
- // a validation failure leaves the staged Home's semantic events intact.
182
- for (const task of plan.tasks) {
183
- for (const generation of task.generations) {
184
- telemetry.importGeneration(generation.window, generation.aggregate);
185
- }
186
- }
187
- validateAggregates(telemetry, plan);
188
- store.transaction(() => {
189
- for (const task of plan.tasks) {
190
- if (task.progressEventIds.length === 0)
191
- continue;
192
- store.removeEvents(task.taskId, task.progressEventIds);
193
- }
194
- });
195
- validateSemanticClean(store, plan);
196
- }
197
- return {
198
- schemaVersion: 1,
199
- createdAt: now.toISOString(),
200
- dryRun: options.dryRun,
201
- source: options.source,
202
- terminalKeep: keep,
203
- totals: plan.totals,
204
- tasks: plan.tasks.map((task) => ({
205
- taskId: task.taskId,
206
- removedProgressEvents: options.dryRun ? 0 : task.progressEventIds.length,
207
- generations: task.generations.map((generation) => generation.aggregate)
208
- })),
209
- validation: "passed"
210
- };
211
- }
212
- /**
213
- * Verify every planned Run/generation aggregate in the sidecar matches the
214
- * plan (count, first/last, max sequence) before semantic history is touched.
215
- */
216
- function validateAggregates(telemetry, plan) {
217
- for (const task of plan.tasks) {
218
- for (const generation of task.generations) {
219
- const stored = telemetry.aggregateGeneration(generation.aggregate.taskId, generation.aggregate.roleName, generation.aggregate.runId, generation.aggregate.generation);
220
- if (stored === null) {
221
- throw new Error(`Compaction validation failed: missing aggregate for Run ${generation.aggregate.runId}.`);
222
- }
223
- const expected = generation.aggregate;
224
- if (stored.count !== expected.count
225
- || stored.firstAt !== expected.firstAt
226
- || stored.lastAt !== expected.lastAt
227
- || stored.maxSequence !== expected.maxSequence
228
- || stored.errorCount !== expected.errorCount) {
229
- throw new Error(`Compaction validation failed: aggregate mismatch for Run ${expected.runId} `
230
- + `(expected count=${expected.count} first=${expected.firstAt} last=${expected.lastAt} `
231
- + `maxSequence=${String(expected.maxSequence)} errors=${expected.errorCount}; `
232
- + `got count=${stored.count} first=${stored.firstAt} last=${stored.lastAt} `
233
- + `maxSequence=${String(stored.maxSequence)} errors=${stored.errorCount}).`);
234
- }
235
- }
236
- }
237
- }
238
- /** Verify no planned progress event remains in semantic history. */
239
- function validateSemanticClean(store, plan) {
240
- for (const task of plan.tasks) {
241
- if (task.progressEventIds.length === 0)
242
- continue;
243
- const remaining = new Set(store.listEvents(task.taskId)
244
- .filter((event) => event.type === PROGRESS_EVENT_TYPE)
245
- .map((event) => event.id));
246
- for (const id of task.progressEventIds) {
247
- if (remaining.has(id)) {
248
- throw new Error(`Compaction validation failed: progress event ${task.taskId}/${id} still present.`);
249
- }
250
- }
251
- }
252
- }