omnemo 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
omnemo/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Omnemo โ€” memory-only MCP server: save, recall, search, forget."""
2
+
3
+ __version__ = "0.1.0.dev0"
omnemo/cli.py ADDED
@@ -0,0 +1,175 @@
1
+ """CLI: `omnemo serve` runs the MCP server; save/recall/search/forget are
2
+ direct verbs for testing; `omnemo stats` is the stats-only digest source.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import sys
9
+ from datetime import datetime
10
+
11
+ from . import __version__
12
+ from .config import ConfigError, load_config, store_path
13
+ from .embedder import make_embedder
14
+ from .store import CorruptStoreError, EmbedderMismatchError, Store
15
+
16
+ # Anything the store/config layer reports as a clean, named error.
17
+ _KNOWN_ERRORS = (ConfigError, CorruptStoreError, EmbedderMismatchError)
18
+
19
+
20
+ def _open_store() -> Store:
21
+ config = load_config()
22
+ embedder = make_embedder(config.embedder)
23
+ return Store(store_path(), embedder, config)
24
+
25
+
26
+ def _fmt_time(ts: float | None) -> str:
27
+ if ts is None:
28
+ return "never"
29
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ parser = argparse.ArgumentParser(
34
+ prog="omnemo", description="Memory-only MCP server."
35
+ )
36
+ parser.add_argument("--version", action="version", version=__version__)
37
+ sub = parser.add_subparsers(dest="command", required=True)
38
+
39
+ sub.add_parser("serve", help="run the MCP server on stdio")
40
+
41
+ p_save = sub.add_parser("save", help="save one memory")
42
+ p_save.add_argument("text")
43
+ p_save.add_argument("--category", "-c", default=None)
44
+
45
+ p_recall = sub.add_parser("recall", help="recall memories by meaning")
46
+ p_recall.add_argument("query")
47
+ p_recall.add_argument("--limit", "-n", type=int, default=None)
48
+
49
+ p_search = sub.add_parser("search", help="search memories by substring")
50
+ p_search.add_argument("query")
51
+ p_search.add_argument("--limit", "-n", type=int, default=None)
52
+
53
+ p_forget = sub.add_parser("forget", help="permanently delete a memory by id")
54
+ p_forget.add_argument("id", type=int)
55
+
56
+ p_stats = sub.add_parser(
57
+ "stats", help="memory count, learned today, last recall"
58
+ )
59
+ p_stats.add_argument("--json", action="store_true", dest="as_json")
60
+
61
+ sub.add_parser("warm", help="pre-load the embedder (boot warm-up)")
62
+
63
+ sub.add_parser(
64
+ "setup",
65
+ help="install the Omarchy glove: skill, bar plugin, warm-up, connect",
66
+ )
67
+ sub.add_parser("connect", help="register the MCP server with every harness")
68
+ p_disc = sub.add_parser("disconnect", help="unregister from every harness")
69
+ p_disc.add_argument(
70
+ "--all-pieces", action="store_true",
71
+ help="also remove skill links, bar plugin, and warm-up unit",
72
+ )
73
+
74
+ args = parser.parse_args(argv)
75
+
76
+ if args.command in ("setup", "connect", "disconnect"):
77
+ from .omarchy import glove
78
+ from .omarchy.harnesses import connect_all, disconnect_all
79
+
80
+ if args.command == "setup":
81
+ lines, results = glove.setup()
82
+ elif args.command == "connect":
83
+ lines, results = [], connect_all()
84
+ elif getattr(args, "all_pieces", False):
85
+ lines, results = glove.teardown()
86
+ else:
87
+ lines, results = [], disconnect_all()
88
+ for line in lines:
89
+ print(line)
90
+ failed = False
91
+ for r in results:
92
+ mark = "ok " if r.ok else "FAIL"
93
+ detail = f" โ€” {r.detail}" if r.detail else ""
94
+ print(f"[{mark}] {r.harness}: {r.action}{detail}")
95
+ failed = failed or not r.ok
96
+ return 1 if failed else 0
97
+
98
+ if args.command == "warm":
99
+ try:
100
+ config = load_config()
101
+ embedder = make_embedder(config.embedder)
102
+ embedder.embed(["omnemo warm-up"])
103
+ except _KNOWN_ERRORS as e:
104
+ print(f"error: {e}", file=sys.stderr)
105
+ return 1
106
+ print("embedder warm")
107
+ return 0
108
+
109
+ if args.command == "serve":
110
+ from .server import serve
111
+
112
+ try:
113
+ serve()
114
+ except _KNOWN_ERRORS as e:
115
+ print(f"error: {e}", file=sys.stderr)
116
+ return 1
117
+ return 0
118
+
119
+ try:
120
+ store = _open_store()
121
+ except _KNOWN_ERRORS as e:
122
+ print(f"error: {e}", file=sys.stderr)
123
+ return 1
124
+
125
+ try:
126
+ if args.command == "save":
127
+ try:
128
+ memory = store.save(args.text, args.category)
129
+ except ValueError as e:
130
+ print(f"error: {e}", file=sys.stderr)
131
+ return 1
132
+ print(f"saved #{memory.id} [{memory.category}]")
133
+
134
+ elif args.command == "recall":
135
+ results = store.recall(args.query, args.limit)
136
+ if not results:
137
+ print("no memories recalled")
138
+ for r in results:
139
+ print(
140
+ f"#{r.memory.id} [{r.memory.category}] "
141
+ f"score={r.score:.3f} sim={r.similarity:.3f} {r.memory.text}"
142
+ )
143
+
144
+ elif args.command == "search":
145
+ memories = store.search(args.query, args.limit)
146
+ if not memories:
147
+ print("no matches")
148
+ for m in memories:
149
+ print(f"#{m.id} [{m.category}] {m.text}")
150
+
151
+ elif args.command == "forget":
152
+ if store.forget(args.id):
153
+ print(f"forgot #{args.id}")
154
+ else:
155
+ print(f"error: no memory #{args.id}", file=sys.stderr)
156
+ return 1
157
+
158
+ elif args.command == "stats":
159
+ s = store.stats()
160
+ if args.as_json:
161
+ import json
162
+
163
+ print(json.dumps(s))
164
+ else:
165
+ print(f"memories: {s['memory_count']}")
166
+ print(f"learned today: {s['learned_today']}")
167
+ print(f"last recall: {_fmt_time(s['last_recall_at'])}")
168
+ finally:
169
+ store.close()
170
+
171
+ return 0
172
+
173
+
174
+ if __name__ == "__main__":
175
+ sys.exit(main())
omnemo/config.py ADDED
@@ -0,0 +1,208 @@
1
+ """Configuration: exposed defaults, optionally overridden by a TOML file.
2
+
3
+ Every ranking weight, threshold, and category parameter lives here โ€” never
4
+ hard-coded at the point of use โ€” so recall can be tuned on real data
5
+ without touching code.
6
+
7
+ Config file: $XDG_CONFIG_HOME/omnemo/config.toml (default ~/.config/omnemo/).
8
+ Missing file or missing keys fall back to the shipped defaults below.
9
+ Invalid values fail at load time with a ConfigError naming the key โ€”
10
+ hand-edited TOML is the supported tuning surface, so it must never crash
11
+ a verb with a raw traceback later.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import tomllib
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+
22
+ class ConfigError(Exception):
23
+ """config.toml contains an invalid value."""
24
+
25
+
26
+ def config_path() -> Path:
27
+ base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
28
+ return Path(base) / "omnemo" / "config.toml"
29
+
30
+
31
+ def data_dir() -> Path:
32
+ base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
33
+ return Path(base) / "omnemo"
34
+
35
+
36
+ def store_path() -> Path:
37
+ return data_dir() / "store.db"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Category:
42
+ """Per-category ranking parameters.
43
+
44
+ importance: additive weight in the composite recall score (0..1).
45
+ half_life_multiplier: scales the base recency half-life โ€” memories in
46
+ a category with multiplier 2.0 fade half as fast as the base rate.
47
+ """
48
+
49
+ importance: float
50
+ half_life_multiplier: float
51
+
52
+
53
+ DEFAULT_CATEGORIES: dict[str, Category] = {
54
+ "fact": Category(importance=0.6, half_life_multiplier=4.0),
55
+ "decision": Category(importance=1.0, half_life_multiplier=6.0),
56
+ "preference": Category(importance=0.8, half_life_multiplier=8.0),
57
+ "incident": Category(importance=0.9, half_life_multiplier=2.0),
58
+ "transient": Category(importance=0.2, half_life_multiplier=0.25),
59
+ }
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Config:
64
+ # Embedder spec: "fastembed:<model-name>".
65
+ embedder: str = "fastembed:BAAI/bge-small-en-v1.5"
66
+
67
+ # Recall ranking โ€” composite score:
68
+ # score = weight_similarity * cosine_similarity
69
+ # + weight_recency * 0.5 ** (age_days / (base_half_life_days * category.half_life_multiplier))
70
+ # + weight_importance * category.importance
71
+ # + weight_recall_count * min(recall_count, recall_count_cap) / recall_count_cap
72
+ # Candidates below min_similarity are dropped before scoring.
73
+ min_similarity: float = 0.35
74
+ weight_similarity: float = 1.0
75
+ weight_recency: float = 0.25
76
+ weight_importance: float = 0.15
77
+ weight_recall_count: float = 0.10
78
+ base_half_life_days: float = 30.0
79
+ recall_count_cap: int = 10
80
+
81
+ default_category: str = "fact"
82
+ recall_limit: int = 5
83
+ search_limit: int = 20
84
+
85
+ # Parameters used for memories whose saved category is no longer in
86
+ # the config (removed or renamed): recall degrades, never crashes.
87
+ fallback_importance: float = 0.5
88
+ fallback_half_life_multiplier: float = 1.0
89
+
90
+ categories: dict[str, Category] = field(
91
+ default_factory=lambda: dict(DEFAULT_CATEGORIES)
92
+ )
93
+
94
+
95
+ _SCALAR_TYPES: dict[str, type] = {
96
+ "embedder": str,
97
+ "min_similarity": float,
98
+ "weight_similarity": float,
99
+ "weight_recency": float,
100
+ "weight_importance": float,
101
+ "weight_recall_count": float,
102
+ "base_half_life_days": float,
103
+ "recall_count_cap": int,
104
+ "default_category": str,
105
+ "recall_limit": int,
106
+ "search_limit": int,
107
+ "fallback_importance": float,
108
+ "fallback_half_life_multiplier": float,
109
+ }
110
+
111
+
112
+ def _scalar(key: str, value: object) -> object:
113
+ expected = _SCALAR_TYPES[key]
114
+ is_bool = isinstance(value, bool)
115
+ if expected is float and isinstance(value, (int, float)) and not is_bool:
116
+ return float(value)
117
+ if expected is int and isinstance(value, int) and not is_bool:
118
+ return value
119
+ if expected is str and isinstance(value, str):
120
+ return value
121
+ kind = {float: "number", int: "integer", str: "string"}[expected]
122
+ raise ConfigError(f"{key} must be a {kind} (got {value!r})")
123
+
124
+
125
+ def _number(name: str, value: object) -> float:
126
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
127
+ return float(value)
128
+ raise ConfigError(f"{name} must be a number (got {value!r})")
129
+
130
+
131
+ def load_config(path: Path | None = None) -> Config:
132
+ """Load config from TOML, merging over the shipped defaults.
133
+
134
+ Raises ConfigError with the offending key named for any invalid value.
135
+ """
136
+ path = path if path is not None else config_path()
137
+ if not path.exists():
138
+ return Config()
139
+
140
+ with open(path, "rb") as f:
141
+ raw = tomllib.load(f)
142
+
143
+ kwargs: dict = {k: _scalar(k, raw[k]) for k in _SCALAR_TYPES if k in raw}
144
+
145
+ defaults = Config()
146
+ # Base for categories new to this file that omit a field.
147
+ new_base = Category(
148
+ importance=kwargs.get("fallback_importance", defaults.fallback_importance),
149
+ half_life_multiplier=kwargs.get(
150
+ "fallback_half_life_multiplier", defaults.fallback_half_life_multiplier
151
+ ),
152
+ )
153
+
154
+ raw_categories = raw.get("categories", {})
155
+ if not isinstance(raw_categories, dict):
156
+ raise ConfigError(f"categories must be a table (got {raw_categories!r})")
157
+ categories = dict(DEFAULT_CATEGORIES)
158
+ for name, params in raw_categories.items():
159
+ if not isinstance(params, dict):
160
+ raise ConfigError(f"categories.{name} must be a table (got {params!r})")
161
+ base = categories.get(name, new_base)
162
+ categories[name] = Category(
163
+ importance=_number(
164
+ f"categories.{name}.importance",
165
+ params.get("importance", base.importance),
166
+ ),
167
+ half_life_multiplier=_number(
168
+ f"categories.{name}.half_life_multiplier",
169
+ params.get("half_life_multiplier", base.half_life_multiplier),
170
+ ),
171
+ )
172
+ kwargs["categories"] = categories
173
+
174
+ config = Config(**kwargs)
175
+ _validate(config)
176
+ return config
177
+
178
+
179
+ def _validate(config: Config) -> None:
180
+ if config.base_half_life_days <= 0:
181
+ raise ConfigError(
182
+ f"base_half_life_days must be positive (got {config.base_half_life_days})"
183
+ )
184
+ if config.fallback_half_life_multiplier <= 0:
185
+ raise ConfigError(
186
+ "fallback_half_life_multiplier must be positive "
187
+ f"(got {config.fallback_half_life_multiplier})"
188
+ )
189
+ for name, cat in config.categories.items():
190
+ if cat.half_life_multiplier <= 0:
191
+ raise ConfigError(
192
+ f"categories.{name}.half_life_multiplier must be positive "
193
+ f"(got {cat.half_life_multiplier})"
194
+ )
195
+ if config.recall_count_cap < 1:
196
+ raise ConfigError(
197
+ f"recall_count_cap must be >= 1 (got {config.recall_count_cap})"
198
+ )
199
+ if config.recall_limit < 1:
200
+ raise ConfigError(f"recall_limit must be >= 1 (got {config.recall_limit})")
201
+ if config.search_limit < 1:
202
+ raise ConfigError(f"search_limit must be >= 1 (got {config.search_limit})")
203
+ if config.default_category not in config.categories:
204
+ known = ", ".join(sorted(config.categories))
205
+ raise ConfigError(
206
+ f"default_category {config.default_category!r} is not a "
207
+ f"configured category (known: {known})"
208
+ )
omnemo/embedder.py ADDED
@@ -0,0 +1,83 @@
1
+ """Embedders: local only, one per store.
2
+
3
+ The store records which embedder produced its vectors and refuses to open
4
+ under a different one โ€” mixing embedders fragments the vector space and
5
+ silently breaks recall.
6
+
7
+ No provider API keys are ever read from the environment; embedding never
8
+ leaves the machine (fastembed's one-time model download excepted).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Protocol
14
+
15
+
16
+ class Embedder(Protocol):
17
+ """A text embedder. `name` identifies it in store metadata; `dim` is the
18
+ vector length. Both must be stable for the life of a store."""
19
+
20
+ @property
21
+ def name(self) -> str: ...
22
+
23
+ @property
24
+ def dim(self) -> int: ...
25
+
26
+ def warm_up(self) -> None:
27
+ """Load the model and run one embedding so first real use is fast."""
28
+ ...
29
+
30
+ def embed(self, texts: list[str]) -> list[list[float]]: ...
31
+
32
+
33
+ class FastEmbedEmbedder:
34
+ """Local ONNX embedder via the fastembed package.
35
+
36
+ The model is downloaded once to fastembed's cache on first use; after
37
+ that everything runs offline.
38
+ """
39
+
40
+ def __init__(self, model_name: str = "BAAI/bge-small-en-v1.5") -> None:
41
+ self._model_name = model_name
42
+ self._model = None
43
+ self._dim: int | None = None
44
+
45
+ @property
46
+ def name(self) -> str:
47
+ return f"fastembed:{self._model_name}"
48
+
49
+ @property
50
+ def dim(self) -> int:
51
+ if self._dim is None:
52
+ # Known models: read the dimension from fastembed's registry so
53
+ # opening a store (which checks it) never forces a model load.
54
+ from fastembed import TextEmbedding
55
+
56
+ for entry in TextEmbedding.list_supported_models():
57
+ if entry["model"] == self._model_name:
58
+ self._dim = int(entry["dim"])
59
+ break
60
+ else:
61
+ self.warm_up()
62
+ assert self._dim is not None
63
+ return self._dim
64
+
65
+ def warm_up(self) -> None:
66
+ if self._model is None:
67
+ from fastembed import TextEmbedding # deferred: heavy import
68
+
69
+ self._model = TextEmbedding(model_name=self._model_name)
70
+ self._dim = len(next(iter(self._model.embed(["warm up"]))))
71
+
72
+ def embed(self, texts: list[str]) -> list[list[float]]:
73
+ self.warm_up()
74
+ assert self._model is not None
75
+ return [[float(x) for x in vec] for vec in self._model.embed(texts)]
76
+
77
+
78
+ def make_embedder(spec: str) -> Embedder:
79
+ """Build an embedder from a config spec string:
80
+ "fastembed:<model>" โ€” local ONNX model (the default)."""
81
+ if spec.startswith("fastembed:"):
82
+ return FastEmbedEmbedder(spec.removeprefix("fastembed:"))
83
+ raise ValueError(f"unknown embedder spec: {spec!r}")
@@ -0,0 +1 @@
1
+ """The Omarchy glove: harness registration, skill links, bar plugin, warm-up."""
@@ -0,0 +1,78 @@
1
+ import QtQuick
2
+ import Quickshell
3
+ import Quickshell.Io
4
+ import qs.Commons
5
+ import qs.Ui
6
+
7
+ // Omnemo bar widget: memory count and what the machine learned today,
8
+ // read from `omnemo stats --json` once a minute (and on click).
9
+ //
10
+ // Degrades honestly: if the CLI is missing or errors, the widget shows
11
+ // "mem ?" rather than a stale or invented number.
12
+ BarWidget {
13
+ id: root
14
+ moduleName: "omnemo.memory"
15
+
16
+ property int memoryCount: -1
17
+ property int learnedToday: 0
18
+
19
+ readonly property string displayText: memoryCount < 0
20
+ ? "๐‡ ?"
21
+ : (learnedToday > 0
22
+ ? "๐‡ " + memoryCount + " +" + learnedToday
23
+ : "๐‡ " + memoryCount)
24
+
25
+ readonly property int refreshIntervalSec: 60
26
+
27
+ function refresh() {
28
+ if (!statsProcess.running) statsProcess.running = true
29
+ }
30
+
31
+ function applyStats(text) {
32
+ try {
33
+ var s = JSON.parse(text)
34
+ root.memoryCount = s.memory_count
35
+ root.learnedToday = s.learned_today
36
+ } catch (e) {
37
+ root.memoryCount = -1
38
+ }
39
+ }
40
+
41
+ implicitWidth: button.implicitWidth
42
+ implicitHeight: button.implicitHeight
43
+
44
+ Process {
45
+ id: statsProcess
46
+ // pipx puts omnemo in ~/.local/bin; the shell's PATH may not include it.
47
+ command: ["/bin/sh", "-c", "PATH=\"$HOME/.local/bin:$PATH\" exec omnemo stats --json"]
48
+ stdout: StdioCollector {
49
+ onStreamFinished: root.applyStats(text)
50
+ }
51
+ onExited: function(exitCode) {
52
+ if (exitCode !== 0) root.memoryCount = -1
53
+ }
54
+ }
55
+
56
+ Timer {
57
+ interval: root.refreshIntervalSec * 1000
58
+ running: true
59
+ repeat: true
60
+ triggeredOnStart: true
61
+ onTriggered: root.refresh()
62
+ }
63
+
64
+ WidgetButton {
65
+ id: button
66
+ anchors.fill: parent
67
+ bar: root.bar
68
+ text: root.vertical ? "๐‡" : root.displayText
69
+ labelVisible: true
70
+ hasVisualContent: text !== ""
71
+ horizontalMargin: 8.75
72
+ verticalPadding: 8.75
73
+
74
+ onPressed: function(b) {
75
+ root.refresh()
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "omnemo.memory",
4
+ "name": "Omnemo Memory",
5
+ "version": "0.1.0",
6
+ "author": "Omnemo",
7
+ "description": "What your machine remembered: memory count and today's learning",
8
+ "kinds": [
9
+ "bar-widget"
10
+ ],
11
+ "entryPoints": {
12
+ "barWidget": "BarWidget.qml"
13
+ },
14
+ "barWidget": {
15
+ "displayName": "Omnemo Memory",
16
+ "description": "Memory count ยท learned today, from the shared agent memory store",
17
+ "category": "System",
18
+ "allowMultiple": false
19
+ }
20
+ }
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: omnemo
3
+ description: Shared memory for every agent on this machine. Use to RECALL relevant context before starting non-trivial work, and to SAVE decisions, lessons, and durable facts other sessions (or other agents) will need. One store, all harnesses โ€” what Codex saves, Claude recalls.
4
+ ---
5
+
6
+ # Omnemo โ€” the machine's shared memory
7
+
8
+ This box runs one memory store, shared by every agent harness on it. It is
9
+ served over MCP as `omnemo` (tools: `save`, `recall`, `search`, `forget`).
10
+ If the MCP tools are unavailable, the same verbs work from the shell:
11
+ `omnemo save "..."`, `omnemo recall "..."`, `omnemo search "..."`,
12
+ `omnemo forget <id>`, `omnemo stats`.
13
+
14
+ ## When to RECALL
15
+
16
+ - Before starting non-trivial work on this machine โ€” one `recall` with a
17
+ short description of the task. Cheap, often decisive.
18
+ - When the user references past work ("like last time", "the usual way",
19
+ "what we decided about X").
20
+ - Before diagnosing a crash or recurring error: recall the binary or error
21
+ first โ€” this machine may have seen it before and stored the fix.
22
+
23
+ Recall returns ranked matches (similarity + recency + importance). Treat
24
+ results as leads, not gospel: they reflect what was true when saved.
25
+
26
+ ## When to SAVE โ€” conservative by default
27
+
28
+ Save things that would change a future session's answer:
29
+
30
+ - **Decisions with a why** โ€” "chose X over Y because Z".
31
+ - **Lessons paid for** โ€” a root cause found, a fix that worked, a trap hit.
32
+ - **Durable facts about this machine or its people** โ€” preferences the user
33
+ stated, quirks of the hardware, project conventions not written elsewhere.
34
+
35
+ One memory = one fact, in one or two plain sentences, with enough context to
36
+ stand alone months from now. Name concrete things (paths, versions, project
37
+ names) โ€” future recall queries will match on them.
38
+
39
+ ## Do NOT save (this list does the real work)
40
+
41
+ Over-saving is the #1 recall killer: every junk memory competes with a good
42
+ one in every future recall. Skip:
43
+
44
+ - **Transient state** โ€” "the build is currently failing", "server is up",
45
+ today's TODO. It will be false soon, and recalled anyway.
46
+ - **One-off mentions** โ€” a file you touched once, a command's routine output,
47
+ a value you looked up and used.
48
+ - **Anything re-derivable** โ€” code structure, git history, contents of
49
+ config files. The next agent can read those directly; the repo is not
50
+ going anywhere.
51
+ - **Whole transcripts or long dumps** โ€” save the conclusion, not the journey.
52
+ - **Secrets** โ€” never API keys, passwords, tokens, or private credentials.
53
+ If the fact is "where a credential lives", save the location, never the value.
54
+
55
+ Rule of thumb: if you would not write it on a sticky note for a colleague
56
+ taking over tomorrow, do not save it.
57
+
58
+ ## Crash memory
59
+
60
+ When Omarchy hands you a crash to diagnose (`omarchy-agent-crash`), FIRST
61
+ `recall` the crashing binary's name. If a prior crash of the same binary was
62
+ saved, lead with what fixed it last time. After diagnosing a NEW crash to a
63
+ root cause, save one memory: binary, symptom, root cause, fix.
64
+
65
+ ## Forget
66
+
67
+ When the user says a saved fact is wrong or asks to remove it: `search` for
68
+ it, confirm the id, `forget <id>`. Forget deletes โ€” it does not archive.
69
+ Tell the user what was deleted.
@@ -0,0 +1,11 @@
1
+ [Unit]
2
+ Description=Omnemo embedder warm-up (pre-loads the local embedding model)
3
+
4
+ [Service]
5
+ Type=oneshot
6
+ ExecStart=%h/.local/bin/omnemo warm
7
+ # The model download on first ever run can take a while; be patient once.
8
+ TimeoutStartSec=600
9
+
10
+ [Install]
11
+ WantedBy=default.target