session: readline + π* preflight (33/66 → 1/2) + Qwen default

Three fixes from interactive feedback:

1. **readline in the REPL** — input() now has up-arrow line history
   and emacs/vi editing. Persists to ~/.arborist/sessions/.history
   across runs (2000-entry cap). Without this, every typo required
   retyping the whole line.

2. **π* canonical projection preflight in providence_query** —
   bare arithmetic ("33/66", "0.1+0.2") was tokenized by FTS5 as
   ["33", "66"] and matched the year-article titles "33" and "66"
   (low-DF integer tokens dominate bm25); the LLM then "interpreted"
   the bare fraction as years 33 AD / 66 AD. The legacy query()
   pipeline has the same preflight at the top of its pipeline;
   providence_query was missing it (drift from the #000015/#000030
   π* registry work). Now firing the arithmetic@v1 / algebra@v1 /
   logic-kernel@v1 kernel short-circuits retrieval + LLM entirely
   and returns audit_mode=CANONICAL_PROJECTION with the exact
   rational answer.

   Live: `make query Q="33/66"` → "1/2" (was HYBRID citing year
   articles).

3. **Qwen as default LLM for `make session`** — interactive multi-
   turn use rewards a smarter model over Hermes-3-8B's fast-sweep
   strengths. LLM=hermes still toggles back to 8B; SESSION_LLM_*
   env vars override.

66 verify+session+providence tests pass.
This commit is contained in:
russell@unturf.com 2026-06-01 17:16:27 -04:00
parent 508fec6975
commit 2c629d5a11
No known key found for this signature in database
3 changed files with 96 additions and 3 deletions

View file

@ -211,9 +211,18 @@ query: bootstrap ## ask the corpus a question [JSON=1 BURN=1 LLM=qwen|hermes REP
# tree node with a Bates id + a session-wide subtree_hash. Forking is
# implicit: /cd to a parent node and ask again → sibling under that
# parent. See arborist/qa/session.py + docs/sessions.md.
session: bootstrap ## interactive multi-turn Q&A REPL [SID=... LLM=qwen|hermes ANSWER_MODE=... BURN=1]
$(if $(LLM_ENDPOINT),@echo "# llm: $(LLM_ENDPOINT) / $(LLM_MODEL)" >&2,)
$(ARBORIST) --shards-dir $(SHARDS_DIR) session $(if $(SID),--sid $(SID),) --top-k $(QUERY_TOP_K) $(if $(LLM_ENDPOINT),--endpoint $(LLM_ENDPOINT) --model $(LLM_MODEL),) $(if $(ANSWER_MODE),--answer-mode $(ANSWER_MODE),) $(if $(BURN),--burn,)
#
# Session defaults to Qwen3.6-27B (better answers for interactive
# multi-turn use). Override with LLM=hermes for the 8B fast path.
SESSION_LLM_ENDPOINT ?= https://qwen.ai.unturf.com/v1
SESSION_LLM_MODEL ?= Qwen3.6-27B-UD-Q4_K_XL.gguf
ifeq ($(LLM),hermes)
SESSION_LLM_ENDPOINT := https://hermes.ai.unturf.com/v1
SESSION_LLM_MODEL := adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic
endif
session: bootstrap ## interactive multi-turn Q&A REPL [SID=... LLM=qwen|hermes ANSWER_MODE=... BURN=1]; defaults LLM=qwen
@echo "# llm: $(SESSION_LLM_ENDPOINT) / $(SESSION_LLM_MODEL)" >&2
$(ARBORIST) --shards-dir $(SHARDS_DIR) session $(if $(SID),--sid $(SID),) --top-k $(QUERY_TOP_K) --endpoint $(SESSION_LLM_ENDPOINT) --model $(SESSION_LLM_MODEL) $(if $(ANSWER_MODE),--answer-mode $(ANSWER_MODE),) $(if $(BURN),--burn,)
session-list: bootstrap ## list all session dbs in ~/.arborist/sessions/
$(ARBORIST) session --list $(if $(JSON),--json,)

