Dynamic Agent Config Developer Guide
What Changed and Why
Previously, all agent configuration (presets, voices, languages, features) lived in a static shared/agent_config.json file. Every change required editing the file and redeploying. This migration moves everything into MongoDB, making config dynamic, org-scoped, versioned, and manageable via API.
Before vs After
| Aspect | Before | After |
|---|---|---|
| Config storage | shared/agent_config.json (static file) | MongoDB SystemConfig collection |
| Voices | Embedded in each preset (duplicated) | Separate Voice collection, referenced by ID |
| Agent fields | Copied provider fields from preset on create | Stores only preset_id + optional language, resolved at dispatch time |
| Config changes | Edit JSON → redeploy | API call → instant, versioned |
| Version history | None | Full snapshots with diff and rollback |
| Customer visibility | All fields exposed | Provider fields stripped for customer role |
| Permissions | None | Org-level edit_system_config + role check |
| Multi-language | One provider stack per preset | Per-language LLM/TTS/STT combinations (max 20) with canonical language keys |
| Provider catalog | None (free-text) | ProviderModel_Registry all providers/models must be registered first, with language key→value mappings |
| Language handling | Raw BCP-47 codes (inconsistent across providers) | Canonical language keys as source of truth, resolved to provider-specific codes at dispatch time |
How Config is Managed Step by Step
1. Default Config (Superadmin-Managed)
There is a single platform-wide default config stored with orgUID = "__default__". It is:
- Auto-seeded from
agent_config.jsonon the first request from any org without its own config - Managed by superadmin via the System Config page in the superadmin dashboard
- Used by all orgs that don't have
edit_system_configenabled
2. Org-Level Config (Admin-Managed)
When an org has edit_system_config enabled in their plan features:
- The org admin sees a Config Management page in their dashboard
- Initially shows "Using platform default" with a "Customize Config" button
- Clicking "Customize" forks (copies) the default config into an org-specific config
- The org can then CRUD their own presets independently
- They can "Revert to Default" at any time to delete their custom config
3. Config Resolution Flow
Request for org config
│
├── Org has own SystemConfig? → Use it
│
└── No → Use __default__ SystemConfig
└── __default__ doesn't exist? → Auto-seed from agent_config.json4. After Seed JSON File is Ignored
Once the __default__ SystemConfig exists in MongoDB, the JSON file is never read again. All changes happen through the UI or API.
Architecture Flow
┌─────────────────────────────────────────────────────────┐
│ Frontend (oktivo_app) │
│ │
│ useAgentConfig hook ──→ GET /agent-config │
│ configService ──→ /api/v2/config/* (admin CRUD) │
└──────────────────────────┬───────────────────────────────┘
│
┌──────────────────────────▼───────────────────────────────┐
│ Node.js Backend │
│ │
│ config.controller.ts │
│ └─ getAgentConfig() │
│ ├─ configService.getConfigForOrg(orgUID) │
│ │ └─ auto-seeds from JSON if no config exists │
│ ├─ presetResolver.resolveAllForDisplay(orgUID) │
│ │ └─ populates voice details from Voice collection│
│ └─ sanitizeConfigResponse middleware │
│ └─ strips provider fields for customer role │
│ │
│ agent.controller.ts │
│ ├─ createAgent: validates preset_id via configService │
│ ├─ updateAgent: validates new preset_id if changed │
│ └─ testAgent/testAgentWeb: uses presetResolver │
│ └─ resolveForDispatch() builds agent_config │
│ with provider fields + system_prompt + metadata │
│ │
│ systemConfig.controller.ts (NEW) │
│ └─ Admin CRUD for presets, voices, versions │
│ └─ Permission-gated: edit_system_config + role │
└──────────────────────────┬───────────────────────────────┘
│
┌──────────────────────────▼───────────────────────────────┐
│ MongoDB │
│ │
│ ProviderModel_Registry ─ Catalog of LLM/TTS/STT models │
│ SystemConfig ─ One per org, presets + language_combos │
│ Voice ─ Deduplicated, requires tts_provider+model │
│ ConfigVersion ─ Immutable snapshots for audit/rollback │
│ Agent ─ Stores preset_id + language (dispatch key)│
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Python Agent Server (LiveKit) │
│ │
│ Receives fully resolved agent_config in dispatch metadata│
│ No changes needed extra="allow" on MetadataSchema │
│ New fields: system_prompt, metadata (passthrough) │
│ Voice behavior: backchannels, exit_phrases_agent, │
│ exit_phrases_user, pronunciation_dict (top-level + │
│ inside agent_config) │
└──────────────────────────────────────────────────────────┘File Changes Summary
New Files Created
| File | Purpose |
|---|---|
src/models/Voice.ts | Voice MongoDB model deduplicated voice entries (requires tts_provider + tts_model) |
src/models/SystemConfig.ts | SystemConfig model org-scoped config with presets + language_combinations |
src/models/ConfigVersion.ts | ConfigVersion model immutable version snapshots |
src/models/ProviderModelRegistry.ts | Provider/model catalog with language key-value mappings and multilingual flag |
src/utils/preset.resolver.ts | PresetResolver resolves preset_id + language to full dispatch config |
src/utils/preset.utils.ts | Utility functions incl. deriveSupportedLanguages |
src/services/config.service.ts | ConfigService all config CRUD, versioning, auto-seed |
src/services/providerModelRegistry.service.ts | Registry CRUD with deletion guards |
src/services/languageCombination.validator.ts | Validates language_combinations arrays |
src/services/cascadeConsistency.service.ts | Detects consistency issues on voice/registry changes |
src/data/language-keys.ts | Canonical language key definitions (Hindi, English, Telugu, etc.) with validation |
src/middleware/sanitize.middleware.ts | Strips provider fields for customer role |
src/middleware/configPermission.middleware.ts | Permission gate for config mutations |
src/controllers/systemConfig.controller.ts | Admin CRUD API handlers |
src/routes/systemConfig.routes.ts | Admin CRUD routes at /api/v2/config |
src/routes/providerModelRegistry.routes.ts | Provider Registry CRUD routes (superadmin only) |
src/scripts/migrate-language-combinations.ts | Migration: seeds registry, adds language_combinations, populates Voice.tts_model |
src/scripts/migrate-config-to-mongodb.ts | One-time migration script (original JSON → MongoDB) |
Modified Files
| File | What Changed |
|---|---|
src/models/Agent.ts | Removed 7 provider fields, made preset_id required |
src/types/index.ts | Updated IAgent interface to match |
src/controllers/config.controller.ts | Reads from MongoDB instead of JSON, uses PresetResolver |
src/controllers/agent.controller.ts | Removed static JSON loading, uses ConfigService + PresetResolver for create/update/dispatch. Test call dispatch uses shared dispatch.utils.ts helpers. |
src/utils/preset.utils.ts | Reads preset IDs from MongoDB instead of JSON |
src/controllers/admin.controller.ts | Updated validatePresetIds calls (now async with orgUID) |
src/controllers/superadmin.controller.ts | Updated validatePresetIds calls (now async with orgUID) |
src/routes/config.routes.ts | Added sanitizeConfigResponse middleware |
src/app.ts | Mounted systemConfigRoutes at /api/v2/config |
app/src/hooks/useAgentConfig.ts | Provider fields optional, new response parsing, language_key voice matching |
app/src/services/config.service.ts | Added admin CRUD methods, updated response type |
app/src/pages/superadmin/ProviderRegistryPanel.tsx | Language key-value pair UI, multilingual LLM toggle |
app/src/pages/superadmin/LanguageCombinationEditor.tsx | Simplified to language key + voice only, inherits providers from preset, multilingual LLM exclusion |
app/src/components/features/SystemConfigPanel.tsx | Language key labels, preset update impact guard, voice language_key dropdown |
app/src/components/features/LanguageSelector.tsx | Language key labels with flag emojis |
app/src/lib/languageKeys.ts | (NEW) Client-side language key definitions and helpers |
Follow-up Fix: Voice uid vs Provider voice_id
Problem: The original Voice model enforced unique: true directly on voice_id. Since voice_id was also the TTS provider's own voice identifier, admins could not add the same provider voice for a second language or model (some providers, e.g. Azure/Google, reuse one voice identifier across locales).
Fix: Introduced a system-generated uid field as the sole internal reference key, decoupled from the provider's voice_id.
| File | What Changed |
|---|---|
src/models/Voice.ts | Added required unique uid field. Removed unique: true from voice_id. Added compound unique index (voice_id, tts_provider, tts_model, language_key) so the same provider voice_id can be reused across different languages/models, while still preventing exact duplicates. |
src/services/config.service.ts | createVoice generates uid, validates the compound key instead of raw voice_id. updateVoice/deleteVoice now key off uid. autoPopulateCombinationVoices and the JSON auto-seed path (seedDefaultFromJson) build/store uid references in presets. |
src/utils/preset.resolver.ts | resolveAllForDisplay/resolveForDisplay/populateVoices now look up voices by uid. Added resolveVoiceUidToProviderId() resolves a preset's stored voice uid to the provider voice_id at dispatch time (resolveForDispatch). |
src/utils/dispatch.utils.ts | Added resolveAgentVoiceForDispatch(ttsVoice) resolves an agent's tts_voice (a uid) to the provider voice_id before building the dispatch payload. buildAgentConfig accepts an optional pre-resolved resolvedAgentVoice. |
src/controllers/agent.controller.ts, src/controllers/landing.controller.ts, src/services/health.service.ts, src/services/calling.service.ts, src/services/crmApi.service.ts | All dispatch call sites (test calls, web calls, campaign dispatch, CRM-triggered calls, landing page demo calls) now call resolveAgentVoiceForDispatch() before sending tts_voice to the Agent Server. |
src/services/cascadeConsistency.service.ts, src/services/languageCombination.validator.ts | Voice consistency checks now look up voices by uid (with legacy voice_id fallback). |
src/controllers/systemConfig.controller.ts | Voice CRUD routes (:voiceId param), upload-sample, and stream-sample handlers now treat :voiceId as the voice uid. |
src/middleware/sanitize.middleware.ts | Voice sanitization now also strips tts_model (previously missed) and preserves uid, which customers need to reference a voice. |
src/scripts/migrate-voice-uid.ts | (NEW) One-time, idempotent migration: backfills uid on existing Voice documents, rewrites SystemConfig preset voice references and Agent.tts_voice from old voice_id → new uid, and drops the old unique index on voice_id. Runnable standalone (npx tsx src/scripts/migrate-voice-uid.ts) or via API. |
src/routes/script.routes.ts | Added POST /internal/scripts/migrate-voice-uid (protected by verifyWebhookSecret / x-api-secret), following the same pattern as other internal migration endpoints. |
src/scripts/migrate-config-to-mongodb.ts, src/scripts/migrate-voice-language-keys.ts, src/controllers/language-combination-migration.controller.ts | Updated pre-existing migration scripts to remain correct under the new schema: generate/preserve uid, look up voices by uid (with voice_id fallback), and target updates by _id instead of the now-non-unique voice_id. |
app/src/hooks/useAgentConfig.ts, app/src/components/features/VoiceGrid.tsx, app/src/components/features/SystemConfigPanel.tsx, app/src/pages/superadmin/LanguageCombinationEditor.tsx, app/src/pages/customer/AgentEditorPage.tsx, app/src/components/features/AgentFormDialog.tsx, app/src/components/shared/DataTable.tsx, app/src/services/config.service.ts | Voice selection, matching, and CRUD across the customer voice picker and superadmin System Config / Language Combination editors now use uid as the selection/reference key. The "Add Voice" dialog's Voice ID field is now labeled "Provider Voice ID" and is a required, freely reusable field (no longer auto-generated/unique). |
Migrating existing data: Run the migration once per environment before relying on the new behavior:
# Standalone
npx tsx src/scripts/migrate-voice-uid.ts
# Or via API (idempotent safe to re-run)
curl -X POST -H "x-api-secret: <SUPERADMIN_API_SECRET>" \
http://localhost:3000/internal/scripts/migrate-voice-uidThis is safe to skip on brand-new deployments (no existing Voice documents), since createVoice always generates a uid going forward.
Key Concepts
Preset-by-Reference
Agents store only preset_id and an optional language field. When you fetch an agent or dispatch a call, PresetResolver looks up the preset from SystemConfig and resolves the full provider/model fields. This means:
- Updating a preset instantly affects all agents using it
- No need to update individual agent documents when preset config changes
- The
languagefield controls which language_combination is used at dispatch time
Multi-Language Provider System
Each preset can define up to 20 language_combinations per-language LLM/TTS/STT provider/model/voice configurations. At dispatch time:
- Agent has
preset_id+language(e.g., "hindi") PresetResolver.resolveForDispatch(orgUID, presetId, language)looks up the preset- If a
language_combinationmatches (case-insensitive on the key), its providers are used - For TTS: if the row has a non-empty
tts_languageoverride, that key is used for TTS resolution; otherwiselanguage_codeis used - For STT: if the row has a non-empty
stt_languageoverride, that key is used for STT resolution; otherwiselanguage_codeis used - Each language key is resolved to provider-specific codes via Provider Registry
language_mappings - If no match, flat fallback fields are used (backward-compatible)
- Output is always a flat
IDispatchConfigAgent Server sees no difference
Workflow:
Provider Registry (key→value mappings) → Preset (language_combinations with keys) → Voice (language_key) → Agent (selects key) → Dispatch (resolves to provider codes)Key rules:
- Providers and models must be registered in
ProviderModel_Registryfirst. The preset editor uses registry-driven dropdowns no free-text entry. - Language combinations in presets use canonical language keys (e.g., "hindi") not raw codes.
- Available languages for a preset = intersection of TTS languages ∩ STT languages ∩ LLM languages. If the selected LLM is marked
is_multilingual, it's excluded from this intersection (most LLMs handle all languages natively). - Each language combination requires an accent (voice). A preset cannot be saved without a voice for each language.
- Each language combination can optionally override TTS and/or STT language resolution via
tts_languageandstt_languagefields. When set, they decouple TTS/STT language from the row's primarylanguage_code.
Auto-derivation: When a preset is saved with language_combinations, the supported_languages array is automatically derived (union of all language_code values). This feeds the customer-facing language dropdown.
Preset Update Guard: When editing a preset that would remove languages used by existing agents, the system warns the superadmin with an impact assessment (affected agent count, removed languages) and suggests creating a new preset instead. The update can still proceed with explicit confirmation.
Provider Model Registry
A catalog of all available LLM/TTS/STT provider-model combinations:
- Superadmin manages via
/superadmin/provider-registryUI - Each entry defines: provider_type, provider_name, model_name, display_name, language_mappings, is_multilingual, is_active
- Language Key-Value Mappings: Instead of raw BCP-47 codes, each provider entry maps canonical language keys (e.g., "hindi", "english") to provider-specific codes (e.g., "hi", "hi-IN"). This solves the problem where different TTS/STT providers use different codes for the same language.
- Multi Language flag (LLM only): Most LLMs support all languages natively. When
is_multilingual: true, the entry doesn't need language mappings and is excluded from the language intersection calculation (presets only consider TTS ∩ STT for available languages). - Preset editor dropdowns are populated exclusively from active registry entries
- Deletion is blocked if any preset references the entry
- Deactivation blocks new references but existing presets continue to resolve
Language Key System
The language key is the single source of truth for language identification across the entire platform:
| Layer | Uses Language Key |
|---|---|
| Provider Registry | Maps key → provider-specific value (e.g., "hindi" → "hi" for Deepgram, "hi-IN" for ElevenLabs) |
| System Config Presets | language_combinations[].language_code stores the key |
| Voice collection | language_key field identifies which language the voice belongs to |
| Customer Dashboard | Language dropdown shows human-readable labels derived from keys |
| Dispatch (Agent Server) | PresetResolver resolves key → actual provider code at call time |
Predefined keys are in src/data/language-keys.ts. Custom keys can be added via the "Other" option in the UI.
Dispatch-Time Language Resolution
When a call is dispatched with a language key (e.g., "hindi"):
PresetResolverfinds the matchinglanguage_combinationin the preset- Determines TTS language key: uses
tts_languageoverride if set, otherwise falls back tolanguage_code - Determines STT language key: uses
stt_languageoverride if set, otherwise falls back tolanguage_code - Looks up the TTS registry entry for that provider+model, finds
language_mappingsentry matching the TTS key → gets provider-specific code - Does the same for STT with the STT key
- If no mapping is found for a key, falls back to the raw key itself
- Passes the resolved provider-specific codes to the Agent Server
This means the same preset+language works correctly across providers that use different codes, and TTS/STT can be independently overridden to different languages than the row's primary language.
Voice Separation
Voices are stored in a separate Voice collection. Presets, agents, and all internal references use a system-generated uid (e.g., vce_abc123...) not the raw provider voice_id. This distinction exists because TTS providers often reuse the same voice_id across different languages or models (e.g., Azure/Google voices that share an identifier but differ by locale). Enforcing uniqueness directly on voice_id made it impossible to add such variants.
| Field | Meaning | Uniqueness |
|---|---|---|
uid | System-generated internal identifier. Used in preset.supported_voices, preset.default_voice, language_combinations[].default_voice/supported_voices, and Agent.tts_voice. | Globally unique (unique: true index) |
voice_id | The TTS provider's own voice identifier (UUID, name, etc.), passed to the TTS SDK at dispatch time. | Unique only per (voice_id, tts_provider, tts_model, language_key) compound index the same voice_id can be reused across different languages/models/providers |
Key rules:
- Same voice used across multiple presets without duplication
- Deleting a voice (by
uid) cascades removes it from all presets'supported_voices,default_voice, andlanguage_combinations - Each voice has a
language_keyfield (canonical key) alongside the rawlanguagefield (provider-specific code) - Voice matching uses
language_key(primary) with fallback tolanguagefor backward compatibility - Dispatch-time resolution:
PresetResolver.resolveForDispatch()andresolveAgentVoiceForDispatch()(indispatch.utils.ts) resolve a voiceuid→ providervoice_idright before building the dispatch payload. The Python Agent Server only ever receives the resolved providervoice_idastts_voiceit has no awareness ofuid. - All Voice CRUD API routes (
/api/v2/config/voices/:voiceId,.../upload-sample,.../sample) take the voiceuidas the:voiceIdroute param, not the providervoice_id. - Legacy data written before this distinction existed is handled by fallback lookups (
uidfirst, thenvoice_id) in the resolver helpers, and normalized permanently by themigrate-voice-uidone-time migration script (see Migration Scripts section below).
System Prompt Fallback
When dispatching, the system prompt is resolved with this priority:
- Preset's
system_prompt(if non-empty) - SystemConfig's
default_system_prompt(if non-empty) - Empty string
Frontend Sanitization
The sanitizeConfigResponse middleware intercepts res.json() and strips internal fields for customer role users.
Stripped from presets: llm_provider, llm_model, tts_provider, tts_model, stt_provider, stt_model, realtime_model, system_prompt, metadata
Stripped from voice objects inside supported_voices: _id, __v, createdAt, updatedAt, orgUID, tts_provider, tts_model
Customers only see: preset name, description, cost, show flag, tts_speed, supported languages, default voice/language, and voice details (uid, voice_id, name, sample_url, description, language, gender). The uid field is required by the customer dashboard to select/persist a voice on an agent it is not stripped.
Admin/owner/superadmin see everything.
Important for new developers: Any new endpoint that returns preset data to customers MUST apply sanitizeConfigResponse middleware. See the Developer Guide section 8 for details.
Versioning
Every config mutation (create/update/delete preset) automatically:
- Increments the
versioncounter on SystemConfig - Creates a
ConfigVersionsnapshot with the full config state - Records who made the change and a description
You can view history, diff between versions, and rollback to any previous version.
API Endpoints
Existing (Modified)
GET /agent-config Returns config from MongoDB (auto-seeds if needed)
Sanitized for customer roleNew Admin CRUD (all require JWT + edit_system_config permission)
GET /api/v2/config/presets List all presets
POST /api/v2/config/presets Create preset
PUT /api/v2/config/presets/:presetId Update preset
DELETE /api/v2/config/presets/:presetId Delete preset (body: { replacement_preset_id })
GET /api/v2/config/voices List voices (optional filters)
POST /api/v2/config/voices Create voice (body: { voice_id, tts_provider, tts_model, language_key, ... })
voice_id is the provider's own identifier the same voice_id
can be reused across different providers/models/languages.
A system-generated `uid` is always returned; use it for all
subsequent references (preset supported_voices, agent tts_voice, etc.)
PUT /api/v2/config/voices/:voiceId Update voice (:voiceId param is the voice's `uid`, not provider voice_id)
DELETE /api/v2/config/voices/:voiceId Delete voice by uid (cascades to presets)
POST /api/v2/config/voices/:voiceId/upload-sample Upload audio sample (:voiceId param is the voice's `uid`)
GET /api/v2/config/voices/:voiceId/sample Stream/redirect audio sample (:voiceId param is the voice's `uid`)
GET /api/v2/config/versions Version history (paginated)
GET /api/v2/config/versions/diff?from=X&to=Y Diff between versions
POST /api/v2/config/versions/:versionNumber/rollback Rollback to versionProvider Registry API (superadmin only)
GET /api/config/provider-registry List entries (filter: provider_type, provider_name, language, page, limit)
GET /api/config/provider-registry/language-keys Get predefined language key definitions (for dropdown population)
POST /api/config/provider-registry Create entry (requires unique type+provider+model, with language_mappings or is_multilingual)
GET /api/config/provider-registry/:id Get single entry
PUT /api/config/provider-registry/:id Update entry (merge semantics)
DELETE /api/config/provider-registry/:id Delete entry (blocked if referenced by presets)How to Verify Step by Step
Pre-requisite: Start both servers
- Start the backend:
cd OP_oktivo_backend/client_backend && npm run dev - Start the frontend:
cd oktivo_app && npm run dev - Watch the backend console on the first API call from any org, you should see:
⚙️ No SystemConfig found for org <orgUID> auto-seeding from agent_config.json ✅ Seeded X voices ✅ Auto-seeded SystemConfig for org <orgUID> (N presets, v1)
Test 1: Superadmin Grant config edit permission to an org
- Login as superadmin
- Go to Organization Management page
- Click the ⋮ menu on any org → Edit
- In the Features checkbox grid, you'll see "edit system config" check it
- Click Update
- The org now has the
edit_system_configpermission
Test 2: Login as customer verify presets load
- Login as a customer user (belonging to the org from Test 1)
- Navigate to Agents page (
/agents) - Verify: The page loads without errors. Existing agents show their preset name
- Open browser DevTools → Network tab → find the
agent-configrequest - Verify response shape:
{ success: true, data: { presets: [...], features, languages, default_cost_per_min }, allowedPresets } - Verify: For customer role, presets should NOT contain
llm_provider,llm_model,tts_provider,tts_model,stt_provider,stt_model,realtime_model
Test 3: Login as admin verify full data visible
- Login as an admin user
- Navigate to Agents page
- Open DevTools → Network → check the
agent-configresponse - Verify: Presets SHOULD contain all provider fields (
llm_provider,tts_provider, etc.) - Compare with the customer response from Test 2 customer should be missing those fields
Test 4: Create a new agent
- Login as a customer
- Go to Agents → New Agent → pick a template → Agent Editor
- Step 1 (Agent Setup): Fill in agent name, prompt, first message
- Step 2 (Voice & Settings):
- Verify: Voice Plan selector shows all visible presets (cards with name, description)
- Select a preset → language dropdown updates to that preset's supported languages
- Select a language → voice grid updates to show voices for that language
- Each voice card shows: name, gender icon, description, audio sample player
- Select a voice → adjust TTS speed slider
- Step 3 (Analysis): Pick an analysis schema
- Click Create Agent
- Verify: Agent created successfully (toast notification)
Test 5: Edit an agent change preset
- Go to Agents → click edit on an existing agent
- In Step 2, change the Voice Plan to a different preset
- Verify: Language dropdown resets to the new preset's default language
- Verify: Voice grid updates to show the new preset's voices
- Verify: If your previously selected voice isn't in the new preset, it auto-selects the default voice
- Save the agent → verify success
Test 6: Make a test call
- Go to Agents → click the test call button on any agent
- Enter a phone number → click Call
- Verify: Call dispatches successfully
- This confirms
presetResolver.resolveForDispatch()works resolves provider/model fields at dispatch time
Test 7: Make a web test call
- Go to Agents → click the web test button (if available)
- Verify: Web call connects and you can talk to the agent
Test 8: Verify preset changes propagate instantly (via API)
- Using curl or Postman (as admin with
edit_system_config):bashcurl -X PUT -H "Authorization: Bearer <admin-jwt>" -H "Content-Type: application/json" \ http://localhost:3000/api/v2/config/presets/<preset_id> \ -d '{"description":"Updated via API"}' - Refresh the Agent Editor page in the browser
- Verify: The preset card now shows "Updated via API" as the description
- No agent documents were modified the change propagated at read time
Test 9: Verify version history (via API)
After the update in Test 8:
curl -H "Authorization: Bearer <admin-jwt>" http://localhost:3000/api/v2/config/versionsVerify: Shows version 2 with the update description and version 1 from the auto-seed.
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
404: No configuration found and seed file unavailable | agent_config.json not found and no SystemConfig in DB | Ensure shared/agent_config.json exists, or run the migration script |
503: Service temporarily unavailable | MongoDB connection issue | Check MongoDB connectivity |
403: Organization does not have permission | Org missing edit_system_config in plan.features | Superadmin → Org Management → Edit org → check "edit system config" |
403: Insufficient role for config management | User is not admin/owner/superadmin | Use an admin account |
400: Preset not found in organization configuration | Invalid preset_id on agent create/update | Check the org's SystemConfig for valid preset_ids |
| Agent test call fails with missing provider fields | Agent was created before migration | Run the migration script to clean up agent documents |
| Frontend shows empty presets | Response parsing mismatch | Clear browser cache, verify backend returns new response shape |
409: A voice with this ID already exists when adding a voice for a different language/model than an existing entry | Environment hasn't run migrate-voice-uid yet, so voice_id still has the old unique: true index at the DB level even though the app code no longer expects it | Run npx tsx src/scripts/migrate-voice-uid.ts (or POST /internal/scripts/migrate-voice-uid) to backfill uid and drop the stale unique index on voice_id |
| Voice selection in agent editor shows "voice not found" or resets to preset default unexpectedly | Frontend/backend version mismatch one side still expects voice_id as the selection key instead of uid | Ensure both app/ and backend/client_backend/ are on the version that uses uid for voice references; clear browser cache |
| Dispatched call uses the wrong/garbled voice with the TTS provider | tts_voice sent to the Agent Server is a uid instead of the resolved provider voice_id (a dispatch call site is missing resolveAgentVoiceForDispatch) | Confirm the dispatch call site resolves agentDoc.tts_voice via resolveAgentVoiceForDispatch() before calling buildAgentConfig() |
FAQ
Q: Do I need to run the migration script? A: Not for new deployments. The auto-seed handles it. Only run the migration script if you have existing agents with old denormalized provider fields that need cleanup.
Q: Can I still edit agent_config.json? A: You can, but it only affects the initial seed. Once a SystemConfig exists in MongoDB, the JSON file is ignored. Edit config through the API instead.
Q: What happens if I delete agent_config.json? A: Nothing, as long as SystemConfig already exists in MongoDB. If you delete it before any org has been seeded, new orgs will get a 404 until you either restore the file or create a SystemConfig manually.
Q: How do I add a new preset? A: POST /api/v2/config/presets with the preset data. Requires admin role + edit_system_config permission.
Q: How do I rollback a bad config change? A: POST /api/v2/config/versions/:versionNumber/rollback. Every mutation creates a version snapshot, so you can always go back.
Q: Can I reuse the same provider voice_id for a different language or model now? A: Yes. voice_id (the TTS provider's own identifier) is no longer required to be globally unique. Uniqueness is only enforced per (voice_id, tts_provider, tts_model, language_key) combination so the same voice_id can appear multiple times as long as the provider, model, or language differs. Every Voice document also gets a system-generated uid, which is what's actually referenced by presets and agents; you never need to manage it manually.
Q: I'm on an environment created before the voice uid change do I need to do anything? A: Run the one-time migration once: npx tsx src/scripts/migrate-voice-uid.ts or POST /internal/scripts/migrate-voice-uid (with x-api-secret). It backfills uid on existing voices, updates preset and agent references, and drops the old unique index on voice_id. It's idempotent safe to re-run or run on an already-migrated environment (it will just report zero changes).