OmniMem 7.0.3: v7.0.x into main #40

Merged
ric merged 74 commits from v7.0.x into main 2026-10-21 13:39:13 +00:00
Owner

With this merge, main becomes OmniMem 7: the Rust rewrite, released today as 7.0.3. It's one app instead of the four 6.x containers.

What ships

  • A signed and notarised DMG for Apple silicon Macs.
  • An MSI for Windows x64.
  • Flatpaks and tray-app tarballs for Linux desktops, on amd64 and arm64.
  • .deb, .rpm and tarball server packages with a systemd unit.
  • richarvey/omnimem:7.0.3, which is also latest, on amd64 and arm64.

The full list is in the CHANGELOG under [7.0.3].

What changes for 6.x users

  • Running 6.x stacks aren't affected. They use the richarvey/omnimem-mcp, -rss and -web images, which this doesn't touch.
  • The one-line installer on omnimem.org (curl …/raw/branch/main/install.sh | bash) keeps working. install.sh on main now says 7 is out, then runs the 6.7 installer from v6.7.x, which fetches its compose file from there.
  • To move to 7, take a backup on 6.x and import it (see "Coming from 6.x" in the README).
  • omnimem.org itself is updated separately, after this merge.

Checked

  • Build run 65 and checks run 66 are green on e11e669, the commit tagged v7.0.3. The one commit after it only adds a CLAUDE.md note.
  • Tag build run 67 is green and published everything.
  • The released DMG passes Gatekeeper as "Notarized Developer ID" when downloaded through Safari.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL

With this merge, `main` becomes OmniMem 7: the Rust rewrite, released today as [7.0.3](https://code.squarecows.com/ric/omnimem/releases/tag/v7.0.3). It's one app instead of the four 6.x containers. **What ships** - A signed and notarised DMG for Apple silicon Macs. - An MSI for Windows x64. - Flatpaks and tray-app tarballs for Linux desktops, on amd64 and arm64. - `.deb`, `.rpm` and tarball server packages with a systemd unit. - `richarvey/omnimem:7.0.3`, which is also `latest`, on amd64 and arm64. The full list is in the CHANGELOG under `[7.0.3]`. **What changes for 6.x users** - Running 6.x stacks aren't affected. They use the `richarvey/omnimem-mcp`, `-rss` and `-web` images, which this doesn't touch. - The one-line installer on omnimem.org (`curl …/raw/branch/main/install.sh | bash`) keeps working. `install.sh` on `main` now says 7 is out, then runs the 6.7 installer from `v6.7.x`, which fetches its compose file from there. - To move to 7, take a backup on 6.x and import it (see "Coming from 6.x" in the README). - omnimem.org itself is updated separately, after this merge. **Checked** - Build run 65 and checks run 66 are green on `e11e669`, the commit tagged `v7.0.3`. The one commit after it only adds a CLAUDE.md note. - Tag build run 67 is green and published everything. - The released DMG passes Gatekeeper as "Notarized Developer ID" when downloaded through Safari. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
ric added 74 commits 2026-10-21 13:39:07 +00:00
The original omnimem-v7-change-spec.md isn't in the repo or on the dev
machine. Rebuild it from the decisions recorded in OmniMem when it was
drafted, marked as reconstructed, and settle the value the record left
open: ClusterSummary.provenance_class uses exactly PROVENANCE_CLASSES
(retrieved / concluded / asserted) as shipped in 6.6.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
breakthrough keeps its meaning (what worked this time); the new optional
lesson holds the generalisable claim. extract_lessons uses lesson for the
do-rule and falls back to breakthrough, under the same outcome gate and
weight, so existing memories compile exactly as before and there is no
migration. bless() is unchanged and now promotes a lesson when present.

lesson is added to the episodic return fields and the compiler's pool
projection, and surfaced by recall, recall_detail, get_experience,
explain_memory and the web UI detail page. Agent instructions and docs
ask for a lesson only when something transfers.

Closes #35

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
First phase of the single-binary Rust port (docs/rust-port-plan.md):
SQLite store with in-memory vector search, 6.x backup import, parity
port with the v7 schema from day one, Cargo workspace beside the Python.

- omnimem-core: namespaces and mem:{ns}:{id} keys (6.x shapes round-trip,
  new ids are ULIDs), and the v7 content hash (NFC, LF, trailing
  whitespace and edge blank lines stripped, sha256: prefix)
- omnimem-embed: ONNX Runtime + tokenizers port of onnx_embedding.py.
  Pooling read from the model config, token cap bounded by the position
  table, pinned revision, local dir or HF cache resolution
- reference_vectors.json written by the Python 6.7 engine; the Rust engine
  matches every vector (accents, CJK, emoji, empty, truncation, padded
  batches) within 1e-4 per component, cosine > 0.99999

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
MSI (Windows), DMG (macOS) and Flatpak (Linux), x86_64 and arm64 on each,
running OmniMem with a tray / menu bar icon that opens a native settings
window (tao + wry) over the local web UI. Headless mode stays for Docker.

Decisions recorded: native window rather than a browser tab; Mac and
Windows Forgejo runners for native builds; Apple Developer ID signing and
notarisation, MSI unsigned until a Windows certificate exists. New
omnimem-desktop crate and a phase 9 for desktop and installers, with the
Flatpak's offline ONNX Runtime source flagged to prove early.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Phase 1 of the port, and the rest of phase 0.

omnimem-store: each memory is one SQLite row holding the 6.x field map as
JSON, with state/project/timestamps/content_hash as indexed generated
columns so filters can never drift from the record. Vectors are stored
beside the rows and searched exactly in memory (cosine, key tiebreak).
Non-memory keys (meta, recall logs, topics, caches) live in a kv table
with expiry. v7 identity (origin_id, content_hash, epoch, classification)
is stamped on write.

Import follows restore_from_file (merge, newer wins) then runs the ported
6.x migrations (state, project names, RSS labels, licence and provenance
with their inheritance order), backfills v7 identity and re-embeds.
Expired recall logs are skipped rather than restored forever.

omnimem-embed: downloads a missing model into the shared Hugging Face
cache at the pinned revision, honouring HF_HUB_OFFLINE, HF_ENDPOINT and
HF_TOKEN; implements the core TextEmbedder trait.

omnimem binary: import, export, stats, search, embed.

Verified on a real 6.6.2 production backup (3,500 memories): all 32
query/namespace searches give the identical ordered top-10 to the Python
engine's exact search, and export round-trips every field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
omnimem-engine ports the 6.x recall pipeline and core tool behaviours:
abandoned-approach fast path, exact filtered search, surface / recency /
experience / temporal scoring, reinstate candidates, fact collapse, the
raw-score relevance floor and weak band, recall logging and counters;
lifecycle transitions and topic suppression; dedup and duplicate
clusters; the tier-1 contradiction heuristic; chunking; domain routing;
licence and provenance resolution. Tool results keep 6.x's field order,
and stored fields keep Python's str(float) and json.dumps formatting.
Date parsing replaces dateparser with a regex parser for the forms memory
queries use.

omnimem-mcp serves 20 tools over streamable HTTP at /mcp (rmcp 3.3,
manual ServerHandler so the 6.x docstrings can be used verbatim), with
bearer auth, Host/Origin allowlists from MCP_PUBLIC_URL / OAUTH_BASE_URL,
fail-closed public binds, per-tool metrics and /healthz. `omnimem serve`
runs it. The enrichment queue is now a durable table (store schema v2).

Checked end to end: an HTTP MCP session in the tests, and a real serve
with the ONNX model on the imported production store (recall 65-90 ms on
a debug build). Query expansion is deferred to phase 5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Ports the rest of the everyday MCP tools to the engine and registers them
with 6.x's verbatim descriptions, taking the server from 20 tools to 43:
experience, project context and domains (including the bulk project tools
and compile_project_context), memory_audit, why_did_you_mention,
explain_memory, reindex, set_licence and set_provenance through one lineage
stamp, tier-1 check_contradictions, recent_knowledge and briefing with its
auto-maintenance counter.

migrate_project_domains, carried from phase 1, now runs at startup and
after an import or restore. reindex keeps its report shape but reloads the
in-memory vectors, since there is no separate index left to drift.
explain_memory also reports the v7 identity fields. The briefing's skill
sections and promote_knowledge move to phase 4 with the skill compiler.

Also plans a headless Linux build (deb, rpm and tarball with a systemd
unit, no desktop feature) alongside the desktop installers.

Checked over a real MCP session against the imported production store with
the real model: every new tool answers in 2 to 146 ms on a debug build.
133 workspace tests pass, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Ports the v6 skill compiler to the engine: domain pools, lesson extraction,
clustering and the reinforcement gate, reference and feed-watch rules,
deterministic SKILL.md rendering, rule-change risk summaries and unified
diffs. compile_skill runs the propose-and-accept flow (stale-proposal and
authored-work refusals, export inside SKILL_EXPORT_DIR); find_skills,
get_skill, bless and promote_knowledge complete the tools, taking the MCP
server to 48. The briefing gains its skill sections: suggestions, pending
updates, the time-gated auto scan with its seen-sha gate, and the knowledge
watch. Feed influence is read from the mirrored meta:feed:influence hash,
and skill transfer bundles (export, validate, plan, apply, feed merge) are
in the engine for the web UI.

Rendering and the other pure functions are checked byte for byte against a
golden fixture written by the 6.7.1 Python for the same inputs. On the
imported production store with the real model, opentofu and preferences
recompile unchanged, and wcag-accessibility proposes only seven Feed watch
articles from a feed whose influence was set after it was compiled.
find_skills scores match the Python ONNX engine on the same store.

The generated banner still names Valkey so existing skills don't all
propose a diff; that line changes at cut-over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Adds omnimem-llm, a blocking Anthropic Messages client keeping the SDK
retry behaviour 6.x relied on, behind a LanguageModel trait in
omnimem-core so the engine is tested against a scripted model.

The engine ports fact extraction (6.x prompt, fence stripping, event-date
parsing), the enrichment worker that serve now runs on its own thread
(preference routing, the event-date fallback chain, licence and provenance
inherited from the live source, duplicate facts skipped, batch mode),
query expansion with the qexp: cache and variants scored through the same
path as the query, and contradiction tier 2. With no ANTHROPIC_API_KEY each
degrades as 6.x did. The queue now returns an unreadable payload as null so
the worker drops it instead of stalling.

Also records the decision that 7.0 has no web UI over HTTP: its pages move
into the desktop app's settings panel, served to the window without a
network listener, and the only HTTP surface is /mcp, OAuth and /healthz.
The plan and changelog are updated to match.

171 workspace tests pass, clippy clean. Smoke-tested on the imported store
without a key; a run against the live API is still to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Adds omnimem-rss, the 6.x worker as a thread sharing the engine: feed-rs
parsing, summary and digest modes with the 6.x prompts, refusal phrases
and truncation fallback, the teaser page fetch with its byte cap, the
licence gate before any fetch, project labels, dedup by URL hash, expiry,
and the meta:feed:influence mirror, now written by the engine's
normalise_feed_entry and sync_feed_influences. The scheduler runs a cycle
at start, every RSS_SCHEDULE_HOURS and whenever feeds.yml changes;
`omnimem rss` runs one by hand and --dry-run previews it. Engine setup in
the binary moves into a shared open_engine.

Checked against 6.x on the repository's feeds.yml: Rust and Python
feedparser, through the 6.x ingester's helpers, agree on all 59 articles
across five live feeds, in order, with identical keys, titles, links,
publication times and stripped text. A real cycle with no key stored and
embedded all 59 in 4.9 s on a debug build and the next skipped them all.

185 workspace tests pass, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Pulls the desktop shell forward ahead of the settings panel, because the
platform event loop decides how startup is structured and the panel is
served through the app's window.

omnimem-app holds what the server and the app share: run_services opens
the engine and runs the enrichment worker, RSS scheduler and MCP server,
reporting state as it goes and checking the fail-closed bind rule before
anything opens; plus the per-platform data folder and a single-instance
lock with a show-window request. `omnimem serve` now runs on it, and
ServerConfig::validate carries the bind rule.

omnimem-desktop is the app: tao event loop on the main thread, services on
a background thread, a tray or menu bar icon with status, Settings, Copy
MCP URL, Start at login and Quit, and a wry settings window whose page is
served over omnimem:// with IPC and no network listener. It opens the
window when no tray is available. `omnimem` runs it with no command when
built with the desktop feature; the crate is outside the default members
so host builds need no GTK.

scripts/desktop-check.sh builds it in a Debian trixie container pinned to
Rust 1.96 (ci/desktop/Dockerfile) and passes clippy with warnings denied,
its unit tests, and a smoke test under Xvfb and D-Bus that creates the
tray and window, loads the page and completes an IPC round trip. The
vector decoder moves to as_chunks, which a newer clippy flags.

189 host tests pass, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
The web UI's pages move into the desktop app's settings window. A new
omnimem-settings crate is an axum router never bound to a socket: the
window's omnimem:// handler passes each request to Panel::handle. The 6.x
templates and static files are embedded and rendered with minijinja,
escaping as markupsafe does.

Ported: the dashboard and version check, the memories list with its
filtered views and htmx rows, memory detail with the tag, licence and
provenance forms, lifecycle actions, create with the duplicate check,
semantic search, projects with domain filtering, suggestions, editing and
bulk state changes, and experience with the graveyard. Forms go through
the same engine calls as the MCP tools.

Two things the webview forced, both caught by the container smoke test:
wry's linux-body feature (WebKitGTK otherwise hands the custom scheme
empty POST bodies), and redirects, which WebKitGTK doesn't follow from a
custom scheme, so a form answers with a page that replaces itself with
the target. The smoke test now submits a real form and checks it lands.
Also fixes a lookalike host counting as a panel address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
/skills in the panel: the list with pending proposals, skill detail, the
New Skill modal through the same compile_skill propose-and-write gate
(existing skills refused, recompiles stay on MCP), delete, and transfer
bundles.

