KDCube
← Our Journal

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:

CONFIGURATION CEILING → SCOPED USER CHOICE → THE TURN application configuration the ceiling and fallback configured tools · skills · services allowed MCP servers and models configured default model no row → use this declared per application · agent scoped user choice optional user baseline agent_selection:<agent> exact conversation selection conversation:<id>:agent_selection:<agent> deny-lists · model pick · pending clamp on write explicit Save changes the typed key names the scope = the turn runs with effective = configured ∩ chosen read fresh every turn denied means uncallable fails open settings failure → configured behavior new conversation — materialize the baseline once; if absent, seed from application configuration. later baseline changes do not rewrite an existing conversation.
No scoped row means the application-configured behavior; a stored choice can narrow but never widen it.

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 capabilities widget follows the caller's scope: chat carries the same conversation_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.

ONE PICKER BODY, TWO EXPLICIT TARGETS composer "+" popover active conversation draft expanded in-chat modal same conversation draft served capabilities widget conversation id or baseline mode one picker body the caller supplies the scope conversation target · explicit Save changes conversation:<id>:agent_selection:<agent> future-conversation baseline · unscoped widget agent_selection:<agent>
One picker body, two explicit targets: the active conversation or the baseline for future conversations.

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.

KDCube Shorts · 06.07.2026 · updated 12.07.2026