Config-JSONB realignment backlog
Status: Current | Last reviewed: 2026-06-23
A prioritized record of agent_versions.config / workflow_versions.config JSONB inconsistencies that were audited during the 2026-06-23 config-shape pass but not (or only partially) executed. Read this before touching config-shape internals so you don't re-discover a deferred item or re-litigate a deliberate decision.
This is the config-internals sibling of cleanup-proposals.md — that page tracks stale tables/columns; this one tracks the shapes inside the two config JSONB blobs. When you act on an open item, move it to "Resolved" with the migration number.
The 2026-06-23 pass shipped 005_normalize_config_shapes.sql plus matching runtime/editor changes. What it converged is under Resolved; what it deliberately left is under Open.
Open backlog
Each item: Issue / Blast radius / Recommendation / Why deferred.
1. Migration-003 gap: orphaned top-level config.injection / config.ivr
|
|
| Issue |
003_migrate_node_builtin_configs.sql moved Operations capability config (injection, ivr, ces, warm_transfer, mcp_url_catalogs) off the top level and onto the owning pipeline node's builtin_tool_configs, then stripped the top-level keys. The audit observed ~6 agent_versions still carrying a top-level config.injection (and ~3 a top-level config.ivr) with no matching node-level builtin_tool_configs.message_injection. These rows were saved after 003 ran, so 003 never touched them. The runtime no longer reads the top-level copy — injection/ivr is silently un-wired on those agents. |
| Blast radius |
Small (a handful of agents) but a correctness bug on each: injection/ivr looks configured in the row but does nothing at runtime. Verify the exact row count before acting. |
| Recommendation |
A follow-up forward migration that re-runs the 003 node-promotion for any remaining top-level injection/ivr/ces/warm_transfer keys, or simply re-save each affected agent through the editor (which writes the node-level shape). |
| Why deferred |
This is a 003-completion task (rows that slipped in after the one-time migration), not part of the config-shape normalization that 004 owned. |
2. Orphaned Injection / Ivr $defs in the agent schema
|
|
| Issue |
agent-config.schema.json still defines $defs.Injection and $defs.Ivr, but nothing $refs them anymore (confirmed: zero references) — injection/ivr config now lives inline under the node builtin_tool_configs objects (per item 1's migration). Dead weight. |
| Blast radius |
None. ajv ignores unreferenced $defs; validation is unaffected. |
| Recommendation |
Either $ref them from the builtin_tool_configs inline objects (so the shapes are validated again) or delete them. |
| Why deferred |
Cosmetic; harmless. Low priority. |
3. camelCase vs snake_case key mix
|
|
| Issue |
Top-level config keys are snake_case (execution_mode, output_bindings, target_language), but some stage/editor-owned fields are camelCase — notably canvas/UI fields such as position. The two conventions coexist in one blob. |
| Blast radius |
High. Renaming touches the editor serializer (flowElementsToPipeline), the collab Y.Doc shape converters (pipeline-shape.ts), the TS types, and every reader — and the collab allow-list silently drops any key not threaded through both directions. |
| Recommendation |
Document as a known, accepted inconsistency. Do not converge opportunistically. |
| Why deferred |
Cosmetic with high blast radius and no runtime payoff. |
|
|
| Issue |
config.tools is string[] (see pipeline-types.ts L872) whose entries are one of three forms with no discriminator key: a bare tool-config uuid, builtin:<id>, or inline:<id>. Readers disambiguate by string prefix. |
| Blast radius |
Large. A discriminated object form ({kind, id}) would touch the editor, the collab shape, the orchestrator wiring, and every persisted config. |
| Recommendation |
Document the prefix convention (already enforced in code — bare = uuid, builtin: = built-in capability, inline: = config.inline_tools[] entry). Leave the wire shape as a string union. |
| Why deferred |
The prefix convention works and is enforced; an object form is a large migration for stylistic gain. |
5. output_parameters vs response_variables on workflow config
|
|
| Issue |
Near-aliases on WorkflowConfig. async-agent reads output_parameters or response_variables (one falls back to the other — see workflow_tool.py L145-L146); consumer.py L555 reads output_parameters only. |
| Blast radius |
Low. Both are optional and additive. |
| Recommendation |
Keep both — they carry distinct documented intent (workflow-types.ts L696-L705): output_parameters = outputs surfaced to a generic workflow caller; response_variables = outputs consumed by a conversation lifecycle hook (become {{lifecycle.<name>}} merge fields). Document the precedence (output_parameters wins where both readers run). |
| Why deferred |
Not actually a duplicate; collapsing them would lose the documented distinction. Low priority. |
6. multimodality.catalog_id (singular) vs catalogs[]
|
|
| Issue |
Node config.multimodality.catalog_id is a deprecated singular field; the canonical field is catalogs?: string[] (a multi-select of catalog URIs). The type is explicitly marked @deprecated (pipeline-types.ts L1154-L1165) and kept only so legacy rows read without crashing. |
| Blast radius |
Low. Readers already prefer catalogs. |
| Recommendation |
Treat catalog_id as read-only legacy; never write it. A future migration could fold any surviving catalog_id into catalogs[] and drop the field. |
| Why deferred |
Already deprecated and tolerated; not worth a migration alone. |
7. TS ⇄ Python runtime field-name divergence (verify per field)
|
|
| Issue |
The TS editor types and the Python node handlers are independent surfaces, so a field can drift. The audit flagged a few; on inspection they are dual-shape back-compat, not bugs — but they confirm the divergence risk: foreach reads two distinct body representations (body = legacy list of sibling node ids run sequentially; body_graph = {nodes, edges} inline sub-graph for nested foreach) — not aliases, both live (foreach.py L95-L109). assignment bindings read value (canonical) with a source legacy fallback and an operator of set\|append\|prepend\|concat\|increment (assignment.py L63-L70). |
| Blast radius |
Varies per field; a true mismatch silently drops config on one side. |
| Recommendation |
Per field, confirm the authoritative side (Python handler is the runtime source of truth) before changing either. Audit observed no concrete typo-path mismatch in foreach/assignment — the apparent ones are deliberate legacy fallbacks. Treat any new finding as "verify against the node handler, then fix the non-authoritative side." |
| Why deferred |
No confirmed bug; this is a standing risk to check, not a scheduled change. |
Resolved
Shipped 2026-06-23 by 005_normalize_config_shapes.sql (idempotent; folded into migrate-city-db.sh) plus the runtime/editor changes noted. After 004, the schemas under shared/service-definitions/schemas/ validate every config with zero errors.
| Area |
What converged |
| Dead-data cleanup |
Dropped config.settings.lifecycle (0 rows used it; canonical is top-level config.lifecycle). Normalized execution_mode: 'scheduled' → 'async' (unhandled value; scheduling rides trigger.kind='schedule'). Stripped deprecated top-level config.name / config.description / config.extension mirrors (authoritative on the agents row / stage config). Coerced code-node output_bindings: [] → {} (canonical object shape). |
ocr_extract |
Legacy input{source,value} + integration_id + features + queries + output_schema → canonical document_source / document_reference / ocr_integration_id / aws_features / aws_queries / llm_output_schema / processing_type. The runtime _normalise_legacy_config shim was removed from ocr_extract.py. |
router |
Legacy condition_field / routes → canonical config.outputs=[{id,label,expression}], with output ids derived from the outgoing edge condition values (edges are the routing source of truth). The runtime _normalise_config legacy branch in router.py and the config.routes fallback in workflow_graph_builder.py were removed. This also fixed a latent bug: in the squashed runtime a condition_field-only router produced empty outputs and always fell through to __default__ (it never branched). |
translation |
Flow-level source_language / target_language / voice_config collapsed down into the owning stages (STT language, translation/S2ST stage source_language/target_language, TTS voice_id). The orchestrator translation_builder.py now reads stage-first; the editor (TranslationPipelineEditor.tsx, default-configs.ts), schema.ts validation, and pipeline-types.ts were moved to stage-level. Flow-level fields stay optional in the schema as a fallback for not-yet-migrated rows but are deprecated. |
lifecycle dual-home |
universal_builder.py no longer reads config.settings.lifecycle as an or fallback — only top-level config.lifecycle. |
See also