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 +3 -0
- omnemo/cli.py +175 -0
- omnemo/config.py +208 -0
- omnemo/embedder.py +83 -0
- omnemo/omarchy/__init__.py +1 -0
- omnemo/omarchy/assets/plugin/BarWidget.qml +78 -0
- omnemo/omarchy/assets/plugin/manifest.json +20 -0
- omnemo/omarchy/assets/skill/SKILL.md +69 -0
- omnemo/omarchy/assets/systemd/omnemo-warm.service +11 -0
- omnemo/omarchy/glove.py +147 -0
- omnemo/omarchy/harnesses.py +369 -0
- omnemo/server.py +98 -0
- omnemo/store.py +286 -0
- omnemo-0.1.0.dist-info/METADATA +104 -0
- omnemo-0.1.0.dist-info/RECORD +19 -0
- omnemo-0.1.0.dist-info/WHEEL +5 -0
- omnemo-0.1.0.dist-info/entry_points.txt +2 -0
- omnemo-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnemo-0.1.0.dist-info/top_level.txt +1 -0
omnemo/__init__.py
ADDED
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
|