|
88 | 88 | "experience-incubation": "experience", |
89 | 89 | "profile-source-window": "profile", |
90 | 90 | } |
| 91 | +_COMPATIBLE_DREAM_BINDING_ADDITIONS = { |
| 92 | + "handoff.dream.v1": "handoff", |
| 93 | + "prompt.dream.v1": "prompt", |
| 94 | +} |
| 95 | +_COMPATIBLE_DREAM_CAPABILITY_ADDITIONS = {"handoff", "prompt"} |
91 | 96 | _STATE_COLUMNS = { |
92 | 97 | "last_schedule_checkpoint_at": "DATETIME NULL", |
93 | 98 | "scan_generation": "BIGINT NOT NULL DEFAULT 0", |
@@ -164,6 +169,7 @@ async def bootstrap_processing_schema( |
164 | 169 |
|
165 | 170 | marker = await _marker(connection) |
166 | 171 | if marker is not None: |
| 172 | + await _upgrade_compatible_dream_manifest(connection, config_manifest) |
167 | 173 | await assert_processing_schema_ready(connection, config_manifest) |
168 | 174 | return |
169 | 175 | columns = await _columns(connection, STATES.name) |
@@ -234,6 +240,82 @@ async def assert_processing_schema_ready( |
234 | 240 | raise ProcessingSchemaNotReadyError(reason="; ".join(problems)) |
235 | 241 |
|
236 | 242 |
|
| 243 | +async def _upgrade_compatible_dream_manifest( |
| 244 | + connection: AsyncConnection, |
| 245 | + config_manifest: Mapping[str, Any] | str | None, |
| 246 | +) -> None: |
| 247 | + """Record the additive Handoff and Prompt bindings on existing deployments.""" |
| 248 | + |
| 249 | + if config_manifest is None: |
| 250 | + return |
| 251 | + marker = await _marker(connection, for_update=True) |
| 252 | + if marker is None or marker["schema_version"] != PROCESSING_SCHEMA_VERSION or marker["phase"] != "complete": |
| 253 | + return |
| 254 | + previous_manifest = _manifest(str(marker["config_manifest"])) |
| 255 | + current_manifest = _manifest(config_manifest) |
| 256 | + if _deployment_manifest(previous_manifest) == _deployment_manifest(current_manifest): |
| 257 | + return |
| 258 | + previous = json.loads(previous_manifest) |
| 259 | + current = json.loads(current_manifest) |
| 260 | + if not _is_compatible_dream_manifest_extension(previous, current): |
| 261 | + return |
| 262 | + upgraded = dict(previous) |
| 263 | + upgraded["bindings"] = {**previous["bindings"], **_manifest_binding_additions(previous, current)} |
| 264 | + upgraded["capabilities"] = current["capabilities"] |
| 265 | + upgraded_manifest = _manifest(upgraded) |
| 266 | + if _deployment_manifest(upgraded_manifest) != _deployment_manifest(current_manifest): |
| 267 | + return |
| 268 | + await connection.execute( |
| 269 | + update(SCHEMA) |
| 270 | + .where(SCHEMA.c.singleton == 1, SCHEMA.c.config_manifest == marker["config_manifest"]) |
| 271 | + .values(config_manifest=upgraded_manifest) |
| 272 | + ) |
| 273 | + |
| 274 | + |
| 275 | +def _is_compatible_dream_manifest_extension(previous: Mapping[str, Any], current: Mapping[str, Any]) -> bool: |
| 276 | + if set(previous) != set(current) or previous.get("mode") != current.get("mode"): |
| 277 | + return False |
| 278 | + previous_bindings = previous.get("bindings") |
| 279 | + current_bindings = current.get("bindings") |
| 280 | + previous_capabilities = previous.get("capabilities") |
| 281 | + current_capabilities = current.get("capabilities") |
| 282 | + if ( |
| 283 | + not isinstance(previous_bindings, dict) |
| 284 | + or not isinstance(current_bindings, dict) |
| 285 | + or not isinstance(previous_capabilities, list) |
| 286 | + or not isinstance(current_capabilities, list) |
| 287 | + or any(not isinstance(item, str) for item in (*previous_capabilities, *current_capabilities)) |
| 288 | + ): |
| 289 | + return False |
| 290 | + if any(current_bindings.get(name) != family for name, family in previous_bindings.items()): |
| 291 | + return False |
| 292 | + additions = _manifest_binding_additions(previous, current) |
| 293 | + if not additions or any( |
| 294 | + _COMPATIBLE_DREAM_BINDING_ADDITIONS.get(name) != family for name, family in additions.items() |
| 295 | + ): |
| 296 | + return False |
| 297 | + previous_capability_set = set(previous_capabilities) |
| 298 | + current_capability_set = set(current_capabilities) |
| 299 | + added_capabilities = current_capability_set - previous_capability_set |
| 300 | + return ( |
| 301 | + previous_capability_set <= current_capability_set |
| 302 | + and added_capabilities <= _COMPATIBLE_DREAM_CAPABILITY_ADDITIONS |
| 303 | + and added_capabilities <= set(additions.values()) |
| 304 | + ) |
| 305 | + |
| 306 | + |
| 307 | +def _manifest_binding_additions(previous: Mapping[str, Any], current: Mapping[str, Any]) -> dict[str, str]: |
| 308 | + previous_bindings = previous.get("bindings") |
| 309 | + current_bindings = current.get("bindings") |
| 310 | + if not isinstance(previous_bindings, dict) or not isinstance(current_bindings, dict): |
| 311 | + return {} |
| 312 | + return { |
| 313 | + name: family |
| 314 | + for name, family in current_bindings.items() |
| 315 | + if isinstance(name, str) and isinstance(family, str) and name not in previous_bindings |
| 316 | + } |
| 317 | + |
| 318 | + |
237 | 319 | async def apply_processing_migration( # noqa: C901 - explicit durable maintenance phases |
238 | 320 | connection: AsyncConnection, |
239 | 321 | *, |
|
0 commit comments