@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,918 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* state.json -> SQLite staged migration (task-21 §8, work-item-4).
|
|
3
|
-
*
|
|
4
|
-
* This module is the document-to-database population and verification seam for
|
|
5
|
-
* the layout 6 -> 7 offline migration. It is deliberately side-effect-light:
|
|
6
|
-
*
|
|
7
|
-
* - `populateSqliteFromState` opens a {@link SqliteTaskStore} on the sidecar
|
|
8
|
-
* `yui.db.staged` and bulk-loads every record family from the parsed
|
|
9
|
-
* `state.json` document, preserving the Home identity, revision, and ID
|
|
10
|
-
* high-water marks. The source document is never written.
|
|
11
|
-
* - `computeStateFamilyChecksums` / `computeDbFamilyChecksums` compute a
|
|
12
|
-
* per-family `{ count, hash }` over the canonical JSON of every record, so
|
|
13
|
-
* the staged database can be verified against an independent re-read of
|
|
14
|
-
* `state.json` (the Verify phase of §8.2).
|
|
15
|
-
* - `verifySqliteMigration` compares the two checksum maps and throws on any
|
|
16
|
-
* count or content mismatch, leaving the source untouched.
|
|
17
|
-
*
|
|
18
|
-
* The checksum is content-based: each record is canonicalised (sorted keys,
|
|
19
|
-
* stable array ordering) and hashed individually; the per-family hash is the
|
|
20
|
-
* sha256 of the sorted record hashes. This is order-independent (a map
|
|
21
|
-
* serialised in any key order produces the same checksum) and catches any
|
|
22
|
-
* dropped, duplicated, or mutated record.
|
|
23
|
-
*/
|
|
24
|
-
import { createHash } from "node:crypto";
|
|
25
|
-
import { existsSync, rmSync } from "node:fs";
|
|
26
|
-
import { join } from "node:path";
|
|
27
|
-
import Database from "better-sqlite3";
|
|
28
|
-
import { mailboxTargetKey } from "../../coordination/workMailbox.js";
|
|
29
|
-
import { managedWorkspaceKey } from "../../worktree/managedWorkspace.js";
|
|
30
|
-
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
31
|
-
import { readStorageSchemaManifest } from "../storageSchema.js";
|
|
32
|
-
import { CURRENT_STORED_TASK_SCHEMA_VERSION } from "../taskStore.js";
|
|
33
|
-
/** The sidecar database filename used during staging. */
|
|
34
|
-
export const STAGED_DATABASE_FILENAME = "yui.db.staged";
|
|
35
|
-
/** The committed database filename. */
|
|
36
|
-
export const COMMITTED_DATABASE_FILENAME = "yui.db";
|
|
37
|
-
/** Disposable, schema-current snapshot used to reconstruct an older source. */
|
|
38
|
-
export const MIGRATION_SOURCE_DATABASE_FILENAME = "yui.db.migration-source";
|
|
39
|
-
/**
|
|
40
|
-
* Reconstruct a migration snapshot without advancing the authoritative DB.
|
|
41
|
-
*
|
|
42
|
-
* `VACUUM INTO` takes a consistent SQLite snapshot, including committed WAL
|
|
43
|
-
* state, without writing the source. Pending physical migrations are then
|
|
44
|
-
* applied only to that disposable copy before the current reader enumerates
|
|
45
|
-
* record families that may not exist in the older source schema.
|
|
46
|
-
*/
|
|
47
|
-
export function readMigrationSourceStateFromSqlite(home) {
|
|
48
|
-
const sourcePath = join(home, COMMITTED_DATABASE_FILENAME);
|
|
49
|
-
const snapshotPath = join(home, MIGRATION_SOURCE_DATABASE_FILENAME);
|
|
50
|
-
if (existsSync(snapshotPath)) {
|
|
51
|
-
throw new Error(`Refusing to overwrite an existing SQLite migration-source snapshot: ${snapshotPath}. `
|
|
52
|
-
+ "Discard it and retry.");
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
const source = new Database(sourcePath, { readonly: true });
|
|
56
|
-
try {
|
|
57
|
-
source.prepare("VACUUM INTO ?").run(snapshotPath);
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
source.close();
|
|
61
|
-
}
|
|
62
|
-
// Physical migration 14 rewrites the WorkMailbox family while adding its
|
|
63
|
-
// current columns. Preserve the source rows before applying it so the
|
|
64
|
-
// logical record migration still sees and transforms v1 exactly once.
|
|
65
|
-
const sourceMailboxes = readWorkMailboxesFromSqlite(snapshotPath);
|
|
66
|
-
const stagedStore = new SqliteTaskStore(home, {
|
|
67
|
-
databaseFilename: MIGRATION_SOURCE_DATABASE_FILENAME,
|
|
68
|
-
migration: true
|
|
69
|
-
});
|
|
70
|
-
stagedStore.close();
|
|
71
|
-
const state = readStateFromSqlite(home, MIGRATION_SOURCE_DATABASE_FILENAME);
|
|
72
|
-
state.mailboxes = sourceMailboxes;
|
|
73
|
-
return state;
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
rmSync(snapshotPath, { force: true });
|
|
77
|
-
rmSync(`${snapshotPath}-wal`, { force: true });
|
|
78
|
-
rmSync(`${snapshotPath}-shm`, { force: true });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Preserve SQLite-owned durable state that intentionally sits outside the
|
|
83
|
-
* state.json-shaped Task aggregate snapshot. Volatile coordination locks and
|
|
84
|
-
* derived projections are rebuilt or dropped at the offline boundary.
|
|
85
|
-
*/
|
|
86
|
-
export function copySqlitePassthroughState(home, sourceDatabaseFilename, targetDatabaseFilename) {
|
|
87
|
-
if (sourceDatabaseFilename === targetDatabaseFilename) {
|
|
88
|
-
throw new Error("SQLite passthrough copy requires distinct source and target databases.");
|
|
89
|
-
}
|
|
90
|
-
const source = new Database(join(home, sourceDatabaseFilename), { readonly: true });
|
|
91
|
-
const target = new Database(join(home, targetDatabaseFilename));
|
|
92
|
-
try {
|
|
93
|
-
target.pragma("foreign_keys = ON");
|
|
94
|
-
target.transaction(() => {
|
|
95
|
-
mergeGlobalSequences(source, target);
|
|
96
|
-
copyTableRows(source, target, "outbox");
|
|
97
|
-
copyTableRows(source, target, "work_item_candidates");
|
|
98
|
-
copyTableRows(source, target, "review_findings");
|
|
99
|
-
copyTableRows(source, target, "telemetry");
|
|
100
|
-
if (sqliteTableExists(source, "telemetry_aggregate")) {
|
|
101
|
-
target.exec("DELETE FROM telemetry_aggregate");
|
|
102
|
-
copyTableRows(source, target, "telemetry_aggregate");
|
|
103
|
-
}
|
|
104
|
-
copyTableRows(source, target, "session_owners");
|
|
105
|
-
copyTableRows(source, target, "resource_registry");
|
|
106
|
-
copyTableRows(source, target, "gate_artifacts");
|
|
107
|
-
copyTableRows(source, target, "gate_artifact_logs");
|
|
108
|
-
})();
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
source.close();
|
|
112
|
-
target.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function mergeGlobalSequences(source, target) {
|
|
116
|
-
if (!sqliteTableExists(source, "global_sequences"))
|
|
117
|
-
return;
|
|
118
|
-
const rows = source.prepare("SELECT name, high_water FROM global_sequences").all();
|
|
119
|
-
const merge = target.prepare(`INSERT INTO global_sequences (name, high_water) VALUES (?, ?)
|
|
120
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
121
|
-
high_water = MAX(global_sequences.high_water, excluded.high_water)`);
|
|
122
|
-
for (const row of rows)
|
|
123
|
-
merge.run(row.name, row.high_water);
|
|
124
|
-
}
|
|
125
|
-
function copyTableRows(source, target, table) {
|
|
126
|
-
if (!sqliteTableExists(source, table) || !sqliteTableExists(target, table))
|
|
127
|
-
return;
|
|
128
|
-
const columns = source.prepare(`PRAGMA table_info(${quoteSqliteIdentifier(table)})`).all().map((row) => row.name);
|
|
129
|
-
if (columns.length === 0)
|
|
130
|
-
return;
|
|
131
|
-
const quotedColumns = columns.map(quoteSqliteIdentifier);
|
|
132
|
-
const rows = source.prepare(`SELECT ${quotedColumns.join(", ")} FROM ${quoteSqliteIdentifier(table)}`).iterate();
|
|
133
|
-
const insert = target.prepare(`INSERT INTO ${quoteSqliteIdentifier(table)} (${quotedColumns.join(", ")}) `
|
|
134
|
-
+ `VALUES (${columns.map(() => "?").join(", ")})`);
|
|
135
|
-
for (const row of rows)
|
|
136
|
-
insert.run(...columns.map((column) => row[column]));
|
|
137
|
-
}
|
|
138
|
-
function sqliteTableExists(db, table) {
|
|
139
|
-
return db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined;
|
|
140
|
-
}
|
|
141
|
-
function quoteSqliteIdentifier(value) {
|
|
142
|
-
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
143
|
-
}
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// Canonical JSON and hashing
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
/**
|
|
148
|
-
* Deterministic JSON serialisation: object keys are sorted recursively so that
|
|
149
|
-
* two semantically-equal objects serialise identically regardless of key
|
|
150
|
-
* insertion order. Arrays preserve their order.
|
|
151
|
-
*/
|
|
152
|
-
function canonicalJson(value) {
|
|
153
|
-
if (value === null || typeof value !== "object")
|
|
154
|
-
return JSON.stringify(value);
|
|
155
|
-
if (Array.isArray(value)) {
|
|
156
|
-
return `[${value.map(canonicalJson).join(",")}]`;
|
|
157
|
-
}
|
|
158
|
-
const record = value;
|
|
159
|
-
const keys = Object.keys(record).sort();
|
|
160
|
-
return `{${keys
|
|
161
|
-
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
162
|
-
.join(",")}}`;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Compute a per-family checksum over a set of records. Each record is hashed
|
|
166
|
-
* individually (over its canonical JSON); the family hash is the sha256 of
|
|
167
|
-
* the sorted per-record hashes, making the result independent of record
|
|
168
|
-
* ordering.
|
|
169
|
-
*/
|
|
170
|
-
function hashRecords(records) {
|
|
171
|
-
const hashes = records
|
|
172
|
-
.map((record) => createHash("sha256").update(canonicalJson(record)).digest("hex"))
|
|
173
|
-
.sort();
|
|
174
|
-
const hash = createHash("sha256").update(hashes.join("\n")).digest("hex");
|
|
175
|
-
return { count: records.length, hash };
|
|
176
|
-
}
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// Document shape helpers
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
function asObject(value) {
|
|
181
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
182
|
-
return {};
|
|
183
|
-
return value;
|
|
184
|
-
}
|
|
185
|
-
function asObjectMap(value) {
|
|
186
|
-
const record = asObject(value);
|
|
187
|
-
const result = {};
|
|
188
|
-
for (const [key, entry] of Object.entries(record)) {
|
|
189
|
-
if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
|
|
190
|
-
result[key] = entry;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
/** Read a persisted map whose values are opaque strings without silently
|
|
196
|
-
* dropping malformed entries during an offline migration. */
|
|
197
|
-
function asStringMap(value, label) {
|
|
198
|
-
const record = asObject(value);
|
|
199
|
-
const result = {};
|
|
200
|
-
for (const [key, entry] of Object.entries(record)) {
|
|
201
|
-
if (typeof entry !== "string") {
|
|
202
|
-
throw new Error(`${label}.${key} must be a string.`);
|
|
203
|
-
}
|
|
204
|
-
result[key] = entry;
|
|
205
|
-
}
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
function asNullableObject(value) {
|
|
209
|
-
if (value === null || value === undefined)
|
|
210
|
-
return null;
|
|
211
|
-
if (typeof value !== "object" || Array.isArray(value))
|
|
212
|
-
return null;
|
|
213
|
-
return value;
|
|
214
|
-
}
|
|
215
|
-
function asStoredTask(value) {
|
|
216
|
-
const record = asObject(value);
|
|
217
|
-
return {
|
|
218
|
-
task: asObject(record.task),
|
|
219
|
-
brief: asNullableObject(record.brief),
|
|
220
|
-
roles: asObjectMap(record.roles),
|
|
221
|
-
managedWorkspaces: asObjectMap(record.managedWorkspaces),
|
|
222
|
-
roleSessionSets: asObjectMap(record.roleSessionSets),
|
|
223
|
-
workItems: asObjectMap(record.workItems),
|
|
224
|
-
contextSnapshots: asObjectMap(record.contextSnapshots),
|
|
225
|
-
agentRuns: asObjectMap(record.agentRuns),
|
|
226
|
-
reviewRounds: asObjectMap(record.reviewRounds),
|
|
227
|
-
changeSets: asObjectMap(record.changeSets),
|
|
228
|
-
integrationAttempts: asObjectMap(record.integrationAttempts),
|
|
229
|
-
integrationQueue: asObjectMap(record.integrationQueue),
|
|
230
|
-
durableJobs: asObjectMap(record.durableJobs),
|
|
231
|
-
jobCallerKeyHashes: asStringMap(record.jobCallerKeyHashes, "jobCallerKeyHashes"),
|
|
232
|
-
activeRuns: asObjectMap(record.activeRuns),
|
|
233
|
-
messages: asObjectMap(record.messages),
|
|
234
|
-
inputRequests: asObjectMap(record.inputRequests),
|
|
235
|
-
decisions: asObjectMap(record.decisions),
|
|
236
|
-
milestones: asObjectMap(record.milestones),
|
|
237
|
-
events: asObjectMap(record.events),
|
|
238
|
-
wakes: asObjectMap(record.wakes),
|
|
239
|
-
leaderFailure: asNullableObject(record.leaderFailure),
|
|
240
|
-
operatorNotification: asNullableObject(record.operatorNotification),
|
|
241
|
-
idHighWaterMarks: asObject(record.idHighWaterMarks),
|
|
242
|
-
capabilityGrants: asObjectMap(record.capabilityGrants),
|
|
243
|
-
releaseWorkflows: asObjectMap(record.releaseWorkflows),
|
|
244
|
-
publicationReferences: asObjectMap(record.publicationReferences)
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
function tasksOf(state) {
|
|
248
|
-
const result = {};
|
|
249
|
-
for (const [taskId, raw] of Object.entries(asObject(state.tasks))) {
|
|
250
|
-
result[taskId] = asStoredTask(raw);
|
|
251
|
-
}
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
// Population (Snapshot -> Stage)
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
/**
|
|
258
|
-
* Populate a fresh SQLite database from the parsed `state.json` document.
|
|
259
|
-
*
|
|
260
|
-
* The database is opened with the `migration` option (fence bypass) because
|
|
261
|
-
* the staged load runs while the upgrade fence is active. Every record family
|
|
262
|
-
* is saved through the canonical {@link SqliteTaskStore} methods so the
|
|
263
|
-
* typed-column projections and payload contents match what a live store would
|
|
264
|
-
* produce. The Home identity, revision, and ID high-water marks are preserved
|
|
265
|
-
* so the opened store continues from the same counters.
|
|
266
|
-
*
|
|
267
|
-
* This function NEVER writes `state.json`; it only creates/populates the
|
|
268
|
-
* sidecar database file.
|
|
269
|
-
*/
|
|
270
|
-
export function populateSqliteFromState(home, state, databaseFilename) {
|
|
271
|
-
const retiredActiveRuns = [];
|
|
272
|
-
const store = new SqliteTaskStore(home, { databaseFilename, migration: true });
|
|
273
|
-
try {
|
|
274
|
-
store.transaction(() => {
|
|
275
|
-
// Home identity + revision continuity.
|
|
276
|
-
const identity = asNullableObject(state.homeIdentity);
|
|
277
|
-
const revision = typeof state.revision === "number" ? state.revision : 0;
|
|
278
|
-
if (identity !== null) {
|
|
279
|
-
store.migrationSetHomeMeta(identity, revision);
|
|
280
|
-
}
|
|
281
|
-
// Config singleton.
|
|
282
|
-
const config = asNullableObject(state.config);
|
|
283
|
-
if (config !== null)
|
|
284
|
-
store.saveConfig(config);
|
|
285
|
-
// Global record families.
|
|
286
|
-
for (const agent of Object.values(asObjectMap(state.configuredAgents))) {
|
|
287
|
-
store.saveConfiguredAgent(agent);
|
|
288
|
-
}
|
|
289
|
-
for (const project of Object.values(asObjectMap(state.projects))) {
|
|
290
|
-
store.saveProject(project);
|
|
291
|
-
}
|
|
292
|
-
for (const profile of Object.values(asObjectMap(state.agentProfiles))) {
|
|
293
|
-
store.saveAgentProfile(profile);
|
|
294
|
-
}
|
|
295
|
-
for (const role of Object.values(asObjectMap(state.globalRoles))) {
|
|
296
|
-
store.saveGlobalRole(role);
|
|
297
|
-
}
|
|
298
|
-
for (const sessions of Object.values(asObjectMap(state.globalRoleSessionSets))) {
|
|
299
|
-
store.saveGlobalRoleSessionSet(sessions);
|
|
300
|
-
}
|
|
301
|
-
// Tasks and their per-task families.
|
|
302
|
-
const tasks = tasksOf(state);
|
|
303
|
-
for (const [taskId, stored] of Object.entries(tasks)) {
|
|
304
|
-
store.saveTask(stored.task);
|
|
305
|
-
if (stored.brief !== null) {
|
|
306
|
-
store.saveTaskBrief(taskId, stored.brief);
|
|
307
|
-
}
|
|
308
|
-
for (const role of Object.values(stored.roles)) {
|
|
309
|
-
store.saveRole(taskId, role);
|
|
310
|
-
}
|
|
311
|
-
for (const workspace of Object.values(stored.managedWorkspaces)) {
|
|
312
|
-
store.saveManagedWorkspace(workspace);
|
|
313
|
-
}
|
|
314
|
-
for (const sessions of Object.values(stored.roleSessionSets)) {
|
|
315
|
-
store.saveRoleSessionSet(sessions);
|
|
316
|
-
}
|
|
317
|
-
for (const item of Object.values(stored.workItems)) {
|
|
318
|
-
store.saveWorkItem(taskId, item);
|
|
319
|
-
}
|
|
320
|
-
for (const snapshot of Object.values(stored.contextSnapshots)) {
|
|
321
|
-
store.saveContextSnapshot(snapshot);
|
|
322
|
-
}
|
|
323
|
-
// Review Agent Runs carry a strict reference to their ReviewRound, so
|
|
324
|
-
// persist the parent records before any Run that belongs to one. A
|
|
325
|
-
// ReviewRound may name its Reviewer Run, but saveReviewRound deliberately
|
|
326
|
-
// accepts that forward reference while the fresh database is populated.
|
|
327
|
-
for (const round of Object.values(stored.reviewRounds)) {
|
|
328
|
-
store.saveReviewRound(taskId, round);
|
|
329
|
-
}
|
|
330
|
-
for (const run of Object.values(stored.agentRuns)) {
|
|
331
|
-
store.saveAgentRun(run);
|
|
332
|
-
}
|
|
333
|
-
for (const changeSet of Object.values(stored.changeSets)) {
|
|
334
|
-
store.saveChangeSet(taskId, changeSet);
|
|
335
|
-
}
|
|
336
|
-
for (const attempt of Object.values(stored.integrationAttempts)) {
|
|
337
|
-
store.saveIntegrationAttempt(taskId, attempt);
|
|
338
|
-
}
|
|
339
|
-
for (const entry of Object.values(stored.integrationQueue)) {
|
|
340
|
-
store.saveIntegrationQueueEntry(taskId, entry);
|
|
341
|
-
}
|
|
342
|
-
for (const job of Object.values(stored.durableJobs)) {
|
|
343
|
-
store.saveDurableJob(taskId, job);
|
|
344
|
-
}
|
|
345
|
-
for (const [key, hash] of Object.entries(stored.jobCallerKeyHashes)) {
|
|
346
|
-
const separator = key.indexOf("\0");
|
|
347
|
-
if (separator <= 0 || separator === key.length - 1
|
|
348
|
-
|| key.indexOf("\0", separator + 1) !== -1) {
|
|
349
|
-
throw new Error(`Job caller key hash identity is invalid: ${taskId}/${key}.`);
|
|
350
|
-
}
|
|
351
|
-
store.setJobCallerKeyHash(taskId, key.slice(0, separator), key.slice(separator + 1), hash);
|
|
352
|
-
}
|
|
353
|
-
// Active-run pointers: the document stores { schemaVersion, runId }
|
|
354
|
-
// keyed by pointer; the store derives the pointer from the Run.
|
|
355
|
-
for (const [pointer, value] of Object.entries(stored.activeRuns)) {
|
|
356
|
-
if (stored.task.status === "retired") {
|
|
357
|
-
retiredActiveRuns.push({
|
|
358
|
-
taskId,
|
|
359
|
-
pointer,
|
|
360
|
-
value,
|
|
361
|
-
updatedAt: typeof stored.task.updatedAt === "string"
|
|
362
|
-
? stored.task.updatedAt
|
|
363
|
-
: new Date().toISOString()
|
|
364
|
-
});
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
const run = stored.agentRuns[value.runId];
|
|
368
|
-
if (run === undefined) {
|
|
369
|
-
throw new Error(`Active run pointer ${taskId}/${pointer} references missing agent run ${value.runId}.`);
|
|
370
|
-
}
|
|
371
|
-
if (pointer.startsWith("/execution-lane/")) {
|
|
372
|
-
store.saveActiveExecutionLaneRun(run);
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
store.saveActiveAgentRun(run);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
for (const message of Object.values(stored.messages)) {
|
|
379
|
-
store.saveMessage(taskId, message);
|
|
380
|
-
}
|
|
381
|
-
for (const request of Object.values(stored.inputRequests)) {
|
|
382
|
-
store.saveInputRequest(taskId, request);
|
|
383
|
-
}
|
|
384
|
-
for (const decision of Object.values(stored.decisions)) {
|
|
385
|
-
store.saveDecision(taskId, decision);
|
|
386
|
-
}
|
|
387
|
-
for (const milestone of Object.values(stored.milestones)) {
|
|
388
|
-
store.saveMilestone(taskId, milestone);
|
|
389
|
-
}
|
|
390
|
-
for (const event of Object.values(stored.events)) {
|
|
391
|
-
store.saveEvent(taskId, event);
|
|
392
|
-
}
|
|
393
|
-
for (const wake of Object.values(stored.wakes)) {
|
|
394
|
-
store.saveTaskWake(taskId, wake);
|
|
395
|
-
}
|
|
396
|
-
if (stored.leaderFailure !== null) {
|
|
397
|
-
store.saveLeaderFailure(stored.leaderFailure);
|
|
398
|
-
}
|
|
399
|
-
// Per-task ID high-water marks.
|
|
400
|
-
for (const [kind, highWater] of Object.entries(stored.idHighWaterMarks)) {
|
|
401
|
-
if (typeof highWater === "number" && Number.isFinite(highWater)) {
|
|
402
|
-
store.migrationSeedIdSequence(taskId, kind, highWater);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// Capability grants and release workflows (task-15 record families).
|
|
406
|
-
for (const grant of Object.values(stored.capabilityGrants)) {
|
|
407
|
-
store.migrationRestoreCapabilityGrant(taskId, grant);
|
|
408
|
-
}
|
|
409
|
-
for (const workflow of Object.values(stored.releaseWorkflows)) {
|
|
410
|
-
store.saveReleaseWorkflow(taskId, workflow);
|
|
411
|
-
}
|
|
412
|
-
for (const reference of Object.values(stored.publicationReferences)) {
|
|
413
|
-
store.savePublicationReference(taskId, reference);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
// Work mailboxes.
|
|
417
|
-
for (const mailbox of Object.values(asObjectMap(state.mailboxes))) {
|
|
418
|
-
store.saveWorkMailbox(mailbox);
|
|
419
|
-
}
|
|
420
|
-
// Global ID high-water marks, derived from existing task/project ids.
|
|
421
|
-
seedGlobalSequences(store, state);
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
finally {
|
|
425
|
-
store.close();
|
|
426
|
-
}
|
|
427
|
-
persistRetiredActiveRunPointers(home, databaseFilename, retiredActiveRuns);
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* A retired Task is an explicit isolation boundary. Its active-run rows are
|
|
431
|
-
* retained byte-for-byte at the logical record level even when their Run is
|
|
432
|
-
* missing; normal Tasks continue through the referentially strict store path.
|
|
433
|
-
*/
|
|
434
|
-
function persistRetiredActiveRunPointers(home, databaseFilename, pointers) {
|
|
435
|
-
if (pointers.length === 0)
|
|
436
|
-
return;
|
|
437
|
-
const db = new Database(join(home, databaseFilename));
|
|
438
|
-
try {
|
|
439
|
-
const insert = db.prepare(`INSERT INTO active_runs (task_id, pointer, run_id, payload, updated_at)
|
|
440
|
-
VALUES (?, ?, ?, ?, ?)`);
|
|
441
|
-
db.transaction(() => {
|
|
442
|
-
for (const entry of pointers) {
|
|
443
|
-
if (typeof entry.value.runId !== "string") {
|
|
444
|
-
throw new Error(`Retired Task active run pointer ${entry.taskId}/${entry.pointer} `
|
|
445
|
-
+ "cannot be represented because runId is not a string.");
|
|
446
|
-
}
|
|
447
|
-
insert.run(entry.taskId, entry.pointer, entry.value.runId, JSON.stringify(entry.value), entry.updatedAt);
|
|
448
|
-
}
|
|
449
|
-
})();
|
|
450
|
-
}
|
|
451
|
-
finally {
|
|
452
|
-
db.close();
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Seed `global_sequences` from the numeric suffixes of existing task and
|
|
457
|
-
* project ids so the next allocated id never collides with a historical one.
|
|
458
|
-
* Ids that do not match `<kind>-<n>` are ignored (legacy formats).
|
|
459
|
-
*/
|
|
460
|
-
function seedGlobalSequences(store, state) {
|
|
461
|
-
const taskMax = maxIdSuffix(Object.keys(asObject(state.tasks)), "task");
|
|
462
|
-
const projectMax = maxIdSuffix(Object.keys(asObject(state.projects)), "project");
|
|
463
|
-
if (taskMax > 0)
|
|
464
|
-
store.migrationSeedGlobalSequence("task", taskMax);
|
|
465
|
-
if (projectMax > 0)
|
|
466
|
-
store.migrationSeedGlobalSequence("project", projectMax);
|
|
467
|
-
}
|
|
468
|
-
function maxIdSuffix(ids, prefix) {
|
|
469
|
-
let max = 0;
|
|
470
|
-
const pattern = new RegExp(`^${prefix}-(\\d+)$`, "u");
|
|
471
|
-
for (const id of ids) {
|
|
472
|
-
const match = pattern.exec(id);
|
|
473
|
-
if (match !== null) {
|
|
474
|
-
const value = Number.parseInt(match[1], 10);
|
|
475
|
-
if (Number.isSafeInteger(value))
|
|
476
|
-
max = Math.max(max, value);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return max;
|
|
480
|
-
}
|
|
481
|
-
// ---------------------------------------------------------------------------
|
|
482
|
-
// State-side checksums (the source of truth)
|
|
483
|
-
// ---------------------------------------------------------------------------
|
|
484
|
-
/**
|
|
485
|
-
* Compute per-family checksums from the parsed `state.json` document. This is
|
|
486
|
-
* the expected checksum set; the staged database is verified against it.
|
|
487
|
-
*/
|
|
488
|
-
export function computeStateFamilyChecksums(state) {
|
|
489
|
-
const checksums = {};
|
|
490
|
-
checksums.config = hashRecords(asNullableObject(state.config) === null ? [] : [asObject(state.config)]);
|
|
491
|
-
checksums.configuredAgent = hashRecords(Object.values(asObjectMap(state.configuredAgents)));
|
|
492
|
-
checksums.project = hashRecords(Object.values(asObjectMap(state.projects)));
|
|
493
|
-
checksums.agentProfile = hashRecords(Object.values(asObjectMap(state.agentProfiles)));
|
|
494
|
-
checksums.globalRole = hashRecords(Object.values(asObjectMap(state.globalRoles)));
|
|
495
|
-
checksums.globalRoleSessionSet = hashRecords(Object.values(asObjectMap(state.globalRoleSessionSets)));
|
|
496
|
-
// Flatten the per-task families across all tasks.
|
|
497
|
-
const tasks = tasksOf(state);
|
|
498
|
-
const taskRecords = [];
|
|
499
|
-
const briefs = [];
|
|
500
|
-
const roles = [];
|
|
501
|
-
const workspaces = [];
|
|
502
|
-
const roleSessionSets = [];
|
|
503
|
-
const workItems = [];
|
|
504
|
-
const contextSnapshots = [];
|
|
505
|
-
const agentRuns = [];
|
|
506
|
-
const reviewRounds = [];
|
|
507
|
-
const changeSets = [];
|
|
508
|
-
const integrationAttempts = [];
|
|
509
|
-
const integrationQueue = [];
|
|
510
|
-
const durableJobs = [];
|
|
511
|
-
const jobCallerKeyHashes = [];
|
|
512
|
-
const activeRunPointers = [];
|
|
513
|
-
const messages = [];
|
|
514
|
-
const inputRequests = [];
|
|
515
|
-
const decisions = [];
|
|
516
|
-
const milestones = [];
|
|
517
|
-
const events = [];
|
|
518
|
-
const wakes = [];
|
|
519
|
-
const leaderFailures = [];
|
|
520
|
-
const operatorNotifications = [];
|
|
521
|
-
const capabilityGrants = [];
|
|
522
|
-
const releaseWorkflows = [];
|
|
523
|
-
const publicationReferences = [];
|
|
524
|
-
for (const stored of Object.values(tasks)) {
|
|
525
|
-
taskRecords.push(stored.task);
|
|
526
|
-
if (stored.brief !== null)
|
|
527
|
-
briefs.push(stored.brief);
|
|
528
|
-
roles.push(...Object.values(stored.roles));
|
|
529
|
-
workspaces.push(...Object.values(stored.managedWorkspaces));
|
|
530
|
-
roleSessionSets.push(...Object.values(stored.roleSessionSets));
|
|
531
|
-
workItems.push(...Object.values(stored.workItems));
|
|
532
|
-
contextSnapshots.push(...Object.values(stored.contextSnapshots));
|
|
533
|
-
agentRuns.push(...Object.values(stored.agentRuns));
|
|
534
|
-
reviewRounds.push(...Object.values(stored.reviewRounds));
|
|
535
|
-
changeSets.push(...Object.values(stored.changeSets));
|
|
536
|
-
integrationAttempts.push(...Object.values(stored.integrationAttempts));
|
|
537
|
-
integrationQueue.push(...Object.values(stored.integrationQueue));
|
|
538
|
-
durableJobs.push(...Object.values(stored.durableJobs));
|
|
539
|
-
for (const [key, hash] of Object.entries(stored.jobCallerKeyHashes)) {
|
|
540
|
-
const separator = key.indexOf("\0");
|
|
541
|
-
if (separator <= 0 || separator === key.length - 1
|
|
542
|
-
|| key.indexOf("\0", separator + 1) !== -1) {
|
|
543
|
-
throw new Error(`Job caller key hash identity is invalid: ${stored.task.id}/${key}.`);
|
|
544
|
-
}
|
|
545
|
-
jobCallerKeyHashes.push({
|
|
546
|
-
taskId: stored.task.id,
|
|
547
|
-
key,
|
|
548
|
-
hash
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
activeRunPointers.push(...Object.values(stored.activeRuns));
|
|
552
|
-
messages.push(...Object.values(stored.messages));
|
|
553
|
-
inputRequests.push(...Object.values(stored.inputRequests));
|
|
554
|
-
decisions.push(...Object.values(stored.decisions));
|
|
555
|
-
milestones.push(...Object.values(stored.milestones));
|
|
556
|
-
events.push(...Object.values(stored.events));
|
|
557
|
-
wakes.push(...Object.values(stored.wakes));
|
|
558
|
-
if (stored.leaderFailure !== null)
|
|
559
|
-
leaderFailures.push(stored.leaderFailure);
|
|
560
|
-
if (stored.operatorNotification !== null)
|
|
561
|
-
operatorNotifications.push(stored.operatorNotification);
|
|
562
|
-
capabilityGrants.push(...Object.values(stored.capabilityGrants));
|
|
563
|
-
releaseWorkflows.push(...Object.values(stored.releaseWorkflows));
|
|
564
|
-
publicationReferences.push(...Object.values(stored.publicationReferences));
|
|
565
|
-
}
|
|
566
|
-
checksums.task = hashRecords(taskRecords);
|
|
567
|
-
checksums.taskBrief = hashRecords(briefs);
|
|
568
|
-
checksums.taskRole = hashRecords(roles);
|
|
569
|
-
checksums.managedWorkspace = hashRecords(workspaces);
|
|
570
|
-
checksums.taskRoleSessionSet = hashRecords(roleSessionSets);
|
|
571
|
-
checksums.workItem = hashRecords(workItems);
|
|
572
|
-
checksums.contextSnapshot = hashRecords(contextSnapshots);
|
|
573
|
-
checksums.agentRun = hashRecords(agentRuns);
|
|
574
|
-
checksums.reviewRound = hashRecords(reviewRounds);
|
|
575
|
-
checksums.changeSet = hashRecords(changeSets);
|
|
576
|
-
checksums.integrationAttempt = hashRecords(integrationAttempts);
|
|
577
|
-
checksums.integrationQueue = hashRecords(integrationQueue);
|
|
578
|
-
checksums.durableJob = hashRecords(durableJobs);
|
|
579
|
-
checksums.jobCallerKeyHash = hashRecords(jobCallerKeyHashes);
|
|
580
|
-
checksums.activeRunPointer = hashRecords(activeRunPointers);
|
|
581
|
-
checksums.message = hashRecords(messages);
|
|
582
|
-
checksums.inputRequest = hashRecords(inputRequests);
|
|
583
|
-
checksums.decision = hashRecords(decisions);
|
|
584
|
-
checksums.milestone = hashRecords(milestones);
|
|
585
|
-
checksums.event = hashRecords(events);
|
|
586
|
-
checksums.taskWake = hashRecords(wakes);
|
|
587
|
-
checksums.leaderFailure = hashRecords(leaderFailures);
|
|
588
|
-
checksums.operatorNotification = hashRecords(operatorNotifications);
|
|
589
|
-
checksums.capabilityGrant = hashRecords(capabilityGrants);
|
|
590
|
-
checksums.releaseWorkflow = hashRecords(releaseWorkflows);
|
|
591
|
-
checksums.publicationReference = hashRecords(publicationReferences);
|
|
592
|
-
checksums.workMailbox = hashRecords(Object.values(asObjectMap(state.mailboxes)));
|
|
593
|
-
return checksums;
|
|
594
|
-
}
|
|
595
|
-
function hashPayloadTable(db, sql) {
|
|
596
|
-
const rows = db.prepare(sql).all();
|
|
597
|
-
return hashRecords(rows.map((row) => JSON.parse(row.payload)));
|
|
598
|
-
}
|
|
599
|
-
export function rowToMailbox(row) {
|
|
600
|
-
let target;
|
|
601
|
-
switch (row.target_kind) {
|
|
602
|
-
case "operator":
|
|
603
|
-
target = { kind: "operator" };
|
|
604
|
-
break;
|
|
605
|
-
case "task":
|
|
606
|
-
target = { kind: "task", taskId: row.task_id };
|
|
607
|
-
break;
|
|
608
|
-
case "role":
|
|
609
|
-
target = { kind: "role", taskId: row.task_id, roleName: row.role_name };
|
|
610
|
-
break;
|
|
611
|
-
case "role-runtime":
|
|
612
|
-
target = { kind: "role-runtime", taskId: row.task_id, roleName: row.role_name };
|
|
613
|
-
break;
|
|
614
|
-
case "global-role-runtime":
|
|
615
|
-
target = { kind: "global-role-runtime", roleName: row.role_name };
|
|
616
|
-
break;
|
|
617
|
-
default:
|
|
618
|
-
target = { kind: row.target_kind };
|
|
619
|
-
}
|
|
620
|
-
const common = {
|
|
621
|
-
target,
|
|
622
|
-
nextSequence: row.next_sequence,
|
|
623
|
-
processing: row.processing === null ? null : JSON.parse(row.processing),
|
|
624
|
-
pending: row.pending === null ? null : JSON.parse(row.pending)
|
|
625
|
-
};
|
|
626
|
-
return Object.hasOwn(row, "input_delivery")
|
|
627
|
-
? {
|
|
628
|
-
schemaVersion: 2,
|
|
629
|
-
...common,
|
|
630
|
-
inputDelivery: row.input_delivery === null
|
|
631
|
-
? null
|
|
632
|
-
: JSON.parse(row.input_delivery)
|
|
633
|
-
}
|
|
634
|
-
: { schemaVersion: 1, ...common };
|
|
635
|
-
}
|
|
636
|
-
function readWorkMailboxRows(db) {
|
|
637
|
-
const hasInputDelivery = db.prepare("PRAGMA table_info(mailboxes)").all().some(({ name }) => name === "input_delivery");
|
|
638
|
-
return db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending${hasInputDelivery ? ", input_delivery" : ""} FROM mailboxes ORDER BY target_key`).all();
|
|
639
|
-
}
|
|
640
|
-
function readWorkMailboxesFromSqlite(dbPath) {
|
|
641
|
-
const db = new Database(dbPath, { readonly: true });
|
|
642
|
-
try {
|
|
643
|
-
const mailboxes = {};
|
|
644
|
-
for (const row of readWorkMailboxRows(db)) {
|
|
645
|
-
const mailbox = rowToMailbox(row);
|
|
646
|
-
mailboxes[mailboxTargetKey(mailbox.target)] = mailbox;
|
|
647
|
-
}
|
|
648
|
-
return mailboxes;
|
|
649
|
-
}
|
|
650
|
-
finally {
|
|
651
|
-
db.close();
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Compute per-family checksums from the SQLite database. The database is
|
|
656
|
-
* opened read-only; this is the independent verification read.
|
|
657
|
-
*/
|
|
658
|
-
export function computeDbFamilyChecksums(home, databaseFilename) {
|
|
659
|
-
const dbPath = join(home, databaseFilename);
|
|
660
|
-
const db = new Database(dbPath, { readonly: true });
|
|
661
|
-
try {
|
|
662
|
-
const checksums = {};
|
|
663
|
-
checksums.config = hashPayloadTable(db, "SELECT payload FROM config");
|
|
664
|
-
checksums.configuredAgent = hashPayloadTable(db, "SELECT payload FROM configured_agents");
|
|
665
|
-
checksums.project = hashPayloadTable(db, "SELECT payload FROM projects");
|
|
666
|
-
checksums.agentProfile = hashPayloadTable(db, "SELECT payload FROM agent_profiles");
|
|
667
|
-
checksums.globalRole = hashPayloadTable(db, "SELECT payload FROM global_roles");
|
|
668
|
-
checksums.globalRoleSessionSet = hashPayloadTable(db, "SELECT payload FROM global_role_session_sets");
|
|
669
|
-
checksums.task = hashPayloadTable(db, "SELECT payload FROM task_records");
|
|
670
|
-
checksums.taskBrief = hashPayloadTable(db, "SELECT brief AS payload FROM task_records WHERE brief IS NOT NULL");
|
|
671
|
-
checksums.taskRole = hashPayloadTable(db, "SELECT payload FROM task_roles");
|
|
672
|
-
checksums.managedWorkspace = hashPayloadTable(db, "SELECT payload FROM managed_workspaces");
|
|
673
|
-
checksums.taskRoleSessionSet = hashPayloadTable(db, "SELECT payload FROM role_session_sets");
|
|
674
|
-
checksums.workItem = hashPayloadTable(db, "SELECT payload FROM work_items");
|
|
675
|
-
checksums.contextSnapshot = hashPayloadTable(db, "SELECT payload FROM context_snapshots");
|
|
676
|
-
checksums.agentRun = hashPayloadTable(db, "SELECT payload FROM agent_runs");
|
|
677
|
-
checksums.reviewRound = hashPayloadTable(db, "SELECT payload FROM review_rounds");
|
|
678
|
-
checksums.changeSet = hashPayloadTable(db, "SELECT payload FROM change_sets");
|
|
679
|
-
checksums.integrationAttempt = hashPayloadTable(db, "SELECT payload FROM integration_attempts");
|
|
680
|
-
checksums.integrationQueue = hashPayloadTable(db, "SELECT payload FROM integration_queue");
|
|
681
|
-
checksums.durableJob = hashPayloadTable(db, "SELECT payload FROM durable_jobs");
|
|
682
|
-
const callerHashRows = db.prepare("SELECT task_id, role_name, agent_id, hash FROM job_caller_key_hashes").all();
|
|
683
|
-
checksums.jobCallerKeyHash = hashRecords(callerHashRows.map((row) => ({
|
|
684
|
-
taskId: row.task_id,
|
|
685
|
-
key: `${row.role_name}\0${row.agent_id}`,
|
|
686
|
-
hash: row.hash
|
|
687
|
-
})));
|
|
688
|
-
checksums.activeRunPointer = hashPayloadTable(db, "SELECT payload FROM active_runs");
|
|
689
|
-
checksums.message = hashPayloadTable(db, "SELECT payload FROM messages");
|
|
690
|
-
checksums.inputRequest = hashPayloadTable(db, "SELECT payload FROM input_requests");
|
|
691
|
-
checksums.decision = hashPayloadTable(db, "SELECT payload FROM decisions");
|
|
692
|
-
checksums.milestone = hashPayloadTable(db, "SELECT payload FROM milestones");
|
|
693
|
-
checksums.event = hashPayloadTable(db, "SELECT payload FROM events");
|
|
694
|
-
checksums.taskWake = hashPayloadTable(db, "SELECT payload FROM task_wakes");
|
|
695
|
-
checksums.leaderFailure = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'leader-failure' AND payload IS NOT NULL");
|
|
696
|
-
checksums.operatorNotification = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'operator-notification' AND payload IS NOT NULL");
|
|
697
|
-
checksums.capabilityGrant = hashPayloadTable(db, "SELECT payload FROM capability_grants");
|
|
698
|
-
checksums.releaseWorkflow = hashPayloadTable(db, "SELECT payload FROM release_workflows");
|
|
699
|
-
checksums.publicationReference = hashPayloadTable(db, "SELECT payload FROM publication_references");
|
|
700
|
-
// Mailboxes are reconstructed from typed columns (no payload column).
|
|
701
|
-
const mailboxRows = readWorkMailboxRows(db);
|
|
702
|
-
checksums.workMailbox = hashRecords(mailboxRows.map(rowToMailbox));
|
|
703
|
-
return checksums;
|
|
704
|
-
}
|
|
705
|
-
finally {
|
|
706
|
-
db.close();
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
// ---------------------------------------------------------------------------
|
|
710
|
-
// Verification (Verify phase of §8.2)
|
|
711
|
-
// ---------------------------------------------------------------------------
|
|
712
|
-
/**
|
|
713
|
-
* Compare the per-family checksums of the source document against those of the
|
|
714
|
-
* staged database. Throws on any count or content mismatch, naming every
|
|
715
|
-
* divergent family. The source document is never modified.
|
|
716
|
-
*/
|
|
717
|
-
export function verifySqliteChecksums(state, home, databaseFilename) {
|
|
718
|
-
const expected = computeStateFamilyChecksums(state);
|
|
719
|
-
const actual = computeDbFamilyChecksums(home, databaseFilename);
|
|
720
|
-
const families = new Set([...Object.keys(expected), ...Object.keys(actual)]);
|
|
721
|
-
const mismatches = [];
|
|
722
|
-
for (const family of families) {
|
|
723
|
-
const e = expected[family];
|
|
724
|
-
const a = actual[family];
|
|
725
|
-
if (e === undefined || a === undefined || e.count !== a.count || e.hash !== a.hash) {
|
|
726
|
-
mismatches.push(`${family} (expected ${e === undefined ? "absent" : `${e.count}/${e.hash.slice(0, 12)}`}, ` +
|
|
727
|
-
`found ${a === undefined ? "absent" : `${a.count}/${a.hash.slice(0, 12)}`})`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
if (mismatches.length > 0) {
|
|
731
|
-
throw new Error(`SQLite migration checksum mismatch for ${mismatches.length} family/families: ${mismatches.join("; ")}`);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
// ---------------------------------------------------------------------------
|
|
735
|
-
// SQLite -> Snapshot reconstruction (record-migration source, Issue 01 Phase 2)
|
|
736
|
-
// ---------------------------------------------------------------------------
|
|
737
|
-
/**
|
|
738
|
-
* Reconstruct the state.json-shaped snapshot from a committed SQLite database.
|
|
739
|
-
*
|
|
740
|
-
* This is the reverse of {@link populateSqliteFromState}: it reads every
|
|
741
|
-
* record family from `yui.db` and rebuilds the nested document shape the
|
|
742
|
-
* generic migration engine's record transforms operate on. Payloads are read
|
|
743
|
-
* RAW (`JSON.parse` without the strict current-version parsers) so a record
|
|
744
|
-
* family at an older persisted version survives the round-trip with its
|
|
745
|
-
* original `schemaVersion` intact — the strict parsers only understand the
|
|
746
|
-
* current version and would reject the very records the migration is meant to
|
|
747
|
-
* transform.
|
|
748
|
-
*
|
|
749
|
-
* The database is opened read-only; this function never writes.
|
|
750
|
-
*/
|
|
751
|
-
export function readStateFromSqlite(home, databaseFilename = COMMITTED_DATABASE_FILENAME) {
|
|
752
|
-
const dbPath = join(home, databaseFilename);
|
|
753
|
-
const db = new Database(dbPath, { readonly: true });
|
|
754
|
-
try {
|
|
755
|
-
const manifest = readStorageSchemaManifest(home);
|
|
756
|
-
const storedTaskVersion = typeof manifest.recordVersions?.storedTask === "number"
|
|
757
|
-
? manifest.recordVersions.storedTask
|
|
758
|
-
: CURRENT_STORED_TASK_SCHEMA_VERSION;
|
|
759
|
-
// Home identity + revision continuity.
|
|
760
|
-
const meta = db.prepare("SELECT home_identity, revision FROM home_meta WHERE id = 1").get();
|
|
761
|
-
const state = {
|
|
762
|
-
// The state document's `schemaVersion` is the Yui aggregate version,
|
|
763
|
-
// sourced from the durable manifest (the version contract). The
|
|
764
|
-
// `home_meta.aggregate_version` column is the SQLite database's own
|
|
765
|
-
// schema version (SQLITE_AGGREGATE_VERSION), a different fact.
|
|
766
|
-
schemaVersion: manifest.aggregateSchemaVersion,
|
|
767
|
-
revision: meta.revision,
|
|
768
|
-
homeIdentity: JSON.parse(meta.home_identity),
|
|
769
|
-
configuredAgents: {},
|
|
770
|
-
projects: {},
|
|
771
|
-
agentProfiles: {},
|
|
772
|
-
globalRoles: {},
|
|
773
|
-
globalRoleSessionSets: {},
|
|
774
|
-
tasks: {},
|
|
775
|
-
mailboxes: {}
|
|
776
|
-
};
|
|
777
|
-
// Config singleton (absent on a fresh, never-configured Home).
|
|
778
|
-
const configRow = db.prepare("SELECT payload FROM config WHERE id = 1").get();
|
|
779
|
-
if (configRow !== undefined) {
|
|
780
|
-
state.config = JSON.parse(configRow.payload);
|
|
781
|
-
}
|
|
782
|
-
// Global record families.
|
|
783
|
-
loadGlobalPayloadMap(db, "configured_agents", state, "configuredAgents", (record) => record.id);
|
|
784
|
-
loadGlobalPayloadMap(db, "projects", state, "projects", (record) => record.id);
|
|
785
|
-
loadGlobalPayloadMap(db, "agent_profiles", state, "agentProfiles", (record) => record.id);
|
|
786
|
-
loadGlobalPayloadMap(db, "global_roles", state, "globalRoles", (record) => record.name);
|
|
787
|
-
loadGlobalPayloadMap(db, "global_role_session_sets", state, "globalRoleSessionSets", (record) => record.owner.roleName);
|
|
788
|
-
// Tasks: the StoredTask aggregate wrapper. The wrapper `schemaVersion` is
|
|
789
|
-
// not persisted in the database (records live in flat tables), so it is
|
|
790
|
-
// taken from the manifest's declared `storedTask` version — the version
|
|
791
|
-
// the database was created with — so the version scanner and the migration
|
|
792
|
-
// engine see a consistent source.
|
|
793
|
-
const tasks = state.tasks;
|
|
794
|
-
const taskRows = db.prepare("SELECT task_id, payload, brief FROM task_records").all();
|
|
795
|
-
for (const row of taskRows) {
|
|
796
|
-
const aggregate = {
|
|
797
|
-
schemaVersion: storedTaskVersion,
|
|
798
|
-
task: JSON.parse(row.payload),
|
|
799
|
-
brief: row.brief === null ? null : JSON.parse(row.brief),
|
|
800
|
-
roles: {},
|
|
801
|
-
managedWorkspaces: {},
|
|
802
|
-
roleSessionSets: {},
|
|
803
|
-
workItems: {},
|
|
804
|
-
contextSnapshots: {},
|
|
805
|
-
agentRuns: {},
|
|
806
|
-
reviewRounds: {},
|
|
807
|
-
changeSets: {},
|
|
808
|
-
integrationAttempts: {},
|
|
809
|
-
integrationQueue: {},
|
|
810
|
-
durableJobs: {},
|
|
811
|
-
jobCallerKeyHashes: {},
|
|
812
|
-
activeRuns: {},
|
|
813
|
-
messages: {},
|
|
814
|
-
inputRequests: {},
|
|
815
|
-
decisions: {},
|
|
816
|
-
milestones: {},
|
|
817
|
-
events: {},
|
|
818
|
-
wakes: {},
|
|
819
|
-
leaderFailure: null,
|
|
820
|
-
idHighWaterMarks: {},
|
|
821
|
-
capabilityGrants: {},
|
|
822
|
-
releaseWorkflows: {},
|
|
823
|
-
publicationReferences: {}
|
|
824
|
-
};
|
|
825
|
-
if (storedTaskVersion <= 17)
|
|
826
|
-
aggregate.operatorNotification = null;
|
|
827
|
-
tasks[row.task_id] = aggregate;
|
|
828
|
-
}
|
|
829
|
-
// Per-task record families, keyed by their state.json map keys.
|
|
830
|
-
loadTaskPayloadMap(db, "task_roles", tasks, "roles", (record) => record.name);
|
|
831
|
-
loadTaskPayloadMap(db, "managed_workspaces", tasks, "managedWorkspaces", (record) => managedWorkspaceKey(record.owner));
|
|
832
|
-
loadTaskPayloadMap(db, "role_session_sets", tasks, "roleSessionSets", (record) => record.owner.roleName);
|
|
833
|
-
loadTaskPayloadMap(db, "work_items", tasks, "workItems", (record) => record.id);
|
|
834
|
-
loadTaskPayloadMap(db, "context_snapshots", tasks, "contextSnapshots", (record) => record.id);
|
|
835
|
-
loadTaskPayloadMap(db, "agent_runs", tasks, "agentRuns", (record) => record.id);
|
|
836
|
-
loadTaskPayloadMap(db, "review_rounds", tasks, "reviewRounds", (record) => record.id);
|
|
837
|
-
loadTaskPayloadMap(db, "change_sets", tasks, "changeSets", (record) => record.id);
|
|
838
|
-
loadTaskPayloadMap(db, "integration_attempts", tasks, "integrationAttempts", (record) => record.id);
|
|
839
|
-
loadTaskPayloadMap(db, "integration_queue", tasks, "integrationQueue", (record) => record.id);
|
|
840
|
-
loadTaskPayloadMap(db, "durable_jobs", tasks, "durableJobs", (record) => record.id);
|
|
841
|
-
loadTaskPayloadMap(db, "messages", tasks, "messages", (record) => record.id);
|
|
842
|
-
loadTaskPayloadMap(db, "input_requests", tasks, "inputRequests", (record) => record.id);
|
|
843
|
-
loadTaskPayloadMap(db, "decisions", tasks, "decisions", (record) => record.id);
|
|
844
|
-
loadTaskPayloadMap(db, "milestones", tasks, "milestones", (record) => record.id);
|
|
845
|
-
loadTaskPayloadMap(db, "events", tasks, "events", (record) => record.id);
|
|
846
|
-
loadTaskPayloadMap(db, "task_wakes", tasks, "wakes", (record) => record.id);
|
|
847
|
-
loadTaskPayloadMap(db, "capability_grants", tasks, "capabilityGrants", (record) => record.id);
|
|
848
|
-
loadTaskPayloadMap(db, "release_workflows", tasks, "releaseWorkflows", (record) => record.id);
|
|
849
|
-
loadTaskPayloadMap(db, "publication_references", tasks, "publicationReferences", (record) => record.id);
|
|
850
|
-
// Job caller key hashes: keyed by `${roleName}\0${agentId}`.
|
|
851
|
-
const callerHashRows = db.prepare("SELECT task_id, role_name, agent_id, hash FROM job_caller_key_hashes").all();
|
|
852
|
-
for (const row of callerHashRows) {
|
|
853
|
-
const stored = requireTaskAggregate(tasks, row.task_id, "jobCallerKeyHashes");
|
|
854
|
-
stored.jobCallerKeyHashes[`${row.role_name}\0${row.agent_id}`] = row.hash;
|
|
855
|
-
}
|
|
856
|
-
// Active-run pointers: keyed by pointer, value is the persisted payload.
|
|
857
|
-
const activeRunRows = db.prepare("SELECT task_id, pointer, payload FROM active_runs").all();
|
|
858
|
-
for (const row of activeRunRows) {
|
|
859
|
-
const stored = requireTaskAggregate(tasks, row.task_id, "activeRuns");
|
|
860
|
-
stored.activeRuns[row.pointer] = JSON.parse(row.payload);
|
|
861
|
-
}
|
|
862
|
-
// Leader failure / operator notification projections.
|
|
863
|
-
const projectionRows = db.prepare("SELECT task_id, kind, payload FROM task_projections WHERE payload IS NOT NULL").all();
|
|
864
|
-
for (const row of projectionRows) {
|
|
865
|
-
const stored = requireTaskAggregate(tasks, row.task_id, row.kind);
|
|
866
|
-
if (row.kind === "leader-failure") {
|
|
867
|
-
stored.leaderFailure = JSON.parse(row.payload);
|
|
868
|
-
}
|
|
869
|
-
else if (storedTaskVersion <= 17) {
|
|
870
|
-
stored.operatorNotification = JSON.parse(row.payload);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
// Per-task ID high-water marks.
|
|
874
|
-
const idSeqRows = db.prepare("SELECT task_id, kind, high_water FROM id_sequences").all();
|
|
875
|
-
for (const row of idSeqRows) {
|
|
876
|
-
const stored = requireTaskAggregate(tasks, row.task_id, "idHighWaterMarks");
|
|
877
|
-
stored.idHighWaterMarks[row.kind] = row.high_water;
|
|
878
|
-
}
|
|
879
|
-
// Work mailboxes: reconstructed from typed columns (no payload column).
|
|
880
|
-
const mailboxRows = readWorkMailboxRows(db);
|
|
881
|
-
const mailboxes = state.mailboxes;
|
|
882
|
-
for (const row of mailboxRows) {
|
|
883
|
-
const mailbox = rowToMailbox(row);
|
|
884
|
-
mailboxes[mailboxTargetKey(mailbox.target)] = mailbox;
|
|
885
|
-
}
|
|
886
|
-
return state;
|
|
887
|
-
}
|
|
888
|
-
finally {
|
|
889
|
-
db.close();
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
/** Load a global (non-task-scoped) payload table into a keyed map on `state`. */
|
|
893
|
-
function loadGlobalPayloadMap(db, table, state, stateKey, keyOf) {
|
|
894
|
-
const rows = db.prepare(`SELECT payload FROM ${table}`).all();
|
|
895
|
-
const target = state[stateKey];
|
|
896
|
-
for (const row of rows) {
|
|
897
|
-
const record = JSON.parse(row.payload);
|
|
898
|
-
target[keyOf(record)] = record;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
/** Load a task-scoped payload table into the matching family map of each task. */
|
|
902
|
-
function loadTaskPayloadMap(db, table, tasks, familyKey, keyOf) {
|
|
903
|
-
const rows = db.prepare(`SELECT task_id, payload FROM ${table}`).all();
|
|
904
|
-
for (const row of rows) {
|
|
905
|
-
const stored = requireTaskAggregate(tasks, row.task_id, table);
|
|
906
|
-
const family = stored[familyKey];
|
|
907
|
-
const record = JSON.parse(row.payload);
|
|
908
|
-
family[keyOf(record)] = record;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
/** Resolve a task's StoredTask aggregate, or throw on an orphaned child row. */
|
|
912
|
-
function requireTaskAggregate(tasks, taskId, context) {
|
|
913
|
-
const stored = tasks[taskId];
|
|
914
|
-
if (stored === undefined) {
|
|
915
|
-
throw new Error(`SQLite reconstruction found ${context} row for unknown task ${taskId}.`);
|
|
916
|
-
}
|
|
917
|
-
return stored;
|
|
918
|
-
}
|