@zq-silk/yui 0.13.9 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/ARCHITECTURE.md +69 -428
  2. package/README.md +108 -298
  3. package/dist/agent/managedRuntimeEnvironment.js +2 -2
  4. package/dist/cli/commandCatalog.js +39 -77
  5. package/dist/cli/interactionCandidates.js +4 -4
  6. package/dist/cli/interactionPolicy.js +7 -10
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/updateCommand.js +6 -27
  9. package/dist/cli/updateOrchestrator.js +63 -484
  10. package/dist/cli/updatePorts.js +15 -333
  11. package/dist/cli/upgradeCommand.js +8 -65
  12. package/dist/cli.js +57 -242
  13. package/dist/commands/configCommands.js +11 -62
  14. package/dist/commands/executionAuditCommands.js +23 -23
  15. package/dist/commands/globalRoleCommands.js +2 -7
  16. package/dist/commands/jobCommands.js +1 -4
  17. package/dist/commands/projectCommands.js +5 -5
  18. package/dist/commands/releaseCommands.js +3 -10
  19. package/dist/commands/resourcesCommands.js +2 -2
  20. package/dist/commands/sessionCommands.js +2 -2
  21. package/dist/commands/taskActor.js +31 -39
  22. package/dist/commands/taskChangeSetCommands.js +5 -12
  23. package/dist/commands/taskCommands.js +464 -1249
  24. package/dist/commands/taskContextCommand.js +35 -36
  25. package/dist/commands/taskExecutionCommands.js +33 -46
  26. package/dist/commands/taskInputCommands.js +20 -42
  27. package/dist/commands/taskNextActionCommand.js +9 -9
  28. package/dist/commands/taskOverviewCommand.js +56 -95
  29. package/dist/commands/taskRoleRuntimeStatus.js +77 -107
  30. package/dist/commands/taskUpstreamCommands.js +3 -3
  31. package/dist/commands/taskWorkspaceCommands.js +7 -79
  32. package/dist/commands/telemetryCommands.js +32 -124
  33. package/dist/commands/workflowCommands.js +3 -3
  34. package/dist/completion/fileCompletionManager.js +1 -1
  35. package/dist/config/configCatalog.js +4 -6
  36. package/dist/config/yuiConfig.js +6 -43
  37. package/dist/context/dispatchContext.js +14 -30
  38. package/dist/context/roleSessionContext.js +2 -2
  39. package/dist/context/sessionBootstrapManifest.js +2 -2
  40. package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
  41. package/dist/context/turnInputContract.js +204 -0
  42. package/dist/context/wakeNotification.js +2 -2
  43. package/dist/controller/agentRuntimeObserver.js +18 -18
  44. package/dist/controller/clientRuntime.js +13 -20
  45. package/dist/controller/controller.js +89 -116
  46. package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
  47. package/dist/controller/jobControl.js +17 -17
  48. package/dist/controller/resourceInventory.js +6 -6
  49. package/dist/controller/resourceInventoryLinux.js +38 -59
  50. package/dist/controller/resourceInventoryRpc.js +3 -3
  51. package/dist/controller/resourceInventoryWorker.js +1 -1
  52. package/dist/controller/runtime.js +46 -53
  53. package/dist/controller/runtimeEventInbox.js +23 -31
  54. package/dist/controller/runtimeEventProcessor.js +15 -15
  55. package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
  56. package/dist/controller/runtimeLaunchCoordinator.js +3 -3
  57. package/dist/controller/runtimeObservationHook.js +11 -8
  58. package/dist/controller/sessionNotify.js +11 -11
  59. package/dist/controller/structuredProviderObservation.js +89 -16
  60. package/dist/coordination/workMailbox.js +309 -579
  61. package/dist/coordination/workMailboxQueue.js +5 -6
  62. package/dist/core/controllerClient.js +1 -91
  63. package/dist/core/controllerServer.js +3 -6
  64. package/dist/doctor/doctor.js +51 -314
  65. package/dist/execution/candidateConvergence.js +3 -3
  66. package/dist/execution/executionGroup.js +32 -33
  67. package/dist/execution/executionHealth.js +36 -38
  68. package/dist/execution/resourceBroker.js +12 -12
  69. package/dist/executor/agentExecutor.js +30 -265
  70. package/dist/executor/effectiveLaunch.js +9 -19
  71. package/dist/executor/executorRegistry.js +43 -17
  72. package/dist/executor/fileRoleLaunchPlanner.js +56 -56
  73. package/dist/executor/launchPlan.js +3 -3
  74. package/dist/executor/workspacePreflightClassification.js +16 -16
  75. package/dist/grant/capabilityGrant.js +20 -25
  76. package/dist/input/inputRequest.js +12 -12
  77. package/dist/integration/changeSet.js +9 -13
  78. package/dist/integration/changeSetManifest.js +1 -1
  79. package/dist/integration/gitIntegrationService.js +2 -2
  80. package/dist/integration/integrationQueueService.js +5 -21
  81. package/dist/integration/overlapDiagnostics.js +2 -2
  82. package/dist/interaction/operatorPresentation.js +1 -1
  83. package/dist/job/jobRunner.js +1 -1
  84. package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
  85. package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
  86. package/dist/message/message.js +6 -6
  87. package/dist/observability/executionAudit.js +74 -86
  88. package/dist/observability/faultClassification.js +7 -24
  89. package/dist/observability/orchestrationMetrics.js +5 -5
  90. package/dist/observability/runtimeIdentity.js +34 -101
  91. package/dist/profile/agentProfile.js +1 -1
  92. package/dist/release/cliHomeReleaseFence.js +3 -3
  93. package/dist/release/releaseWorkflowEngine.js +6 -10
  94. package/dist/release/releaseWorkflowPorts.js +2 -2
  95. package/dist/release/workflowFileLock.js +1 -1
  96. package/dist/repository/checkoutSwap.js +1 -1
  97. package/dist/repository/gitWorkspace.js +0 -13
  98. package/dist/repository/project.js +1 -1
  99. package/dist/repository/projectMaintenanceLock.js +1 -2
  100. package/dist/repository/taskBaseFreshness.js +13 -13
  101. package/dist/repository/taskWorkspaceCoordinator.js +21 -25
  102. package/dist/repository/taskWorkspaceIdentity.js +8 -21
  103. package/dist/repository/taskWorkspacePreparer.js +63 -620
  104. package/dist/resources/autoResourceGc.js +3 -3
  105. package/dist/resources/liveReferences.js +1 -1
  106. package/dist/resources/resourceGc.js +1 -1
  107. package/dist/resources/sqliteResourceRegistry.js +1 -1
  108. package/dist/review/reviewDecision.js +7 -7
  109. package/dist/review/reviewFindingLedger.js +1 -1
  110. package/dist/review/reviewOutcomeClassifier.js +35 -50
  111. package/dist/review/reviewRound.js +14 -25
  112. package/dist/review/reviewerAvailability.js +4 -4
  113. package/dist/review/taskFinalReviewContractResolution.js +41 -0
  114. package/dist/role/role.js +6 -3
  115. package/dist/runtime/agentDriver.js +4 -0
  116. package/dist/runtime/agentHost.js +151 -36
  117. package/dist/runtime/builtinAgentDrivers.js +79 -2
  118. package/dist/runtime/codexAppServerRuntime.js +122 -0
  119. package/dist/runtime/continuationManager.js +3 -5
  120. package/dist/runtime/exactControlPlane.js +17 -42
  121. package/dist/runtime/firstProgressAdvisory.js +1 -1
  122. package/dist/runtime/index.js +2 -2
  123. package/dist/runtime/launchDiagnostics.js +2 -2
  124. package/dist/runtime/lifecycleReservation.js +4 -4
  125. package/dist/runtime/processExitObservation.js +1 -1
  126. package/dist/runtime/processExitOutbox.js +0 -32
  127. package/dist/runtime/promptEnvelope.js +9 -9
  128. package/dist/runtime/providerContinuation.js +2 -2
  129. package/dist/runtime/providerContinuationReconciliationService.js +2 -2
  130. package/dist/runtime/providerRuntimeIdentity.js +76 -24
  131. package/dist/runtime/recentTurnIds.js +38 -0
  132. package/dist/runtime/runtimeContinuationProjection.js +2 -2
  133. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  134. package/dist/runtime/runtimeObservation.js +58 -21
  135. package/dist/runtime/runtimeProjection.js +19 -27
  136. package/dist/runtime/sessionLaunchRequest.js +2 -2
  137. package/dist/runtime/structuredProviderHost.js +177 -12
  138. package/dist/runtime/tmuxAdapters.js +46 -11
  139. package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
  140. package/dist/scheduler/leaderWakeupProcessor.js +147 -445
  141. package/dist/scheduler/operatorEvent.js +4 -6
  142. package/dist/scheduler/resourceQueueProjection.js +71 -0
  143. package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
  144. package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
  145. package/dist/scheduler/taskExecutionProjection.js +99 -99
  146. package/dist/scheduler/taskObservabilityProjection.js +10 -10
  147. package/dist/scheduler/taskWake.js +4 -4
  148. package/dist/scheduler/wakeReason.js +6 -10
  149. package/dist/scheduler/wakeupQueue.js +0 -9
  150. package/dist/setup/setupCommand.js +2 -2
  151. package/dist/storage/currentTaskStore.js +46 -0
  152. package/dist/storage/persistenceWorker.js +1 -1
  153. package/dist/storage/sqliteSchema.js +81 -485
  154. package/dist/storage/sqliteStore.js +206 -365
  155. package/dist/storage/storageSchema.js +1 -15
  156. package/dist/storage/storageVersions.js +8 -9
  157. package/dist/storage/storeRpc.js +10 -34
  158. package/dist/storage/taskStore.js +63 -3415
  159. package/dist/storage/upgrade/recordVersions.js +55 -149
  160. package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
  161. package/dist/task/completionReadiness.js +5 -5
  162. package/dist/task/deliveryGuard.js +10 -10
  163. package/dist/task/nextAction.js +74 -77
  164. package/dist/task/task.js +2 -4
  165. package/dist/task/taskRecordReference.js +3 -3
  166. package/dist/task/taskRecordRetirement.js +1 -1
  167. package/dist/telemetry/sqliteTelemetryStore.js +53 -53
  168. package/dist/telemetry/telemetryConfig.js +14 -14
  169. package/dist/telemetry/telemetryWiring.js +8 -14
  170. package/dist/turn/turn.js +356 -0
  171. package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
  172. package/dist/verification/gateArtifactStore.js +2 -2
  173. package/dist/web/assets/client/app.js +1 -1
  174. package/dist/web/assets/client/components.js +29 -54
  175. package/dist/web/assets/client/i18n.js +17 -37
  176. package/dist/web/assets/client/view.js +37 -37
  177. package/dist/web/assets/shell.js +1 -1
  178. package/dist/web/assets/styles/cards.js +5 -5
  179. package/dist/web/assets/styles/widgets.js +1 -2
  180. package/dist/web/webSnapshot.js +65 -60
  181. package/dist/workItem/workItem.js +11 -11
  182. package/dist/workspace/workItemChangeSetManager.js +1 -1
  183. package/docs/task-local-identity.md +5 -5
  184. package/i18n/README.zh-CN.md +41 -54
  185. package/package.json +1 -1
  186. package/skills/yui-leader/SKILL.md +85 -83
  187. package/skills/yui-operator/SKILL.md +16 -16
  188. package/skills/yui-reviewer/SKILL.md +10 -10
  189. package/skills/yui-runtime/SKILL.md +17 -18
  190. package/skills/yui-worker/SKILL.md +16 -18
  191. package/dist/context/runContextContract.js +0 -162
  192. package/dist/executor/turnCompletion.js +0 -126
  193. package/dist/review/taskFinalReviewContractEvent.js +0 -1
  194. package/dist/review/taskFinalReviewContractRebind.js +0 -367
  195. package/dist/run/agentRun.js +0 -356
  196. package/dist/run/rejectedYieldAttempt.js +0 -221
  197. package/dist/run/runControlRequest.js +0 -50
  198. package/dist/run/yieldReceipt.js +0 -65
  199. package/dist/runtime/turnCompletion.js +0 -3
  200. package/dist/scheduler/actionability.js +0 -323
  201. package/dist/scheduler/activeRoleRunDelivery.js +0 -868
  202. package/dist/scheduler/activeTaskProgress.js +0 -126
  203. package/dist/storage/compatibleTaskStore.js +0 -248
  204. package/dist/storage/migration/baseline.js +0 -78
  205. package/dist/storage/migration/classifier.js +0 -51
  206. package/dist/storage/migration/compatibleCodec.js +0 -53
  207. package/dist/storage/migration/engine.js +0 -147
  208. package/dist/storage/migration/index.js +0 -33
  209. package/dist/storage/migration/planner.js +0 -154
  210. package/dist/storage/migration/productionRegistry.js +0 -3931
  211. package/dist/storage/migration/registry.js +0 -169
  212. package/dist/storage/migration/report.js +0 -54
  213. package/dist/storage/migration/types.js +0 -31
  214. package/dist/storage/upgrade/homeClassification.js +0 -353
  215. package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
  216. package/dist/storage/upgrade/migrationReceipt.js +0 -67
  217. package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
  218. package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
  219. package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
  220. package/dist/storage/upgrade/recordVersionScan.js +0 -176
  221. package/dist/storage/upgrade/sqliteFileSet.js +0 -50
  222. package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
  223. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
  224. package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
  225. package/dist/storage/upgrade/switchProgress.js +0 -80
  226. package/dist/storage/upgrade/upgradeReceipt.js +0 -161
  227. package/dist/storage/upgradeCoordination.js +0 -187
  228. package/dist/storage/upgradeFence.js +0 -366
  229. package/dist/telemetry/telemetryCompaction.js +0 -252
@@ -1,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
- }