@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.
- package/ARCHITECTURE.md +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- 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
|
-
}
|