mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
fix: retain durable chat history gaps
Project consolidator-owned memory gap blocks into explicit history coverage and bind their stable identifiers into the stateless pagination snapshot. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
5defe0ccda
commit
1ea6d304b7
2 changed files with 94 additions and 0 deletions
|
|
@ -116,6 +116,13 @@ def _chat_history_snapshot_id(
|
|||
"schema_version": 1,
|
||||
"query": _normalized_chat_history_query(filters, search),
|
||||
"generations": generations,
|
||||
# Existing consolidator gap blocks are the durable truth that an older
|
||||
# span is unknowable after its cursor is rebased. Their small stable IDs
|
||||
# participate in the stateless token; no block-content/full-file hashing
|
||||
# or new continuation state is introduced.
|
||||
"durable_gap_ids": [
|
||||
str(value) for value in (coverage.get("durable_gap_ids") or [])
|
||||
],
|
||||
}
|
||||
encoded = json.dumps(
|
||||
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
|
||||
|
|
@ -338,6 +345,31 @@ class Memory:
|
|||
path = self.drive_root / "memory" / "dialogue_blocks.json"
|
||||
return self._load_json_blocks(path)
|
||||
|
||||
def _durable_dialogue_gaps(self) -> Tuple[List[Dict[str, Any]], List[str]]:
|
||||
"""Project consolidator-owned durable discontinuities into raw coverage."""
|
||||
|
||||
gaps: List[Dict[str, Any]] = []
|
||||
identities: List[str] = []
|
||||
for index, block in enumerate(self.load_dialogue_blocks()):
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
gap_id = str(block.get("gap_id") or "").strip()
|
||||
content = str(block.get("content") or "")
|
||||
if not gap_id and "[MEMORY GAP]" not in content:
|
||||
continue
|
||||
identity = gap_id or (
|
||||
"legacy-memory-gap:"
|
||||
f"{index}:{str(block.get('ts') or '')}:{str(block.get('range') or '')}"
|
||||
)
|
||||
identities.append(identity)
|
||||
gaps.append({
|
||||
"kind": "durable_consolidation_gap",
|
||||
"gap_id": gap_id,
|
||||
"block_index": index,
|
||||
"detail": "A durable dialogue block records a known history discontinuity.",
|
||||
})
|
||||
return gaps, identities
|
||||
|
||||
def load_dialogue_meta(self) -> Dict[str, Any]:
|
||||
path = self.drive_root / "memory" / "dialogue_meta.json"
|
||||
return read_json_dict(path) or {}
|
||||
|
|
@ -568,6 +600,9 @@ class Memory:
|
|||
"kind": "consolidation_cursor_state_unreadable",
|
||||
"error": type(exc).__name__,
|
||||
})
|
||||
durable_gaps, durable_gap_ids = self._durable_dialogue_gaps()
|
||||
coverage["gaps"].extend(durable_gaps)
|
||||
coverage["durable_gap_ids"] = durable_gap_ids
|
||||
last_entries, last_coverage = entries, coverage
|
||||
if stable_generations:
|
||||
return entries, coverage
|
||||
|
|
|
|||
|
|
@ -202,6 +202,65 @@ def test_explicit_chat_history_surfaces_missing_consolidation_generation(tmp_pat
|
|||
assert "completeness unknown" in no_survivors
|
||||
|
||||
|
||||
def test_chat_history_keeps_durable_gap_after_consolidator_rebases_cursor(tmp_path):
|
||||
from ouroboros.consolidator import consolidate, should_consolidate
|
||||
|
||||
_write(tmp_path / "logs" / "chat.jsonl", [_row("2026-08-21T09:00:00Z", "survivor")])
|
||||
memory_dir = tmp_path / "memory"
|
||||
memory_dir.mkdir(parents=True)
|
||||
blocks_path = memory_dir / "dialogue_blocks.json"
|
||||
meta_path = memory_dir / "dialogue_meta.json"
|
||||
meta_path.write_text(json.dumps({
|
||||
"last_consolidated_offset": 50,
|
||||
"chat_log_signature": {"first_line_sha256": "f" * 64, "size": 999},
|
||||
}), encoding="utf-8")
|
||||
memory = Memory(tmp_path)
|
||||
|
||||
first = memory.chat_history(count=20)
|
||||
old_snapshot = re.search(r"snapshot=([0-9a-f]{64})", first)
|
||||
assert old_snapshot is not None
|
||||
assert "consolidation_cursor_generation_missing" in first
|
||||
assert should_consolidate(meta_path, tmp_path / "logs" / "chat.jsonl") is True
|
||||
|
||||
class NoLlmExpected:
|
||||
def chat(self, **_kwargs):
|
||||
raise AssertionError("gap rebasing below BLOCK_SIZE must not call the LLM")
|
||||
|
||||
assert consolidate(
|
||||
chat_path=tmp_path / "logs" / "chat.jsonl",
|
||||
blocks_path=blocks_path,
|
||||
meta_path=meta_path,
|
||||
llm_client=NoLlmExpected(),
|
||||
) is None
|
||||
blocks = json.loads(blocks_path.read_text(encoding="utf-8"))
|
||||
assert blocks[0]["gap_id"].startswith("gap:")
|
||||
assert "[MEMORY GAP]" in blocks[0]["content"]
|
||||
|
||||
second = memory.chat_history(count=20)
|
||||
stale_page = memory.chat_history(count=20, snapshot=old_snapshot.group(1))
|
||||
assert "durable_consolidation_gap" in second
|
||||
assert "completeness unknown" in second
|
||||
assert stale_page.startswith("CHAT_HISTORY_SNAPSHOT_CHANGED:")
|
||||
|
||||
|
||||
def test_chat_history_recognizes_legacy_memory_gap_block_without_gap_id(tmp_path):
|
||||
_write(tmp_path / "logs" / "chat.jsonl", [_row("2026-08-21T09:00:00Z", "survivor")])
|
||||
memory_dir = tmp_path / "memory"
|
||||
memory_dir.mkdir(parents=True)
|
||||
(memory_dir / "dialogue_blocks.json").write_text(json.dumps([{
|
||||
"ts": "2026-08-21T10:00:00Z",
|
||||
"type": "summary",
|
||||
"range": "unknown",
|
||||
"message_count": 0,
|
||||
"content": "[MEMORY GAP] Legacy durable discontinuity.",
|
||||
}]), encoding="utf-8")
|
||||
|
||||
result = Memory(tmp_path).chat_history(count=20)
|
||||
|
||||
assert "durable_consolidation_gap" in result
|
||||
assert "completeness unknown" in result
|
||||
|
||||
|
||||
def test_chat_history_tool_exposes_only_exact_filter_fields():
|
||||
tool = next(entry for entry in get_tools() if entry.name == "chat_history")
|
||||
assert set(tool.schema["parameters"]["properties"]) == {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue