@zq-silk/yui 0.13.10 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/ARCHITECTURE.md +69 -428
  2. package/README.md +113 -300
  3. package/dist/agent/argumentPolicy.js +2 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +2 -2
  5. package/dist/cli/commandCatalog.js +39 -77
  6. package/dist/cli/interactionCandidates.js +4 -4
  7. package/dist/cli/interactionPolicy.js +7 -10
  8. package/dist/cli/invocationRouter.js +1 -1
  9. package/dist/cli/updateCommand.js +6 -27
  10. package/dist/cli/updateOrchestrator.js +63 -484
  11. package/dist/cli/updatePorts.js +15 -333
  12. package/dist/cli/upgradeCommand.js +8 -65
  13. package/dist/cli.js +57 -242
  14. package/dist/commands/configCommands.js +11 -62
  15. package/dist/commands/executionAuditCommands.js +23 -23
  16. package/dist/commands/globalRoleCommands.js +2 -7
  17. package/dist/commands/jobCommands.js +1 -4
  18. package/dist/commands/projectCommands.js +5 -5
  19. package/dist/commands/releaseCommands.js +3 -10
  20. package/dist/commands/resourcesCommands.js +2 -2
  21. package/dist/commands/sessionCommands.js +2 -2
  22. package/dist/commands/taskActor.js +31 -39
  23. package/dist/commands/taskChangeSetCommands.js +5 -12
  24. package/dist/commands/taskCommands.js +464 -1249
  25. package/dist/commands/taskContextCommand.js +35 -36
  26. package/dist/commands/taskExecutionCommands.js +33 -46
  27. package/dist/commands/taskInputCommands.js +20 -42
  28. package/dist/commands/taskNextActionCommand.js +9 -9
  29. package/dist/commands/taskOverviewCommand.js +56 -95
  30. package/dist/commands/taskRoleRuntimeStatus.js +77 -107
  31. package/dist/commands/taskUpstreamCommands.js +3 -3
  32. package/dist/commands/taskWorkspaceCommands.js +7 -79
  33. package/dist/commands/telemetryCommands.js +32 -124
  34. package/dist/commands/workflowCommands.js +3 -3
  35. package/dist/completion/fileCompletionManager.js +1 -1
  36. package/dist/config/configCatalog.js +4 -6
  37. package/dist/config/yuiConfig.js +6 -43
  38. package/dist/context/dispatchContext.js +14 -30
  39. package/dist/context/roleSessionContext.js +2 -2
  40. package/dist/context/sessionBootstrapManifest.js +20 -3
  41. package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
  42. package/dist/context/turnInputContract.js +204 -0
  43. package/dist/context/wakeNotification.js +2 -2
  44. package/dist/controller/agentRuntimeObserver.js +18 -18
  45. package/dist/controller/clientRuntime.js +13 -20
  46. package/dist/controller/controller.js +89 -116
  47. package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
  48. package/dist/controller/jobControl.js +17 -17
  49. package/dist/controller/resourceInventory.js +6 -6
  50. package/dist/controller/resourceInventoryLinux.js +38 -59
  51. package/dist/controller/resourceInventoryRpc.js +3 -3
  52. package/dist/controller/resourceInventoryWorker.js +1 -1
  53. package/dist/controller/runtime.js +46 -53
  54. package/dist/controller/runtimeEventInbox.js +23 -31
  55. package/dist/controller/runtimeEventProcessor.js +15 -15
  56. package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
  57. package/dist/controller/runtimeLaunchCoordinator.js +3 -3
  58. package/dist/controller/runtimeObservationHook.js +11 -8
  59. package/dist/controller/sessionNotify.js +11 -11
  60. package/dist/controller/structuredProviderObservation.js +89 -16
  61. package/dist/coordination/workMailbox.js +309 -579
  62. package/dist/coordination/workMailboxQueue.js +5 -6
  63. package/dist/core/controllerClient.js +1 -91
  64. package/dist/core/controllerServer.js +3 -6
  65. package/dist/doctor/doctor.js +51 -314
  66. package/dist/execution/candidateConvergence.js +3 -3
  67. package/dist/execution/executionGroup.js +32 -33
  68. package/dist/execution/executionHealth.js +36 -38
  69. package/dist/execution/resourceBroker.js +12 -12
  70. package/dist/executor/agentExecutor.js +30 -265
  71. package/dist/executor/effectiveLaunch.js +9 -19
  72. package/dist/executor/executorRegistry.js +43 -17
  73. package/dist/executor/fileRoleLaunchPlanner.js +91 -57
  74. package/dist/executor/launchPlan.js +3 -3
  75. package/dist/executor/workspacePreflightClassification.js +16 -16
  76. package/dist/grant/capabilityGrant.js +20 -25
  77. package/dist/input/inputRequest.js +12 -12
  78. package/dist/integration/changeSet.js +9 -13
  79. package/dist/integration/changeSetManifest.js +1 -1
  80. package/dist/integration/gitIntegrationService.js +2 -2
  81. package/dist/integration/integrationQueueService.js +5 -21
  82. package/dist/integration/overlapDiagnostics.js +2 -2
  83. package/dist/interaction/operatorPresentation.js +1 -1
  84. package/dist/job/jobRunner.js +1 -1
  85. package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
  86. package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
  87. package/dist/message/message.js +6 -6
  88. package/dist/observability/executionAudit.js +74 -86
  89. package/dist/observability/faultClassification.js +7 -24
  90. package/dist/observability/orchestrationMetrics.js +5 -5
  91. package/dist/observability/runtimeIdentity.js +34 -101
  92. package/dist/profile/agentProfile.js +1 -1
  93. package/dist/release/cliHomeReleaseFence.js +3 -3
  94. package/dist/release/releaseWorkflowEngine.js +6 -10
  95. package/dist/release/releaseWorkflowPorts.js +2 -2
  96. package/dist/release/workflowFileLock.js +1 -1
  97. package/dist/repository/checkoutSwap.js +1 -1
  98. package/dist/repository/gitWorkspace.js +0 -13
  99. package/dist/repository/project.js +1 -1
  100. package/dist/repository/projectMaintenanceLock.js +1 -2
  101. package/dist/repository/taskBaseFreshness.js +13 -13
  102. package/dist/repository/taskWorkspaceCoordinator.js +21 -25
  103. package/dist/repository/taskWorkspaceIdentity.js +8 -21
  104. package/dist/repository/taskWorkspacePreparer.js +63 -620
  105. package/dist/resources/autoResourceGc.js +3 -3
  106. package/dist/resources/liveReferences.js +1 -1
  107. package/dist/resources/resourceGc.js +1 -1
  108. package/dist/resources/sqliteResourceRegistry.js +1 -1
  109. package/dist/review/reviewDecision.js +7 -7
  110. package/dist/review/reviewFindingLedger.js +1 -1
  111. package/dist/review/reviewOutcomeClassifier.js +35 -50
  112. package/dist/review/reviewRound.js +14 -25
  113. package/dist/review/reviewerAvailability.js +4 -4
  114. package/dist/review/taskFinalReviewContractResolution.js +41 -0
  115. package/dist/role/role.js +6 -3
  116. package/dist/runtime/agentDriver.js +4 -0
  117. package/dist/runtime/agentHost.js +151 -36
  118. package/dist/runtime/builtinAgentDrivers.js +79 -2
  119. package/dist/runtime/codexAppServerRuntime.js +122 -0
  120. package/dist/runtime/continuationManager.js +3 -5
  121. package/dist/runtime/exactControlPlane.js +17 -42
  122. package/dist/runtime/firstProgressAdvisory.js +1 -1
  123. package/dist/runtime/index.js +2 -2
  124. package/dist/runtime/launchDiagnostics.js +2 -2
  125. package/dist/runtime/lifecycleReservation.js +4 -4
  126. package/dist/runtime/processExitObservation.js +1 -1
  127. package/dist/runtime/processExitOutbox.js +0 -32
  128. package/dist/runtime/promptEnvelope.js +9 -9
  129. package/dist/runtime/providerContinuation.js +2 -2
  130. package/dist/runtime/providerContinuationReconciliationService.js +2 -2
  131. package/dist/runtime/providerRuntimeIdentity.js +76 -24
  132. package/dist/runtime/recentTurnIds.js +38 -0
  133. package/dist/runtime/runtimeContinuationProjection.js +2 -2
  134. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  135. package/dist/runtime/runtimeObservation.js +58 -21
  136. package/dist/runtime/runtimeProjection.js +19 -27
  137. package/dist/runtime/sessionLaunchRequest.js +2 -2
  138. package/dist/runtime/structuredProviderHost.js +177 -12
  139. package/dist/runtime/tmuxAdapters.js +46 -11
  140. package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
  141. package/dist/scheduler/leaderWakeupProcessor.js +147 -445
  142. package/dist/scheduler/operatorEvent.js +4 -6
  143. package/dist/scheduler/resourceQueueProjection.js +71 -0
  144. package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
  145. package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
  146. package/dist/scheduler/taskExecutionProjection.js +99 -99
  147. package/dist/scheduler/taskObservabilityProjection.js +10 -10
  148. package/dist/scheduler/taskWake.js +4 -4
  149. package/dist/scheduler/wakeReason.js +6 -10
  150. package/dist/scheduler/wakeupQueue.js +0 -9
  151. package/dist/setup/setupCommand.js +2 -2
  152. package/dist/storage/currentTaskStore.js +46 -0
  153. package/dist/storage/persistenceWorker.js +1 -1
  154. package/dist/storage/sqliteSchema.js +81 -485
  155. package/dist/storage/sqliteStore.js +206 -365
  156. package/dist/storage/storageSchema.js +1 -15
  157. package/dist/storage/storageVersions.js +8 -9
  158. package/dist/storage/storeRpc.js +10 -34
  159. package/dist/storage/taskStore.js +63 -3415
  160. package/dist/storage/upgrade/recordVersions.js +55 -149
  161. package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
  162. package/dist/task/completionReadiness.js +5 -5
  163. package/dist/task/deliveryGuard.js +10 -10
  164. package/dist/task/nextAction.js +74 -77
  165. package/dist/task/task.js +2 -4
  166. package/dist/task/taskRecordReference.js +3 -3
  167. package/dist/task/taskRecordRetirement.js +1 -1
  168. package/dist/telemetry/sqliteTelemetryStore.js +53 -53
  169. package/dist/telemetry/telemetryConfig.js +14 -14
  170. package/dist/telemetry/telemetryWiring.js +8 -14
  171. package/dist/turn/turn.js +356 -0
  172. package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
  173. package/dist/verification/gateArtifactStore.js +2 -2
  174. package/dist/web/assets/client/app.js +1 -1
  175. package/dist/web/assets/client/components.js +29 -54
  176. package/dist/web/assets/client/i18n.js +17 -37
  177. package/dist/web/assets/client/view.js +37 -37
  178. package/dist/web/assets/shell.js +1 -1
  179. package/dist/web/assets/styles/cards.js +5 -5
  180. package/dist/web/assets/styles/widgets.js +1 -2
  181. package/dist/web/webSnapshot.js +65 -60
  182. package/dist/workItem/workItem.js +11 -11
  183. package/dist/workspace/workItemChangeSetManager.js +1 -1
  184. package/docs/task-local-identity.md +5 -5
  185. package/i18n/README.zh-CN.md +42 -55
  186. package/package.json +1 -1
  187. package/skills/yui-leader/SKILL.md +85 -83
  188. package/skills/yui-operator/SKILL.md +16 -16
  189. package/skills/yui-reviewer/SKILL.md +10 -10
  190. package/skills/yui-runtime/SKILL.md +22 -24
  191. package/skills/yui-worker/SKILL.md +16 -18
  192. package/dist/context/runContextContract.js +0 -162
  193. package/dist/executor/turnCompletion.js +0 -126
  194. package/dist/review/taskFinalReviewContractEvent.js +0 -1
  195. package/dist/review/taskFinalReviewContractRebind.js +0 -367
  196. package/dist/run/agentRun.js +0 -356
  197. package/dist/run/rejectedYieldAttempt.js +0 -221
  198. package/dist/run/runControlRequest.js +0 -50
  199. package/dist/run/yieldReceipt.js +0 -65
  200. package/dist/runtime/turnCompletion.js +0 -3
  201. package/dist/scheduler/actionability.js +0 -323
  202. package/dist/scheduler/activeRoleRunDelivery.js +0 -868
  203. package/dist/scheduler/activeTaskProgress.js +0 -126
  204. package/dist/storage/compatibleTaskStore.js +0 -248
  205. package/dist/storage/migration/baseline.js +0 -78
  206. package/dist/storage/migration/classifier.js +0 -51
  207. package/dist/storage/migration/compatibleCodec.js +0 -53
  208. package/dist/storage/migration/engine.js +0 -147
  209. package/dist/storage/migration/index.js +0 -33
  210. package/dist/storage/migration/planner.js +0 -154
  211. package/dist/storage/migration/productionRegistry.js +0 -3931
  212. package/dist/storage/migration/registry.js +0 -169
  213. package/dist/storage/migration/report.js +0 -54
  214. package/dist/storage/migration/types.js +0 -31
  215. package/dist/storage/upgrade/homeClassification.js +0 -353
  216. package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
  217. package/dist/storage/upgrade/migrationReceipt.js +0 -67
  218. package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
  219. package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
  220. package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
  221. package/dist/storage/upgrade/recordVersionScan.js +0 -176
  222. package/dist/storage/upgrade/sqliteFileSet.js +0 -50
  223. package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
  224. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
  225. package/dist/storage/upgrade/sqliteStateMigration.js +0 -926
  226. package/dist/storage/upgrade/switchProgress.js +0 -80
  227. package/dist/storage/upgrade/upgradeReceipt.js +0 -161
  228. package/dist/storage/upgradeCoordination.js +0 -187
  229. package/dist/storage/upgradeFence.js +0 -366
  230. package/dist/telemetry/telemetryCompaction.js +0 -252
