diff --git a/tests/test_app_js.py b/tests/test_app_js.py
index cbaec8f0..0bfa9a55 100644
--- a/tests/test_app_js.py
+++ b/tests/test_app_js.py
@@ -708,6 +708,131 @@ def test_audio_roles_gated_to_openai_sdk_providers() -> None:
assert '_providerCarriesAudio((md && md.provider) || "openai")' in body
+# Tile keys that are deliberately NOT ``ModelCapabilities`` fields.
+# ``supports_rerank`` is a registry-level flag read off the model row.
+_NON_DATACLASS_TILES = {"supports_rerank"}
+
+
+def test_capability_bool_lift_agrees_with_the_backend_coercion() -> None:
+ """The tile lift coerces a stored capability the way the backend does.
+
+ The capabilities dict is hand-edited JSON, so a stored string
+ ``"false"`` is TRUTHY to JS while ``apply_capability_overrides`` reads
+ it as ``False``. Lifting it into a tile with bare ``!!`` renders the
+ row checked and then persists boolean ``true`` on the next save —
+ inverting the capability without the operator touching it. For
+ ``server_parses_reasoning`` that silently disables the inline tag scan,
+ which is the leak model_turn's own comment names.
+
+ Cases are generated FROM the Python table, so a spelling added on one
+ side and not the other fails here rather than in the field.
+ """
+ import tempfile
+
+ from turnstone.core.model_turn import _CAPABILITY_BOOL_STRINGS
+
+ admin = _CONSOLE_ADMIN_JS.read_text(encoding="utf-8")
+ table = re.search(r"const _CAP_BOOL_STRINGS = \{.*?\n\};", admin, re.S)
+ fn = re.search(r"function _capBool\(value\) \{.*?\n\}", admin, re.S)
+ assert table and fn, "capability bool coercion not found in admin.js"
+
+ checks: list[str] = []
+ for spelling, expected in _CAPABILITY_BOOL_STRINGS.items():
+ # Same spelling, uppercased, and padded — the Python arm strips and
+ # lowercases before lookup, so the JS must too.
+ for variant in (spelling, spelling.upper(), f" {spelling} "):
+ lit = json.dumps(variant)
+ checks.append(
+ f"if (_capBool({lit}) !== {json.dumps(expected)}) "
+ f"throw new Error('spelling ' + {lit} + ' -> ' + _capBool({lit}));"
+ )
+ checks += [
+ "if (_capBool(true) !== true) throw new Error('boolean true');",
+ "if (_capBool(false) !== false) throw new Error('boolean false');",
+ "if (_capBool(1) !== true) throw new Error('number 1');",
+ "if (_capBool(0) !== false) throw new Error('number 0');",
+ # Unrecognized values must NOT coerce — the caller leaves them in the
+ # raw JSON instead of rewriting them (the thinking_mode policy).
+ "if (_capBool('maybe') !== undefined) throw new Error('garbage string');",
+ "if (_capBool(null) !== undefined) throw new Error('null');",
+ "if (_capBool({}) !== undefined) throw new Error('object');",
+ ]
+ harness = table.group(0) + chr(10) + fn.group(0) + chr(10) + chr(10).join(checks)
+
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".mjs", delete=False) as f:
+ f.write(harness)
+ tmp = f.name
+ try:
+ proc = subprocess.run(["node", tmp], capture_output=True, text=True, timeout=15)
+ except FileNotFoundError:
+ pytest.skip("node binary not available on PATH")
+ finally:
+ os.unlink(tmp)
+ assert proc.returncode == 0, (
+ f"_capBool disagrees with the backend coercion. stderr={proc.stderr!r}"
+ )
+
+ # The lift must actually USE it — a reverted call site would leave the
+ # helper in place and every case above still passing.
+ assert "_modelCapsExplicit[k] = !!capsObj[k]" not in admin
+ assert "const asBool = _capBool(capsObj[k]);" in admin
+
+
+def test_capability_tiles_agree_with_the_capabilities_dataclass() -> None:
+ """Every tile is a real capability, rendered, and defaulted like Python.
+
+ The tile matrix is a hand-maintained mirror of
+ :class:`ModelCapabilities`, so it drifts silently: a renamed field
+ leaves a tile that writes a key nothing reads, and a JS default that
+ disagrees with the dataclass shows the operator a state the backend
+ would not apply. A tile whose key is not a capability field is
+ allowed only by NAME (``_NON_DATACLASS_TILES``) — a blanket
+ "skip what the dataclass lacks" would exempt exactly the rename this
+ test exists to catch.
+ """
+ from turnstone.core.providers._protocol import ModelCapabilities
+
+ admin = _CONSOLE_ADMIN_JS.read_text(encoding="utf-8")
+ html = _CONSOLE_INDEX.read_text(encoding="utf-8")
+
+ keys_block = re.search(r"const _MODEL_CAP_KEYS = \[(.*?)\];", admin, re.S)
+ defaults_block = re.search(r"const _MODEL_CAP_DEFAULTS = \{(.*?)\};", admin, re.S)
+ assert keys_block and defaults_block, "capability tile matrix not found in admin.js"
+ keys = re.findall(r'"(\w+)"', keys_block.group(1))
+ defaults = {
+ k: v == "true" for k, v in re.findall(r"(\w+):\s*(true|false)", defaults_block.group(1))
+ }
+ assert keys, "no capability tile keys parsed"
+
+ # The tile is only reachable if it renders INSIDE the container
+ # ``_modelTileEl`` queries; outside it, ``_modelGetTile`` silently falls
+ # back to the default and a saved ``true`` is rewritten as ``false``.
+ grid_start = html.index('id="model-capgrid"')
+ grid = html[grid_start : html.index("", grid_start)]
+
+ caps = ModelCapabilities()
+ for key in keys:
+ assert f'data-cap="{key}"' in grid, f"tile {key} renders outside #model-capgrid"
+ assert key in defaults, f"tile {key} has no entry in _MODEL_CAP_DEFAULTS"
+ # No hasattr escape hatch: a tile whose key is neither a capability
+ # field nor a known registry flag writes a key nothing reads, which
+ # is the first drift this test exists to catch.
+ assert hasattr(caps, key) or key in _NON_DATACLASS_TILES, (
+ f"tile {key} is neither a ModelCapabilities field nor a known registry flag"
+ )
+ if hasattr(caps, key):
+ assert defaults[key] == getattr(caps, key), (
+ f"tile default for {key} disagrees with ModelCapabilities"
+ )
+ assert set(defaults) == set(keys), "_MODEL_CAP_DEFAULTS and _MODEL_CAP_KEYS disagree"
+
+ # The inline-tag scan is a FALLBACK for servers with no reasoning parser
+ # (and for misconfigured ones). An operator running vLLM/llama.cpp with a
+ # parser configured needs a discoverable way to say so — without it the
+ # only route is hand-editing the raw capabilities JSON.
+ assert "server_parses_reasoning" in keys
+
+
def test_model_response_controls_are_capability_driven_and_sparse() -> None:
"""The model shelf surfaces Responses-only scalar controls without
hard-coding GPT-5.6 IDs or pinning inherited capability-table values."""
diff --git a/tests/test_cancel.py b/tests/test_cancel.py
index 7a3041df..1d589c3b 100644
--- a/tests/test_cancel.py
+++ b/tests/test_cancel.py
@@ -1193,6 +1193,49 @@ class TestOrphanArmDutiesGate:
tracker.record_success.assert_called_once()
+class TestOlderSitesAskTheSharedPredicate:
+ """The two supersession sites that PREDATE the shared predicate ask it
+ too, so generation 0 stays unscoped at both.
+
+ Each carried its own inline copy of the formula, so the drift the
+ helper exists to prevent had two live places to start from. A bare
+ ``!=`` in either reads a direct seam caller (generation 0) as an
+ orphan: ``_check_cancelled`` would raise a cancel on a live turn, and
+ ``_compaction_event`` would stamp a live compaction ``superseded`` —
+ which suppresses its end notice, so the operator watching a real
+ compaction fail would be told nothing at all.
+ """
+
+ def test_check_cancelled_leaves_an_unscoped_generation_alone(self, tmp_db):
+ session = _make_session()
+ session._generation = 7
+ session._check_cancelled(0) # unscoped — a direct seam caller
+ session._check_cancelled(7) # the live generation
+ with pytest.raises(GenerationCancelled):
+ session._check_cancelled(2) # a real orphan
+
+ def test_compaction_event_calls_an_unscoped_generation_live(self, tmp_db):
+ class _Recorder(NullUI):
+ def __init__(self):
+ super().__init__()
+ self.events = []
+
+ def on_compaction(self, event):
+ self.events.append(event)
+
+ ui = _Recorder()
+ session = _make_session(ui=ui)
+ session._generation = 7
+ failed_end = {"phase": "end", "ok": False, "reason": "cancelled", "trigger": "manual"}
+ session._compaction_event(0, dict(failed_end))
+ session._compaction_event(2, dict(failed_end))
+
+ assert ui.events[0]["superseded"] is False
+ assert ui.events[0]["notice"] is True # the operator is told
+ assert ui.events[1]["superseded"] is True
+ assert ui.events[1]["notice"] is False
+
+
class TestSupersessionVerdictAgreement:
"""Every arm of one streaming turn must reach the SAME supersession
verdict for the same generation shape. Generation 0 is unscoped, so
diff --git a/tests/test_midstream_retry.py b/tests/test_midstream_retry.py
index 22d86dfa..1089fa27 100644
--- a/tests/test_midstream_retry.py
+++ b/tests/test_midstream_retry.py
@@ -828,16 +828,29 @@ class TestRecreateWindowClassification:
patch.object(
session, "_prepare_wire_messages", side_effect=ValueError("malformed turn 7")
),
- pytest.raises(WirePreparationError),
+ pytest.raises(WirePreparationError) as excinfo,
):
session.send("test")
+ # The wrapper carries the cause's CLASS, not its text. Callers that
+ # render ``str(exc)`` directly — the interactive retry arm's dashboard
+ # line — would otherwise re-emit the stored-history text the fatal
+ # formatter just withheld, on a surface that reaches the operator and
+ # the persisted error row.
+ assert str(excinfo.value) == "ValueError"
+ assert "malformed turn 7" not in str(excinfo.value)
+
tracker.record_failure.assert_not_called()
fb_spy.assert_called_once()
provider.create_streaming.assert_not_called()
errors = ui.of("error")
assert errors and "stored history" in errors[-1]
- assert "malformed turn 7" in errors[-1]
+ # Reached the dedicated branch — identified by the cause's CLASS.
+ # Its message is withheld all the way through the live send path:
+ # it is our lowering's text over stored history, and this string
+ # is persisted (see TestWirePrepFaultRedaction).
+ assert "ValueError" in errors[-1]
+ assert "malformed turn 7" not in errors[-1]
def test_fallback_prep_fault_continues_walk(self, tmp_db):
"""A prep fault on one lane must not abort the walk: the next
@@ -951,6 +964,37 @@ class TestFallbackFailureRedaction:
assert not any("SECRETKEY" in i for i in infos)
+class TestWirePrepFaultRedaction:
+ def test_operator_text_carries_the_cause_class_only(self, tmp_db):
+ """A wire-prep fault renders its cause's CLASS, never its message.
+
+ Every other branch of the formatter tails the backend's own
+ diagnostic text, which is what the operator needs. This one is
+ different in kind: ``prepare_wire`` is our lowering over the
+ session's STORED HISTORY, so its exception message can quote that
+ history — and this string is both shown to the operator and
+ persisted to ``last_error``, which a coordinating agent reads.
+ ``redact_credentials`` is a best-effort regex by its own
+ docstring, so it is no floor for arbitrary conversation text.
+ """
+ from turnstone.core.model_turn import WirePreparationError
+
+ session = _make_session(RecordingUI())
+ cause = ValueError("malformed block in turn 4: {'text': 'the user's private notes'}")
+ exc = WirePreparationError(str(cause))
+ exc.__cause__ = cause
+
+ rendered = session._format_backend_error(exc)
+
+ assert rendered is not None
+ assert "ValueError" in rendered
+ assert "private notes" not in rendered
+ assert "malformed block" not in rendered
+ # Still actionable: the operator learns what failed and what to try.
+ assert "stored history" in rendered
+ assert "/compact" in rendered
+
+
class TestPrepareWireLaneCaps:
"""The per-attempt wire prep folds with the SERVING lane's
capabilities — a fallback whose template rejects mid-conversation
diff --git a/turnstone/console/static/admin.js b/turnstone/console/static/admin.js
index 3193c550..1cd8afab 100644
--- a/turnstone/console/static/admin.js
+++ b/turnstone/console/static/admin.js
@@ -6434,6 +6434,7 @@ const _MODEL_CAP_KEYS = [
"supports_web_search",
"supports_temperature",
"supports_effort",
+ "server_parses_reasoning",
"supports_verbosity",
"supports_pro_mode",
"supports_transcription",
@@ -6448,6 +6449,7 @@ const _MODEL_CAP_DEFAULTS = {
supports_web_search: false,
supports_temperature: true,
supports_effort: false,
+ server_parses_reasoning: false,
supports_verbosity: false,
supports_pro_mode: false,
supports_transcription: false,
@@ -6455,6 +6457,37 @@ const _MODEL_CAP_DEFAULTS = {
supports_audio_input: false,
supports_rerank: false,
};
+// Mirrors ``_CAPABILITY_BOOL_STRINGS`` / ``apply_capability_overrides`` in
+// core/model_turn.py. The capabilities dict is hand-edited JSON, so a stored
+// string "false" is TRUTHY to JS while the backend reads it as False — lifting
+// it into a tile with bare ``!!`` would render the row checked and then persist
+// boolean true, inverting the capability on a routine save. Returns undefined
+// for anything the backend would not coerce; such a value stays in the raw JSON
+// rather than being silently rewritten (the thinking_mode policy below).
+const _CAP_BOOL_STRINGS = {
+ "true": true,
+ yes: true,
+ on: true,
+ 1: true,
+ "false": false,
+ no: false,
+ off: false,
+ 0: false,
+ "": false,
+};
+function _capBool(value) {
+ if (typeof value === "boolean") return value;
+ // bool-before-int, like the Python arm: JS has no bool/int overlap, but
+ // 0/1 rows must coerce the same way the backend coerces them.
+ if (typeof value === "number") return !!value;
+ if (typeof value === "string") {
+ const spelling = value.trim().toLowerCase();
+ return Object.prototype.hasOwnProperty.call(_CAP_BOOL_STRINGS, spelling)
+ ? _CAP_BOOL_STRINGS[spelling]
+ : undefined;
+ }
+ return undefined;
+}
let _modelCapsBaseline = {}; // known-model table values (display + delta base)
let _modelCapsExplicit = {}; // keys that persist: saved-in-JSON + user-toggled
@@ -7912,7 +7945,9 @@ function showEditModelModal(definitionId) {
_modelCapsExplicit = {};
_MODEL_CAP_KEYS.forEach(function (k) {
if (k in capsObj) {
- _modelCapsExplicit[k] = !!capsObj[k];
+ const asBool = _capBool(capsObj[k]);
+ if (asBool === undefined) return; // unrepresentable — leave it raw
+ _modelCapsExplicit[k] = asBool;
delete capsObj[k];
}
});
diff --git a/turnstone/console/static/index.html b/turnstone/console/static/index.html
index 0430e1ed..2138be4f 100644
--- a/turnstone/console/static/index.html
+++ b/turnstone/console/static/index.html
@@ -1978,6 +1978,13 @@
>Reasoning-effort control
+