Release v0.51.268 — Release IJ (stage-b1 — low-risk perf + provider/clarify fixes) (#3671)

* perf(providers): O(1) codex cache merge membership checks (#3656)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* fix(models): add MiniMax-M3 to WebUI MiniMax fallback catalog test (#3627)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(config): make DeepSeek reasoning-effort heuristic position-independent (#3650)

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>

* fix(clarify): don't stash clarify draft while submission is in flight (#3651)

Co-authored-by: carryzuo00 <carryzuo00@gmail.com>

* perf(sessions): batch lineage report child fetch by parent id (#3659)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* perf(sessions): batch orphan sidecar state.db existence probes (#3657)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* test(streaming): pin DOM-INFLIGHT reattach invariant (#3572)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.268 — Release IJ (stage-b1)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: carryzuo00 <carryzuo00@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-06-05 10:44:02 -07:00
1 parent f1211e1f0c
commit 442b033e67
13 files changed
+499 -44

No files matched your search

+15
View File
@@ -3,6 +3,21 @@
## [Unreleased]
## [v0.51.268] — 2026-06-05 — Release IJ (stage-b1 — low-risk perf + provider/clarify fixes)
### Fixed
- **The DeepSeek reasoning-effort heuristic is now position-independent**, so the reasoning-effort selector correctly appears for V/R-series DeepSeek models even when a custom aggregator prepends a provider slug to the id (e.g. `@custom:name:DeepSeek-V4-Flash` → `my-provider-deepseek-v4-flash`). Non-reasoning variants (`deepseek-chat`, `deepseek-coder`) stay correctly excluded, and a provider slug that happens to start with `v`/`r` (e.g. `vertex`) no longer falsely triggers the version guard. (#3650, @happy5318)
- **The clarify draft is no longer stashed while a clarify submission is in flight**, preventing a just-submitted answer from being re-stashed as a leftover draft when the prompt resolves. (#3651, @Carry00)
### Performance
- **Codex live-model cache merge now uses O(1) set membership** instead of repeated list scans when reconciling the visible Codex model cache against live ids. (#3656, @pamnard)
- **Session lineage child fetch is batched into a single query** (parents fetched with one `IN (...)` query and grouped in memory) instead of one SQLite query per parent segment. (#3659, @pamnard)
- **Orphan CLI-sidecar existence probes are batched** into chunked `IN (...)` queries via a new `agent_session_rows_existing()` helper, replacing the per-row `agent_session_row_exists()` connection in the sidebar prune path. Degrades safely to "assume present" on any error so a transient failure never prunes a real session. (#3657, @pamnard)
### Internal
- **Added a static-analysis contract test pinning the DOM-INFLIGHT reattach persistence invariant** (#3040): every `INFLIGHT[activeSid]` write in `send()` is paired with a `saveInflightState()` call so a reconnect/session-switch reseeds from durable state. (#3572, @rodboev)
- **Added a regression test asserting `MiniMax-M3` is present in the MiniMax fallback model catalog.** (#3627, @rodboev)
## [v0.51.267] — 2026-06-04 — Release II (stage-r17 — TTS + CSRF forwarded-header security hardening)
### Security
+13 -4
View File
@@ -595,7 +595,10 @@ def read_session_lineage_report(db_path: Path, session_id: str | None, max_hops:
segment_ids = {row['id'] for row in segments}
child_rows: list[dict] = []
for parent in segments:
parent_ids = [row['id'] for row in segments]
children_by_parent: dict[str, list[dict]] = {pid: [] for pid in parent_ids}
if parent_ids:
placeholders = ','.join('?' * len(parent_ids))
cur.execute(
f"""
SELECT s.id,
@@ -607,13 +610,19 @@ def read_session_lineage_report(db_path: Path, session_id: str | None, max_hops:
{ended_expr},
{end_reason_expr}
FROM sessions s
WHERE s.parent_session_id = ?
ORDER BY s.started_at DESC
WHERE s.parent_session_id IN ({placeholders})
""",
(parent['id'],),
parent_ids,
)
for child_row in cur.fetchall():
child = dict(child_row)
parent_id = child.get('parent_session_id')
if parent_id in children_by_parent:
children_by_parent[parent_id].append(child)
for parent in segments:
parent_children = children_by_parent.get(parent['id'], [])
parent_children.sort(key=lambda row: row.get('started_at') or 0, reverse=True)
for child in parent_children:
if child['id'] in segment_ids:
continue
if _is_continuation_session(parent, child):
+11 -4
View File
@@ -2246,10 +2246,17 @@ def _candidate_supports_reasoning(candidate: str) -> bool:
return True
if "step" in token_set or normalized.startswith("step"):
return True
if normalized.startswith(("deepseek-v", "deepseek-r")):
return True
if len(tokens) >= 2 and tokens[0] == "deepseek" and tokens[1].startswith(("v", "r")):
return True
if "deepseek" in token_set:
# Check the token immediately after "deepseek" for a V-series or R-series
# version marker (e.g. "v4", "r1"). This is position-independent so it
# handles bare "deepseek-v4-flash" and @custom:name:DeepSeek-V4-Flash
# (→ "my-provider-deepseek-v4-flash") equally, while correctly excluding
# non-reasoning models like deepseek-chat and deepseek-coder.
# Using tokens.index() ensures the provider slug (e.g. "vertex" in
# "vertex-deepseek-chat") cannot falsely trigger the version guard.
idx = tokens.index("deepseek")
if idx + 1 < len(tokens) and tokens[idx + 1].startswith(("v", "r")):
return True
return False
+66 -25
View File
@@ -2723,6 +2723,71 @@ def _active_state_db_path() -> Path:
return hermes_home / 'state.db'
def _agent_state_db_path(*, profile=None) -> Path | None:
"""Return agent ``state.db`` for *profile*, or ``None`` when unavailable."""
if isinstance(profile, str) and profile:
db_path = _get_profile_home(profile) / 'state.db'
if not db_path.exists():
db_path = _active_state_db_path()
else:
db_path = _active_state_db_path()
if not db_path.exists():
return None
return db_path
def agent_session_rows_existing(
session_ids: list[str] | set[str] | frozenset[str],
*,
profile=None,
) -> frozenset[str]:
"""Return session ids confirmed present in the agent ``sessions`` table.
Used by the sidebar orphan-prune path (#3238) to batch existence probes
instead of opening one SQLite connection per candidate row.
Degrades safely to ``frozenset(wanted)`` (assume all present) on any error,
when the DB is missing, or when the ``sessions`` table is absent — matching
``agent_session_row_exists()`` so a transient failure never causes pruning.
"""
wanted = {str(sid).strip() for sid in (session_ids or []) if str(sid or "").strip()}
if not wanted:
return frozenset()
try:
import sqlite3
except ImportError:
return frozenset(wanted)
db_path = _agent_state_db_path(profile=profile)
if db_path is None:
return frozenset(wanted)
try:
with closing(sqlite3.connect(str(db_path))) as conn:
cur = conn.cursor()
cur.execute("PRAGMA table_info(sessions)")
cols = {str(row[1]) for row in cur.fetchall()}
if 'id' not in cols:
return frozenset(wanted)
existing: set[str] = set()
ids = list(wanted)
chunk_size = 500
for i in range(0, len(ids), chunk_size):
chunk = ids[i:i + chunk_size]
placeholders = ','.join('?' * len(chunk))
cur.execute(
f"SELECT id FROM sessions WHERE id IN ({placeholders})",
chunk,
)
existing.update(str(row[0]).strip() for row in cur.fetchall())
return frozenset(existing)
except Exception:
logger.debug(
"agent_session_rows_existing probe failed for %d ids",
len(wanted),
exc_info=True,
)
return frozenset(wanted)
def agent_session_row_exists(session_id: str, *, profile=None) -> bool:
"""Return True if ``session_id`` still has a backing row in the agent state.db.
@@ -2739,31 +2804,7 @@ def agent_session_row_exists(session_id: str, *, profile=None) -> bool:
sid = str(session_id or "").strip()
if not sid:
return False
try:
import sqlite3
except ImportError:
return True
if isinstance(profile, str) and profile:
db_path = _get_profile_home(profile) / 'state.db'
if not db_path.exists():
db_path = _active_state_db_path()
else:
db_path = _active_state_db_path()
if not db_path.exists():
# No agent DB at all on this instance — can't claim the row is gone.
return True
try:
with closing(sqlite3.connect(str(db_path))) as conn:
cur = conn.cursor()
cur.execute("PRAGMA table_info(sessions)")
cols = {str(row[1]) for row in cur.fetchall()}
if 'id' not in cols:
return True
cur.execute("SELECT 1 FROM sessions WHERE id = ? LIMIT 1", (sid,))
return cur.fetchone() is not None
except Exception:
logger.debug("agent_session_row_exists probe failed for %s", sid, exc_info=True)
return True
return sid in agent_session_rows_existing([sid], profile=profile)
def _sidebar_title_is_generic_webui(title: str | None) -> bool:
+3 -1
View File
@@ -1866,8 +1866,10 @@ def get_providers() -> dict[str, Any]:
# follow-up to v0.51.19 #1812.)
if pid == "openai-codex":
live_ids = _read_live_provider_model_ids("openai-codex")
live_id_set = set(live_ids)
for mid in _read_visible_codex_cache_model_ids():
if mid not in live_ids:
if mid not in live_id_set:
live_id_set.add(mid)
live_ids.append(mid)
live_models = _models_from_live_provider_ids(pid, live_ids)
if live_models:
+40 -10
View File
@@ -22,6 +22,7 @@ import threading
import time
import uuid
import re
from collections import defaultdict
from pathlib import Path
from contextlib import closing
from urllib.parse import parse_qs, urlsplit
@@ -2900,7 +2901,7 @@ from api.models import (
_is_empty_partial_activity_message,
_hide_from_default_sidebar,
prune_session_from_index,
agent_session_row_exists,
agent_session_rows_existing,
ensure_cron_project,
is_cron_session,
is_safe_session_id,
@@ -5422,12 +5423,13 @@ def handle_get(handler, parsed) -> bool:
# the sidecar is never pruned and the stale row lingers in the
# sidebar forever (there is no WebUI delete affordance for it).
# Drop rows whose backing agent row is genuinely gone. We probe
# state.db directly (agent_session_row_exists) rather than trust
# state.db directly (agent_session_rows_existing) rather than trust
# cli_by_id absence, because get_cli_sessions() caps at
# CLI_VISIBLE_SESSION_LIMIT (20) — an existing session can fall
# out of that window and look deleted. Native WebUI sessions
# (source == "webui") that merely have a CLI ancestor are never
# pruned.
_orphan_probe_rows = []
_kept_after_orphan_prune = []
for s in webui_sessions:
_sid = s.get("session_id")
@@ -5436,15 +5438,43 @@ def handle_get(handler, parsed) -> bool:
and is_cli_session_row(s)
and not _session_source_is_webui(s)
and _sid not in cli_by_id
and not agent_session_row_exists(_sid, profile=s.get("profile"))
):
try:
prune_session_from_index(_sid)
except Exception:
logger.debug("Failed to prune orphaned CLI sidecar %s", _sid, exc_info=True)
diag.stage("prune_orphaned_cli_sidecar")
continue
_kept_after_orphan_prune.append(s)
_orphan_probe_rows.append(s)
else:
_kept_after_orphan_prune.append(s)
if _orphan_probe_rows:
rows_by_profile: dict[object, list[dict]] = defaultdict(list)
for row in _orphan_probe_rows:
rows_by_profile[row.get("profile")].append(row)
missing_orphan_ids: set[str] = set()
for profile_key, rows in rows_by_profile.items():
probe_ids = [
str(row.get("session_id")).strip()
for row in rows
if str(row.get("session_id") or "").strip()
]
existing = agent_session_rows_existing(
probe_ids,
profile=profile_key if isinstance(profile_key, str) and profile_key else None,
)
for row in rows:
sid = str(row.get("session_id") or "").strip()
if sid and sid not in existing:
missing_orphan_ids.add(sid)
for s in _orphan_probe_rows:
_sid = str(s.get("session_id") or "").strip()
if _sid in missing_orphan_ids:
try:
prune_session_from_index(_sid)
except Exception:
logger.debug(
"Failed to prune orphaned CLI sidecar %s",
_sid,
exc_info=True,
)
diag.stage("prune_orphaned_cli_sidecar")
continue
_kept_after_orphan_prune.append(s)
webui_sessions = _kept_after_orphan_prune
for s in webui_sessions:
meta = cli_by_id.get(s.get("session_id"))
+2
View File
@@ -3284,6 +3284,8 @@ function _startClarifyCountdown(pending) {
function _stashClarifyDraft(reason) {
if (reason !== "expired" && reason !== "terminal") return false;
const submit = $("clarifySubmit");
if (submit && submit.classList.contains("loading")) return false;
const input = $("clarifyInput");
const draft = String((input && input.value) || "").trim();
if (!draft) return false;
@@ -207,3 +207,44 @@ def test_unsupported_model_families_and_versions():
)
# ── position-independent DeepSeek version detection (#3650) ───────────────────
#
# The DeepSeek V/R-series check keys off the token immediately AFTER "deepseek"
# rather than requiring "deepseek" to lead the string. This keeps detection
# working when a provider slug is prepended (e.g. a custom aggregator rewriting
# @custom:name:DeepSeek-V4-Flash → "my-provider-deepseek-v4-flash"), while a
# provider slug that happens to start with "v"/"r" (e.g. "vertex") must NOT by
# itself satisfy the version guard.
@pytest.mark.parametrize(
"model_id",
[
"vertex-deepseek-v4-flash",
"my-provider-deepseek-r1",
"newapi-deepseek-v5",
],
)
def test_deepseek_version_detected_after_provider_slug(model_id):
efforts = cfg.resolve_model_reasoning_efforts(model_id, provider_id="custom:newapi")
assert set(efforts) >= {"low", "medium", "high"}, (
f"{model_id}: DeepSeek V/R-series marker after a provider slug should "
"expose reasoning efforts (position-independent detection, #3650)"
)
@pytest.mark.parametrize(
"model_id",
[
"deepseek-chat",
"deepseek-coder",
"vertex-deepseek-chat",
],
)
def test_deepseek_non_reasoning_variants_excluded(model_id):
efforts = cfg.resolve_model_reasoning_efforts(model_id, provider_id="custom:newapi")
assert efforts == [], (
f"{model_id}: non-reasoning DeepSeek variant must NOT resolve reasoning "
"support, and a 'v'/'r' provider slug must not falsely trigger it (#3650)"
)
+212
View File
@@ -0,0 +1,212 @@
"""Static-analysis contract tests for the DOM-INFLIGHT reattach sync invariant (#3040).
Every site that writes to INFLIGHT[] must persist the change so that a reconnect
or session-switch can seed the new closure from durable state rather than a stale
DOM snapshot. These tests pin the structural invariants to catch regressions that
remove a persistence call or add a new INFLIGHT write without pairing it with one.
"""
from __future__ import annotations
import re
from pathlib import Path
MESSAGES_JS = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text(
encoding="utf-8"
)
_LINES = MESSAGES_JS.splitlines()
def _line_window(line_no: int, radius: int = 20) -> str:
"""Return up to `radius` lines on either side of `line_no` (1-based) as one string."""
start = max(0, line_no - 1 - radius)
end = min(len(_LINES), line_no - 1 + radius + 1)
return "\n".join(_LINES[start:end])
def _inflight_write_line_numbers() -> list[int]:
"""Return 1-based line numbers for every INFLIGHT[activeSid] assignment site."""
results = []
for i, line in enumerate(_LINES, start=1):
if re.search(r"INFLIGHT\[activeSid\]\s*=\s*\{", line) or re.search(
r"INFLIGHT\[activeSid\]\s*\|\|\s*\(INFLIGHT\[activeSid\]\s*=\s*\{", line
):
results.append(i)
return results
def test_inflight_write_sites_exist():
"""Sanity-check: messages.js must contain at least 3 INFLIGHT[activeSid] write sites."""
sites = _inflight_write_line_numbers()
assert len(sites) >= 3, (
f"Expected at least 3 INFLIGHT[activeSid] assignment sites, found {len(sites)}. "
"If send() was refactored, update the expected count."
)
def _extract_send_body() -> str:
"""Extract the body of the send() function up to attachLiveStream() as one string."""
# send() starts with `async function send(` and the INFLIGHT writes in the
# optimistic path and catch block both precede the attachLiveStream call.
send_start = MESSAGES_JS.find("async function send(")
assert send_start != -1, "async function send() not found in messages.js."
attach_call = MESSAGES_JS.find("attachLiveStream(activeSid, streamId", send_start)
assert attach_call != -1, "attachLiveStream call not found inside send()."
return MESSAGES_JS[send_start:attach_call]
def test_send_inflight_writes_are_paired_with_saveInflightState():
"""The INFLIGHT[activeSid] assignments in send() must be covered by saveInflightState()
calls within the same send() scope.
send() makes the optimistic UI write before calling /api/chat/start so that a tab
reload during the network round-trip can restore the user's message. If the write
is not persisted, the only recovery window is lost.
There are 3 write sites but only 2 saveInflightState calls because the catch-block
fallback write (line ~530) shares its persistence call with the post-start path:
if /api/chat/start succeeds, the post-start block's saveInflightState covers it;
if /api/chat/start fails, the error handler deletes INFLIGHT[activeSid] anyway.
A minimum of 2 saveInflightState calls pins this intentional design.
"""
send_body = _extract_send_body()
# Count how many INFLIGHT assignments appear in send() before attachLiveStream.
write_count = len(re.findall(r"INFLIGHT\[activeSid\]\s*=\s*\{", send_body))
assert write_count >= 2, (
f"Expected at least 2 INFLIGHT[activeSid] write sites in send(), found {write_count}. "
"The optimistic write and the catch-block fallback should both be present."
)
# There must be at least 2 saveInflightState calls covering the write paths.
# (The catch-block fallback write shares the post-start persistence call.)
persist_count = send_body.count("saveInflightState(")
assert persist_count >= 2, (
f"send() has only {persist_count} saveInflightState() call(s) before attachLiveStream "
"(expected at least 2: one before /api/chat/start for optimistic reload recovery, "
"one after start for stream-id persistence). "
"Removing either leaves a reload window with unrecoverable INFLIGHT state."
)
def test_syncInflightAssistantMessage_calls_throttled_persist():
"""syncInflightAssistantMessage() must call _throttledPersist() to flush the live
assistant text into persistent INFLIGHT state on every token and interim_assistant event."""
fn_start = MESSAGES_JS.find("function syncInflightAssistantMessage(){")
assert fn_start != -1, "syncInflightAssistantMessage() not found in messages.js."
fn_body_start = MESSAGES_JS.find("{", fn_start)
# Walk braces to find the closing brace of the function body.
depth = 1
i = fn_body_start + 1
while i < len(MESSAGES_JS) and depth:
if MESSAGES_JS[i] == "{":
depth += 1
elif MESSAGES_JS[i] == "}":
depth -= 1
i += 1
fn_body = MESSAGES_JS[fn_body_start:i]
assert "_throttledPersist()" in fn_body, (
"syncInflightAssistantMessage() must call _throttledPersist() so each "
"live-text update is scheduled for persistence. Without this, a reconnect "
"seeds from the last tool-event snapshot and loses all interim assistant text."
)
def test_interim_assistant_calls_syncInflightAssistantMessage_after_accumulating_text():
"""The interim_assistant event handler must call syncInflightAssistantMessage()
after it appends to assistantText and visibleInterimSnippets.
Without this call, the INFLIGHT assistant message is not updated before a
potential reconnect, causing already-streamed content to be lost.
"""
handler_start = MESSAGES_JS.find("source.addEventListener('interim_assistant',")
assert handler_start != -1, "interim_assistant event listener not found in messages.js."
# Extract the handler body up to the matching closing brace.
brace_pos = MESSAGES_JS.find("{", handler_start)
depth = 1
i = brace_pos + 1
while i < len(MESSAGES_JS) and depth:
if MESSAGES_JS[i] == "{":
depth += 1
elif MESSAGES_JS[i] == "}":
depth -= 1
i += 1
handler_body = MESSAGES_JS[brace_pos:i]
assert "assistantText" in handler_body, (
"interim_assistant handler does not reference assistantText — handler may have been refactored."
)
assert "visibleInterimSnippets" in handler_body, (
"interim_assistant handler does not reference visibleInterimSnippets — "
"interim snippet tracking was removed."
)
# syncInflightAssistantMessage() must appear AFTER the accumulation lines.
accum_pos = handler_body.find("visibleInterimSnippets.push(")
sync_pos = handler_body.find("syncInflightAssistantMessage()")
assert sync_pos != -1, (
"interim_assistant handler does not call syncInflightAssistantMessage(). "
"Without this, INFLIGHT is not updated with interim text and a reconnect loses progress."
)
assert accum_pos != -1 and sync_pos > accum_pos, (
"syncInflightAssistantMessage() must appear AFTER visibleInterimSnippets.push() "
"so that the persisted snapshot includes the freshly accumulated snippet."
)
def test_attachLiveStream_reconnect_seeds_from_INFLIGHT_not_querySelector():
"""On reconnect, attachLiveStream() must seed assistantText from INFLIGHT state,
not from a live DOM query (querySelector).
Seeding from the DOM is fragile: an off-screen or BFCache-restored session may
have no rendered content, so the closure would start empty and duplicate tokens
would overwrite the already-displayed response.
"""
fn_start = MESSAGES_JS.find("function attachLiveStream(")
assert fn_start != -1, "attachLiveStream() not found in messages.js."
# Capture up to 60 lines after the function signature for the seed section.
fn_head_end = fn_start
for _ in range(60):
fn_head_end = MESSAGES_JS.find("\n", fn_head_end + 1)
if fn_head_end == -1:
break
seed_section = MESSAGES_JS[fn_start:fn_head_end]
# The reconnect seed must come from INFLIGHT[activeSid].
assert "INFLIGHT[activeSid]" in seed_section, (
"attachLiveStream() seed section does not reference INFLIGHT[activeSid]. "
"Reconnect must read accumulated text from durable INFLIGHT state."
)
assert "reconnecting" in seed_section, (
"attachLiveStream() seed section does not branch on `reconnecting`. "
"The reconnect path must distinguish a fresh open from a re-attach."
)
# The seed must NOT use querySelector to read DOM content for the initial text.
idx = seed_section.find("let assistantText")
assert idx != -1, "let assistantText not found in seed section — capture window too narrow or variable renamed"
seed_up_to_assistantText = seed_section[:idx]
assert "querySelector" not in seed_up_to_assistantText, (
"attachLiveStream() reads DOM via querySelector before seeding assistantText. "
"Use INFLIGHT state exclusively; DOM queries are unreliable for off-screen sessions."
)
def test_syncInflightAssistantMessage_exists_as_closure_inside_attachLiveStream():
"""syncInflightAssistantMessage() must be defined as a closure inside attachLiveStream()
so it closes over the live assistantText and reasoningText accumulators.
If it were a module-level function it could not access the per-stream state,
breaking the token→INFLIGHT sync for every concurrent stream.
"""
attach_start = MESSAGES_JS.find("function attachLiveStream(")
assert attach_start != -1, "attachLiveStream() not found."
# syncInflightAssistantMessage definition must appear after attachLiveStream opens.
sync_def = MESSAGES_JS.find("function syncInflightAssistantMessage(){", attach_start)
assert sync_def != -1, (
"syncInflightAssistantMessage() is not defined inside attachLiveStream(). "
"It must be a closure over the stream accumulators (assistantText, reasoningText)."
)
# And there must NOT be a top-level definition before attachLiveStream.
top_level_def = MESSAGES_JS.find("function syncInflightAssistantMessage(){")
assert top_level_def == sync_def, (
"syncInflightAssistantMessage() has a top-level definition. "
"It must live inside attachLiveStream() to close over per-stream state."
)
@@ -104,6 +104,55 @@ def test_agent_session_row_exists_handles_missing_sessions_table(tmp_path, monke
assert models.agent_session_row_exists("whatever") is True
def test_agent_session_rows_existing_returns_present_subset(tmp_path, monkeypatch):
from api import models
home = tmp_path / "home"
home.mkdir()
_make_state_db(home / "state.db", ["sess-a", "sess-b"])
monkeypatch.setattr(models, "_active_state_db_path", lambda: home / "state.db")
existing = models.agent_session_rows_existing(
["sess-a", "sess-b", "sess-missing", "", None]
)
assert existing == frozenset({"sess-a", "sess-b"})
def test_agent_session_rows_existing_safe_when_db_missing(tmp_path, monkeypatch):
from api import models
monkeypatch.setattr(
models, "_active_state_db_path", lambda: tmp_path / "nope" / "state.db"
)
wanted = ["orphan-a", "orphan-b"]
assert models.agent_session_rows_existing(wanted) == frozenset(wanted)
def test_agent_session_rows_existing_batches_over_500_ids(tmp_path, monkeypatch):
from api import models
home = tmp_path / "home"
home.mkdir()
ids = [f"sess-{i:04d}" for i in range(600)]
_make_state_db(home / "state.db", ids[:300])
monkeypatch.setattr(models, "_active_state_db_path", lambda: home / "state.db")
existing = models.agent_session_rows_existing(ids)
assert existing == frozenset(ids[:300])
def test_agent_session_rows_existing_normalizes_whitespace_in_probe_ids(tmp_path, monkeypatch):
from api import models
home = tmp_path / "home"
home.mkdir()
_make_state_db(home / "state.db", ["cli-padded"])
monkeypatch.setattr(models, "_active_state_db_path", lambda: home / "state.db")
existing = models.agent_session_rows_existing([" cli-padded "])
assert existing == frozenset({"cli-padded"})
# ── Orphan-prune decision predicate (mirrors the sidebar merge-loop guard) ──
+9
View File
@@ -115,6 +115,15 @@ def test_minimax_fallback_provider_label():
# ── _PROVIDER_MODELS ──────────────────────────────────────────────────────────
def test_minimax_provider_models_has_m3():
"""_PROVIDER_MODELS['minimax'] must include MiniMax-M3."""
models = config._PROVIDER_MODELS.get('minimax', [])
ids = [m['id'] for m in models]
assert 'MiniMax-M3' in ids, (
f"MiniMax-M3 missing from _PROVIDER_MODELS['minimax']. Found: {ids}"
)
def test_minimax_provider_models_has_m2_7():
"""_PROVIDER_MODELS['minimax'] must include MiniMax-M2.7."""
models = config._PROVIDER_MODELS.get('minimax', [])
+27
View File
@@ -263,3 +263,30 @@ def test_lineage_report_endpoint_returns_404_for_unknown_session(tmp_path):
routes.handle_get(handler, parsed)
assert captured == {"status": 404, "message": "Session not found"}
def test_lineage_report_preserves_child_order_for_each_segment_parent(tmp_path):
"""Behavioural coverage for batched child fetch: started_at DESC per parent."""
conn = _ensure_state_db(tmp_path / "state.db")
t0 = time.time() - 200
try:
_insert_state_row(conn, "lineage_report_root", started_at=t0, ended_at=t0 + 5, end_reason="compression")
_insert_state_row(
conn,
"lineage_report_tip",
parent="lineage_report_root",
started_at=t0 + 6,
ended_at=t0 + 15,
end_reason="user_stop",
)
_insert_state_row(conn, "lineage_report_child_old", parent="lineage_report_tip", started_at=t0 + 8)
_insert_state_row(conn, "lineage_report_child_new", parent="lineage_report_tip", started_at=t0 + 20)
report = agent_sessions.read_session_lineage_report(tmp_path / "state.db", "lineage_report_tip")
assert [child["session_id"] for child in report["children"]] == [
"lineage_report_child_new",
"lineage_report_child_old",
]
finally:
conn.close()
+11
View File
@@ -608,6 +608,17 @@ class TestClarifyCardTimerLogic:
assert '\\n\\n${draft}' in body, \
'preserved clarify drafts should be separated from existing composer text'
def test_stash_clarify_draft_skips_while_submit_in_flight(self):
src = self._get_js().read_text()
m = re.search(r'function _stashClarifyDraft.*?(?=\nfunction |\nasync function |\Z)',
src, re.DOTALL)
assert m, '_stashClarifyDraft function not found'
body = m.group(0)
assert 'classList.contains("loading")' in body, \
'must not stash draft while a clarify submit is in flight (#3651)'
assert body.index('classList.contains("loading")') < body.index('clarifyInput'), \
'loading guard must short-circuit before reading the draft'
def test_cancel_stream_does_not_preserve_clarify_draft(self):
src = self._get_js().read_text()
m = re.search(r"source\.addEventListener\('cancel'.*?\n \}\);",