Export writes the zip into the Downloads folder without overwriting and
the page says where it went: a webview download is saved silently on
WebKitGTK and its path isn't reported on macOS. Import takes a multipart
upload up to 20 MB, previews under a one-shot 30-minute token and, on
confirm, folds bundled feed influences into feeds.yml and the influence
mirror. The desktop app tells the panel where the reading list lives,
through a feeds_path helper the services share.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Every page the 6.x web UI had is now in the settings panel: duplicates
(with the last auto-maintenance run noted), contradictions, topic
suppressions, telemetry, token overhead, the RSS feed editor and backups.

The feed editor keeps licence classes, drops a pre-filled note when the
class changes, edits skill influence rows, uploads and downloads
feeds.yml, and mirrors every change for the skill compiler. Backups
create, upload up to 100 MB, preview, restore through restore_from_file
(so every migration runs and memories are re-embedded), download and
delete.

Token overhead is measured rather than hardcoded:
omnimem_mcp::context_overhead sizes the instructions, the serialised tool
schemas and the deferred tool names from what the server sends, and the
desktop app hands the numbers to the panel so the settings crate needs no
MCP dependency. Downloads of backups and feeds.yml are saved into the
Downloads folder, as skill exports are, through a shared files module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
The desktop app gets /configuration for what a headless install sets as
environment variables: MCP server, Claude, recall, RSS, skills,
embeddings and panel settings, each with its default and a note when a
real environment variable overrides it. Ordinary settings are written to
omnimem.env in the data folder, validated as a whole first and keeping
hand-written lines. Secrets (ANTHROPIC_API_KEY, MCP_AUTH_TOKEN, HF_TOKEN)
go to the OS keychain through keyring 4.2 and are never shown again.

Every crate now reads settings through omnimem_core::env::var: the
environment first, then an overlay the desktop app installs once at start
from the file and the keychain. Nothing writes to the process
environment, which isn't sound once the keychain has started threads.
omnimem serve never installs the overlay, so headless installs stay
environment-only. Settings apply at the next start.

