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:
Codex 2026-08-21 16:46:07 +03:00 • committed by Ouroboros
parent 5defe0ccda
commit 1ea6d304b7
2 changed files with 94 additions and 0 deletions

View file

@ -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

View file

@ -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"]) == {