Skip to content

Commit 7d804a9

Browse files
committed
eidetic-memory: ### Added
- **Vendored the `remember` + `recall` memory skills from eidetic-cli** (cite-don't-import) — the write/read halves of eidetic's shared `~/.eidetic/memory` surface, so this agent (Claude and its colleague backend) can persist facts across sessions and recall them later, sharing one store. `remember` drives `eidetic remember` (idempotent upsert of one JSON record or an NDJSON batch on stdin, dedup by id + content hash); `recall` drives `eidetic recall` with four search modes — exact / approximate / keyword / hybrid — each hit carrying text, full provenance metadata, a relevance score, and a freshness signal. The `.sh` wrappers are byte-verbatim from eidetic-cli (their first-party origin); each `SKILL.md` is localized only in the illustrative `--scope <nick>` examples (Provenance keeps "First-party to eidetic-cli"). Both default to this agent's PRIVATE scope, reading the suffix from `culture.yaml`. Runtime dep: the `eidetic` CLI on PATH (else a local eidetic-cli checkout with `uv`). Propagated by rollout-cli's `eidetic-memory` recipe.
1 parent ba64bd2 commit 7d804a9

6 files changed

Lines changed: 599 additions & 1 deletion

File tree

.claude/skills/recall/SKILL.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
name: recall
3+
type: command
4+
description: >
5+
Search the shared eidetic memory store and get back ranked, provenanced
6+
records. Drives `eidetic recall` with four search modes — exact (verbatim
7+
substring), approximate (vector/semantic), keyword (BM25 lexical), and hybrid
8+
(a weighted blend of vector+keyword, the default) — each hit carrying its
9+
text, full metadata, a relevance `score`, and a freshness `signal`. Recall
10+
passively reinforces matched records (bumps last_recall + recall_count).
11+
Shadowed and archived records are excluded by default; use
12+
--include-shadowed / --include-archived to retrieve them. The store lives at
13+
~/.eidetic/memory (a home-dir path outside any git worktree); the wrapper
14+
defaults queries to this agent's PERSONAL, PRIVATE scope (`--scope culture-tools
15+
--visibility private`, suffix read from culture.yaml) — matching where
16+
/remember writes — so a no-flag recall returns this agent's own private records
17+
plus the shared public pool, and Claude and the colleague backend recall each
18+
other's memories because both resolve the same suffix via this skill. Use
19+
when the user says "recall", "what do we know about X", "search memory",
20+
"have we seen X before", "look it up in memory", "eidetic recall", or before
21+
answering from scratch when prior context may already be stored. Pairs with
22+
the sibling /remember skill.
23+
---
24+
25+
# recall — search the shared eidetic memory
26+
27+
`recall` drives **`eidetic recall`**: given a query, it returns the top-k stored
28+
records ranked by relevance, each with its `text`, full `metadata` (provenance),
29+
a numeric `score`, and a freshness `signal`. It is the read half of the memory
30+
surface; the write half is the sibling **/remember** skill.
31+
32+
The point of a *shared* store is that memory is a **team faculty**, not a
33+
per-agent silo: a record Claude wrote is recallable by the colleague backend
34+
(and vice versa), because both resolve the same `~/.eidetic/memory` path.
35+
36+
## How to run
37+
38+
```bash
39+
bash .claude/skills/recall/scripts/recall.sh "<query>" [flags...]
40+
```
41+
42+
The wrapper resolves the CLI portably (installed `eidetic` on `PATH`, else
43+
`uv run eidetic` from the checkout) and forwards every flag verbatim, so it is
44+
exactly `eidetic recall …`. Run it from anywhere; the store is the same.
45+
46+
## Search modes (`--mode`, default `hybrid`)
47+
48+
| Mode | What it matches | Needs embed server? |
49+
|------|-----------------|---------------------|
50+
| `exact` | case-insensitive verbatim substring (`--case-sensitive` to tighten) | no — offline-safe |
51+
| `approximate` | vector cosine / semantic similarity | yes (falls back offline) |
52+
| `keyword` | BM25 lexical; only records sharing a query term | no — offline-safe |
53+
| `hybrid` | `alpha*approximate + (1-alpha)*keyword` (`--alpha`, default 0.5) | uses it when up |
54+
55+
`hybrid` is the default because the two signals cover each other's blind spots:
56+
vector catches paraphrases, keyword catches exact ids/quotes. When the embed
57+
server is unreachable, `hybrid` collapses to keyword-only (it never fuses
58+
meaningless offline-fallback cosine).
59+
60+
## Output fields
61+
62+
Each hit in `--json` output includes:
63+
64+
| Field | Notes |
65+
|-------|-------|
66+
| `id` | stable record identity |
67+
| `text` | the stored chunk |
68+
| `type` | record type |
69+
| `metadata` | full provenance, round-tripped verbatim from ingest |
70+
| `score` | relevance score from the chosen search mode (freshness-blended) |
71+
| `signal` | freshness strength in [0, 1]; computed at recall time from age, recall frequency, and staleness |
72+
| `created` | ISO-8601 ingest date (may be DATE_UNKNOWN for legacy records) |
73+
| `last_recall` | ISO-8601 timestamp of the most recent recall hit (null if never recalled) |
74+
| `recall_count` | number of times this record has been recalled (passive reinforcement counter) |
75+
| `lifecycle` | `active`, `shadowed`, or `archived` |
76+
| `links` | list of related-memory ids |
77+
78+
## Freshness signal
79+
80+
Every `recall` hit carries a `signal` field (float in `[0, 1]`). The signal
81+
blends **multiplicatively** into the lexical/vector score so recently-created
82+
and frequently-recalled records surface ahead of stale ones. The formula:
83+
84+
```
85+
access_bonus = min(0.5, recall_count * 0.05)
86+
age_factor = 1 / (1 + days_since_creation * 0.01)
87+
staleness = days_since_last_recall * 0.01
88+
signal = clamp((0.5 - staleness + access_bonus) * age_factor, 0, 1)
89+
blended_score = score * (1 + 0.25 * (signal - 0.5))
90+
```
91+
92+
Records with no temporal data (legacy, undated) are an exact no-op — the blend
93+
is skipped for them so pre-existing fixture scores are unchanged.
94+
95+
Each `recall` call is also **passive reinforcement**: it bumps `last_recall` and
96+
`recall_count` on every matched record, so frequently-recalled memories organically
97+
gain signal strength over time.
98+
99+
## Lifecycle flags
100+
101+
By default, `recall` returns only `active` records. Use these flags to retrieve
102+
non-active records:
103+
104+
- `--include-shadowed` — include records whose `lifecycle == "shadowed"` (records
105+
superseded within their scope by a newer record). Shadowed records are preserved
106+
and still searchable; they are just hidden from the default result set.
107+
- `--include-archived` — include records whose `lifecycle == "archived"` (records
108+
older than ~1 year or below the signal threshold). Archived records are fully
109+
preserved; the flag makes them retrievable again.
110+
111+
Both flags can be combined. Neither affects ranking — shadowed/archived records
112+
compete on score/signal just like active ones when included.
113+
114+
## Common flags (forwarded to `eidetic recall`)
115+
116+
- `--mode exact|approximate|keyword|hybrid` — default `hybrid`.
117+
- `--top-k N` — max results (default 5).
118+
- `--alpha F` — hybrid blend weight in `[0,1]` (default 0.5).
119+
- `--case-sensitive` — for `--mode exact`.
120+
- `--filter KEY=VALUE` — metadata facet filter (repeatable): e.g. `--filter source=docs`.
121+
- `--scope NAME` / `--visibility public|private` — scope isolation (no private
122+
leak). **The wrapper defaults this to the agent's PERSONAL, PRIVATE scope**
123+
(`--scope culture-tools --visibility private`, suffix read from `culture.yaml`),
124+
matching where `/remember` writes — so a no-flag recall returns this agent's
125+
own private records **plus** the shared public pool, while those private records
126+
stay invisible to a `default`/other-scope recall. Pass `--scope`/`--visibility`
127+
to query elsewhere; a wheel install with no `culture.yaml` falls back to the
128+
CLI default `default`/`public`.
129+
- `--backend files|mongo|neo4j` — default `files` (the shared home-dir store).
130+
- `--include-shadowed` — include shadowed records in results (excluded by default).
131+
- `--include-archived` — include archived records in results (excluded by default).
132+
- `--json` — structured list to stdout (use this when an agent parses the result).
133+
134+
## Examples
135+
136+
```bash
137+
# Default hybrid recall, JSON for an agent to parse:
138+
bash .claude/skills/recall/scripts/recall.sh "jetson nano power draw" --json
139+
140+
# Find the exact message that mentions a phrase:
141+
bash .claude/skills/recall/scripts/recall.sh "Orin Nano" --mode exact
142+
143+
# Keyword search, offline-safe, narrowed to a source:
144+
bash .claude/skills/recall/scripts/recall.sh "thermal throttle" --mode keyword \
145+
--filter source=discord --top-k 10
146+
147+
# Retrieve a record that was recently shadowed (its superseding record is now active):
148+
bash .claude/skills/recall/scripts/recall.sh "old topic" --include-shadowed --json
149+
150+
# Retrieve all records including archived (to audit stale memories):
151+
bash .claude/skills/recall/scripts/recall.sh "power" --include-archived --include-shadowed --json
152+
```
153+
154+
## Notes
155+
156+
- **Provenance is mandatory** on every hit — recall is for *cited* answers.
157+
- The embed endpoint defaults to the local model-gear embed gear
158+
(`http://localhost:8002/v1`, model `Qwen/Qwen3-Embedding-0.6B`); override with
159+
`EIDETIC_EMBED_URL` / `EIDETIC_EMBED_MODEL`. `exact`/`keyword` ignore it.
160+
- **Use the wrapper, not a bare `eidetic`.** The console script may not be on
161+
`PATH` (in a dev checkout it isn't) — the wrapper resolves it for you (`PATH`
162+
first, else `uv run eidetic`). For the docs, run `eidetic explain recall` if
163+
installed, otherwise `uv run --project <eidetic-cli checkout> eidetic explain
164+
recall`. (`explain` is an **`eidetic`** verb — a sibling tool like `devex`
165+
won't know it.)
166+
- **Reading scores:** `exact`, `keyword`, and `hybrid` drop non-matching records
167+
(hybrid drops any record with a `0.0` blended score), so their hits are real
168+
matches. `approximate` keeps every candidate ranked by raw cosine, so it can
169+
return low/near-zero scores when the store is small — lower `--top-k` to trim.
170+
A `--min-score` threshold is a tracked follow-up.
171+
- **Sharing scope = one OS user.** The default store is `~/.eidetic/memory`, so
172+
every agent/process running as the *same* OS user shares it (that is the point —
173+
Claude + colleague). It is not isolated between OS users by anything but file
174+
permissions; keep genuinely private data in a `--visibility private` scope and
175+
treat the host as the trust boundary.
176+
177+
## Provenance
178+
179+
First-party to **eidetic-cli** — eidetic owns its memory surface. Cite, don't
180+
import: downstream repos copy this skill, they don't symlink it. See
181+
[`docs/skill-sources.md`](../../../docs/skill-sources.md).
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env bash
2+
# recall.sh — search the shared eidetic memory store (the /recall skill).
3+
#
4+
# Thin, portable wrapper around `eidetic recall`. It resolves the CLI, points
5+
# the embedding modes at the local model-gear embed gear (overridable), and
6+
# forwards every flag verbatim — so `recall.sh "<query>" --mode hybrid --json`
7+
# is exactly `eidetic recall "<query>" --mode hybrid --json`.
8+
#
9+
# The store is the files backend at ~/.eidetic/memory by default — a home-dir
10+
# path OUTSIDE any git worktree, so Claude and the colleague backend (which runs
11+
# in throwaway worktrees) read the SAME memories. Set EIDETIC_DATA_DIR to opt out
12+
# of sharing; set EIDETIC_MONGO_URI / NEO4J_URI + --backend for a server store.
13+
14+
set -euo pipefail
15+
16+
# ── resolve the eidetic CLI (installed tool first, then dev checkout) ────────
17+
EIDETIC=()
18+
resolve_eidetic() {
19+
if command -v eidetic >/dev/null 2>&1; then
20+
EIDETIC=(eidetic) # installed console script — the normal case
21+
return 0
22+
fi
23+
# Dev fallback: inside the eidetic-cli checkout, run via uv.
24+
local dir
25+
dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
26+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
27+
if [ -f "$dir/pyproject.toml" ] \
28+
&& grep -q '^name = "eidetic-cli"' "$dir/pyproject.toml" 2>/dev/null; then
29+
if command -v uv >/dev/null 2>&1; then
30+
EIDETIC=(uv run --project "$dir" eidetic)
31+
return 0
32+
fi
33+
break
34+
fi
35+
dir=$(dirname "$dir")
36+
done
37+
cat >&2 <<'EOF'
38+
error: eidetic CLI not found.
39+
hint: install it with `uv tool install eidetic-cli` (or `pipx install eidetic-cli`),
40+
or run from inside the eidetic-cli checkout with `uv` available.
41+
The console script is `eidetic` (dist name: eidetic-cli).
42+
EOF
43+
return 1
44+
}
45+
46+
usage() {
47+
cat <<'EOF'
48+
recall.sh — search the shared eidetic memory store (the /recall skill).
49+
50+
Usage:
51+
recall.sh "<query>" [--mode exact|approximate|keyword|hybrid] [--top-k N] \
52+
[--alpha F] [--case-sensitive] [--filter KEY=VALUE]... \
53+
[--backend files|mongo|neo4j] [--scope NAME] [--visibility public|private] \
54+
[--json]
55+
56+
Modes (default: hybrid):
57+
exact case-insensitive verbatim substring (--case-sensitive to tighten); offline-safe
58+
approximate vector cosine / semantic similarity (uses the embed server)
59+
keyword BM25 lexical; only records sharing a query term; offline-safe
60+
hybrid alpha*approximate + (1-alpha)*keyword (--alpha, default 0.5);
61+
degrades to keyword-only when the embed server is offline
62+
63+
Every flag is forwarded verbatim to `eidetic recall`. See `eidetic explain recall`.
64+
EOF
65+
}
66+
67+
case "${1:-}" in
68+
-h | --help | help | "")
69+
usage
70+
exit 0
71+
;;
72+
esac
73+
74+
resolve_eidetic || exit 2
75+
76+
# ── default to this agent's PERSONAL, PRIVATE scope (culture.yaml `suffix`) ──
77+
# Query this agent's OWN personal scope by default, matching where /remember
78+
# writes, instead of the global `default` scope shared by every project on this
79+
# host. We read the `suffix` from the nearest culture.yaml (walking up from this
80+
# script), so the scope follows the repo identity rather than being hard-coded —
81+
# a downstream cite-don't-import copy adapts to its own suffix, and the colleague
82+
# backend (running in a worktree of this same repo) resolves the same suffix,
83+
# keeping the Claude↔colleague shared-memory story intact.
84+
#
85+
# The personal scope is PRIVATE by default to match /remember: in eidetic's model
86+
# a private record is served only to a recall in the SAME scope (`can_serve`), so
87+
# querying with --scope <suffix> --visibility private is what retrieves those
88+
# isolated records (a public/default recall can't see them). Scope and visibility
89+
# are paired — the private default applies only when we inject the resolved scope,
90+
# and only if the caller didn't pass --visibility (so an explicit
91+
# `--visibility public` still wins). An explicit --scope on the command line takes
92+
# over steering entirely; a wheel install with no culture.yaml falls back to the
93+
# plain CLI default (`default`/`public`).
94+
resolve_scope() {
95+
local dir suffix=""
96+
dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
97+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
98+
if [ -f "$dir/culture.yaml" ]; then
99+
# Capture only the first non-space token after `suffix:` (so an
100+
# inline `# comment` or trailing space can't bleed into the scope),
101+
# then strip surrounding quotes only — matching the canonical parser
102+
# in .claude/skills/cicd/scripts/_resolve-nick.sh.
103+
suffix=$(sed -n \
104+
's/^[[:space:]]*-\{0,1\}[[:space:]]*suffix:[[:space:]]*\([^[:space:]]*\).*/\1/p' \
105+
"$dir/culture.yaml" | head -n1 | tr -d "\"'")
106+
break
107+
fi
108+
dir=$(dirname "$dir")
109+
done
110+
printf '%s' "$suffix"
111+
}
112+
113+
has_flag() {
114+
local needle=$1
115+
shift
116+
local a
117+
for a in "$@"; do
118+
case "$a" in
119+
"$needle" | "$needle"=*) return 0 ;;
120+
esac
121+
done
122+
return 1
123+
}
124+
125+
SCOPE_ARGS=()
126+
if ! has_flag --scope "$@"; then
127+
EIDETIC_SCOPE=$(resolve_scope)
128+
if [ -n "$EIDETIC_SCOPE" ]; then
129+
SCOPE_ARGS+=(--scope "$EIDETIC_SCOPE")
130+
has_flag --visibility "$@" || SCOPE_ARGS+=(--visibility private)
131+
fi
132+
fi
133+
134+
# Default the embedding endpoint to the local model-gear embed gear. eidetic
135+
# falls back to a deterministic offline embedding if it's unreachable, so this
136+
# is safe even when the gear is down. Override by exporting these yourself.
137+
: "${EIDETIC_EMBED_URL:=http://localhost:8002/v1}"
138+
: "${EIDETIC_EMBED_MODEL:=Qwen/Qwen3-Embedding-0.6B}"
139+
export EIDETIC_EMBED_URL EIDETIC_EMBED_MODEL
140+
141+
exec "${EIDETIC[@]}" recall "${SCOPE_ARGS[@]}" "$@"

0 commit comments

Comments
 (0)