@@ -1,241 +0,0 @@
1
- /**
2
- * Deterministic repair for a *pseudo-layout-7* Home (Issue 01).
3
- *
4
- * A pseudo-layout-7 Home declares layout 7 in `schema.json` but has no `yui.db`;
5
- * its `state.json` is still the only authoritative copy. The classifier reports
6
- * this as `NEEDS_STORAGE_REPAIR`. This module rebuilds the SQLite database from
7
- * the pinned `state.json`, verifies every record family against an independent
8
- * re-read, promotes the staged database atomically, certifies the switch with a
9
- * persistent migration receipt, read-backs through a fresh store, and archives
10
- * `state.json` to a timestamped backup so it can never serve as a writable
11
- * fallback again.
12
- *
13
- * Failure semantics (issue: 最简失败语义):
14
- * - staging/verification failure: `state.json` stays authoritative, the staged
15
- * database is discarded, the manifest is untouched;
16
- * - a stale staged database from a crashed attempt is always rebuilt, never
17
- * reused;
18
- * - any failure after the atomic promote quarantines the promoted database and
19
- * removes the receipt, returning the Home to its exact pre-repair shape;
20
- * - the one non-fatal tail is archiving `state.json`: if that rename fails the
21
- * database is already promoted, verified, and receipt-certified, and the
22
- * result is `blocked` with the exact manual finishing step.
23
- */
24
- import { createHash } from "node:crypto";
25
- import { existsSync, readdirSync, readFileSync, renameSync, rmSync } from "node:fs";
26
- import { join } from "node:path";
27
- import { readStorageSchemaManifest } from "../storageSchema.js";
28
- import { STORAGE_STATE_FILE } from "../taskStore.js";
29
- import { SqliteTaskStore } from "../sqliteStore.js";
30
- import { migrationReceiptPath, writeMigrationReceipt } from "./migrationReceipt.js";
31
- import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, computeDbFamilyChecksums, populateSqliteFromState, verifySqliteChecksums } from "./sqliteStateMigration.js";
32
- /**
33
- * Run the staged state.json→SQLite repair. Never throws for an expected
34
- * blocker; a malformed manifest or an unreadable `state.json` is a `blocked`
35
- * result, not an exception.
36
- */
37
- export function repairPseudoLayout7(options) {
38
- const { home, latest, mode } = options;
39
- const now = options.now ?? (() => new Date());
40
- const statePath = join(home, STORAGE_STATE_FILE);
41
- const stagedPath = join(home, STAGED_DATABASE_FILENAME);
42
- const committedPath = join(home, COMMITTED_DATABASE_FILENAME);
43
- // 1. Re-verify the preconditions fail-closed; the repair never trusts a
44
- // classifier verdict produced by an earlier process.
45
- let manifest;
46
- try {
47
- manifest = readStorageSchemaManifest(home);
48
- }
49
- catch (error) {
50
- return blocked("validate", `The storage manifest could not be read: ${messageOf(error)}`, "Restore schema.json from a backup; the repair requires a readable layout-7 manifest.");
51
- }
52
- if (manifest.storageVersion !== latest.layout) {
53
- return blocked("validate", `Pseudo-layout-7 repair requires a layout-${latest.layout} manifest; found layout ${manifest.storageVersion}.`, "Re-run `yui doctor`; this repair only applies to a pseudo-layout-7 Home.");
54
- }
55
- if (existsSync(committedPath)) {
56
- return blocked("validate", `Refusing to repair: ${COMMITTED_DATABASE_FILENAME} already exists.`, "The Home already has a SQLite database. If it is damaged, restore it from a backup; do not rebuild over it.");
57
- }
58
- if (!existsSync(statePath)) {
59
- return blocked("validate", `Pseudo-layout-7 repair requires a readable ${STORAGE_STATE_FILE}; none exists.`, `Restore ${STORAGE_STATE_FILE} from a backup; the repair cannot rebuild a database without its source.`);
60
- }
61
- // 2. Pin state.json (revision, size, sha256). A document that is not a
62
- // strictly readable JSON object fails the repair.
63
- const pin = pinStateFile(statePath);
64
- if (pin === null) {
65
- return blocked("validate", `${STORAGE_STATE_FILE} is not a strictly readable JSON object.`, "The source document is damaged; restore it from a backup before repairing.");
66
- }
67
- // 3. A stale staged database from a crashed attempt is rebuilt, never reused.
68
- discardStaged(stagedPath);
69
- // 4. Stage: populate yui.db.staged from the pinned document.
70
- try {
71
- populateSqliteFromState(home, pin.state, STAGED_DATABASE_FILENAME);
72
- }
73
- catch (error) {
74
- discardStaged(stagedPath);
75
- return blocked("validate", `Staging the SQLite database failed: ${messageOf(error)}`, `${STORAGE_STATE_FILE} remains the authoritative store; the staged database was discarded.`);
76
- }
77
- // 5. Verify: the pinned bytes must be unchanged (no concurrent writer), and
78
- // every record family must match an independent state.json re-read by
79
- // count and content checksum.
80
- try {
81
- if (sha256(readFileSync(statePath, "utf8")) !== pin.sha256) {
82
- discardStaged(stagedPath);
83
- return blocked("validate", `${STORAGE_STATE_FILE} changed during the repair; a concurrent writer is active.`, "Quiesce all writers and retry; the staged database was discarded and state.json remains authoritative.");
84
- }
85
- verifySqliteChecksums(pin.state, home, STAGED_DATABASE_FILENAME);
86
- }
87
- catch (error) {
88
- discardStaged(stagedPath);
89
- return blocked("validate", `Staged database verification failed: ${messageOf(error)}`, `${STORAGE_STATE_FILE} remains the authoritative store; the staged database was discarded.`);
90
- }
91
- const verifiedFamilies = Object.keys(computeDbFamilyChecksums(home, STAGED_DATABASE_FILENAME)).length;
92
- if (mode === "dry-run") {
93
- discardStaged(stagedPath);
94
- return { outcome: "dry-run", verifiedFamilies, sourceRevision: pin.revision };
95
- }
96
- // 6. Promote: atomic rename staged -> yui.db. From here the database exists;
97
- // any failure rolls the promotion back so the Home keeps its pre-repair
98
- // shape (manifest 7, no yui.db, state.json authoritative).
99
- try {
100
- renameSync(stagedPath, committedPath);
101
- // The staged connection may leave empty WAL/SHM sidecars behind even
102
- // after a clean close; they are dead once the main file is promoted.
103
- rmSync(`${stagedPath}-wal`, { force: true });
104
- rmSync(`${stagedPath}-shm`, { force: true });
105
- }
106
- catch (error) {
107
- discardStaged(stagedPath);
108
- return blocked("switch", `Promoting the staged database failed: ${messageOf(error)}`, `${STORAGE_STATE_FILE} remains the authoritative store; the staged database was discarded.`);
109
- }
110
- // 7. Write the persistent migration receipt. A dual-copy Home without a
111
- // receipt is a conflict, so a receipt-write failure rolls the promotion
112
- // back rather than leaving an uncertified database behind.
113
- try {
114
- writeMigrationReceipt(home, {
115
- kind: "pseudo-layout-7-repair",
116
- completedAt: now().toISOString(),
117
- sourceRevision: pin.revision,
118
- targetLayoutVersion: latest.layout,
119
- sourceStateSha256: pin.sha256,
120
- verifiedFamilies
121
- });
122
- }
123
- catch (error) {
124
- rollbackPromotion(home, committedPath, now);
125
- return blocked("post-verify", `The database was promoted but the migration receipt could not be written: ${messageOf(error)}`, `The promoted database was quarantined and the receipt removed; ${STORAGE_STATE_FILE} remains authoritative. Retry the repair.`);
126
- }
127
- // 8. Read-back through a fresh store, including revision continuity.
128
- try {
129
- const store = new SqliteTaskStore(home);
130
- try {
131
- store.getConfig();
132
- store.listTasks();
133
- store.listProjects();
134
- store.listConfiguredAgents();
135
- store.listWorkMailboxes();
136
- const dbRevision = store.getRevision();
137
- if (dbRevision !== pin.revision) {
138
- throw new Error(`revision mismatch: ${STORAGE_STATE_FILE}=${pin.revision} database=${dbRevision}`);
139
- }
140
- }
141
- finally {
142
- store.close();
143
- }
144
- }
145
- catch (error) {
146
- rollbackPromotion(home, committedPath, now);
147
- return blocked("post-verify", `Post-promote read-back failed: ${messageOf(error)}`, `The promoted database was quarantined and the receipt removed; ${STORAGE_STATE_FILE} remains authoritative. Retry the repair.`);
148
- }
149
- // 9. Archive state.json so it can never serve as a writable fallback. The
150
- // receipt certifies the dual-copy window, so a failure here leaves a
151
- // usable Home: the database is authoritative and the repair is finished by
152
- // moving the file manually.
153
- const stamp = now().toISOString().replace(/[:.]/g, "-");
154
- const stateBackupPath = join(home, `${STORAGE_STATE_FILE}.backup-${stamp}`);
155
- try {
156
- if (existsSync(stateBackupPath)) {
157
- throw new Error(`refusing to overwrite an existing state backup: ${stateBackupPath}`);
158
- }
159
- renameSync(statePath, stateBackupPath);
160
- }
161
- catch (error) {
162
- return blocked("post-verify", `The database was promoted and verified, but ${STORAGE_STATE_FILE} could not be archived: ${messageOf(error)}`, `The database is authoritative and the migration receipt certifies it. Move ${STORAGE_STATE_FILE} to ${stateBackupPath} manually to finish the repair.`);
163
- }
164
- return {
165
- outcome: "repaired",
166
- stateBackupPath,
167
- verifiedFamilies,
168
- sourceRevision: pin.revision
169
- };
170
- }
171
- /** The newest `state.json.backup-*` path in a Home, or `null` when none exists. */
172
- export function latestStateBackupPath(home) {
173
- const entries = listStateBackups(home);
174
- return entries.length === 0 ? null : entries[entries.length - 1];
175
- }
176
- /** All `state.json.backup-*` paths in a Home, sorted oldest-first. */
177
- export function listStateBackups(home) {
178
- let names;
179
- try {
180
- names = readdirSync(home);
181
- }
182
- catch {
183
- return [];
184
- }
185
- return names
186
- .filter((name) => name.startsWith(`${STORAGE_STATE_FILE}.backup-`))
187
- .sort()
188
- .map((name) => join(home, name));
189
- }
190
- function pinStateFile(statePath) {
191
- let raw;
192
- try {
193
- raw = readFileSync(statePath, "utf8");
194
- }
195
- catch {
196
- return null;
197
- }
198
- let parsed;
199
- try {
200
- parsed = JSON.parse(raw);
201
- }
202
- catch {
203
- return null;
204
- }
205
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
206
- return null;
207
- }
208
- const state = parsed;
209
- return {
210
- state,
211
- revision: typeof state.revision === "number" ? state.revision : 0,
212
- sha256: createHash("sha256").update(raw, "utf8").digest("hex"),
213
- size: Buffer.byteLength(raw, "utf8")
214
- };
215
- }
216
- function sha256(raw) {
217
- return createHash("sha256").update(raw, "utf8").digest("hex");
218
- }
219
- function discardStaged(stagedPath) {
220
- rmSync(stagedPath, { force: true });
221
- rmSync(`${stagedPath}-wal`, { force: true });
222
- rmSync(`${stagedPath}-shm`, { force: true });
223
- }
224
- function quarantine(committedPath, now) {
225
- const stamp = now().toISOString().replace(/[:.]/g, "-");
226
- const quarantinePath = `${committedPath}.quarantine-${stamp}`;
227
- renameSync(committedPath, quarantinePath);
228
- rmSync(`${committedPath}-wal`, { force: true });
229
- rmSync(`${committedPath}-shm`, { force: true });
230
- return quarantinePath;
231
- }
232
- function rollbackPromotion(home, committedPath, now) {
233
- quarantine(committedPath, now);
234
- rmSync(migrationReceiptPath(home), { force: true });
235
- }
236
- function blocked(stage, message, action) {
237
- return { outcome: "blocked", stage, message, action };
238
- }
239
- function messageOf(error) {
240
- return error instanceof Error ? error.message : String(error);
241
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * Pure, read-only structural scan of raw `state.json`, never through the strict
3
- * `parseState`/`FileTaskStore` gate. The durable manifest remains authoritative;
4
- * this scanner supplies member versions and counts so callers can detect
5
- * manifest/state contradictions without treating an empty locator as current.
6
- * Older member versions remain version evidence rather than a strict-loader
7
- * parse failure, while malformed JSON/containers/schemaVersion fields are real
8
- * `CORRUPTED` evidence.
9
- *
10
- * The `path` for each family (from `recordVersions.ts`) is a small locator into
11
- * `state.json`: segments are `/`-separated after the `state.json#/` prefix, and a
12
- * `*` segment fans out over every member of the map at that level (so the locator
13
- * `state.json#/tasks/{star}/workItems` visits the `workItems` map of every task,
14
- * where `{star}` is a literal `*`).
15
- */
16
- import { existsSync, readFileSync } from "node:fs";
17
- import { join } from "node:path";
18
- import { STORAGE_STATE_FILE } from "../taskStore.js";
19
- /**
20
- * Scan per-family member versions and counts from raw `state.json`.
21
- *
22
- * Rules, per family, evaluated purely (no strict parser):
23
- * - family absent or empty on disk -> latest as an internal scan sentinel,
24
- * with count zero so the manifest combiner never infers source currency;
25
- * - every member at one older/current/newer version -> report that version;
26
- * - members carrying different versions -> corruption.
27
- *
28
- * A family is one durable schema boundary, so a manifest cannot make a mixed
29
- * member set trustworthy by matching either its minimum or maximum. Mixed
30
- * members are partial-write/corruption evidence, not a migration source version.
31
- *
32
- * Returns a `corruption` instead when the JSON is unparseable, the root is not an
33
- * object, a path container is not the shape the locator describes, or a record is
34
- * missing/has an invalid `schemaVersion`.
35
- */
36
- export function scanSourceRecordVersions(home, latestRecord) {
37
- const statePath = join(home, STORAGE_STATE_FILE);
38
- if (!existsSync(statePath)) {
39
- // No state.json means zero persisted members. The latest-valued scan entry
40
- // is only a sentinel; counts preserve absence for the manifest combiner.
41
- return {
42
- record: cloneRecord(latestRecord),
43
- counts: Object.fromEntries(Object.keys(latestRecord).map((kind) => [kind, 0]))
44
- };
45
- }
46
- let root;
47
- try {
48
- root = JSON.parse(readFileSync(statePath, "utf8"));
49
- }
50
- catch (error) {
51
- return corruption(`state.json is not valid JSON: ${messageOf(error)}`);
52
- }
53
- if (!isObject(root)) {
54
- return corruption("state.json is not a JSON object.");
55
- }
56
- return scanRecordVersionsFromState(root, latestRecord);
57
- }
58
- /** Extract record versions from an already-parsed state snapshot, read-only. */
59
- export function scanRecordVersionsFromState(root, latestRecord) {
60
- if (!isObject(root))
61
- return corruption("state snapshot is not a JSON object.");
62
- const record = {};
63
- const counts = {};
64
- for (const [kind, entry] of Object.entries(latestRecord)) {
65
- const scan = scanFamily(root, entry.path, entry.version);
66
- if ("corruption" in scan) {
67
- return corruption(`record family '${kind}' (${entry.path}): ${scan.corruption}`);
68
- }
69
- record[kind] = { version: scan.version, path: entry.path };
70
- counts[kind] = scan.count;
71
- }
72
- return { record, counts };
73
- }
74
- /** Resolve one family's source version from `root` following `path`. */
75
- function scanFamily(root, path, latestVersion) {
76
- const segments = parseLocator(path);
77
- if (segments === null)
78
- return { corruption: `unrecognized path syntax "${path}"` };
79
- // Resolve the path to the family's endpoint node(s). A `*` fans out over the
80
- // members of the map at that level; an absent named key contributes nothing
81
- // (that branch simply has no records), but a present-but-wrong-typed node is
82
- // structural damage.
83
- let nodes = [root];
84
- for (const segment of segments) {
85
- const next = [];
86
- for (const node of nodes) {
87
- if (segment === "*") {
88
- if (!isObject(node))
89
- return { corruption: `expected an object to enumerate at "*"` };
90
- next.push(...Object.values(node));
91
- }
92
- else {
93
- if (!isObject(node))
94
- return { corruption: `expected an object at "${segment}"` };
95
- if (!Object.hasOwn(node, segment))
96
- continue; // absent branch: no records here
97
- next.push(node[segment]);
98
- }
99
- }
100
- nodes = next;
101
- }
102
- // Each endpoint is either a single record (it carries `schemaVersion`, e.g.
103
- // tasks/* (the StoredTask aggregate), tasks/*/task, tasks/*/brief) or a map of records (its values carry
104
- // `schemaVersion`, e.g. configuredAgents, tasks/*/workItems).
105
- let min = Number.POSITIVE_INFINITY;
106
- let max = Number.NEGATIVE_INFINITY;
107
- let count = 0;
108
- for (const endpoint of nodes) {
109
- if (endpoint === null || endpoint === undefined)
110
- continue; // e.g. brief: null
111
- if (!isObject(endpoint))
112
- return { corruption: "family endpoint is not an object" };
113
- if (typeof endpoint.schemaVersion === "number") {
114
- const version = readSchemaVersion(endpoint);
115
- if (version === null)
116
- return { corruption: `record has an invalid schemaVersion` };
117
- min = Math.min(min, version);
118
- max = Math.max(max, version);
119
- count += 1;
120
- continue;
121
- }
122
- for (const member of Object.values(endpoint)) {
123
- if (member === null)
124
- continue;
125
- if (!isObject(member))
126
- return { corruption: "record member is not an object" };
127
- const version = readSchemaVersion(member);
128
- if (version === null)
129
- return { corruption: "record member has a missing/invalid schemaVersion" };
130
- min = Math.min(min, version);
131
- max = Math.max(max, version);
132
- count += 1;
133
- }
134
- }
135
- if (count === 0)
136
- return { version: latestVersion, count }; // zero-count sentinel only
137
- if (min !== max) {
138
- return {
139
- corruption: `family contains mixed schemaVersion values ${min} and ${max}`
140
- };
141
- }
142
- return { version: min, count };
143
- }
144
- /** Parse a `state.json#/`-prefixed locator into path segments, or `null`. */
145
- function parseLocator(path) {
146
- const prefix = `${STORAGE_STATE_FILE}#/`;
147
- if (!path.startsWith(prefix))
148
- return null;
149
- const body = path.slice(prefix.length);
150
- if (body.length === 0)
151
- return null;
152
- return body.split("/");
153
- }
154
- /** A positive-integer `schemaVersion`, or `null` when missing/invalid. */
155
- function readSchemaVersion(record) {
156
- const version = record.schemaVersion;
157
- if (typeof version !== "number" || !Number.isInteger(version) || version < 1)
158
- return null;
159
- return version;
160
- }
161
- function cloneRecord(record) {
162
- const copy = {};
163
- for (const [kind, entry] of Object.entries(record)) {
164
- copy[kind] = { version: entry.version, path: entry.path };
165
- }
166
- return copy;
167
- }
168
- function corruption(detail) {
169
- return { corruption: { corrupted: true, detail } };
170
- }
171
- function isObject(value) {
172
- return typeof value === "object" && value !== null && !Array.isArray(value);
173
- }
174
- function messageOf(error) {
175
- return error instanceof Error ? error.message : String(error);
176
- }
@@ -1,50 +0,0 @@
1
- import { existsSync, renameSync, rmSync } from "node:fs";
2
- const SQLITE_FILE_SUFFIXES = ["", "-wal", "-shm"];
3
- /**
4
- * Move a SQLite database together with any live WAL/SHM sidecars.
5
- *
6
- * A WAL database is one logical file set. Moving only the main file can leave
7
- * the old WAL under the promoted database name, causing SQLite to replay it
8
- * against an unrelated database. The migration fence guarantees there are no
9
- * writers while this bounded rename runs.
10
- */
11
- export function moveSqliteFileSet(sourcePath, targetPath, rename = renameSync) {
12
- const suffixes = SQLITE_FILE_SUFFIXES.filter((suffix) => existsSync(`${sourcePath}${suffix}`));
13
- for (const suffix of suffixes) {
14
- if (existsSync(`${targetPath}${suffix}`)) {
15
- throw new Error(`Refusing to overwrite an existing SQLite switch target: ${targetPath}${suffix}.`);
16
- }
17
- }
18
- const moved = [];
19
- try {
20
- for (const suffix of suffixes) {
21
- rename(`${sourcePath}${suffix}`, `${targetPath}${suffix}`);
22
- moved.push(suffix);
23
- }
24
- }
25
- catch (error) {
26
- let rollbackError;
27
- for (const suffix of moved.reverse()) {
28
- try {
29
- rename(`${targetPath}${suffix}`, `${sourcePath}${suffix}`);
30
- }
31
- catch (candidate) {
32
- rollbackError ??= candidate;
33
- }
34
- }
35
- if (rollbackError !== undefined) {
36
- throw new Error(`SQLite file-set move failed and rollback was incomplete: ${messageOf(error)}; `
37
- + `rollback failed: ${messageOf(rollbackError)}`);
38
- }
39
- throw error;
40
- }
41
- }
42
- /** Remove a promoted SQLite file set before restoring its timestamped backup. */
43
- export function removeSqliteFileSet(databasePath) {
44
- for (const suffix of SQLITE_FILE_SUFFIXES) {
45
- rmSync(`${databasePath}${suffix}`, { force: true });
46
- }
47
- }
48
- function messageOf(error) {
49
- return error instanceof Error ? error.message : String(error);
50
- }