feat: complete multi-graph support across CLI, scripts, and systemd timers

Non-default graphs were second-class citizens — timers only maintained the
default graph, git sync ignored named graphs, there was no way to create
a graph without editing config manually, cross-graph edge errors were
confusing, and utility scripts were hardcoded to the default graph.

- Add `graph-create` CLI command + `create_graph()` in common.py, with
  custom path registration written to the default graph's _config.json
- Add `scripts/maintain-all-graphs.sh` to loop decay/core/embed/reflect
  over all discovered graphs; update systemd services to call it
- Refactor `memory-git-sync.sh` into sync_repo() function that iterates
  default + all named graphs with .git directories
- Improve cross-graph edge ValueError to explain the same-graph constraint
- Add --graph flag to edge-proposer.py and session_memory.py
- Update systemd/README.md with portable paths and new architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-02-28 22:35:55 -06:00
parent adc9a64c8d
commit d8dd1f35a5
11 changed files with 424 additions and 52 deletions

19
cli.py
View File

@ -8,6 +8,7 @@ Command-line interface for the cognitive memory system.
import argparse import argparse
import json import json
import sys import sys
from pathlib import Path
from client import CognitiveMemoryClient from client import CognitiveMemoryClient
from common import ( from common import (
@ -15,6 +16,7 @@ from common import (
VALID_RELATION_TYPES, VALID_RELATION_TYPES,
VALID_TYPES, VALID_TYPES,
_load_memory_config, _load_memory_config,
create_graph,
resolve_graph_path, resolve_graph_path,
list_graphs, list_graphs,
) )
@ -224,6 +226,19 @@ def main():
# graphs # graphs
subparsers.add_parser("graphs", help="List available memory graphs") subparsers.add_parser("graphs", help="List available memory graphs")
# graph-create
sp = subparsers.add_parser("graph-create", help="Create a new named memory graph")
sp.add_argument("name", help="Graph name (alphanumeric, hyphens OK)")
sp.add_argument(
"--path",
default=None,
help=(
"Custom directory path for the graph. "
"If omitted, uses the convention path (~/.local/share/cognitive-memory-graphs/<name>). "
"Custom paths are registered in the default graph's _config.json."
),
)
args = parser.parse_args() args = parser.parse_args()
if not args.command: if not args.command:
@ -467,6 +482,10 @@ def main():
elif args.command == "graphs": elif args.command == "graphs":
result = list_graphs() result = list_graphs()
elif args.command == "graph-create":
custom_path = Path(args.path) if args.path else None
result = create_graph(args.name, path=custom_path)
elif args.command == "config": elif args.command == "config":
config_path = client.memory_dir / "_config.json" config_path = client.memory_dir / "_config.json"
config = _load_memory_config(config_path) config = _load_memory_config(config_path)

View File

@ -557,6 +557,50 @@ def resolve_graph_path(
return GRAPHS_BASE_DIR / graph_name return GRAPHS_BASE_DIR / graph_name
def create_graph(name: str, path: Optional[Path] = None) -> Dict[str, Any]:
"""Create a new named graph directory structure.
If path is None, uses the convention path (GRAPHS_BASE_DIR / name) and
does NOT modify any config file the convention path is auto-discovered.
If a custom path is given, the mapping is written to the default graph's
_config.json under graphs.<name>.path so resolve_graph_path() can find it.
Returns a dict with keys: name, path, created (bool), registered (bool).
"""
if path is None:
graph_path = GRAPHS_BASE_DIR / name
register = False
else:
graph_path = Path(path).expanduser()
register = True
# Track whether this is a new graph or already exists
already_existed = graph_path.exists()
# Create the standard subdirectory layout that CognitiveMemoryClient expects
for type_dir in TYPE_DIRS.values():
(graph_path / "graph" / type_dir).mkdir(parents=True, exist_ok=True)
(graph_path / "graph" / EDGES_DIR_NAME).mkdir(parents=True, exist_ok=True)
(graph_path / "episodes").mkdir(parents=True, exist_ok=True)
(graph_path / "vault").mkdir(parents=True, exist_ok=True)
# Register custom path in the default graph's _config.json
if register:
cfg = _load_memory_config(CONFIG_PATH)
graphs_section = cfg.setdefault("graphs", {})
graphs_section[name] = {"path": str(graph_path)}
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
CONFIG_PATH.write_text(json.dumps(cfg, indent=2))
return {
"name": name,
"path": str(graph_path),
"created": not already_existed,
"registered": register,
}
def list_graphs(config_path: Optional[Path] = None) -> List[Dict[str, Any]]: def list_graphs(config_path: Optional[Path] = None) -> List[Dict[str, Any]]:
"""List all known graphs: default + configured + discovered on disk.""" """List all known graphs: default + configured + discovered on disk."""
result = [{"name": "default", "path": str(MEMORY_DIR)}] result = [{"name": "default", "path": str(MEMORY_DIR)}]

View File

@ -43,7 +43,11 @@ class EdgesMixin:
from_path = self._resolve_memory_path(from_id) from_path = self._resolve_memory_path(from_id)
to_path = self._resolve_memory_path(to_id) to_path = self._resolve_memory_path(to_id)
if not from_path or not to_path: if not from_path or not to_path:
raise ValueError(f"Memory not found: {from_id if not from_path else to_id}") missing_id = from_id if not from_path else to_id
raise ValueError(
f"Memory not found: {missing_id}. "
f"Note: edges can only connect memories within the same graph."
)
# Read source memory # Read source memory
fm, body = self._read_memory_file(from_path) fm, body = self._read_memory_file(from_path)

View File

@ -33,6 +33,7 @@ First run: 2026-02-19 — produced 5186 candidates from 473 memories,
20 high-quality edges were manually selected and created. 20 high-quality edges were manually selected and created.
""" """
import argparse
import json import json
import os import os
import re import re
@ -40,20 +41,53 @@ from pathlib import Path
from collections import defaultdict from collections import defaultdict
from itertools import combinations from itertools import combinations
# Resolve data directory: COGNITIVE_MEMORY_DIR > XDG_DATA_HOME > default # Resolve base data directory: COGNITIVE_MEMORY_DIR > XDG_DATA_HOME > default
_env_dir = os.environ.get("COGNITIVE_MEMORY_DIR", "") _env_dir = os.environ.get("COGNITIVE_MEMORY_DIR", "")
if _env_dir: if _env_dir:
MEMORY_DIR = Path(_env_dir).expanduser() _BASE_MEMORY_DIR = Path(_env_dir).expanduser()
else: else:
_xdg_data = os.environ.get("XDG_DATA_HOME", "") or str( _xdg_data = os.environ.get("XDG_DATA_HOME", "") or str(
Path.home() / ".local" / "share" Path.home() / ".local" / "share"
) )
MEMORY_DIR = Path(_xdg_data) / "cognitive-memory" _BASE_MEMORY_DIR = Path(_xdg_data) / "cognitive-memory"
# These are set at runtime in main() after --graph is resolved
MEMORY_DIR = _BASE_MEMORY_DIR
STATE_FILE = MEMORY_DIR / "_state.json" STATE_FILE = MEMORY_DIR / "_state.json"
GRAPH_DIR = MEMORY_DIR / "graph" GRAPH_DIR = MEMORY_DIR / "graph"
EDGES_DIR = GRAPH_DIR / "edges" EDGES_DIR = GRAPH_DIR / "edges"
def _resolve_graph_dir(graph_name: str | None) -> Path:
"""Resolve a graph name to its directory path.
Mirrors the logic in common.resolve_graph_path without importing the full
client package (which would pull in heavy dependencies).
None / 'default' -> MEMORY_DIR (the default graph).
Named graph -> sibling directory next to MEMORY_DIR, e.g.
~/.local/share/cognitive-memory-<name>.
"""
if not graph_name or graph_name == "default":
return _BASE_MEMORY_DIR
# Check _config.json for a registered path
config_path = _BASE_MEMORY_DIR / "_config.json"
if config_path.exists():
try:
cfg = json.loads(config_path.read_text())
graphs = cfg.get("graphs", {})
if graph_name in graphs:
p = graphs[graph_name].get("path", "")
if p:
return Path(p).expanduser()
except Exception:
pass
# Convention: ~/.local/share/cognitive-memory-graphs/<name>
return _BASE_MEMORY_DIR.parent / "cognitive-memory-graphs" / graph_name
# Type-based heuristics: (type_a, type_b) -> (suggested_rel, direction, base_score) # Type-based heuristics: (type_a, type_b) -> (suggested_rel, direction, base_score)
# direction: "ab" means a->b, "ba" means b->a # direction: "ab" means a->b, "ba" means b->a
TYPE_HEURISTICS = { TYPE_HEURISTICS = {
@ -349,6 +383,31 @@ def score_pair(mem_a: dict, mem_b: dict) -> dict | None:
def main(): def main():
global MEMORY_DIR, STATE_FILE, GRAPH_DIR, EDGES_DIR
parser = argparse.ArgumentParser(
description="Analyze cognitive memories and propose high-quality edges."
)
parser.add_argument(
"--graph",
default=None,
metavar="NAME",
help=(
"Named memory graph to analyze (default: the default graph). "
"Use 'claude-memory graphs' to list available graphs."
),
)
args = parser.parse_args()
# Resolve graph directory and update module-level path globals
MEMORY_DIR = _resolve_graph_dir(args.graph)
STATE_FILE = MEMORY_DIR / "_state.json"
GRAPH_DIR = MEMORY_DIR / "graph"
EDGES_DIR = GRAPH_DIR / "edges"
if args.graph:
print(f"Using graph: {args.graph} ({MEMORY_DIR})")
print("Loading memories...") print("Loading memories...")
memories = load_memories() memories = load_memories()
print(f" Found {len(memories)} memories") print(f" Found {len(memories)} memories")

135
scripts/maintain-all-graphs.sh Executable file
View File

@ -0,0 +1,135 @@
#!/bin/bash
# Run cognitive memory maintenance (decay, core, embed, reflect) for ALL graphs.
#
# Called by systemd service units instead of individual claude-memory commands,
# so that named graphs receive the same maintenance as the default graph.
#
# Usage:
# maintain-all-graphs.sh [--daily | --embed | --weekly]
#
# --daily (default) Run decay + core for every graph
# --embed Run embed --if-changed for every graph
# --weekly Run reflect for every graph
#
# Named graph directories that do not exist on disk are silently skipped.
# The default graph is always processed first regardless of disk discovery.
set -euo pipefail
# ── configuration ──────────────────────────────────────────────────────────────
CLAUDE_MEMORY="${CLAUDE_MEMORY_BIN:-/home/cal/.local/bin/claude-memory}"
export PATH="/home/cal/.local/bin:$PATH"
MODE="${1:---daily}"
# ── helpers ────────────────────────────────────────────────────────────────────
log() {
echo "maintain-all-graphs [$(date '+%H:%M:%S')]: $*"
}
# Run a claude-memory command for one graph.
# $1 = graph name ("default" omits --graph flag, others pass --graph <name>)
# remaining args = claude-memory subcommand + flags
run_for_graph() {
local graph_name="$1"
shift
local graph_flag=()
if [ "$graph_name" != "default" ]; then
graph_flag=(--graph "$graph_name")
fi
log "[$graph_name] $*"
"$CLAUDE_MEMORY" "${graph_flag[@]}" "$@"
}
# ── discover graphs ────────────────────────────────────────────────────────────
#
# claude-memory graphs outputs a JSON array:
# [{"name": "default", "path": "/home/cal/.local/share/cognitive-memory"}, ...]
#
# We parse it with only sh/awk — no jq dependency.
# Strategy: extract "name" and "path" values line by line from pretty-printed JSON.
graphs_json=$("$CLAUDE_MEMORY" graphs 2>/dev/null) || {
log "ERROR: 'claude-memory graphs' failed; aborting"
exit 1
}
# Build parallel arrays: GRAPH_NAMES and GRAPH_PATHS
GRAPH_NAMES=()
GRAPH_PATHS=()
# State machine: after seeing "name": capture value; after "path": capture value.
# Each object ends when we collect both; append to arrays.
_name=""
_path=""
while IFS= read -r line; do
# Strip leading/trailing whitespace
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
# Match: "name": "value"
if [[ "$line" =~ ^\"name\":\ *\"([^\"]+)\" ]]; then
_name="${BASH_REMATCH[1]}"
fi
# Match: "path": "value" (path may contain slashes — [^"]+ is fine)
if [[ "$line" =~ ^\"path\":\ *\"([^\"]+)\" ]]; then
_path="${BASH_REMATCH[1]}"
fi
# When we have both, flush to arrays (object boundary)
if [ -n "$_name" ] && [ -n "$_path" ]; then
GRAPH_NAMES+=("$_name")
GRAPH_PATHS+=("$_path")
_name=""
_path=""
fi
done <<< "$graphs_json"
if [ "${#GRAPH_NAMES[@]}" -eq 0 ]; then
log "ERROR: no graphs discovered; aborting"
exit 1
fi
log "discovered ${#GRAPH_NAMES[@]} graph(s): ${GRAPH_NAMES[*]}"
# ── main loop ─────────────────────────────────────────────────────────────────
ERRORS=0
for i in "${!GRAPH_NAMES[@]}"; do
name="${GRAPH_NAMES[$i]}"
path="${GRAPH_PATHS[$i]}"
# Skip graphs whose directory doesn't exist on disk (named graphs may not
# be initialised yet on this machine).
if [ "$name" != "default" ] && [ ! -d "$path" ]; then
log "[$name] skipping — directory not found: $path"
continue
fi
case "$MODE" in
--daily)
run_for_graph "$name" decay || { log "[$name] decay FAILED"; ERRORS=$((ERRORS+1)); }
run_for_graph "$name" core || { log "[$name] core FAILED"; ERRORS=$((ERRORS+1)); }
;;
--embed)
run_for_graph "$name" embed --if-changed || { log "[$name] embed FAILED"; ERRORS=$((ERRORS+1)); }
;;
--weekly)
run_for_graph "$name" reflect || { log "[$name] reflect FAILED"; ERRORS=$((ERRORS+1)); }
;;
*)
echo "Usage: $0 [--daily | --embed | --weekly]" >&2
exit 1
;;
esac
done
if [ "$ERRORS" -gt 0 ]; then
log "completed with $ERRORS error(s)"
exit 1
fi
log "done (mode: $MODE)"

View File

@ -1,47 +1,103 @@
#!/bin/bash #!/bin/bash
# Commit and push cognitive memory changes to Gitea (cal/claude-memory) # Commit and push cognitive memory changes to Gitea.
# #
# Called daily by cognitive-memory-daily.service after decay/core/symlinks. # Syncs the default graph and any named graphs whose directory is a git repo.
# Only commits if there are actual changes. Safe to run multiple times. # Called daily by cognitive-memory-daily.service after decay/core runs.
# Safe to run multiple times — only commits when there are actual changes.
#
# Default graph: COGNITIVE_MEMORY_DIR > XDG_DATA_HOME > ~/.local/share/cognitive-memory
# Named graphs: ~/.local/share/cognitive-memory-graphs/<name>/ (any with .git/)
# #
# Location: ~/.claude/skills/cognitive-memory/scripts/memory-git-sync.sh # Location: ~/.claude/skills/cognitive-memory/scripts/memory-git-sync.sh
# Repo: cognitive-memory data dir -> https://git.manticorum.com/cal/claude-memory.git
set -euo pipefail set -uo pipefail
# Note: -e intentionally omitted at the top level so that one graph's failure
# does not prevent the remaining graphs from being synced. Each function call
# is checked explicitly.
# ── resolve directories ────────────────────────────────────────────────────────
# Resolve data directory: COGNITIVE_MEMORY_DIR > XDG_DATA_HOME > default
MEMORY_DIR="${COGNITIVE_MEMORY_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/cognitive-memory}" MEMORY_DIR="${COGNITIVE_MEMORY_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/cognitive-memory}"
cd "$MEMORY_DIR" # Named graphs live in a sibling directory: cognitive-memory-graphs/<name>/
GRAPHS_BASE_DIR="${MEMORY_DIR%cognitive-memory}cognitive-memory-graphs"
# Check if there are any changes to commit # ── sync function ──────────────────────────────────────────────────────────────
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then # sync_repo <dir> <label>
echo "memory-git-sync: no changes to commit" # Commits and pushes any pending changes in the given git repo directory.
# Returns 0 on success or "nothing to commit", non-zero on error.
sync_repo() {
local dir="$1"
local label="$2"
# Directory must exist and be a git repo
if [ ! -d "$dir/.git" ]; then
echo "memory-git-sync [$label]: not a git repo, skipping"
return 0
fi
(
# Run everything inside a subshell so cd is scoped and set -e is safe
set -euo pipefail
cd "$dir"
# Nothing to commit?
if git diff --quiet && git diff --cached --quiet \
&& [ -z "$(git ls-files --others --exclude-standard)" ]; then
echo "memory-git-sync [$label]: no changes to commit"
exit 0 exit 0
fi fi
# Stage all changes (.gitignore handles exclusions) # Stage all changes (.gitignore handles exclusions)
git add -A git add -A
# Build a commit message from what changed # Build a descriptive commit message
ADDED=$(git diff --cached --name-only --diff-filter=A | wc -l) ADDED=$(git diff --cached --name-only --diff-filter=A | wc -l)
MODIFIED=$(git diff --cached --name-only --diff-filter=M | wc -l) MODIFIED=$(git diff --cached --name-only --diff-filter=M | wc -l)
DELETED=$(git diff --cached --name-only --diff-filter=D | wc -l) DELETED=$(git diff --cached --name-only --diff-filter=D | wc -l)
EDGES=$(git diff --cached --name-only --diff-filter=ACM | grep -c '^graph/edges/' || true) EDGES=$(git diff --cached --name-only --diff-filter=ACM | grep -c '^graph/edges/' || true)
MSG="daily sync: ${ADDED} added, ${MODIFIED} modified, ${DELETED} deleted" MSG="daily sync: ${ADDED} added, ${MODIFIED} modified, ${DELETED} deleted"
if [ "$EDGES" -gt 0 ]; then if [ "$EDGES" -gt 0 ]; then
MSG="$MSG (${EDGES} edges)" MSG="$MSG (${EDGES} edges)"
fi
git commit -m "$MSG" --no-gpg-sign 2>/dev/null || {
echo "memory-git-sync [$label]: commit failed (pre-commit hook?)"
exit 1
}
git push origin main 2>/dev/null || {
echo "memory-git-sync [$label]: push failed"
exit 1
}
echo "memory-git-sync [$label]: pushed to origin/main"
)
}
# ── sync default graph ─────────────────────────────────────────────────────────
ERRORS=0
sync_repo "$MEMORY_DIR" "default" || ERRORS=$((ERRORS + 1))
# ── sync named graphs ──────────────────────────────────────────────────────────
# Iterate subdirectories of GRAPHS_BASE_DIR; skip non-git directories silently.
if [ -d "$GRAPHS_BASE_DIR" ]; then
for graph_dir in "$GRAPHS_BASE_DIR"/*/; do
# glob may yield literal "*/'" if directory is empty
[ -d "$graph_dir" ] || continue
graph_name="$(basename "$graph_dir")"
sync_repo "$graph_dir" "$graph_name" || ERRORS=$((ERRORS + 1))
done
fi fi
git commit -m "$MSG" --no-gpg-sign 2>/dev/null || { # ── exit status ───────────────────────────────────────────────────────────────
echo "memory-git-sync: commit failed (pre-commit hook?)"
exit 1
}
git push origin main 2>/dev/null || { if [ "$ERRORS" -gt 0 ]; then
echo "memory-git-sync: push failed" echo "memory-git-sync: completed with $ERRORS error(s)"
exit 1 exit 1
} fi
echo "memory-git-sync: pushed to origin/main"

View File

@ -7,6 +7,7 @@ architecture decisions, new patterns, configurations), and stores them as
cognitive memories via claude-memory CLI. cognitive memories via claude-memory CLI.
""" """
import argparse
import json import json
import re import re
import subprocess import subprocess
@ -480,8 +481,13 @@ def build_title(summary: dict) -> str:
return f"[{project}] Session: {work}" return f"[{project}] Session: {work}"
def store_memory(summary: dict): def store_memory(summary: dict, graph: str | None = None):
"""Store the session memory via claude-memory CLI.""" """Store the session memory via claude-memory CLI.
Args:
summary: Session summary dict from build_session_summary().
graph: Named memory graph to store into, or None for the default graph.
"""
title = build_title(summary) title = build_title(summary)
content = build_memory_content(summary) content = build_memory_content(summary)
mem_type = determine_memory_type(summary) mem_type = determine_memory_type(summary)
@ -501,8 +507,11 @@ def store_memory(summary: dict):
tags.append("session-log") tags.append("session-log")
tag_str = ",".join(tags) tag_str = ",".join(tags)
cmd = [ # Base command: optionally target a named graph
"claude-memory", cmd = ["claude-memory"]
if graph:
cmd += ["--graph", graph]
cmd += [
"store", "store",
"--type", "--type",
mem_type, mem_type,
@ -544,6 +553,24 @@ def store_memory(summary: dict):
def main(): def main():
log_separator() log_separator()
parser = argparse.ArgumentParser(
description="Session-end memory hook: store session events as cognitive memories.",
add_help=False, # keep --help available but don't conflict with hook stdin
)
parser.add_argument(
"--graph",
default=None,
metavar="NAME",
help=(
"Named memory graph to store memories into (default: the default graph). "
"Use 'claude-memory graphs' to list available graphs."
),
)
parser.add_argument(
"--help", "-h", action="help", help="Show this help message and exit."
)
args, _ = parser.parse_known_args()
hook_input = read_stdin() hook_input = read_stdin()
transcript_path = hook_input.get("transcript_path", "") transcript_path = hook_input.get("transcript_path", "")
cwd = hook_input.get("cwd", "") cwd = hook_input.get("cwd", "")
@ -579,7 +606,7 @@ def main():
log(f"[main] ABORT: build_session_summary returned '{summary}'") log(f"[main] ABORT: build_session_summary returned '{summary}'")
sys.exit(0) sys.exit(0)
store_memory(summary) store_memory(summary, graph=args.graph)
log("[main] Done") log("[main] Done")

View File

@ -6,16 +6,31 @@ Reference copies of the systemd user units that automate memory maintenance.
| Unit | Schedule | What it does | | Unit | Schedule | What it does |
|------|----------|-------------| |------|----------|-------------|
| `cognitive-memory-daily` | daily | Decay scores, regenerate CORE.md, refresh MEMORY.md symlinks | | `cognitive-memory-daily` | daily | Decay scores, regenerate CORE.md, git sync — for ALL graphs |
| `cognitive-memory-embed` | hourly | Refresh embeddings (skips if unchanged) | | `cognitive-memory-embed` | hourly | Refresh embeddings (skips if unchanged) — for ALL graphs |
| `cognitive-memory-weekly` | weekly | Run reflection cycle | | `cognitive-memory-weekly` | weekly | Run reflection cycle — for ALL graphs |
All three services now call `scripts/maintain-all-graphs.sh` instead of
individual `claude-memory` commands. The script discovers every graph via
`claude-memory graphs` (default graph + named graphs under
`~/.local/share/cognitive-memory-graphs/`) and runs the appropriate command
for each one, passing `--graph <name>` for non-default graphs.
Named graph directories that do not exist on disk are silently skipped.
## Scripts
| Script | Purpose |
|--------|---------|
| `scripts/maintain-all-graphs.sh` | Loop decay/core/embed/reflect over all graphs |
| `scripts/memory-git-sync.sh` | Git commit+push for default graph and all named git repos |
## Install / Update ## Install / Update
```bash ```bash
# Copy units into place # Copy units into place (adjust source path to your cognitive-memory install)
cp ~/.claude/skills/cognitive-memory/systemd/*.service \ CMDIR=~/.claude/skills/cognitive-memory # or /mnt/NV2/Development/cognitive-memory
~/.claude/skills/cognitive-memory/systemd/*.timer \ cp "$CMDIR"/systemd/*.service "$CMDIR"/systemd/*.timer \
~/.config/systemd/user/ ~/.config/systemd/user/
# Reload and enable # Reload and enable
@ -32,3 +47,16 @@ systemctl --user list-timers 'cognitive-memory-*'
systemctl --user start cognitive-memory-daily.service # manual test run systemctl --user start cognitive-memory-daily.service # manual test run
journalctl --user -u cognitive-memory-daily.service --since today journalctl --user -u cognitive-memory-daily.service --since today
``` ```
## Manual test (single graph)
```bash
# Default graph only
claude-memory decay && claude-memory core
# Named graph
claude-memory --graph paper-dynasty decay && claude-memory --graph paper-dynasty core
# All graphs at once (adjust path to your install)
/path/to/cognitive-memory/scripts/maintain-all-graphs.sh --daily
```

View File

@ -1,6 +1,6 @@
[Unit] [Unit]
Description=Cognitive Memory daily maintenance (decay, core, git sync) Description=Cognitive Memory daily maintenance (decay, core, git sync) for all graphs
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/bin/bash -c 'export PATH="/home/cal/.local/bin:$PATH" && /home/cal/.local/bin/claude-memory decay && /home/cal/.local/bin/claude-memory core && /mnt/NV2/Development/cognitive-memory/scripts/memory-git-sync.sh' ExecStart=/bin/bash -c 'export PATH="/home/cal/.local/bin:$PATH" && /mnt/NV2/Development/cognitive-memory/scripts/maintain-all-graphs.sh --daily && /mnt/NV2/Development/cognitive-memory/scripts/memory-git-sync.sh'

View File

@ -1,6 +1,6 @@
[Unit] [Unit]
Description=Cognitive Memory hourly embedding refresh (skips if unchanged) Description=Cognitive Memory hourly embedding refresh (skips if unchanged) for all graphs
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/bin/bash -c 'export PATH="/home/cal/.local/bin:$PATH" && /home/cal/.local/bin/claude-memory embed --if-changed' ExecStart=/bin/bash -c 'export PATH="/home/cal/.local/bin:$PATH" && /mnt/NV2/Development/cognitive-memory/scripts/maintain-all-graphs.sh --embed'

View File

@ -1,6 +1,6 @@
[Unit] [Unit]
Description=Cognitive Memory weekly reflection Description=Cognitive Memory weekly reflection for all graphs
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/home/cal/.local/bin/claude-memory reflect ExecStart=/bin/bash -c 'export PATH="/home/cal/.local/bin:$PATH" && /mnt/NV2/Development/cognitive-memory/scripts/maintain-all-graphs.sh --weekly'