@zq-silk/yui 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQLite-backed {@link MigrationTarget} for record-family migrations on a
|
|
3
|
+
* layout-7 Home (Issue 01 Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* The layout 6 -> 7 target ({@link createSqliteMigrationTarget}) stages a
|
|
6
|
+
* fresh database from `state.json`. Once a Home is at layout 7 its
|
|
7
|
+
* authoritative store is `yui.db`; `state.json` may have been archived by the
|
|
8
|
+
* pseudo-layout-7 repair, so a record-only migration on such a Home cannot
|
|
9
|
+
* read its source from the document. This target closes that gap:
|
|
10
|
+
*
|
|
11
|
+
* - Snapshot: `readSource` reads `schema.json` and reconstructs the
|
|
12
|
+
* state.json-shaped snapshot from `yui.db` via
|
|
13
|
+
* {@link readStateFromSqlite} (raw payloads, so older record
|
|
14
|
+
* versions survive with their original `schemaVersion`).
|
|
15
|
+
* - Stage: `writeFreshOutput` populates a sidecar `yui.db.staged` from
|
|
16
|
+
* the (possibly transformed) snapshot; refuses to overwrite an
|
|
17
|
+
* existing stage.
|
|
18
|
+
* - Verify: `validateCurrentState` independently re-reads `yui.db`,
|
|
19
|
+
* re-derives the expected state through the registered
|
|
20
|
+
* transforms, and compares per-family checksums against the
|
|
21
|
+
* staged database.
|
|
22
|
+
* - Commit: `atomicSwitchWithBackup` swaps the sidecar into `yui.db` (with
|
|
23
|
+
* a timestamped backup of the prior database) and advances
|
|
24
|
+
* `schema.json`'s record-family versions in the same
|
|
25
|
+
* coordination critical section. The layout version is
|
|
26
|
+
* unchanged (this target never crosses a layout boundary).
|
|
27
|
+
*
|
|
28
|
+
* The source `yui.db` is retained read-only throughout: it is never
|
|
29
|
+
* overwritten, truncated, or deleted by the migration. This preserves the
|
|
30
|
+
* rollback path (restore the timestamped backup).
|
|
31
|
+
*/
|
|
32
|
+
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { writeTextFileAtomically } from "../durableFile.js";
|
|
35
|
+
import { STORAGE_SCHEMA_FILE } from "../storageSchema.js";
|
|
36
|
+
import { planMigration } from "../migration/planner.js";
|
|
37
|
+
import { AmbiguousSwitchError } from "../migration/index.js";
|
|
38
|
+
import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
|
|
39
|
+
import { writeSwitchProgress } from "./switchProgress.js";
|
|
40
|
+
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
|
|
41
|
+
/** Build the SQLite-backed record-migration target. */
|
|
42
|
+
export function createSqliteRecordMigrationTarget(options) {
|
|
43
|
+
const home = options.home;
|
|
44
|
+
const latest = options.latest;
|
|
45
|
+
const registry = options.registry;
|
|
46
|
+
const now = options.now ?? (() => new Date());
|
|
47
|
+
const callerPid = options.callerPid ?? process.pid;
|
|
48
|
+
const promoteRename = options.renameImpl ?? renameSync;
|
|
49
|
+
const stagedDbPath = join(home, STAGED_DATABASE_FILENAME);
|
|
50
|
+
const committedDbPath = join(home, COMMITTED_DATABASE_FILENAME);
|
|
51
|
+
// The transformed schema manifest is cached during writeFreshOutput so the
|
|
52
|
+
// switch can advance schema.json without re-reading or re-deriving it.
|
|
53
|
+
let stagedSchemaManifest = null;
|
|
54
|
+
return {
|
|
55
|
+
stagedDbPath,
|
|
56
|
+
inspectVersions() {
|
|
57
|
+
const inspected = inspectSourceVersionState(home, latest);
|
|
58
|
+
if ("corruption" in inspected) {
|
|
59
|
+
throw new Error(inspected.corruption.detail);
|
|
60
|
+
}
|
|
61
|
+
return inspected.source;
|
|
62
|
+
},
|
|
63
|
+
detectLiveRuntime() {
|
|
64
|
+
const signals = inspectHomeRuntime(home, callerPid);
|
|
65
|
+
if (!homeRuntimeIsActive(signals))
|
|
66
|
+
return { active: false };
|
|
67
|
+
return { active: true, detail: describeActiveRuntime(signals) };
|
|
68
|
+
},
|
|
69
|
+
readSource() {
|
|
70
|
+
const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
|
|
71
|
+
const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
|
|
72
|
+
const state = readStateFromSqlite(home);
|
|
73
|
+
return Object.freeze({ schemaManifest, state });
|
|
74
|
+
},
|
|
75
|
+
writeFreshOutput(snapshot) {
|
|
76
|
+
if (existsSync(stagedDbPath)) {
|
|
77
|
+
throw new Error(`Refusing to overwrite an existing staged SQLite database: ${stagedDbPath}. ` +
|
|
78
|
+
"Discard it and retry.");
|
|
79
|
+
}
|
|
80
|
+
// Cache the transformed manifest for the switch's schema.json advancement.
|
|
81
|
+
// The orchestrator applies the record transforms before calling us, but
|
|
82
|
+
// direct callers (tests, drills) may pass an untransformed snapshot. The
|
|
83
|
+
// record-family versions are re-derived from the latest map so the
|
|
84
|
+
// staged manifest always declares the post-migration versions.
|
|
85
|
+
const recordVersions = {};
|
|
86
|
+
for (const [kind, entry] of Object.entries(latest.record)) {
|
|
87
|
+
recordVersions[kind] = entry.version;
|
|
88
|
+
}
|
|
89
|
+
stagedSchemaManifest = {
|
|
90
|
+
...snapshot.schemaManifest,
|
|
91
|
+
recordVersions,
|
|
92
|
+
updatedAt: now().toISOString()
|
|
93
|
+
};
|
|
94
|
+
populateSqliteFromState(home, snapshot.state ?? {}, STAGED_DATABASE_FILENAME);
|
|
95
|
+
},
|
|
96
|
+
rebuildDerivedState(effects) {
|
|
97
|
+
// The SQLite database is fully normalised by populateSqliteFromState;
|
|
98
|
+
// there is no separate derived index to rebuild. Echo the declared
|
|
99
|
+
// effects for the report, mirroring the layout 6 -> 7 target.
|
|
100
|
+
return { rebuiltEffects: [...effects] };
|
|
101
|
+
},
|
|
102
|
+
validateCurrentState() {
|
|
103
|
+
// Independently re-read yui.db from disk (not the in-memory snapshot
|
|
104
|
+
// used for staging) and re-derive the expected state by applying the
|
|
105
|
+
// registered transforms. This catches staging corruption, a torn read,
|
|
106
|
+
// or a concurrent writer that slipped past the quiesce gate.
|
|
107
|
+
const freshSnapshot = readSourceFresh();
|
|
108
|
+
const expectedState = deriveExpectedState(freshSnapshot);
|
|
109
|
+
if (expectedState !== null) {
|
|
110
|
+
verifyChecksums(expectedState);
|
|
111
|
+
}
|
|
112
|
+
const dbChecksums = computeDbFamilyChecksums(home, STAGED_DATABASE_FILENAME);
|
|
113
|
+
const familyCount = Object.keys(dbChecksums).length;
|
|
114
|
+
return {
|
|
115
|
+
checks: [
|
|
116
|
+
{
|
|
117
|
+
name: "SQLite staged-database checksum verification",
|
|
118
|
+
outcome: "passed",
|
|
119
|
+
detail: `verified ${familyCount} record families against an independent yui.db re-read`
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
atomicSwitchWithBackup() {
|
|
125
|
+
if (!existsSync(stagedDbPath)) {
|
|
126
|
+
throw new Error(`No staged SQLite database to promote: ${stagedDbPath}.`);
|
|
127
|
+
}
|
|
128
|
+
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
129
|
+
let backupPath;
|
|
130
|
+
// Phase 1: back up the existing yui.db, then promote the sidecar.
|
|
131
|
+
try {
|
|
132
|
+
if (existsSync(committedDbPath)) {
|
|
133
|
+
backupPath = join(home, `${COMMITTED_DATABASE_FILENAME}.backup-${stamp}`);
|
|
134
|
+
if (existsSync(backupPath)) {
|
|
135
|
+
throw new Error(`Refusing to overwrite an existing database backup: ${backupPath}.`);
|
|
136
|
+
}
|
|
137
|
+
renameSync(committedDbPath, backupPath);
|
|
138
|
+
}
|
|
139
|
+
promoteRename(stagedDbPath, committedDbPath);
|
|
140
|
+
// The staged connection may leave empty WAL/SHM sidecars behind
|
|
141
|
+
// even after a clean close; they are dead once promoted.
|
|
142
|
+
rmSync(`${stagedDbPath}-wal`, { force: true });
|
|
143
|
+
rmSync(`${stagedDbPath}-shm`, { force: true });
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// Pre-promotion failure: restore the original database if we moved it.
|
|
147
|
+
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
148
|
+
try {
|
|
149
|
+
promoteRename(backupPath, committedDbPath);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
writeInterruptedMarker(home, backupPath, stagedDbPath, now);
|
|
153
|
+
throw new AmbiguousSwitchError({
|
|
154
|
+
homePath: home,
|
|
155
|
+
backupPath,
|
|
156
|
+
stagingPath: stagedDbPath,
|
|
157
|
+
detail: `SQLite switch failed (${messageOf(error)}) and the automatic rollback also failed. ` +
|
|
158
|
+
`The original database is at ${backupPath}; recover manually by renaming it to ${committedDbPath}.`
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
// Phase 2: advance schema.json record-family versions in the same
|
|
165
|
+
// critical section. The layout version is unchanged.
|
|
166
|
+
try {
|
|
167
|
+
if (stagedSchemaManifest !== null) {
|
|
168
|
+
writeTextFileAtomically(join(home, STORAGE_SCHEMA_FILE), `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
// The database is promoted but schema.json could not be advanced.
|
|
173
|
+
// Attempt to restore the original database; if that fails, the Home
|
|
174
|
+
// is ambiguous and must be recovered manually.
|
|
175
|
+
try {
|
|
176
|
+
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
177
|
+
promoteRename(backupPath, committedDbPath);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
rmSync(committedDbPath, { force: true });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
writeInterruptedMarker(home, backupPath ?? committedDbPath, stagedDbPath, now);
|
|
185
|
+
throw new AmbiguousSwitchError({
|
|
186
|
+
homePath: home,
|
|
187
|
+
backupPath: backupPath ?? committedDbPath,
|
|
188
|
+
stagingPath: stagedDbPath,
|
|
189
|
+
detail: `SQLite database was promoted but schema.json could not be advanced (${messageOf(error)}), ` +
|
|
190
|
+
`and the automatic rollback also failed. The database is at ${committedDbPath}; ` +
|
|
191
|
+
`recover by advancing schema.json recordVersions or restoring the backup.`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
status: "switched",
|
|
198
|
+
...(backupPath === undefined ? {} : { backupPath }),
|
|
199
|
+
detail: `SQLite database promoted to ${committedDbPath} and schema.json record versions advanced.`
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
discardFreshOutput() {
|
|
203
|
+
rmSync(stagedDbPath, { force: true });
|
|
204
|
+
// Clean up WAL/SHM sidecars if the connection left them.
|
|
205
|
+
rmSync(`${stagedDbPath}-wal`, { force: true });
|
|
206
|
+
rmSync(`${stagedDbPath}-shm`, { force: true });
|
|
207
|
+
stagedSchemaManifest = null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
// -- helpers ---------------------------------------------------------------
|
|
211
|
+
function readSourceFresh() {
|
|
212
|
+
const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
|
|
213
|
+
const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
|
|
214
|
+
const state = readStateFromSqlite(home);
|
|
215
|
+
return Object.freeze({ schemaManifest, state });
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Re-derive the expected post-migration state by reading the fresh snapshot,
|
|
219
|
+
* planning from its versions to `latest`, and applying every registered
|
|
220
|
+
* step transform. This mirrors the engine's apply phase but starts from an
|
|
221
|
+
* independent disk read.
|
|
222
|
+
*/
|
|
223
|
+
function deriveExpectedState(snapshot) {
|
|
224
|
+
const inspected = inspectSnapshotVersionState(snapshot, latest);
|
|
225
|
+
if ("corruption" in inspected) {
|
|
226
|
+
throw new Error(inspected.corruption.detail);
|
|
227
|
+
}
|
|
228
|
+
const plan = planMigration(registry, inspected.source, latest);
|
|
229
|
+
if (plan.kind === "blocked") {
|
|
230
|
+
throw new Error(`SQLite record migration verification cannot derive expected state: ${plan.blocker.message}`);
|
|
231
|
+
}
|
|
232
|
+
if (plan.kind === "no-op")
|
|
233
|
+
return snapshot.state;
|
|
234
|
+
let current = snapshot;
|
|
235
|
+
for (const planned of plan.steps) {
|
|
236
|
+
planned.step.preconditions(current);
|
|
237
|
+
current = planned.step.transform(current);
|
|
238
|
+
}
|
|
239
|
+
return current.state;
|
|
240
|
+
}
|
|
241
|
+
function verifyChecksums(expectedState) {
|
|
242
|
+
const expected = computeStateFamilyChecksums(expectedState);
|
|
243
|
+
const actual = computeDbFamilyChecksums(home, STAGED_DATABASE_FILENAME);
|
|
244
|
+
const families = new Set([...Object.keys(expected), ...Object.keys(actual)]);
|
|
245
|
+
const mismatches = [];
|
|
246
|
+
for (const family of families) {
|
|
247
|
+
const e = expected[family];
|
|
248
|
+
const a = actual[family];
|
|
249
|
+
if (e === undefined || a === undefined || e.count !== a.count || e.hash !== a.hash) {
|
|
250
|
+
mismatches.push(`${family} (expected ${e === undefined ? "absent" : `${e.count}/${e.hash.slice(0, 12)}`}, ` +
|
|
251
|
+
`found ${a === undefined ? "absent" : `${a.count}/${a.hash.slice(0, 12)}`})`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (mismatches.length > 0) {
|
|
255
|
+
throw new Error(`SQLite record migration checksum mismatch for ${mismatches.length} family/families: ${mismatches.join("; ")}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function parseJsonObject(raw, label) {
|
|
260
|
+
const value = JSON.parse(raw);
|
|
261
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
262
|
+
throw new Error(`${label} must be a JSON object.`);
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
function messageOf(error) {
|
|
267
|
+
return error instanceof Error ? error.message : String(error);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Persist the durable `interrupted` switch-progress marker for an ambiguous
|
|
271
|
+
* SQLite switch (P1-4): the original database was moved to its timestamped
|
|
272
|
+
* backup and neither the promotion nor its rollback completed. The marker is
|
|
273
|
+
* the honest durable signal — a completion receipt is never written for a
|
|
274
|
+
* switch that did not commit. Best-effort: the backup and on-disk state still
|
|
275
|
+
* recover the Home even if the marker write fails.
|
|
276
|
+
*/
|
|
277
|
+
function writeInterruptedMarker(home, backupPath, stagingPath, now) {
|
|
278
|
+
try {
|
|
279
|
+
writeSwitchProgress(home, {
|
|
280
|
+
phase: "interrupted",
|
|
281
|
+
homePath: home,
|
|
282
|
+
backupPath,
|
|
283
|
+
stagingPath,
|
|
284
|
+
updatedAt: now().toISOString()
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Marker best-effort; the backup + on-disk state still recover the Home.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import Database from "better-sqlite3";
|
|
27
|
+
import { mailboxTargetKey } from "../../coordination/workMailbox.js";
|
|
28
|
+
import { managedWorkspaceKey } from "../../worktree/managedWorkspace.js";
|
|
27
29
|
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
30
|
+
import { readStorageSchemaManifest } from "../storageSchema.js";
|
|
31
|
+
import { CURRENT_STORED_TASK_SCHEMA_VERSION } from "../taskStore.js";
|
|
28
32
|
/** The sidecar database filename used during staging. */
|
|
29
33
|
export const STAGED_DATABASE_FILENAME = "yui.db.staged";
|
|
30
34
|
/** The committed database filename. */
|
|
@@ -80,6 +84,19 @@ function asObjectMap(value) {
|
|
|
80
84
|
}
|
|
81
85
|
return result;
|
|
82
86
|
}
|
|
87
|
+
/** Read a persisted map whose values are opaque strings without silently
|
|
88
|
+
* dropping malformed entries during an offline migration. */
|
|
89
|
+
function asStringMap(value, label) {
|
|
90
|
+
const record = asObject(value);
|
|
91
|
+
const result = {};
|
|
92
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
93
|
+
if (typeof entry !== "string") {
|
|
94
|
+
throw new Error(`${label}.${key} must be a string.`);
|
|
95
|
+
}
|
|
96
|
+
result[key] = entry;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
83
100
|
function asNullableObject(value) {
|
|
84
101
|
if (value === null || value === undefined)
|
|
85
102
|
return null;
|
|
@@ -100,6 +117,9 @@ function asStoredTask(value) {
|
|
|
100
117
|
reviewRounds: asObjectMap(record.reviewRounds),
|
|
101
118
|
changeSets: asObjectMap(record.changeSets),
|
|
102
119
|
integrationAttempts: asObjectMap(record.integrationAttempts),
|
|
120
|
+
integrationQueue: asObjectMap(record.integrationQueue),
|
|
121
|
+
durableJobs: asObjectMap(record.durableJobs),
|
|
122
|
+
jobCallerKeyHashes: asStringMap(record.jobCallerKeyHashes, "jobCallerKeyHashes"),
|
|
103
123
|
activeRuns: asObjectMap(record.activeRuns),
|
|
104
124
|
messages: asObjectMap(record.messages),
|
|
105
125
|
inputRequests: asObjectMap(record.inputRequests),
|
|
@@ -108,7 +128,9 @@ function asStoredTask(value) {
|
|
|
108
128
|
events: asObjectMap(record.events),
|
|
109
129
|
leaderFailure: asNullableObject(record.leaderFailure),
|
|
110
130
|
operatorNotification: asNullableObject(record.operatorNotification),
|
|
111
|
-
idHighWaterMarks: asObject(record.idHighWaterMarks)
|
|
131
|
+
idHighWaterMarks: asObject(record.idHighWaterMarks),
|
|
132
|
+
capabilityGrants: asObjectMap(record.capabilityGrants),
|
|
133
|
+
releaseWorkflows: asObjectMap(record.releaseWorkflows)
|
|
112
134
|
};
|
|
113
135
|
}
|
|
114
136
|
function tasksOf(state) {
|
|
@@ -195,6 +217,20 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
195
217
|
for (const attempt of Object.values(stored.integrationAttempts)) {
|
|
196
218
|
store.saveIntegrationAttempt(taskId, attempt);
|
|
197
219
|
}
|
|
220
|
+
for (const entry of Object.values(stored.integrationQueue)) {
|
|
221
|
+
store.saveIntegrationQueueEntry(taskId, entry);
|
|
222
|
+
}
|
|
223
|
+
for (const job of Object.values(stored.durableJobs)) {
|
|
224
|
+
store.saveDurableJob(taskId, job);
|
|
225
|
+
}
|
|
226
|
+
for (const [key, hash] of Object.entries(stored.jobCallerKeyHashes)) {
|
|
227
|
+
const separator = key.indexOf("\0");
|
|
228
|
+
if (separator <= 0 || separator === key.length - 1
|
|
229
|
+
|| key.indexOf("\0", separator + 1) !== -1) {
|
|
230
|
+
throw new Error(`Job caller key hash identity is invalid: ${taskId}/${key}.`);
|
|
231
|
+
}
|
|
232
|
+
store.setJobCallerKeyHash(taskId, key.slice(0, separator), key.slice(separator + 1), hash);
|
|
233
|
+
}
|
|
198
234
|
// Active-run pointers: the document stores { schemaVersion, runId }
|
|
199
235
|
// keyed by pointer; the store derives the pointer from the Run.
|
|
200
236
|
for (const [pointer, value] of Object.entries(stored.activeRuns)) {
|
|
@@ -236,6 +272,13 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
236
272
|
store.migrationSeedIdSequence(taskId, kind, highWater);
|
|
237
273
|
}
|
|
238
274
|
}
|
|
275
|
+
// Capability grants and release workflows (task-15 record families).
|
|
276
|
+
for (const grant of Object.values(stored.capabilityGrants)) {
|
|
277
|
+
store.saveCapabilityGrant(taskId, grant);
|
|
278
|
+
}
|
|
279
|
+
for (const workflow of Object.values(stored.releaseWorkflows)) {
|
|
280
|
+
store.saveReleaseWorkflow(taskId, workflow);
|
|
281
|
+
}
|
|
239
282
|
}
|
|
240
283
|
// Work mailboxes.
|
|
241
284
|
for (const mailbox of Object.values(asObjectMap(state.mailboxes))) {
|
|
@@ -302,6 +345,9 @@ export function computeStateFamilyChecksums(state) {
|
|
|
302
345
|
const reviewRounds = [];
|
|
303
346
|
const changeSets = [];
|
|
304
347
|
const integrationAttempts = [];
|
|
348
|
+
const integrationQueue = [];
|
|
349
|
+
const durableJobs = [];
|
|
350
|
+
const jobCallerKeyHashes = [];
|
|
305
351
|
const activeRunPointers = [];
|
|
306
352
|
const messages = [];
|
|
307
353
|
const inputRequests = [];
|
|
@@ -310,6 +356,8 @@ export function computeStateFamilyChecksums(state) {
|
|
|
310
356
|
const events = [];
|
|
311
357
|
const leaderFailures = [];
|
|
312
358
|
const operatorNotifications = [];
|
|
359
|
+
const capabilityGrants = [];
|
|
360
|
+
const releaseWorkflows = [];
|
|
313
361
|
for (const stored of Object.values(tasks)) {
|
|
314
362
|
taskRecords.push(stored.task);
|
|
315
363
|
if (stored.brief !== null)
|
|
@@ -322,6 +370,20 @@ export function computeStateFamilyChecksums(state) {
|
|
|
322
370
|
reviewRounds.push(...Object.values(stored.reviewRounds));
|
|
323
371
|
changeSets.push(...Object.values(stored.changeSets));
|
|
324
372
|
integrationAttempts.push(...Object.values(stored.integrationAttempts));
|
|
373
|
+
integrationQueue.push(...Object.values(stored.integrationQueue));
|
|
374
|
+
durableJobs.push(...Object.values(stored.durableJobs));
|
|
375
|
+
for (const [key, hash] of Object.entries(stored.jobCallerKeyHashes)) {
|
|
376
|
+
const separator = key.indexOf("\0");
|
|
377
|
+
if (separator <= 0 || separator === key.length - 1
|
|
378
|
+
|| key.indexOf("\0", separator + 1) !== -1) {
|
|
379
|
+
throw new Error(`Job caller key hash identity is invalid: ${stored.task.id}/${key}.`);
|
|
380
|
+
}
|
|
381
|
+
jobCallerKeyHashes.push({
|
|
382
|
+
taskId: stored.task.id,
|
|
383
|
+
key,
|
|
384
|
+
hash
|
|
385
|
+
});
|
|
386
|
+
}
|
|
325
387
|
activeRunPointers.push(...Object.values(stored.activeRuns));
|
|
326
388
|
messages.push(...Object.values(stored.messages));
|
|
327
389
|
inputRequests.push(...Object.values(stored.inputRequests));
|
|
@@ -332,6 +394,8 @@ export function computeStateFamilyChecksums(state) {
|
|
|
332
394
|
leaderFailures.push(stored.leaderFailure);
|
|
333
395
|
if (stored.operatorNotification !== null)
|
|
334
396
|
operatorNotifications.push(stored.operatorNotification);
|
|
397
|
+
capabilityGrants.push(...Object.values(stored.capabilityGrants));
|
|
398
|
+
releaseWorkflows.push(...Object.values(stored.releaseWorkflows));
|
|
335
399
|
}
|
|
336
400
|
checksums.task = hashRecords(taskRecords);
|
|
337
401
|
checksums.taskBrief = hashRecords(briefs);
|
|
@@ -343,6 +407,9 @@ export function computeStateFamilyChecksums(state) {
|
|
|
343
407
|
checksums.reviewRound = hashRecords(reviewRounds);
|
|
344
408
|
checksums.changeSet = hashRecords(changeSets);
|
|
345
409
|
checksums.integrationAttempt = hashRecords(integrationAttempts);
|
|
410
|
+
checksums.integrationQueue = hashRecords(integrationQueue);
|
|
411
|
+
checksums.durableJob = hashRecords(durableJobs);
|
|
412
|
+
checksums.jobCallerKeyHash = hashRecords(jobCallerKeyHashes);
|
|
346
413
|
checksums.activeRunPointer = hashRecords(activeRunPointers);
|
|
347
414
|
checksums.message = hashRecords(messages);
|
|
348
415
|
checksums.inputRequest = hashRecords(inputRequests);
|
|
@@ -351,6 +418,8 @@ export function computeStateFamilyChecksums(state) {
|
|
|
351
418
|
checksums.event = hashRecords(events);
|
|
352
419
|
checksums.leaderFailure = hashRecords(leaderFailures);
|
|
353
420
|
checksums.operatorNotification = hashRecords(operatorNotifications);
|
|
421
|
+
checksums.capabilityGrant = hashRecords(capabilityGrants);
|
|
422
|
+
checksums.releaseWorkflow = hashRecords(releaseWorkflows);
|
|
354
423
|
checksums.workMailbox = hashRecords(Object.values(asObjectMap(state.mailboxes)));
|
|
355
424
|
return checksums;
|
|
356
425
|
}
|
|
@@ -359,7 +428,7 @@ function hashPayloadTable(db, sql) {
|
|
|
359
428
|
return hashRecords(rows.map((row) => JSON.parse(row.payload)));
|
|
360
429
|
}
|
|
361
430
|
/** Reconstruct a WorkMailbox from the normalised mailboxes table columns. */
|
|
362
|
-
function rowToMailbox(row) {
|
|
431
|
+
export function rowToMailbox(row) {
|
|
363
432
|
let target;
|
|
364
433
|
switch (row.target_kind) {
|
|
365
434
|
case "operator":
|
|
@@ -413,6 +482,14 @@ export function computeDbFamilyChecksums(home, databaseFilename) {
|
|
|
413
482
|
checksums.reviewRound = hashPayloadTable(db, "SELECT payload FROM review_rounds");
|
|
414
483
|
checksums.changeSet = hashPayloadTable(db, "SELECT payload FROM change_sets");
|
|
415
484
|
checksums.integrationAttempt = hashPayloadTable(db, "SELECT payload FROM integration_attempts");
|
|
485
|
+
checksums.integrationQueue = hashPayloadTable(db, "SELECT payload FROM integration_queue");
|
|
486
|
+
checksums.durableJob = hashPayloadTable(db, "SELECT payload FROM durable_jobs");
|
|
487
|
+
const callerHashRows = db.prepare("SELECT task_id, role_name, agent_id, hash FROM job_caller_key_hashes").all();
|
|
488
|
+
checksums.jobCallerKeyHash = hashRecords(callerHashRows.map((row) => ({
|
|
489
|
+
taskId: row.task_id,
|
|
490
|
+
key: `${row.role_name}\0${row.agent_id}`,
|
|
491
|
+
hash: row.hash
|
|
492
|
+
})));
|
|
416
493
|
checksums.activeRunPointer = hashPayloadTable(db, "SELECT payload FROM active_runs");
|
|
417
494
|
checksums.message = hashPayloadTable(db, "SELECT payload FROM messages");
|
|
418
495
|
checksums.inputRequest = hashPayloadTable(db, "SELECT payload FROM input_requests");
|
|
@@ -421,6 +498,8 @@ export function computeDbFamilyChecksums(home, databaseFilename) {
|
|
|
421
498
|
checksums.event = hashPayloadTable(db, "SELECT payload FROM events");
|
|
422
499
|
checksums.leaderFailure = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'leader-failure' AND payload IS NOT NULL");
|
|
423
500
|
checksums.operatorNotification = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'operator-notification' AND payload IS NOT NULL");
|
|
501
|
+
checksums.capabilityGrant = hashPayloadTable(db, "SELECT payload FROM capability_grants");
|
|
502
|
+
checksums.releaseWorkflow = hashPayloadTable(db, "SELECT payload FROM release_workflows");
|
|
424
503
|
// Mailboxes are reconstructed from typed columns (no payload column).
|
|
425
504
|
const mailboxRows = db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending FROM mailboxes ORDER BY target_key").all();
|
|
426
505
|
checksums.workMailbox = hashRecords(mailboxRows.map(rowToMailbox));
|
|
@@ -455,3 +534,180 @@ export function verifySqliteChecksums(state, home, databaseFilename) {
|
|
|
455
534
|
throw new Error(`SQLite migration checksum mismatch for ${mismatches.length} family/families: ${mismatches.join("; ")}`);
|
|
456
535
|
}
|
|
457
536
|
}
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// SQLite -> Snapshot reconstruction (record-migration source, Issue 01 Phase 2)
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
/**
|
|
541
|
+
* Reconstruct the state.json-shaped snapshot from a committed SQLite database.
|
|
542
|
+
*
|
|
543
|
+
* This is the reverse of {@link populateSqliteFromState}: it reads every
|
|
544
|
+
* record family from `yui.db` and rebuilds the nested document shape the
|
|
545
|
+
* generic migration engine's record transforms operate on. Payloads are read
|
|
546
|
+
* RAW (`JSON.parse` without the strict current-version parsers) so a record
|
|
547
|
+
* family at an older persisted version survives the round-trip with its
|
|
548
|
+
* original `schemaVersion` intact — the strict parsers only understand the
|
|
549
|
+
* current version and would reject the very records the migration is meant to
|
|
550
|
+
* transform.
|
|
551
|
+
*
|
|
552
|
+
* The database is opened read-only; this function never writes.
|
|
553
|
+
*/
|
|
554
|
+
export function readStateFromSqlite(home) {
|
|
555
|
+
const dbPath = join(home, COMMITTED_DATABASE_FILENAME);
|
|
556
|
+
const db = new Database(dbPath, { readonly: true });
|
|
557
|
+
try {
|
|
558
|
+
const manifest = readStorageSchemaManifest(home);
|
|
559
|
+
const storedTaskVersion = typeof manifest.recordVersions?.storedTask === "number"
|
|
560
|
+
? manifest.recordVersions.storedTask
|
|
561
|
+
: CURRENT_STORED_TASK_SCHEMA_VERSION;
|
|
562
|
+
// Home identity + revision continuity.
|
|
563
|
+
const meta = db.prepare("SELECT home_identity, revision FROM home_meta WHERE id = 1").get();
|
|
564
|
+
const state = {
|
|
565
|
+
// The state document's `schemaVersion` is the Yui aggregate version,
|
|
566
|
+
// sourced from the durable manifest (the version contract). The
|
|
567
|
+
// `home_meta.aggregate_version` column is the SQLite database's own
|
|
568
|
+
// schema version (SQLITE_AGGREGATE_VERSION), a different fact.
|
|
569
|
+
schemaVersion: manifest.aggregateSchemaVersion,
|
|
570
|
+
revision: meta.revision,
|
|
571
|
+
homeIdentity: JSON.parse(meta.home_identity),
|
|
572
|
+
configuredAgents: {},
|
|
573
|
+
projects: {},
|
|
574
|
+
agentProfiles: {},
|
|
575
|
+
globalRoles: {},
|
|
576
|
+
globalRoleSessionSets: {},
|
|
577
|
+
tasks: {},
|
|
578
|
+
mailboxes: {}
|
|
579
|
+
};
|
|
580
|
+
// Config singleton (absent on a fresh, never-configured Home).
|
|
581
|
+
const configRow = db.prepare("SELECT payload FROM config WHERE id = 1").get();
|
|
582
|
+
if (configRow !== undefined) {
|
|
583
|
+
state.config = JSON.parse(configRow.payload);
|
|
584
|
+
}
|
|
585
|
+
// Global record families.
|
|
586
|
+
loadGlobalPayloadMap(db, "configured_agents", state, "configuredAgents", (record) => record.id);
|
|
587
|
+
loadGlobalPayloadMap(db, "projects", state, "projects", (record) => record.id);
|
|
588
|
+
loadGlobalPayloadMap(db, "agent_profiles", state, "agentProfiles", (record) => record.id);
|
|
589
|
+
loadGlobalPayloadMap(db, "global_roles", state, "globalRoles", (record) => record.name);
|
|
590
|
+
loadGlobalPayloadMap(db, "global_role_session_sets", state, "globalRoleSessionSets", (record) => record.owner.roleName);
|
|
591
|
+
// Tasks: the StoredTask aggregate wrapper. The wrapper `schemaVersion` is
|
|
592
|
+
// not persisted in the database (records live in flat tables), so it is
|
|
593
|
+
// taken from the manifest's declared `storedTask` version — the version
|
|
594
|
+
// the database was created with — so the version scanner and the migration
|
|
595
|
+
// engine see a consistent source.
|
|
596
|
+
const tasks = state.tasks;
|
|
597
|
+
const taskRows = db.prepare("SELECT task_id, payload, brief FROM task_records").all();
|
|
598
|
+
for (const row of taskRows) {
|
|
599
|
+
tasks[row.task_id] = {
|
|
600
|
+
schemaVersion: storedTaskVersion,
|
|
601
|
+
task: JSON.parse(row.payload),
|
|
602
|
+
brief: row.brief === null ? null : JSON.parse(row.brief),
|
|
603
|
+
roles: {},
|
|
604
|
+
managedWorkspaces: {},
|
|
605
|
+
roleSessionSets: {},
|
|
606
|
+
workItems: {},
|
|
607
|
+
agentRuns: {},
|
|
608
|
+
reviewRounds: {},
|
|
609
|
+
changeSets: {},
|
|
610
|
+
integrationAttempts: {},
|
|
611
|
+
integrationQueue: {},
|
|
612
|
+
durableJobs: {},
|
|
613
|
+
jobCallerKeyHashes: {},
|
|
614
|
+
activeRuns: {},
|
|
615
|
+
messages: {},
|
|
616
|
+
inputRequests: {},
|
|
617
|
+
decisions: {},
|
|
618
|
+
milestones: {},
|
|
619
|
+
events: {},
|
|
620
|
+
leaderFailure: null,
|
|
621
|
+
operatorNotification: null,
|
|
622
|
+
idHighWaterMarks: {},
|
|
623
|
+
capabilityGrants: {},
|
|
624
|
+
releaseWorkflows: {}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
// Per-task record families, keyed by their state.json map keys.
|
|
628
|
+
loadTaskPayloadMap(db, "task_roles", tasks, "roles", (record) => record.name);
|
|
629
|
+
loadTaskPayloadMap(db, "managed_workspaces", tasks, "managedWorkspaces", (record) => managedWorkspaceKey(record.owner));
|
|
630
|
+
loadTaskPayloadMap(db, "role_session_sets", tasks, "roleSessionSets", (record) => record.owner.roleName);
|
|
631
|
+
loadTaskPayloadMap(db, "work_items", tasks, "workItems", (record) => record.id);
|
|
632
|
+
loadTaskPayloadMap(db, "agent_runs", tasks, "agentRuns", (record) => record.id);
|
|
633
|
+
loadTaskPayloadMap(db, "review_rounds", tasks, "reviewRounds", (record) => record.id);
|
|
634
|
+
loadTaskPayloadMap(db, "change_sets", tasks, "changeSets", (record) => record.id);
|
|
635
|
+
loadTaskPayloadMap(db, "integration_attempts", tasks, "integrationAttempts", (record) => record.id);
|
|
636
|
+
loadTaskPayloadMap(db, "integration_queue", tasks, "integrationQueue", (record) => record.id);
|
|
637
|
+
loadTaskPayloadMap(db, "durable_jobs", tasks, "durableJobs", (record) => record.id);
|
|
638
|
+
loadTaskPayloadMap(db, "messages", tasks, "messages", (record) => record.id);
|
|
639
|
+
loadTaskPayloadMap(db, "input_requests", tasks, "inputRequests", (record) => record.id);
|
|
640
|
+
loadTaskPayloadMap(db, "decisions", tasks, "decisions", (record) => record.id);
|
|
641
|
+
loadTaskPayloadMap(db, "milestones", tasks, "milestones", (record) => record.id);
|
|
642
|
+
loadTaskPayloadMap(db, "events", tasks, "events", (record) => record.id);
|
|
643
|
+
loadTaskPayloadMap(db, "capability_grants", tasks, "capabilityGrants", (record) => record.id);
|
|
644
|
+
loadTaskPayloadMap(db, "release_workflows", tasks, "releaseWorkflows", (record) => record.id);
|
|
645
|
+
// Job caller key hashes: keyed by `${roleName}\0${agentId}`.
|
|
646
|
+
const callerHashRows = db.prepare("SELECT task_id, role_name, agent_id, hash FROM job_caller_key_hashes").all();
|
|
647
|
+
for (const row of callerHashRows) {
|
|
648
|
+
const stored = requireTaskAggregate(tasks, row.task_id, "jobCallerKeyHashes");
|
|
649
|
+
stored.jobCallerKeyHashes[`${row.role_name}\0${row.agent_id}`] = row.hash;
|
|
650
|
+
}
|
|
651
|
+
// Active-run pointers: keyed by pointer, value is the persisted payload.
|
|
652
|
+
const activeRunRows = db.prepare("SELECT task_id, pointer, payload FROM active_runs").all();
|
|
653
|
+
for (const row of activeRunRows) {
|
|
654
|
+
const stored = requireTaskAggregate(tasks, row.task_id, "activeRuns");
|
|
655
|
+
stored.activeRuns[row.pointer] = JSON.parse(row.payload);
|
|
656
|
+
}
|
|
657
|
+
// Leader failure / operator notification projections.
|
|
658
|
+
const projectionRows = db.prepare("SELECT task_id, kind, payload FROM task_projections WHERE payload IS NOT NULL").all();
|
|
659
|
+
for (const row of projectionRows) {
|
|
660
|
+
const stored = requireTaskAggregate(tasks, row.task_id, row.kind);
|
|
661
|
+
if (row.kind === "leader-failure") {
|
|
662
|
+
stored.leaderFailure = JSON.parse(row.payload);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
stored.operatorNotification = JSON.parse(row.payload);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Per-task ID high-water marks.
|
|
669
|
+
const idSeqRows = db.prepare("SELECT task_id, kind, high_water FROM id_sequences").all();
|
|
670
|
+
for (const row of idSeqRows) {
|
|
671
|
+
const stored = requireTaskAggregate(tasks, row.task_id, "idHighWaterMarks");
|
|
672
|
+
stored.idHighWaterMarks[row.kind] = row.high_water;
|
|
673
|
+
}
|
|
674
|
+
// Work mailboxes: reconstructed from typed columns (no payload column).
|
|
675
|
+
const mailboxRows = db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending FROM mailboxes ORDER BY target_key").all();
|
|
676
|
+
const mailboxes = state.mailboxes;
|
|
677
|
+
for (const row of mailboxRows) {
|
|
678
|
+
const mailbox = rowToMailbox(row);
|
|
679
|
+
mailboxes[mailboxTargetKey(mailbox.target)] = mailbox;
|
|
680
|
+
}
|
|
681
|
+
return state;
|
|
682
|
+
}
|
|
683
|
+
finally {
|
|
684
|
+
db.close();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/** Load a global (non-task-scoped) payload table into a keyed map on `state`. */
|
|
688
|
+
function loadGlobalPayloadMap(db, table, state, stateKey, keyOf) {
|
|
689
|
+
const rows = db.prepare(`SELECT payload FROM ${table}`).all();
|
|
690
|
+
const target = state[stateKey];
|
|
691
|
+
for (const row of rows) {
|
|
692
|
+
const record = JSON.parse(row.payload);
|
|
693
|
+
target[keyOf(record)] = record;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/** Load a task-scoped payload table into the matching family map of each task. */
|
|
697
|
+
function loadTaskPayloadMap(db, table, tasks, familyKey, keyOf) {
|
|
698
|
+
const rows = db.prepare(`SELECT task_id, payload FROM ${table}`).all();
|
|
699
|
+
for (const row of rows) {
|
|
700
|
+
const stored = requireTaskAggregate(tasks, row.task_id, table);
|
|
701
|
+
const family = stored[familyKey];
|
|
702
|
+
const record = JSON.parse(row.payload);
|
|
703
|
+
family[keyOf(record)] = record;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Resolve a task's StoredTask aggregate, or throw on an orphaned child row. */
|
|
707
|
+
function requireTaskAggregate(tasks, taskId, context) {
|
|
708
|
+
const stored = tasks[taskId];
|
|
709
|
+
if (stored === undefined) {
|
|
710
|
+
throw new Error(`SQLite reconstruction found ${context} row for unknown task ${taskId}.`);
|
|
711
|
+
}
|
|
712
|
+
return stored;
|
|
713
|
+
}
|