memory.storage¶
memory.storage ¶
File-based storage for the memory tree.
Entries, skills, sessions, and documents are stored as individual markdown files
with YAML frontmatter inside .memory/. The KGLite .memory.kgl file becomes
a disposable cache rebuilt from these files.
Directory layout::
.memory/
config.yaml
project.md
entries/<id>.md
skills/<name>.md
sessions/<id>.md
documents/<id>.md
archive/entries/<id>.md
Derived caches (git-ignored, under .angelo/)::
.angelo/memory.kgl
.angelo/memory.index.json
.angelo/memory.embeddings.json
pid_alive ¶
Best-effort check whether a process with this PID is still running.
Used to detect live-session sidecars left behind by dead MCP processes. PID reuse can cause false positives; heartbeat staleness covers that case.
Source code in memory/storage.py
source_fileset_signature ¶
Exact signature of the local .memory/ SOURCE files the graph is built from.
Covers project.md plus every *.md under the entries, skills, sessions,
and documents directories — the authoritative source of truth. Each file
contributes its path and st_mtime_ns, so an add/delete/rename (a changed
path set) or an in-place edit (a changed mtime) both move the value. The parts
are \x00-joined; the directory globals are read at call time so tests that
monkeypatch them (or a re-anchored workspace) are honoured.
Derived caches (the .kgl snapshot) are DELIBERATELY excluded. They are
rebuilt from these files, so folding a cache's own mtime into the freshness
key would make it self-referential: merely re-saving the cache would
invalidate it. Keeping the signature source-only also lets it double as a
cross-process cache-validity token — any process that rebuilds from these
files can persist the resulting .kgl beside this signature (see
:func:write_cache_signature), and any other process may adopt that cache iff
the signature still matches its own view of the sources.
Source code in memory/storage.py
sig_path_for ¶
Path of the .sig sidecar that records a .kgl cache's source signature.
A single convention (<name>.kgl → <name>.kgl.sig) shared by every
writer (the memory server) and reader (the dashboard loader) so they agree on
where the cross-process validity token lives.
Source code in memory/storage.py
write_cache_signature ¶
Persist the current source-fileset signature beside a .kgl cache.
Call this immediately after saving the .kgl so a separate process can
validate and adopt that cache on a cold start (skipping a full rebuild) iff
the signature still matches its own view of the source files. Writing the sig
second is load-bearing: a torn write leaves the sig absent or stale, which a
reader treats as a miss and falls back to rebuilding — never a false adoption.
The write is atomic; callers may ignore failures (a missing sig is a safe skip).
Source code in memory/storage.py
migrate_legacy_caches ¶
Move legacy cache locations into ANGELO_DIR.
Handles both pre-.cassius root-level files and the pre-.angelo
.cassius/ directory. Safe to call on every startup: only moves a
legacy location when it exists and the new location doesn't.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kgl_target
|
Path | None
|
the configured |
None
|
Source code in memory/storage.py
write_entry ¶
Write an entry to .memory/entries/<id>.md.
Source code in memory/storage.py
write_skill ¶
Write a skill to .memory/skills/<name>.md.
Source code in memory/storage.py
write_session ¶
Write a session to .memory/sessions/<id>.md.
Source code in memory/storage.py
write_document ¶
Write a document to .memory/documents/<id>.md.
Source code in memory/storage.py
write_project ¶
Write project metadata to .memory/project.md.
Source code in memory/storage.py
archive_entry ¶
Move an entry file to archive/entries/. Returns the new path or None.
Source code in memory/storage.py
update_entry_field ¶
Read an existing entry file, apply field updates, write it back.
The read-modify-write is guarded by a per-id cross-process lock so two MCP
processes updating the same entry cannot lose each other's field changes.
write_entry itself does not take the lock, so there is no nested-lock
deadlock when it is called from inside the critical section.
Source code in memory/storage.py
update_skill_field ¶
Read an existing skill file, apply field updates, write it back.
Source code in memory/storage.py
update_document_field ¶
Read an existing document file, apply field updates, write it back.
Source code in memory/storage.py
read_file ¶
Parse a single markdown file back into a flat dict.
read_all ¶
Read the entire .memory/ tree into a dict.
Source code in memory/storage.py
write_config ¶
Write .memory/config.yaml.
Source code in memory/storage.py
read_config ¶
Read .memory/config.yaml, or return defaults if missing.
Source code in memory/storage.py
store_is_established ¶
Whether .memory/ is an authoritative file-based store.
A store counts as established once it carries the format stamp
(config.yaml) or any entry files. A merely-present .memory/ that
is empty or holds only runtime sidecars (e.g. a stray active-sessions/
directory, or a folder a user created by hand) does NOT count — so a legacy
.kgl is still allowed to migrate into it.
This is the startup gate's source of truth: True means rebuild the cache
from files (and never overwrite them); False with a .kgl present
means migrate. Checking for entry files (not just config.yaml) protects
a real but unstamped store from being clobbered by a stale .kgl.
Source code in memory/storage.py
build_index ¶
Walk .memory/ and build {rel_path: {mtime, mtime_ns, size, content_hash}}.
Change detection is two-pass: every file is stat-ed for a
(mtime_ns, size) fingerprint and its content SHA-256 is recomputed only
when that fingerprint differs from the cached entry — otherwise the prior
hash is reused. This avoids re-hashing the entire corpus (O(total bytes)) on
every auto-sync poll while keeping :func:get_changed_files exact: a file is
reported changed iff its content hash changed (mtime/size are only a fast
pre-filter; a bare mtime bump forces a re-hash and comparison, never a false
positive). When prev_index is omitted the persisted index cache is used
as the reuse source.
Source code in memory/storage.py
load_index ¶
Read the index cache from ANGELO_DIR.
Source code in memory/storage.py
save_index ¶
get_changed_files ¶
get_changed_files(old_index: dict[str, dict[str, Any]], new_index: dict[str, dict[str, Any]]) -> list[str]
Return file paths that were added, modified, or removed.
Source code in memory/storage.py
classify_memory_file ¶
Map a changed .memory/ index key to (node_type, file_stem).
Used by the incremental delta path to decide which graph node a changed
file backs. Returns None for anything that is not a graph-backed node
file — config.yaml, archived entries, runtime sidecars, or any file that
is not a direct child of one of the node directories. The returned
file_stem equals the node id for entries/sessions/documents; for skills
(filename is a sanitized name) and the project it is only a locator and the
caller must read the file for the authoritative id.
Keys are workspace-relative POSIX paths (as produced by :func:_index_key),
so the comparison works whether the storage dirs are the historical
relative .memory or an absolute path anchored to ANGELO_WORKSPACE.
Source code in memory/storage.py
load_embeddings_sidecar ¶
Read the embeddings sidecar: {entry_id: [float, ...], ...}.
Source code in memory/storage.py
save_embeddings_sidecar ¶
load_embedding_meta ¶
Read the embedding metadata: {"model": str, "dimension": int}.
Returns {} when absent (legacy repos embedded before meta existed) or
unreadable. Callers must treat a missing/empty meta as "unknown" and fall
back to inspecting the actual stored vector dimension.
Source code in memory/storage.py
save_embedding_meta ¶
Write the embedding metadata under ANGELO_DIR (crash-safe).
Write this only after a successful (re-)embed so a crashed/aborted migration never advertises a dimension the stored vectors don't match.
Source code in memory/storage.py
migrate_from_kgl ¶
Read a KGLite .kgl file and export all nodes to .memory/ files.
Captures every node property present and reconstructs relationships that
legacy (pre-file-storage) graphs stored only as edges, so the subsequent
rebuild-from-files preserves the hierarchy and cross-references. A
timestamped backup of the source .kgl is written before any files are
created (the rewrite is one-way and an upgrading user may have no other
copy).
Returns a stats dict with counts of exported nodes plus the backup path.
Source code in memory/storage.py
892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 | |
verify_migration ¶
Verify migrated files against the source .kgl.
Compares node id sets (not just counts) for entries/skills/sessions/ documents — a mismatch fails the check. Also logs a relationship-preservation summary comparing source edge counts to the relationship fields written to files; relationship differences are warning-only because legacy edge direction and rationale cannot always round-trip exactly.
Source code in memory/storage.py
1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 | |