Segmented, scoped memory for agents: one file, SQLite, no daemon. Wake at session start, recall on every prompt, staleness nag at stop.

Long-term memory for coding agents that knows the difference between who you are, how this repo works, and what happened on Tuesday.
One Python file. SQLite. No server, no daemon, no API key. Works with any agent that can run a shell command. On Claude Code and Codex CLI, hooks make the startup read and per-prompt recall automatic.
An agent forgets everything when the session ends. Bolt-on memory tools fix that by appending every note to one timeline, and two things go wrong:
Both come from the same mistake: storing facts with different lifetimes on one axis. segmem gives each fact a kind (how it decays) and a scope (where it applies), then loads only what the current project needs.
At the start of a session, in the segmem repo:
## Memory (project: segmem)
### Identity
#3 prefers short commits, one concern each (global)
### Procedural
#7 prefers pnpm (global) OVERRIDDEN by #12 uses npm, Lambda runtime needs it (segmem)
#9 tests: python3 test_segmem.py (segmem)
### People known: alice
Run `segmem recall <name>` before acting on or about them.
### Episodic (segmem), oldest first
#0-3 2026-08-21 chose SQLite over DuckDB; stdlib, WAL, FTS5
#4 2026-08-21 naps come due only for blocks the wake cover prints
#5 2026-08-22 wake runs from a SessionStart hook; the agent skipped it once
You are awake.
Global preferences load whole. The project's conventions load whole and win over global ones, with both shown so the agent knows why. History loads with detail that decays: yesterday verbatim, last month as one line. People load as names only; the agent looks them up when they come up.
Requires Python 3.8 or later and git. Nothing else.
/plugin marketplace add mahuebel/segmem
/plugin install segmem@segmem
That's the whole install: the three hooks register automatically, and the doctrine (what to record and when) is injected at session start, so there is no CLAUDE.md paste and no settings.json merge. Skip the manual steps below. The hooks they print claim each session with --once, so running both no longer doubles wake and recall, but it still starts a second process per event.
git clone https://github.com/mahuebel/segmem ~/.segmem/src
~/.segmem/src/segmem init
init prints two things:
## Memory block. Paste it into ~/.claude/CLAUDE.md (or your agent's AGENTS.md). It tells the agent what to record and when.hooks block. Merge it into ~/.claude/settings.json (Claude Code) or save it as ~/.codex/hooks.json (Codex CLI); both use the same shape. It makes the harness run wake at every session start and search memory on every prompt, so neither depends on the agent remembering.On a harness without hooks (Cursor, Aider, your own), skip step 2. The prompt block alone carries it: the agent runs wake and recall itself. That works, but it relies on the agent following instructions. If your harness can run a command at session start or pipe each prompt to a command, point it at segmem wake and segmem hook; hook accepts the JSON that Claude Code and Codex send, or plain text, on stdin.
Start a new session. The first wake prints an empty header; the agent fills it in as you work.
| Kind | Holds | At wake | Replaced by newer facts? |
|---|---|---|---|
identity | who you are, how you like to work: "prefers" | loaded whole | yes |
procedural | how this project works and why: "uses" | loaded whole | yes |
episodic | decisions with reasons, root causes, handoffs | decaying window | no, it's history |
people | who someone is | names only | yes |
The word choice is the classification. Prefers is about you and goes global. Uses is about a repo and stays there.
--entities=a,b tags a fact with its subjects: people, components, files. Tags are one vocabulary across projects. When you write one that matches an existing tag ignoring case, the stored spelling wins; when it's new but close to an existing one, note says so (new tag: github_actions (similar: github-actions)), and you use the suggestion next time. A tag that looks like a name with no people record gets a nudge to create one. recall prints tags in brackets so you can see what's in use.
Tags earn their keep in three places: they pair project facts with the global facts they override, they let promote match the same fact across projects, and they're what makes a capitalized word in a prompt count as something worth looking up.
A fact is global or belongs to one project, keyed by the git main repo path so every worktree shares it. identity and people default to global; procedural and episodic default to the current project.
Project wins. When a project fact and a global fact share a subject (an entity tag), wake prints OVERRIDDEN with both.
Promotion needs evidence. A project fact becomes global only when the same statement is live in three projects. One observation is a convention; three is a preference.
Memory is a supply chain, not an archive; ARCHITECTURE.md holds the full design. A fact lives at the narrowest altitude whose audience covers everyone who needs it: the device (this store), the project repo (where the export verdict sends stable, hot facts, strongest form first: enforcement, a skill, CLAUDE.md, then docs), and an org layer for the cross-project residue.
The org layer is a cloned knowledge repo of one-fact-per-file markdown. Point SEGMEM_ORG_DIR at it and segmem indexes it read-only, reindexing when its git HEAD moves. recall and the prompt hook search it, hits marked (org). Wake never loads it whole; it surfaces three things only: one summary line, conflicts (a local fact overriding an org fact, which is the org layer's staleness signal: open an issue on the fact's file), and co-sign nudges when a local fact matches an open candidate.
Upward writes are always a PR a human approves. segmem contribute <id> prints the candidate file and the commands; facts tagged with a person, and every identity, people, or raw episodic note, never leave the device. segmem org-init <dir> scaffolds a new knowledge repo with the witnessing convention: candidates merge to facts at three witnesses, and CODEOWNERS names the human on the other end of every staleness signal.
A stored claim is a claim under test, and the store tracks the evidence arriving against it. Every time an entity is tagged in a new note (weight 3), served by recall (2), or mentioned in a prompt (1), that's a touch. Touches are telemetry, not memory: wake never prints them as facts. Each touch carries the project it came from: a project fact feels only its own project's attention, a global fact feels all of it.
A session nobody is watching (a builder fleet, a scheduled run) should read memory without pressing on it. Set SEGMEM_QUIET=1 in its environment: the prompt hook still recalls but records no touches and asks for no upkeep.
People dossiers. Only a people note or an explicit review resets the clock; an episodic note about a person raises pressure on their dossier, it never relieves it. When the weighted touches since the last revision reach the threshold (6):
segmem stale lists the dossiers under pressure, with counts and dates.wake flags them under the people list: alice: dossier from 2026-08-24, 3 notes since.segmem touch <name>.Procedural facts. The same clock runs per note, against the touches on its entities, with a higher threshold (12), because busy components accrue touches fast. The first time a note comes under pressure, the verdict is verify: check the claim against the repo, then supersede what changed or confirm with segmem touch <id>, which prints the exact claim back so a blind reset is at least a visible one.
A confirmed fact that comes under pressure again has proven two things: it's stable, and it's load-bearing. The verdict changes to export: its home is the repo. Write it into README, CLAUDE.md, or the file it governs (~/.claude/CLAUDE.md for a global fact), then supersede the note with a pointer to where it landed. Memory is the staging ground, not the archive; a fact everyone should see belongs where everyone looks. segmem touch <id> --keep is the escape for a fact the repo can't hold (private context, another team's repo); it stops export suggestions while verify cycles continue. segmem never writes the repo itself: the agent does, and decides.
touch is the honest way out: it records "reviewed, no change needed" without writing a fake supersede that would pollute history. Nothing ever rewrites a note without an agent deciding to.
| Command | What it does | |
|---|---|---|
segmem wake [--all] [--conflicts] | print the memory for the current project; --conflicts prints the overriding id pairs alone | |
| `segmem note <kind> "<text>" [--entities=a,b] [--scope=global\ | project] [--supersedes=id]` | record one fact, up to 280 bytes; superseding an episodic leaf drops the summaries over it, and the next nap rebuilds them |
segmem recall <query> | full-text search across every kind and scope | |
segmem nap <lo>-<hi> "<text>" | answer a compression request; the range must be the pending block | |
segmem promote <id> | lift a project fact to global, if three projects agree | |
segmem forget <id> ["why"] | delete a note and record the rejection: the same line is refused after; episodic only when newest | |
segmem forget <lo>-<hi> | drop a bad summary; it's rebuilt on request | |
segmem stale [--min=n] [--hook] [--count] | list people notes and procedural facts under evidence pressure; --count prints the number alone | |
| `segmem touch <entity\ | id> [--keep]` | claim reviewed, unchanged; resets its pressure; --keep marks a procedural fact memory-resident |
segmem contribute <id> | print the org-repo candidate for a procedural fact, and the PR commands | |
segmem org-init <dir> | scaffold a knowledge repo with the witnessing convention | |
| `segmem hook [--once --session=id --served=command\ | function]` | the prompt hook; reads JSON on stdin |
segmem hook --tool [...] | the PreToolUse hook: facts tagged with the program a Bash command runs, once per session | |
segmem check-note | read a shell command on stdin; run the note checks on it and write nothing | |
segmem serve [--port=7878] [--no-open] | serve a live page over the store on loopback; Ctrl-C stops it | |
segmem html [file] [--no-open] | write a self-contained snapshot page of the store, and open it | |
segmem mcp | run as an MCP server over stdio | |
segmem prompt [--subagent] | print the doctrine block; --subagent prints the read-only paragraph a subagent gets | |
segmem next-nap [--json] | print the compression wake would ask for; --json for its range and prompt as data | |
segmem audit | the numbers a store review reads: kinds, wake cost per project, pressure with its sources, session bursts, duplicates, untagged facts | |
segmem project | print the scope key for the current directory |
Examples:
segmem note identity "prefers rebase over merge" --entities=git
segmem note procedural "uses npm, the Lambda runtime needs it" --entities=pkg
segmem note episodic "chose SQLite over DuckDB: stdlib, no install" --entities=sqlite
segmem note people "Alice owns deploys, ask before touching infra" --entities=alice
segmem note identity "lives in Lisbon" --supersedes=14
segmem recall lambda
Episodic facts form a binary tree. Two adjacent facts compress into one line, two of those into another, and so on. wake prints a fixed budget of lines (16 per project, 8 for global), chosen greedily: a block may be as wide as it is old, so the newest facts appear verbatim and each doubling of age gets about the same number of lines. Picking the cover takes one pass and well under a millisecond at a million memories.
The agent writes each summary itself. When wake needs a summary that doesn't exist, it prints the block raw, never blocks, and asks for one line; the agent answers with nap. If the turn reads past the ask, the prompt hook repeats it once per session. Compression is requested only when wake would print the block, never ahead of time, and never in the background.
Raw facts are never edited. A misfiled note can be forgotten by id; anything else is superseded, not deleted. Summaries are a cache: drop one with forget <lo>-<hi> and the next request rebuilds it.
The init output includes this block. For Claude Code, merge it into ~/.claude/settings.json; for Codex CLI, save it as ~/.codex/hooks.json:
{"hooks": {
"SessionStart": [{"hooks": [{"type": "command", "command": "~/.segmem/src/segmem wake --once --served=manual"}]}],
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "~/.segmem/src/segmem hook --once --served=manual"}]}]
}}
SessionStart runs wake on every start, resume, and compaction and puts the output in context. A startup rule the agent has to remember is a rule it will sometimes skip; this removes the dependency.UserPromptSubmit runs hook, which pulls identifiers out of your message (code spans, #123, paths, snake and kebab names, and capitalized words that are known tags), searches the current project plus global memory, and adds up to eight hits as a <segmem-recall> block. No identifiers or no hits means no output. An identifier that matches more than a sixth of the store (wake, in this repo) is too broad to mean anything and is dropped, so a prompt made only of such words stays quiet. It never blocks a prompt.<segmem-upkeep> block when a people note or a procedural fact has fallen behind the evidence: one subject per prompt, each once per session, done before the answer and not mentioned in it. With nothing under pressure, it asks once per session for the compression wake printed raw. It used to ask from a Stop hook, which put the upkeep after the answer, so the answer scrolled away. stale --hook still works for installs that wire it, but init no longer prints it.Claude Code 2.1.261 carries an early-access hook type behind CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1: a TypeScript module the engine runs in-process, with typed events instead of stdin JSON. The plugin ships one, hooks/hooks.ts. It answers prompt.context with the wake output as a context block named segmem, and prompt.submit with the recall hits as typed context beside the prompt. The command hooks stay in hooks.json, so a session without the flag (Desktop, cloud, Codex, an older build) loses nothing.
With the flag on, both paths run and neither knows the other's order, so each output path calls the same command with --once: wake --once for the memory at start, hook --once for the recall on every prompt. The first to insert a row in the claims table owns it, and only the owner prints. The key is the session, the command, and the source: startup for wake, so a compaction is a new source and wakes again, and session for recall, so the first prompt decides who serves every prompt after it. Without a session id, --once does nothing. The served column records which path spoke, so a transcript that looks wrong can be traced.
The recall path takes every prompt the command hook takes, whatever its origin: a scheduled prompt, a peer session's message and one you typed all get the same memories.
At agent.spawn the module appends the read-only rule to the subagent's prompt: read with wake and recall, never note, nap, or promote, and report what you learned so the parent records it. It never refuses a spawn. The rule's text comes from segmem prompt --subagent, so it is written once, in Python. Without the flag the parent is still told to pass the rule on, which is the line that has always been in the doctrine block. This path needs no claim: it writes the subagent's prompt, which no command hook can reach, so there is nothing to double.
At tool.call the module checks a Bash command that mentions segmem before the shell runs it. segmem check-note reads the whole command on stdin, finds the note arguments, and runs the same length and tag checks the real note runs without writing anything: a line over 280 bytes or a wrong kind is refused with the trim mark, and a near-miss tag or a missing people record comes back as a hint beside the result. Every other command passes untouched. Without the flag the model learns the same thing from the note that failed; with it, the note is never sent.
Also at tool.call, the module runs segmem hook --tool on every Bash command: a fact tagged with the program about to run (sqlite, git, a tag that is a prefix of the program name) prints before the shell runs it, once per session. Only tags match, never text, so git does not print half the store. The command hook in hooks.json does the same as a PreToolUse hook and answers as JSON, because plain stdout there reaches only the debug log. The two paths share the prompt hook's claim, so one of them speaks.
After wake, the module counts what is under pressure with segmem stale --count and pins segmem: N under pressure beneath the prompt, or clears the line when nothing is due. It is a notice, not a prompt: the prompt hook's <segmem-upkeep> block is what asks the model to verify a fact against the repo. A headless run has nowhere to draw it, and the engine says so in the debug log rather than failing.
At session.start the module registers one tool, recall, which the model calls as mcp__segmem__recall with a query. It runs segmem recall and returns what it printed, so a call presses the entities it finds exactly as a Bash recall does. Bash recall keeps working in every session, flagged or not. note is not registered: it stays a shell command, which is what keeps "subagents never note" true without anything extra to enforce it.
The module makes no model call. Version 0.8.0 drafted the pending compression with the session's model at start; that cost one completion on the first-prompt path and, in the one project with a backlog, never once turned into a nap. The prompt hook asks for the compression instead, and the plugin still never runs nap: a summary that turns an unknown cause into a cause is worse than no summary, and only the model reading the originals can tell the two apart.
When a project fact overrides a global one, or a local fact overrides an org fact, the module toasts the pair of ids: segmem: #12 overrides #7, or segmem: #9 overrides org:pkg-standard. A contradiction is the point of the altitudes and the easiest thing to miss inside a long wake, so the user sees one without reading it. segmem wake --conflicts answers with the id pairs alone and takes no claim: showing them is not serving the session's memory, and the command hook owns that.
A compaction is served by the command hook, not the module. prompt.context fires only when a conversation computes the context its first message carries, and a compaction is not a new conversation: it does not fire again, so the typed blocks go with the context that was compacted. What does fire is SessionStart with source compact, which runs wake --once. Because the claim is keyed on the source, a startup claim held by the module does not silence it, and memory comes back.
To type-check the module after a Claude Code update, run /plugin-types in a session (it writes .claude/types/), then tsc -p tsconfig.json. claude plugin validate .claude-plugin/plugin.json shows what the engine reads from the module.
segmem serve
opens a live page at http://127.0.0.1:7878/. It runs in the foreground while you look and stops on Ctrl-C; nothing in the hooks depends on it, so the no-daemon promise holds. Stdlib only, loopback only, read-only: every route is a SELECT, and searching from the page never writes a touch, so browsing doesn't press on a fact. A spine across the top shows every memory as one tick in id order, colored by kind, superseded ones hollow. Seven tabs, reachable with g then a letter (o c e h t n l); / jumps to search:
stale would give, the export target, its supersede history, and the commands to copy. Episodic lines show the wake seq in the gutter and the global id at the right, since wake numbers by seq and recall by id.recall, facets by kind, scope, and entity, an expandable row per hit, and a live feed of touches and notes as they land.#123, path-like words, capitalized words that are already tags), which they drop and why, the FTS5 query, and the exact <segmem-recall> block the agent would get. It runs identifiers() and the hook's query in-process without the touch, so trying phrasings presses on nothing.nap would ask for first dashed. Scrub the budget and the stream length to see what wake looks like at 200 or 1,000 notes and where compressions land.The page re-renders whenever any other connection commits: it holds one event stream and the server polls PRAGMA data_version twice a second. Where an action is implied it offers a command to copy; the CLI stays the only path that writes.
segmem html
writes one self-contained HTML file (no libraries, no server, no network) to ~/.segmem/segmem.html and opens it: a snapshot you can send to someone. Five views:
hooks/hooks.ts 236 lines1// segmem as a function hook: wake and recall as typed results, no stdout
2// parsing. Loads only when CLAUDE_CODE_ENABLE_FUNCTION_HOOKS is on (or the
3// engine's flag is). The command hooks in hooks.json keep running either
4// way; every path here calls the same command with `--once`, and the claims
5// table lets exactly one of the two speak.
6//
7// Wake runs in prompt.context, not session.start: prompt.context fires first.
8//
9// prompt.context is dispatched more than once and the dispatches OVERLAP;
10// the engine keeps whichever answer settles first. So the once-guard caches
11// the promise, not the value: a guard that sets its sentinel before the
12// await lets the second dispatch through with nothing, and that empty
13// answer, being the fast one, is the one the engine keeps.
14//
15// Ordering. The UserPromptSubmit command hook runs *inside* next(e), so a
16// hook that waited for next(e) before claiming could never win. Each hook
17// below claims first and holds its output before it calls next, so a claim
18// it wins is a claim it can deliver. On wake the race is lost anyway: the
19// SessionStart command hook and this module start at the same moment and
20// the command hook's process reaches the insert first, every time measured.
21// That is fine, because both paths print the same wake. Anything only this
22// module can produce must therefore not be gated on winning: the status line
23// and the toast take no claim.
24//
25// The module makes no model call. A nap draft lived here from S6 to 0.8.0:
26// it cost one session-model completion on the first-prompt path and, in the
27// one project with a compression backlog, never once converted. The prompt
28// hook now asks for the nap, one subject per prompt, before the work: at
29// Stop the ask landed after the answer and buried it.
30import type { EngineInterface, Register } from "claude-code";
31
32const TIMEOUT = 10_000;
33
34/**
35 * Wake, the pressure count, and the contradiction toast, once per module
36 * life. The three runs are independent, so they run at once: the first
37 * prompt waits on the slowest, not the sum.
38 *
39 * Wake is claimed with `--once`, so it prints only if this path owns the
40 * session. The other two are not behind that claim: the two paths race on
41 * it and the command hook wins, so anything gated on winning would never
42 * ship. Neither doubles anything a command hook prints.
43 */
44async function gather($: EngineInterface): Promise<string> {
45 const root = $.plugin.root + "/segmem";
46 const cwd = await $.session.cwd();
47 const id = await $.session.id();
48 const run = (args: string[]) =>
49 $.process.run([root, ...args], { cwd, timeoutMs: TIMEOUT });
50 const [wake, stale, conflicts] = await Promise.allSettled([
51 run(["wake", "--once", "--session=" + id, "--served=function"]),
52 run(["stale", "--count"]),
53 run(["wake", "--conflicts"]),
54 ]);
55
56 let out = "";
57 if (wake.status === "fulfilled" && wake.value.exitCode === 0) out = wake.value.stdout.trim();
58 else $.ui.log("segmem wake failed: " + (wake.status === "fulfilled"
59 ? wake.value.stderr.trim() : String(wake.reason)));
60
61 // What the prompt hook will ask the model to review, as a line the user
62 // can see. The <segmem-upkeep> block stays the model's trigger; this is a
63 // notice.
64 if (stale.status === "fulfilled" && stale.value.exitCode === 0) {
65 const n = Number(stale.value.stdout.trim());
66 $.ui.status(n > 0 ? "segmem: " + n + " under pressure" : undefined);
67 } else $.ui.log("segmem stale failed: " + (stale.status === "fulfilled"
68 ? stale.value.stderr.trim() : String(stale.reason)));
69
70 // A contradiction is the whole point of the altitudes, and it is easy to
71 // miss inside a long wake. Toast the ids so the user sees one without
72 // reading it. `wake --conflicts` answers as data and takes no claim: the
73 // command hook owns wake's text, and this is not serving that text.
74 if (conflicts.status === "fulfilled" && conflicts.value.exitCode === 0) {
75 const ids = conflicts.value.stdout.trim().split("\n").filter(Boolean);
76 if (ids.length) $.ui.toast("segmem: " + ids.join("; "), { timeoutMs: 8000 });
77 } else $.ui.log("segmem wake --conflicts failed: " + (conflicts.status === "fulfilled"
78 ? conflicts.value.stderr.trim() : String(conflicts.reason)));
79 return out;
80}
81
82export const register: Register = (on) => {
83 let once: Promise<string> | undefined;
84
85 on("prompt.context", async ($, e, next) => {
86 let wake: string;
87 try {
88 once ??= gather($);
89 wake = await once;
90 } catch (err) {
91 $.ui.log("segmem gather failed: " + String(err));
92 return next(e);
93 }
94 if (!wake) return next(e);
95 return next({ ...e, blocks: [...e.blocks, { name: "segmem", text: wake }] });
96 });
97
98 // Recall on every prompt, as the UserPromptSubmit command hook does. No
99 // origin filtering: parity with that hook is the default, so a scheduled
100 // or peer prompt gets the same memories a typed one does.
101 on("prompt.submit", async ($, e, next) => {
102 let found = "";
103 try {
104 const id = await $.session.id();
105 const r = await $.process.run(
106 [$.plugin.root + "/segmem", "hook", "--once",
107 "--session=" + id, "--served=function"],
108 {
109 cwd: await $.session.cwd(),
110 timeoutMs: TIMEOUT,
111 stdin: JSON.stringify({ prompt: e.text, session_id: id }),
112 },
113 );
114 if (r.exitCode === 0) found = r.stdout.trim();
115 else $.ui.log("segmem hook failed: " + r.stderr.trim());
116 } catch (err) {
117 $.ui.log("segmem hook failed: " + String(err));
118 }
119 const r = await next(e);
120 if (!found || r.drop !== undefined) return r;
121 return { ...r, context: [...(r.context ?? []), found] };
122 });
123
124 // The rule the parent is told to pass on, passed on by the harness instead.
125 // No claim: this writes the subagent's prompt, which no command hook can
126 // reach, so there is nothing to double. Never denies a spawn.
127 let doctrine: string | undefined;
128
129 on("agent.spawn", async ($, e, next) => {
130 // Cached only on success: one timeout must not strip the rule from
131 // every later spawn in this module's life.
132 if (doctrine === undefined) {
133 try {
134 const r = await $.process.run(
135 [$.plugin.root + "/segmem", "prompt", "--subagent"],
136 { cwd: await $.session.cwd(), timeoutMs: TIMEOUT },
137 );
138 if (r.exitCode === 0) doctrine = r.stdout.trim();
139 else $.ui.log("segmem prompt --subagent failed: " + r.stderr.trim());
140 } catch (err) {
141 $.ui.log("segmem prompt --subagent failed: " + String(err));
142 }
143 }
144 if (!doctrine) return next(e);
145 return next({ ...e, prompt: e.prompt + "\n\n" + doctrine });
146 });
147
148 // The note rules, before the shell runs rather than after it failed. The
149 // check writes nothing: Python re-runs its own length and tag checks on
150 // the command text and says refuse or warn. Any other Bash command passes
151 // untouched, and the substring test keeps the shell-out off that path.
152 on("tool.call", { tool: "Bash" }, async ($, e, next) => {
153 // check-note reads only `note`, so `cd ~/Node/segmem && ...` pays no spawn.
154 if (!e.command.includes("segmem") || !e.command.includes("note")) return next(e);
155 let hint = "";
156 try {
157 const r = await $.process.run(
158 [$.plugin.root + "/segmem", "check-note"],
159 { cwd: await $.session.cwd(), timeoutMs: TIMEOUT, stdin: e.command },
160 );
161 if (r.exitCode !== 0) return { deny: r.stderr.trim() || r.stdout.trim() };
162 hint = r.stdout.trim();
163 } catch (err) {
164 $.ui.log("segmem check-note failed: " + String(err));
165 }
166 const r = await next(e);
167 if (!hint || r.deny !== undefined) return r;
168 return { ...r, context: [...(r.context ?? []), hint] };
169 });
170
171 // Recall keyed on the action, not the prompt: a fact tagged with the
172 // program a Bash command runs prints as that command is about to run.
173 // Same claim as the prompt hook, so the two paths never both speak.
174 on("tool.call", { tool: "Bash" }, async ($, e, next) => {
175 let found = "";
176 try {
177 const id = await $.session.id();
178 const r = await $.process.run(
179 [$.plugin.root + "/segmem", "hook", "--tool", "--once",
180 "--session=" + id, "--served=function"],
181 {
182 cwd: await $.session.cwd(),
183 timeoutMs: TIMEOUT,
184 stdin: JSON.stringify({ tool_input: { command: e.command }, session_id: id }),
185 },
186 );
187 if (r.exitCode === 0) found = r.stdout.trim();
188 else $.ui.log("segmem hook --tool failed: " + r.stderr.trim());
189 } catch (err) {
190 $.ui.log("segmem hook --tool failed: " + String(err));
191 }
192 const r = await next(e);
193 if (!found || r.deny !== undefined) return r;
194 return { ...r, context: [...(r.context ?? []), found] };
195 });
196
197 // Recall as a tool the model can call directly. Bash recall keeps working
198 // everywhere, so an unflagged session loses nothing. `note` is not
199 // registered: it stays a Bash command, which is what keeps "subagents
200 // never note" true with no extra enforcement.
201 on("session.start", async ($, e, next) => {
202 try {
203 const t = await $.tool.register({
204 name: "recall",
205 description:
206 "Search segmem, the project's memory, across every kind and scope. " +
207 "Use it when a prompt names a person, component, branch, issue, or " +
208 "past decision.",
209 inputSchema: {
210 type: "object",
211 required: ["query"],
212 properties: { query: { type: "string", description: "words to search for" } },
213 },
214 });
215 $.ui.log("segmem registered " + t.tool);
216 } catch (err) {
217 $.ui.log("segmem tool.register failed: " + String(err));
218 }
219 return next(e);
220 });
221
222 on("tool.call", { tool: "mcp__segmem__recall" }, async ($, e, next) => {
223 try {
224 const r = await $.process.run(
225 [$.plugin.root + "/segmem", "recall", String(e.query ?? "")],
226 { cwd: await $.session.cwd(), timeoutMs: TIMEOUT },
227 );
228 if (r.exitCode === 0) return { result: r.stdout.trim() };
229 return { deny: r.stderr.trim() || "segmem recall failed" };
230 } catch (err) {
231 $.ui.log("segmem recall failed: " + String(err));
232 return next(e);
233 }
234 });
235};
236