scripts/desktop-check.sh now caps cargo with CARGO_BUILD_JOBS (default 4):
running beside the host's other services, the container build was being
stopped for low memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
The authorisation server FastMCP provided in 6.x, hand-built on axum:
discovery (RFC 8414, RFC 9728), dynamic client registration, authorise
with S256 PKCE, the login page with its rate limit, token exchange and
refresh with rotation, grace-window replay and an absolute chain cap,
revocation, and the connector icon. /mcp accepts the shared bearer
token, an OAuth access token, or either, and the fail-closed rule
counts OAuth as authentication.

The HTTP surface is checked against a capture of the 6.x provider under
FastMCP 4.0.3 and mcp 2.1.1 (tests/fixtures/oauth_golden.json). Clients,
codes and tokens live in schema 3's oauth tables, codes and tokens keyed
by their SHA-256. The configuration page gains an OAuth group, with the
admin password in the keychain.

Deliberate differences: incomplete OAuth settings refuse to start, the
Origin check accepts the server's own host whatever the scheme, and
revocation doesn't demand a client_secret from a public client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
The Rust binary no longer needs the 6.x Python as a reference, so it goes
from this branch: mcp_server/, web_ui/, rss_worker/, the Compose files,
install.sh, the 6.x scripts, the Python image workflow and the Python CI
jobs (the secret scan stays). 6.x carries on in the v6.x branches. The
sample reading list moves to examples/feeds.yml, and the golden fixture
capture scripts now say they run against a v6.7.x checkout.

README, quick start, configuration, architecture, features, the tool
reference, the settings panel (was web-ui.md), remote access, reverse
proxy, RSS, the skill compiler, the memory specs and every guide are
rewritten for one binary with SQLite, the desktop app and the rebuilt
OAuth. Install steps describe the planned MSI, DMG, Flatpak, deb/rpm and
Docker image, marked as coming with 7.0.0; docker-hub.md becomes
docker.md. CLAUDE.md becomes the Rust development guide, the
claude_config template matches the server's instructions again, and the
plan's storage section matches the schema as built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Lead with what OmniMem 7 is (one program, on the desktop or a server),
the planned installers and the Claude Code config, with a real graveyard
example from the port in place of the 6.x one. Moving from 6.x gets its
own short section near the end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
OAuth: 64 KB bodies, bounded registrations and a capped client table,
per-address rate limits on registration and sign-in, RFC 7636 checks on
PKCE, no fragments or script schemes in redirect URIs, a login page that
names the client and its redirect host, a bounded failed-login limiter,
503 on store errors instead of invalid_token, IPv6 binds.

Settings panel: every inline script and handler moved to static files
behind a Content Security Policy (names inside handlers were a stored
XSS), an escaped redirect page, only web and mail links opened outside,
validated and atomic feeds.yml uploads, no overwriting backups, project
names checked, the delete fallback limited to memories.

Store and bundles: bundle entries read against their declared size, an
allowlist of importable fields, vectorless memories re-embedded by
restore and reindex, create_new backup writes, saturating counters,
stricter keys, finite timestamps.

Engine: validated abandoned-approach names matched on word boundaries,
capped warnings that no longer count towards top_k, destructive query
tools that ignore warnings and gate on raw similarity, writable
namespaces only for lifecycle and experience tools, chunk caps, text
caps, one-line filtering in SKILL.md rendering, a typed tier 2 verdict.

RSS and downloads: no private or loopback hosts unless
RSS_ALLOW_PRIVATE_HOSTS, every redirect hop re-checked, a wall-clock
budget per fetch, capped titles and labels, case-insensitive stripping,
a scheduler that survives a panic, a unique partial file and sha256
check for the model download, a redacted Anthropic config.

rustls 0.23.45 for RUSTSEC-2026-0285.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Self in impls, plain raw strings where hashes weren't needed, method
paths instead of redundant closures, map_or over map().unwrap_or(),
lossless casts, Eq where PartialEq was derived, digit separators in
long literals, and inlined format arguments. No behaviour change: the
322 tests pass unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Store: listings evaluate their conditions over the generated columns and
indexes (MemoryFilter and list_memories) instead of scanning every key
into Rust, projections run as json_extract so content never crosses the
boundary unless asked for, multi-key reads and hash loads are one
IN (...) query per chunk, deletes are chunked and use RETURNING to keep
the vector matrix in step, and the vector search walks the matrix with
chunks_exact, filtering before the dot product and keeping the
sequential sum so distances stay bit-identical to 6.x.

Engine: the briefing, abandoned cache, skill pools, project listings and
audit read only the fields they need through the filtered listing,
embeddings go through one batched path, and capped scans say when the
cap bites.

Everywhere else: OAuth handlers and the panel's file and store routes
run on the blocking pool rather than tokio workers; borrowed arguments
where values weren't consumed; write! instead of format! appends; float
comparisons audited and annotated; the remaining pedantic and nursery
lints resolved by hand, with mul_add refused anywhere a stored number
must match 6.x.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
The brain and wordmark with < > drawn in the site green at the
wordmark's own stroke weight, in docs/images/omnimem-logo.png.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
The alpha is recovered from the artwork's anti-aliasing separately for
the navy and the green, so the edges stay smooth rather than cut out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Claude Code connects, negotiates protocol 2026-07-28, then rejects the
tools/list response and shows the server with zero tools. The only symptom
is a small "tools fetch failed" note, which reads like a user
misconfiguration. 6.x is unaffected, so this went unnoticed.

That revision requires ttlMs and cacheScope on paginated results (SEP-2549).
rmcp models them as Option with skip_serializing_if, so leaving them unset
omits them from the wire entirely, and a strict client rejects the whole
response. ListToolsResult::with_all_items sets both to None, which is what
list_tools was returning. rmcp 3.4.0 does not change this, so a version bump
is not the fix.

Gated on the negotiated version, exactly as rmcp's own #[tool_handler] macro
does it, so peers on older revisions are unaffected. list_tools previously
discarded its context; it needs it to read the version.

Verified against the real client: connected, 48 tools listed, and a remember
call made by Claude Code lands in the store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Every session carried ~13,700 tokens before any work happened: ~9,000 of tool
definitions and ~4,700 of instructions. Measured in a real run, attaching the
server cost ~7,300 tokens per turn after caching, enough to cancel out the
saving from recalling a prior result instead of re-deriving it.

Two changes, neither of which removes information from the wire:

Descriptions no longer repeat the parameter list. The Args: blocks duplicated
inputSchema, which is already sent alongside every tool, so 11,623 bytes of the
19,610 bytes of description text were redundant. Verified first that no
constant carried prose after its Args: block: 0 of 45 did.

Instructions keep the load-bearing guidance and lose the reference material.
The dead-end check, the recall-quality rules, experience recording and the
principles stay. Session-start procedure, skill mechanics, and the long-form
licence and tagging passages move to docs/agent-guide.md, which costs nothing
on the wire. Skills keep a short pointer so agents can still find them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
warn_if_abandoned was a literal lookup. An approach stored as "tollgate-rs"
warned for "tollgate-rs" and "tollgate", but not for "Tollgate RS" or
"tollgate_rs", because mentions() requires the occurrence to be bounded by
non-alphanumeric characters and neither string contains the other that way.

That is the failure that matters most: an agent proposing a crate names it
however it happens to write it, not however a previous session recorded it,
so a hyphen-for-space slip sends it straight back down the dead end the
memory exists to prevent.

Runs of separators now collapse to a single space on both the stored name and
the query. Word boundaries still apply, so a one-character name cannot match
everything: verified that tollgate-rs, tollgate, Tollgate RS, tollgate_rs and
TOLLGATE-RS all warn, while "rate limiting", "throttling library", "governor",
"shardgate" and "e" all stay clear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Recording an abandoned approach made a later session more likely to repeat it,
not less. Measured end to end: an agent asked which crate to use recalled
nothing, fell back on its prior that the popular crate was right, and
recommended the one a previous session had abandoned. The control, with no
memory at all, re-derived the correct answer.

