SLOPSHOPPER

memesh

MeMesh — local collaboration for AI coding agents. Share memory across sessions, exchange task messages, and preserve work context. One SQLite file; no cloud…

panecommand
★ 16v4.11.4MITupdated 2026-10-21PCIRCLE-AI/memesh
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · memesh
│ ┃ MeMesh memories ✕ › fix the failing auth test and add an audit log call │ ┃ Project folder: app │ ┃ /work/app ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ Search: type words and press Enter; leave em ⏺ Update(src/auth.ts) │ ┃ This did not finish. Reopen the memory to ⎿ Added 2 lines, removed 1 line │ ┃ check what it holds now. ⏺ Bash(bun test) │ ┃ Reason: JSON Parse error: Unexpected ⎿ 3 pass, 1 fail │ ┃ identifier "undefined" │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /memesh-memories │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · MeMesh memories
Project folder: app /work/app Search: type words and press Enter; leave empty for recent m This did not finish. Reopen the memory to check what it holds now. Reason: JSON Parse error: Unexpected identifier "undefined"
README

🌐 English | 繁體中文

MeMesh

Shared local memory for Claude Code and Codex.

MeMesh helps your coding agents remember your preferences, project decisions, and lessons from past work, so you spend less time explaining the same things in every new session.

  • Remember across sessions. Save a decision or lesson and find it again later.
  • Share context between tools. Claude Code and Codex on the same computer can use the same memories.
  • Leave a handoff. Keep a note of where work stopped for the next session to check.
  • Exchange messages. Running local agents can send each other work, questions, and results.

Memories live in one file on your computer: ~/.memesh/knowledge-graph.db. MeMesh needs no cloud memory service or separate AI model. Automatic reminders depend on the host integration; a handoff is context, not a guarantee that a task will restart itself.

Install

Claude Code — run these inside Claude Code, then restart it:

/plugin marketplace add PCIRCLE-AI/memesh
/plugin install memesh@pcircle-memesh

Codex — run these in your terminal, then restart Codex:

codex plugin marketplace add PCIRCLE-AI/memesh
codex plugin add memesh@pcircle-memesh

To use the command-line tools too, install Node.js 22.13+, then run:

npm install -g @pcircle/memesh
memesh doctor

Installation and update guide · Other integrations

Use it

Ask your agent in plain language:

Remember that this project uses SQLite.

What did we decide about login last time?

Save where we left off so the next session can pick it up.

Claude Code's plugin provides automatic capture and reminders. The Codex plugin can provide context at session start once its hooks are allowed to run; other capture behavior varies by host. You can also ask the agent to store or look up memories directly.

Claude Code inbox reminders work without Channel setup: hooks use the local claude-code recipient unless you configured another identity. They check at startup, your next prompt and turn end, and stop reminding once the agent records intake for that message. Live Channel push is optional; see the messaging guide.

Ordinary Claude Code MCP sessions register in message discover without enabling Channel. Online means the session is connected; immediate push still requires Channel. Without it, use the durable inbox and normal hook reminders.

Unreleased source builds add /memesh-memories in Claude Code to view and edit project memories. Claude Desktop edits inside the pane; terminal Claude Code and Codex use a native form. In Codex, ask $memesh to find and edit a memory. Fill only the title or paragraphs you want to change, leaving other fields blank. Save reads back the stored result; Cancel saves nothing. Unsupported hosts keep the existing conversation workflow.

Full tool and command reference · Agent messaging · How it works · Contributing

All 12 Memory and Coordination Tools

ToolWhat it does
rememberSave a memory; decisions need a reason.
recallFind memories.
forgetArchive a memory or remove one observation.
learnSave a mistake, its cause, and how to avoid it.
briefingRead the project's current memory context.
task_stateRead or record the goal, next step, blocker, and done work.
messageFind running local agents and exchange messages.
exportBack up memories as JSON.
importImport a memory backup.
work_packagePrepare a bounded memory digest or transcript package for human review.
user_patternsSummarize work patterns.
improvementPropose an improvement for human review.

Plugins ship 10 hooks. The intent reminder recognizes "remember this" (5 languages). Memory editing lives in Claude Code/Claude Desktop Code through the MeMesh Mod, and in Codex through MCP/native forms. There is no standalone Web Dashboard.

Scored Ranking — relevance (30%) + recency (25%) + frequency (18%) + confidence (17%) + recall impact (10%). See the architecture for details.

