Fix Leo restart truth and read command reliability

This commit is contained in:
twentyOne2x 2026-07-16 07:14:31 +02:00
parent 021aaf6f09
commit b59bee8844
10 changed files with 144 additions and 21 deletions

View file

@ -230,7 +230,7 @@ def parse_args() -> argparse.Namespace:
add_output_flag(status)
search = sub.add_parser("search", help="Search canonical claims and soul/context rows.")
search.add_argument("query")
search.add_argument("query", nargs="+")
search.add_argument("--limit", type=int, default=5)
search.add_argument("--context-limit", type=int, default=5)
add_output_flag(search)
@ -250,7 +250,7 @@ def parse_args() -> argparse.Namespace:
add_output_flag(edges)
context = sub.add_parser("context", help="Build an agent-ready KB context bundle for a question.")
context.add_argument("query")
context.add_argument("query", nargs="+")
context.add_argument("--limit", type=int, default=4)
context.add_argument("--context-limit", type=int, default=6)
add_output_flag(context)
@ -331,7 +331,7 @@ def parse_args() -> argparse.Namespace:
add_output_flag(list_proposals)
search_proposals = sub.add_parser("search-proposals", help="Search KB mutation proposals across statuses.")
search_proposals.add_argument("query")
search_proposals.add_argument("query", nargs="+")
search_proposals.add_argument("--status", default="all")
search_proposals.add_argument("--limit", type=int, default=10)
add_output_flag(search_proposals)
@ -346,7 +346,10 @@ def parse_args() -> argparse.Namespace:
)
add_output_flag(decision_matrix_status)
return parser.parse_args()
args = parser.parse_args()
if isinstance(getattr(args, "query", None), list):
args.query = " ".join(args.query)
return args
def sql_literal(value: str) -> str:

View file

