mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
memory: route knowledge journal appends through append_jsonl (CPL4-C17)
knowledge_history.jsonl and knowledge_journal.jsonl were the last memory
journals appended with raw open("a") — a torn-line hazard under
concurrent writers. All three sites now use the sidecar-locked
append_jsonl seam every other journal uses (never-raise wrapping kept).
This commit is contained in:
parent
1dc9345ef2
commit
24f5e58371
3 changed files with 63 additions and 24 deletions
|
|
@ -134,7 +134,7 @@ data-relative path to be covered by a row here (count-anchored both ways).
|
|||
| `memory/dialogue_blocks.json` + `dialogue_meta.json` | `ouroboros/consolidator.py` (locked atomic) | none | bounded by era compression (10 blocks, oldest 4 compressed) | blocks: compressed biography irreproducible; meta: full re-consolidation (cost, not loss) |
|
||||
| `memory/dialogue_summary.md` | none — legacy read-only (reader in context.py) | none | frozen | legacy artifact; nothing writes it |
|
||||
| `memory/knowledge/**` (topic .md + `index-full.md` + `patterns.md`) | `ouroboros/tools/knowledge.py`, `consolidator.py` (index rebuild), `reflection.py` (patterns CAS rewrite) | none | topic files unbounded — accepted (curated by consolidation); backlog topic merge-only fail-closed | recreated lazily; knowledge lost |
|
||||
| `memory/*_journal.jsonl`, `memory/knowledge_history.jsonl`, `memory/knowledge/patterns_history.jsonl` | `ouroboros/memory.py`, `tools/control_runtime.py`, `tools/knowledge.py` (RAW `open("a")` — no append lock), `reflection.py` | scratchpad journal: `type` rows; others unversioned full-text snapshots | UNBOUNDED and worst-offenders by bytes: full old+new document text per write (O(doc×edits)) → CPL4-C16; unlocked appends → CPL4-C17 | undo/provenance record lost (live .md survives); eviction/rewrite paths fail closed when journal append fails |
|
||||
| `memory/*_journal.jsonl`, `memory/knowledge_history.jsonl`, `memory/knowledge/patterns_history.jsonl` | `ouroboros/memory.py`, `tools/control_runtime.py`, `tools/knowledge.py`, `reflection.py` — every append through the `append_jsonl` sidecar-lock seam (CPL4-C17) | scratchpad journal: `type` rows; others unversioned full-text snapshots | UNBOUNDED and worst-offenders by bytes: full old+new document text per write (O(doc×edits)) → CPL4-C16 | undo/provenance record lost (live .md survives); eviction/rewrite paths fail closed when journal append fails |
|
||||
| `memory/owner_mailbox/<task>.jsonl` + `.acks.jsonl` | `ouroboros/owner_mailbox.py` (append-only; revocation appends, reader resolves) | `kind` discriminator | lifecycle-bounded: unlinked at task terminal; a task that dies off-path leaks its mailbox → CPL4-C18 | undelivered owner directives + restart-surviving hurry latch lost; acks lost ⇒ re-delivery |
|
||||
|
||||
## 7. Skills payloads, tasks, uploads, projects, services
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"""Persistent topic-based knowledge files with an auto-maintained index."""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -10,7 +9,7 @@ from pathlib import Path
|
|||
from typing import List
|
||||
|
||||
from ouroboros.tools.registry import ToolEntry, ToolContext
|
||||
from ouroboros.utils import utc_now_iso
|
||||
from ouroboros.utils import append_jsonl, utc_now_iso
|
||||
from ouroboros.platform_layer import file_lock_exclusive, file_unlock
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -244,14 +243,15 @@ def _record_backlog_history(backlog_file: Path, topic: str, mode: str, task_id:
|
|||
try:
|
||||
history_path = backlog_file.parent.parent / "knowledge_history.jsonl"
|
||||
new_content = backlog_file.read_text(encoding="utf-8") if backlog_file.exists() else ""
|
||||
with open(history_path, "a", encoding="utf-8") as hf:
|
||||
hf.write(json.dumps({
|
||||
"ts": utc_now_iso(),
|
||||
"task_id": task_id,
|
||||
"topic": topic,
|
||||
"mode": f"{mode}->merge",
|
||||
"new_sha256": hashlib.sha256(new_content.encode("utf-8")).hexdigest() if new_content else "",
|
||||
}, ensure_ascii=False) + "\n")
|
||||
# CPL4-C17: the sidecar-locked append seam every other journal uses —
|
||||
# a raw open("a") could tear a line under concurrent writers.
|
||||
append_jsonl(history_path, {
|
||||
"ts": utc_now_iso(),
|
||||
"task_id": task_id,
|
||||
"topic": topic,
|
||||
"mode": f"{mode}->merge",
|
||||
"new_sha256": hashlib.sha256(new_content.encode("utf-8")).hexdigest() if new_content else "",
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -314,17 +314,17 @@ def _knowledge_write(ctx: ToolContext, topic: str, content: str, mode: str = "ov
|
|||
|
||||
try:
|
||||
history_path = _knowledge_dir(ctx).parent / "knowledge_history.jsonl"
|
||||
with open(history_path, "a", encoding="utf-8") as hf:
|
||||
hf.write(json.dumps({
|
||||
"ts": utc_now_iso(),
|
||||
"task_id": str(getattr(ctx, "task_id", "") or ""),
|
||||
"topic": sanitized_topic,
|
||||
"mode": mode,
|
||||
"old_sha256": hashlib.sha256(old_content.encode("utf-8")).hexdigest() if old_content else "",
|
||||
"new_sha256": hashlib.sha256(new_content.encode("utf-8")).hexdigest() if new_content else "",
|
||||
"old_content": old_content,
|
||||
"new_content": new_content,
|
||||
}, ensure_ascii=False) + "\n")
|
||||
# CPL4-C17: locked append (torn-line hazard under concurrent writers).
|
||||
append_jsonl(history_path, {
|
||||
"ts": utc_now_iso(),
|
||||
"task_id": str(getattr(ctx, "task_id", "") or ""),
|
||||
"topic": sanitized_topic,
|
||||
"mode": mode,
|
||||
"old_sha256": hashlib.sha256(old_content.encode("utf-8")).hexdigest() if old_content else "",
|
||||
"new_sha256": hashlib.sha256(new_content.encode("utf-8")).hexdigest() if new_content else "",
|
||||
"old_content": old_content,
|
||||
"new_content": new_content,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -343,8 +343,7 @@ def _knowledge_write(ctx: ToolContext, topic: str, content: str, mode: str = "ov
|
|||
"file_kb": path.stat().st_size / 1024,
|
||||
"total_knowledge_kb": round(total_kb, 2),
|
||||
}
|
||||
with open(journal_path, "a", encoding="utf-8") as jf:
|
||||
jf.write(json.dumps(entry) + "\n")
|
||||
append_jsonl(journal_path, entry) # CPL4-C17: same locked seam
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
40
tests/test_memory_journal_hygiene.py
Normal file
40
tests/test_memory_journal_hygiene.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""CPL4-C17 pins: knowledge journals append through the sidecar-lock seam.
|
||||
|
||||
``knowledge_history.jsonl`` / ``knowledge_journal.jsonl`` used raw
|
||||
``open("a")`` — the one torn-line hazard left among the memory journals.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
|
||||
def test_knowledge_module_has_no_raw_journal_appends():
|
||||
import ouroboros.tools.knowledge as knowledge
|
||||
|
||||
src = inspect.getsource(knowledge)
|
||||
assert 'open(history_path, "a"' not in src
|
||||
assert 'open(journal_path, "a"' not in src
|
||||
assert src.count("append_jsonl(") >= 3 # both history writers + the journal
|
||||
|
||||
|
||||
def test_knowledge_history_rows_land_via_locked_seam(tmp_path, monkeypatch):
|
||||
import ouroboros.tools.knowledge as knowledge
|
||||
|
||||
calls = []
|
||||
real = knowledge.append_jsonl
|
||||
|
||||
def spy(path, obj, **kw):
|
||||
calls.append(path.name)
|
||||
return real(path, obj, **kw)
|
||||
|
||||
monkeypatch.setattr(knowledge, "append_jsonl", spy)
|
||||
backlog = tmp_path / "memory" / "knowledge" / "backlog.md"
|
||||
backlog.parent.mkdir(parents=True)
|
||||
backlog.write_text("row", encoding="utf-8")
|
||||
|
||||
knowledge._record_backlog_history(backlog, "topic-x", "append", "t1")
|
||||
|
||||
assert calls == ["knowledge_history.jsonl"]
|
||||
line = (tmp_path / "memory" / "knowledge_history.jsonl").read_text(encoding="utf-8")
|
||||
assert '"topic": "topic-x"' in line and line.endswith("\n")
|
||||
Loading…
Add table
Add a link
Reference in a new issue