Files
agent-radar 1a3a4ccde1
Creator App Tests / PR Spam Gate (push) Skipped
Frontend Tests / PR Spam Gate (push) Skipped
NPM Format (website & console) / PR Spam Gate (push) Skipped
Pre-commit Checks / PR Spam Gate (push) Skipped
QwenPaw Creator Plugin Release / check-version (push) Canceled after 0s
QwenPaw Creator Plugin Release / upload-oss (push) Canceled after 0s
Tests / PR Spam Gate (push) Skipped
CodeQL / CodeQL (python) (push) Canceled after 0s
CodeQL / CodeQL (typescript) (push) Canceled after 0s
Creator App Tests / Creator Backend Tests (push) Canceled after 0s
Creator App Tests / Creator UI Build & Tests (push) Canceled after 0s
E2E Smoke Tests / Playwright UI Smoke (push) Canceled after 0s
Frontend Tests / Detect console changes (push) Canceled after 0s
NPM Format (website & console) / website format check (push) Canceled after 0s
NPM Format (website & console) / console format check (push) Canceled after 0s
Pre-commit Checks / Pre-commit (push) Canceled after 0s
QwenPaw Data Plugin Release / check-version (push) Canceled after 0s
Tests / Detect code changes (push) Canceled after 0s
Frontend Tests / Vitest Unit Tests (push) Canceled after 0s
Frontend Tests / Frontend Summary (push) Canceled after 0s
QwenPaw Data Plugin Release / upload-oss (push) Canceled after 0s
Tests / Maintainer Approval (push) Canceled after 0s
Tests / Unit Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - fallback (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p2 (push) Canceled after 0s
Tests / Coverage Report (push) Canceled after 0s
Tests / Test Summary (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:01:50 +08:00

2012 lines
58 KiB
Python

# -*- coding: utf-8 -*-
# pylint: disable=redefined-outer-name,protected-access,unused-argument
"""Unit tests for :class:`MemorySpace` — the model's SQLite recall surface.
The security-critical guarantee is that the model, which runs arbitrary SQL
here, cannot escape the read-only attach of durable history. These tests pin
the SQLite-authorizer contract plus the recall ``scope`` semantics.
"""
import sqlite3
from pathlib import Path
import pytest
from qwenpaw.agents.context.scroll.history import HistoryStore
from qwenpaw.agents.context.scroll.memoryspace import (
MemorySpace,
fts_match_query,
sanitize_suffix,
)
from qwenpaw.agents.context.types import LogEntry
@pytest.fixture
def history_db(tmp_path: Path) -> Path:
"""A durable store with two agents across two sessions."""
h = HistoryStore(tmp_path / "history.db")
h.append(
session_id="s1",
agent_id="ag1",
dedup_key="m1",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="tanks rolled in",
headline="battle",
),
)
h.append(
session_id="s2",
agent_id="ag1",
dedup_key="m2",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="tanks regrouped later",
),
)
h.append(
session_id="s3",
agent_id="ag2",
dedup_key="m3",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="tanks of another agent",
),
)
h.close()
return tmp_path / "history.db"
@pytest.fixture
def ms(history_db: Path) -> MemorySpace:
space = MemorySpace(
history_db_path=str(history_db),
session_id="s1",
agent_id="ag1",
)
yield space
space.close()
# -- the read-only-attach contract ------------------------------------------
@pytest.mark.parametrize(
"sql",
[
"ATTACH DATABASE ':memory:' AS other",
"DETACH DATABASE hist",
"INSERT INTO hist.conversation_history(session_id, kind) "
"VALUES ('x', 'k')",
"UPDATE hist.conversation_history SET content = 'tampered'",
"DELETE FROM hist.conversation_history",
"DROP TABLE hist.conversation_history",
],
)
def test_authorizer_blocks_escape_attempts(ms: MemorySpace, sql: str):
with pytest.raises(sqlite3.Error):
ms.sql_exec(sql)
# And the durable data is untouched.
assert (
ms.sql_query(
"SELECT COUNT(*) AS n FROM hist.conversation_history",
)[
0
]["n"]
== 3
)
def test_scratch_is_read_write(ms: MemorySpace):
ms.sql_exec("CREATE TABLE notes(x INTEGER)")
ms.sql_exec("INSERT INTO notes VALUES (42)")
assert ms.sql_query("SELECT x FROM notes")[0]["x"] == 42
assert "notes" in ms.tables()
def test_hist_is_readable(ms: MemorySpace):
rows = ms.sql_query(
"SELECT content FROM hist.conversation_history ORDER BY seq",
)
assert rows[0]["content"] == "tanks rolled in"
# -- recall scope semantics --------------------------------------------------
def test_search_default_is_this_agent_cross_session(ms: MemorySpace):
contents = {r["content"] for r in ms.search("tanks")}
# Both of ag1's turns (s1 + s2), none of ag2's — isolation by default.
assert "tanks rolled in" in contents
assert "tanks regrouped later" in contents
assert "tanks of another agent" not in contents
def test_search_uses_seq_as_stable_bm25_tie_breaker(tmp_path: Path):
h = HistoryStore(tmp_path / "history.db")
expected_seqs = []
for index in range(3):
expected_seqs.append(
h.append(
session_id="archive",
agent_id="ag1",
dedup_key=f"equal-{index}",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="identical ranking text",
),
),
)
h.close()
space = MemorySpace(
history_db_path=str(tmp_path / "history.db"),
session_id="current",
agent_id="ag1",
)
try:
first = space.search("identical ranking", k=3)
second = space.search("identical ranking", k=3)
finally:
space.close()
assert [row["seq"] for row in first] == expected_seqs
assert [row["seq"] for row in second] == expected_seqs
def test_search_excludes_recall_tool_own_turns(tmp_path: Path):
"""The recall tool's own source/output must not surface as search hits, or
a query matches the agent's earlier queries (self-pollution)."""
h = HistoryStore(tmp_path / "history.db")
h.append(
session_id="s1",
agent_id="ag1",
dedup_key="real",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="the car needs service after 10000 miles",
),
)
# The agent's own recall call (its Python source) and its printed output —
# both carry the searched keywords and both must be excluded.
h.append(
session_id="s1",
agent_id="ag1",
dedup_key="recall_call",
entry=LogEntry(
kind="model_turn",
role="assistant",
name="recall_history_python",
content='ms.search("car service")',
),
)
h.append(
session_id="s1",
agent_id="ag1",
dedup_key="recall_out",
entry=LogEntry(
kind="tool_result",
role="assistant",
name="recall_history",
content="stdout: searching for car service ...",
tool_call_id="t1",
),
)
h.close()
space = MemorySpace(
history_db_path=str(tmp_path / "history.db"),
session_id="s1",
agent_id="ag1",
)
try:
hits = space.search("car service")
contents = [r["content"] for r in hits]
assert contents == ["the car needs service after 10000 miles"]
finally:
space.close()
def test_search_excludes_the_active_turn(tmp_path: Path):
"""The current request and its in-progress reply must not surface as
hits: they are already in the live window, and a second recall round
would otherwise top-k-match the previous round's quoted findings
(echo loop). Earlier turns of the SAME session stay searchable."""
h = HistoryStore(tmp_path / "history.db")
rows = [
("old_u", "context_msg", "user", "tanks question from earlier"),
("old_a", "model_turn", "assistant", "tanks were parked at base"),
# The ACTIVE turn: the latest user request + the reply being written.
("cur_u", "context_msg", "user", "tanks question retried"),
("cur_a", "model_turn", "assistant", "tanks quote from last recall"),
]
for key, kind, role, content in rows:
h.append(
session_id="s1",
agent_id="ag1",
dedup_key=key,
entry=LogEntry(kind=kind, role=role, content=content),
)
h.append( # another session is untouched by the exclusion
session_id="s2",
agent_id="ag1",
dedup_key="other",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="tanks moved in another session",
),
)
h.close()
space = MemorySpace(
history_db_path=str(tmp_path / "history.db"),
session_id="s1",
agent_id="ag1",
)
try:
legacy_expected = {
"tanks question from earlier",
"tanks were parked at base",
"tanks moved in another session",
}
hits = space.search("tanks", k=10)
assert {r["content"] for r in hits} == {
"tanks question from earlier",
"tanks moved in another session",
}
old_turn = next(
row["turn"] for row in hits if row["session_id"] == "s1"
)
assert {row["content"] for row in old_turn} == {
"tanks question from earlier",
"tanks were parked at base",
}
legacy_hits = space.search(
"tanks",
k=10,
include_turn=False,
)
assert {r["content"] for r in legacy_hits} == legacy_expected
# The LIKE fallback applies the same exclusion.
like = space._search_like("tanks", [("agent_id", "ag1")], None, 10)
got = {r["content"] for r in like if r["kind"] != "_notice"}
assert got == legacy_expected
finally:
space.close()
def test_search_returns_and_deduplicates_complete_turn(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
user_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="u1",
entry=LogEntry(
kind="context_msg",
role="user",
content="order question for a compass",
),
)
assistant_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="a1",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="order answer is north",
),
)
tool_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="t1",
entry=LogEntry(
kind="tool_result",
role="assistant",
name="lookup",
content="order receipt confirmed",
tool_call_id="call-1",
),
)
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="u2",
entry=LogEntry(
kind="context_msg",
role="user",
content="unrelated active request",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="archive",
agent_id="ag1",
)
try:
hits = space.search("order", k=10)
user_hits = space.search("compass", k=10)
finally:
space.close()
assert len(hits) == 1
assert hits[0]["matched_seqs"] == [
user_seq,
assistant_seq,
tool_seq,
]
assert hits[0]["turn_start_seq"] == user_seq
assert hits[0]["turn_end_seq"] == tool_seq
assert [row["content"] for row in hits[0]["turn"]] == [
"order question for a compass",
"order answer is north",
"order receipt confirmed",
]
assert len(user_hits) == 1
assert user_hits[0]["match_seq"] == user_seq
assert user_hits[0]["turn"] == hits[0]["turn"]
def test_search_cjk_substring_returns_complete_turn(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
user_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="u1",
entry=LogEntry(
kind="context_msg",
role="user",
content="紫水晶河马在周二跳舞",
),
)
assistant_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="a1",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="我记住了这个暗号",
),
)
tool_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="t1",
entry=LogEntry(
kind="tool_result",
role="assistant",
name="lookup",
content="工具结果",
tool_call_id="call-1",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
hits = space.search("紫水晶河马", session_id="archive", k=10)
legacy_hits = space.search(
"紫水晶河马",
session_id="archive",
k=10,
include_turn=False,
)
finally:
space.close()
assert len(hits) == 1
assert hits[0]["match_seq"] == user_seq
assert hits[0]["turn_start_seq"] == user_seq
assert hits[0]["turn_end_seq"] == tool_seq
assert hits[0]["turn_complete"] is True
assert [row["seq"] for row in hits[0]["turn"]] == [
user_seq,
assistant_seq,
tool_seq,
]
assert [row["content"] for row in hits[0]["turn"]] == [
"紫水晶河马在周二跳舞",
"我记住了这个暗号",
"工具结果",
]
assert [row["seq"] for row in legacy_hits] == [user_seq]
def test_search_cjk_multiple_terms_are_and_combined(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
user_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="u1",
entry=LogEntry(
kind="context_msg",
role="user",
content="项目的截止日期是周二",
),
)
assistant_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="a1",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="好的,我记住了",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
hits = space.search(
"项目 截止日期",
session_id="archive",
k=10,
)
finally:
space.close()
assert len(hits) == 1
assert hits[0]["match_seq"] == user_seq
assert hits[0]["turn_complete"] is True
assert [row["seq"] for row in hits[0]["turn"]] == [
user_seq,
assistant_seq,
]
def test_search_cjk_uppercase_or_matches_either_term(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
project_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="project",
entry=LogEntry(
kind="context_msg",
role="user",
content="项目状态",
),
)
deadline_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="deadline",
entry=LogEntry(
kind="context_msg",
role="user",
content="截止日期",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
hits = space.search(
"项目 OR 截止日期",
session_id="archive",
k=10,
)
matching_rows = space.search(
"项目 OR 截止日期",
session_id="archive",
k=10,
include_turn=False,
)
finally:
space.close()
assert {hit["match_seq"] for hit in hits} == {
project_seq,
deadline_seq,
}
assert {row["content"] for row in matching_rows} == {
"项目状态",
"截止日期",
}
def test_search_like_metacharacters_are_literal(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
for index, content in enumerate(
(
"项目_编号",
"项目X编号",
"项目%进度",
"项目任意长度进度",
"项目\\路径",
),
):
history.append(
session_id="archive",
agent_id="ag1",
dedup_key=f"row-{index}",
entry=LogEntry(
kind="model_turn",
role="assistant",
content=content,
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
underscore = space.search(
"项目_编号",
session_id="archive",
include_turn=False,
)
percent = space.search(
"项目%进度",
session_id="archive",
include_turn=False,
)
backslash = space.search(
"项目\\路径",
session_id="archive",
include_turn=False,
)
finally:
space.close()
assert [row["content"] for row in underscore] == ["项目_编号"]
assert [row["content"] for row in percent] == ["项目%进度"]
assert [row["content"] for row in backslash] == ["项目\\路径"]
def test_search_loads_duplicate_hit_turn_once(
tmp_path: Path,
monkeypatch,
):
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="user",
entry=LogEntry(
kind="context_msg",
role="user",
content="find the repeated records",
),
)
matched = []
for index in range(100):
matched.append(
history.append(
session_id="archive",
agent_id="ag1",
dedup_key=f"match-{index}",
entry=LogEntry(
kind="model_turn",
role="assistant",
content=f"duplicate-needle result {index}",
),
),
)
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="next-user",
entry=LogEntry(
kind="context_msg",
role="user",
content="current unrelated request",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="archive",
agent_id="ag1",
)
load_calls = 0
original = space._load_turn
def counted_load(*args, **kwargs):
nonlocal load_calls
load_calls += 1
return original(*args, **kwargs)
monkeypatch.setattr(space, "_load_turn", counted_load)
try:
rows = space.search("duplicate-needle", k=100)
finally:
space.close()
turns = [row for row in rows if row["kind"] != "_notice"]
assert len(turns) == 1
assert turns[0]["matched_seqs"] == matched
assert load_calls == 1
def test_search_pages_until_it_finds_k_distinct_turns(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="first-user",
entry=LogEntry(
kind="context_msg",
role="user",
content="first question",
),
)
for index in range(80):
history.append(
session_id="archive",
agent_id="ag1",
dedup_key=f"first-hit-{index}",
entry=LogEntry(
kind="model_turn",
role="assistant",
content=f"needle {index}",
),
)
second_start = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="second-user",
entry=LogEntry(
kind="context_msg",
role="user",
content="second question",
),
)
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="second-hit",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="needle " + ("lower ranked filler " * 40),
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
rows = space.search("needle", k=2)
finally:
space.close()
turns = [row for row in rows if row["kind"] != "_notice"]
assert len(turns) == 2
assert second_start in {row["turn_start_seq"] for row in turns}
assert not [row for row in rows if row["kind"] == "_notice"]
def test_search_marks_partial_when_raw_hit_cap_hides_turns(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="first-user",
entry=LogEntry(
kind="context_msg",
role="user",
content="first question",
),
)
for index in range(20):
history.append(
session_id="archive",
agent_id="ag1",
dedup_key=f"first-hit-{index}",
entry=LogEntry(
kind="model_turn",
role="assistant",
content=f"needle {index}",
),
)
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="second-user",
entry=LogEntry(
kind="context_msg",
role="user",
content="second question",
),
)
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="second-hit",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="needle " + ("lower ranked filler " * 40),
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
row_cap=10,
)
try:
rows = space.search("needle", k=2)
finally:
space.close()
turns = [row for row in rows if row["kind"] != "_notice"]
notices = [row for row in rows if row["kind"] == "_notice"]
assert len(turns) == 1
assert len(notices) == 1
assert "maximum 10 matching rows" in notices[0]["content"]
def test_search_turn_expansion_has_total_row_budget(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
turn_seqs = [
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="user",
entry=LogEntry(
kind="context_msg",
role="user",
content="budget question",
),
),
]
for index in range(10):
turn_seqs.append(
history.append(
session_id="archive",
agent_id="ag1",
dedup_key=f"row-{index}",
entry=LogEntry(
kind="model_turn",
role="assistant",
content=f"budget-needle row {index}",
),
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
search_turn_max_rows=3,
)
try:
rows = space.search("budget-needle", k=10)
finally:
space.close()
result = next(row for row in rows if row["kind"] != "_notice")
turn = result["turn"]
assert len([row for row in turn if not row.get("_truncated")]) == 3
assert turn[-1]["_truncated"] is True
assert result["turn_end_seq"] == turn_seqs[-1]
assert result["turn_loaded_end_seq"] == turn_seqs[2]
assert result["turn_complete"] is False
notices = [row for row in rows if row["kind"] == "_notice"]
assert len(notices) == 1
assert "total turn row budget" in notices[0]["content"]
def test_search_turn_expansion_has_total_byte_budget(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
user_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="user",
entry=LogEntry(
kind="context_msg",
role="user",
content="small byte budget question",
),
)
actual_end = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="large",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="byte-budget-needle " + ("x" * 2000),
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
search_turn_max_bytes=600,
)
try:
rows = space.search("byte-budget-needle")
finally:
space.close()
result = next(row for row in rows if row["kind"] != "_notice")
turn = result["turn"]
assert len([row for row in turn if not row.get("_truncated")]) == 1
assert turn[-1]["_truncated"] is True
assert result["turn_end_seq"] == actual_end
assert result["turn_loaded_end_seq"] == user_seq
assert result["turn_complete"] is False
notices = [row for row in rows if row["kind"] == "_notice"]
assert len(notices) == 1
assert "total turn byte budget" in notices[0]["content"]
def test_search_execution_timeout_is_enforced(history_db: Path):
space = MemorySpace(
history_db_path=history_db,
session_id="s1",
agent_id="ag1",
search_max_seconds=0,
)
try:
with pytest.raises(TimeoutError, match="execution timeout"):
space.search("tanks")
finally:
space.close()
def test_search_groups_imported_kind_by_user_role(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
user_seq = history.append(
session_id="beam-batch",
agent_id="ag1",
dedup_key="beam-u1",
entry=LogEntry(
kind="beam_chat_turn", # type: ignore[arg-type]
role="user",
content="How many Jira tasks did I log?",
created_at="2024-11-05T00:00:00+00:00",
),
)
assistant_seq = history.append(
session_id="beam-batch",
agent_id="ag1",
dedup_key="beam-a1",
entry=LogEntry(
kind="beam_chat_turn", # type: ignore[arg-type]
role="assistant",
content="You logged 18 Jira tasks.",
created_at="2024-11-05T00:00:00+00:00",
),
)
history.append(
session_id="beam-batch",
agent_id="ag1",
dedup_key="beam-u2",
entry=LogEntry(
kind="beam_chat_turn", # type: ignore[arg-type]
role="user",
content="A separate benchmark question",
created_at="2024-11-06T00:00:00+00:00",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="probe",
agent_id="ag1",
)
try:
assistant_hit = space.search(
"18 Jira",
kind="beam_chat_turn",
)
user_hit = space.search(
"How many",
kind="beam_chat_turn",
)
finally:
space.close()
assert len(assistant_hit) == 1
assert assistant_hit[0]["turn_start_seq"] == user_seq
assert assistant_hit[0]["turn_end_seq"] == assistant_seq
assert [row["content"] for row in assistant_hit[0]["turn"]] == [
"How many Jira tasks did I log?",
"You logged 18 Jira tasks.",
]
assert user_hit[0]["turn"] == assistant_hit[0]["turn"]
def test_search_turn_does_not_cross_session_or_agent(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
for session_id, agent_id, suffix in (
("s1", "ag1", "one"),
("s2", "ag1", "two"),
("s1", "ag2", "three"),
):
history.append(
session_id=session_id,
agent_id=agent_id,
dedup_key=f"u-{suffix}",
entry=LogEntry(
kind="context_msg",
role="user",
content=f"needle request {suffix}",
),
)
history.append(
session_id=session_id,
agent_id=agent_id,
dedup_key=f"a-{suffix}",
entry=LogEntry(
kind="model_turn",
role="assistant",
content=f"answer {suffix}",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
hits = space.search("needle", all_agents=True, k=10)
finally:
space.close()
assert len(hits) == 3
for hit in hits:
assert {
(row["session_id"], row["agent_id"]) for row in hit["turn"]
} == {
(hit["session_id"], hit["agent_id"]),
}
def test_active_turn_floor_is_computed_once_per_instance(
ms: MemorySpace,
monkeypatch,
):
"""The MAX(seq) scan behind the active-turn exclusion is memoized: a
single search consults the floor twice (FTS path + LIKE fallback) and the
read-only history can't change under the instance, so it must run at most
once — the cost that accrues on large histories in the recall subprocess.
"""
calls = {"n": 0}
real = ms._compute_active_turn_floor
def counting():
calls["n"] += 1
return real()
monkeypatch.setattr(ms, "_compute_active_turn_floor", counting)
# Consult it across every path that would otherwise re-query.
ms.search("tanks", k=5)
ms._search_like("tanks", [("agent_id", "ag1")], None, 5)
ms._active_turn_floor()
assert calls["n"] == 1
def test_active_turn_floor_ignores_continuation_stubs(tmp_path: Path):
"""A loop-continuation stub row (user-role, tagged) must not move the
active-turn floor: the floor anchors on the REAL request that started
the turn, so the whole still-live extended turn stays excluded from
search instead of leaking back in as echo."""
h = HistoryStore(tmp_path / "history.db")
rows = [
("old", "model_turn", "assistant", "tanks parked at base", None),
("req", "context_msg", "user", "tanks question", None),
("a1", "model_turn", "assistant", "tanks quote from recall", None),
(
"stub",
"context_msg",
"user",
"Continue working on the task.",
{"qwenpaw_tag": "loop_continuation"},
),
("a2", "model_turn", "assistant", "tanks continued reply", None),
]
for key, kind, role, content, metadata in rows:
h.append(
session_id="s1",
agent_id="ag1",
dedup_key=key,
entry=LogEntry(
kind=kind,
role=role,
content=content,
metadata=metadata or {},
),
)
h.close()
space = MemorySpace(
history_db_path=str(tmp_path / "history.db"),
session_id="s1",
agent_id="ag1",
)
try:
# Floor = the real request's seq (NOT the stub's): everything from
# the request onward is active-turn and excluded from search.
hits = {r["content"] for r in space.search("tanks", k=10)}
assert hits == {"tanks parked at base"}
finally:
space.close()
def test_search_rows_carry_session_id(ms: MemorySpace):
# Cross-session/agent search is only useful if a hit says which session it
# came from — the model needs ``session_id`` to follow up (it used to guess
# the key and crash with KeyError).
rows = {r["content"]: r for r in ms.search("tanks", all_agents=True)}
assert rows["tanks rolled in"]["session_id"] == "s1"
assert rows["tanks regrouped later"]["session_id"] == "s2"
def test_fts_match_query_passes_boolean_operators():
# Bare UPPERCASE AND/OR/NOT are FTS5 operators (so the model can cast a
# wide net); every other token is a quoted literal; a plain query is AND.
assert fts_match_query("tank OR aquarium") == '"tank" OR "aquarium"'
assert fts_match_query("plain words") == '"plain" "words"'
# lowercase 'or' is a search term, not an operator
assert fts_match_query("salt or pepper") == '"salt" "or" "pepper"'
# punctuation operators are still neutralised
assert fts_match_query("F-15") == '"F" "15"'
def test_search_or_widens_beyond_a_single_term(tmp_path: Path):
h = HistoryStore(tmp_path / "history.db")
h.append(
session_id="s1",
agent_id="ag1",
dedup_key="a",
entry=LogEntry(
kind="model_turn",
role="user",
content="cleaned the goldfish tank",
),
)
h.append(
session_id="s1",
agent_id="ag1",
dedup_key="b",
entry=LogEntry(
kind="model_turn",
role="user",
content="bought an aquarium filter",
),
)
h.close()
space = MemorySpace(
history_db_path=str(tmp_path / "history.db"),
session_id="s1",
agent_id="ag1",
)
try:
# OR matches EITHER term (2 rows); the AND form would match neither.
assert len(space.search("tank OR aquarium")) == 2
assert len(space.search("tank aquarium")) == 0
finally:
space.close()
def test_search_all_agents_spans_the_workspace(ms: MemorySpace):
contents = {r["content"] for r in ms.search("tanks", all_agents=True)}
assert "tanks of another agent" in contents
assert len(contents) == 3
def test_search_pins_to_an_explicit_session(ms: MemorySpace):
# ms is on s1, but an explicit session_id targets a different one.
contents = {r["content"] for r in ms.search("tanks", session_id="s2")}
assert contents == {"tanks regrouped later"}
def test_search_pins_to_an_explicit_agent(ms: MemorySpace):
# The default agent scope hides ag2; pin to it to read its history.
contents = {r["content"] for r in ms.search("tanks", agent_id="ag2")}
assert contents == {"tanks of another agent"}
def test_explicit_target_takes_precedence(ms: MemorySpace):
# An explicit session_id wins even against all_agents=True.
contents = {
r["content"]
for r in ms.search("tanks", all_agents=True, session_id="s1")
}
assert contents == {"tanks rolled in"}
def test_search_filters_by_created_at_calendar_dates(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
rows = [
(
"u1",
"context_msg",
"user",
"Jira sprint alpha",
"2024-11-05T08:00:00+08:00",
),
(
"a1",
"model_turn",
"assistant",
"logged 18 tasks",
"2024-11-05T08:01:00+08:00",
),
(
"u2",
"context_msg",
"user",
"Jira sprint beta",
"2024-11-06T23:00:00-08:00",
),
(
"a2",
"model_turn",
"assistant",
"logged 21 tasks",
"2024-11-06T23:01:00-08:00",
),
(
"u3",
"context_msg",
"user",
"unrelated current boundary",
"2024-11-07T00:00:00Z",
),
]
for key, kind, role, content, created_at in rows:
history.append(
session_id="archive",
agent_id="ag1",
dedup_key=key,
entry=LogEntry(
kind=kind,
role=role,
content=content,
created_at=created_at,
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
exact = space.search("tasks", created_on="2024-11-05")
date_only = space.search("", created_on="2024-11-06")
date_range = space.search(
"Jira",
created_from="2024-11-05",
created_to="2024-11-06",
)
space._fts_ok = False
like = space.search("tasks", created_on="2024-11-05")
finally:
space.close()
assert len(exact) == 1
assert exact[0]["content"] == "logged 18 tasks"
assert exact[0]["created_at"].startswith("2024-11-05")
assert {row["content"] for row in exact[0]["turn"]} == {
"Jira sprint alpha",
"logged 18 tasks",
}
assert len(date_only) == 1
assert date_only[0]["turn_start_seq"] == 3
assert len(date_range) == 2
assert {row["content"] for row in like if row["seq"] >= 0} == {
"logged 18 tasks",
}
def test_date_only_search_keeps_null_content_rows(tmp_path: Path):
history = HistoryStore(tmp_path / "history.db")
target_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="structured",
entry=LogEntry(
kind="context_msg",
role="user",
content=None,
blocks=[{"type": "text", "text": "structured history"}],
created_at="2024-11-05T08:00:00Z",
),
)
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="next-day",
entry=LogEntry(
kind="context_msg",
role="user",
content="next boundary",
created_at="2024-11-06T08:00:00Z",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
space._fts_ok = False
try:
matching_rows = space.search(
"",
created_on="2024-11-05",
include_turn=False,
)
turns = space.search("", created_on="2024-11-05")
finally:
space.close()
assert [row["seq"] for row in matching_rows] == [target_seq]
assert matching_rows[0]["content"] is None
assert len(turns) == 1
assert turns[0]["turn_start_seq"] == target_seq
assert turns[0]["turn"][0]["content"] is None
assert "structured history" in turns[0]["turn"][0]["blocks"]
assert all(row["kind"] != "_notice" for row in turns)
@pytest.mark.parametrize(
"kwargs",
[
{"created_on": "2024-02-30"},
{
"created_on": "2024-11-05",
"created_from": "2024-11-01",
},
{
"created_from": "2024-11-06",
"created_to": "2024-11-05",
},
],
)
def test_search_rejects_invalid_created_at_filters(
ms: MemorySpace,
kwargs: dict,
):
with pytest.raises(ValueError):
ms.search("tanks", **kwargs)
@pytest.mark.parametrize(
"kwargs",
[
{"created_on": "9999-12-31"},
{"created_from": "9999-12-31", "created_to": "9999-12-31"},
],
)
def test_search_accepts_date_max_without_overflow(
tmp_path: Path,
kwargs: dict,
):
history = HistoryStore(tmp_path / "history.db")
target_seq = history.append(
session_id="archive",
agent_id="ag1",
dedup_key="max-date",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="last representable day",
created_at="9999-12-31T23:59:59.999999+14:00",
),
)
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="previous-date",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="previous representable day",
created_at="9999-12-30T23:59:59Z",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
rows = space.search(
"representable",
include_turn=False,
**kwargs,
)
finally:
space.close()
assert [row["seq"] for row in rows] == [target_seq]
def test_row_cap_truncates_with_marker(history_db: Path):
space = MemorySpace(
history_db_path=str(history_db),
session_id="s1",
agent_id="ag1",
row_cap=2,
)
try:
rows = space.sql_query("SELECT seq FROM hist.conversation_history")
assert rows[-1].get("_truncated") is True
assert len([r for r in rows if "_truncated" not in r]) == 2
finally:
space.close()
def _hits(rows: list[dict]) -> set:
"""Result contents minus the LIKE-degraded notice row."""
return {r["content"] for r in rows if r["kind"] != "_notice"}
def test_like_fallback_respects_scope(ms: MemorySpace):
"""Force the no-FTS path; scope + explicit targeting still hold."""
ms._fts_ok = False
contents = _hits(ms.search("tanks"))
assert "tanks of another agent" not in contents
assert "tanks rolled in" in contents
# explicit targeting works on the LIKE path too
pinned = _hits(ms.search("tanks", agent_id="ag2"))
assert pinned == {"tanks of another agent"}
def test_like_fallback_preserves_uppercase_or(ms: MemorySpace):
"""The public OR contract also holds when FTS5 is unavailable."""
ms._fts_ok = False
assert _hits(ms.search("rolled OR regrouped")) == {
"tanks rolled in",
"tanks regrouped later",
}
def test_like_fallback_emits_degradation_notice(ms: MemorySpace):
"""Without FTS5 the model must be told search degraded to a LIKE scan, so
it stops using OR/boolean grammar that silently matches nothing."""
ms._fts_ok = False
rows = ms.search("tanks")
assert rows[0]["kind"] == "_notice"
assert "FTS5" in rows[0]["content"]
# The notice shares the row schema, so a content-iterating loop is safe.
assert set(rows[0].keys()) >= {"seq", "kind", "role", "content"}
def test_no_notice_when_fts_available(ms: MemorySpace):
"""The notice is FTS-unavailable-only — a normal FTS build never sees it,
even when a query degrades to LIKE for lack of word tokens."""
# All-punctuation query falls back to LIKE, but FTS5 *is* available here.
rows = ms.search("!!!")
assert all(r["kind"] != "_notice" for r in rows)
# -- strict date arithmetic -------------------------------------------------
@pytest.mark.parametrize(
("start", "end", "expected"),
[
("2024-11-01", "2024-12-16", 45),
("2024-12-16", "2024-11-01", -45),
("2024-02-28", "2024-03-01", 2),
("2023-02-28", "2023-03-01", 1),
(
"2024-11-01T23:59:59.123456+08:00",
"2024-12-16T00:00:00Z",
45,
),
(
"2024-11-01T00:00-07:00",
"2024-10-31T23:59+14:00",
-1,
),
],
)
def test_days_between_is_signed_and_accepts_iso_timestamps(
ms: MemorySpace,
start: str,
end: str,
expected: int,
):
assert ms.days_between(start, end) == expected
@pytest.mark.parametrize(
("start", "end", "expected"),
[
("2024-11-01", "2024-11-01", 1),
("2024-11-01", "2024-11-02", 2),
("2024-11-02", "2024-11-01", -2),
],
)
def test_days_between_inclusive_preserves_direction(
ms: MemorySpace,
start: str,
end: str,
expected: int,
):
assert ms.days_between(start, end, inclusive=True) == expected
@pytest.mark.parametrize(
"value",
[
"2023-02-29",
"2024-02-30",
"2024/11/01",
"prefix 2024-11-01",
"2024-11-01 12:30:00Z",
"2024-11-01T12:30:00+24:00",
"2024-11-01T12:30:00 PST",
" 2024-11-01",
],
)
def test_days_between_rejects_invalid_or_non_strict_dates(
ms: MemorySpace,
value: str,
):
with pytest.raises(ValueError, match="invalid ISO date or timestamp"):
ms.days_between(value, "2024-12-16")
def test_days_between_rejects_unsupported_types(ms: MemorySpace):
with pytest.raises(TypeError, match="must be an ISO string"):
ms.days_between(20241101, "2024-12-16")
# -- intent-named recall helpers --------------------------------------------
def test_expand_returns_full_turns_in_span(ms: MemorySpace):
rows = ms.expand(1, 99)
assert [r["content"] for r in rows] == ["tanks rolled in"]
def test_expand_includes_legacy_unowned_rows(history_db: Path):
history = HistoryStore(history_db)
legacy_seq = history.append(
session_id="legacy",
agent_id=None,
dedup_key="legacy-null-agent",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="legacy unowned turn",
),
)
history.close()
space = MemorySpace(
history_db_path=str(history_db),
session_id="legacy",
agent_id="ag1",
)
try:
rows = space.expand(legacy_seq, legacy_seq)
finally:
space.close()
assert [row["content"] for row in rows] == ["legacy unowned turn"]
def test_recall_tool_is_agent_scoped_by_default(history_db: Path, tmp_path):
h = HistoryStore(history_db)
h.append(
session_id="s9",
agent_id="ag2",
dedup_key="tcX",
entry=LogEntry(
kind="tool_result",
role="assistant",
content="other agent tool",
tool_call_id="shared",
),
)
h.close()
space = MemorySpace(
history_db_path=str(history_db),
session_id="s1",
agent_id="ag1",
)
try:
# ag1 has no 'shared' tcid → empty; widening reaches ag2's row.
assert not space.recall_tool("shared")
assert len(space.recall_tool("shared", all_agents=True)) == 1
finally:
space.close()
# -- session / agent discovery ----------------------------------------------
def test_sessions_lists_this_agents_conversations(ms: MemorySpace):
rows = {r["session_id"]: r for r in ms.sessions()}
# ag1 ran in s1 and s2; ag2's s3 is hidden by the default agent scope.
assert set(rows) == {"s1", "s2"}
assert rows["s1"]["turns"] == 1
def test_sessions_all_agents_spans_the_workspace(ms: MemorySpace):
ids = {r["session_id"] for r in ms.sessions(all_agents=True)}
assert ids == {"s1", "s2", "s3"}
def test_session_is_agent_scoped_by_default(ms: MemorySpace):
# ag2's s3 is hidden by the default agent scope: session ids are not
# globally unique (main/local/cron:<job> recur across agents), so the
# default must not leak another agent's conversation. all_agents widens.
assert ms.session("s3") == []
rows = ms.session("s3", all_agents=True)
assert [r["content"] for r in rows] == ["tanks of another agent"]
def test_agents_is_workspace_wide(ms: MemorySpace):
rows = {r["agent_id"]: r for r in ms.agents()}
assert set(rows) == {"ag1", "ag2"}
assert rows["ag1"]["sessions"] == 2 # s1 + s2
def test_sanitize_suffix():
assert sanitize_suffix(None) == "scratch"
assert sanitize_suffix("a-b.c/d") == "a_b_c_d"
assert sanitize_suffix("ok_123") == "ok_123"
# -- SQL values are bound, not f-string-concatenated ------------------------
def test_recall_values_with_sql_metacharacters_are_bound(tmp_path: Path):
"""Recall must bind ``session_id``/``agent_id``/``tool_call_id`` as SQL
parameters, never f-string them in. A value carrying a single quote (e.g.
``O'Brien's task``) would otherwise break the WHERE clause or open an
injection; here it must round-trip cleanly and match only its own row."""
quoted_session = "O'Brien's task"
quoted_agent = "ag'1"
quoted_tcid = "tc'1"
h = HistoryStore(tmp_path / "history.db")
h.append(
session_id=quoted_session,
agent_id=quoted_agent,
dedup_key="m1",
entry=LogEntry(
kind="tool_result",
role="assistant",
content="briefing for the quoted session",
tool_call_id=quoted_tcid,
),
)
# A decoy under a different agent the scoped recall must NOT return.
h.append(
session_id=quoted_session,
agent_id="ag2",
dedup_key="m2",
entry=LogEntry(
kind="model_turn",
role="assistant",
content="other agent same session name",
),
)
h.close()
space = MemorySpace(
history_db_path=str(tmp_path / "history.db"),
session_id=quoted_session,
agent_id=quoted_agent,
)
try:
# session(): the path ekzhu flagged — agent-scoped, value bound.
rows = space.session(quoted_session)
assert [r["content"] for r in rows] == [
"briefing for the quoted session",
]
# recall_tool(): tool_call_id bound, not concatenated.
rows = space.recall_tool(quoted_tcid)
assert [r["content"] for r in rows] == [
"briefing for the quoted session",
]
# search() with an explicit quoted agent_id pin — both MATCH arg and
# the lineage filter are bound.
rows = space.search("briefing", agent_id=quoted_agent)
assert [r["content"] for r in rows] == [
"briefing for the quoted session",
]
# LIKE fallback path takes the same bound (col, value) filters.
# (Drop the leading FTS-unavailable notice row the LIKE path adds.)
space._fts_ok = False
rows = space.search("briefing", agent_id=quoted_agent)
assert [r["content"] for r in rows if r["kind"] != "_notice"] == [
"briefing for the quoted session",
]
finally:
space.close()
# -- saved tool-output search -----------------------------------------------
def _saved_tool_notice(path: Path, *, quoted: bool = False) -> str:
rendered_path = f'"{path}"' if quoted else str(path)
return (
"[tool output truncated]\n"
"If more content is needed, call `read_file` with "
f"file_path={rendered_path} start_line=1 to read more."
)
def test_saved_tool_paths_accept_quoted_and_legacy_paths_with_spaces(
tmp_path: Path,
):
artifact_dir = tmp_path / "tool results with spaces"
artifact_dir.mkdir()
quoted_file = artifact_dir / "quoted result.txt"
quoted_file.write_text("quoted\n", encoding="utf-8")
legacy_file = artifact_dir / "legacy result.txt"
legacy_file.write_text("legacy\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
paths = space._saved_tool_paths(
_saved_tool_notice(quoted_file, quoted=True)
+ "\n"
+ _saved_tool_notice(legacy_file),
)
finally:
space.close()
assert paths == [quoted_file.resolve(), legacy_file.resolve()]
def test_saved_tool_paths_prefer_structured_artifact_metadata(tmp_path: Path):
artifact = tmp_path / "metadata-only-result.txt"
artifact.write_text("structured artifact\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
paths = space._saved_tool_paths(
"preview without a legacy path notice",
{
"qwenpaw_truncation": {
"0": {
"file_path": str(artifact),
},
},
},
)
finally:
space.close()
assert paths == [artifact.resolve()]
def test_saved_tool_search_checks_each_multiblock_artifact(tmp_path: Path):
decoy_file = tmp_path / "first-block.txt"
decoy_file.write_text("nothing relevant\n", encoding="utf-8")
target_file = tmp_path / "second-block.txt"
target_file.write_text("the deepneedle is here\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="multi-block-result",
entry=LogEntry(
kind="tool_result",
role="assistant",
content=(
_saved_tool_notice(decoy_file)
+ "\n\n"
+ _saved_tool_notice(target_file)
),
tool_call_id="multi-block-call",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
rows = space.search("deepneedle", k=1)
finally:
space.close()
assert len(rows) == 1
assert rows[0]["kind"] == "tool_result"
assert f"file_path={target_file}" in rows[0]["content"]
assert "deepneedle" in rows[0]["content"]
def test_saved_tool_search_preserves_uppercase_or(tmp_path: Path):
artifact = tmp_path / "saved-tool-output.txt"
artifact.write_text("项目状态\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="saved-result",
entry=LogEntry(
kind="tool_result",
role="assistant",
content=_saved_tool_notice(artifact),
tool_call_id="saved-call",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
rows = space.search("项目 OR 截止日期", k=10)
finally:
space.close()
assert len(rows) == 1
assert rows[0]["kind"] == "tool_result"
assert f"file_path={artifact}" in rows[0]["content"]
assert "项目状态" in rows[0]["content"]
def test_recall_tool_annotates_each_multiblock_artifact(tmp_path: Path):
first_file = tmp_path / "first-block.txt"
first_file.write_text("first block\n", encoding="utf-8")
second_file = tmp_path / "second-block.txt"
second_file.write_text("second block\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="multi-block-result",
entry=LogEntry(
kind="tool_result",
role="assistant",
content=(
_saved_tool_notice(first_file)
+ "\n\n"
+ _saved_tool_notice(second_file)
),
tool_call_id="multi-block-call",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
rows = space.recall_tool("multi-block-call")
finally:
space.close()
artifacts = [
row["content"] for row in rows if row["kind"] == "_saved_tool_output"
]
assert artifacts == [
"Full saved tool output is available at "
f"file_path={str(first_file)!r} start_line=1.",
"Full saved tool output is available at "
f"file_path={str(second_file)!r} start_line=1.",
]
def test_recall_tool_returns_preview_when_artifact_expired(tmp_path: Path):
artifact = tmp_path / "expired-result.txt"
artifact.write_text("complete output\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="expired-result",
entry=LogEntry(
kind="tool_result",
role="assistant",
content="bounded preview",
tool_call_id="expired-call",
metadata={
"qwenpaw_truncation": {
"0": {
"file_path": str(artifact),
"start_line": 1,
},
},
},
),
)
history.close()
artifact.unlink()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
rows = space.recall_tool("expired-call")
finally:
space.close()
assert rows[0]["kind"] == "_saved_tool_output_unavailable"
assert "ARTIFACT_UNAVAILABLE" in rows[0]["content"]
assert rows[1]["kind"] == "tool_result"
assert rows[1]["content"] == "bounded preview"
def test_saved_tool_search_pages_past_first_200_candidates(tmp_path: Path):
target_file = tmp_path / "target.txt"
target_file.write_text("the deepneedle is here\n", encoding="utf-8")
decoy_file = tmp_path / "decoy.txt"
decoy_file.write_text("nothing relevant\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.append(
session_id="archive",
agent_id="ag1",
dedup_key="oldest-target",
entry=LogEntry(
kind="tool_result",
role="assistant",
content=_saved_tool_notice(target_file),
tool_call_id="target-call",
),
)
for index in range(200):
history.append(
session_id="archive",
agent_id="ag1",
dedup_key=f"newer-decoy-{index}",
entry=LogEntry(
kind="tool_result",
role="assistant",
content=_saved_tool_notice(decoy_file),
tool_call_id=f"decoy-{index}",
),
)
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
)
try:
rows = space.search("deepneedle", k=1)
finally:
space.close()
assert len(rows) == 1
assert rows[0]["kind"] == "tool_result"
assert "tool_call_id=target-call" in rows[0]["content"]
assert "deepneedle" in rows[0]["content"]
def test_saved_tool_file_search_streams_without_read_text(
tmp_path: Path,
monkeypatch,
):
artifact = tmp_path / "large.txt"
artifact.write_text(
"before\nneedle match\nafter\n",
encoding="utf-8",
)
def fail_read_text(*args, **kwargs):
raise AssertionError("saved artifact search must stream")
monkeypatch.setattr(Path, "read_text", fail_read_text)
matches = MemorySpace._file_line_matches(artifact, [["needle"]])
assert matches == [
{
"line": 2,
"excerpt": "1: before\n2: needle match\n3: after",
},
]
def test_saved_tool_file_search_keeps_and_terms_on_same_line(
tmp_path: Path,
):
artifact = tmp_path / "and-terms.txt"
artifact.write_text(
"foo\nbar\nfoo bar\n",
encoding="utf-8",
)
needle_groups = MemorySpace._query_needle_groups("foo bar")
matches = MemorySpace._file_line_matches(
artifact,
needle_groups,
context=0,
)
assert matches == [{"line": 3, "excerpt": "3: foo bar"}]
def test_attach_saved_tool_preserves_preview_when_scan_budget_exhausts(
tmp_path: Path,
):
artifact = tmp_path / "large.txt"
artifact.write_text("x" * 100 + " needle\n", encoding="utf-8")
history = HistoryStore(tmp_path / "history.db")
history.close()
space = MemorySpace(
history_db_path=tmp_path / "history.db",
session_id="current",
agent_id="ag1",
saved_tool_scan_max_bytes=32,
)
try:
rows = space._attach_saved_tool_file_matches(
[
{
"seq": 1,
"kind": "tool_result",
"role": "assistant",
"name": "read_file",
"content": (
"bounded preview retained in history\n"
+ _saved_tool_notice(artifact)
),
},
],
"needle",
)
finally:
space.close()
notices = [row for row in rows if row["kind"] == "_notice"]
assert len(notices) == 1
assert "Results are partial" in notices[0]["content"]
previews = [row for row in rows if row["kind"] == "tool_result"]
assert len(previews) == 1
assert "bounded preview retained in history" in previews[0]["content"]