@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,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
- }