Fix Leo restart truth and read command reliability
This commit is contained in:
parent
021aaf6f09
commit
b59bee8844
10 changed files with 144 additions and 21 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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"}]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue