One home for what the user decided
The platform keeps durable user choices in one typed home. The key names the scope — platform, application, or conversation — configuration remains the ceiling, and an explicit Save changes writes the current conversation.
A signed-in user makes durable choices about how an application behaves: whether memory participates, which model an agent uses, and which tools, skills, MCP servers, or named-service operations it may call. Some choices are platform-wide; agent choices can belong to one conversation.
one row per durable choice and exact scope
(user_id, bundle_id, key) → value_json [+ subsystem = owning store]
Everything rides user_bundle_props. Each setting family is a
store: a thin SDK layer that owns its subsystem, typed key convention,
versioned JSON shape, fallbacks, and write rules. Applications add stores; they do not
write anonymous JSON rows ad hoc.
Configuration is the ceiling
Application configuration declares what is possible. A user may narrow that space but cannot widen it. The scoped choice and the configuration meet on every turn:
The contract has four practical rules:
- Clamp on write, intersect on read. A choice outside what config grants is stripped or ignored.
- Configuration supplies the fallback. If no scoped row exists, the configured inventory and model apply.
- Partial writes preserve sibling fields from their read snapshot. One tool toggle does not erase the model pick. Concurrent writes to the same key are last-writer-wins; callers serialize when they need stronger ordering.
- Per-turn reads, fail open. The runtime reads the record fresh at the turn's application point and treats every failure as "use the configured behavior" — a broken settings store never breaks the app.
Scope lives in the key
Memory preferences (subsystem='memory') — the user's memory
posture — remain platform-wide with bundle_id='*' and
key='preferences'.
Agent selection (subsystem='agents') uses two exact keys under
the real application id:
user baseline for future conversations
agent_selection:<agent_id>
effective choice for one conversation
conversation:<conversation_id>:agent_selection:<agent_id>
The baseline is optional. If it does not exist, a new conversation starts from the
application configuration. On the first capabilities read or first turn, KDCube inserts
the current baseline into the conversation key with
ON CONFLICT DO NOTHING. The conversation then owns its full model and capability
selection; later baseline changes do not rewrite it.
application-configured fallback
↓
optional user baseline: agent_selection:main
↓ first read/turn; insert if absent
conversation:conv-42:agent_selection:main
↓ local draft → Save changes
next turn in conv-42 reads this exact row
Save is deliberate, and scope is visible
The chat composer calls agent_capabilities with the current
conversation_id. Toggles update a local draft. Nothing is written while the
user clicks, and sending a message does not save the draft. Save changes
sends one scoped agent_selection_update patch.
The same picker body has three shells, but two explicit targets:
- The composer popover and expanded in-chat modal share the current conversation draft.
- The served
capabilitieswidget follows the caller's scope: chat carries the sameconversation_id; an independently mounted widget has no id and edits the baseline for future conversations.
That independent, unscoped mode is how
agent_selection:<agent_id> is populated. Its UI says
defaults for future conversations. A host opening the widget from chat
forwards conversation_id, so presentation can change without changing
persistence scope. Switching conversations drops unsaved chat edits, and late responses
cannot overwrite another conversation's picker.
The runtime enforces the selection
The deny-list narrows whole tool groups or individual tools, MCP servers or listed tools,
skills, subagent delegation, and named-service namespaces. It can reach individual
operations and actions such as object.search and
object.action.send. A denial is not merely hidden UI: runtime removes it from
the turn inventory or rejects it at named-service dispatch.
A durable choice scoped to one conversation belongs in the settings construct. Conversation execution state — turns, timeline events, cache warmness, summaries, and artifacts — does not. Secrets never belong here either; credentials stay in the user secret lifecycle behind Connection Hub.
Documentation on GitHub
The live docs behind this entry: