@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,3931 +0,0 @@
1
- /** The one production transition registry and its baseline-to-current delivery gate. */
2
- import { generateHomeIdentity } from "../../repository/homeIdentity.js";
3
- import { latestStorageVersionState } from "../upgrade/recordVersions.js";
4
- import { assertBaselineConsistency, baselineStorageVersionState } from "./baseline.js";
5
- import { planMigration } from "./planner.js";
6
- import { MigrationRegistry } from "./registry.js";
7
- const FINAL_REVIEW_AGGREGATE_FROM_VERSION = 16;
8
- const FINAL_REVIEW_AGGREGATE_TO_VERSION = 17;
9
- const HOME_IDENTITY_AGGREGATE_FROM_VERSION = 17;
10
- const HOME_IDENTITY_AGGREGATE_TO_VERSION = 18;
11
- const RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION = 18;
12
- const RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION = 19;
13
- const MULTI_AGENT_RUNTIME_AGGREGATE_FROM_VERSION = 19;
14
- const MULTI_AGENT_RUNTIME_AGGREGATE_TO_VERSION = 20;
15
- const SQLITE_LAYOUT_FROM_VERSION = 6;
16
- const SQLITE_LAYOUT_TO_VERSION = 7;
17
- const PROJECT_FROM_VERSION = 2;
18
- const PROJECT_TO_VERSION = 3;
19
- const PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION = 3;
20
- const PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION = 4;
21
- const PROJECT_LIFECYCLE_FROM_VERSION = 4;
22
- const PROJECT_LIFECYCLE_TO_VERSION = 5;
23
- const TASK_FROM_VERSION = 3;
24
- const TASK_TO_VERSION = 4;
25
- const TASK_INTENT_FROM_VERSION = 4;
26
- const TASK_INTENT_TO_VERSION = 5;
27
- const TASK_EXECUTION_GATE_FROM_VERSION = 5;
28
- const TASK_EXECUTION_GATE_TO_VERSION = 6;
29
- const WORK_ITEM_FROM_VERSION = 6;
30
- const WORK_ITEM_TO_VERSION = 7;
31
- const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
32
- const WORK_ITEM_GIT_SNAPSHOT_TO_VERSION = 8;
33
- const WORK_ITEM_GROUP_HISTORY_FROM_VERSION = 8;
34
- const WORK_ITEM_GROUP_HISTORY_TO_VERSION = 9;
35
- const WORK_ITEM_EXPLORATION_STAGE_FROM_VERSION = 9;
36
- const WORK_ITEM_EXPLORATION_STAGE_TO_VERSION = 10;
37
- const WORK_ITEM_CANDIDATE_CONVERGENCE_FROM_VERSION = 10;
38
- const WORK_ITEM_CANDIDATE_CONVERGENCE_TO_VERSION = 11;
39
- const WORK_ITEM_RESOURCE_SCHEDULING_FROM_VERSION = 11;
40
- const WORK_ITEM_RESOURCE_SCHEDULING_TO_VERSION = 12;
41
- const AGENT_RUN_FROM_VERSION = 5;
42
- const AGENT_RUN_TO_VERSION = 6;
43
- /**
44
- * v7 combines the optional Issue 04 `providerRetry`/`yieldReceipt` fields and
45
- * Issue 05 Leader actionability fields. All are optional, so the transition is
46
- * a version-only rewrite; legacy v6 records without them remain valid.
47
- */
48
- const AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION = 6;
49
- const AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION = 7;
50
- const AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION = 7;
51
- const AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION = 8;
52
- const AGENT_RUN_RETRY_EPISODE_FROM_VERSION = 8;
53
- const AGENT_RUN_RETRY_EPISODE_TO_VERSION = 9;
54
- const AGENT_RUN_ERROR_FACTS_FROM_VERSION = 9;
55
- const AGENT_RUN_ERROR_FACTS_TO_VERSION = 10;
56
- const MESSAGE_WAKE_POLICY_FROM_VERSION = 2;
57
- const MESSAGE_WAKE_POLICY_TO_VERSION = 3;
58
- const REVIEW_ROUND_FROM_VERSION = 2;
59
- const REVIEW_ROUND_TO_VERSION = 3;
60
- const REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION = 3;
61
- const REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION = 4;
62
- const REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION = 4;
63
- const REVIEW_ROUND_TASK_ANCHOR_TO_VERSION = 5;
64
- const REVIEW_ROUND_ACTOR_FROM_VERSION = 5;
65
- const REVIEW_ROUND_ACTOR_TO_VERSION = 6;
66
- const ACTIVE_RUN_POINTER_FROM_VERSION = 1;
67
- const ACTIVE_RUN_POINTER_TO_VERSION = 2;
68
- const ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION = 2;
69
- const ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION = 3;
70
- const MANAGED_WORKSPACE_FROM_VERSION = 1;
71
- const MANAGED_WORKSPACE_TO_VERSION = 2;
72
- const CHANGE_SET_MANIFEST_FROM_VERSION = 2;
73
- const CHANGE_SET_MANIFEST_TO_VERSION = 3;
74
- const INTEGRATION_ATTEMPT_FROM_VERSION = 2;
75
- const INTEGRATION_ATTEMPT_TO_VERSION = 3;
76
- const INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION = 3;
77
- const INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION = 4;
78
- const INTEGRATION_ATTEMPT_ACTOR_FROM_VERSION = 4;
79
- const INTEGRATION_ATTEMPT_ACTOR_TO_VERSION = 5;
80
- const MILESTONE_ACTOR_FROM_VERSION = 1;
81
- const MILESTONE_ACTOR_TO_VERSION = 2;
82
- const INTEGRATION_QUEUE_FROM_VERSION = 0;
83
- const INTEGRATION_QUEUE_TO_VERSION = 1;
84
- const CONTEXT_SNAPSHOT_FROM_VERSION = 0;
85
- const CONTEXT_SNAPSHOT_TO_VERSION = 1;
86
- const STORED_TASK_DURABLE_JOBS_FROM_VERSION = 14;
87
- const STORED_TASK_DURABLE_JOBS_TO_VERSION = 15;
88
- const STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION = 15;
89
- const STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION = 16;
90
- const STORED_TASK_WAKES_FROM_VERSION = 16;
91
- const STORED_TASK_WAKES_TO_VERSION = 17;
92
- const STORED_TASK_EVENT_NOTICES_FROM_VERSION = 17;
93
- const STORED_TASK_EVENT_NOTICES_TO_VERSION = 18;
94
- const TASK_WAKE_FROM_VERSION = 0;
95
- const TASK_WAKE_TO_VERSION = 1;
96
- const STORED_TASK_PUBLICATION_REFERENCES_FROM_VERSION = 16;
97
- const STORED_TASK_PUBLICATION_REFERENCES_TO_VERSION = 17;
98
- const CAPABILITY_GRANT_FROM_VERSION = 0;
99
- const CAPABILITY_GRANT_TO_VERSION = 1;
100
- const RELEASE_WORKFLOW_FROM_VERSION = 0;
101
- const RELEASE_WORKFLOW_TO_VERSION = 1;
102
- const WORK_MAILBOX_FROM_VERSION = 1;
103
- const WORK_MAILBOX_TO_VERSION = 2;
104
- const HOST_OWNED_WORK_MAILBOX_FROM_VERSION = 2;
105
- const HOST_OWNED_WORK_MAILBOX_TO_VERSION = 3;
106
- const TASK_ROLE_FROM_VERSION = 3;
107
- const TASK_ROLE_TO_VERSION = 4;
108
- const TASK_ROLE_SESSION_SET_FROM_VERSION = 4;
109
- const TASK_ROLE_SESSION_SET_TO_VERSION = 5;
110
- const STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION = 5;
111
- const STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION = 6;
112
- const AGENT_FIRST_SESSION_SET_FROM_VERSION = 6;
113
- const AGENT_FIRST_SESSION_SET_TO_VERSION = 7;
114
- const UNIFIED_TASK_SESSION_SET_FROM_VERSION = 7;
115
- const UNIFIED_TASK_SESSION_SET_TO_VERSION = 8;
116
- const UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION = 3;
117
- const UNIFIED_GLOBAL_SESSION_SET_TO_VERSION = 4;
118
- const PUBLICATION_REFERENCE_FROM_VERSION = 0;
119
- const PUBLICATION_REFERENCE_TO_VERSION = 1;
120
- const CONFIG_FROM_VERSION = 1;
121
- const CONFIG_TO_VERSION = 2;
122
- const CONFIG_REVIEW_CONTROLS_FROM_VERSION = 2;
123
- const CONFIG_REVIEW_CONTROLS_TO_VERSION = 3;
124
- const CONFIG_AGENT_RECOVERY_FROM_VERSION = 3;
125
- const CONFIG_AGENT_RECOVERY_TO_VERSION = 4;
126
- /**
127
- * Build the authoritative production graph. Transition intent and executable
128
- * transforms are registered together here; compatible loading and offline
129
- * migration consume this same graph.
130
- */
131
- export function createProductionStorageRegistry() {
132
- assertBaselineConsistency();
133
- const registry = new MigrationRegistry().registerOfflineMigration({
134
- axis: "layout",
135
- fromVersion: SQLITE_LAYOUT_FROM_VERSION,
136
- toVersion: SQLITE_LAYOUT_TO_VERSION,
137
- preconditions: requireLayoutV6Snapshot,
138
- transform: migrateLayoutV6ToV7,
139
- declaredEffects: []
140
- })
141
- .registerOfflineMigration({
142
- axis: "aggregate",
143
- fromVersion: FINAL_REVIEW_AGGREGATE_FROM_VERSION,
144
- toVersion: FINAL_REVIEW_AGGREGATE_TO_VERSION,
145
- preconditions: requireAggregateV16Snapshot,
146
- transform: migrateAggregateV16ToV17,
147
- declaredEffects: []
148
- })
149
- .registerOfflineMigration({
150
- axis: "aggregate",
151
- fromVersion: HOME_IDENTITY_AGGREGATE_FROM_VERSION,
152
- toVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION,
153
- preconditions: requireAggregateV17Snapshot,
154
- transform: migrateAggregateV17ToV18,
155
- declaredEffects: []
156
- })
157
- .registerOfflineMigration({
158
- axis: "aggregate",
159
- fromVersion: MULTI_AGENT_RUNTIME_AGGREGATE_FROM_VERSION,
160
- toVersion: MULTI_AGENT_RUNTIME_AGGREGATE_TO_VERSION,
161
- preconditions: requireAggregateV19Snapshot,
162
- transform: migrateAggregateV19ToV20,
163
- declaredEffects: []
164
- })
165
- .registerOfflineMigration({
166
- axis: "aggregate",
167
- fromVersion: RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION,
168
- toVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION,
169
- preconditions: requireAggregateV18Snapshot,
170
- transform: migrateAggregateV18ToV19,
171
- declaredEffects: []
172
- })
173
- .registerOfflineMigration(projectOwnershipStep())
174
- .registerOfflineMigration(configV2Step())
175
- .registerOfflineMigration(configReviewControlsRemovalStep())
176
- .registerOfflineMigration(configAgentRecoveryStep())
177
- .registerCompatible(projectKnowledgeProposalsStep())
178
- .registerCompatible(projectLifecycleStep())
179
- .registerOfflineMigration(taskWorkspaceIdentityStep())
180
- .registerOfflineMigration(taskIntentStep())
181
- .registerOfflineMigration(taskExecutionGateStep())
182
- .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
183
- .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
184
- .registerOfflineMigration(workItemExecutionGroupHistoryStep())
185
- .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_EXPLORATION_STAGE_FROM_VERSION, WORK_ITEM_EXPLORATION_STAGE_TO_VERSION, "workItems"))
186
- // Candidate convergence is frozen only on newly created exploration
187
- // histories. Existing valid T4 histories remain byte-for-byte evidence,
188
- // so this adjacent persistent transition advances only the family version.
189
- .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_CANDIDATE_CONVERGENCE_FROM_VERSION, WORK_ITEM_CANDIDATE_CONVERGENCE_TO_VERSION, "workItems"))
190
- // Existing v11 histories remain valid immutable evidence without a T6
191
- // resource policy. Newly planned stages always freeze the full policy.
192
- .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_RESOURCE_SCHEDULING_FROM_VERSION, WORK_ITEM_RESOURCE_SCHEDULING_TO_VERSION, "workItems"))
193
- .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_FROM_VERSION, AGENT_RUN_TO_VERSION, "agentRuns"))
194
- .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION, AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION, "agentRuns"))
195
- .registerOfflineMigration(agentRunContextProtocolStep())
196
- .registerOfflineMigration(agentRunRetryEpisodeStep())
197
- .registerOfflineMigration(agentRunErrorFactsStep())
198
- .registerOfflineMigration(messageWakePolicyStep())
199
- .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
200
- .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
201
- .registerOfflineMigration(reviewRoundTaskAnchorStep())
202
- .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_ACTOR_FROM_VERSION, REVIEW_ROUND_ACTOR_TO_VERSION, "reviewRounds"))
203
- .registerOfflineMigration(recordFamilyStep("activeRunPointer", ACTIVE_RUN_POINTER_FROM_VERSION, ACTIVE_RUN_POINTER_TO_VERSION, "activeRuns"))
204
- .registerOfflineMigration(managedWorkspaceFamilyStep())
205
- .registerOfflineMigration(activeRunPointerNamespaceStep())
206
- .registerOfflineMigration(changeSetManifestStep())
207
- .registerOfflineMigration(integrationAttemptSupersededStep())
208
- .registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION, INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION, "integrationAttempts"))
209
- .registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_ACTOR_FROM_VERSION, INTEGRATION_ATTEMPT_ACTOR_TO_VERSION, "integrationAttempts"))
210
- .registerOfflineMigration(recordFamilyStep("milestone", MILESTONE_ACTOR_FROM_VERSION, MILESTONE_ACTOR_TO_VERSION, "milestones"))
211
- .registerOfflineMigration(integrationQueueIntroductionStep())
212
- .registerOfflineMigration(contextSnapshotIntroductionStep())
213
- .registerCompatible(storedTaskDurableJobsStep())
214
- .registerCompatible(storedTaskJobCallerKeyHashesStep())
215
- .registerCompatible(storedTaskWakesAndPublicationReferencesStep())
216
- .registerOfflineMigration(storedTaskEventNoticesStep())
217
- .registerCompatible(taskWakeIntroductionStep())
218
- .registerCompatible(durableJobIntroductionStep())
219
- .registerOfflineMigration(capabilityGrantIntroductionStep())
220
- .registerOfflineMigration(releaseWorkflowIntroductionStep())
221
- .registerOfflineMigration(workMailboxV2Step())
222
- .registerOfflineMigration(hostOwnedWorkMailboxStep())
223
- .registerOfflineMigration(agentRunOwnedTaskRoleStep())
224
- .registerOfflineMigration(taskRoleSessionSetV5Step())
225
- .registerOfflineMigration(structuredProviderSessionSetV6Step())
226
- .registerOfflineMigration(agentFirstSessionSetV7Step())
227
- .registerOfflineMigration(unifiedTaskSessionSetStep())
228
- .registerOfflineMigration(unifiedGlobalSessionSetStep())
229
- .registerOfflineMigration(publicationReferenceIntroductionStep());
230
- assertRegistryCoversBaselineToCurrent(registry);
231
- return registry;
232
- }
233
- /** Removes the independently writable Role runtime projection; AgentRun owns workflow activity. */
234
- function agentRunOwnedTaskRoleStep() {
235
- return {
236
- axis: "record",
237
- recordKind: "taskRole",
238
- fromVersion: TASK_ROLE_FROM_VERSION,
239
- toVersion: TASK_ROLE_TO_VERSION,
240
- preconditions: requireTaskRoleV3Family,
241
- transform: migrateTaskRoleV3ToV4,
242
- declaredEffects: []
243
- };
244
- }
245
- function requireTaskRoleV3Family(snapshot) {
246
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
247
- if (versions.taskRole !== TASK_ROLE_FROM_VERSION) {
248
- throw new Error(`Record taskRole migration requires manifest version ${TASK_ROLE_FROM_VERSION}.`);
249
- }
250
- if (snapshot.state === null)
251
- return;
252
- const tasks = asObject(snapshot.state.tasks, "state tasks");
253
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
254
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
255
- const roles = asObject(aggregate.roles, `Task Roles ${taskId}`);
256
- for (const [roleName, rawRole] of Object.entries(roles)) {
257
- const role = asObject(rawRole, `Task Role ${taskId}/${roleName}`);
258
- if (role.schemaVersion !== TASK_ROLE_FROM_VERSION) {
259
- throw new Error(`Task Role ${taskId}/${roleName} must use schemaVersion ${TASK_ROLE_FROM_VERSION}.`);
260
- }
261
- }
262
- }
263
- }
264
- function migrateTaskRoleV3ToV4(snapshot) {
265
- requireTaskRoleV3Family(snapshot);
266
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
267
- const schemaManifest = {
268
- ...snapshot.schemaManifest,
269
- recordVersions: { ...versions, taskRole: TASK_ROLE_TO_VERSION }
270
- };
271
- if (snapshot.state === null)
272
- return { schemaManifest, state: null };
273
- const tasks = asObject(snapshot.state.tasks, "state tasks");
274
- const nextTasks = Object.fromEntries(Object.entries(tasks).map(([taskId, rawAggregate]) => {
275
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
276
- const roles = asObject(aggregate.roles, `Task Roles ${taskId}`);
277
- const nextRoles = Object.fromEntries(Object.entries(roles).map(([roleName, rawRole]) => {
278
- const role = asObject(rawRole, `Task Role ${taskId}/${roleName}`);
279
- const { status: _status, ...retained } = role;
280
- void _status;
281
- return [roleName, { ...retained, schemaVersion: TASK_ROLE_TO_VERSION }];
282
- }));
283
- return [taskId, { ...aggregate, roles: nextRoles }];
284
- }));
285
- return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
286
- }
287
- function configV2Step() {
288
- return {
289
- axis: "record",
290
- recordKind: "config",
291
- fromVersion: CONFIG_FROM_VERSION,
292
- toVersion: CONFIG_TO_VERSION,
293
- preconditions: requireConfigV1,
294
- transform: migrateConfigV1ToV2,
295
- declaredEffects: []
296
- };
297
- }
298
- function requireConfigV1(snapshot) {
299
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
300
- if (versions.config !== CONFIG_FROM_VERSION) {
301
- throw new Error(`Record config migration requires manifest version ${CONFIG_FROM_VERSION}.`);
302
- }
303
- if (snapshot.state === null)
304
- return;
305
- const config = asObject(snapshot.state.config, "Yui config");
306
- if (config.schemaVersion !== CONFIG_FROM_VERSION) {
307
- throw new Error(`Yui config must use schemaVersion ${CONFIG_FROM_VERSION} before migration.`);
308
- }
309
- }
310
- function migrateConfigV1ToV2(snapshot) {
311
- requireConfigV1(snapshot);
312
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
313
- const schemaManifest = {
314
- ...snapshot.schemaManifest,
315
- recordVersions: { ...versions, config: CONFIG_TO_VERSION }
316
- };
317
- if (snapshot.state === null)
318
- return { schemaManifest, state: null };
319
- const config = asObject(snapshot.state.config, "Yui config");
320
- const { providerRetryMaxWindowMs, yieldReceiptReplay: _yieldReceiptReplay, gitBin: _gitBin, telemetryMode, schemaVersion: _schemaVersion, ...retained } = config;
321
- const providerRetryMaxWindowSeconds = typeof providerRetryMaxWindowMs === "number"
322
- ? Math.ceil(providerRetryMaxWindowMs / 1_000)
323
- : undefined;
324
- const telemetryEnabled = telemetryMode === undefined
325
- ? undefined
326
- : telemetryMode === "dual" || telemetryMode === "bounded";
327
- return {
328
- schemaManifest,
329
- state: {
330
- ...snapshot.state,
331
- config: {
332
- ...retained,
333
- schemaVersion: CONFIG_TO_VERSION,
334
- ...(providerRetryMaxWindowSeconds === undefined
335
- ? {}
336
- : { providerRetryMaxWindowSeconds }),
337
- ...(telemetryEnabled === undefined ? {} : { telemetryEnabled })
338
- }
339
- }
340
- };
341
- }
342
- /** Removes retired Core controls for the Leader-owned Delta decision. */
343
- function configReviewControlsRemovalStep() {
344
- return {
345
- axis: "record",
346
- recordKind: "config",
347
- fromVersion: CONFIG_REVIEW_CONTROLS_FROM_VERSION,
348
- toVersion: CONFIG_REVIEW_CONTROLS_TO_VERSION,
349
- preconditions: requireConfigV2,
350
- transform: migrateConfigV2ToV3,
351
- declaredEffects: []
352
- };
353
- }
354
- function requireConfigV2(snapshot) {
355
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
356
- if (versions.config !== CONFIG_REVIEW_CONTROLS_FROM_VERSION) {
357
- throw new Error(`Record config migration requires manifest version ${CONFIG_REVIEW_CONTROLS_FROM_VERSION}.`);
358
- }
359
- if (snapshot.state === null)
360
- return;
361
- const config = asObject(snapshot.state.config, "Yui config");
362
- if (config.schemaVersion !== CONFIG_REVIEW_CONTROLS_FROM_VERSION) {
363
- throw new Error(`Yui config must use schemaVersion ${CONFIG_REVIEW_CONTROLS_FROM_VERSION} before migration.`);
364
- }
365
- }
366
- function migrateConfigV2ToV3(snapshot) {
367
- requireConfigV2(snapshot);
368
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
369
- const schemaManifest = {
370
- ...snapshot.schemaManifest,
371
- recordVersions: { ...versions, config: CONFIG_REVIEW_CONTROLS_TO_VERSION }
372
- };
373
- if (snapshot.state === null)
374
- return { schemaManifest, state: null };
375
- const config = asObject(snapshot.state.config, "Yui config");
376
- const review = config.review === undefined
377
- ? undefined
378
- : asObject(config.review, "Yui review config");
379
- const migratedReview = review === undefined
380
- ? undefined
381
- : (() => {
382
- const { deltaRecheck: _retiredSwitch, deltaRecheckMaxChangedLines: _retiredLineThreshold, deltaRecheckMaxChangedFiles: _retiredFileThreshold, ...retained } = review;
383
- return retained;
384
- })();
385
- return {
386
- schemaManifest,
387
- state: {
388
- ...snapshot.state,
389
- config: {
390
- ...config,
391
- schemaVersion: CONFIG_REVIEW_CONTROLS_TO_VERSION,
392
- ...(migratedReview === undefined ? {} : { review: migratedReview })
393
- }
394
- }
395
- };
396
- }
397
- /** Removes Core-owned Provider retry policy; recovery is chosen from AgentError facts. */
398
- function configAgentRecoveryStep() {
399
- return {
400
- axis: "record",
401
- recordKind: "config",
402
- fromVersion: CONFIG_AGENT_RECOVERY_FROM_VERSION,
403
- toVersion: CONFIG_AGENT_RECOVERY_TO_VERSION,
404
- preconditions: requireConfigV3,
405
- transform: migrateConfigV3ToV4,
406
- declaredEffects: []
407
- };
408
- }
409
- function requireConfigV3(snapshot) {
410
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
411
- if (versions.config !== CONFIG_AGENT_RECOVERY_FROM_VERSION) {
412
- throw new Error(`Record config migration requires manifest version ${CONFIG_AGENT_RECOVERY_FROM_VERSION}.`);
413
- }
414
- if (snapshot.state === null)
415
- return;
416
- const config = asObject(snapshot.state.config, "Yui config");
417
- if (config.schemaVersion !== CONFIG_AGENT_RECOVERY_FROM_VERSION) {
418
- throw new Error(`Yui config must use schemaVersion ${CONFIG_AGENT_RECOVERY_FROM_VERSION} before migration.`);
419
- }
420
- }
421
- function migrateConfigV3ToV4(snapshot) {
422
- requireConfigV3(snapshot);
423
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
424
- const schemaManifest = {
425
- ...snapshot.schemaManifest,
426
- recordVersions: { ...versions, config: CONFIG_AGENT_RECOVERY_TO_VERSION }
427
- };
428
- if (snapshot.state === null)
429
- return { schemaManifest, state: null };
430
- const config = asObject(snapshot.state.config, "Yui config");
431
- const { providerRetryMode: _mode, providerRetryAdapters: _adapters, providerRetryDelaysSeconds: _delays, providerRetryMaxWindowSeconds: _window, ...retained } = config;
432
- void _mode;
433
- void _adapters;
434
- void _delays;
435
- void _window;
436
- return {
437
- schemaManifest,
438
- state: {
439
- ...snapshot.state,
440
- config: { ...retained, schemaVersion: CONFIG_AGENT_RECOVERY_TO_VERSION }
441
- }
442
- };
443
- }
444
- function workItemExecutionGroupHistoryStep() {
445
- return {
446
- axis: "record",
447
- recordKind: "workItem",
448
- fromVersion: WORK_ITEM_GROUP_HISTORY_FROM_VERSION,
449
- toVersion: WORK_ITEM_GROUP_HISTORY_TO_VERSION,
450
- preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "workItem", WORK_ITEM_GROUP_HISTORY_FROM_VERSION, "workItems"),
451
- transform: migrateWorkItemExecutionGroupHistory,
452
- declaredEffects: []
453
- };
454
- }
455
- /**
456
- * Task Messages gain an optional `wakePolicy` field (Issue 05). The field is
457
- * absent on every older record, so this adjacent step only advances the
458
- * family version; the strict current parser treats the omitted field as
459
- * "no explicit policy" and preserves the existing routing.
460
- */
461
- function messageWakePolicyStep() {
462
- return {
463
- axis: "record",
464
- recordKind: "message",
465
- fromVersion: MESSAGE_WAKE_POLICY_FROM_VERSION,
466
- toVersion: MESSAGE_WAKE_POLICY_TO_VERSION,
467
- preconditions: (snapshot) => requireMessageFamilyVersion(snapshot, MESSAGE_WAKE_POLICY_FROM_VERSION),
468
- transform: migrateMessageWakePolicy,
469
- declaredEffects: []
470
- };
471
- }
472
- function requireMessageFamilyVersion(snapshot, version) {
473
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
474
- if (manifestVersions.message !== version) {
475
- throw new Error(`Message migration requires manifest version ${version}.`);
476
- }
477
- if (snapshot.state === null)
478
- return;
479
- const tasks = asObject(snapshot.state.tasks, "state tasks");
480
- for (const [taskId, rawTask] of Object.entries(tasks)) {
481
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
482
- const messages = task.messages;
483
- if (messages === undefined)
484
- continue;
485
- const map = asObject(messages, `message map ${taskId}`);
486
- for (const [messageId, rawMessage] of Object.entries(map)) {
487
- const message = asObject(rawMessage, `message ${taskId}/${messageId}`);
488
- if (message.schemaVersion !== version) {
489
- throw new Error(`Message ${taskId}/${messageId} must use schemaVersion ${version}.`);
490
- }
491
- }
492
- }
493
- }
494
- function migrateMessageWakePolicy(snapshot) {
495
- requireMessageFamilyVersion(snapshot, MESSAGE_WAKE_POLICY_FROM_VERSION);
496
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
497
- const schemaManifest = {
498
- ...snapshot.schemaManifest,
499
- recordVersions: {
500
- ...manifestVersions,
501
- message: MESSAGE_WAKE_POLICY_TO_VERSION
502
- }
503
- };
504
- if (snapshot.state === null)
505
- return { schemaManifest, state: null };
506
- const tasks = asObject(snapshot.state.tasks, "state tasks");
507
- const nextTasks = {};
508
- for (const [taskId, rawTask] of Object.entries(tasks)) {
509
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
510
- if (task.messages === undefined) {
511
- nextTasks[taskId] = { ...task };
512
- continue;
513
- }
514
- const messages = asObject(task.messages, `message map ${taskId}`);
515
- const nextMessages = {};
516
- for (const [messageId, rawMessage] of Object.entries(messages)) {
517
- nextMessages[messageId] = {
518
- ...asObject(rawMessage, `message ${taskId}/${messageId}`),
519
- schemaVersion: MESSAGE_WAKE_POLICY_TO_VERSION
520
- };
521
- }
522
- nextTasks[taskId] = { ...task, messages: nextMessages };
523
- }
524
- return {
525
- schemaManifest,
526
- state: { ...snapshot.state, tasks: nextTasks }
527
- };
528
- }
529
- /**
530
- * ManagedWorkspace is stored once in the owner-keyed workspace map and copied
531
- * into Candidates, AgentRuns, and ReviewRounds as immutable lifecycle
532
- * evidence. Those copies are the same record family, not independent parent
533
- * records: advancing only the map leaves an otherwise legal historical Home
534
- * unreadable by the strict current parser.
535
- */
536
- function managedWorkspaceFamilyStep() {
537
- return {
538
- axis: "record",
539
- recordKind: "managedWorkspace",
540
- fromVersion: MANAGED_WORKSPACE_FROM_VERSION,
541
- toVersion: MANAGED_WORKSPACE_TO_VERSION,
542
- preconditions: requireLegacyManagedWorkspaceFamily,
543
- transform: migrateManagedWorkspaceFamily,
544
- declaredEffects: []
545
- };
546
- }
547
- function requireLegacyManagedWorkspaceFamily(snapshot) {
548
- requireRecordFamilyVersion(snapshot, "managedWorkspace", MANAGED_WORKSPACE_FROM_VERSION, "managedWorkspaces");
549
- visitEmbeddedManagedWorkspaces(snapshot, (workspace, label) => {
550
- requireManagedWorkspaceVersion(workspace, MANAGED_WORKSPACE_FROM_VERSION, label);
551
- return workspace;
552
- });
553
- }
554
- function migrateManagedWorkspaceFamily(snapshot) {
555
- requireLegacyManagedWorkspaceFamily(snapshot);
556
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
557
- const schemaManifest = {
558
- ...snapshot.schemaManifest,
559
- recordVersions: {
560
- ...manifestVersions,
561
- managedWorkspace: MANAGED_WORKSPACE_TO_VERSION
562
- }
563
- };
564
- if (snapshot.state === null)
565
- return { schemaManifest, state: null };
566
- const migrated = visitEmbeddedManagedWorkspaces(snapshot, (workspace) => ({ ...workspace, schemaVersion: MANAGED_WORKSPACE_TO_VERSION }));
567
- return { schemaManifest, state: migrated.state };
568
- }
569
- /**
570
- * Rewrite every persisted ManagedWorkspace occurrence while preserving the
571
- * surrounding record bytes. The direct map is handled by the ordinary
572
- * record-family migration; this traversal covers the immutable embedded
573
- * snapshots which the strict validators also treat as ManagedWorkspace.
574
- */
575
- function visitEmbeddedManagedWorkspaces(snapshot, visit) {
576
- if (snapshot.state === null)
577
- return snapshot;
578
- const tasks = asObject(snapshot.state.tasks, "state tasks");
579
- const nextTasks = {};
580
- for (const [taskId, rawTask] of Object.entries(tasks)) {
581
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
582
- const nextTask = { ...task };
583
- if (task.managedWorkspaces !== undefined) {
584
- const workspaces = asObject(task.managedWorkspaces, `managedWorkspace map ${taskId}`);
585
- nextTask.managedWorkspaces = Object.fromEntries(Object.entries(workspaces).map(([recordId, rawWorkspace]) => [
586
- recordId,
587
- visit(asObject(rawWorkspace, `managedWorkspace ${taskId}/${recordId}`), `managedWorkspace ${taskId}/${recordId}`)
588
- ]));
589
- }
590
- if (task.workItems !== undefined) {
591
- const workItems = asObject(task.workItems, `workItem map ${taskId}`);
592
- nextTask.workItems = Object.fromEntries(Object.entries(workItems).map(([workItemId, rawWorkItem]) => {
593
- const workItem = asObject(rawWorkItem, `workItem ${taskId}/${workItemId}`);
594
- if (workItem.candidates === undefined)
595
- return [workItemId, { ...workItem }];
596
- if (!Array.isArray(workItem.candidates)) {
597
- throw new Error(`workItem ${taskId}/${workItemId} candidates must be an array.`);
598
- }
599
- return [workItemId, {
600
- ...workItem,
601
- candidates: workItem.candidates.map((rawCandidate, index) => {
602
- const candidate = asObject(rawCandidate, `Candidate ${taskId}/${workItemId}/${index}`);
603
- return migrateOptionalManagedWorkspace(candidate, `Candidate ${taskId}/${workItemId}/${index}`, visit);
604
- })
605
- }];
606
- }));
607
- }
608
- for (const mapKey of ["agentRuns", "reviewRounds"]) {
609
- if (task[mapKey] === undefined)
610
- continue;
611
- const records = asObject(task[mapKey], `${mapKey} map ${taskId}`);
612
- nextTask[mapKey] = Object.fromEntries(Object.entries(records).map(([recordId, rawRecord]) => [
613
- recordId,
614
- migrateOptionalManagedWorkspace(asObject(rawRecord, `${mapKey} ${taskId}/${recordId}`), `${mapKey} ${taskId}/${recordId}`, visit)
615
- ]));
616
- }
617
- nextTasks[taskId] = nextTask;
618
- }
619
- return {
620
- schemaManifest: snapshot.schemaManifest,
621
- state: { ...snapshot.state, tasks: nextTasks }
622
- };
623
- }
624
- function migrateOptionalManagedWorkspace(record, label, visit) {
625
- if (record.workspace === undefined)
626
- return { ...record };
627
- return {
628
- ...record,
629
- workspace: visit(asObject(record.workspace, `${label} workspace`), `${label} workspace`)
630
- };
631
- }
632
- function requireManagedWorkspaceVersion(workspace, version, label) {
633
- if (workspace.schemaVersion !== version) {
634
- throw new Error(`${label} must use schemaVersion ${version}.`);
635
- }
636
- }
637
- function migrateWorkItemExecutionGroupHistory(snapshot) {
638
- requireRecordFamilyVersion(snapshot, "workItem", WORK_ITEM_GROUP_HISTORY_FROM_VERSION, "workItems");
639
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
640
- const schemaManifest = {
641
- ...snapshot.schemaManifest,
642
- recordVersions: { ...manifestVersions, workItem: WORK_ITEM_GROUP_HISTORY_TO_VERSION }
643
- };
644
- if (snapshot.state === null)
645
- return { schemaManifest, state: null };
646
- const tasks = asObject(snapshot.state.tasks, "state tasks");
647
- const nextTasks = {};
648
- for (const [taskId, rawTask] of Object.entries(tasks)) {
649
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
650
- if (task.workItems === undefined) {
651
- nextTasks[taskId] = { ...task };
652
- continue;
653
- }
654
- const workItems = asObject(task.workItems, `workItem map ${taskId}`);
655
- const nextWorkItems = {};
656
- for (const [recordId, rawRecord] of Object.entries(workItems)) {
657
- const record = asObject(rawRecord, `workItem ${taskId}/${recordId}`);
658
- const { executionGroup, ...rest } = record;
659
- if (executionGroup !== undefined
660
- && (executionGroup === null || typeof executionGroup !== "object" || Array.isArray(executionGroup))) {
661
- throw new Error(`workItem ${taskId}/${recordId} executionGroup must be an object.`);
662
- }
663
- const groupId = executionGroup === undefined
664
- ? undefined
665
- : asObject(executionGroup, `ExecutionGroup ${taskId}/${recordId}`).id;
666
- if (groupId !== undefined && (typeof groupId !== "string" || groupId.trim().length === 0)) {
667
- throw new Error(`workItem ${taskId}/${recordId} executionGroup id is invalid.`);
668
- }
669
- nextWorkItems[recordId] = {
670
- ...rest,
671
- schemaVersion: WORK_ITEM_GROUP_HISTORY_TO_VERSION,
672
- executionGroups: executionGroup === undefined ? [] : [executionGroup],
673
- ...(groupId === undefined ? {} : { currentExecutionGroupId: groupId })
674
- };
675
- }
676
- nextTasks[taskId] = { ...task, workItems: nextWorkItems };
677
- }
678
- return {
679
- schemaManifest,
680
- state: { ...snapshot.state, tasks: nextTasks }
681
- };
682
- }
683
- /**
684
- * Move the legacy `lane:<group>:<lane>` keys into a namespace that cannot be
685
- * mistaken for a legal Role identity. A legacy key is only rewritten when
686
- * its pointed Run proves the lane lineage; otherwise a legal Role with that
687
- * exact name is retained. Ambiguous or malformed lane-looking records fail
688
- * closed instead of guessing which active Run the old bytes meant.
689
- */
690
- function activeRunPointerNamespaceStep() {
691
- return {
692
- axis: "record",
693
- recordKind: "activeRunPointer",
694
- fromVersion: ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION,
695
- toVersion: ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION,
696
- preconditions: (snapshot) => requireActiveRunPointerNamespaceVersion(snapshot),
697
- transform: (snapshot) => migrateActiveRunPointerNamespace(snapshot),
698
- declaredEffects: []
699
- };
700
- }
701
- function requireActiveRunPointerNamespaceVersion(snapshot) {
702
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
703
- if (manifestVersions.activeRunPointer !== ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION) {
704
- throw new Error(`Record activeRunPointer migration requires manifest version ${ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION}.`);
705
- }
706
- if (snapshot.state === null)
707
- return;
708
- const tasks = asObject(snapshot.state.tasks, "state tasks");
709
- for (const [taskId, rawTask] of Object.entries(tasks)) {
710
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
711
- const activeRuns = task.activeRuns;
712
- if (activeRuns === undefined)
713
- continue;
714
- const pointers = asObject(activeRuns, `activeRunPointer map ${taskId}`);
715
- for (const [key, rawPointer] of Object.entries(pointers)) {
716
- const pointer = asObject(rawPointer, `Active run ${taskId}/${key}`);
717
- if (pointer.schemaVersion !== ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION) {
718
- throw new Error(`Active run ${taskId}/${key} must use schemaVersion ${ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION}.`);
719
- }
720
- if (typeof pointer.runId !== "string" || pointer.runId.trim().length === 0) {
721
- throw new Error(`Active run ${taskId}/${key} has an invalid runId.`);
722
- }
723
- }
724
- }
725
- }
726
- function migrateActiveRunPointerNamespace(snapshot) {
727
- requireActiveRunPointerNamespaceVersion(snapshot);
728
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
729
- const schemaManifest = {
730
- ...snapshot.schemaManifest,
731
- recordVersions: {
732
- ...manifestVersions,
733
- activeRunPointer: ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION
734
- }
735
- };
736
- if (snapshot.state === null)
737
- return { schemaManifest, state: null };
738
- const tasks = asObject(snapshot.state.tasks, "state tasks");
739
- const nextTasks = {};
740
- for (const [taskId, rawTask] of Object.entries(tasks)) {
741
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
742
- const rawActiveRuns = task.activeRuns;
743
- if (rawActiveRuns === undefined) {
744
- nextTasks[taskId] = { ...task };
745
- continue;
746
- }
747
- const activeRuns = asObject(rawActiveRuns, `activeRunPointer map ${taskId}`);
748
- const rawAgentRuns = asObject(task.agentRuns, `agentRun map ${taskId}`);
749
- const nextActiveRuns = {};
750
- for (const [key, rawPointer] of Object.entries(activeRuns)) {
751
- const pointer = asObject(rawPointer, `Active run ${taskId}/${key}`);
752
- const runId = typeof pointer.runId === "string" ? pointer.runId : "";
753
- const run = asObject(rawAgentRuns[runId], `Agent run ${taskId}/${runId}`);
754
- const lane = legacyLaneKeyParts(key);
755
- const migratedPointer = {
756
- ...pointer,
757
- schemaVersion: ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION
758
- };
759
- const addPointer = (nextKey) => {
760
- if (nextActiveRuns[nextKey] !== undefined) {
761
- throw new Error(`Active run pointer key collides after migration: ${taskId}/${nextKey}.`);
762
- }
763
- nextActiveRuns[nextKey] = migratedPointer;
764
- };
765
- if (lane !== null) {
766
- const laneBacked = run.executionGroupId === lane.executionGroupId
767
- && run.executionLaneId === lane.executionLaneId;
768
- // The prior writer used the same map key for a legal Role and a Lane
769
- // when the Role itself happened to contain the `lane:g:l` shape. The
770
- // v3 namespace must retain both identities instead of rejecting the
771
- // record or silently dropping the Role pointer.
772
- const roleBacked = run.roleName === key;
773
- if (!laneBacked && !roleBacked) {
774
- throw new Error(`Active run pointer lineage is invalid: ${taskId}/${key}.`);
775
- }
776
- if (roleBacked)
777
- addPointer(key);
778
- if (laneBacked) {
779
- addPointer(executionLaneNamespaceKey(lane.executionGroupId, lane.executionLaneId));
780
- }
781
- }
782
- else if (key.startsWith("lane:")) {
783
- // A malformed lane-looking key is allowed only when it is a legal
784
- // legacy Role pointer. It must not be silently reinterpreted.
785
- if (run.roleName !== key) {
786
- throw new Error(`Active run pointer key is malformed: ${taskId}/${key}.`);
787
- }
788
- addPointer(key);
789
- }
790
- else {
791
- // Role pointers remain keyed by the Role even when the pointed Run
792
- // also carries execution lineage (a shape emitted by the prior
793
- // writer). The lane namespace above preserves that second identity.
794
- if (run.roleName !== key) {
795
- throw new Error(`Active run Role pointer is invalid: ${taskId}/${key}.`);
796
- }
797
- addPointer(key);
798
- }
799
- }
800
- nextTasks[taskId] = { ...task, activeRuns: nextActiveRuns };
801
- }
802
- return {
803
- schemaManifest,
804
- state: { ...snapshot.state, tasks: nextTasks }
805
- };
806
- }
807
- /**
808
- * ChangeSet v3 adds the optional integration manifest (tags, deleted paths,
809
- * target ref, evidence references). The manifest is optional, so the
810
- * transition only rewrites the record version; legacy records without a
811
- * manifest remain valid and keep integrating.
812
- */
813
- function changeSetManifestStep() {
814
- return {
815
- axis: "record",
816
- recordKind: "changeSet",
817
- fromVersion: CHANGE_SET_MANIFEST_FROM_VERSION,
818
- toVersion: CHANGE_SET_MANIFEST_TO_VERSION,
819
- preconditions: requireChangeSetManifestVersion,
820
- transform: migrateChangeSetManifest,
821
- declaredEffects: []
822
- };
823
- }
824
- function requireChangeSetManifestVersion(snapshot) {
825
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
826
- if (manifestVersions.changeSet !== CHANGE_SET_MANIFEST_FROM_VERSION) {
827
- throw new Error(`Record changeSet migration requires manifest version ${CHANGE_SET_MANIFEST_FROM_VERSION}.`);
828
- }
829
- if (snapshot.state === null)
830
- return;
831
- const tasks = asObject(snapshot.state.tasks, "state tasks");
832
- for (const [taskId, rawTask] of Object.entries(tasks)) {
833
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
834
- if (task.changeSets === undefined)
835
- continue;
836
- const changeSets = asObject(task.changeSets, `changeSet map ${taskId}`);
837
- for (const [changeSetId, rawRecord] of Object.entries(changeSets)) {
838
- const record = asObject(rawRecord, `changeSet ${taskId}/${changeSetId}`);
839
- if (record.schemaVersion !== CHANGE_SET_MANIFEST_FROM_VERSION) {
840
- throw new Error(`changeSet ${taskId}/${changeSetId} must use schemaVersion ${CHANGE_SET_MANIFEST_FROM_VERSION}.`);
841
- }
842
- }
843
- }
844
- }
845
- function migrateChangeSetManifest(snapshot) {
846
- requireChangeSetManifestVersion(snapshot);
847
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
848
- const schemaManifest = {
849
- ...snapshot.schemaManifest,
850
- recordVersions: {
851
- ...manifestVersions,
852
- changeSet: CHANGE_SET_MANIFEST_TO_VERSION
853
- }
854
- };
855
- if (snapshot.state === null)
856
- return { schemaManifest, state: null };
857
- const tasks = asObject(snapshot.state.tasks, "state tasks");
858
- const nextTasks = {};
859
- for (const [taskId, rawTask] of Object.entries(tasks)) {
860
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
861
- if (task.changeSets === undefined) {
862
- nextTasks[taskId] = { ...task };
863
- continue;
864
- }
865
- const changeSets = asObject(task.changeSets, `changeSet map ${taskId}`);
866
- const nextChangeSets = {};
867
- for (const [changeSetId, rawRecord] of Object.entries(changeSets)) {
868
- const record = asObject(rawRecord, `changeSet ${taskId}/${changeSetId}`);
869
- nextChangeSets[changeSetId] = {
870
- ...record,
871
- schemaVersion: CHANGE_SET_MANIFEST_TO_VERSION
872
- };
873
- }
874
- nextTasks[taskId] = { ...task, changeSets: nextChangeSets };
875
- }
876
- return {
877
- schemaManifest,
878
- state: { ...snapshot.state, tasks: nextTasks }
879
- };
880
- }
881
- /**
882
- * IntegrationAttempt v2->v3 adds the "superseded" terminal status for
883
- * committed Integrations that are obsolete. The migration preserves all
884
- * existing fields and advances the schema version; the new status is
885
- * opt-in, so old records remain valid until explicitly superseded.
886
- */
887
- function integrationAttemptSupersededStep() {
888
- return {
889
- axis: "record",
890
- recordKind: "integrationAttempt",
891
- fromVersion: INTEGRATION_ATTEMPT_FROM_VERSION,
892
- toVersion: INTEGRATION_ATTEMPT_TO_VERSION,
893
- preconditions: requireIntegrationAttemptV2,
894
- transform: migrateIntegrationAttemptV2ToV3,
895
- declaredEffects: []
896
- };
897
- }
898
- function requireIntegrationAttemptV2(snapshot) {
899
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
900
- if (manifestVersions.integrationAttempt !== INTEGRATION_ATTEMPT_FROM_VERSION) {
901
- throw new Error(`Record integrationAttempt migration requires manifest version ${INTEGRATION_ATTEMPT_FROM_VERSION}.`);
902
- }
903
- if (snapshot.state === null)
904
- return;
905
- const tasks = asObject(snapshot.state.tasks, "state tasks");
906
- for (const [taskId, rawTask] of Object.entries(tasks)) {
907
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
908
- if (task.integrationAttempts === undefined)
909
- continue;
910
- const attempts = asObject(task.integrationAttempts, `integrationAttempt map ${taskId}`);
911
- for (const [attemptId, rawRecord] of Object.entries(attempts)) {
912
- const record = asObject(rawRecord, `integrationAttempt ${taskId}/${attemptId}`);
913
- if (record.schemaVersion !== INTEGRATION_ATTEMPT_FROM_VERSION) {
914
- throw new Error(`integrationAttempt ${taskId}/${attemptId} must use schemaVersion ${INTEGRATION_ATTEMPT_FROM_VERSION}.`);
915
- }
916
- }
917
- }
918
- }
919
- function migrateIntegrationAttemptV2ToV3(snapshot) {
920
- requireIntegrationAttemptV2(snapshot);
921
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
922
- const schemaManifest = {
923
- ...snapshot.schemaManifest,
924
- recordVersions: {
925
- ...manifestVersions,
926
- integrationAttempt: INTEGRATION_ATTEMPT_TO_VERSION
927
- }
928
- };
929
- if (snapshot.state === null)
930
- return { schemaManifest, state: null };
931
- const tasks = asObject(snapshot.state.tasks, "state tasks");
932
- const nextTasks = {};
933
- for (const [taskId, rawTask] of Object.entries(tasks)) {
934
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
935
- if (task.integrationAttempts === undefined) {
936
- nextTasks[taskId] = { ...task };
937
- continue;
938
- }
939
- const attempts = asObject(task.integrationAttempts, `integrationAttempt map ${taskId}`);
940
- const nextAttempts = {};
941
- for (const [attemptId, rawRecord] of Object.entries(attempts)) {
942
- const record = asObject(rawRecord, `integrationAttempt ${taskId}/${attemptId}`);
943
- nextAttempts[attemptId] = {
944
- ...record,
945
- schemaVersion: INTEGRATION_ATTEMPT_TO_VERSION
946
- };
947
- }
948
- nextTasks[taskId] = { ...task, integrationAttempts: nextAttempts };
949
- }
950
- return {
951
- schemaManifest,
952
- state: { ...snapshot.state, tasks: nextTasks }
953
- };
954
- }
955
- /**
956
- * The integration queue is a post-baseline per-Task record family. Its
957
- * explicit 0->1 introduction adds the empty map and its id high-water mark to
958
- * every Task aggregate and the family to the record manifest; old Homes keep
959
- * integrating without it until the queue is first used.
960
- */
961
- function integrationQueueIntroductionStep() {
962
- return {
963
- axis: "record",
964
- recordKind: "integrationQueue",
965
- fromVersion: INTEGRATION_QUEUE_FROM_VERSION,
966
- toVersion: INTEGRATION_QUEUE_TO_VERSION,
967
- introduction: true,
968
- preconditions: requireIntegrationQueueIntroduction,
969
- transform: introduceIntegrationQueue,
970
- declaredEffects: []
971
- };
972
- }
973
- function requireIntegrationQueueIntroduction(snapshot) {
974
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
975
- if (manifestVersions.integrationQueue !== undefined) {
976
- throw new Error("integrationQueue is already introduced in the record manifest.");
977
- }
978
- if (snapshot.state === null)
979
- return;
980
- const tasks = asObject(snapshot.state.tasks, "state tasks");
981
- for (const [taskId, rawTask] of Object.entries(tasks)) {
982
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
983
- if (task.integrationQueue !== undefined) {
984
- throw new Error(`Task aggregate already carries an integrationQueue map: ${taskId}.`);
985
- }
986
- }
987
- }
988
- function introduceIntegrationQueue(snapshot) {
989
- requireIntegrationQueueIntroduction(snapshot);
990
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
991
- const schemaManifest = {
992
- ...snapshot.schemaManifest,
993
- recordVersions: {
994
- ...manifestVersions,
995
- integrationQueue: INTEGRATION_QUEUE_TO_VERSION
996
- }
997
- };
998
- if (snapshot.state === null)
999
- return { schemaManifest, state: null };
1000
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1001
- const nextTasks = {};
1002
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1003
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1004
- const highWaterMarks = asObject(task.idHighWaterMarks ?? {}, `Task id high-water marks ${taskId}`);
1005
- nextTasks[taskId] = {
1006
- ...task,
1007
- idHighWaterMarks: { ...highWaterMarks, integrationQueue: 0 },
1008
- integrationQueue: {}
1009
- };
1010
- }
1011
- return {
1012
- schemaManifest,
1013
- state: { ...snapshot.state, tasks: nextTasks }
1014
- };
1015
- }
1016
- /** Introduce the immutable Task-scoped ContextSnapshot record family. */
1017
- function contextSnapshotIntroductionStep() {
1018
- return {
1019
- axis: "record",
1020
- recordKind: "contextSnapshot",
1021
- fromVersion: CONTEXT_SNAPSHOT_FROM_VERSION,
1022
- toVersion: CONTEXT_SNAPSHOT_TO_VERSION,
1023
- introduction: true,
1024
- preconditions: requireContextSnapshotIntroduction,
1025
- transform: introduceContextSnapshots,
1026
- declaredEffects: []
1027
- };
1028
- }
1029
- function requireContextSnapshotIntroduction(snapshot) {
1030
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1031
- const namedVersion = manifestVersions.contextSnapshot;
1032
- if (namedVersion !== undefined && namedVersion !== CONTEXT_SNAPSHOT_FROM_VERSION) {
1033
- throw new Error("Record contextSnapshot introduction requires an absent manifest version or 0.");
1034
- }
1035
- if (snapshot.state === null)
1036
- return;
1037
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1038
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1039
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1040
- if (task.contextSnapshots !== undefined) {
1041
- const records = asObject(task.contextSnapshots, `contextSnapshot map ${taskId}`);
1042
- if (Object.keys(records).length > 0) {
1043
- throw new Error(`Context Snapshot introduction found existing records: ${taskId}.`);
1044
- }
1045
- }
1046
- const marks = asObject(task.idHighWaterMarks ?? {}, `Task id high-water marks ${taskId}`);
1047
- const mark = marks.contextSnapshot;
1048
- if (mark !== undefined && mark !== CONTEXT_SNAPSHOT_FROM_VERSION) {
1049
- throw new Error(`Context Snapshot introduction found a high-water mark: ${taskId}.`);
1050
- }
1051
- }
1052
- }
1053
- function introduceContextSnapshots(snapshot) {
1054
- requireContextSnapshotIntroduction(snapshot);
1055
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1056
- const schemaManifest = {
1057
- ...snapshot.schemaManifest,
1058
- recordVersions: {
1059
- ...manifestVersions,
1060
- contextSnapshot: CONTEXT_SNAPSHOT_TO_VERSION
1061
- }
1062
- };
1063
- if (snapshot.state === null)
1064
- return { schemaManifest, state: null };
1065
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1066
- const nextTasks = {};
1067
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1068
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1069
- const marks = asObject(task.idHighWaterMarks ?? {}, `Task id high-water marks ${taskId}`);
1070
- nextTasks[taskId] = {
1071
- ...task,
1072
- idHighWaterMarks: { ...marks, contextSnapshot: 0 },
1073
- contextSnapshots: {}
1074
- };
1075
- }
1076
- return {
1077
- schemaManifest,
1078
- state: { ...snapshot.state, tasks: nextTasks }
1079
- };
1080
- }
1081
- function legacyLaneKeyParts(key) {
1082
- const match = /^lane:([^:]+):([^:]+)$/u.exec(key);
1083
- if (match === null)
1084
- return null;
1085
- return { executionGroupId: match[1], executionLaneId: match[2] };
1086
- }
1087
- function executionLaneNamespaceKey(executionGroupId, executionLaneId) {
1088
- return `/execution-lane/${encodeLaneKeyPart(executionGroupId)}:${encodeLaneKeyPart(executionLaneId)}`;
1089
- }
1090
- function encodeLaneKeyPart(value) {
1091
- return encodeURIComponent(value).replace(/:/gu, "%3A");
1092
- }
1093
- /**
1094
- * The StoredTask aggregate gains the `durableJobs` record family. Every v14
1095
- * task defaults to an empty map; the compatible normalizer adds it in place
1096
- * without rewriting any other field. The strict source validator rejects
1097
- * unknown fields so a pre-v15 Home cannot smuggle an unrecognized shape.
1098
- */
1099
- function storedTaskDurableJobsStep() {
1100
- return {
1101
- kind: "compatible",
1102
- axis: "record",
1103
- recordKind: "storedTask",
1104
- fromVersion: STORED_TASK_DURABLE_JOBS_FROM_VERSION,
1105
- toVersion: STORED_TASK_DURABLE_JOBS_TO_VERSION,
1106
- defaults: [
1107
- "durableJobs defaults to an empty map on every Task aggregate",
1108
- "idHighWaterMarks.durableJob defaults to 0 on every Task aggregate"
1109
- ],
1110
- validateSource: (snapshot) => requireStoredTaskV14Shape(snapshot),
1111
- normalize: (snapshot) => normalizeStoredTaskV14ToV15(snapshot)
1112
- };
1113
- }
1114
- const STORED_TASK_V14_FIELDS = [
1115
- "schemaVersion",
1116
- "task",
1117
- "idHighWaterMarks",
1118
- "brief",
1119
- "changeSets",
1120
- "integrationAttempts",
1121
- "integrationQueue",
1122
- // durableJobs may already be present on a v14 task when the Home was
1123
- // written by a build that had the family before the storedTask version
1124
- // advanced (or after a state.json→SQLite repair round-trip, which
1125
- // reconstructs every current family map). The 14→15 normalizer preserves
1126
- // existing records instead of overwriting them with an empty map.
1127
- "durableJobs",
1128
- // jobCallerKeyHashes is introduced at v16 but the state.json→SQLite repair
1129
- // round-trip reconstructs every current family map, so a v14 task may carry
1130
- // an empty map after the repair. The 14→15 normalizer preserves it.
1131
- "jobCallerKeyHashes",
1132
- "capabilityGrants",
1133
- "releaseWorkflows",
1134
- "roles",
1135
- "managedWorkspaces",
1136
- "roleSessionSets",
1137
- "workItems",
1138
- "agentRuns",
1139
- "reviewRounds",
1140
- "activeRuns",
1141
- "messages",
1142
- "inputRequests",
1143
- "decisions",
1144
- "milestones",
1145
- "events",
1146
- "leaderFailure",
1147
- "operatorNotification"
1148
- ];
1149
- function requireStoredTaskV14Shape(snapshot) {
1150
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1151
- if (manifestVersions.storedTask !== STORED_TASK_DURABLE_JOBS_FROM_VERSION) {
1152
- throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_DURABLE_JOBS_FROM_VERSION}.`);
1153
- }
1154
- if (snapshot.state === null)
1155
- return;
1156
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1157
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1158
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1159
- if (task.schemaVersion !== STORED_TASK_DURABLE_JOBS_FROM_VERSION) {
1160
- throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_DURABLE_JOBS_FROM_VERSION}.`);
1161
- }
1162
- const allowed = new Set(STORED_TASK_V14_FIELDS);
1163
- const unknown = Object.keys(task).find((key) => !allowed.has(key));
1164
- if (unknown !== undefined) {
1165
- throw new Error(`Task aggregate ${taskId} has an unknown v14 field: ${unknown}.`);
1166
- }
1167
- }
1168
- }
1169
- function normalizeStoredTaskV14ToV15(snapshot) {
1170
- requireStoredTaskV14Shape(snapshot);
1171
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1172
- const schemaManifest = {
1173
- ...snapshot.schemaManifest,
1174
- recordVersions: {
1175
- ...manifestVersions,
1176
- storedTask: STORED_TASK_DURABLE_JOBS_TO_VERSION
1177
- }
1178
- };
1179
- if (snapshot.state === null)
1180
- return { schemaManifest, state: null };
1181
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1182
- const nextTasks = {};
1183
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1184
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1185
- // The v14 high-water marks have no durableJob family; the strict current
1186
- // parser requires every family key, so the normalizer supplies the zero
1187
- // default in the same pass as the empty durableJobs map.
1188
- const highWaterMarks = asObject(task.idHighWaterMarks, `Task aggregate ${taskId} idHighWaterMarks`);
1189
- const existingJobs = task.durableJobs === undefined
1190
- ? {}
1191
- : asObject(task.durableJobs, `Task aggregate ${taskId} durableJobs`);
1192
- const existingJobMark = highWaterMarks.durableJob;
1193
- nextTasks[taskId] = {
1194
- ...task,
1195
- schemaVersion: STORED_TASK_DURABLE_JOBS_TO_VERSION,
1196
- idHighWaterMarks: {
1197
- ...highWaterMarks,
1198
- durableJob: typeof existingJobMark === "number" ? existingJobMark : 0
1199
- },
1200
- durableJobs: existingJobs
1201
- };
1202
- }
1203
- return {
1204
- schemaManifest,
1205
- state: { ...snapshot.state, tasks: nextTasks }
1206
- };
1207
- }
1208
- /**
1209
- * rr13: The StoredTask aggregate gains the `jobCallerKeyHashes` map. Every v15
1210
- * task defaults to an empty map; the compatible normalizer adds it in place
1211
- * without rewriting any other field. A v15 task must not already carry the
1212
- * field. No legacy fallback: a Session whose hash is absent is rejected at the
1213
- * Controller boundary (fail-closed).
1214
- */
1215
- function storedTaskJobCallerKeyHashesStep() {
1216
- return {
1217
- kind: "compatible",
1218
- axis: "record",
1219
- recordKind: "storedTask",
1220
- fromVersion: STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION,
1221
- toVersion: STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION,
1222
- defaults: [
1223
- "jobCallerKeyHashes defaults to an empty map on every Task aggregate"
1224
- ],
1225
- validateSource: (snapshot) => requireStoredTaskV15Shape(snapshot),
1226
- normalize: (snapshot) => normalizeStoredTaskV15ToV16(snapshot)
1227
- };
1228
- }
1229
- const STORED_TASK_V15_FIELDS = [
1230
- "schemaVersion",
1231
- "task",
1232
- "idHighWaterMarks",
1233
- "brief",
1234
- "changeSets",
1235
- "integrationAttempts",
1236
- "integrationQueue",
1237
- "durableJobs",
1238
- // May already be present after a state.json→SQLite repair round-trip (the
1239
- // reverse reader reconstructs every current family map). The 15→16
1240
- // normalizer preserves existing hashes instead of overwriting them.
1241
- "jobCallerKeyHashes",
1242
- "capabilityGrants",
1243
- "releaseWorkflows",
1244
- "roles",
1245
- "managedWorkspaces",
1246
- "roleSessionSets",
1247
- "workItems",
1248
- "agentRuns",
1249
- "reviewRounds",
1250
- "activeRuns",
1251
- "messages",
1252
- "inputRequests",
1253
- "decisions",
1254
- "milestones",
1255
- "events",
1256
- "leaderFailure",
1257
- "operatorNotification"
1258
- ];
1259
- function requireStoredTaskV15Shape(snapshot) {
1260
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1261
- if (manifestVersions.storedTask !== STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION) {
1262
- throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION}.`);
1263
- }
1264
- if (snapshot.state === null)
1265
- return;
1266
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1267
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1268
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1269
- if (task.schemaVersion !== STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION) {
1270
- throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION}.`);
1271
- }
1272
- const allowed = new Set(STORED_TASK_V15_FIELDS);
1273
- const unknown = Object.keys(task).find((key) => !allowed.has(key));
1274
- if (unknown !== undefined) {
1275
- throw new Error(`Task aggregate ${taskId} has an unknown v15 field: ${unknown}.`);
1276
- }
1277
- }
1278
- }
1279
- function normalizeStoredTaskV15ToV16(snapshot) {
1280
- requireStoredTaskV15Shape(snapshot);
1281
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1282
- const schemaManifest = {
1283
- ...snapshot.schemaManifest,
1284
- recordVersions: {
1285
- ...manifestVersions,
1286
- storedTask: STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION
1287
- }
1288
- };
1289
- if (snapshot.state === null)
1290
- return { schemaManifest, state: null };
1291
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1292
- const nextTasks = {};
1293
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1294
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1295
- const existingHashes = task.jobCallerKeyHashes === undefined
1296
- ? {}
1297
- : asObject(task.jobCallerKeyHashes, `Task aggregate ${taskId} jobCallerKeyHashes`);
1298
- nextTasks[taskId] = {
1299
- ...task,
1300
- schemaVersion: STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION,
1301
- jobCallerKeyHashes: existingHashes
1302
- };
1303
- }
1304
- return {
1305
- schemaManifest,
1306
- state: { ...snapshot.state, tasks: nextTasks }
1307
- };
1308
- }
1309
- /**
1310
- * StoredTask v17 introduces both the durable TaskWake ledger (Issue 04
1311
- * long-term design) and the publicationReferences map (Issue 11). Every
1312
- * aggregate gains empty `wakes` and `publicationReferences` maps and zero
1313
- * high-water marks.
1314
- */
1315
- function storedTaskWakesAndPublicationReferencesStep() {
1316
- return {
1317
- kind: "compatible",
1318
- axis: "record",
1319
- recordKind: "storedTask",
1320
- fromVersion: STORED_TASK_WAKES_FROM_VERSION,
1321
- toVersion: STORED_TASK_WAKES_TO_VERSION,
1322
- defaults: [
1323
- "wakes defaults to an empty map on every Task aggregate",
1324
- "publicationReferences defaults to an empty map on every Task aggregate"
1325
- ],
1326
- validateSource: (snapshot) => requireStoredTaskV16Shape(snapshot),
1327
- normalize: (snapshot) => normalizeStoredTaskV16ToV17(snapshot)
1328
- };
1329
- }
1330
- const STORED_TASK_V16_FIELDS = [
1331
- "schemaVersion",
1332
- "task",
1333
- "idHighWaterMarks",
1334
- "brief",
1335
- "changeSets",
1336
- "integrationAttempts",
1337
- "integrationQueue",
1338
- "durableJobs",
1339
- "jobCallerKeyHashes",
1340
- // The SQLite reverse reader reconstructs every physical family map even
1341
- // when the older manifest has not introduced that family yet. A v16
1342
- // aggregate may therefore contain an empty wakes map; preserve that exact
1343
- // repair shape, but reject records because taskWake has no v16 version.
1344
- "wakes",
1345
- // May already be present after the publicationReference introduction runs
1346
- // first; the 16->17 normalizer preserves the empty introduced map.
1347
- "publicationReferences",
1348
- "capabilityGrants",
1349
- "releaseWorkflows",
1350
- "roles",
1351
- "managedWorkspaces",
1352
- "roleSessionSets",
1353
- "workItems",
1354
- "agentRuns",
1355
- "reviewRounds",
1356
- "activeRuns",
1357
- "messages",
1358
- "inputRequests",
1359
- "decisions",
1360
- "milestones",
1361
- "events",
1362
- "leaderFailure",
1363
- "operatorNotification"
1364
- ];
1365
- function requireStoredTaskV16Shape(snapshot) {
1366
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1367
- if (manifestVersions.storedTask !== STORED_TASK_WAKES_FROM_VERSION) {
1368
- throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_WAKES_FROM_VERSION}.`);
1369
- }
1370
- if (snapshot.state === null)
1371
- return;
1372
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1373
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1374
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1375
- if (task.schemaVersion !== STORED_TASK_WAKES_FROM_VERSION) {
1376
- throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_WAKES_FROM_VERSION}.`);
1377
- }
1378
- const allowed = new Set(STORED_TASK_V16_FIELDS);
1379
- const unknown = Object.keys(task).find((key) => !allowed.has(key));
1380
- if (unknown !== undefined) {
1381
- throw new Error(`Task aggregate ${taskId} has an unknown v16 field: ${unknown}.`);
1382
- }
1383
- if (task.wakes !== undefined) {
1384
- const wakes = asObject(task.wakes, `Task aggregate ${taskId} wakes`);
1385
- if (Object.keys(wakes).length > 0) {
1386
- throw new Error(`Task aggregate ${taskId} already has Task wakes before v17.`);
1387
- }
1388
- }
1389
- if (task.publicationReferences !== undefined) {
1390
- const references = asObject(task.publicationReferences, `Task aggregate ${taskId} publicationReferences`);
1391
- if (Object.keys(references).length > 0) {
1392
- throw new Error(`Task aggregate ${taskId} already has publication references before v17.`);
1393
- }
1394
- }
1395
- }
1396
- }
1397
- function normalizeStoredTaskV16ToV17(snapshot) {
1398
- requireStoredTaskV16Shape(snapshot);
1399
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1400
- const schemaManifest = {
1401
- ...snapshot.schemaManifest,
1402
- recordVersions: {
1403
- ...manifestVersions,
1404
- storedTask: STORED_TASK_WAKES_TO_VERSION
1405
- }
1406
- };
1407
- if (snapshot.state === null)
1408
- return { schemaManifest, state: null };
1409
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1410
- const nextTasks = {};
1411
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1412
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1413
- const existingReferences = task.publicationReferences === undefined
1414
- ? {}
1415
- : asObject(task.publicationReferences, `Task aggregate ${taskId} publicationReferences`);
1416
- const existingWakes = task.wakes === undefined
1417
- ? {}
1418
- : asObject(task.wakes, `Task aggregate ${taskId} wakes`);
1419
- const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1420
- const existingMark = marks.publicationReference;
1421
- const existingWakeMark = marks.taskWake;
1422
- nextTasks[taskId] = {
1423
- ...task,
1424
- schemaVersion: STORED_TASK_WAKES_TO_VERSION,
1425
- idHighWaterMarks: {
1426
- ...marks,
1427
- publicationReference: typeof existingMark === "number" ? existingMark : 0,
1428
- taskWake: typeof existingWakeMark === "number" ? existingWakeMark : TASK_WAKE_FROM_VERSION
1429
- },
1430
- publicationReferences: existingReferences,
1431
- wakes: existingWakes
1432
- };
1433
- }
1434
- return {
1435
- schemaManifest,
1436
- state: { ...snapshot.state, tasks: nextTasks }
1437
- };
1438
- }
1439
- /** StoredTask v18 retires the mutable OperatorNotification projection. */
1440
- function storedTaskEventNoticesStep() {
1441
- return {
1442
- axis: "record",
1443
- recordKind: "storedTask",
1444
- fromVersion: STORED_TASK_EVENT_NOTICES_FROM_VERSION,
1445
- toVersion: STORED_TASK_EVENT_NOTICES_TO_VERSION,
1446
- preconditions: requireStoredTaskV17OperatorNoticeShape,
1447
- transform: normalizeStoredTaskV17ToV18,
1448
- declaredEffects: ["operator-mailbox-event-references"]
1449
- };
1450
- }
1451
- const STORED_TASK_V17_FIELDS = [
1452
- "schemaVersion",
1453
- "task",
1454
- "idHighWaterMarks",
1455
- "brief",
1456
- "changeSets",
1457
- "integrationAttempts",
1458
- "integrationQueue",
1459
- "durableJobs",
1460
- "roles",
1461
- "managedWorkspaces",
1462
- "roleSessionSets",
1463
- "jobCallerKeyHashes",
1464
- "workItems",
1465
- "contextSnapshots",
1466
- "agentRuns",
1467
- "reviewRounds",
1468
- "activeRuns",
1469
- "messages",
1470
- "inputRequests",
1471
- "decisions",
1472
- "milestones",
1473
- "events",
1474
- "wakes",
1475
- "capabilityGrants",
1476
- "releaseWorkflows",
1477
- "publicationReferences",
1478
- "leaderFailure",
1479
- "operatorNotification"
1480
- ];
1481
- function requireStoredTaskV17OperatorNoticeShape(snapshot) {
1482
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1483
- if (versions.storedTask !== STORED_TASK_EVENT_NOTICES_FROM_VERSION) {
1484
- throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_EVENT_NOTICES_FROM_VERSION}.`);
1485
- }
1486
- if (snapshot.state === null)
1487
- return;
1488
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1489
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1490
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1491
- if (task.schemaVersion !== STORED_TASK_EVENT_NOTICES_FROM_VERSION) {
1492
- throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_EVENT_NOTICES_FROM_VERSION}.`);
1493
- }
1494
- const allowed = new Set(STORED_TASK_V17_FIELDS);
1495
- const unknown = Object.keys(task).find((key) => !allowed.has(key));
1496
- if (unknown !== undefined) {
1497
- throw new Error(`Task aggregate ${taskId} has an unknown v17 field: ${unknown}.`);
1498
- }
1499
- const missing = STORED_TASK_V17_FIELDS.find((key) => !Object.hasOwn(task, key));
1500
- if (missing !== undefined) {
1501
- throw new Error(`Task aggregate ${taskId} is missing v17 field: ${missing}.`);
1502
- }
1503
- if (task.operatorNotification !== null) {
1504
- requireLegacyOperatorNotice(task.operatorNotification, taskId);
1505
- }
1506
- }
1507
- }
1508
- function requireLegacyOperatorNotice(value, taskId) {
1509
- const notice = asObject(value, `Operator notification ${taskId}`);
1510
- if (notice.schemaVersion !== 1 || notice.taskId !== taskId) {
1511
- throw new Error(`Operator notification identity is invalid: ${taskId}.`);
1512
- }
1513
- if (notice.type === "leader-recovery-failed") {
1514
- requireLegacyNoticeText(notice.message, `Operator notification message ${taskId}`);
1515
- }
1516
- else if (notice.type === "leader-stalled") {
1517
- requireLegacyNoticeText(notice.message, `Operator notification message ${taskId}`);
1518
- requireLegacyNoticeText(notice.runId, `Operator notification Run ${taskId}`);
1519
- requireLegacyNoticeTimestamp(notice.progressAt, `Operator notification progress ${taskId}`);
1520
- requireLegacyNoticeText(notice.evidenceKey, `Operator notification evidence ${taskId}`);
1521
- if (notice.classification !== "truly-stalled") {
1522
- throw new Error(`Operator notification classification is invalid: ${taskId}.`);
1523
- }
1524
- }
1525
- else if (notice.type === "task-terminal") {
1526
- if (notice.status !== "completed" && notice.status !== "retired") {
1527
- throw new Error(`Operator notification Task status is invalid: ${taskId}.`);
1528
- }
1529
- if (!["user", "operator", "leader"].includes(String(notice.by))) {
1530
- throw new Error(`Operator notification Task actor is invalid: ${taskId}.`);
1531
- }
1532
- requireLegacyNoticeText(notice.summary, `Operator notification summary ${taskId}`);
1533
- }
1534
- else {
1535
- throw new Error(`Operator notification type is invalid: ${taskId}.`);
1536
- }
1537
- requireLegacyNoticeTimestamp(notice.createdAt, `Operator notification createdAt ${taskId}`);
1538
- requireLegacyNoticeTimestamp(notice.updatedAt, `Operator notification updatedAt ${taskId}`);
1539
- }
1540
- function requireLegacyNoticeText(value, label) {
1541
- if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
1542
- throw new Error(`${label} is invalid.`);
1543
- }
1544
- return value;
1545
- }
1546
- function requireLegacyNoticeTimestamp(value, label) {
1547
- const timestamp = requireLegacyNoticeText(value, label);
1548
- if (!Number.isFinite(Date.parse(timestamp)))
1549
- throw new Error(`${label} is invalid.`);
1550
- return timestamp;
1551
- }
1552
- function normalizeStoredTaskV17ToV18(snapshot) {
1553
- requireStoredTaskV17OperatorNoticeShape(snapshot);
1554
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1555
- const schemaManifest = {
1556
- ...snapshot.schemaManifest,
1557
- recordVersions: {
1558
- ...versions,
1559
- storedTask: STORED_TASK_EVENT_NOTICES_TO_VERSION
1560
- }
1561
- };
1562
- if (snapshot.state === null)
1563
- return { schemaManifest, state: null };
1564
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1565
- const nextTasks = {};
1566
- const legacyNotices = new Map();
1567
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1568
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1569
- legacyNotices.set(taskId, task.operatorNotification === null
1570
- ? null
1571
- : asObject(task.operatorNotification, `Operator notification ${taskId}`));
1572
- const nextTask = {
1573
- ...task,
1574
- schemaVersion: STORED_TASK_EVENT_NOTICES_TO_VERSION,
1575
- events: { ...asObject(task.events, `Task events ${taskId}`) },
1576
- idHighWaterMarks: {
1577
- ...asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`)
1578
- }
1579
- };
1580
- delete nextTask.operatorNotification;
1581
- nextTasks[taskId] = nextTask;
1582
- }
1583
- const eventByTask = new Map();
1584
- const eventRefForTask = (taskId) => {
1585
- const existing = eventByTask.get(taskId);
1586
- if (existing !== undefined)
1587
- return { type: "event", taskId, id: existing };
1588
- const task = asObject(nextTasks[taskId], `Task aggregate ${taskId}`);
1589
- const events = asObject(task.events, `Task events ${taskId}`);
1590
- const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1591
- let sequence = Number(marks.event ?? 0) + 1;
1592
- if (!Number.isSafeInteger(sequence) || sequence < 1) {
1593
- throw new Error(`Task event high-water mark is invalid: ${taskId}.`);
1594
- }
1595
- while (events[`event-${sequence}`] !== undefined)
1596
- sequence += 1;
1597
- const eventId = `event-${sequence}`;
1598
- const notice = legacyNotices.get(taskId) ?? null;
1599
- const migrated = migratedOperatorNoticeEvent(taskId, eventId, notice, task);
1600
- events[eventId] = migrated;
1601
- marks.event = sequence;
1602
- eventByTask.set(taskId, eventId);
1603
- return { type: "event", taskId, id: eventId };
1604
- };
1605
- const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
1606
- const nextMailboxes = {};
1607
- for (const [key, rawMailbox] of Object.entries(mailboxes)) {
1608
- const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
1609
- const target = asObject(mailbox.target, `WorkMailbox target ${key}`);
1610
- nextMailboxes[key] = target.kind === "operator"
1611
- ? rewriteLegacyOperatorMailbox(mailbox, key, eventRefForTask)
1612
- : { ...mailbox };
1613
- }
1614
- for (const [taskId, notice] of legacyNotices) {
1615
- if (notice !== null)
1616
- eventRefForTask(taskId);
1617
- }
1618
- return {
1619
- schemaManifest,
1620
- state: { ...snapshot.state, tasks: nextTasks, mailboxes: nextMailboxes }
1621
- };
1622
- }
1623
- function migratedOperatorNoticeEvent(taskId, eventId, notice, taskAggregate) {
1624
- const task = asObject(taskAggregate.task, `Task ${taskId}`);
1625
- const timestamp = String(notice?.updatedAt ?? task.updatedAt ?? task.createdAt ?? "");
1626
- if (!Number.isFinite(Date.parse(timestamp))) {
1627
- throw new Error(`Legacy Operator notice timestamp is invalid: ${taskId}.`);
1628
- }
1629
- const type = notice?.type;
1630
- if (notice !== null && type === "task-terminal") {
1631
- const status = String(notice.status ?? "");
1632
- if (status !== "completed" && status !== "retired") {
1633
- throw new Error(`Legacy terminal Operator notice status is invalid: ${taskId}.`);
1634
- }
1635
- return {
1636
- schemaVersion: 2,
1637
- id: eventId,
1638
- taskId,
1639
- type: `task.${status}`,
1640
- payload: {
1641
- status,
1642
- by: String(notice.by ?? "operator"),
1643
- summary: String(notice.summary ?? "Task reached a terminal state."),
1644
- migratedFrom: "operator-notification"
1645
- },
1646
- createdAt: timestamp
1647
- };
1648
- }
1649
- if (notice !== null && type === "leader-stalled") {
1650
- return {
1651
- schemaVersion: 2,
1652
- id: eventId,
1653
- taskId,
1654
- type: "run.stalled",
1655
- payload: {
1656
- runId: String(notice.runId ?? "unknown"),
1657
- roleName: "leader",
1658
- progressAt: String(notice.progressAt ?? timestamp),
1659
- classification: "truly-stalled",
1660
- evidenceKey: String(notice.evidenceKey ?? "legacy-operator-notification"),
1661
- migratedFrom: "operator-notification"
1662
- },
1663
- createdAt: timestamp
1664
- };
1665
- }
1666
- return {
1667
- schemaVersion: 2,
1668
- id: eventId,
1669
- taskId,
1670
- type: "leader.attention-required",
1671
- payload: {
1672
- reason: type === "leader-recovery-failed"
1673
- ? "leader-recovery-failed"
1674
- : "legacy-operator-mailbox",
1675
- message: String(notice?.message ?? "Inspect the migrated Operator notice."),
1676
- migratedFrom: notice === null ? "operator-mailbox" : "operator-notification"
1677
- },
1678
- createdAt: timestamp
1679
- };
1680
- }
1681
- function rewriteLegacyOperatorMailbox(mailbox, key, eventRefForTask) {
1682
- const rewriteRef = (rawRef) => {
1683
- const ref = asObject(rawRef, `WorkMailbox ${key} entity ref`);
1684
- if (ref.type === "input" || ref.type === "event")
1685
- return { ...ref };
1686
- const taskId = ref.type === "task" ? ref.id : ref.taskId;
1687
- if (typeof taskId !== "string" || taskId.trim().length === 0) {
1688
- throw new Error(`Legacy Operator mailbox ref has no Task identity: ${key}.`);
1689
- }
1690
- return eventRefForTask(taskId);
1691
- };
1692
- const rewriteBatch = (rawBatch, label) => {
1693
- const batch = asObject(rawBatch, label);
1694
- if (!Array.isArray(batch.refs))
1695
- throw new Error(`${label} refs must be an array.`);
1696
- const sourceRefs = batch.refs.map((rawRef) => asObject(rawRef, `${label} entity ref`));
1697
- const semanticTaskIds = new Set(sourceRefs.flatMap((ref) => ((ref.type === "input" || ref.type === "event") && typeof ref.taskId === "string"
1698
- ? [ref.taskId]
1699
- : [])));
1700
- const refs = [];
1701
- const seen = new Set();
1702
- for (const sourceRef of sourceRefs) {
1703
- const referencedTaskId = sourceRef.type === "task" ? sourceRef.id : sourceRef.taskId;
1704
- if (sourceRef.type !== "input"
1705
- && sourceRef.type !== "event"
1706
- && typeof referencedTaskId === "string"
1707
- && semanticTaskIds.has(referencedTaskId)) {
1708
- continue;
1709
- }
1710
- const ref = rewriteRef(sourceRef);
1711
- const identity = `${String(ref.type)}\0${String(ref.taskId ?? "")}\0${String(ref.id)}`;
1712
- if (seen.has(identity))
1713
- continue;
1714
- seen.add(identity);
1715
- refs.push(ref);
1716
- }
1717
- return { ...batch, refs };
1718
- };
1719
- const rewriteProcessing = (raw) => {
1720
- if (raw === null)
1721
- return null;
1722
- const processing = asObject(raw, `WorkMailbox ${key} processing`);
1723
- return {
1724
- ...processing,
1725
- batch: rewriteBatch(processing.batch, `WorkMailbox ${key} processing batch`),
1726
- ...(processing.executionRef === undefined
1727
- ? {}
1728
- : { executionRef: rewriteRef(processing.executionRef) })
1729
- };
1730
- };
1731
- const rewriteDelivery = (raw) => {
1732
- if (raw === null)
1733
- return null;
1734
- const delivery = asObject(raw, `WorkMailbox ${key} input delivery`);
1735
- return {
1736
- ...delivery,
1737
- batch: rewriteBatch(delivery.batch, `WorkMailbox ${key} input delivery batch`),
1738
- executionRef: rewriteRef(delivery.executionRef)
1739
- };
1740
- };
1741
- const pending = asObject(mailbox.pending, `WorkMailbox ${key} pending lanes`);
1742
- return {
1743
- ...mailbox,
1744
- processing: rewriteProcessing(mailbox.processing),
1745
- pending: {
1746
- ...pending,
1747
- normal: pending.normal === null
1748
- ? null
1749
- : rewriteBatch(pending.normal, `WorkMailbox ${key} normal batch`),
1750
- userCorrection: pending.userCorrection === null
1751
- ? null
1752
- : rewriteBatch(pending.userCorrection, `WorkMailbox ${key} correction batch`)
1753
- },
1754
- inputDelivery: rewriteDelivery(mailbox.inputDelivery)
1755
- };
1756
- }
1757
- /**
1758
- * The taskWake record family is introduced alongside StoredTask v17. The
1759
- * compatible step supplies the empty map and high-water mark; this step only
1760
- * advances the manifest record version so the planner can resolve the 0->1
1761
- * boundary.
1762
- */
1763
- function taskWakeIntroductionStep() {
1764
- return {
1765
- kind: "compatible",
1766
- axis: "record",
1767
- recordKind: "taskWake",
1768
- fromVersion: TASK_WAKE_FROM_VERSION,
1769
- toVersion: TASK_WAKE_TO_VERSION,
1770
- introduction: true,
1771
- defaults: [
1772
- "taskWake family starts at version 1 with an empty ledger"
1773
- ],
1774
- validateSource: (snapshot) => {
1775
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1776
- const namedVersion = manifestVersions.taskWake;
1777
- if (namedVersion !== undefined && namedVersion !== TASK_WAKE_FROM_VERSION) {
1778
- throw new Error(`Record taskWake introduction requires an absent manifest version or ${TASK_WAKE_FROM_VERSION}.`);
1779
- }
1780
- },
1781
- normalize: (snapshot) => ({
1782
- schemaManifest: {
1783
- ...snapshot.schemaManifest,
1784
- recordVersions: {
1785
- ...asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions"),
1786
- taskWake: TASK_WAKE_TO_VERSION
1787
- }
1788
- },
1789
- state: snapshot.state
1790
- })
1791
- };
1792
- }
1793
- /**
1794
- * The durableJob record family is introduced alongside the StoredTask v15
1795
- * aggregate. A pre-baseline Home has no durableJobs map and no manifest entry;
1796
- * the introduction declares the family at version 1. The StoredTask 14->15
1797
- * compatible step supplies the empty map; this step only advances the manifest
1798
- * record version so the planner can resolve the 0->1 boundary.
1799
- */
1800
- function durableJobIntroductionStep() {
1801
- return {
1802
- kind: "compatible",
1803
- axis: "record",
1804
- recordKind: "durableJob",
1805
- fromVersion: 0,
1806
- toVersion: 1,
1807
- introduction: true,
1808
- defaults: ["durableJob family is introduced as an empty map on every Task"],
1809
- validateSource: (snapshot) => {
1810
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1811
- const declared = manifestVersions.durableJob;
1812
- if (declared !== undefined && declared !== 0) {
1813
- throw new Error(`Record durableJob introduction requires manifest version 0 or absent, found ${String(declared)}.`);
1814
- }
1815
- if (snapshot.state === null)
1816
- return;
1817
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1818
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1819
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1820
- if (task.durableJobs !== undefined) {
1821
- const jobs = asObject(task.durableJobs, `durableJobs map ${taskId}`);
1822
- if (Object.keys(jobs).length > 0) {
1823
- throw new Error(`Task aggregate ${taskId} must not carry durableJobs before introduction.`);
1824
- }
1825
- }
1826
- }
1827
- },
1828
- normalize: (snapshot) => {
1829
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1830
- return {
1831
- schemaManifest: {
1832
- ...snapshot.schemaManifest,
1833
- recordVersions: {
1834
- ...manifestVersions,
1835
- durableJob: 1
1836
- }
1837
- },
1838
- state: snapshot.state === null
1839
- ? null
1840
- : { ...snapshot.state }
1841
- };
1842
- }
1843
- };
1844
- }
1845
- /**
1846
- * Upgrade one nested Task record family without guessing fields or repairing
1847
- * malformed state. The new execution lineage fields are optional, so legal
1848
- * old single-lane records retain their direct shape; the next dispatch creates
1849
- * the unified one-lane Group explicitly. The version transition is still
1850
- * durable and centralized, which keeps old Homes out of the strict current
1851
- * parser until this step has run.
1852
- */
1853
- /**
1854
- * The post-baseline introduction of the task-scoped CapabilityGrant family.
1855
- * A pre-introduction Home names no capabilityGrant manifest version and its
1856
- * Task aggregates carry no `capabilityGrants` map; this explicit 0->1 step
1857
- * adds the empty family to every Task (plus the matching high-water mark) so
1858
- * the strict current parser can open the migrated state.
1859
- */
1860
- function capabilityGrantIntroductionStep() {
1861
- return {
1862
- axis: "record",
1863
- recordKind: "capabilityGrant",
1864
- fromVersion: CAPABILITY_GRANT_FROM_VERSION,
1865
- toVersion: CAPABILITY_GRANT_TO_VERSION,
1866
- introduction: true,
1867
- preconditions: requireCapabilityGrantIntroduction,
1868
- transform: introduceCapabilityGrantFamily,
1869
- declaredEffects: []
1870
- };
1871
- }
1872
- function requireCapabilityGrantIntroduction(snapshot) {
1873
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1874
- const namedVersion = manifestVersions.capabilityGrant;
1875
- if (namedVersion !== undefined && namedVersion !== CAPABILITY_GRANT_FROM_VERSION) {
1876
- throw new Error(`Record capabilityGrant introduction requires an absent manifest version or ${CAPABILITY_GRANT_FROM_VERSION}.`);
1877
- }
1878
- if (snapshot.state === null)
1879
- return;
1880
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1881
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1882
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1883
- if (task.capabilityGrants !== undefined) {
1884
- const grants = asObject(task.capabilityGrants, `capabilityGrant map ${taskId}`);
1885
- if (Object.keys(grants).length > 0) {
1886
- throw new Error(`Capability grant introduction found existing records: ${taskId}.`);
1887
- }
1888
- }
1889
- const marks = task.idHighWaterMarks;
1890
- if (marks !== undefined) {
1891
- const highWaterMarks = asObject(marks, `Task id high-water marks ${taskId}`);
1892
- const mark = highWaterMarks.capabilityGrant;
1893
- if (mark !== undefined && mark !== CAPABILITY_GRANT_FROM_VERSION) {
1894
- throw new Error(`Capability grant introduction found a high-water mark: ${taskId}.`);
1895
- }
1896
- }
1897
- }
1898
- }
1899
- function introduceCapabilityGrantFamily(snapshot) {
1900
- requireCapabilityGrantIntroduction(snapshot);
1901
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1902
- const schemaManifest = {
1903
- ...snapshot.schemaManifest,
1904
- recordVersions: {
1905
- ...manifestVersions,
1906
- capabilityGrant: CAPABILITY_GRANT_TO_VERSION
1907
- }
1908
- };
1909
- if (snapshot.state === null)
1910
- return { schemaManifest, state: null };
1911
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1912
- const nextTasks = {};
1913
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1914
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1915
- const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1916
- nextTasks[taskId] = {
1917
- ...task,
1918
- idHighWaterMarks: { ...marks, capabilityGrant: CAPABILITY_GRANT_FROM_VERSION },
1919
- capabilityGrants: {}
1920
- };
1921
- }
1922
- return {
1923
- schemaManifest,
1924
- state: { ...snapshot.state, tasks: nextTasks }
1925
- };
1926
- }
1927
- /**
1928
- * The post-baseline introduction of the task-scoped ReleaseWorkflow family.
1929
- * A pre-introduction Home names no releaseWorkflow manifest version and its
1930
- * Task aggregates carry no `releaseWorkflows` map; this explicit 0->1 step
1931
- * adds the empty family to every Task (plus the matching high-water mark) so
1932
- * the strict current parser can open the migrated state.
1933
- */
1934
- function releaseWorkflowIntroductionStep() {
1935
- return {
1936
- axis: "record",
1937
- recordKind: "releaseWorkflow",
1938
- fromVersion: RELEASE_WORKFLOW_FROM_VERSION,
1939
- toVersion: RELEASE_WORKFLOW_TO_VERSION,
1940
- introduction: true,
1941
- preconditions: requireReleaseWorkflowIntroduction,
1942
- transform: introduceReleaseWorkflowFamily,
1943
- declaredEffects: []
1944
- };
1945
- }
1946
- function requireReleaseWorkflowIntroduction(snapshot) {
1947
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1948
- const namedVersion = manifestVersions.releaseWorkflow;
1949
- if (namedVersion !== undefined && namedVersion !== RELEASE_WORKFLOW_FROM_VERSION) {
1950
- throw new Error(`Record releaseWorkflow introduction requires an absent manifest version or ${RELEASE_WORKFLOW_FROM_VERSION}.`);
1951
- }
1952
- if (snapshot.state === null)
1953
- return;
1954
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1955
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1956
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1957
- if (task.releaseWorkflows !== undefined) {
1958
- const workflows = asObject(task.releaseWorkflows, `releaseWorkflow map ${taskId}`);
1959
- if (Object.keys(workflows).length > 0) {
1960
- throw new Error(`Release workflow introduction found existing records: ${taskId}.`);
1961
- }
1962
- }
1963
- const marks = task.idHighWaterMarks;
1964
- if (marks !== undefined) {
1965
- const highWaterMarks = asObject(marks, `Task id high-water marks ${taskId}`);
1966
- const mark = highWaterMarks.releaseWorkflow;
1967
- if (mark !== undefined && mark !== RELEASE_WORKFLOW_FROM_VERSION) {
1968
- throw new Error(`Release workflow introduction found a high-water mark: ${taskId}.`);
1969
- }
1970
- }
1971
- }
1972
- }
1973
- function introduceReleaseWorkflowFamily(snapshot) {
1974
- requireReleaseWorkflowIntroduction(snapshot);
1975
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1976
- const schemaManifest = {
1977
- ...snapshot.schemaManifest,
1978
- recordVersions: {
1979
- ...manifestVersions,
1980
- releaseWorkflow: RELEASE_WORKFLOW_TO_VERSION
1981
- }
1982
- };
1983
- if (snapshot.state === null)
1984
- return { schemaManifest, state: null };
1985
- const tasks = asObject(snapshot.state.tasks, "state tasks");
1986
- const nextTasks = {};
1987
- for (const [taskId, rawTask] of Object.entries(tasks)) {
1988
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
1989
- const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1990
- nextTasks[taskId] = {
1991
- ...task,
1992
- idHighWaterMarks: { ...marks, releaseWorkflow: RELEASE_WORKFLOW_FROM_VERSION },
1993
- releaseWorkflows: {}
1994
- };
1995
- }
1996
- return {
1997
- schemaManifest,
1998
- state: { ...snapshot.state, tasks: nextTasks }
1999
- };
2000
- }
2001
- /**
2002
- * Issue 11: introduce the task-scoped PublicationReference family. A
2003
- * pre-introduction Home names no publicationReference manifest version and its
2004
- * Task aggregates carry no `publicationReferences` map; this explicit 0->1
2005
- * step adds the empty family to every Task (plus the matching high-water mark).
2006
- */
2007
- function publicationReferenceIntroductionStep() {
2008
- return {
2009
- axis: "record",
2010
- recordKind: "publicationReference",
2011
- fromVersion: PUBLICATION_REFERENCE_FROM_VERSION,
2012
- toVersion: PUBLICATION_REFERENCE_TO_VERSION,
2013
- introduction: true,
2014
- preconditions: requirePublicationReferenceIntroduction,
2015
- transform: introducePublicationReferenceFamily,
2016
- declaredEffects: []
2017
- };
2018
- }
2019
- function requirePublicationReferenceIntroduction(snapshot) {
2020
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2021
- const namedVersion = manifestVersions.publicationReference;
2022
- if (namedVersion !== undefined && namedVersion !== PUBLICATION_REFERENCE_FROM_VERSION) {
2023
- throw new Error(`Record publicationReference introduction requires an absent manifest version or ${PUBLICATION_REFERENCE_FROM_VERSION}.`);
2024
- }
2025
- if (snapshot.state === null)
2026
- return;
2027
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2028
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2029
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
2030
- if (task.publicationReferences !== undefined) {
2031
- const references = asObject(task.publicationReferences, `publicationReference map ${taskId}`);
2032
- if (Object.keys(references).length > 0) {
2033
- throw new Error(`Publication reference introduction found existing records: ${taskId}.`);
2034
- }
2035
- }
2036
- const marks = task.idHighWaterMarks;
2037
- if (marks !== undefined) {
2038
- const highWaterMarks = asObject(marks, `Task id high-water marks ${taskId}`);
2039
- const mark = highWaterMarks.publicationReference;
2040
- if (mark !== undefined && mark !== PUBLICATION_REFERENCE_FROM_VERSION) {
2041
- throw new Error(`Publication reference introduction found a high-water mark: ${taskId}.`);
2042
- }
2043
- }
2044
- }
2045
- }
2046
- function introducePublicationReferenceFamily(snapshot) {
2047
- requirePublicationReferenceIntroduction(snapshot);
2048
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2049
- const schemaManifest = {
2050
- ...snapshot.schemaManifest,
2051
- recordVersions: {
2052
- ...manifestVersions,
2053
- publicationReference: PUBLICATION_REFERENCE_TO_VERSION
2054
- }
2055
- };
2056
- if (snapshot.state === null)
2057
- return { schemaManifest, state: null };
2058
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2059
- const nextTasks = {};
2060
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2061
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
2062
- const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
2063
- nextTasks[taskId] = {
2064
- ...task,
2065
- idHighWaterMarks: { ...marks, publicationReference: PUBLICATION_REFERENCE_FROM_VERSION },
2066
- publicationReferences: {}
2067
- };
2068
- }
2069
- return {
2070
- schemaManifest,
2071
- state: { ...snapshot.state, tasks: nextTasks }
2072
- };
2073
- }
2074
- function recordFamilyStep(recordKind, fromVersion, toVersion, taskMapKey) {
2075
- return {
2076
- axis: "record",
2077
- recordKind,
2078
- fromVersion,
2079
- toVersion,
2080
- preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, recordKind, fromVersion, taskMapKey),
2081
- transform: (snapshot) => migrateRecordFamily(snapshot, recordKind, fromVersion, toVersion, taskMapKey),
2082
- declaredEffects: []
2083
- };
2084
- }
2085
- function reviewRoundTaskAnchorStep() {
2086
- return {
2087
- axis: "record",
2088
- recordKind: "reviewRound",
2089
- fromVersion: REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION,
2090
- toVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION,
2091
- preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "reviewRound", REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION, "reviewRounds"),
2092
- transform: migrateReviewRoundTaskAnchors,
2093
- declaredEffects: []
2094
- };
2095
- }
2096
- function migrateReviewRoundTaskAnchors(snapshot) {
2097
- requireRecordFamilyVersion(snapshot, "reviewRound", REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION, "reviewRounds");
2098
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2099
- const schemaManifest = {
2100
- ...snapshot.schemaManifest,
2101
- recordVersions: {
2102
- ...manifestVersions,
2103
- reviewRound: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION
2104
- }
2105
- };
2106
- if (snapshot.state === null)
2107
- return { schemaManifest, state: null };
2108
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2109
- const nextTasks = {};
2110
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2111
- const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
2112
- const rawRounds = aggregate.reviewRounds;
2113
- if (rawRounds === undefined) {
2114
- nextTasks[taskId] = { ...aggregate };
2115
- continue;
2116
- }
2117
- const rounds = asObject(rawRounds, `reviewRound map ${taskId}`);
2118
- const nextRounds = {};
2119
- for (const [roundId, rawRound] of Object.entries(rounds)) {
2120
- const round = asObject(rawRound, `reviewRound ${taskId}/${roundId}`);
2121
- if (round.scope !== "task") {
2122
- nextRounds[roundId] = {
2123
- ...round,
2124
- schemaVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION
2125
- };
2126
- continue;
2127
- }
2128
- const workItemId = requiredMigrationText(round.workItemId, `reviewRound ${taskId}/${roundId} WorkItem id`);
2129
- const candidateId = requiredMigrationText(round.candidateId, `reviewRound ${taskId}/${roundId} Candidate id`);
2130
- const { workItemId: _workItemId, candidateId: _candidateId, schemaVersion: _schemaVersion, ...retained } = round;
2131
- nextRounds[roundId] = {
2132
- ...retained,
2133
- schemaVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION,
2134
- legacyAnchor: { workItemId, candidateId }
2135
- };
2136
- }
2137
- nextTasks[taskId] = { ...aggregate, reviewRounds: nextRounds };
2138
- }
2139
- return {
2140
- schemaManifest,
2141
- state: { ...snapshot.state, tasks: nextTasks }
2142
- };
2143
- }
2144
- function agentRunContextProtocolStep() {
2145
- return {
2146
- axis: "record",
2147
- recordKind: "agentRun",
2148
- fromVersion: AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION,
2149
- toVersion: AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION,
2150
- preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION, "agentRuns"),
2151
- transform: migrateAgentRunsToContextProtocol,
2152
- declaredEffects: []
2153
- };
2154
- }
2155
- function migrateAgentRunsToContextProtocol(snapshot) {
2156
- requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION, "agentRuns");
2157
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2158
- const schemaManifest = {
2159
- ...snapshot.schemaManifest,
2160
- recordVersions: {
2161
- ...manifestVersions,
2162
- agentRun: AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION
2163
- }
2164
- };
2165
- if (snapshot.state === null)
2166
- return { schemaManifest, state: null };
2167
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2168
- const nextTasks = {};
2169
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2170
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
2171
- const records = asObject(task.agentRuns ?? {}, `agentRun map ${taskId}`);
2172
- const nextRecords = {};
2173
- for (const [runId, rawRecord] of Object.entries(records)) {
2174
- const record = asObject(rawRecord, `agentRun ${taskId}/${runId}`);
2175
- const { input: rawInput, schemaVersion: _schemaVersion, ...rest } = record;
2176
- const roleName = requiredMigrationText(record.roleName, "Agent Run roleName");
2177
- const purpose = record.purpose === "review" ? "review" : "execution";
2178
- const workItemId = optionalMigrationText(record.workItemId, "Agent Run workItemId");
2179
- const reviewRoundId = optionalMigrationText(record.reviewRoundId, "Agent Run reviewRoundId");
2180
- const executionGroupId = optionalMigrationText(record.executionGroupId, "Agent Run executionGroupId");
2181
- const executionLaneId = optionalMigrationText(record.executionLaneId, "Agent Run executionLaneId");
2182
- const action = purpose === "review"
2183
- ? "review-round"
2184
- : roleName === "leader"
2185
- ? "leader-wake"
2186
- : workItemId === undefined
2187
- ? "lead-task"
2188
- : "execute-work-item";
2189
- const subject = {
2190
- taskId,
2191
- ...(workItemId === undefined ? {} : { workItemId }),
2192
- ...(reviewRoundId === undefined ? {} : { reviewRoundId }),
2193
- ...(executionGroupId === undefined ? {} : { executionGroupId }),
2194
- ...(executionLaneId === undefined ? {} : { executionLaneId })
2195
- };
2196
- const assignment = {
2197
- schemaVersion: 1,
2198
- runId,
2199
- roleName,
2200
- purpose,
2201
- action,
2202
- subject,
2203
- ...(typeof rawInput === "string" && rawInput.trim().length > 0
2204
- ? { directive: rawInput }
2205
- : {}),
2206
- deltaRefIds: []
2207
- };
2208
- nextRecords[runId] = {
2209
- ...rest,
2210
- schemaVersion: AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION,
2211
- assignment,
2212
- bootstrapEnvelope: {
2213
- protocol: "yui-run/v1",
2214
- runId,
2215
- roleName,
2216
- purpose,
2217
- action,
2218
- subject,
2219
- deltaRefIds: []
2220
- }
2221
- };
2222
- }
2223
- nextTasks[taskId] = { ...task, agentRuns: nextRecords };
2224
- }
2225
- return {
2226
- schemaManifest,
2227
- state: { ...snapshot.state, tasks: nextTasks }
2228
- };
2229
- }
2230
- function agentRunRetryEpisodeStep() {
2231
- return {
2232
- axis: "record",
2233
- recordKind: "agentRun",
2234
- fromVersion: AGENT_RUN_RETRY_EPISODE_FROM_VERSION,
2235
- toVersion: AGENT_RUN_RETRY_EPISODE_TO_VERSION,
2236
- preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_RETRY_EPISODE_FROM_VERSION, "agentRuns"),
2237
- transform: migrateAgentRunsToRetryEpisodes,
2238
- declaredEffects: []
2239
- };
2240
- }
2241
- function migrateAgentRunsToRetryEpisodes(snapshot) {
2242
- requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_RETRY_EPISODE_FROM_VERSION, "agentRuns");
2243
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2244
- const schemaManifest = {
2245
- ...snapshot.schemaManifest,
2246
- recordVersions: { ...manifestVersions, agentRun: AGENT_RUN_RETRY_EPISODE_TO_VERSION }
2247
- };
2248
- if (snapshot.state === null)
2249
- return { schemaManifest, state: null };
2250
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2251
- const nextTasks = {};
2252
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2253
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
2254
- const records = asObject(task.agentRuns ?? {}, `agentRun map ${taskId}`);
2255
- const nextRecords = {};
2256
- for (const [runId, rawRecord] of Object.entries(records)) {
2257
- const record = asObject(rawRecord, `agentRun ${taskId}/${runId}`);
2258
- const legacyRetry = record.providerRetry === undefined
2259
- ? undefined
2260
- : asObject(record.providerRetry, `providerRetry ${taskId}/${runId}`);
2261
- let providerRetry = undefined;
2262
- let deliveryReceiptId;
2263
- if (legacyRetry !== undefined) {
2264
- const attempt = requiredMigrationPositiveInteger(legacyRetry.attempt, `providerRetry attempt ${taskId}/${runId}`);
2265
- const firstFailureAt = requiredMigrationTimestamp(legacyRetry.firstFailureAt, `providerRetry firstFailureAt ${taskId}/${runId}`);
2266
- const lastFailureAt = requiredMigrationTimestamp(legacyRetry.lastFailureAt, `providerRetry lastFailureAt ${taskId}/${runId}`);
2267
- const nextAttemptAt = optionalMigrationTimestamp(legacyRetry.nextAttemptAt, `providerRetry nextAttemptAt ${taskId}/${runId}`);
2268
- const policyBlocked = legacyRetry.errorClass === "policy-denied";
2269
- const deadlineMs = Math.max(Date.parse(firstFailureAt) + 600_000, nextAttemptAt === undefined ? 0 : Date.parse(nextAttemptAt) + 1);
2270
- deliveryReceiptId = nextAttemptAt !== undefined || policyBlocked
2271
- ? undefined
2272
- : `legacy-retry-receipt-${runId}-${attempt}`;
2273
- providerRetry = {
2274
- schemaVersion: 2,
2275
- episodeId: `legacy-retry-${runId}`,
2276
- failureEventId: `legacy-provider-failure-${runId}-${attempt}`,
2277
- policyVersion: 1,
2278
- state: nextAttemptAt !== undefined
2279
- ? "scheduled"
2280
- : policyBlocked ? "blocked" : "awaiting-progress",
2281
- errorClass: legacyRetry.errorClass,
2282
- consecutiveFailures: attempt,
2283
- dispatchedRetries: Math.min(3, nextAttemptAt === undefined ? attempt : attempt - 1),
2284
- maxRetries: 3,
2285
- firstFailureAt,
2286
- lastFailureAt,
2287
- episodeDeadlineAt: new Date(deadlineMs).toISOString(),
2288
- ...(nextAttemptAt === undefined ? {} : { nextAttemptAt }),
2289
- ...(deliveryReceiptId === undefined
2290
- ? {}
2291
- : { lastRetryReceiptId: deliveryReceiptId }),
2292
- ...(legacyRetry.launchId === undefined ? {} : { launchId: legacyRetry.launchId }),
2293
- ...(legacyRetry.nativeSessionId === undefined
2294
- ? {}
2295
- : { nativeSessionId: legacyRetry.nativeSessionId }),
2296
- lastErrorSummary: legacyRetry.lastErrorSummary
2297
- };
2298
- }
2299
- nextRecords[runId] = {
2300
- ...record,
2301
- schemaVersion: AGENT_RUN_RETRY_EPISODE_TO_VERSION,
2302
- ...(deliveryReceiptId === undefined ? {} : { deliveryReceiptId }),
2303
- ...(providerRetry === undefined ? {} : { providerRetry })
2304
- };
2305
- }
2306
- nextTasks[taskId] = { ...task, agentRuns: nextRecords };
2307
- }
2308
- return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
2309
- }
2310
- /** Deletes the retired retry episode; an AgentRun now records scheduling only. */
2311
- function agentRunErrorFactsStep() {
2312
- return {
2313
- axis: "record",
2314
- recordKind: "agentRun",
2315
- fromVersion: AGENT_RUN_ERROR_FACTS_FROM_VERSION,
2316
- toVersion: AGENT_RUN_ERROR_FACTS_TO_VERSION,
2317
- preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_ERROR_FACTS_FROM_VERSION, "agentRuns"),
2318
- transform: migrateAgentRunsToErrorFacts,
2319
- declaredEffects: []
2320
- };
2321
- }
2322
- function migrateAgentRunsToErrorFacts(snapshot) {
2323
- requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_ERROR_FACTS_FROM_VERSION, "agentRuns");
2324
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2325
- const schemaManifest = {
2326
- ...snapshot.schemaManifest,
2327
- recordVersions: { ...versions, agentRun: AGENT_RUN_ERROR_FACTS_TO_VERSION }
2328
- };
2329
- if (snapshot.state === null)
2330
- return { schemaManifest, state: null };
2331
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2332
- const nextTasks = {};
2333
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2334
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
2335
- const records = asObject(task.agentRuns ?? {}, `agentRun map ${taskId}`);
2336
- const nextRecords = {};
2337
- for (const [runId, rawRecord] of Object.entries(records)) {
2338
- const record = asObject(rawRecord, `agentRun ${taskId}/${runId}`);
2339
- const { providerRetry: _retry, ...retained } = record;
2340
- void _retry;
2341
- nextRecords[runId] = {
2342
- ...retained,
2343
- schemaVersion: AGENT_RUN_ERROR_FACTS_TO_VERSION
2344
- };
2345
- }
2346
- nextTasks[taskId] = { ...task, agentRuns: nextRecords };
2347
- }
2348
- return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
2349
- }
2350
- function requiredMigrationPositiveInteger(value, label) {
2351
- if (!Number.isSafeInteger(value) || value < 1) {
2352
- throw new Error(`${label} must be a positive integer.`);
2353
- }
2354
- return value;
2355
- }
2356
- function requiredMigrationTimestamp(value, label) {
2357
- if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
2358
- throw new Error(`${label} must be a timestamp.`);
2359
- }
2360
- return value;
2361
- }
2362
- function optionalMigrationTimestamp(value, label) {
2363
- return value === undefined ? undefined : requiredMigrationTimestamp(value, label);
2364
- }
2365
- function requiredMigrationText(value, label) {
2366
- if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
2367
- throw new Error(`${label} is invalid.`);
2368
- }
2369
- return value.trim();
2370
- }
2371
- function optionalMigrationText(value, label) {
2372
- return value === undefined ? undefined : requiredMigrationText(value, label);
2373
- }
2374
- /**
2375
- * The snapshot boundary is nested inside WorkItem/ReviewRound ExecutionGroup
2376
- * results. The parent record versions make that persisted shape explicit;
2377
- * this adjacent step intentionally performs no field rewrite, preserving old
2378
- * records while preventing a pre-v8/pre-v4 Home from entering the strict
2379
- * current parser without the declared transition.
2380
- */
2381
- function requireRecordFamilyVersion(snapshot, recordKind, fromVersion, taskMapKey) {
2382
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2383
- if (manifestVersions[recordKind] !== fromVersion) {
2384
- throw new Error(`Record ${recordKind} migration requires manifest version ${fromVersion}.`);
2385
- }
2386
- if (snapshot.state === null)
2387
- return;
2388
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2389
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2390
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
2391
- const records = task[taskMapKey];
2392
- if (records === undefined)
2393
- continue;
2394
- const map = asObject(records, `${recordKind} map ${taskId}`);
2395
- for (const [recordId, rawRecord] of Object.entries(map)) {
2396
- const record = asObject(rawRecord, `${recordKind} ${taskId}/${recordId}`);
2397
- if (record.schemaVersion !== fromVersion) {
2398
- throw new Error(`Record ${recordKind} ${taskId}/${recordId} must use schemaVersion ${fromVersion}.`);
2399
- }
2400
- }
2401
- }
2402
- }
2403
- function migrateRecordFamily(snapshot, recordKind, fromVersion, toVersion, taskMapKey) {
2404
- // Keep the same source-shape checks in the transform so a direct caller
2405
- // cannot bypass the migration's precondition contract.
2406
- requireRecordFamilyVersion(snapshot, recordKind, fromVersion, taskMapKey);
2407
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2408
- const schemaManifest = {
2409
- ...snapshot.schemaManifest,
2410
- recordVersions: {
2411
- ...manifestVersions,
2412
- [recordKind]: toVersion
2413
- }
2414
- };
2415
- if (snapshot.state === null)
2416
- return { schemaManifest, state: null };
2417
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2418
- const nextTasks = {};
2419
- for (const [taskId, rawTask] of Object.entries(tasks)) {
2420
- const task = asObject(rawTask, `Task aggregate ${taskId}`);
2421
- const records = task[taskMapKey];
2422
- if (records === undefined) {
2423
- nextTasks[taskId] = { ...task };
2424
- continue;
2425
- }
2426
- const map = asObject(records, `${recordKind} map ${taskId}`);
2427
- const nextMap = {};
2428
- for (const [recordId, rawRecord] of Object.entries(map)) {
2429
- nextMap[recordId] = {
2430
- ...asObject(rawRecord, `${recordKind} ${taskId}/${recordId}`),
2431
- schemaVersion: toVersion
2432
- };
2433
- }
2434
- nextTasks[taskId] = { ...task, [taskMapKey]: nextMap };
2435
- }
2436
- return {
2437
- schemaManifest,
2438
- state: { ...snapshot.state, tasks: nextTasks }
2439
- };
2440
- }
2441
- function asObject(value, label) {
2442
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
2443
- throw new Error(`${label} must be an object.`);
2444
- }
2445
- return value;
2446
- }
2447
- /** Historical public spelling retained as an alias to the single graph. */
2448
- export const createProductionRegistry = createProductionStorageRegistry;
2449
- /**
2450
- * Layout 6 -> 7 is the state.json -> SQLite WAL transition (task-21 §8). The
2451
- * transform only advances the manifest's layout version; the staged SQLite
2452
- * database is populated by the migration target's `writeFreshOutput`, and the
2453
- * `state.json` document is retained read-only for rollback. The precondition
2454
- * requires a layout-6 manifest so a future layout cannot be silently regressed.
2455
- */
2456
- function requireLayoutV6Snapshot(snapshot) {
2457
- if (snapshot.schemaManifest.storageVersion !== SQLITE_LAYOUT_FROM_VERSION) {
2458
- throw new Error(`Layout ${SQLITE_LAYOUT_FROM_VERSION}->${SQLITE_LAYOUT_TO_VERSION} migration requires ` +
2459
- `schema.json storageVersion ${SQLITE_LAYOUT_FROM_VERSION}.`);
2460
- }
2461
- }
2462
- function migrateLayoutV6ToV7(snapshot) {
2463
- requireLayoutV6Snapshot(snapshot);
2464
- return {
2465
- schemaManifest: {
2466
- ...snapshot.schemaManifest,
2467
- storageVersion: SQLITE_LAYOUT_TO_VERSION
2468
- },
2469
- state: snapshot.state
2470
- };
2471
- }
2472
- /** Advance the aggregate identity after proving manifest/root agreement at v16. */
2473
- function migrateAggregateV16ToV17(snapshot) {
2474
- return {
2475
- schemaManifest: {
2476
- ...snapshot.schemaManifest,
2477
- aggregateSchemaVersion: FINAL_REVIEW_AGGREGATE_TO_VERSION
2478
- },
2479
- state: snapshot.state === null
2480
- ? null
2481
- : {
2482
- ...snapshot.state,
2483
- schemaVersion: FINAL_REVIEW_AGGREGATE_TO_VERSION
2484
- }
2485
- };
2486
- }
2487
- function requireAggregateV16Snapshot(snapshot) {
2488
- if (snapshot.schemaManifest.aggregateSchemaVersion
2489
- !== FINAL_REVIEW_AGGREGATE_FROM_VERSION) {
2490
- throw new Error("Aggregate 16->17 migration requires schema.json aggregateSchemaVersion 16.");
2491
- }
2492
- if (snapshot.state !== null
2493
- && snapshot.state.schemaVersion !== FINAL_REVIEW_AGGREGATE_FROM_VERSION) {
2494
- throw new Error("Aggregate 16->17 migration requires state.json schemaVersion 16 to match schema.json.");
2495
- }
2496
- }
2497
- /**
2498
- * Introduce the durable Home identity. A v18 aggregate always carries one; a
2499
- * Home with no state.json yet gets its identity when its first state is
2500
- * written, so this step only mints one when state.json already exists.
2501
- */
2502
- function migrateAggregateV17ToV18(snapshot) {
2503
- const schemaManifest = {
2504
- ...snapshot.schemaManifest,
2505
- aggregateSchemaVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION
2506
- };
2507
- if (snapshot.state === null)
2508
- return { schemaManifest, state: null };
2509
- const state = snapshot.state;
2510
- if (state.homeIdentity !== undefined) {
2511
- throw new Error("Aggregate 17->18 migration found an unexpected homeIdentity.");
2512
- }
2513
- return {
2514
- schemaManifest,
2515
- state: {
2516
- ...state,
2517
- schemaVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION,
2518
- homeIdentity: generateHomeIdentity(new Date())
2519
- }
2520
- };
2521
- }
2522
- function requireAggregateV17Snapshot(snapshot) {
2523
- if (snapshot.schemaManifest.aggregateSchemaVersion
2524
- !== HOME_IDENTITY_AGGREGATE_FROM_VERSION) {
2525
- throw new Error("Aggregate 17->18 migration requires schema.json aggregateSchemaVersion 17.");
2526
- }
2527
- if (snapshot.state !== null
2528
- && snapshot.state.schemaVersion !== HOME_IDENTITY_AGGREGATE_FROM_VERSION) {
2529
- throw new Error("Aggregate 17->18 migration requires state.json schemaVersion 17 to match schema.json.");
2530
- }
2531
- }
2532
- /**
2533
- * Runtime state is now projected exclusively from canonical
2534
- * `runtime.observation` events. Offline upgrade inventory proves there are no
2535
- * active Runs or live Sessions. A retired Task has also explicitly abandoned
2536
- * its runtime, so its stored runtime inconsistencies no longer block the Home;
2537
- * the anomalous records themselves remain available as history. Non-retired
2538
- * Tasks still fail closed and receive the supported retirement command.
2539
- */
2540
- function migrateAggregateV18ToV19(snapshot) {
2541
- requireAggregateV18Snapshot(snapshot);
2542
- const schemaManifest = {
2543
- ...snapshot.schemaManifest,
2544
- aggregateSchemaVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION
2545
- };
2546
- if (snapshot.state === null)
2547
- return { schemaManifest, state: null };
2548
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2549
- const nextTasks = {};
2550
- for (const [taskId, rawStoredTask] of Object.entries(tasks)) {
2551
- const storedTask = asObject(rawStoredTask, `Task aggregate ${taskId}`);
2552
- const rawTask = storedTask.task;
2553
- const task = rawTask !== null && typeof rawTask === "object" && !Array.isArray(rawTask)
2554
- ? rawTask
2555
- : undefined;
2556
- if (task?.status === "retired") {
2557
- nextTasks[taskId] = { ...storedTask };
2558
- continue;
2559
- }
2560
- requireResolvableActiveRunPointers(taskId, storedTask);
2561
- nextTasks[taskId] = { ...storedTask };
2562
- }
2563
- return {
2564
- schemaManifest,
2565
- state: {
2566
- ...snapshot.state,
2567
- schemaVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION,
2568
- tasks: nextTasks
2569
- }
2570
- };
2571
- }
2572
- function requireResolvableActiveRunPointers(taskId, storedTask) {
2573
- if (storedTask.activeRuns === undefined)
2574
- return;
2575
- const activeRuns = asObject(storedTask.activeRuns, `activeRunPointer map ${taskId}`);
2576
- if (Object.keys(activeRuns).length === 0)
2577
- return;
2578
- const agentRuns = asObject(storedTask.agentRuns, `agentRun map ${taskId}`);
2579
- for (const [pointer, rawActiveRun] of Object.entries(activeRuns)) {
2580
- const activeRun = asObject(rawActiveRun, `Active run ${taskId}/${pointer}`);
2581
- const runId = typeof activeRun.runId === "string" ? activeRun.runId.trim() : "";
2582
- if (runId.length === 0) {
2583
- throw new Error(`Active run pointer ${taskId}/${pointer} has an invalid runId. `
2584
- + taskRetirementUpgradeHint(taskId));
2585
- }
2586
- if (agentRuns[runId] === undefined) {
2587
- throw new Error(`Active run pointer ${taskId}/${pointer} references missing agent run ${runId}. `
2588
- + taskRetirementUpgradeHint(taskId));
2589
- }
2590
- }
2591
- }
2592
- function taskRetirementUpgradeHint(taskId) {
2593
- return `Retire Task ${taskId} with `
2594
- + `\`yui task retire ${taskId} --summary "abandon inconsistent runtime state"\`, `
2595
- + "then retry `yui update`.";
2596
- }
2597
- function requireAggregateV18Snapshot(snapshot) {
2598
- if (snapshot.schemaManifest.aggregateSchemaVersion
2599
- !== RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION) {
2600
- throw new Error("Aggregate 18->19 migration requires schema.json aggregateSchemaVersion 18.");
2601
- }
2602
- if (snapshot.state !== null
2603
- && snapshot.state.schemaVersion !== RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION) {
2604
- throw new Error("Aggregate 18->19 migration requires state.json schemaVersion 18 to match schema.json.");
2605
- }
2606
- }
2607
- function requireAggregateV19Snapshot(snapshot) {
2608
- if (snapshot.schemaManifest.aggregateSchemaVersion
2609
- !== MULTI_AGENT_RUNTIME_AGGREGATE_FROM_VERSION) {
2610
- throw new Error("Aggregate 19->20 migration requires schema.json aggregateSchemaVersion 19.");
2611
- }
2612
- if (snapshot.state !== null
2613
- && snapshot.state.schemaVersion !== MULTI_AGENT_RUNTIME_AGGREGATE_FROM_VERSION) {
2614
- throw new Error("Aggregate 19->20 migration requires state.json schemaVersion 19 to match schema.json.");
2615
- }
2616
- }
2617
- function migrateAggregateV19ToV20(snapshot) {
2618
- requireAggregateV19Snapshot(snapshot);
2619
- const schemaManifest = {
2620
- ...snapshot.schemaManifest,
2621
- aggregateSchemaVersion: MULTI_AGENT_RUNTIME_AGGREGATE_TO_VERSION
2622
- };
2623
- if (snapshot.state === null)
2624
- return { schemaManifest, state: null };
2625
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2626
- const nextTasks = {};
2627
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
2628
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
2629
- const events = asObject(aggregate.events, `Task events ${taskId}`);
2630
- const nextEvents = {};
2631
- for (const [eventId, rawEvent] of Object.entries(events)) {
2632
- const event = asObject(rawEvent, `Task event ${taskId}/${eventId}`);
2633
- nextEvents[eventId] = event.type === "runtime.observation"
2634
- ? migrateRuntimeObservationEventV1ToV2(event, taskId, eventId)
2635
- : { ...event };
2636
- }
2637
- nextTasks[taskId] = { ...aggregate, events: nextEvents };
2638
- }
2639
- return {
2640
- schemaManifest,
2641
- state: {
2642
- ...snapshot.state,
2643
- schemaVersion: MULTI_AGENT_RUNTIME_AGGREGATE_TO_VERSION,
2644
- tasks: nextTasks
2645
- }
2646
- };
2647
- }
2648
- function migrateRuntimeObservationEventV1ToV2(event, taskId, eventId) {
2649
- const payload = asObject(event.payload, `Runtime observation event payload ${taskId}/${eventId}`);
2650
- if (typeof payload.observation !== "string") {
2651
- throw new Error(`Runtime observation event has no canonical observation: ${taskId}/${eventId}.`);
2652
- }
2653
- let raw;
2654
- try {
2655
- raw = JSON.parse(payload.observation);
2656
- }
2657
- catch {
2658
- throw new Error(`Runtime observation event JSON is invalid: ${taskId}/${eventId}.`);
2659
- }
2660
- const observation = asObject(raw, `Runtime observation ${taskId}/${eventId}`);
2661
- if (observation.schemaVersion !== 1) {
2662
- throw new Error(`Runtime observation ${taskId}/${eventId} must use schemaVersion 1.`);
2663
- }
2664
- const fence = asObject(observation.fence, `Runtime observation fence ${taskId}/${eventId}`);
2665
- const kind = String(observation.kind);
2666
- const semanticKey = migratedRuntimeObservationSemanticKey(observation, fence);
2667
- const nextObservation = {
2668
- ...observation,
2669
- schemaVersion: 2,
2670
- semanticKey,
2671
- fence: {
2672
- ...fence,
2673
- ...(fence.nativeSessionId === undefined
2674
- ? {}
2675
- : { conversationId: String(fence.nativeSessionId) }),
2676
- activationId: String(fence.launchId)
2677
- }
2678
- };
2679
- return {
2680
- ...event,
2681
- payload: {
2682
- ...payload,
2683
- semanticKey,
2684
- kind,
2685
- observation: JSON.stringify(nextObservation)
2686
- }
2687
- };
2688
- }
2689
- function migratedRuntimeObservationSemanticKey(observation, fence) {
2690
- const kind = String(observation.kind);
2691
- if (Number.isSafeInteger(observation.sequence)) {
2692
- return `provider-sequence:${String(fence.driverId)}:${String(fence.nativeSessionId ?? fence.launchId)}:${String(observation.sequence)}:${kind}`;
2693
- }
2694
- const terminal = [
2695
- "session.ended",
2696
- "session.failed",
2697
- "turn.completed",
2698
- "turn.failed",
2699
- "turn.cancelled"
2700
- ].includes(kind);
2701
- if (terminal) {
2702
- const payload = asObject(observation.payload, "Runtime observation payload");
2703
- const failure = payload.failure === undefined
2704
- ? undefined
2705
- : asObject(payload.failure, "Runtime observation failure");
2706
- return [
2707
- "terminal",
2708
- String(fence.driverId),
2709
- String(fence.nativeSessionId ?? fence.launchId),
2710
- String(fence.launchId),
2711
- String(fence.nativeTurnId ?? "none"),
2712
- kind,
2713
- String(payload.outcome ?? failure?.code ?? "terminal")
2714
- ].join(":");
2715
- }
2716
- return `provider-event:${String(observation.eventId)}`;
2717
- }
2718
- function workMailboxV2Step() {
2719
- return {
2720
- axis: "record",
2721
- recordKind: "workMailbox",
2722
- fromVersion: WORK_MAILBOX_FROM_VERSION,
2723
- toVersion: WORK_MAILBOX_TO_VERSION,
2724
- preconditions: requireWorkMailboxV1Family,
2725
- transform: migrateWorkMailboxV1ToV2,
2726
- declaredEffects: []
2727
- };
2728
- }
2729
- function requireWorkMailboxV1Family(snapshot) {
2730
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2731
- if (versions.workMailbox !== WORK_MAILBOX_FROM_VERSION) {
2732
- throw new Error(`Record workMailbox migration requires manifest version ${WORK_MAILBOX_FROM_VERSION}.`);
2733
- }
2734
- if (snapshot.state === null)
2735
- return;
2736
- const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
2737
- for (const [key, rawMailbox] of Object.entries(mailboxes)) {
2738
- const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
2739
- if (mailbox.schemaVersion !== WORK_MAILBOX_FROM_VERSION) {
2740
- throw new Error(`WorkMailbox ${key} must use schemaVersion ${WORK_MAILBOX_FROM_VERSION}.`);
2741
- }
2742
- if (mailbox.pending !== null)
2743
- asObject(mailbox.pending, `WorkMailbox ${key} pending`);
2744
- if (mailbox.processing !== null)
2745
- asObject(mailbox.processing, `WorkMailbox ${key} processing`);
2746
- }
2747
- }
2748
- function migrateWorkMailboxV1ToV2(snapshot) {
2749
- requireWorkMailboxV1Family(snapshot);
2750
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2751
- const schemaManifest = {
2752
- ...snapshot.schemaManifest,
2753
- recordVersions: { ...versions, workMailbox: WORK_MAILBOX_TO_VERSION }
2754
- };
2755
- if (snapshot.state === null)
2756
- return { schemaManifest, state: null };
2757
- const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
2758
- const nextMailboxes = {};
2759
- for (const [key, rawMailbox] of Object.entries(mailboxes)) {
2760
- const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
2761
- const pending = mailbox.pending === null
2762
- ? null
2763
- : migrateMailboxBatchV1(mailbox.pending, key, "pending");
2764
- const migratedProcessing = mailbox.processing === null
2765
- ? null
2766
- : migrateMailboxProcessingV1(mailbox.processing, key);
2767
- const continuationDelivery = migratedProcessing === null
2768
- ? null
2769
- : migrateMailboxContinuationDeliveryV1(snapshot, mailbox, migratedProcessing, key);
2770
- nextMailboxes[key] = {
2771
- ...mailbox,
2772
- schemaVersion: WORK_MAILBOX_TO_VERSION,
2773
- processing: continuationDelivery === null ? migratedProcessing : null,
2774
- pending: {
2775
- normal: pending,
2776
- userCorrection: null,
2777
- cursors: { normal: 0, userCorrection: 0 },
2778
- recentDedupeKeys: []
2779
- },
2780
- inputDelivery: continuationDelivery
2781
- };
2782
- }
2783
- return {
2784
- schemaManifest,
2785
- state: { ...snapshot.state, mailboxes: nextMailboxes }
2786
- };
2787
- }
2788
- /** A Host launch reservation belongs to the Role owner and launchId, never a Run. */
2789
- function hostOwnedWorkMailboxStep() {
2790
- return {
2791
- axis: "record",
2792
- recordKind: "workMailbox",
2793
- fromVersion: HOST_OWNED_WORK_MAILBOX_FROM_VERSION,
2794
- toVersion: HOST_OWNED_WORK_MAILBOX_TO_VERSION,
2795
- preconditions: requireWorkMailboxV2Family,
2796
- transform: migrateWorkMailboxV2ToV3,
2797
- declaredEffects: []
2798
- };
2799
- }
2800
- function requireWorkMailboxV2Family(snapshot) {
2801
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2802
- if (versions.workMailbox !== HOST_OWNED_WORK_MAILBOX_FROM_VERSION) {
2803
- throw new Error(`Record workMailbox migration requires manifest version ${HOST_OWNED_WORK_MAILBOX_FROM_VERSION}.`);
2804
- }
2805
- if (snapshot.state === null)
2806
- return;
2807
- const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
2808
- for (const [key, rawMailbox] of Object.entries(mailboxes)) {
2809
- const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
2810
- if (mailbox.schemaVersion !== HOST_OWNED_WORK_MAILBOX_FROM_VERSION) {
2811
- throw new Error(`WorkMailbox ${key} must use schemaVersion ${HOST_OWNED_WORK_MAILBOX_FROM_VERSION}.`);
2812
- }
2813
- }
2814
- }
2815
- function migrateWorkMailboxV2ToV3(snapshot) {
2816
- requireWorkMailboxV2Family(snapshot);
2817
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2818
- const schemaManifest = {
2819
- ...snapshot.schemaManifest,
2820
- recordVersions: { ...versions, workMailbox: HOST_OWNED_WORK_MAILBOX_TO_VERSION }
2821
- };
2822
- if (snapshot.state === null)
2823
- return { schemaManifest, state: null };
2824
- const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
2825
- const nextMailboxes = {};
2826
- for (const [key, rawMailbox] of Object.entries(mailboxes)) {
2827
- const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
2828
- const target = asObject(mailbox.target, `WorkMailbox ${key} target`);
2829
- const processing = mailbox.processing === null
2830
- ? null
2831
- : asObject(mailbox.processing, `WorkMailbox ${key} processing`);
2832
- const batch = processing === null
2833
- ? null
2834
- : asObject(processing.batch, `WorkMailbox ${key} processing batch`);
2835
- const lifecycleReservation = (target.kind === "role-runtime"
2836
- || target.kind === "global-role-runtime")
2837
- && Array.isArray(batch?.reasons)
2838
- && batch.reasons.includes("runtime-launch-reserved");
2839
- const nextProcessing = processing === null
2840
- ? null
2841
- : lifecycleReservation
2842
- ? (() => {
2843
- const { executionRef: _run, ...retained } = processing;
2844
- void _run;
2845
- return retained;
2846
- })()
2847
- : processing;
2848
- nextMailboxes[key] = {
2849
- ...mailbox,
2850
- schemaVersion: HOST_OWNED_WORK_MAILBOX_TO_VERSION,
2851
- processing: nextProcessing
2852
- };
2853
- }
2854
- return {
2855
- schemaManifest,
2856
- state: { ...snapshot.state, mailboxes: nextMailboxes }
2857
- };
2858
- }
2859
- /**
2860
- * v1 used `processing` for both internal/initial dispatch ownership and later
2861
- * model input. A crashed later input may already have reached the provider;
2862
- * migrate it to an explicit delivery-unknown fence instead of making it
2863
- * eligible for a blind resend. Other processing batches retain their old
2864
- * controller ownership semantics.
2865
- */
2866
- function migrateMailboxContinuationDeliveryV1(snapshot, mailbox, processing, mailboxKey) {
2867
- const batchId = String(processing.batchId ?? "");
2868
- if (!batchId.startsWith("agent-input:"))
2869
- return null;
2870
- const target = asObject(mailbox.target, `WorkMailbox ${mailboxKey} target`);
2871
- if (target.kind !== "role") {
2872
- throw new Error(`WorkMailbox ${mailboxKey} continuation input requires a Role target.`);
2873
- }
2874
- const executionRef = asObject(processing.executionRef, `WorkMailbox ${mailboxKey} continuation executionRef`);
2875
- if (executionRef.type !== "run") {
2876
- throw new Error(`WorkMailbox ${mailboxKey} continuation input requires a Run reference.`);
2877
- }
2878
- const taskId = String(target.taskId);
2879
- const roleName = String(target.roleName);
2880
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2881
- const aggregate = asObject(tasks[taskId], `Task aggregate ${taskId}`);
2882
- const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
2883
- const set = asObject(sets[roleName], `Task Role session set ${taskId}/${roleName}`);
2884
- const sessions = asObject(set.sessions, `Task Role sessions ${taskId}/${roleName}`);
2885
- const active = asObject(sessions[String(set.activeAgentId)], `Task Role active Session ${taskId}/${roleName}`);
2886
- const activationId = String(active.launchId ?? asObject(set.inFlight, `Task Role in-flight Run ${taskId}/${roleName}`).receiptId);
2887
- return {
2888
- attemptId: batchId,
2889
- lane: "normal",
2890
- mode: "followup",
2891
- batch: processing.batch,
2892
- owner: processing.owner,
2893
- status: "delivery-unknown",
2894
- startedAt: processing.startedAt,
2895
- pushedAt: processing.startedAt,
2896
- unknownReason: "migrated-unconfirmed-provider-acceptance",
2897
- executionRef,
2898
- providerFence: {
2899
- conversationId: String(active.nativeSessionId),
2900
- activationId
2901
- }
2902
- };
2903
- }
2904
- function migrateMailboxProcessingV1(value, mailboxKey) {
2905
- const processing = asObject(value, `WorkMailbox ${mailboxKey} processing`);
2906
- return {
2907
- ...processing,
2908
- lane: "normal",
2909
- batch: migrateMailboxBatchV1(processing.batch, mailboxKey, "processing")
2910
- };
2911
- }
2912
- function migrateMailboxBatchV1(value, mailboxKey, location) {
2913
- const batch = asObject(value, `WorkMailbox ${mailboxKey} ${location} batch`);
2914
- const from = batch.fromSequence;
2915
- const to = batch.toSequence;
2916
- if (!Number.isInteger(from) || !Number.isInteger(to)) {
2917
- throw new Error(`WorkMailbox ${mailboxKey} ${location} sequence range is invalid.`);
2918
- }
2919
- return {
2920
- ...batch,
2921
- sources: ["migration-v1"],
2922
- dedupeKeys: [`mailbox-v1:${mailboxKey}:${String(from)}-${String(to)}`],
2923
- deliveryModes: ["followup"]
2924
- };
2925
- }
2926
- function taskRoleSessionSetV5Step() {
2927
- return {
2928
- axis: "record",
2929
- recordKind: "taskRoleSessionSet",
2930
- fromVersion: TASK_ROLE_SESSION_SET_FROM_VERSION,
2931
- toVersion: TASK_ROLE_SESSION_SET_TO_VERSION,
2932
- preconditions: requireTaskRoleSessionSetV4Family,
2933
- transform: migrateTaskRoleSessionSetV4ToV5,
2934
- declaredEffects: []
2935
- };
2936
- }
2937
- /**
2938
- * v6 is a deliberate runtime cutover, not an emulation layer. Existing Task,
2939
- * Run, Conversation, and Session identities remain as audit evidence, while
2940
- * every pre-v6 managed process is terminalized locally. A new Agent Host must
2941
- * establish structured Provider evidence before any further write.
2942
- */
2943
- function structuredProviderSessionSetV6Step() {
2944
- return {
2945
- axis: "record",
2946
- recordKind: "taskRoleSessionSet",
2947
- fromVersion: STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION,
2948
- toVersion: STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION,
2949
- preconditions: requireTaskRoleSessionSetV5Family,
2950
- transform: migrateTaskRoleSessionSetV5ToV6,
2951
- declaredEffects: []
2952
- };
2953
- }
2954
- /**
2955
- * A Provider binding describes one reusable Conversation. Run correlation now
2956
- * belongs to the concrete Provider Turn instead of making the Conversation a
2957
- * second writable projection of the current AgentRun.
2958
- */
2959
- function agentFirstSessionSetV7Step() {
2960
- return {
2961
- axis: "record",
2962
- recordKind: "taskRoleSessionSet",
2963
- fromVersion: AGENT_FIRST_SESSION_SET_FROM_VERSION,
2964
- toVersion: AGENT_FIRST_SESSION_SET_TO_VERSION,
2965
- preconditions: requireTaskRoleSessionSetV6Family,
2966
- transform: migrateTaskRoleSessionSetV6ToV7,
2967
- declaredEffects: []
2968
- };
2969
- }
2970
- function requireTaskRoleSessionSetV6Family(snapshot) {
2971
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2972
- if (versions.taskRoleSessionSet !== AGENT_FIRST_SESSION_SET_FROM_VERSION) {
2973
- throw new Error(`Record taskRoleSessionSet migration requires manifest version ${AGENT_FIRST_SESSION_SET_FROM_VERSION}.`);
2974
- }
2975
- if (snapshot.state === null)
2976
- return;
2977
- const tasks = asObject(snapshot.state.tasks, "state tasks");
2978
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
2979
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
2980
- const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
2981
- for (const [roleName, rawSet] of Object.entries(sets)) {
2982
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
2983
- if (set.schemaVersion !== AGENT_FIRST_SESSION_SET_FROM_VERSION) {
2984
- throw new Error(`Task Role session set ${taskId}/${roleName} must use schemaVersion ${AGENT_FIRST_SESSION_SET_FROM_VERSION}.`);
2985
- }
2986
- if (set.providerBinding !== null) {
2987
- const binding = asObject(set.providerBinding, `Provider Binding ${taskId}/${roleName}`);
2988
- if (binding.schemaVersion !== 2 || typeof binding.runId !== "string") {
2989
- throw new Error(`Provider Binding ${taskId}/${roleName} must use schemaVersion 2.`);
2990
- }
2991
- }
2992
- }
2993
- }
2994
- }
2995
- function migrateTaskRoleSessionSetV6ToV7(snapshot) {
2996
- requireTaskRoleSessionSetV6Family(snapshot);
2997
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2998
- const schemaManifest = {
2999
- ...snapshot.schemaManifest,
3000
- recordVersions: {
3001
- ...versions,
3002
- taskRoleSessionSet: AGENT_FIRST_SESSION_SET_TO_VERSION
3003
- }
3004
- };
3005
- if (snapshot.state === null)
3006
- return { schemaManifest, state: null };
3007
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3008
- const nextTasks = {};
3009
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3010
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3011
- const rawSets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3012
- const nextSets = {};
3013
- for (const [roleName, rawSet] of Object.entries(rawSets)) {
3014
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3015
- let providerBinding = null;
3016
- if (set.providerBinding !== null) {
3017
- const binding = asObject(set.providerBinding, `Provider Binding ${taskId}/${roleName}`);
3018
- const { runId, ...conversation } = binding;
3019
- providerBinding = {
3020
- ...conversation,
3021
- schemaVersion: 3,
3022
- turn: binding.turn === null
3023
- ? null
3024
- : { ...asObject(binding.turn, `Provider Turn ${taskId}/${roleName}`), runId }
3025
- };
3026
- }
3027
- nextSets[roleName] = {
3028
- ...set,
3029
- schemaVersion: AGENT_FIRST_SESSION_SET_TO_VERSION,
3030
- providerBinding
3031
- };
3032
- }
3033
- nextTasks[taskId] = { ...aggregate, roleSessionSets: nextSets };
3034
- }
3035
- return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
3036
- }
3037
- /** Session stores existence only; Host readiness and Turn activity are runtime facts. */
3038
- function unifiedTaskSessionSetStep() {
3039
- return {
3040
- axis: "record",
3041
- recordKind: "taskRoleSessionSet",
3042
- fromVersion: UNIFIED_TASK_SESSION_SET_FROM_VERSION,
3043
- toVersion: UNIFIED_TASK_SESSION_SET_TO_VERSION,
3044
- preconditions: requireTaskRoleSessionSetV7Family,
3045
- transform: migrateTaskRoleSessionSetV7ToV8,
3046
- declaredEffects: []
3047
- };
3048
- }
3049
- function requireTaskRoleSessionSetV7Family(snapshot) {
3050
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3051
- if (versions.taskRoleSessionSet !== UNIFIED_TASK_SESSION_SET_FROM_VERSION) {
3052
- throw new Error(`Record taskRoleSessionSet migration requires manifest version ${UNIFIED_TASK_SESSION_SET_FROM_VERSION}.`);
3053
- }
3054
- if (snapshot.state === null)
3055
- return;
3056
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3057
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3058
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3059
- const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3060
- for (const [roleName, rawSet] of Object.entries(sets)) {
3061
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3062
- if (set.schemaVersion !== UNIFIED_TASK_SESSION_SET_FROM_VERSION) {
3063
- throw new Error(`Task Role session set ${taskId}/${roleName} must use schemaVersion ${UNIFIED_TASK_SESSION_SET_FROM_VERSION}.`);
3064
- }
3065
- }
3066
- }
3067
- }
3068
- function migrateTaskRoleSessionSetV7ToV8(snapshot) {
3069
- requireTaskRoleSessionSetV7Family(snapshot);
3070
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3071
- const schemaManifest = {
3072
- ...snapshot.schemaManifest,
3073
- recordVersions: {
3074
- ...versions,
3075
- taskRoleSessionSet: UNIFIED_TASK_SESSION_SET_TO_VERSION
3076
- }
3077
- };
3078
- if (snapshot.state === null)
3079
- return { schemaManifest, state: null };
3080
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3081
- const nextTasks = {};
3082
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3083
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3084
- const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3085
- const nextSets = {};
3086
- for (const [roleName, rawSet] of Object.entries(sets)) {
3087
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3088
- const sessions = migrateRoleAgentSessionMap(set.sessions, `Task Role sessions ${taskId}/${roleName}`);
3089
- const history = set.history === undefined
3090
- ? undefined
3091
- : migrateRoleAgentSessionArray(set.history, `Task Role session history ${taskId}/${roleName}`);
3092
- nextSets[roleName] = {
3093
- ...set,
3094
- schemaVersion: UNIFIED_TASK_SESSION_SET_TO_VERSION,
3095
- sessions,
3096
- ...(history === undefined ? {} : { history })
3097
- };
3098
- }
3099
- nextTasks[taskId] = { ...aggregate, roleSessionSets: nextSets };
3100
- }
3101
- return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
3102
- }
3103
- function unifiedGlobalSessionSetStep() {
3104
- return {
3105
- axis: "record",
3106
- recordKind: "globalRoleSessionSet",
3107
- fromVersion: UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION,
3108
- toVersion: UNIFIED_GLOBAL_SESSION_SET_TO_VERSION,
3109
- preconditions: requireGlobalRoleSessionSetV3Family,
3110
- transform: migrateGlobalRoleSessionSetV3ToV4,
3111
- declaredEffects: []
3112
- };
3113
- }
3114
- function requireGlobalRoleSessionSetV3Family(snapshot) {
3115
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3116
- if (versions.globalRoleSessionSet !== UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION) {
3117
- throw new Error(`Record globalRoleSessionSet migration requires manifest version ${UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION}.`);
3118
- }
3119
- if (snapshot.state === null)
3120
- return;
3121
- const sets = asObject(snapshot.state.globalRoleSessionSets, "Global Role session sets");
3122
- for (const [roleName, rawSet] of Object.entries(sets)) {
3123
- const set = asObject(rawSet, `Global Role session set ${roleName}`);
3124
- if (set.schemaVersion !== UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION) {
3125
- throw new Error(`Global Role session set ${roleName} must use schemaVersion ${UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION}.`);
3126
- }
3127
- }
3128
- }
3129
- function migrateGlobalRoleSessionSetV3ToV4(snapshot) {
3130
- requireGlobalRoleSessionSetV3Family(snapshot);
3131
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3132
- const schemaManifest = {
3133
- ...snapshot.schemaManifest,
3134
- recordVersions: {
3135
- ...versions,
3136
- globalRoleSessionSet: UNIFIED_GLOBAL_SESSION_SET_TO_VERSION
3137
- }
3138
- };
3139
- if (snapshot.state === null)
3140
- return { schemaManifest, state: null };
3141
- const sets = asObject(snapshot.state.globalRoleSessionSets, "Global Role session sets");
3142
- const nextSets = {};
3143
- for (const [roleName, rawSet] of Object.entries(sets)) {
3144
- const set = asObject(rawSet, `Global Role session set ${roleName}`);
3145
- const history = set.history === undefined
3146
- ? undefined
3147
- : migrateRoleAgentSessionMap(set.history, `Global Role session history ${roleName}`);
3148
- nextSets[roleName] = {
3149
- ...set,
3150
- schemaVersion: UNIFIED_GLOBAL_SESSION_SET_TO_VERSION,
3151
- sessions: migrateRoleAgentSessionMap(set.sessions, `Global Role sessions ${roleName}`),
3152
- ...(history === undefined ? {} : { history })
3153
- };
3154
- }
3155
- return {
3156
- schemaManifest,
3157
- state: { ...snapshot.state, globalRoleSessionSets: nextSets }
3158
- };
3159
- }
3160
- function migrateRoleAgentSessionMap(value, label) {
3161
- const sessions = asObject(value, label);
3162
- return Object.fromEntries(Object.entries(sessions).map(([key, session]) => [
3163
- key,
3164
- migrateRoleAgentSessionV3(session, `${label}/${key}`)
3165
- ]));
3166
- }
3167
- function migrateRoleAgentSessionArray(value, label) {
3168
- if (!Array.isArray(value))
3169
- throw new Error(`${label} must be an array.`);
3170
- return value.map((session, index) => migrateRoleAgentSessionV3(session, `${label}/${String(index)}`));
3171
- }
3172
- function migrateRoleAgentSessionV3(value, label) {
3173
- const session = asObject(value, label);
3174
- if (session.schemaVersion !== 3) {
3175
- throw new Error(`${label} must use schemaVersion 3.`);
3176
- }
3177
- const status = session.status;
3178
- if (status !== "reserved"
3179
- && status !== "ready"
3180
- && status !== "running"
3181
- && status !== "stopped"
3182
- && status !== "broken") {
3183
- throw new Error(`${label} has an invalid Session status.`);
3184
- }
3185
- const { status: _status, ...retained } = session;
3186
- void _status;
3187
- return {
3188
- ...retained,
3189
- schemaVersion: 4,
3190
- status: status === "stopped" || status === "broken" ? "ended" : "active",
3191
- ...(status === "stopped"
3192
- ? { endReason: "stopped" }
3193
- : status === "broken" ? { endReason: "failed" } : {})
3194
- };
3195
- }
3196
- function requireTaskRoleSessionSetV5Family(snapshot) {
3197
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3198
- if (versions.taskRoleSessionSet !== STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION) {
3199
- throw new Error(`Record taskRoleSessionSet migration requires manifest version ${STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION}.`);
3200
- }
3201
- if (snapshot.state === null)
3202
- return;
3203
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3204
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3205
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3206
- const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3207
- for (const [roleName, rawSet] of Object.entries(sets)) {
3208
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3209
- if (set.schemaVersion !== STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION) {
3210
- throw new Error(`Task Role session set ${taskId}/${roleName} must use schemaVersion ${STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION}.`);
3211
- }
3212
- }
3213
- }
3214
- }
3215
- function migrateTaskRoleSessionSetV5ToV6(snapshot) {
3216
- requireTaskRoleSessionSetV5Family(snapshot);
3217
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3218
- const schemaManifest = {
3219
- ...snapshot.schemaManifest,
3220
- recordVersions: {
3221
- ...versions,
3222
- taskRoleSessionSet: STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION
3223
- }
3224
- };
3225
- if (snapshot.state === null)
3226
- return { schemaManifest, state: null };
3227
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3228
- const nextTasks = {};
3229
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3230
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3231
- const rawSets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3232
- const nextSets = {};
3233
- for (const [roleName, rawSet] of Object.entries(rawSets)) {
3234
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3235
- const invalidatedAt = String(set.updatedAt);
3236
- const sessions = asObject(set.sessions, `Task Role sessions ${taskId}/${roleName}`);
3237
- const nextSessions = Object.fromEntries(Object.entries(sessions).map(([agentId, rawSession]) => {
3238
- const session = asObject(rawSession, `Task Role Session ${taskId}/${roleName}/${agentId}`);
3239
- return [agentId, session.status === "stopped" || session.status === "broken"
3240
- ? session
3241
- : { ...session, status: "broken", updatedAt: invalidatedAt }];
3242
- }));
3243
- nextSets[roleName] = {
3244
- ...set,
3245
- schemaVersion: STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION,
3246
- sessions: nextSessions,
3247
- providerBinding: set.providerBinding === null
3248
- ? null
3249
- : invalidateLegacyProviderBinding(asObject(set.providerBinding, `Provider Binding ${taskId}/${roleName}`), invalidatedAt)
3250
- };
3251
- }
3252
- nextTasks[taskId] = { ...aggregate, roleSessionSets: nextSets };
3253
- }
3254
- return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
3255
- }
3256
- function invalidateLegacyProviderBinding(binding, invalidatedAt) {
3257
- if (binding.schemaVersion !== 1) {
3258
- throw new Error("Pre-v6 Provider Runtime Binding must use schemaVersion 1.");
3259
- }
3260
- const activations = Array.isArray(binding.activations) ? binding.activations : [];
3261
- return {
3262
- ...binding,
3263
- schemaVersion: 2,
3264
- conversations: (Array.isArray(binding.conversations) ? binding.conversations : []).map((rawConversation) => {
3265
- const conversation = asObject(rawConversation, "Provider Conversation");
3266
- return conversation.status === "current"
3267
- ? { ...conversation, recoverability: "unknown" }
3268
- : conversation;
3269
- }),
3270
- activations: activations.map((rawActivation) => {
3271
- const activation = asObject(rawActivation, "Provider Activation");
3272
- const { writerLease: _removed, ...withoutLease } = activation;
3273
- void _removed;
3274
- return activation.status === "active"
3275
- ? {
3276
- ...withoutLease,
3277
- status: "failed",
3278
- endedAt: invalidatedAt,
3279
- terminalReason: "legacy-managed-runtime-invalidated"
3280
- }
3281
- : withoutLease;
3282
- }),
3283
- authority: { epoch: 1, owner: "none", changedAt: invalidatedAt },
3284
- turn: null
3285
- };
3286
- }
3287
- function requireTaskRoleSessionSetV4Family(snapshot) {
3288
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3289
- if (versions.taskRoleSessionSet !== TASK_ROLE_SESSION_SET_FROM_VERSION) {
3290
- throw new Error(`Record taskRoleSessionSet migration requires manifest version ${TASK_ROLE_SESSION_SET_FROM_VERSION}.`);
3291
- }
3292
- if (snapshot.state === null)
3293
- return;
3294
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3295
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3296
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3297
- const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3298
- for (const [roleName, rawSet] of Object.entries(sets)) {
3299
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3300
- if (set.schemaVersion !== TASK_ROLE_SESSION_SET_FROM_VERSION) {
3301
- throw new Error(`Task Role session set ${taskId}/${roleName} must use schemaVersion ${TASK_ROLE_SESSION_SET_FROM_VERSION}.`);
3302
- }
3303
- }
3304
- }
3305
- }
3306
- function migrateTaskRoleSessionSetV4ToV5(snapshot) {
3307
- requireTaskRoleSessionSetV4Family(snapshot);
3308
- const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3309
- const schemaManifest = {
3310
- ...snapshot.schemaManifest,
3311
- recordVersions: {
3312
- ...versions,
3313
- taskRoleSessionSet: TASK_ROLE_SESSION_SET_TO_VERSION
3314
- }
3315
- };
3316
- if (snapshot.state === null)
3317
- return { schemaManifest, state: null };
3318
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3319
- const nextTasks = {};
3320
- for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3321
- const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3322
- const rawSets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3323
- const nextSets = {};
3324
- let events = { ...asObject(aggregate.events, `Task events ${taskId}`) };
3325
- const highWater = { ...asObject(aggregate.idHighWaterMarks, `Task id high-water marks ${taskId}`) };
3326
- let eventHighWater = Number(highWater.event ?? 0);
3327
- if (!Number.isSafeInteger(eventHighWater) || eventHighWater < 0) {
3328
- throw new Error(`Task event high-water mark is invalid: ${taskId}.`);
3329
- }
3330
- for (const [roleName, rawSet] of Object.entries(rawSets)) {
3331
- const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3332
- const sessions = asObject(set.sessions, `Task Role sessions ${taskId}/${roleName}`);
3333
- const inFlight = set.inFlight === null
3334
- ? null
3335
- : asObject(set.inFlight, `Task Role in-flight Run ${taskId}/${roleName}`);
3336
- const pending = set.pendingTurnCompletion === null
3337
- ? null
3338
- : asObject(set.pendingTurnCompletion, `Task Role Turn completion ${taskId}/${roleName}`);
3339
- const activeAgentId = String(set.activeAgentId ?? "");
3340
- const activeSession = sessions[activeAgentId] === undefined
3341
- ? null
3342
- : asObject(sessions[activeAgentId], `Task Role active Session ${taskId}/${roleName}`);
3343
- if (inFlight !== null && activeSession === null) {
3344
- throw new Error(`Task Role in-flight Run has no active Session: ${taskId}/${roleName}.`);
3345
- }
3346
- const nextSessions = { ...sessions };
3347
- if (pending !== null) {
3348
- if (activeSession === null
3349
- || pending.agentId !== activeAgentId
3350
- || pending.runId !== inFlight?.runId
3351
- || pending.nativeSessionId !== activeSession.nativeSessionId) {
3352
- throw new Error(`Task Role Turn completion identity is inconsistent: ${taskId}/${roleName}.`);
3353
- }
3354
- const recent = Array.isArray(activeSession.recentCompletedTurnIds)
3355
- ? activeSession.recentCompletedTurnIds.map(String)
3356
- : [];
3357
- const turnId = String(pending.turnId);
3358
- nextSessions[activeAgentId] = {
3359
- ...activeSession,
3360
- recentCompletedTurnIds: recent.includes(turnId) ? recent : [...recent, turnId],
3361
- updatedAt: String(pending.observedAt)
3362
- };
3363
- eventHighWater += 1;
3364
- const eventId = `event-${eventHighWater}`;
3365
- const driverId = migratedDriverId(String(activeSession.adapterId));
3366
- const launchId = String(activeSession.launchId ?? inFlight?.receiptId ?? `legacy-${eventId}`);
3367
- const observation = {
3368
- schemaVersion: 2,
3369
- eventId: `migrated-turn-completed:${taskId}:${roleName}:${turnId}`,
3370
- semanticKey: [
3371
- "terminal",
3372
- driverId,
3373
- activeAgentId,
3374
- String(activeSession.nativeSessionId),
3375
- launchId,
3376
- "none",
3377
- "none",
3378
- turnId,
3379
- "turn.completed",
3380
- "terminal",
3381
- "none",
3382
- "turn-terminal"
3383
- ].join(":"),
3384
- kind: "turn.completed",
3385
- authority: "controller",
3386
- receivedAt: String(pending.observedAt),
3387
- observedAt: String(pending.observedAt),
3388
- fence: {
3389
- taskId,
3390
- roleName,
3391
- runId: String(pending.runId),
3392
- agentId: activeAgentId,
3393
- driverId,
3394
- launchId,
3395
- sessionGenerationId: launchId,
3396
- conversationId: String(activeSession.nativeSessionId),
3397
- activationId: launchId,
3398
- nativeSessionId: String(activeSession.nativeSessionId),
3399
- nativeTurnId: turnId
3400
- },
3401
- payload: { summary: String(pending.summary) }
3402
- };
3403
- events[eventId] = {
3404
- schemaVersion: 2,
3405
- id: eventId,
3406
- taskId,
3407
- type: "runtime.observation",
3408
- payload: {
3409
- eventId: observation.eventId,
3410
- roleName,
3411
- agentId: activeAgentId,
3412
- driverId,
3413
- launchId,
3414
- taskId,
3415
- runId: String(pending.runId),
3416
- nativeSessionId: String(activeSession.nativeSessionId),
3417
- kind: "turn.completed",
3418
- receivedAt: String(pending.observedAt),
3419
- observation: JSON.stringify(observation)
3420
- },
3421
- createdAt: String(pending.observedAt)
3422
- };
3423
- }
3424
- const providerBinding = inFlight === null || activeSession === null
3425
- ? null
3426
- : migratedProviderBinding(activeSession, inFlight);
3427
- const { pendingTurnCompletion: _removed, ...withoutPending } = set;
3428
- void _removed;
3429
- nextSets[roleName] = {
3430
- ...withoutPending,
3431
- schemaVersion: TASK_ROLE_SESSION_SET_TO_VERSION,
3432
- sessions: nextSessions,
3433
- providerBinding
3434
- };
3435
- }
3436
- highWater.event = eventHighWater;
3437
- nextTasks[taskId] = {
3438
- ...aggregate,
3439
- roleSessionSets: nextSets,
3440
- events,
3441
- idHighWaterMarks: highWater
3442
- };
3443
- }
3444
- return {
3445
- schemaManifest,
3446
- state: { ...snapshot.state, tasks: nextTasks }
3447
- };
3448
- }
3449
- function migratedProviderBinding(session, inFlight) {
3450
- const conversationId = String(session.nativeSessionId);
3451
- const activationId = String(session.launchId ?? inFlight.receiptId);
3452
- const startedAt = String(inFlight.preparedAt);
3453
- const live = session.status !== "stopped" && session.status !== "broken";
3454
- return {
3455
- schemaVersion: 1,
3456
- providerNamespace: migratedDriverId(String(session.adapterId)),
3457
- accountScope: String(session.agentId),
3458
- runId: String(inFlight.runId),
3459
- currentConversationEpoch: 1,
3460
- conversations: [{
3461
- conversationId,
3462
- epoch: 1,
3463
- status: "current",
3464
- recoverability: "unknown",
3465
- createdAt: String(session.createdAt ?? startedAt)
3466
- }],
3467
- activations: [{
3468
- activationId,
3469
- conversationId,
3470
- generation: 1,
3471
- status: live ? "active" : session.status === "broken" ? "failed" : "ended",
3472
- writerLease: live,
3473
- startedAt,
3474
- ...(live ? {} : { endedAt: String(session.updatedAt) })
3475
- }]
3476
- };
3477
- }
3478
- function migratedDriverId(adapterId) {
3479
- if (adapterId === "claude")
3480
- return "anthropic/claude-code";
3481
- if (adapterId === "codex")
3482
- return "openai/codex";
3483
- return adapterId.includes("/") ? adapterId : `legacy/${adapterId}`;
3484
- }
3485
- /**
3486
- * Project ownership is a new required field. Every pre-v3 Project is a
3487
- * user-registered checkout, so the historical binding is `external`; a managed
3488
- * Home-owned repository is only ever created explicitly by the new binding
3489
- * path. The version transition is durable and centralized.
3490
- */
3491
- function projectOwnershipStep() {
3492
- return {
3493
- axis: "record",
3494
- recordKind: "project",
3495
- fromVersion: PROJECT_FROM_VERSION,
3496
- toVersion: PROJECT_TO_VERSION,
3497
- preconditions: requireProjectV2Family,
3498
- transform: migrateProjectV2ToV3,
3499
- declaredEffects: []
3500
- };
3501
- }
3502
- function requireProjectV2Family(snapshot) {
3503
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3504
- if (manifestVersions.project !== PROJECT_FROM_VERSION) {
3505
- throw new Error(`Record project migration requires manifest version ${PROJECT_FROM_VERSION}.`);
3506
- }
3507
- if (snapshot.state === null)
3508
- return;
3509
- const projects = snapshot.state.projects;
3510
- if (projects === undefined)
3511
- return;
3512
- const map = asObject(projects, "project map");
3513
- for (const [projectId, rawProject] of Object.entries(map)) {
3514
- const project = asObject(rawProject, `Project ${projectId}`);
3515
- if (project.schemaVersion !== PROJECT_FROM_VERSION) {
3516
- throw new Error(`Project ${projectId} must use schemaVersion ${PROJECT_FROM_VERSION}.`);
3517
- }
3518
- }
3519
- }
3520
- function migrateProjectV2ToV3(snapshot) {
3521
- requireProjectV2Family(snapshot);
3522
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3523
- const schemaManifest = {
3524
- ...snapshot.schemaManifest,
3525
- recordVersions: { ...manifestVersions, project: PROJECT_TO_VERSION }
3526
- };
3527
- if (snapshot.state === null)
3528
- return { schemaManifest, state: null };
3529
- const projects = snapshot.state.projects;
3530
- if (projects === undefined) {
3531
- return { schemaManifest, state: { ...snapshot.state } };
3532
- }
3533
- const map = asObject(projects, "project map");
3534
- const nextProjects = {};
3535
- for (const [projectId, rawProject] of Object.entries(map)) {
3536
- const project = asObject(rawProject, `Project ${projectId}`);
3537
- nextProjects[projectId] = {
3538
- ...project,
3539
- schemaVersion: PROJECT_TO_VERSION,
3540
- ownership: "external"
3541
- };
3542
- }
3543
- return {
3544
- schemaManifest,
3545
- state: { ...snapshot.state, projects: nextProjects }
3546
- };
3547
- }
3548
- /**
3549
- * Project v4 adds the `knowledgeProposals` workflow list (Leader-proposed
3550
- * Knowledge candidates awaiting an Operator decision). Every v3 Project
3551
- * defaults to an empty list; the compatible normalizer adds it in place
3552
- * without rewriting any other field.
3553
- */
3554
- function projectKnowledgeProposalsStep() {
3555
- return {
3556
- kind: "compatible",
3557
- axis: "record",
3558
- recordKind: "project",
3559
- fromVersion: PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION,
3560
- toVersion: PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION,
3561
- defaults: [
3562
- "knowledgeProposals defaults to an empty list on every Project"
3563
- ],
3564
- validateSource: (snapshot) => requireProjectV3Shape(snapshot),
3565
- normalize: (snapshot) => normalizeProjectV3ToV4(snapshot)
3566
- };
3567
- }
3568
- function requireProjectV3Shape(snapshot) {
3569
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3570
- if (manifestVersions.project !== PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION) {
3571
- throw new Error(`Record project compatible step requires manifest version ${PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION}.`);
3572
- }
3573
- if (snapshot.state === null)
3574
- return;
3575
- const projects = snapshot.state.projects;
3576
- if (projects === undefined)
3577
- return;
3578
- const map = asObject(projects, "project map");
3579
- const allowed = new Set(PROJECT_V3_FIELDS);
3580
- for (const [projectId, rawProject] of Object.entries(map)) {
3581
- const project = asObject(rawProject, `Project ${projectId}`);
3582
- if (project.schemaVersion !== PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION) {
3583
- throw new Error(`Project ${projectId} must use schemaVersion ${PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION}.`);
3584
- }
3585
- const unknown = Object.keys(project).find((key) => !allowed.has(key));
3586
- if (unknown !== undefined) {
3587
- throw new Error(`Project ${projectId} has an unknown v3 field: ${unknown}.`);
3588
- }
3589
- }
3590
- }
3591
- const PROJECT_V3_FIELDS = [
3592
- "schemaVersion",
3593
- "id",
3594
- "name",
3595
- "aliases",
3596
- "path",
3597
- "ownership",
3598
- "remoteUrl",
3599
- "stableBranch",
3600
- "developmentBranch",
3601
- "knowledge",
3602
- "createdAt",
3603
- "updatedAt"
3604
- ];
3605
- function normalizeProjectV3ToV4(snapshot) {
3606
- requireProjectV3Shape(snapshot);
3607
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3608
- const schemaManifest = {
3609
- ...snapshot.schemaManifest,
3610
- recordVersions: { ...manifestVersions, project: PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION }
3611
- };
3612
- if (snapshot.state === null)
3613
- return { schemaManifest, state: null };
3614
- const projects = snapshot.state.projects;
3615
- if (projects === undefined) {
3616
- return { schemaManifest, state: { ...snapshot.state } };
3617
- }
3618
- const map = asObject(projects, "project map");
3619
- const nextProjects = {};
3620
- for (const [projectId, rawProject] of Object.entries(map)) {
3621
- const project = asObject(rawProject, `Project ${projectId}`);
3622
- nextProjects[projectId] = {
3623
- ...project,
3624
- schemaVersion: PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION,
3625
- knowledgeProposals: []
3626
- };
3627
- }
3628
- return {
3629
- schemaManifest,
3630
- state: { ...snapshot.state, projects: nextProjects }
3631
- };
3632
- }
3633
- /**
3634
- * Project v5 adds the lifecycle status (`active`/`retired`) plus the
3635
- * retirement audit record. Historical Projects are all active; the step only
3636
- * defaults the new field, so it is a compatible adjacent migration.
3637
- */
3638
- function projectLifecycleStep() {
3639
- return {
3640
- kind: "compatible",
3641
- axis: "record",
3642
- recordKind: "project",
3643
- fromVersion: PROJECT_LIFECYCLE_FROM_VERSION,
3644
- toVersion: PROJECT_LIFECYCLE_TO_VERSION,
3645
- defaults: [
3646
- "status defaults to active on every Project"
3647
- ],
3648
- validateSource: (snapshot) => requireProjectV4Shape(snapshot),
3649
- normalize: (snapshot) => normalizeProjectV4ToV5(snapshot)
3650
- };
3651
- }
3652
- function requireProjectV4Shape(snapshot) {
3653
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3654
- if (manifestVersions.project !== PROJECT_LIFECYCLE_FROM_VERSION) {
3655
- throw new Error(`Record project compatible step requires manifest version ${PROJECT_LIFECYCLE_FROM_VERSION}.`);
3656
- }
3657
- if (snapshot.state === null)
3658
- return;
3659
- const projects = snapshot.state.projects;
3660
- if (projects === undefined)
3661
- return;
3662
- const map = asObject(projects, "project map");
3663
- const allowed = new Set(PROJECT_V4_FIELDS);
3664
- for (const [projectId, rawProject] of Object.entries(map)) {
3665
- const project = asObject(rawProject, `Project ${projectId}`);
3666
- if (project.schemaVersion !== PROJECT_LIFECYCLE_FROM_VERSION) {
3667
- throw new Error(`Project ${projectId} must use schemaVersion ${PROJECT_LIFECYCLE_FROM_VERSION}.`);
3668
- }
3669
- const unknown = Object.keys(project).find((key) => !allowed.has(key));
3670
- if (unknown !== undefined) {
3671
- throw new Error(`Project ${projectId} has an unknown v4 field: ${unknown}.`);
3672
- }
3673
- }
3674
- }
3675
- function normalizeProjectV4ToV5(snapshot) {
3676
- requireProjectV4Shape(snapshot);
3677
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3678
- const schemaManifest = {
3679
- ...snapshot.schemaManifest,
3680
- recordVersions: { ...manifestVersions, project: PROJECT_LIFECYCLE_TO_VERSION }
3681
- };
3682
- if (snapshot.state === null)
3683
- return { schemaManifest, state: null };
3684
- const projects = snapshot.state.projects;
3685
- if (projects === undefined) {
3686
- return { schemaManifest, state: { ...snapshot.state } };
3687
- }
3688
- const map = asObject(projects, "project map");
3689
- const nextProjects = {};
3690
- for (const [projectId, rawProject] of Object.entries(map)) {
3691
- const project = asObject(rawProject, `Project ${projectId}`);
3692
- nextProjects[projectId] = {
3693
- ...project,
3694
- schemaVersion: PROJECT_LIFECYCLE_TO_VERSION,
3695
- status: "active"
3696
- };
3697
- }
3698
- return {
3699
- schemaManifest,
3700
- state: { ...snapshot.state, projects: nextProjects }
3701
- };
3702
- }
3703
- const PROJECT_V4_FIELDS = [
3704
- "schemaVersion",
3705
- "id",
3706
- "name",
3707
- "aliases",
3708
- "path",
3709
- "ownership",
3710
- "remoteUrl",
3711
- "stableBranch",
3712
- "developmentBranch",
3713
- "knowledge",
3714
- "knowledgeProposals",
3715
- "createdAt",
3716
- "updatedAt"
3717
- ];
3718
- /**
3719
- * Task v4 adds the optional durable workspace identity. Historical Tasks have
3720
- * no identity; they keep working against their existing (legacy) refs until the
3721
- * controlled rebuild mints one. This adjacent step performs no field rewrite,
3722
- * preserving old records while keeping a pre-v4 Task out of the strict current
3723
- * parser.
3724
- */
3725
- function taskWorkspaceIdentityStep() {
3726
- return {
3727
- axis: "record",
3728
- recordKind: "task",
3729
- fromVersion: TASK_FROM_VERSION,
3730
- toVersion: TASK_TO_VERSION,
3731
- preconditions: requireTaskV3Family,
3732
- transform: migrateTaskV3ToV4,
3733
- declaredEffects: []
3734
- };
3735
- }
3736
- function requireTaskV3Family(snapshot) {
3737
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3738
- if (manifestVersions.task !== TASK_FROM_VERSION) {
3739
- throw new Error(`Record task migration requires manifest version ${TASK_FROM_VERSION}.`);
3740
- }
3741
- if (snapshot.state === null)
3742
- return;
3743
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3744
- for (const [taskId, rawTask] of Object.entries(tasks)) {
3745
- const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3746
- const task = aggregate.task;
3747
- if (task === undefined)
3748
- continue;
3749
- const record = asObject(task, `Task ${taskId}`);
3750
- if (record.schemaVersion !== TASK_FROM_VERSION) {
3751
- throw new Error(`Task ${taskId} must use schemaVersion ${TASK_FROM_VERSION}.`);
3752
- }
3753
- }
3754
- }
3755
- function migrateTaskV3ToV4(snapshot) {
3756
- requireTaskV3Family(snapshot);
3757
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3758
- const schemaManifest = {
3759
- ...snapshot.schemaManifest,
3760
- recordVersions: { ...manifestVersions, task: TASK_TO_VERSION }
3761
- };
3762
- if (snapshot.state === null)
3763
- return { schemaManifest, state: null };
3764
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3765
- const nextTasks = {};
3766
- for (const [taskId, rawTask] of Object.entries(tasks)) {
3767
- const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3768
- if (aggregate.task === undefined) {
3769
- nextTasks[taskId] = { ...aggregate };
3770
- continue;
3771
- }
3772
- const task = asObject(aggregate.task, `Task ${taskId}`);
3773
- nextTasks[taskId] = {
3774
- ...aggregate,
3775
- task: { ...task, schemaVersion: TASK_TO_VERSION }
3776
- };
3777
- }
3778
- return {
3779
- schemaManifest,
3780
- state: { ...snapshot.state, tasks: nextTasks }
3781
- };
3782
- }
3783
- /**
3784
- * Task v5 replaces the stored delivery topology switch with an optional,
3785
- * Project-defined intent. Historical topology is retained as audit context,
3786
- * but no current workflow decision reads it.
3787
- */
3788
- function taskIntentStep() {
3789
- return {
3790
- axis: "record",
3791
- recordKind: "task",
3792
- fromVersion: TASK_INTENT_FROM_VERSION,
3793
- toVersion: TASK_INTENT_TO_VERSION,
3794
- preconditions: requireTaskV4Family,
3795
- transform: migrateTaskV4ToV5,
3796
- declaredEffects: []
3797
- };
3798
- }
3799
- function requireTaskV4Family(snapshot) {
3800
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3801
- if (manifestVersions.task !== TASK_INTENT_FROM_VERSION) {
3802
- throw new Error(`Record task migration requires manifest version ${TASK_INTENT_FROM_VERSION}.`);
3803
- }
3804
- if (snapshot.state === null)
3805
- return;
3806
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3807
- for (const [taskId, rawTask] of Object.entries(tasks)) {
3808
- const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3809
- if (aggregate.task === undefined)
3810
- continue;
3811
- const record = asObject(aggregate.task, `Task ${taskId}`);
3812
- if (record.schemaVersion !== TASK_INTENT_FROM_VERSION) {
3813
- throw new Error(`Task ${taskId} must use schemaVersion ${TASK_INTENT_FROM_VERSION}.`);
3814
- }
3815
- }
3816
- }
3817
- function migrateTaskV4ToV5(snapshot) {
3818
- requireTaskV4Family(snapshot);
3819
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3820
- const schemaManifest = {
3821
- ...snapshot.schemaManifest,
3822
- recordVersions: { ...manifestVersions, task: TASK_INTENT_TO_VERSION }
3823
- };
3824
- if (snapshot.state === null)
3825
- return { schemaManifest, state: null };
3826
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3827
- const nextTasks = {};
3828
- for (const [taskId, rawTask] of Object.entries(tasks)) {
3829
- const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3830
- if (aggregate.task === undefined) {
3831
- nextTasks[taskId] = { ...aggregate };
3832
- continue;
3833
- }
3834
- const task = asObject(aggregate.task, `Task ${taskId}`);
3835
- const { requireIntegration, schemaVersion: _schemaVersion, ...retained } = task;
3836
- const projectBindings = Array.isArray(task.projectBindings)
3837
- ? task.projectBindings
3838
- : [];
3839
- const legacyDeliveryPath = projectBindings.length === 0
3840
- ? undefined
3841
- : requireIntegration === true ? "integrated" : "direct";
3842
- nextTasks[taskId] = {
3843
- ...aggregate,
3844
- task: {
3845
- ...retained,
3846
- schemaVersion: TASK_INTENT_TO_VERSION,
3847
- ...(legacyDeliveryPath === undefined ? {} : { legacyDeliveryPath })
3848
- }
3849
- };
3850
- }
3851
- return {
3852
- schemaManifest,
3853
- state: { ...snapshot.state, tasks: nextTasks }
3854
- };
3855
- }
3856
- /** Task v6 separates semantic lifecycle from the current execution admission gate. */
3857
- function taskExecutionGateStep() {
3858
- return {
3859
- axis: "record",
3860
- recordKind: "task",
3861
- fromVersion: TASK_EXECUTION_GATE_FROM_VERSION,
3862
- toVersion: TASK_EXECUTION_GATE_TO_VERSION,
3863
- preconditions: requireTaskV5Family,
3864
- transform: migrateTaskV5ToV6,
3865
- declaredEffects: []
3866
- };
3867
- }
3868
- function requireTaskV5Family(snapshot) {
3869
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3870
- if (manifestVersions.task !== TASK_EXECUTION_GATE_FROM_VERSION) {
3871
- throw new Error(`Record task migration requires manifest version ${TASK_EXECUTION_GATE_FROM_VERSION}.`);
3872
- }
3873
- if (snapshot.state === null)
3874
- return;
3875
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3876
- for (const [taskId, rawTask] of Object.entries(tasks)) {
3877
- const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3878
- if (aggregate.task === undefined)
3879
- continue;
3880
- const record = asObject(aggregate.task, `Task ${taskId}`);
3881
- if (record.schemaVersion !== TASK_EXECUTION_GATE_FROM_VERSION) {
3882
- throw new Error(`Task ${taskId} must use schemaVersion ${TASK_EXECUTION_GATE_FROM_VERSION}.`);
3883
- }
3884
- }
3885
- }
3886
- function migrateTaskV5ToV6(snapshot) {
3887
- requireTaskV5Family(snapshot);
3888
- const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3889
- const schemaManifest = {
3890
- ...snapshot.schemaManifest,
3891
- recordVersions: { ...manifestVersions, task: TASK_EXECUTION_GATE_TO_VERSION }
3892
- };
3893
- if (snapshot.state === null)
3894
- return { schemaManifest, state: null };
3895
- const tasks = asObject(snapshot.state.tasks, "state tasks");
3896
- const nextTasks = {};
3897
- for (const [taskId, rawTask] of Object.entries(tasks)) {
3898
- const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
3899
- if (aggregate.task === undefined) {
3900
- nextTasks[taskId] = { ...aggregate };
3901
- continue;
3902
- }
3903
- const task = asObject(aggregate.task, `Task ${taskId}`);
3904
- nextTasks[taskId] = {
3905
- ...aggregate,
3906
- task: {
3907
- ...task,
3908
- schemaVersion: TASK_EXECUTION_GATE_TO_VERSION,
3909
- executionGate: { state: "enabled" }
3910
- }
3911
- };
3912
- }
3913
- return {
3914
- schemaManifest,
3915
- state: { ...snapshot.state, tasks: nextTasks }
3916
- };
3917
- }
3918
- /**
3919
- * A version bump is deliverable only when the shared planner resolves the full
3920
- * adjacent path. This also covers target-only record families as explicit 0->1
3921
- * introductions and rejects offline declarations without executable steps.
3922
- */
3923
- export function assertRegistryCoversBaselineToCurrent(registry, baseline = baselineStorageVersionState(), current = latestStorageVersionState()) {
3924
- const plan = planMigration(registry, baseline, current);
3925
- if (plan.kind !== "blocked")
3926
- return;
3927
- const coordinate = plan.blocker.axis === "record"
3928
- ? `record/${plan.blocker.recordKind ?? "?"}`
3929
- : plan.blocker.axis;
3930
- throw new Error(`Storage migration delivery gate failed for ${coordinate}: ${plan.blocker.message}`);
3931
- }