Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,16 +934,21 @@ async def chat_completion(request: Request):
# below never hit an UnboundLocalError when a client sends {"tools": false}
# (re-audit J1: was a silent opaque 500 — _build_chat_system_prompt is
# called with both names regardless of the tools flag).
# Resolve the user's message UNCONDITIONALLY. It used to be populated only
# inside the use_tools gate, so with tools disabled it stayed empty — and the
# claim-check, which needs the request to spot an unbacked persistence ask,
# silently degraded to reply-pattern-only. A safety check must not depend on
# an unrelated feature flag.
last_user_text = ""
for _m in reversed(messages):
if _m.get("role") == "user" and isinstance(_m.get("content"), str):
last_user_text = _m["content"]
break
has_attachment = False

# ── Tool Calling: check if last user message matches a skill ──
use_tools = body.get("tools", True) # frontend can disable with tools:false
if use_tools:
for m in reversed(messages):
if m.get("role") == "user" and isinstance(m.get("content"), str):
last_user_text = m["content"]
break
# ── Slash commands (BEFORE skill check / attachment check) ──
# Type /help, /skills, /cost, /version, /status, /who, /clear in chat
# to invoke meta-controls without an LLM round-trip. Slash dispatch
Expand Down
16 changes: 16 additions & 0 deletions tests/test_claim_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,19 @@ def test_no_user_request_falls_back_to_reply_patterns():
"""Callers that don't pass the request still get the reply-side checks."""
assert cc.find_unbacked_claims("I have ingested the instruction set.",
actions_taken=set())


def test_last_user_text_is_resolved_outside_the_tools_flag():
"""The claim check needs the user's message to spot an unbacked persistence
ask. It used to be populated only inside `if use_tools:`, so a request with
tools disabled left it "" and the check silently degraded to reply-patterns
only — which is exactly the half that phrasing can evade. A safety check must
not depend on an unrelated feature flag."""
src = (_REPO / "routes" / "chat.py").read_text()
body = src[src.index("async def chat_completion"):]
assign = body.index('last_user_text = ""')
flag = body.index('use_tools = body.get("tools"')
assert assign < flag, "last_user_text must be resolved BEFORE the use_tools branch"
# and the resolution loop itself must sit outside the branch
loop = body.index("for _m in reversed(messages):")
assert loop < flag, "the resolution loop must not live inside `if use_tools:`"