Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
146f132
Skip session-pending markers that vanish before they are stat-ed (#36)
claude Nov 8, 2026
292839e
Put back a fresh sweep lock found during stale takeover instead of de…
claude Nov 8, 2026
9b595c6
Back off and give up on session summaries whose model call keeps fail…
claude Nov 8, 2026
8f2a6a9
docs(plans): Status lines say what shipped; the sweep marker is writt…
claude Nov 8, 2026
07fadc3
fix(engine): resolve 'day before yesterday' to two days back (#9)
claude Nov 8, 2026
31fd224
Match confidence boosters as whole words, not substrings (#23)
claude Nov 8, 2026
d84b59f
fix(engine): match temporal trigger keywords on word boundaries (#17)
claude Nov 8, 2026
0220502
Forward useSynonyms, useIntent, useMmr and mmrLambda through the memo…
claude Nov 8, 2026
0d282f8
fix(recall): vector similarity covers every visible embedded row, not…
claude Nov 8, 2026
0b751e7
Keep every query word in the FTS query; synonyms fill the remaining r…
claude Nov 8, 2026
8194264
fix(engine): expand words in several synonym groups to every group (#12)
claude Nov 8, 2026
b3a159c
fix(engine): every query word and every synonym group fits inside the…
claude Nov 8, 2026
ed2c4a2
Session markers: a notBefore past the longest back-off is not trusted…
claude Nov 8, 2026
d701544
Memories list: only the reply to the latest load writes the rows (#20)
claude Nov 8, 2026
7d595c4
Run a search typed after Escape while a request is in flight (#22)
claude Nov 8, 2026
6d8f220
Keep the knowledge note's Clear button across re-renders (#29)
claude Nov 8, 2026
4b3e564
Show the error note when a review-queue reload fails over an earlier …
claude Nov 8, 2026
d9dbcf1
Keep the shown search hits after a forget instead of collapsing to 10…
claude Nov 8, 2026
fef7919
Keep the shown count after a forget on the first page too, without ba…
claude Nov 8, 2026
b6efa14
Live recall sets the star's last-recalled time (#32)
claude Nov 8, 2026
3715727
Settings: only the last copied command's button says Copied (#33)
claude Nov 8, 2026
80b693f
Settings: let go of copy buttons from earlier draws (#33)
claude Nov 8, 2026
a8db8f7
Merge PR #78 (bug-hunt batch B: recall and query)
claude Nov 8, 2026
7fb7348
chore(release): 0.31.2, engine build .18 with changes 34-40 (#19, #7,…
claude Nov 8, 2026
cdfdbd3
Merge PR #79 (bug-hunt batch D: dashboard)
claude Nov 8, 2026
663b6e0
Merge PR #80 (bug-hunt batch E: housekeeping)
claude Nov 8, 2026
a09ba22
chore(release): 0.31.3 and 0.31.4 changelog (#20, #22, #29, #30, #31,…
claude Nov 8, 2026
05b10ee
test(session): the harness rig reads the session id past #38's --fail…
claude Nov 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "mnemosyne",
"displayName": "Claude Mnemosyne",
"version": "0.31.1",
"version": "0.31.4",
"description": "Long-term memory for Claude Code across sessions and repos, on its own private copy of the Mnemosyne Memory engine and its own store.",
"author": {
"name": "Interstellar-code"
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,43 @@

Newest first. Each release is tagged `vX.Y.Z` on GitHub; the README and DESIGN.md describe the contracts.

## 0.31.4 (bug-hunt batch E: housekeeping)

No engine change.

1. **A session marker that disappears mid-list no longer stops the summaries.** A marker claimed by another session between listing and reading is skipped (#36).
2. **A live sweep lock is never deleted.** When a stale lock was taken over just as another process renewed it, the fresh lock was removed; it is now put back (#37).
3. **Session summaries that keep failing give up.** A summary whose model call fails waits 1, 2, 4, then 8 hours before the next try, and after 5 failed attempts it is dropped with one line in the activity log, instead of being retried on every first prompt forever (#38).
4. **The plans in `docs/plans/` say what shipped** (#39).

`adm-zip` advisories (#35, still open): reviewed; the vulnerable code runs only in `onnxruntime-node`'s install script, which the plugin's own install skips, so nothing reachable at runtime. Moving the engine's `onnxruntime-node` pin from 1.26.0 to 1.31.1 would clear all 8 when the engine is next touched.

## 0.31.3 (bug-hunt batch D: the dashboard)

No engine change.

1. **The Memories list shows the latest load.** A slow reply to an earlier load could overwrite the rows of a later one (#20).
2. **A search typed after Escape runs.** Typing a new search while the previous request was still running did nothing (#22).
3. **The Knowledge note's Clear button keeps focus** across re-renders (#29).
4. **A failed Review reload says so** instead of silently showing the old queue (#30).
5. **Forgetting a search hit keeps your place.** The list no longer collapses back to 10 and the count stays right (#31).
6. **Live recall updates a memory's last-recalled time** on the map (#32).
7. **Only the last copied command says "Copied"** on the Settings page (#33).

#21 (reconnect replays duplicates after 20,000 lines) was already fixed: the stream resumes from a fresh cursor.

## 0.31.2 (bug-hunt batch B: recall and query)

Engine build 18.12.3-mnemosyne.18 (engine changes 34 to 40). Run `/mnemosyne-setup` once after updating; outside Claude Code, `bun bin/update.ts` or Update… on the Health tab.

1. **Meaning search looks at every memory.** With embeddings on, only the 500 newest memories were compared by meaning, so an older memory that matched by meaning but shared no words was never found. Now every memory with an embedding is compared (the work grows with the store; fine for thousands of memories) (#19).
2. **Recall settings from the engine's own API are honoured.** `useSynonyms`, `useIntent`, `useMmr` and `mmrLambda` passed to `recall()` were silently dropped (#7).
3. **Words with two meanings search both.** "token", "push", "setup", "check" and similar words now find memories that use either meaning's words (#12).
4. **"Chicago" is not a time word.** Words that merely contain "ago", "last" or "before" (Chicago, Elasticsearch, plastic, beforehand) no longer pull recent unrelated notes into results (#17).
5. **Long questions keep all their words.** In a question with several words, later words were dropped from the keyword search when earlier ones had many synonyms (#26).
6. **"The day before yesterday" means two days back** (#9).
7. **Notes are not mistaken for events.** The letters "at" and "on" inside other words made almost every note look a bit like an event (#23).

## 0.31.1 (bug-hunt batch C: extraction and parsing)

Engine build 18.12.3-mnemosyne.17 (engine changes 29 to 33). Run `/mnemosyne-setup` once after updating; outside Claude Code, `bun bin/update.ts` or Update… on the Health tab.
Expand Down
77 changes: 67 additions & 10 deletions bin/memories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// and, for the session work (plan phase SESSION):
// bun bin/memories.ts --store <dir> first-prompt <owner> [<pins>] the newest session summary and the pinned memories, as JSON (read-only)
// bun bin/memories.ts --store <dir> summarised <session id>... which of those sessions already have a summary, as JSON (read-only)
// bun bin/memories.ts session-pending write|list|claim|release|remove <root> ... the markers session.end leaves (an id, an owner and a path; never text); claim takes one for a summary, release puts it back
// bun bin/memories.ts session-pending write|list|claim|release|remove <root> ... the markers session.end leaves (an id, an owner and a path; never text); claim takes one for a summary, release puts it back (release --failed counts a failed model call: back-off, dropped at the fifth)
// bun bin/memories.ts sweep-claim <root> whether the daily unused-expiry sweep is due; if so, marks it taken
// bun bin/memories.ts --store <dir> expire [--apply] [--days N] [--owner O] [--root R] [--limit N] [--json]
// the unused-expiry sweep: a dry run unless --apply, which backs the store up first
Expand All @@ -30,7 +30,7 @@ import { basename, dirname, join, resolve } from 'node:path'
import { BACKFILL_TEXT_LIMIT, isProposalId } from '../hooks/backfill'
import { isKind } from '../hooks/kinds'
import { buildRequests, engineEnv, parseReplies } from '../hooks/engine'
import { BLOCK_PINNED_MAX, EXPIRE_DEFAULT_DAYS, expireRequest, expireSetting, parseExpireReply, parsePending, SESSION_CLAIM_STALE_MS, SESSION_CLAIM_SUFFIX, SESSION_ID, SESSION_PENDING_DIR, SWEEP_LIMIT, SWEEP_LOCK, SWEEP_LOCK_STALE_MS, SWEEP_MARKER, sweepDue, sweepMarkerText } from '../hooks/session'
import { BLOCK_PINNED_MAX, EXPIRE_DEFAULT_DAYS, expireRequest, expireSetting, parseExpireReply, parsePending, SESSION_CLAIM_STALE_MS, SESSION_CLAIM_SUFFIX, SESSION_BACKOFF_MAX_MS, SESSION_ID, SESSION_MAX_ATTEMPTS, SESSION_PENDING_DIR, sessionBackoffMs, SWEEP_LIMIT, SWEEP_LOCK, SWEEP_LOCK_STALE_MS, SWEEP_MARKER, sweepDue, sweepMarkerText } from '../hooks/session'
import type { BlockRead, ExpiredRow, PendingSession } from '../hooks/session'
import { cleanTags, MAX_TAGS, normalizeTag } from '../hooks/tags'
import { readConfig } from './settings'
Expand Down Expand Up @@ -476,6 +476,38 @@ export function releaseSessionPending(root: string, sessionId: string) {
}
}

/**
* Issue #38: puts a claimed marker back after a failed summary model call, with one more failed attempt and a time before
* which it is not tried again; the fifth failed attempt drops it instead. The new marker is a 0600 file renamed over the
* claim, then the claim is put back as releaseSessionPending does. Nothing claimed is nothing to do (attempts 0).
*/
export function failSessionPending(root: string, sessionId: string, now: number = Date.now()): { attempts: number; gaveUp: boolean } {
if (!SESSION_ID.test(sessionId)) throw new Error('a session marker needs a plain session id')
const claim = claimFileOf(root, sessionId)
let entry: PendingSession | undefined
try {
entry = parsePending(readFileSync(claim, 'utf8'))
} catch {
return { attempts: 0, gaveUp: false }
}
if (entry === undefined || entry.sessionId !== sessionId) {
releaseSessionPending(root, sessionId)
return { attempts: 0, gaveUp: false }
}
const attempts = (entry.attempts ?? 0) + 1
if (attempts >= SESSION_MAX_ATTEMPTS) {
rmSync(claim, { force: true })
return { attempts, gaveUp: true }
}
const temp = `${claim}.${process.pid}.tmp`
rmSync(temp, { force: true })
writeFileSync(temp, `${JSON.stringify({ sessionId: entry.sessionId, owner: entry.owner, transcript: entry.transcript, attempts, notBefore: now + sessionBackoffMs(attempts) })}\n`, { mode: 0o600, flag: 'wx' })
if (!IS_WINDOWS) chmodSync(temp, 0o600)
renameSync(temp, claim)
releaseSessionPending(root, sessionId)
return { attempts, gaveUp: false }
}

/** Every session marker, newest file first (the latest sessions are summarised first); a torn or foreign file is skipped. */
export function listSessionPending(root: string, now: number = Date.now()): PendingSession[] {
const folder = sessionPendingDir(root)
Expand All @@ -492,13 +524,22 @@ export function listSessionPending(root: string, now: number = Date.now()): Pend
}
const files = readdirSync(folder)
.filter(name => name.endsWith('.json'))
.map(name => ({ name, mtime: statSync(join(folder, name)).mtimeMs }))
.flatMap(name => {
try {
return [{ name, mtime: statSync(join(folder, name)).mtimeMs }]
} catch {
return [] // gone or claimed since the readdir
}
})
.sort((a, b) => b.mtime - a.mtime || (a.name < b.name ? -1 : 1))
const out: PendingSession[] = []
for (const { name } of files) {
try {
const entry = parsePending(readFileSync(join(folder, name), 'utf8'))
if (entry !== undefined && `${entry.sessionId}.json` === name) out.push(entry)
// Issue #38: a marker still waiting after a failed attempt is not listed until its time comes. A wait longer than the
// longest back-off (a clock that jumped back, a hand edit) is not trusted, so such a marker is never hidden for good.
const waiting = entry?.notBefore !== undefined && entry.notBefore > now && entry.notBefore - now <= SESSION_BACKOFF_MAX_MS
if (entry !== undefined && `${entry.sessionId}.json` === name && !waiting) out.push(entry)
} catch {
// unreadable: not an entry
}
Expand All @@ -507,7 +548,7 @@ export function listSessionPending(root: string, now: number = Date.now()): Pend
}

/** Makes the lock with an exclusive create, so one caller gets it; a lock left by a claimant that died is taken over. */
function takeSweepLock(lock: string): boolean {
export function takeSweepLock(lock: string, beforeTakeover?: () => void): boolean {
for (let attempt = 0; attempt < 3; attempt++) {
try {
closeSync(openSync(lock, 'wx', 0o600))
Expand All @@ -524,14 +565,29 @@ function takeSweepLock(lock: string): boolean {
if (age < SWEEP_LOCK_STALE_MS) return false // a live claimant is checking the marker right now
// Stale: moved aside by a rename (one caller's rename wins), and only a lock that is still old is thrown away.
const aside = `${lock}.${process.pid}.stale`
beforeTakeover?.() // test seam: another claimant may replace the stale lock here
try {
renameSync(lock, aside)
} catch {
continue
}
const fresh = Date.now() - statSync(aside).mtimeMs < SWEEP_LOCK_STALE_MS
rmSync(aside, { force: true })
if (fresh) return false // that was a live claimant's new lock
let fresh: boolean
try {
fresh = Date.now() - statSync(aside).mtimeMs < SWEEP_LOCK_STALE_MS
} catch {
continue // it went in the meantime: try the create again
}
if (!fresh) {
rmSync(aside, { force: true })
continue
}
// That was a live claimant's new lock: put it back for them; if that fails, drop the copy, but never claim the lock.
try {
renameSync(aside, lock)
} catch {
rmSync(aside, { force: true })
}
return false
}
return false
}
Expand Down Expand Up @@ -761,8 +817,9 @@ async function main(argv: readonly string[]) {
if (more.length !== 1) throw new Error('usage: session-pending claim <root> <session id>')
process.stdout.write(`${JSON.stringify({ claimed: claimSessionPending(root, more[0]!) })}\n`)
} else if (action === 'release') {
if (more.length !== 1) throw new Error('usage: session-pending release <root> <session id>')
releaseSessionPending(root, more[0]!)
if (more.length === 2 && more[1] === '--failed') process.stdout.write(`${JSON.stringify(failSessionPending(root, more[0]!))}\n`)
else if (more.length !== 1) throw new Error('usage: session-pending release <root> <session id> [--failed]')
else releaseSessionPending(root, more[0]!)
} else {
throw new Error('usage: session-pending write|list|claim|release|remove <root> ...')
}
Expand Down
2 changes: 2 additions & 0 deletions docs/plans/mapx-solar-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Status: decided, ready to build (Rohit's decisions 2026-10-25, see Decisions). Created 2026-10-25. Baseline: `main` at `ba6f97d` (plugin 0.8.2). Delivery: each task group on a short branch off `main`, merged straight into `main` after a separate review; nothing is pushed. Release: 0.8.3 (Rohit, confirmed 2026-10-25).

Status 2026-11-08: shipped in 0.8.3 (2026-10-25), MAPX.0 to MAPX.8, MAPX.10 and MAPX.11; 0.8.4 (2026-10-25) then placed the Galaxy page as in the mockup. Not built: MAPX.9 (Edit, Pin and Retire in the Details panel); the panel is still read-only and points to the Review tab for Restore.

This plan was split out of phase MAPX of `docs/plans/mnemosyne-v3.md` on 2026-10-25, because Rohit wants the map built first, ahead of the rest of 0.9.0 (the dashboard writes, the importers, session summaries and the expiry sweep). Everything the old MAPX text said is carried here; what changed, and why, is listed in "Notes for the reviewer". A later step removes MAPX from the 0.9.0 plan; "What the 0.9.0 plan must drop or point to" is the list for that step.

## Summary
Expand Down
2 changes: 2 additions & 0 deletions docs/plans/mnemosyne-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Status: decided, ready to build (Rohit accepted all 13 proposed answers, 2026-10-25; see Decisions). Created 2026-10-25. Baseline: `main` at `59d1f3e` (0.8.4). Release: 0.8.5, before the 0.9.0 half of `docs/plans/mnemosyne-v3.md` (waves 4-7) starts, so the 0.9.0 lanes branch from a `main` that already has this. Delivery: each phase on a short branch off `main`, merged into `main` after its review; nothing is pushed until the release task.

Status 2026-11-08: shipped in 0.8.5 (2026-10-26), phases CFG, WIRE, SRV, UI and REL85. UI.4 is Rohit's manual check on his installed plugin and is not recorded here.

Research reference: `research/settings-research.md` (next to this plan; read-only homework, 2026-10-25). Section numbers below ("R1.3") point into it.

## Summary
Expand Down
4 changes: 3 additions & 1 deletion docs/plans/mnemosyne-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Status: decided, ready to build (Rohit's decisions 2026-10-24, see Decisions). C

Status 2026-10-25: waves 1-3b are shipped (0.8.0, then fixes 0.8.1 and 0.8.2; `main` at `2f1a104`). The 0.9.0 half (waves 4-7) was reviewed against the code on 2026-10-25 and corrected in place; see "0.9.0 review (2026-10-25)" before Wave 4. Not yet started; Rohit is still adding points before execution is planned. The solar-system map (the old phase MAPX) and the project registry (the old SESSION.5) moved to `docs/plans/mapx-solar-map.md` on 2026-10-25; the map plan ships first and `main` moves on, so lanes branch from the new `main`.

Status 2026-11-08: the 0.9.0 half shipped in 0.9.0 (2026-10-26): CRUDX.0 to CRUDX.5, IMPORT.0 to IMPORT.3, CRUD.1 to CRUD.4, SESSION.1, SESSION.2, SESSION.4 and HARDEN.3. Not built by an agent: HARDEN.4 and HARDEN.5 are Rohit's manual runs on his store and are not recorded here. Wave 8 (LATER.1, LATER.2, LATER.3, LATER.5) is still parked.

## Summary

Version 3 of the Mnemosyne plugin: control what goes into memory, and organise what is there. Every save, from Claude, outside agents, the dashboard or an importer, passes one guard that blocks secrets and oversize text and warns about near-duplicates and task chatter. Saves then record honest provenance and a kind instead of 'imported' and 'unknown'. Tags come from a small shared vocabulary with entity, agent and asset namespaces. The dashboard gains create, edit, retire and a review queue, and documents and session logs can be imported through the same guard. It matters because the store today holds 447 tags for 140 memories (407 used once), five rows marked SUPERSEDED that are still recalled, and no usable provenance at all.
Expand Down Expand Up @@ -806,7 +808,7 @@ When a session ends (exit, `/clear` or resume), the plugin notes it in a small p
1. Goal: the plugin side of auto-expire. Needs MODEL.5 and IMPORT.3 (the pinned flag).
2. Acceptance:
1. a plugin option `expireUnusedDays`: 90 (default), or 0 to turn the sweep off; values 1-29 are refused;
2. the sweep runs at most once per 24 hours (a marker file, mode 0600), triggered from `session.start` and from a session's first prompt (so a long-lived process that only sees `/clear` still sweeps), after the reply, in the background: the C9 backup first (stop if it fails), then `mnemopi_expire_unused` with `dry_run` false and `validator: 'expire-sweep'` (C10), for the owner's own rows only; the activity log line (action 'expire', origin 'session') holds counts and ids, never memory text;
2. the sweep runs at most once per 24 hours (a marker file, mode 0600), triggered from `session.start` and from a session's first prompt (so a long-lived process that only sees `/clear` still sweeps), after the reply, in the background: the C9 backup first (stop if it fails), then `mnemopi_expire_unused` with `dry_run` false and `validator: 'expire-sweep'` (C10), for the owner's own rows only; the activity log line (action 'expire', origin 'session') holds counts and ids, never memory text. The 24-hour marker is written before the sweep runs, by design (see `claimSweep` in `bin/memories.ts`), so a failed sweep is tried again only after 24 hours;
3. `bun bin/memories.ts expire` is a dry run that prints the candidates (id, kind, last used, first 80 characters) and `bun bin/memories.ts expire --apply` runs it for real after a backup, so Rohit can preview the first run on his store;
4. rows with a pinned flag, other kinds or an existing expiry are never touched (the engine enforces this; a spec checks the plugin does not pass wider kinds than the option allows);
5. `tests/session.test.ts` and `tests/bun/` specs cover the daily marker, both triggers, the off setting, a failed backup stopping the sweep, and the dry run, all on a temp store.
Expand Down
1 change: 1 addition & 0 deletions docs/plans/standalone-install.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# standalone-install: install the engine and proxy outside Claude Code (for Switch), issue #43
Status: revision 2, ready to build (2026-11-01). Revised after the OMP DeepSeek v4.1 flash review in `docs/plans/research/standalone-install-review.md` (17 findings F1 to F17, answers Q1 to Q6); every finding is handled below, marked `[F#]`. Baseline: `main` at `8dfb382` (plugin 0.9.0, `ENGINE_BUILD` `18.12.3-mnemosyne.10`). Delivery: one branch in an Orca worktree, one commit per task, merged into `main` only after a separate review; nothing is pushed or tagged without Rohit. Release: 0.10.0 (next minor). Source: GitHub issue #43 (from Interstellar-code/switch-agent #5, decision SD12) and the Switch docs read on 2026-10-31.
Status 2026-11-08: shipped in 0.10.0 (2026-11-01), SA.1 to SA.5; install fixes followed in 0.10.1 (2026-11-01).

## Summary

Expand Down
2 changes: 2 additions & 0 deletions docs/plans/ui-polish-proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Date: 2026-10-24. Status: proposal, nothing built. Source: a read-only review of
`ui/` (four parallel reviews plus the screenshots). No browser run was done, so the
line references are from reading the code, not from a live check.

Status 2026-11-08: partly done, mostly through other plans and in other forms. Already in the first commit of this repository (0.8.1, 2026-10-25): X1 (the Map loads first), X2 (the shared bottom sheet, `ui/sheet.ts`) and Search's Keyword / Keyword + meaning control (Search 1). Section 1 (Map) was superseded by the solar-system map (`docs/plans/mapx-solar-map.md`, 0.8.3, 2026-10-25). The Review tab and `GET /api/review` (section 6, items 8 and 9) shipped in 0.9.0 (2026-10-26). The tab polish of 0.17.0 to 0.25.0 (2026-11-07) followed a later design proposal, not this list, and covers much of sections 2 to 6 in its own way, for example X3 (Health's System panel, 0.23.0), why a hit matched (0.19.0), Activity grouping (0.21.0), Knowledge grouping and filters (0.20.0) and a Keep in Review (0.22.0); see those CHANGELOG entries. Items not named here are open or dropped.

Effort: S = under an hour, M = a few hours, L = a day or more.

## 0. Cross-page changes (do first, every page gains)
Expand Down
Loading