The fine print

Scored Ranking — Results ranked by relevance (30%) + recency (25%) + frequency (18%) + confidence (17%) + recall impact (10%).

Agent messaging, the exact rules (full guide: docs/platforms/agent-messaging.md):

  • Works today: an MCP, HTTP, or CLI sender can durably send one untrusted JSON-encoded payload of at most 65,536 UTF-8 bytes (64 KiB) to one named local recipient. A receiver can fetch it separately, resume from an opaque cursor after restart, and record intake, acknowledgement, workflow disposition, and host activation as separate facts.
  • With the MeMesh Codex plugin enabled, each startup or resumed ordinary Codex CLI thread with a valid thread identity and existing working directory registers automatically under a thread-scoped identity; no manual agent setup is required. SessionStart launches an owner-private companion. SessionEnd keeps a bounded 45-second idle queue window, resume replaces the prior exact generation, and expiry removes the registration. A message accepted during that idle window reaches the thread as a short notice when the same thread resumes, and the agent then fetches the body from the inbox; it is not a claim that a stopped UI was awakened. memesh agent setup codex-session remains available only when one workspace needs a stable named principal. The complete native envelope, including routing metadata and payload, is capped separately at 16,384 bytes (16 KiB). An exact-session send returns success only after that native queue accepts it; an oversized full envelope reports native_message_too_large, an unreachable local router reports router_unreachable, and other unavailable or rejected sessions report recipient_unavailable. Scoped recovery data remains durable for all sender-side and recipient-side failures. Principal targets retain durable store-and-forward behavior. Native acceptance is not acknowledgement or disposition, and native messages must contain no secrets.
  • A stopped, missing, or disconnected Codex session is not woken up or replaced, and a failed exact-session native delivery is not replayed automatically; the sender must retry deliberately. Its scoped recovery data stays available; memesh message storage report shows what is stored. Native delivery works on macOS and Linux only.
  • This documented native path covers ordinary Codex CLI. Do not assume Codex Desktop or an unattached task registers unless that exact running session appears in message discover; this is an evidence boundary, not a claim that those hosts are universally incompatible.
  • Pairing Claude Channel with automatic Codex registration needs no --project value copied between the two: each host derives its own routing project from its own working directory (Claude) or --workspace (Codex), so starting both in the same repository lands them in the same project automatically.

MIT license

