Deletes the _periodic_refresh task and its supporting state
(_refresh_task, _refresh_failures, _refresh_backoff_until,
_REFRESH_BACKOFF_BASE/MAX, _DEFAULT_REFRESH_INTERVAL, refresh_interval
kwarg) from MCPClientManager. Push notifications and operator-driven
manual refresh now cover all catalog-update needs; the long-running
4-hour timer was dead complexity that obscured the per-user pool
work to come.
Catalog freshness on auto-reconnect is preserved by scheduling an
unblocking _refresh_server task on the mcp-loop after _connect_one
succeeds; the calling thread returns immediately so half-open
recovery latency does not double. Adds MCPClientManager.reconnect_sync
(clears the circuit, closes any existing session, calls _connect_one,
clears stale catalog on failure).
Wires a new pair of operator endpoints —
POST /v1/api/admin/mcp-servers/{name}/refresh and
/v1/api/admin/mcp-servers/{name}/reconnect — that fan out to all
nodes through the existing _internal route family, with per-row
"Refresh" and "Reconnect" buttons in the MCP Servers admin tab.
The new node-internal paths /api/_internal/mcp-{refresh,reconnect}/
are gated to the approve scope to prevent direct unprivileged
reconnects bypassing the console's admin.mcp gate. Internal
endpoints return generic error messages and a filtered status
payload (no command/url) to keep transport details admin-gated.
Drops the [mcp] refresh_interval setting, the
--mcp-refresh-interval CLI flag, and the matching config-mapping
entry; updates docs/architecture.md, docs/tools.md,
docs/settings.md, and the three PlantUML diagrams that referenced
the periodic loop.
Tradeoffs (intentional):
- Idle nodes will not auto-rejoin a recovered MCP server until
traffic arrives or an operator clicks Reconnect. The previous
background reconnection loop is gone by design — push
notifications + operator controls replace it.
- Console fan-out blocks on the slowest node (existing pattern);
not changed here.
This is Phase 1 of the OAuth-MCP series — feature subtraction
ahead of per-user state.
12 KiB
System Settings
See also: Settings Architecture diagram
The system settings feature provides database-backed configuration for server
nodes. Settings are stored in the system_settings table and managed through
the admin API or console Settings tab. This replaces config.toml for
non-bootstrap settings on server entry points, while the CLI continues to read
config.toml directly.
Overview
Settings follow a typed registry pattern: every storable setting has a
SettingDef entry in settings_registry.py with type, default, description,
validation constraints, and a restart_required flag. Unknown keys are rejected
at the API boundary.
At runtime, ConfigStore loads all settings from storage into an in-memory
cache. Reads are lock-free dict lookups on an immutable snapshot. Writes acquire
a lock, persist to storage, and swap the cache atomically.
Precedence
Settings resolution differs between entry points:
| Entry point | Chain |
|---|---|
Server (turnstone-server) |
CLI flag > ConfigStore > registry default |
CLI (turnstone) |
CLI flag > config.toml > argparse default |
The server's apply_config() ignores config.toml sections that overlap with
ConfigStore. A startup warning is logged for each overlapping key, directing
users to the admin Settings API.
Per-Model Sampling Overrides
The global model.temperature, model.max_tokens, and model.reasoning_effort
settings serve as cluster-wide defaults. Individual models can override these
via per-model settings in the model_definitions table (admin Models tab).
Resolution order for sampling parameters:
| Priority | Source |
|---|---|
| 1 (highest) | Per-model override (set in Models tab) |
| 2 | Global default (set in Settings tab) |
| 3 | Registry default (code) |
When a per-model override is NULL (empty in the UI), the global default is
used. Switching models via /model <alias> re-resolves sampling parameters
from the new model's overrides or global defaults.
Removed settings: model.name and model.context_window have been removed
from ConfigStore. Model names and context windows are now configured per-model
in the Models tab. A startup warning is logged if these keys appear in
config.toml.
Plan / task agent overrides
plan_agent and task_agent sub-sessions resolve independently from the
conversation model so operators can pick a cheaper/faster model for
autonomous loops:
| Setting | Purpose |
|---|---|
model.plan_alias |
Alias used for plan_agent sub-sessions. Falls back to [model].plan_model in config.toml, then [model].agent_model, then the session's active model. |
model.task_alias |
Alias used for task_agent sub-sessions. Same fallback chain as plan_alias. |
model.plan_effort |
Reasoning effort for plan_agent (none / minimal / low / medium / high / xhigh / max). Defaults to high. |
model.task_effort |
Reasoning effort for task_agent. Empty string means "inherit from the session". |
All four are live-editable from the Settings tab and take effect on the next sub-agent invocation — no restart required.
Bootstrap vs ConfigStore
Bootstrap settings are required before storage is available (database
connection, Redis, auth secrets, server bind address). These stay in
config.toml and environment variables.
| Category | Section | Where |
|---|---|---|
| API credentials | [api] |
config.toml / env |
| Database | [database] |
config.toml / env |
| Auth | [auth] |
config.toml / env |
| Console bind | [console] |
config.toml / env |
ConfigStore settings are loaded from the database after storage initialization:
| Section | Settings |
|---|---|
model |
default_alias, temperature, max_tokens, reasoning_effort, plan_alias, task_alias, plan_effort, task_effort |
session |
instructions, retention_days, compact_max_tokens, auto_compact_pct |
tools |
timeout, truncation, agent_max_turns, skip_permissions, search, search_threshold, search_max_results |
server |
workstream_idle_timeout, max_workstreams |
cluster |
node_fan_out_limit, mcp_max_servers |
mcp |
config_path, registry_url |
ratelimit |
enabled, requests_per_second, burst, trusted_proxies |
health |
backend_probe_interval, backend_probe_timeout, circuit_breaker_threshold, circuit_breaker_cooldown |
judge |
enabled, model, provider, base_url, api_key, confidence_threshold, max_context_ratio, timeout, read_only_tools, output_guard, redact_secrets, cancel_on_approval |
interface |
close_tab_action, theme |
skills |
discovery_url |
memory |
relevance_k, fetch_limit, max_content, nudge_cooldown, nudges |
Settings are addressed by dotted key (e.g. memory.relevance_k). Each has a
declared type (int, float, str, bool), optional min_value/max_value
range, optional choices list, and an is_secret flag.
Storage
The system_settings table (migration 015) stores settings as JSON-encoded
values with a composite primary key of (key, node_id):
| Column | Type | Description |
|---|---|---|
key |
text | Dotted setting key (e.g. model.temperature) |
value |
text | JSON-encoded value |
node_id |
text | Node ID for per-node overrides (empty string = global) |
is_secret |
int | 1 if the setting contains secrets |
changed_by |
text | Username of last editor |
created |
text | ISO timestamp |
updated |
text | ISO timestamp |
Per-node overrides layer on top of global settings. When ConfigStore loads,
it fetches global settings first, then overlays per-node values.
Admin API
Four endpoints on the console server, all requiring the admin.settings
permission.
GET /v1/api/admin/settings
List all settings with their effective values, defaults, and metadata.
Response: 200
{
"settings": [
{
"key": "model.temperature",
"value": 0.7,
"source": "storage",
"type": "float",
"description": "Sampling temperature",
"section": "model",
"is_secret": false,
"node_id": "",
"changed_by": "admin",
"updated": "2026-03-14T10:00:00",
"restart_required": false
}
]
}
GET /v1/api/admin/settings/schema
Return the full registry catalog (all defined settings with metadata). Useful for building dynamic admin UIs.
Response: 200
{
"schema": [
{
"key": "model.temperature",
"type": "float",
"default": 0.5,
"description": "Sampling temperature",
"section": "model",
"is_secret": false,
"min_value": 0.0,
"max_value": 2.0,
"choices": null,
"restart_required": false
}
]
}
PUT /v1/api/admin/settings/{key}
Update a setting. The value is validated against the registry (type coercion,
range, choices). Secret settings (is_secret=true) cannot be written via the
API -- they must be configured via config.toml or environment variables.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
key |
string | Dotted setting key (e.g. model.temperature) |
Request body:
{
"value": 0.7,
"node_id": ""
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
value |
any | yes | -- | New value (type-coerced against registry) |
node_id |
string | no | "" |
Node ID for per-node override |
Response (success): 200
{
"key": "model.temperature",
"value": 0.7,
"source": "storage",
"type": "float",
"description": "Sampling temperature",
"section": "model",
"is_secret": false,
"node_id": "",
"changed_by": "admin",
"updated": "",
"restart_required": false
}
Errors:
| Status | Condition |
|---|---|
| 400 | Unknown key, invalid value, type mismatch, out of range |
| 403 | Secret setting (must use config.toml or env) |
DELETE /v1/api/admin/settings/{key}
Reset a setting to its registry default by removing it from storage.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
key |
string | Dotted setting key |
Query parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
node_id |
string | no | "" |
Node ID (empty = global) |
Response (success): 200
{"status": "ok", "key": "model.temperature", "default": 0.5}
Response (not found): 404
{"error": "Setting 'model.temperature' has no stored value"}
Secret Settings
Settings with is_secret=True (currently only judge.api_key) are blocked
from the write API with a 403 response. This prevents accidental exposure
through the admin UI or audit logs. Secret settings must be configured via
config.toml or environment variables.
The list endpoint masks secret values: stored secrets appear as "***"
rather than their actual value.
Hot Reload
ConfigStore caches all settings in memory for fast, lock-free reads. To
refresh the cache after external changes (e.g. direct database edits or
cluster-wide propagation):
POST /v1/api/_internal/config-reload
This triggers ConfigStore.reload(), which re-reads all settings from storage
and atomically swaps the cache. The version counter increments on every
reload.
Behavior after reload:
- New workstreams pick up updated values immediately (via
session_factory) - Existing sessions keep their frozen configuration (settings are captured at workstream creation time, not read on every turn)
- Settings marked
restart_required=Trueneed a server restart to take effect
Migration from config.toml
On startup, warn_migrated_settings() scans config.toml for keys that are
now managed by ConfigStore. Each overlap produces a warning:
WARNING config.toml [model] temperature is now managed via Settings API —
this value will be ignored. Use the admin Settings tab or
PUT /v1/api/admin/settings/model.temperature to configure.
To migrate:
- Note the values from
config.tomlfor sections that overlap with ConfigStore - Use
PUT /v1/api/admin/settings/{key}or the console Settings tab to set each value - Remove the migrated sections from
config.toml - Restart the server to verify no warnings
SDK
Python
from turnstone.sdk import TurnstoneConsole
with TurnstoneConsole("http://localhost:9090", token="tok_xxx") as admin:
# List all settings with effective values
result = admin.list_settings()
for s in result["settings"]:
print(f"{s['key']} = {s['value']} (source: {s['source']})")
# Get the schema catalog
schema = admin.get_settings_schema()
# Update a setting
admin.update_setting("model.temperature", value=0.7)
# Update with per-node override
admin.update_setting("model.temperature", value=0.3, node_id="node-2")
# Reset to default
admin.delete_setting("model.temperature")
TypeScript
import { TurnstoneConsole } from "@turnstone/sdk";
const admin = new TurnstoneConsole({
baseUrl: "http://localhost:9090",
token: "tok_xxx",
});
// List all settings
const result = await admin.listSettings();
for (const s of result.settings) {
console.log(`${s.key} = ${s.value} (source: ${s.source})`);
}
// Get schema catalog
const schema = await admin.getSettingsSchema();
// Update a setting
await admin.updateSetting("model.temperature", { value: 0.7 });
// Reset to default
await admin.deleteSetting("model.temperature");
Architecture
See Settings Architecture diagram for the full data flow covering server startup, admin API writes, hot reload, and settings precedence.