From 2c629d5a119db2ee69ecd6cc45464630e42fa402 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 1 Jun 2026 17:16:27 -0400 Subject: [PATCH] =?UTF-8?q?session:=20readline=20+=20=CF=80*=20preflight?= =?UTF-8?q?=20(33/66=20=E2=86=92=201/2)=20+=20Qwen=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 15 ++++++++-- arborist/cli.py | 31 +++++++++++++++++++ arborist/qa/providence_query.py | 53 +++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 47e4168..2d47049 100644 --- a/Makefile +++ b/Makefile @@ -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,) diff --git a/arborist/cli.py b/arborist/cli.py index afe5860..85a6110 100644 --- a/arborist/cli.py +++ b/arborist/cli.py @@ -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 ) diff --git a/arborist/qa/providence_query.py b/arborist/qa/providence_query.py index 4def8ef..2384673 100644 --- a/arborist/qa/providence_query.py +++ b/arborist/qa/providence_query.py @@ -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.