Two causes, both here.

Auto-suppression wrote the approach name into SUPPRESSED_KEY, the list
suppress_topic uses. Those are different intents sharing one mechanism:
suppress_topic means "stop showing me this", where hiding is the point, while
recording a dead end means "warn me if I consider this again", where hiding is
backwards. The memory holds the reason it failed and what worked instead, so
suppressing the name hid the memory that mentions it, breakthrough included.
The write is gone. The warning path already did the real work and is
untouched, as is suppress_topic, which remains the only writer of that set.

compute_experience_weight returned a flat 0.1 for an abandoned outcome, which
multiplies into adjusted_score. A memory at 0.60 raw similarity ranked at
0.06, below almost anything, so even unsuppressed it was effectively
unreachable. Abandonment is now neutral. Effort still never amplifies a
failure: the early return skips the multiplier, so a hard abandonment cannot
outrank a success.

The phase3 test asserted the old contract and now asserts the new one,
including that the suppressed set stays empty. suppressed_topics_drop_out_of_recall
still passes, so the manual path is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
The server logged writes and failures but not successful reads, so from the
outside "recall ran and found nothing" and "recall was never called" looked
identical. Diagnosing why an agent was not using its memory meant inferring
connection counts from rmcp transport lines, which is unreliable and wrong:
create-new-session fires for the HTTP session manager, not per client, so
counting it badly undercounts connections.

Two lines. Every successful tool call logs its name, duration and response
size at the one place all 48 pass through; failures already logged. recall
additionally logs how many results it returned, how many were abandoned
warnings, and the top adjusted score.

Neither line carries arguments, query text or result content. A query or a
memory can hold anything the user typed, and the product tells agents not to
put sensitive data in memory; piping it into logs would undercut that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Two changes aimed at the same failure: an agent that recalled a dead end still
had to go and re-derive the answer, so recalling saved it nothing.

An abandoned_warning said what not to do and stopped there, even when the
breakthrough and lesson sat on the same memory. It now carries what worked
instead, the lesson, and how long ago the approach was abandoned. Nothing is
appended when there is nothing worth appending: created_at is written on every
memory, so the age is gated on being at least a day old rather than on being
present, or every fresh warning would read "abandoned 0 days ago".

Recency was a cliff, not a curve: flat until RECENCY_DECAY_DAYS and only then
stepping down, so a memory written yesterday and one written on the
eighty-ninth day scored identically. It now declines steadily from the day a
memory is written, 0.033 per thirty days, reaching 0.9 at the cliff, where the
original rule continues at its own steeper 0.05 per thirty days to the same 0.3
floor. Everything past the cliff therefore sits 0.1 below its old value; the
alternative was a discontinuity where weight jumped back up to 1.0 before
falling.

Warnings decay too, where before they were pinned at 1.0 regardless of age, so
a recent dead end outranks a stale one. They never fall below WARNING_FLOOR and
state their age once it is worth stating: a graveyard that goes quiet with time
is how a later session walks back into the hole. Whether a two-year-old
abandonment still holds is a judgement for the agent, not for a decay curve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
A warning that says only "not that" leaves the agent to go and find the
answer, which is the work the graveyard exists to save. It now closes with
guidance on how settled the matter is, and that guidance tracks effort_score
rather than being uniform: significant effort reads as settled unless nothing
else works, several attempts as revisit only with a reason, and a brief
attempt as may be worth another look.

Uniform firmness would be the wrong call in both directions. Claiming an
approach is settled when it was barely tried is how the whole graveyard gets
discounted; being tentative about something that cost days invites a session
to repeat it.

Also strips trailing punctuation before joining the clauses, which was
producing "no global state.. Lesson:".

docs/session-start-hook.md records the measured problem behind this: an agent
with the server attached and the answer in the store made zero memory calls
and re-derived it from source instead. A SessionStart hook that injects a
reminder is verified to inject; whether that wording changes the behaviour is
not, and the doc says so. It also documents the PreToolUse deny that does work
and argues against it as a default, since blocking verification makes stale
memory authoritative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
`omnimem hook` answers a Claude Code PreToolUse hook: it reads the tool call
on stdin, and if the call proposes an approach this project already tried and
abandoned it denies it and hands back what was abandoned, why, and what worked
instead.

This exists because recall only helps an agent that asks, and measured across
twenty benchmark runs the agent never asked: with the server attached and the
answer in the store it made zero memory calls, took the crate TODO.md
recommended, and walked into the failure the project had already recorded. The
server instructions tell it to call briefing() at session start; it did not.
Catching the proposal at the point of action is what changed the outcome.

On the dead-end benchmark, ten pairs of runs on one task with the same tools
and the same prompt: every attempt at the abandoned crate was stopped before it
ran (7 of 7), no treated run ever reached a failing test suite against 8 of 10
for the control, and cost fell 27% on the mean with the treated arm cheaper in
all ten pairs. Within the control arm alone, runs that walked into the dead end
cost 27% more than those that avoided it, which is the same figure arrived at
from the other side.

Two rules keep it from making stale memory unfalsifiable. Only tools that
change something are guarded, so Read, Glob and Grep are never blocked and a
Bash command is checked only when it mutates: listing a crate's directory is
investigation, `cargo add` is a proposal. And it fails open on everything, so a
missing or locked store cannot stop work.

It reads the store directly rather than going through the server, and loads no
embedder: the graveyard scan is keyword-based, and a hook runs on every tool
call, so a model load would cost about a second each time for a vector nothing
reads. Measured at about 5ms per call. SQLite is in WAL mode, so reading
alongside a running server is safe.

The refusal text is the sentence recall() already shows, lifted into
recall::warning_text and shared, so the warning an agent sees when it asks and
the warning it sees when it acts cannot drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
`omnimem hook session-start` prints what this project already knows as the
session's opening context; `omnimem hook session-end` writes one memory for
what the session actually did. With the existing PreToolUse guard that is three
points where memory works without the agent choosing to use it, which is the
whole reason these exist: measured across twenty benchmark runs with the server
attached and the answer in the store, the agent made zero memory calls. The
instruction to call briefing() at session start is advice, and advice did not
bind.

session-start injects the briefing itself rather than asking for it. The
graveyard leads, because it is the part that changes what the agent does next.
It does not call briefing(), which counts its calls and periodically runs dedup
and a contradiction scan: both write and both embed, and a hook that fires on
every session, including ones about to be abandoned, has no business advancing
a maintenance schedule or standing between the user and their first message.
Engine::session_briefing is the same aggregate, read-only, and the hook runs in
about 6ms. A project with nothing recorded gets silence, because a heading with
nothing under it teaches an agent to skim past the whole thing.

session-end is a safety net and is written not to damage the store. It records
nothing when the session changed nothing, because a store filled with logs of
sessions that read a file and stopped is a store whose recall is worse. It
records a session once, because SessionEnd can fire on a /clear and again on
exit and a duplicate doubles that session's weight in every later recall. And
it redacts credentials before writing, because a transcript is exactly where a
token surfaces and recall would otherwise hand that token to a later session.
Claude Code does not promise SessionEnd fires on a crash or that the transcript
is flushed when it does, so the transcript is read defensively and the doc says
plainly that an abrupt ending leaves nothing behind.

Only session-end writes, so only it loads the embedder: an unembedded memory is
not searchable. That needs SessionEnd's default 1.5s shared budget raised, which
the documented settings.json entry does.

docs/session-start-hook.md is removed. It proposed a shell script that injected
a reminder to call briefing(), and the measurement that prompted it is now the
argument for injecting the briefing instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
An audit of every doc against the source. Six commits over two days changed
recall scoring, removed auto-suppression, trimmed the tool descriptions and
added the hook feature, and touched exactly one documentation file between
them. This is the gap.

The one that mattered most was not a doc. descriptions.rs and instructions.md
told every connecting agent that abandoning a high-effort approach suppresses
its name automatically. That stopped being true when suppression was removed,
so the text shipped to agents described behaviour the code no longer had. Both
now say what happens instead: the approach comes back as a warning when a later
session proposes it.

