import * as Effect from "effect/Effect"; import * as SqlClient from "effect/unstable/sql/SqlClient"; /** * Canonicalize `modelSelection.options` / `defaultModelSelection.options` from * the legacy object shape (`{ effort: "max", fastMode: true, ... }`) to the * current array-of-selections shape (`[{ id: "effort", value: "max" }, ...]`). * * Migration 016 introduced `modelSelection` with `options` stored as a * per-provider object. Later the schema was reshaped so that options are a * generic `Array<{ id, value }>` of user-selected option entries. Stored rows * from before the reshape still have the object shape and fail to decode. * * For each value in the legacy object: * - string values are kept if non-empty after trim * - boolean values are always kept (true | false) * - any other value type (number, null, nested object/array) is dropped, * matching the permissive client-side normalizer in composerDraftStore. * * Touched storage: * - `projection_threads.model_selection_json.options` * - `projection_projects.default_model_selection_json.options` * - `orchestration_events.payload_json.$.modelSelection.options` * (thread.created | thread.meta-updated | thread.turn-start-requested) * - `orchestration_events.payload_json.$.defaultModelSelection.options` * (project.created | project.meta-updated) */ export default Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* sql` UPDATE projection_threads SET model_selection_json = json_set( model_selection_json, '$.options', ( SELECT json_group_array( json_object( 'id', key, 'value', CASE type WHEN 'true' THEN json('true') WHEN 'false' THEN json('false') ELSE atom END ) ) FROM json_each(json_extract(model_selection_json, '$.options')) WHERE (type = 'text' AND trim(coalesce(atom, '')) != '') OR type IN ('true', 'false') ) ) WHERE model_selection_json IS NOT NULL AND json_type(model_selection_json, '$.options') = 'object' `; yield* sql` UPDATE projection_projects SET default_model_selection_json = json_set( default_model_selection_json, '$.options', ( SELECT json_group_array( json_object( 'id', key, 'value', CASE type WHEN 'true' THEN json('true') WHEN 'false' THEN json('false') ELSE atom END ) ) FROM json_each(json_extract(default_model_selection_json, '$.options')) WHERE (type = 'text' AND trim(coalesce(atom, '')) != '') OR type IN ('true', 'false') ) ) WHERE default_model_selection_json IS NOT NULL AND json_type(default_model_selection_json, '$.options') = 'object' `; yield* sql` UPDATE orchestration_events SET payload_json = json_set( payload_json, '$.modelSelection.options', ( SELECT json_group_array( json_object( 'id', key, 'value', CASE type WHEN 'true' THEN json('true') WHEN 'false' THEN json('false') ELSE atom END ) ) FROM json_each(json_extract(payload_json, '$.modelSelection.options')) WHERE (type = 'text' AND trim(coalesce(atom, '')) != '') OR type IN ('true', 'false') ) ) WHERE event_type IN ( 'thread.created', 'thread.meta-updated', 'thread.turn-start-requested' ) AND json_type(payload_json, '$.modelSelection.options') = 'object' `; yield* sql` UPDATE orchestration_events SET payload_json = json_set( payload_json, '$.defaultModelSelection.options', ( SELECT json_group_array( json_object( 'id', key, 'value', CASE type WHEN 'true' THEN json('true') WHEN 'false' THEN json('false') ELSE atom END ) ) FROM json_each(json_extract(payload_json, '$.defaultModelSelection.options')) WHERE (type = 'text' AND trim(coalesce(atom, '')) != '') OR type IN ('true', 'false') ) ) WHERE event_type IN ('project.created', 'project.meta-updated') AND json_type(payload_json, '$.defaultModelSelection.options') = 'object' `; });