View file

@ -7502,6 +7502,15 @@ def build_parser() -> argparse.ArgumentParser:
return p
def _ignore(fn, *a, **kw):
"""Run fn, swallow any exception. Used for atexit hooks where
failure (e.g. write to a closed file) shouldn't crash shutdown."""
try:
fn(*a, **kw)
except Exception:
pass
def _cmd_session(args: argparse.Namespace) -> int:
"""Merkle-rooted multi-turn Q&A REPL with forks.
@ -7661,6 +7670,28 @@ def _session_repl(args: argparse.Namespace, sess) -> int:
)
from arborist.store import connect as _connect
# readline gives up-arrow line history + emacs/vi line editing
# in the input() call below. The module is in the stdlib but
# importing it is what wires it into input(). Persist history to
# ~/.arborist/sessions/.history so commands survive across runs.
try:
import readline
_hist = (
Path.home() / ".arborist" / "sessions" / ".history"
)
_hist.parent.mkdir(parents=True, exist_ok=True)
try:
readline.read_history_file(str(_hist))
except (FileNotFoundError, OSError):
pass
readline.set_history_length(2000)
import atexit
atexit.register(
lambda: _ignore(readline.write_history_file, str(_hist))
)
except ImportError:
pass
shards_dir = getattr(args, "global_shards_dir", None) or getattr(
args, "shards_dir", None
)

View file

@ -308,6 +308,59 @@ def providence_query(
def emit(self, *a, **kw): pass
progress = _NoProgress()
# 0. π* canonical projection preflight (math / algebra / logic).
# For questions that are pure arithmetic ("33/66", "0.1+0.2"),
# pure algebra ("x^2 - 4 = 0"), or pure propositional logic
# ("A AND NOT B"), short-circuit retrieval + LLM entirely and
# return the deterministic canonical bytes. Without this,
# "33/66" was tokenized by FTS5 as ["33", "66"] which matched
# the year-article titles "33" and "66" — LLM then "interpreted"
# the bare fraction as years 33 AD / 66 AD. The legacy query()
# pipeline has the same preflight; providence_query was missing
# it (#000015 / #000030).
# Off-by-default for tests + bench paths that need deterministic
# retrieval shapes; ON for interactive (set by callers).
if policy.get("canonical_projection_preflight", True):
try:
from arborist.qa.query import _canonical_projection_preflight
preflight = _canonical_projection_preflight(question)
except Exception:
preflight = None
if preflight is not None:
pi_star_ref, canonical_bytes = preflight
answer_text = canonical_bytes.decode("utf-8", errors="replace")
progress.emit("canonical_projection.hit", pi_star_ref=pi_star_ref)
return {
"status": "canonical_projection",
"answer_text": answer_text,
"raw_answer": answer_text,
"audit_mode": "CANONICAL_PROJECTION",
"verifier_method": "canonical_projection",
"n_quotes": 0, "n_verified": 0, "violations": [],
"sources": [],
"pi_star_ref": pi_star_ref,
"capacity": {},
"timings": {"total": _time.time() - t_total},
"model": model_id,
"cache_key": "",
"lookup_path": "preflight_canonical",
"burned_existing": 0,
"audit_event_hash": None,
"run_dag_root": None,
"context_root": "",
"prompt_chars": {
"messages_total": 0, "system_prompt": 0,
"evidence_or_context": 0, "user_question": len(question),
"grounding_reminder": 0,
},
"answer_chars": len(answer_text),
"elapsed_s": round(_time.time() - t_total, 3),
"unverified_quotes": [],
"partially_verified_quotes": [],
"warrant_proven_claim_idxs": [],
"format_collapsed": None,
}
# 1. RETRIEVAL ONLY — fast phase. Body + title FTS5 in parallel,
# merge by best bm25 per doc_root. Body alone misses canonical
# primary articles when high-DF tokens dilute the match (e.g.