The minimum Rust version was 1.94, declared when the workspace was created and
never tested. Measured by bisection: 1.89 and 1.90 fail, 1.91 compiles and
passes all 340 tests. The floor is `Duration::from_hours` and `from_mins`
becoming const-stable in 1.91, so rust-version, the README and the quick start
now say 1.91. Nothing in CI pins a toolchain, so nothing else moves.

Corrected: the abandoned experience weight (0.1, now 1.0) in three places; the
recency curve (flat until day 90, now declining from day one) in two; five
claims that recall scoring is identical to 6.x, which three deliberate changes
have made false; five that the tool descriptions are verbatim 6.x; project
filters documented as taking a list when the Rust tools take one name; a
set_project_context example that would fail on a missing field and two type
errors; four claims that the tray status line shows the MCP address, which sits
behind Copy MCP URL; a tray menu item that does not exist; a reference vector
tolerance of 1e-5 against the test's 1e-4; a settings group list missing OAuth;
fixture capture scripts claimed for three fixtures when two have one.

`omnimem hook` existed with a good page that nothing linked to, including the
guide for connecting Claude Code. It is now in the command tables, the crate
descriptions, the docs index and that guide, and the CHANGELOG records it along
with the four behaviour changes it sits on top of.

Also: hook.rs read OMNIMEM_PROJECT through std::env::var, breaking the rule in
CLAUDE.md that settings are read through omnimem_core::env, which would have
left the desktop app's overlay unable to reach it. Fixed in the code rather
than the rule. The valkey-search warning in the v7 change spec is marked as
6.x-only, since filters are SQL here. One dangling link in a 6.x announcement
now says where the file lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWqtyPdN5MHxqoVM3Sj9xD
Two breaks from 16 September that nothing caught, because nothing builds the
desktop feature routinely:

- 4918459 resolved `db` into `db_path` before the match, moving it, so the
  Desktop arm no longer compiled (E0382). It now clones, since the desktop
  default is the per-platform data folder rather than ./data.
- d97976e made the smoke test's /_panel/* routes opt-in through
  Panel::with_smoke_routes(), but the app kept calling Panel::new(), so
  `omnimem desktop --smoke-test` got 404s and timed out on every platform.

Verified on macOS 27 (arm64): the smoke test passes in about a second, the
first run of the menu bar app on a Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
descriptions.rs and handler.rs hadn't been through rustfmt (1.93 and 1.99
agree). Clippy's result_large_err fails -D warnings on the three
Result<T, Response> helpers that move blocking work off the runtime; the
response is what every caller returns at once, so boxing it buys nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci: build, test and package the macOS app on the Mac runner
Some checks failed
macOS / Test and package (macOS arm64) (push) Failing after 9s
64604f9f55
scripts/macos-package.sh builds the desktop binary for aarch64-apple-darwin,
bundles it as OmniMem.app (com.squarecows.OmniMem, LSUIElement, macOS 13.4+),
puts it in a DMG with an Applications link, and tars the bare binary for
headless use, with a .sha256 for each. It signs with MACOS_SIGN_IDENTITY and
notarises with an App Store Connect key when given them, ad-hoc otherwise.

.forgejo/workflows/macos.yml runs on the `macos` runner for v7.* pushes,
pull requests and tags: format, clippy with and without the desktop feature,
tests, packaging, a non-blocking desktop smoke test, then the artifact
upload, and on a tag (checked against Cargo.toml) the release attachments.
It uses its own rustup under ~/.cache/omnimem-ci so the Mac's Rust is left
alone. Signing switches on with the MACOS_CERTIFICATE secrets.

No Intel build: ort has no prebuilt ONNX Runtime for x86_64-apple-darwin, so
the docs now say Apple silicon only instead of a universal DMG, and macOS
13.4, the minimum its static library was built for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci(macos): fetch a pinned Node for the JavaScript actions
All checks were successful
macOS / Test and package (macOS arm64) (push) Successful in 6m8s
f0572496b7
The Mac runner runs jobs on the host, and the host's PATH has no node, so
actions/checkout and upload-artifact couldn't start (run 33: "Cannot find:
node in PATH"). The job now fetches Node 24.22.0 (LTS) for darwin-arm64 once
into ~/.cache/omnimem-ci, checked against the release's SHASUMS256.txt, and
skips that when the runner has a node of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci(macos): make the desktop smoke test a failing check
All checks were successful
macOS / Test and package (macOS arm64) (push) Successful in 2m7s
ee89b549e9
Run 34 showed the runner sits in a logged-in session: the smoke test opened
the menu bar icon and settings window and passed in under a second. So a
desktop build that can't show its panel now fails the run instead of being
reported and ignored, which is how the 16 September break went unseen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci: point the runner diagnostic at both Linux runners
Some checks failed
macOS / Test and package (macOS arm64) (push) Has been cancelled
51067570d8
The probe targeted `sqcows`, which all three runners now carry. It runs as a
matrix on [linux, amd64] and [linux, arm64] and reports CPUs, memory, disk,
Docker and the tools on PATH, ahead of moving the desktop check into CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci: check the workspace and the desktop app on both Linux runners
Some checks failed
macOS / Test and package (macOS arm64) (push) Failing after 4m51s
Linux / Check (Linux arm64) (push) Successful in 17m54s
Linux / Check (Linux amd64) (push) Successful in 43m4s
67ab245125
linux.yml runs scripts/desktop-check.sh --workspace on [linux, amd64] and
[linux, arm64]: format, clippy and the tests for the whole workspace, then
the desktop crate's clippy, tests, build and the smoke test under Xvfb, all
in the ci/desktop container. The probe showed both runners run jobs on the
host as `runner`, in the docker group, with no Rust, so Docker is all they
need, and the build cache stays in ~/.cache/omnimem-desktop between runs.

desktop-check.sh gains --workspace for that, and --locked on every cargo
call. The macOS job now installs the Rust version ci/desktop/Dockerfile pins
(1.96) instead of stable, so both platforms move together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Rust 1.96 on macOS, the release profile's strip = "symbols" reached
the proc-macro dylibs through build-override, and the crates using them
failed with E0463 "can't find crate for `zerofrom_derive`" (or
`tokio_macros`, whichever compiled first). Reproduced from a clean build
directory, natively and with --target; 1.93 and 1.99 were unaffected, and
debug builds never strip, which is why clippy and the tests passed on 1.96.

Proc macros and build scripts never ship, so [profile.release.build-override]
now sets strip = false. The binary itself is still stripped. Macos CI run 39
failed on this when the job moved to the 1.96 pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both workflows now run on pushes to any branch but main. On a branch push the
macOS job ends by replacing that branch's pre-release, tagged dev-<branch>
(slashes become dashes, and the dev- prefix keeps it clear of the v7.* tag
trigger), with the new DMG and tarball, so the newest build is always on the
releases page while each run keeps its own artifact. Version tags still
attach to their own release.