@ -865,11 +865,23 @@ def _schema_contract_issues(response: str, contracts: list[dict[str, Any]]) -> l
return True
return False
if "runtime_persistence" in contract_ids and affirmative(
r"(?:state\.db|session\s+jsonl).{0,100}"
r"(?:in[- ]memory|ephemeral|disappears?|vanishes?|erased|discarded|gone|lost|deleted|absent|zeroed|empty)"
):
issues.append("runtime_persistence_contradiction")
if "runtime_persistence" in contract_ids:
direct_contradiction = affirmative(
r"(?:state\.db|session\s+jsonl).{0,100}"
r"(?:in[- ]memory|ephemeral|disappears?|vanishes?|erased|discarded|gone|lost|deleted|absent|zeroed|empty)"
)
anaphoric_contradiction = bool(
re.search(
r"(?:state\.db|session\s+jsonl).{0,360}"
r"(?:restart|recycle|process launch).{0,100}"
r"(?:can|may|will)\s+(?:clear|erase|reset|delete|drop|lose)\s+"
r"(?:them|both|the files?|the session|state\.db|session\s+jsonl)",
response,
re.I | re.S,
)
)
if direct_contradiction or anaphoric_contradiction:
issues.append("runtime_persistence_contradiction")
if "shared_claims_agent_positions" in contract_ids:
if re.search(

View file

@ -63,7 +63,7 @@ def validate_read_only_bridge_argv(argv: list[str]) -> None:
for name in ("search", "context"):
item = command(name)
item.add_argument("query")
item.add_argument("query", nargs="+")
item.add_argument("--limit", type=int, default=5)
item.add_argument("--context-limit", type=int, default=5 if name == "search" else 6)
show = command("show")
@ -80,7 +80,7 @@ def validate_read_only_bridge_argv(argv: list[str]) -> None:
)
list_proposals.add_argument("--limit", type=int, default=10)
search_proposals = command("search-proposals")
search_proposals.add_argument("query")
search_proposals.add_argument("query", nargs="+")
search_proposals.add_argument(
"--status",
choices=("pending_review", "approved", "applied", "canceled", "rejected", "all"),

View file

@ -427,14 +427,15 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
r"shared (?:claim|knowledge|commons)|claims.{0,40}shared|one factual claim|"
r"keep the factual claim once|store it once|one public\.claims row|"
r"one shared public\.claims row|facts? (?:are |remain )?shared|"
r"(?:shared source material|observation).{0,100}(?:one|single) claim row|"
r"fact shared.{0,80}(?:one|single).{0,40}(?:claim|public\.claims)",
re.I | re.S,
),
re.compile(r"source|evidence", re.I),
re.compile(
r"do not duplicate|don'?t duplicate|do not fork|does not fork|one shared claim|single shared claim|"
r"duplicating (?:(?:a|the) )?claim",
re.I,
r"duplicating (?:(?:a|the) )?claim|one claim row.{0,100}shared (?:sources?|evidence|claim_evidence)",
re.I | re.S,
),
),
"agent_specific_positions": (
@ -458,11 +459,12 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
re.compile(
r"preserve|retain|do not overwrite|don'?t overwrite|keep.{0,60}unmodified|"
r"leave.{0,60}(?:untouched|unmodified)|original.{0,80}(?:survives|unchanged|unmodified)|"
r"historical record",
r"historical record|overwrit(?:e|ing).{0,80}(?:destroy|erase|lose).{0,40}history",
re.I | re.S,
),
re.compile(
r"ambiguous|missing.{0,30}criteria|no.{0,30}criteria|"
r"absence of.{0,30}(?:success|resolution) criteria|"
r"lack(?:ed|s|ing).{0,30}(?:success|resolution) criteria|"
r"criteria.{0,40}(?:never existed|were never defined|did not exist)|"
r"without.{0,60}(?:criteria|resolution rule)",
@ -470,7 +472,10 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
),
),
"forecast_schema_gap": (
re.compile(r"current (?:v1|schema)|public\.claims|the schema|schema has|claims table", re.I),
re.compile(
r"current (?:v1|schema|columns?|fields?|edge types?)|public\.claims|the schema|schema has|claims table",
re.I,
),
re.compile(
r"no.{0,50}(?:forecast[- ]resolution|resolution field|resolved_at)|does not have.{0,50}resolution|"
r"no.{0,80}(?:resolved|outcome|resolution_criteria).{0,30}field|"
@ -478,7 +483,9 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
r"(?:does not exist|is absent|is not present|isn't present)|"
r"(?:resolution field|resolved_at).{0,140}neither.{0,30}present|"
r"(?:resolution outcome|resolved_at|resolution_criteria).{0,180}"
r"none of those fields.{0,50}(?:exist|present)",
r"none of those fields.{0,50}(?:exist|present)|"
r"(?:resolved status|resolution criteria|resolved_at).{0,160}"
r"(?:not current|are not current|not.{0,30}(?:columns?|fields?)|missing|absent)",
re.I | re.S,
),
re.compile(
@ -620,7 +627,7 @@ SCHEMA_GAP_QUALIFIER_RE = re.compile(
r"\b(?:proposed|future|not current|not shipped|does not exist|doesn't exist|absent|missing|"
r"has no|have no|there is no|no column|not an edge|would require|schema gap|must be added|"
r"does not support|doesn't support|supports neither|not supported|must not|do not invent|"
r"schema extension|extension proposal|reviewed schema proposal)\b|"
r"schema extension|extension proposal|reviewed schema proposal|not (?:in|part of|among) the current)\b|"
r"\b(?:requires?|needs?)\s+either\s+(?:an?\s+)?"
r"(?:[a-z_]+\s+){0,3}(?:column|field|table|edge type)\b|"
r"\b(?:would add|would introduce|would create)\s+(?:an?\s+)?"
@ -710,7 +717,10 @@ DURABLE_SESSION_EPHEMERAL_RE = re.compile(
r"session JSONL.{0,100}(?:is|are|gets?|becomes?|confirm).{0,30}"
r"(?:gone|lost|deleted|discarded|absent|zeroed|empty)|"
r"session JSONL.{0,100}(?:gone|lost|deleted|discarded|absent|zeroed|empty).{0,50}"
r"(?:session closes?|restart|process launch)",
r"(?:session closes?|restart|process launch)|"
r"(?:state\.db|session JSONL).{0,360}(?:restart|recycle|process launch).{0,100}"
r"(?:can|may|will)\s+(?:clear|erase|reset|delete|drop|lose)\s+"
r"(?:them|both|the files?|the session|state\.db|session JSONL)",
re.I | re.S,
)
REASONING_TOOL_CLAIM_EDGE_RE = re.compile(
@ -878,8 +888,13 @@ def current_schema_overclaims(reply: str) -> list[str]:
review_context = bool(
re.match(r"^\s*(?:[-*]\s*)?review (?:covers|includes|asks|considers|checks)\b", sentence, re.I)
)
for clause in clauses:
if SCHEMA_GAP_QUALIFIER_RE.search(clause):
for index, clause in enumerate(clauses):
next_clause = clauses[index + 1] if index + 1 < len(clauses) else ""
anaphoric_qualifier = bool(
re.match(r"^\s*(?:it|this|that|they|these|those|neither|none)\b", next_clause, re.I)
and SCHEMA_GAP_QUALIFIER_RE.search(next_clause)
)
if SCHEMA_GAP_QUALIFIER_RE.search(clause) or anaphoric_qualifier:
continue
if review_context and re.search(r"\b(?:whether|if|how)\b", clause, re.I) and not re.search(
r"\bcurrent schema\b.{0,80}\b(?:has|supports|includes|provides|allows)\b",

View file

@ -200,7 +200,8 @@ BLINDED_FAMILIES: tuple[dict[str, Any], ...] = (
(
"The {subject} left all canonical database counts unchanged. Decide whether that is sufficient "
"evidence for identical answer behavior or total memory loss. Explain row fingerprints, skills and "
"SOUL.md, state.db/session JSONL, and the handler-versus-delivery boundary. Do not mutate anything."
"SOUL.md, state.db/session JSONL, and the handler-versus-delivery boundary. Stay under 180 words and "
"do not mutate anything."
),
(
"An operator uses unchanged database totals after a {subject} to claim both behavioral parity and a "

View file

@ -77,6 +77,22 @@ def test_runtime_persistence_contract_rejects_lost_session_jsonl() -> None:
assert "runtime_persistence_contradiction" not in good
def test_runtime_persistence_contract_rejects_split_sentence_restart_loss() -> None:
module = load_plugin()
contracts = [{"id": "runtime_persistence"}]
observed = (
"state.db and session JSONL. These carry prior-turn context. "
"A service recycle can clear them without changing a canonical row."
)
safe = (
"state.db and session JSONL preserve prior-turn context. "
"A service recycle cannot clear them merely because database counts stayed unchanged."
)
assert "runtime_persistence_contradiction" in module.response_contract_issues(observed, contracts)
assert "runtime_persistence_contradiction" not in module.response_contract_issues(safe, contracts)
def test_forecast_contract_requires_reviewed_apply_boundary() -> None:
module = load_plugin()
contracts = [{"id": "forecast_resolution_schema"}]

View file

@ -60,6 +60,21 @@ def test_vps_bridge_global_options_do_not_accept_untraced_abbreviations(monkeypa
module.parse_args()
@pytest.mark.parametrize("command", ("search", "context", "search-proposals"))
def test_vps_bridge_joins_unquoted_multiword_queries(monkeypatch, command: str) -> None:
module = _load_module(BRIDGE_DIR / "kb_tool.py")
monkeypatch.setattr(
module.sys,
"argv",
["kb_tool.py", command, "Atlas", "status", "snapshot", "--limit", "1"],
)
args = module.parse_args()
assert args.query == "Atlas status snapshot"
assert args.limit == 1
def test_cloudsql_wrapper_supports_expected_review_gated_commands() -> None:
wrapper_text = (BRIDGE_DIR / "teleo-kb").read_text()
cloudsql_text = (BRIDGE_DIR / "cloudsql_memory_tool.py").read_text()

View file

@ -45,6 +45,31 @@ def test_guard_accepts_only_exact_read_only_bridge_argv(tmp_path: Path, command:
assert reason is None
def test_guard_accepts_unquoted_multiword_read_query_as_one_safe_invocation(tmp_path: Path) -> None:
profile, wrapper = make_wrapper(tmp_path)
argv, reason = guard.classify_terminal_command(
wrapper,
profile,
{"command": "teleo-kb context Atlas status snapshot --limit 1 --context-limit 1 --format markdown"},
allow_kb_reads=True,
)
assert argv == [
"teleo-kb",
"context",
"Atlas",
"status",
"snapshot",
"--limit",
"1",
"--context-limit",
"1",
"--format",
"markdown",
]
assert reason is None
@pytest.mark.parametrize(
"tool_args",
[

View file

@ -829,6 +829,9 @@ def test_oos_runtime_reasoning_rejects_durable_session_as_ephemeral() -> None:
assert benchmark.broad_semantic_issues(
"Compare row hashes, then confirm session JSONL is absent or zeroed after process launch."
) == ["durable_session_called_ephemeral"]
assert benchmark.broad_semantic_issues(
"state.db and session JSONL. These carry prior-turn context. A service recycle can clear them."
) == ["durable_session_called_ephemeral"]
def test_oos_agent_positions_accept_attribution_without_literal_column_name() -> None:
@ -853,6 +856,15 @@ def test_oos_agent_positions_accept_attribution_without_literal_column_name() ->
)
assert benchmark.matched_concept(observed, "shared_knowledge_commons") is True
live_reply = (
"The shared source material and the observation both sides accept go into one claim row plus shared sources "
"and claim_evidence. Each agent's divergent reading goes into one public.beliefs row per agent."
)
assert benchmark.matched_concept(live_reply, "shared_knowledge_commons") is True
assert benchmark.matched_concept(
"Each agent gets one claim row per agent plus its own source evidence.", "shared_knowledge_commons"
) is False
def test_oos_memory_accepts_concrete_noncanonical_blocker_and_restatement() -> None:
token = "demo-ledger-deadbeef"
@ -902,6 +914,22 @@ def test_oos_forecast_accepts_natural_missing_resolution_language() -> None:
)
assert benchmark.matched_concept(listed_gap, "forecast_schema_gap") is True
live_reply = (
"The original 60% probability stays in its claim. The absence of success criteria is a second claim. "
"Overwriting the original confidence would destroy history. Resolved status and resolution criteria are "
"not current columns, and no resolves edge type exists. Stage a reviewed proposal before any apply."
)
assert benchmark.matched_concept(live_reply, "forecast_history") is True
assert benchmark.matched_concept(live_reply, "forecast_schema_gap") is True
def test_oos_schema_overclaim_accepts_anaphoric_semicolon_denial() -> None:
safe = "A resolves edge type; it is not in the current edge type list and must not be invented."
unsafe = "A resolves edge type; it is current and supported."
assert benchmark.current_schema_overclaims(safe) == []
assert benchmark.current_schema_overclaims(unsafe) == ["invalid_current_edge_type"]
def test_oos_challenge_accepts_plural_and_evidence_bounded_language() -> None:
reply = (

View file

@ -1233,6 +1233,14 @@ def test_evidence_probe_four_line_reply_binds_exact_subject_and_receipt() -> Non
assert protocol_lib._subject_alignment(prompt, result["reply"]) is False
def test_every_runtime_persistence_variant_declares_the_scorer_word_limit() -> None:
runtime_family = next(
item for item in protocol_lib.BLINDED_FAMILIES if item["family_id"] == "runtime_persistence"
)
assert all("under 180 words" in variant.lower() for variant in runtime_family["variants"])
def test_restart_receipt_binds_new_pid_full_fingerprint_and_next_trial(tmp_path: Path) -> None:
protocol = frozen_protocol()
trial = protocol["trials"][-1]