resilient memory sync: decouple push/pull, startup full resync, auth failure handling

- Decouple push and pull in _sync_once() so pull always runs even if push fails
- Add startup full resync to catch drift from other agents and schema changes
- Add periodic full resync every ~10 minutes for continuous drift correction
- Add auth failure detection (401/403) with graceful SQLite-only degradation
- Add /api/auth-check endpoint for lightweight key validation
- Add retry cap (5 attempts) on pending ops to prevent infinite queue buildup
- Add orphan reconciliation: push local-only records with content dedup
- Add memory_count MCP tool for sync diagnostics
- Add version-based SQLite schema migration (PRAGMA user_version)
- Fix API key in ~/.claude.json to match server
- Update README with sync resilience docs, test structure, project layout
- Add 30 new tests covering all new behaviors (155 total, all passing)
This commit is contained in:
Viktor Barzin 2026-03-16 18:35:09 +00:00
parent a18b94d310
commit e47efee6b6
No known key found for this signature in database
GPG key ID: 0EB088298288D958
8 changed files with 948 additions and 134 deletions

View file

@ -12,7 +12,7 @@ haiku to detect learnings worth persisting:
Features:
- Multi-turn context window (last 5 exchanges by default)
- State tracking to avoid duplicate extraction
- Writes to memory API/SQLite AND auto-memory markdown files
- Writes to memory API/SQLite only
- Throttled deep extraction: full window every ~5 turns, single-turn otherwise
Runs with async: true does NOT block the user.
@ -252,36 +252,6 @@ def _store_via_sqlite(content, category, tags, importance, expanded_keywords):
conn.close()
def _append_to_auto_memory(content: str, event_type: str) -> None:
"""Append a learning to the auto-memory markdown file for the current project."""
# Find the project memory directory based on CWD
cwd = os.getcwd()
# Claude Code stores project memory at ~/.claude/projects/<escaped-path>/memory/
escaped = cwd.replace("/", "-")
if escaped.startswith("-"):
escaped = escaped[1:] # Remove leading dash
memory_dir = Path.home() / ".claude" / "projects" / f"-{escaped}" / "memory"
if not memory_dir.exists():
# Try without the leading dash
memory_dir = Path.home() / ".claude" / "projects" / escaped / "memory"
if not memory_dir.exists():
return
auto_learn_file = memory_dir / "auto-learned.md"
now = datetime.now(timezone.utc).strftime("%Y-%m-%d")
header = "# Auto-Learned Knowledge\n\nAutomatically extracted by the auto-learn hook. Review periodically and promote valuable entries to MEMORY.md.\n\n"
if not auto_learn_file.exists():
auto_learn_file.write_text(header)
# Append the new learning
with open(auto_learn_file, "a") as f:
f.write(f"- [{now}] **{event_type}**: {content}\n")
def _parse_llm_response(response_text: str) -> list[dict]:
"""Parse LLM response text into events list."""
response_text = response_text.strip()
@ -485,12 +455,6 @@ def _store_events(events: list[dict], extracted_hashes: list[str]) -> list[str]:
except Exception:
pass
# Also append to auto-memory markdown
try:
_append_to_auto_memory(content, event_type)
except Exception:
pass
new_hashes.append(h)
return new_hashes