scripts/forgejo-release.sh does both: find or create the release for a tag,
or with --replace delete the release and its tag and recreate them on
$GITHUB_SHA, then upload the files. Checked against a mock of the Forgejo
API: creating, replacing, attaching to an existing release, and JSON escaping
of the title and body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(desktop): an <o> menu bar icon on macOS
All checks were successful
Linux / Check (Linux arm64) (push) Successful in 1m20s
macOS / Test and package (macOS arm64) (push) Successful in 3m1s
Linux / Check (Linux amd64) (push) Successful in 11m3s
23783b1dd5
The macOS menu bar shows `<o>` from the logo's wordmark: the logo's green
(#63BD85) chevrons either side of a ring, with a green dot in the middle,
drawn in code at 36 pixels for the 18 point bar (27 points wide). It's a
colour image, since a template image would lose the green, so the ring is
Apple's system grey, which reads on light and dark bars and over the
transparent macOS 26+ bar's wallpaper. Linux and Windows trays keep the
square icon. Smoke test passes on macOS 27 with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The count was read once, when the services reported Running, so the menu
said "running, 0 memories" after an import until the app restarted. The
services now count again every 5 seconds, off the async workers, and report
Running again whenever the number moves, which covers agent writes, RSS,
imports and restores alike. `omnimem serve` ignores the reports, so a
headless server logs nothing new.

The loop is serve_and_recount, with a test that it reports at start, again
when the count moves, and not on the ticks in between.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OmniMem.app now has an icon in Finder, Applications and the Dock: the menu
bar's <o> with a light ring and the logo's green, on a navy tile laid out on
Apple's 1024 point grid (an 824 point rounded square with a soft shadow).
It's drawn by the same code as the menu bar icon, which now shares the mark's
geometry with it, and the app-icon example writes it at every size iconutil
wants; macos-package.sh turns that into Contents/Resources/OmniMem.icns and
sets CFBundleIconFile. png is a dev-dependency for the example, already in the
tree. The icon module is public so the example can reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
linux.yml and macos.yml become one build.yml so a single release job can
wait for every build: the linux jobs (amd64 and arm64) and the macos job
each upload their packages as an artifact, and `release`, after `needs`,
downloads them all and publishes them together, replacing dev-<branch> on
a branch push or attaching to a v7.* tag's release. Publishing from each
job would have raced the --replace and deleted each other's files.

scripts/linux-package.sh builds, per architecture, in the ci/desktop
container:
- omnimem-<version>-linux-<arch>.tar.gz, the headless binary;
- omnimem-desktop-<version>-linux-<arch>.tar.gz, the tray app;
each release-built, checked with --help and tarred like the macOS binary
with a .sha256.

The floor is glibc 2.38 (Debian 13, Ubuntu 24.04, Fedora 39), and ort's
prebuilt ONNX Runtime sets it: libonnxruntime.a calls glibc 2.38's
__isoc23_* functions and needs GCC 14's libstdc++, so a Debian 12 build
fails to link (__cxa_call_terminate, _M_replace_cold) and nothing linking
it starts on an older glibc. The headless build links libstdc++ in
(CXXSTDLIB=static=stdc++, with GCC's folder on rustc's search path) so it
needs only glibc, and is run in ubuntu:24.04 before it's packaged; ldd
shows libc, libm and libgcc_s. Found by building it locally under Docker.

Both scripts now make tarballs with COPYFILE_DISABLE=1 and --no-xattrs
(and --no-mac-metadata on macOS): the macOS tarball on dev-v7.0.x carried
._omnimem and ._LICENSE files and Apple xattr headers, hidden by macOS's
own tar and listed by every other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore: version 7.0.1-dev
Some checks failed
build.yml / chore: version 7.0.1-dev (push) Failing after 0s
c68a7587e2
For the v7.0.1-dev pre-release: the build workflow checks a v7.* tag
against this version before it publishes anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(ci): build.yml held three copies of itself
Some checks failed
Build / macOS arm64 (push) Successful in 3m1s
Build / Linux arm64 (push) Failing after 22m29s
Build / Linux amd64 (push) Successful in 24m13s
Build / Publish (push) Has been skipped
da3b4639cc
Forgejo refused run 42 before it started: "mapping key on already defined
at line 9", at lines 237 and 465. Editing the release notes with Ruby's
String#sub put an escaped backtick in the replacement, and in a sub
replacement \` means everything before the match, so the file's opening was
pasted in twice. Ruby's YAML loader keeps the last duplicate key without a
word, which is why the local check passed. Rewritten whole, with the Linux
notes saying glibc 2.38, and checked for duplicate keys this time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(ci): upload artifacts from the AWS runner over IPv4
Some checks failed
Build / Linux arm64 (push) Failing after 44s
Build / macOS arm64 (push) Successful in 2m55s
Build / Linux amd64 (push) Successful in 18m27s
Build / Publish (push) Has been skipped
dfe9bde095
Run 43's arm64 job built, checked and packaged everything, then spent 12
minutes failing to upload: "connect ETIMEDOUT 64:ff9b::6c80:bae3:443". The
runner is behind DNS64, so code.squarecows.com, which has only an A record,
also resolves to a NAT64 address that doesn't get through. git falls back
to IPv4 on its own, so checkout worked; the runner's Node 18 (Debian 12),
which runs upload-artifact, doesn't. The Linux jobs and the publish job set
NODE_OPTIONS=--dns-result-order=ipv4first, and publishing moves to the
amd64 runner, whose upload in the same run went straight through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(settings): a contradiction pair keeps its explanation whichever side has it
Some checks failed
Build / macOS arm64 (push) Successful in 2m58s
Build / Linux arm64 (push) Successful in 4m53s
Build / Linux amd64 (push) Failing after 12m12s
Build / Publish (push) Has been skipped
413480956f
The Contradictions page lists each pair once, from whichever memory comes
first in key order, and dropped the other side's record. When only one side
carries the explanation (a 6.x import, a link made from one side) the page
showed it or not depending on that order, and two memories made in the same
millisecond sort by their ULIDs' random part. That's why
contradictions_list_each_pair_once failed on the arm64 runner in run 44 and
passed everywhere else. The second sighting now fills in an explanation or
similarity the first lacked. A new test sets the explanation on each side
in turn; it fails on the old code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(desktop): the smoke test retries its check when the page reloads
All checks were successful
Build / macOS arm64 (push) Successful in 3m14s
Build / Linux arm64 (push) Successful in 4m48s
Build / Linux amd64 (push) Successful in 19m6s
Build / Publish (push) Successful in 1m38s
591ea4a9a8
While the services start, the panel shows a page that reloads itself every
two seconds, and the smoke test ran its check once, on the first page
load. On the slower amd64 runner (run 45) the reload cut the check off
before it posted anything, so it waited out its 60 seconds and failed while
the page kept reloading. Every load of an ordinary panel page now runs the
check; the /_panel/ pages, the check's own redirect and landing, don't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
Two items under Copy MCP URL set either client up. Each reads the client's
settings first and never adds OmniMem twice:

- an entry reaching this server, under any name, reads "✓ In Claude
  Desktop" (with "restart it to connect" after this run added it);
- an `omnimem` entry pointing at another server, such as a 6.x install,
  is offered as "Switch Claude Desktop here (now <host>)", which repoints
  that entry instead of adding a second;
- a client that isn't installed reads "Claude Desktop not found".

Claude Desktop's config is edited in place: every other key and the
entry's name and position kept, a copy saved as
claude_desktop_config.json.omnimem-backup, the new file renamed into place,
and a file that isn't valid JSON left alone. Claude Code's ~/.claude.json
is only read, since running sessions rewrite it; changes go through
`claude mcp add --scope user` (and `claude mcp remove` to switch), with
`claude` looked for where its installers put it and the usual bin folders
on PATH, as an app started from Finder doesn't get the shell's. The
settings are read again every 30 seconds and after each change, off-thread
for the CLI. An access token, if set, goes in as the Authorization header.

The Claude Desktop guide's examples gain `npx -y`, as the app writes and
working configs use, so npx never waits on an install prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
chore: version 7.0.2-dev
All checks were successful
Build / macOS arm64 (push) Successful in 3m6s
Build / Linux arm64 (push) Successful in 4m40s
Build / Linux amd64 (push) Successful in 18m27s
Build / Publish (push) Successful in 34s
fba4862652
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
gtk 0.18 (already built by tao and tray-icon) for the desktop crate to ask
the session bus whether anything shows tray icons; windows-sys 0.61 (already
in the tree) for the binary to borrow a terminal's console when it's built
as a windowed Windows program. Neither adds a crate to the lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
On stock GNOME, which shows no tray icons without the AppIndicator
extension, libayatana-appindicator still 'creates' the icon, so the app
started with no visible sign of itself. It now asks the session bus whether
org.kde.StatusNotifierWatcher has an owner and, when nothing will show the
icon, opens the settings window with a note on every page naming the
extension to install. Tested in a container with and without a stand-in
watcher on the bus.

Linux and Windows trays draw the <o> too: those hosts fit icons into a
square, where the wide macOS shape would shrink to two thirds, so a compact
square version (icon::tray) keeps most of the height, in the menu bar's
green and grey. The icon module now describes the mark's proportions as a
Shape shared by the menu bar, the tray and the app icon; the old orange
ring is gone, and the window's icon is the app icon.

The panel gains Panel::set_notice, a note rendered at the top of every page
through a template function read at render time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
scripts/linux-package.sh now also builds, per architecture, a .deb and an
.rpm of the headless binary as the omnimem systemd service (packaging/linux):
its own system user, /etc/omnimem/omnimem.env as the whole configuration
(every setting commented, root-only since it can hold keys; systemd reads it
before dropping privileges), /var/lib/omnimem for the database, feeds.yml,
backups and model, and a sandboxed unit. Installing enables and starts it on
127.0.0.1:8765; removing stops it and leaves the data. The server tarball is
now a folder with the same service, the example env and install.sh, for any
other distribution.

Package versions spell -dev as ~dev so a release sorts after its dev builds.
dpkg-deb and rpmbuild run in the ci/desktop container (which gains rpm), by
the script calling itself with --packages; rpm gets its own database there.
Each build installs the .deb on Debian 13, checks the user, the settings
file's mode and that the binary runs as the service user, then removes it.
The .rpm was tried the same way on Fedora. The quick start's server section
now matches what exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
There's no Windows runner, so scripts/windows-package.sh builds the exe on
Linux in the ci/windows container (cargo-xwin for x86_64-pc-windows-msvc,
fetching Microsoft's CRT and SDK into the cache; clang and lld) and wixl
(msitools) makes OmniMem-<version>-x64.msi from packaging/windows/omnimem.wxs:
a per-user install needing no administrator, a Start menu shortcut, major
upgrades over earlier builds, and the app icon (packaging/windows/OmniMem.ico,
written by the app-icon example, which now also writes .ico files).

ONNX Runtime's Windows build imports DirectML.dll and the Visual C++ runtime
(MSVCP140 from its C++), so the MSI ships DirectML beside the exe and stops
with a download link when the runtime is missing; the script fails on any
imported DLL outside its allowlist. With the desktop feature the exe is a
windowed program, so a Start menu launch opens no console, and commands run
from a terminal attach to its console to print. Checked: PE32+ (GUI) x86-64,
the MSI's tables (per-user, no elevation, upgrade codes, the registry search),
and the exe started under Wine (omnimem 7.0.2-dev). There's no smoke test
until a Windows runner exists.

build.yml gains the windows job on [linux, arm64]; publishing waits for it,
and the release notes describe the MSI, the .deb, the .rpm and the server
tarball.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
docs: Linux server packages, the Windows MSI and the Linux tray
Some checks failed
Build / macOS arm64 (push) Successful in 3m14s
Build / Linux arm64 (push) Successful in 5m56s
Build / Windows x64 (cross-built) (push) Failing after 5m44s
Build / Linux amd64 (push) Successful in 25m50s
Build / Publish (push) Has been skipped
817cd0230f
The changelog, the plan's progress and decisions (Windows is cross-built,
x64 only; Linux floor glibc 2.38) and the CLAUDE.md gotchas: the tray host
check, DirectML and the VC++ runtime, the windowed exe, and ~ in package
versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
fix(windows): alias DirectML.lib and PathCch.lib for case-sensitive disks
All checks were successful
Build / macOS arm64 (push) Successful in 3m9s
Build / Linux arm64 (push) Successful in 5m5s
Build / Windows x64 (cross-built) (push) Successful in 1m48s
Build / Linux amd64 (push) Successful in 18m58s
Build / Publish (push) Successful in 1m6s
be4022df70
CI's first Windows build (run 49) failed to link: lld-link couldn't open
DirectML.lib or PathCch.lib. ONNX Runtime asks for them in mixed case, xwin
lays the Windows SDK out in lower case with aliases for most casings but not
those two, and the Linux runners' disks are case sensitive; the local trial
passed only because a Mac's disk isn't. The script now sets the SDK up with
cargo xwin env and adds the two aliases where they're missing. Reproduced and
checked locally with the cache on a case-sensitive APFS image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
feat(linux): the desktop app as a Flatpak, built and smoke-tested in CI
Some checks failed
Build / macOS arm64 (push) Successful in 3m9s
Build / Linux arm64 (push) Successful in 12m17s
Build / Windows x64 (cross-built) (push) Successful in 1m51s
Build / Linux amd64 (push) Has been cancelled
Build / Publish (push) Has been cancelled
6c9c76ad54
scripts/flatpak-package.sh builds omnimem-desktop-<version>-linux-<arch>.flatpak
on the GNOME 51 runtime, with Flathub's libayatana-appindicator module (pinned)
for the tray, in a privileged ci/flatpak container. It then installs the bundle
and runs the desktop smoke test inside its sandbox. Both Linux jobs run it and
the release job publishes the bundles beside the tarballs.

In the sandbox, the tray icon's file goes in $XDG_RUNTIME_DIR/app/$FLATPAK_ID,
the one folder the desktop can read. Start at login writes its autostart entry
with `flatpak run` as the command. Add to Claude Code is disabled, since the
sandbox can't see or run a claude installed outside it.

tray-icon drops its libxdo default, which the GNOME runtime lacks and the tray
menu never used, so the desktop tarball no longer needs libxdo either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
fix(recall): 7.0.3-dev, and the 6.x divergences accounted for
All checks were successful
Build / macOS arm64 (push) Successful in 3m11s
Build / Linux arm64 (push) Successful in 9m8s
Build / Windows x64 (cross-built) (push) Successful in 1m50s
Build / Linux amd64 (push) Successful in 35m38s
Build / Publish (push) Successful in 54s
dbc962204c
Comparing 6.7.1 with 7.0.2-dev turned up four differences in recall:

- Recency: the v7 curve is deliberate, but past RECENCY_DECAY_DAYS the
  multiplier is now 0.1 below 6.x, which nothing said. The floor and the weak
  band read raw similarity, so neither needs retuning. Documented in the
  CHANGELOG, configuration and architecture docs. The CLAUDE.md compatibility
  contract listed recency as kept exactly and now lists every deliberate
  difference.
- Query expansion: same code path as 6.x, and like 6.x it did nothing
  without ANTHROPIC_API_KEY and said nothing, which is what the comparison hit
  (that instance had no key). It now warns once per process, and the panel
  and docs say the key is needed.
- A 6.x memory its own content couldn't find: backups carry no vectors and
  import embeds every memory from its text, so v7 is unaffected. A new test
  imports a project-namespace ULID memory with a junk vector field and finds
  it first by its content.
- Project-namespace hits carry tags; 6.x's return fields never listed them.
  Kept, and documented as a deliberate difference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
The install table lists the real downloads for every platform with what each
needs, the dev builds and where they're published, and the tray's Add to
Claude items. The README now covers the Claude Code hooks, adds the date boost
to the ranking formula and corrects "x86_64 and arm64 everywhere" (macOS is
arm64 only, Windows x64). Issues now point at GitHub, where the repo's tracker
lives; development stays on Squarecows, mirrored to GitHub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
feat(ci): the Docker image, coverage and security scans; model download fixed
Some checks failed
Coverage and security / Test coverage (push) Successful in 7m50s
Coverage and security / Security scans (push) Successful in 8m0s
Build / macOS arm64 (push) Successful in 2m45s
Build / Linux arm64 (push) Successful in 8m55s
Build / Windows x64 (cross-built) (push) Successful in 1m50s
Build / Linux amd64 (push) Successful in 30m6s
Build / Publish (push) Failing after 55s
5bdf3dc919
The server image (richarvey/omnimem) is the Linux .deb installed on Debian 13,
built natively on both Linux runners by scripts/docker-image.sh and tested
each push the way the Docker guide runs it: it refuses to start without a
token, and with one it goes healthy, answers /healthz and an MCP initialize,
and keeps its database in /data, running as uid 1000. A v7 tag pushes each
architecture and Publish joins them under <version>; latest only moves for
a release.

checks.yml runs beside the build: line coverage with cargo llvm-cov (86.4%,
report-only, with a badge on the badges branch), cargo audit and gitleaks,
via scripts/coverage.sh and scripts/security-check.sh.

The scans and the image's first start found real problems:
- The model download refused every LFS file: the sha256 check compared the
  bytes with the CDN's ETag, a Xet content hash, because the hub's
  x-linked-etag was on a redirect reqwest had already followed. Downloads
  now follow redirects by hand, taking the sha256 only from the hub and
  sending the token only there. A fresh machine couldn't start before this.
- The server packages now depend on ca-certificates, which rustls needs to
  verify TLS and slim systems lack. The .deb test installs with apt.
- rustls 0.23.46 for RUSTSEC-2026-0346 to 0349. zip's extract() advisories
  are recorded as unreachable in .cargo/audit.toml until zip 9 settles.
- gitleaks' two findings are a fake JWT in a redaction test and "valkey"
  read as an API key; both are recorded by fingerprint in .gitleaksignore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
The v7.0.3-dev tag pushed both architectures' images, then the release
job's docker manifest create refused the amd64 one: "is a manifest list".
debian00 runs Docker's containerd image store, which pushes each image as
an index (the image plus its provenance attestation); the arm64 runner's
classic store pushes a plain manifest. docker buildx imagetools create
takes either and joins them on the registry without pulling a layer, and
keeps the attestation. Checked against a local registry holding the same
pair: one index with amd64, its attestation and arm64, and no latest for
a -dev version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
feat(macos): Developer ID signing and notarisation switched on
Some checks failed
Build / macOS arm64 (push) Successful in 4m3s
Build / Linux arm64 (push) Successful in 9m47s
Build / Windows x64 (cross-built) (push) Successful in 1m52s
Coverage and security / Test coverage (push) Successful in 58s
Build / Linux amd64 (push) Successful in 37m19s
Coverage and security / Security scans (push) Failing after 33s
Build / Publish (push) Successful in 53s
c28cca0b1d
The signing secrets now exist: a Developer ID Application certificate
from Apple's G2 issuer (valid to 20 November 2027) and a team App Store
Connect key for the notary service. A local dry run with them produced
a DMG Apple accepted and stapled, which Gatekeeper passes as "Notarized
Developer ID", and an app that passes the smoke test under the hardened
runtime with no extra entitlements.

- CI signs by the certificate's SHA-1 rather than its name: a renewal
  shares its predecessor's name, and codesign refuses an ambiguous one.
  It also warns 30 days before the certificate expires.
- The smoke test runs the app from the signed DMG instead of the
  unsigned build, so a signing change that broke it fails the job.
- The tarball's bare binary is notarised too. It can't hold a stapled
  ticket, so Gatekeeper looks it up online.
- notarytool's exit status doesn't say whether Apple accepted a file, so
  the script reads the status, prints Apple's log on a rejection, and
  runs Gatekeeper's own check on the stapled DMG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
fix(deps): rustls-webpki 0.103.16 and webpki-roots 1.0.10
All checks were successful
Build / macOS arm64 (push) Successful in 4m25s
Build / Linux arm64 (push) Successful in 10m54s
Build / Windows x64 (cross-built) (push) Successful in 1m59s
Coverage and security / Test coverage (push) Successful in 1m24s
Build / Linux amd64 (push) Successful in 31m50s
Coverage and security / Security scans (push) Successful in 34s
Build / Publish (push) Successful in 53s
dc7b1564d3
The security scan on c28cca0 failed on three advisories published on 19
and 20 October: RUSTSEC-2026-0382 (rustls-webpki could accept distrusted
intermediates) and RUSTSEC-2026-0381 and 0383 (webpki-roots and
webpki-root-certs still trusted the distrusted emSign intermediates).
Patch updates in Cargo.lock only; the 345 workspace tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
Brings in the benchmarking harness (63685f8), the one commit on main that
v7.0.x lacked, ahead of v7.0.x replacing main for the 7.0.3 release. It runs
against 6.x or 7 over MCP, so it carries over unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
release: OmniMem 7.0.3, the first release of 7
Some checks failed
Build / macOS arm64 (push) Successful in 3m57s
Build / Linux amd64 (push) Failing after 8m35s
Coverage and security / Security scans (push) Successful in 40s
Build / Linux arm64 (push) Successful in 9m36s
Build / Windows x64 (cross-built) (push) Successful in 1m51s
Build / Publish (push) Has been skipped
Coverage and security / Test coverage (push) Successful in 1m6s
ac4d851598
- Version 7.0.3 in the workspace. The CHANGELOG section is dated, and it
  notes that 7.0.0-dev to 7.0.3-dev were development builds.
- install.sh at the root keeps the 6.x one-liner working once main is 7.
  That command (curl .../raw/branch/main/install.sh | bash) is on
  omnimem.org and in every 6.x README. It now says 7 is out with a link to
  the latest release, then runs the 6.7 installer from v6.7.x, which
  fetches its compose file from there. Checked piped through bash: the
  notice, the download, and the 6.7 installer starting normally.
- The docs lose their "Coming with 7.0.0" notes. The README's
  development-builds banner becomes a release note pointing 6.x users at
  "Coming from 6.x", and the Docker guide's dev-tag advice becomes "pin a
  version".
- The port plan marks installers and release (9b) and the cut-over (10)
  done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
fix(desktop): the smoke check survives the starting page's reload
Some checks failed
Build / macOS arm64 (push) Successful in 3m52s
Build / Linux arm64 (push) Successful in 8m42s
Build / Windows x64 (cross-built) (push) Successful in 1m51s
Coverage and security / Test coverage (push) Successful in 50s
Build / Linux amd64 (push) Failing after 12m40s
Coverage and security / Security scans (push) Successful in 51s
Build / Publish (push) Has been skipped
61a5f56d86
Run 61 failed on Linux amd64 only: "POST through the scheme came back as
TypeError: Load failed". The starting page reloads itself every two
seconds, and a reload that lands mid-check fails the check's fetches.
Every page load already reruns the check for that reason, but the catch
reported the aborted run as the verdict straight away. 7.0.3 made the
race likelier: a release version is the first to make /version-check
fetch the latest release over the network, so the starting page's
requests run longer.

A failure is now reported three seconds late. A reload in that time takes
the pending report with it, and the next page load runs the check again.
A failure no reload explains is still reported. The app itself was never
affected: scheme requests are served asynchronously, and the version
check runs on a blocking thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
fix(desktop): quitting with a panel request in flight no longer aborts on Linux
All checks were successful
Coverage and security / Test coverage (push) Successful in 50s
Coverage and security / Security scans (push) Successful in 34s
Build / macOS arm64 (push) Successful in 4m6s
Build / Linux arm64 (push) Successful in 8m45s
Build / Windows x64 (cross-built) (push) Successful in 1m50s
Build / Linux amd64 (push) Successful in 31m8s
Build / Publish (push) Successful in 1m7s
e11e669e4b
Run 63's Linux amd64 smoke test passed, then the process died with
"Aborted (core dumped)" as it exited. arm64 and macOS were clean.

7.0.3 is the first version /version-check treats as a release, so the
first that fetches the latest release over the network. On amd64 that
takes seconds, so the check was still waiting when the app quit. tao
drops the app on exit, the last reference to the panel's tokio runtime
goes with it, and the runtime's shutdown cancels the waiting task. That
drops wry's unanswered responder, and the WebKit request it holds, on a
worker thread after the window has gone. WebKitGTK only allows that on
the main thread, so it aborts.

The panel runtime is now never shut down: one reference is deliberately
leaked, and the process ending stops its threads. The services still
stop first through quit(). Anyone quitting from the tray within seconds
of opening the window could have hit this, so it isn't only a CI fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
The gotcha behind e11e669, so a later tidy-up doesn't restore the
shutdown and bring back the abort on quit on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9cZqempzHD2h4Xyjm3gFL
ric merged commit 6e5df68033 into main 2026-10-21 13:39:13 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ric/omnimem!40
No description provided.