Source 1 files
hooks/native-memory.js 171 lines
1// Claude Code Mod. Rendering and MCP calls stay inside the existing host; no model turn or web server.
2const SERVER = 'plugin:memesh:memesh';
3const PANE = 'memesh-memory';
4// Left out, a pane gets a third of the terminal, which cuts a memory's text off below the actions.
5const OPEN = { id: PANE, title: 'MeMesh memories', focus: true, closeOnEscape: true, rows: 24 };
6
7// Words a person reads in place of the stored identifiers; anything unlisted is shown as stored.
8const TYPE_NAMES = { decision: 'Decision', note: 'Note', lesson_learned: 'Lesson', pattern: 'Pattern', bug: 'Bug', 'task-state': 'Task state' };
9const HOST_NAMES = { 'claude-code': 'Claude Code', codex: 'Codex', 'codex-mcp-client': 'Codex', cli: 'MeMesh CLI' };
10
11let query = '';
12let project;
13let folder;
14let memories = [];
15let selected;
16let draft;
17let busy = false;
18// tone is undefined (plain), 'success' or 'error'; the words carry the meaning, colour only confirms it.
19let notice = { text: '' };
20
21const call = async ($, tool, args) => {
22  const result = await $.mcp.call(SERVER, tool, args);
23  const text = result.content?.find(item => item.type === 'text')?.text;
24  if (result.isError) throw new Error(text || 'MeMesh could not complete this operation.');
25  return JSON.parse(text);
26};
27const run = async ($, action) => {
28  if (busy) return;
29  busy = true;
30  notice = { text: '' };
31  $.ui.invalidate('ui.render');
32  try { await action(); }
33  catch (error) {
34    // A dropped reply may come after a commit, so promise nothing about the data: point to where to check, then the reason.
35    const reason = (error instanceof Error ? error.message : '').replace(/\s+/g, ' ').trim();
36    notice = { text: 'This did not finish. Reopen the memory to check what it holds now.', tone: 'error', detail: reason && `Reason: ${reason.length > 90 ? `${reason.slice(0, 89)}…` : reason}` };
37  }
38  finally { busy = false; $.ui.invalidate('ui.render'); }
39};
40const search = ($, value) => run($, async () => {
41  query = value;
42  selected = undefined;
43  draft = undefined;
44  const result = await call($, 'recall', { query, limit: 5 });
45  project = result.scope?.project;
46  // The first slice edits only the current project, never an implicitly selected global memory.
47  memories = project ? result.entities.filter(memory => memory.projects?.includes(project)) : [];
48  if (!project) notice = { text: 'This folder has no MeMesh project, so there are no memories to show. Open Claude Code in your project folder.', tone: 'error' };
49  else if (!memories.length) notice = { text: query ? 'No memories match. Try other words.' : 'This project has no memories yet.' };
50});
51const edit = $ => run($, async () => {
52  try {
53    const result = await call($, 'remember', { name: selected.name, project, interactive: true });
54    if (result.status === 'saved') {
55      selected = { ...selected, title: result.memory.title, observations: [result.memory.body], truncated: undefined };
56      memories = memories.map(memory => memory.name === selected.name ? selected : memory);
57      notice = { text: '✓ Saved. Below is the text read back from the database.', tone: 'success' };
58    } else if (result.status === 'cancelled') notice = { text: 'Cancelled. Nothing was changed.' };
59    else notice = { text: 'Nothing changed, so nothing was saved.' };
60  } finally {
61    // Closing the form hands the keys to the prompt, whatever it ended in; ask for them back for the pane.
62    await $.ui.open(OPEN);
63  }
64});
65const editInPane = $ => run($, async () => {
66  const result = await call($, 'remember', { name: selected.name, project, editor: { action: 'prepare' } });
67  selected = { ...selected, ...result.memory, truncated: undefined };
68  draft = { revision: result.revision, memory: result.memory, title: '', observations: result.memory.observations.map(() => '') };
69});
70const saveInPane = $ => run($, async () => {
71  const { memory, revision, title, observations } = draft;
72  const result = await call($, 'remember', { name: memory.name, project, editor: {
73    action: 'save', revision,
74    title: title.trim() ? title : memory.title,
75    observations: observations.map((text, index) => text.trim() ? text : memory.observations[index]),
76  } });
77  selected = { ...selected, ...result.memory, truncated: undefined };
78  memories = memories.map(memory => memory.name === selected.name ? selected : memory);
79  draft = undefined;
80  notice = result.status === 'saved'
81    ? { text: '✓ Saved. Below is the text read back from the database.', tone: 'success' }
82    : { text: 'Nothing changed, so nothing was saved.' };
83});
84
85export function register(on) {
86  on('session.start', async ($, event, next) => {
87    // The plugin's skill already owns /memesh (as /memesh:memesh), so the pane takes its own name.
88    await $.command.register({ name: 'memesh-memories', description: 'View and edit this project’s MeMesh memories' });
89    return next(event);
90  });
91  on('command.run', { command: 'memesh-memories' }, async ($, event) => {
92    try {
93      folder = await $.session.cwd();
94      await $.ui.open(OPEN);
95    } catch (error) {
96      return { text: `Could not open the MeMesh memories pane: ${error instanceof Error ? error.message : 'unknown reason'}` };
97    }
98    await search($, event.args.trim());
99    return {};
100  });
101  on('ui.render', { component: 'Pane' }, async ($, event, next) => {
102    if (event.requestId !== PANE) return next(event);
103    const { Box, Text, Button, Input } = $.ui.resolve(event);
104    const redraw = () => $.ui.invalidate('ui.render');
105    const typeName = memory => TYPE_NAMES[memory.type] || memory.type;
106    const day = memory => memory.created_at?.slice(0, 10) || 'date not recorded';
107    const oneLine = text => {
108      const flat = (text || '').replace(/\s+/g, ' ').trim();
109      return flat.length > 60 ? `${flat.slice(0, 59)}…` : flat;
110    };
111    const hint = text => Text({ children: [text], dimColor: true });
112    const where = folder && [
113      Text({ children: ['Project folder: ', Text({ children: [folder.split('/').filter(Boolean).at(-1) || folder], bold: true })] }),
114      // The full path only where there is room to spare: the list, not an open memory.
115      ...(selected ? [] : [hint(folder.replace(/^\/(Users|home)\/[^/]+/, '~'))]),
116    ];
117    const source = selected?.metadata?.provenance?.source_host;
118    const updateTitle = value => { if (!busy && draft) draft.title = value; };
119    const detail = selected && [
120      Text({ children: [selected.title || typeName(selected)], bold: true }),
121      Text({ children: [`${typeName(selected)} · created ${day(selected)} · `,
122        source ? Text({ children: [`written by ${HOST_NAMES[source] || source}`] }) : Text({ children: ['source not recorded'], color: 'warning' })], dimColor: true }),
123      // Actions sit above the text: a pane is only so tall, and a long memory would push them out of view.
124      Box({ flexDirection: 'row', gap: 2, marginTop: 1, children: draft ? [
125        Button({ key: 'memesh-save', label: 'Save changes', hotkey: 's', variant: 'primary', onPress: () => { void saveInPane($); } }),
126        Button({ key: 'memesh-cancel', label: 'Cancel', hotkey: 'c', onPress: () => {
127          if (!busy) { draft = undefined; notice = { text: 'Cancelled. Your edits were not saved.' }; redraw(); }
128        } }),
129      ] : [
130        // The form waits on a person; a press hook has 10 seconds, so the edit runs on and redraws when it settles.
131        Button({ key: 'memesh-edit', label: 'Edit this memory', hotkey: 'e', variant: 'primary', onPress: () => {
132          if (event.surface === 'desktop') void editInPane($);
133          else void edit($);
134        } }),
135        Button({ key: 'memesh-back', label: 'Back to list', hotkey: 'b', onPress: () => { if (!busy) { selected = undefined; notice = { text: '' }; redraw(); } } }),
136      ] }),
137      hint(draft ? 'Fill only what you want to change; blank keeps the original. Save changes or Cancel above.'
138        : event.surface === 'desktop' ? 'e edit in this pane · b back to list · Esc close'
139          : 'e edit · b back to list · Esc close'),
140      ...(!draft && event.surface !== 'desktop' ? [hint('Edit opens a form: fill in only what you want to change, blank keeps the original; Accept saves, Esc cancels.')] : []),
141      ...(draft ? [
142        Input({ key: 'memesh-title', label: 'New title (blank keeps original)', value: draft.title, autoFocus: true, submitLabel: 'keep editing',
143          onInput: updateTitle, onSubmit: updateTitle }),
144        ...draft.observations.map((text, index) => {
145          const update = value => { if (!busy && draft) draft.observations[index] = value; };
146          return Input({ key: `memesh-paragraph-${index}`, label: `Paragraph ${index + 1} (blank keeps original)`, value: text, submitLabel: 'keep editing', onInput: update, onSubmit: update });
147        }),
148      ] : []),
149      Box({ borderStyle: 'round', borderDimColor: true, paddingX: 1, children: [Text({ children: [selected.observations.join('\n\n')] })] }),
150      ...(selected.truncated ? [hint('Only part is shown here; Edit loads the full text.')] : []),
151    ];
152    const results = memories.map((memory, index) => Box({ flexDirection: 'column', children: [
153      Button({ key: `memesh-memory-${index}`, plain: true, hotkey: String(index + 1), label: memory.title || typeName(memory),
154        onPress: () => { selected = memory; notice = { text: '' }; redraw(); } }),
155      Text({ children: [`   ${typeName(memory)} · ${day(memory)} — ${oneLine(memory.observations?.[0])}`], dimColor: true, wrap: 'truncate-end' }),
156    ] }));
157    const list = memories.length
158      ? [Text({ children: [`Found ${memories.length}. Press a number to open:`] }), ...results, hint('Esc to close')]
159      : [];
160    return Box({ flexDirection: 'column', children: [
161      ...(where || []),
162      ...(!draft ? [Box({ marginTop: 1, children: [Input({ key: 'memesh-search', label: 'Search', placeholder: 'type words and press Enter; leave empty for recent memories', value: query, submitLabel: 'search',
163        onInput: value => { query = value; }, onSubmit: value => search($, value) })] })] : []),
164      // Status sits above the results for the same reason as the actions.
165      ...(busy || notice.text ? [Text({ children: [busy ? 'Working…' : notice.text], ...(notice.tone && !busy ? { color: notice.tone } : { dimColor: true }) })] : []),
166      ...(!busy && notice.detail ? [hint(notice.detail)] : []),
167      Box({ flexDirection: 'column', marginTop: 1, children: detail || list }),
168    ] });
169  });
170}
171