Releases: skymanbp/cc-memory
Release list
v2.5.6 - the plan gate guards steps; two criteria died in the gap
The plan-replacement gate guards steps — and that partial coverage cost a live plan two of its ten success criteria on 2026-08-05.
The replacement passed the R610 carryover gate cleanly, nothing was printed, and one of the two vanished criteria was an achieved-but-never-recorded release gate — so "it is done" and "it was dropped" became indistinguishable after the fact. The gate was not wrong; its charter is 换计划不许丢步骤 and it reads steps. The mistake was reading a green gate as a statement about the whole artifact. Scope of evidence is not scope of claim.
Backfill note: this Release object was created on 2026-08-06. The
v2.5.6tag was pushed at release time but no GitHub Release was ever created, so this page kept showing v2.5.5 as the latest release.
Added
unmatched_criteria(old_structured, new_plan)incore/plan.py— returns every outgoingsuccess_criteriaentry whose best trigram-Jaccard against the replacement's criteria plus itsgoalandcontextis below the steps gate's ownCARRYOVER_MATCH_THRESHOLD = 0.5. A criterion folded into the new context counts as carried; flagging lossy-but-real survival would train the reader to ignore the advisory.- Carryover advisory in
plan-set --from-refiner—cmd_plan_setsnapshots the outgoing plan beforeapply_refined_plan(afterwards it exists only inmemory/.plan_history/) and prints the unmatched criteria, what the gate does and does not cover, and — in its last line — thatcontextis free text and is never compared at all. A gate that hides its own scope is how this failure happened. tests/test_plan_carryover.py§7 — the core result, the context-fold suppression, and an end-to-end assertion that the CLI actually prints the advisory. A core function nobody surfaces is the same silence with extra steps. 14 → 20 checks.
Changed
docs/CONTRACTS.md+docs/CONTRACTS.zh.mdgain a "What the gate does NOT cover" subsection under Door 1, including the verbatim advisory output.
Deliberately not done
- No second refusal gate. Criteria legitimately get reworded, merged, translated and retired-because-achieved; an EN→ZH plan replacement auto-carries nothing, so a hard gate here would block ordinary plan evolution. This is why the advisory is an advisory.
contextis still not compared. It is prose; a similarity score over it would be noise. The advisory says so out loud instead of implying coverage it does not have.
Placement note
unmatched_criteria sits at the end of core/plan.py, not beside check_carryover where it belongs by topic. This repo carries ~600 file:line citations and only the symbol-anchored subset is machine-checked. Measured before choosing: the beside-check_carryover placement broke 29 refs in CONTRACTS.md alone; end-of-file breaks 0. Topic cohesion lost to not rotting the citation graph, and the tradeoff is recorded in a comment at the insertion point.
Verification
$ python tests/test_plan_carryover.py -> RESULT: 20 passed, 0 failed
$ python tests/smoke_test.py -> ===== ALL SMOKE TESTS PASSED =====
$ python tests/test_surfaces.py -> ===== ALL SURFACE TESTS PASSED =====
$ python tools/citation_check.py -> 600 citations, 0 stale, 0 missing
$ python tools/i18n_check.py -> 3 in-sync, no drift
Two of the seven version sites — cc_memory/config.json and .claude-plugin/marketplace.json — were missed by my own survey and caught by the smoke suite's version-drift gate, which is precisely why that gate exists.
v2.5.5 — all 13 docs gated, and their facts too
Asked whether every document was aligned, the answer was checkable rather than assertable — and checking it found that the gate scope itself was the stale thing.
The citation gate covered 7 of 13 markdown files
CHANGELOG.md, both agent prompts, commands/cc-mem.md and both skills were covered by nothing at all. The tracked list is now every markdown file in the repository, and smoke_test.py asserts it equals git ls-files "*.md" — so "which docs are gated" cannot drift again.
599 citations — 354 symbol-anchored, 245 bounds-checked, 0 unchecked, 0 stale
Nothing checked cross-document facts, only line numbers — and three had drifted
-
CLAUDE.md§ Tests still said "Three suites … run all three, plustools/i18n_check.py" aftercitation_check.pybecame a gate. It was telling the next Claude to run seven of the eight gates. It now describes all eight, and a new assertion fails if the section stops naming any gate script. -
commands/cc-mem.mdnamed 23 of the 28 subcommandscli/mem.pydefines. The five missing weresql,sessions,schema,keywords,observations— includingsql, whose read-only guard is a v2.5.0 security fix that only helps someone who knows the command exists. All 28 are listed now, and a new assertion fails if a subcommand ships without a doc row. -
README.mdandREADME.zh.mdstill said doc citations were unenforced — "Nothing enforces them today" — three releases aftercitation_check.pystarted enforcing them. Both also still said "Three stdlib scripts … all three are release gates".
The 11 tables claim is now asserted against core/db.py as well.
Verification
Eight gates green. Independent harnesses re-run unchanged: 42/42 (every v2.5.2 finding's own repro), 12/12 (real installer exe install + uninstall), 6/6 (v2.5.4's four claims). PE subsystem verified — installer 3/CONSOLE, dashboard 2/GUI. Released assets hash-verified against the locally tested build.
v2.5.4 — zero known limits
v2.5.3 closed five of six residuals and recorded four new ones. This release closes all four, by measurement, and gates each one. There is no Known limits section below, because there is nothing to put in it.
Every citation is checked — 0 unchecked, down from 253 of 595
tools/citation_check.py could only anchor a citation on a symbol, so 253 citations opted out of the gate entirely. Two changes closed that:
- an ambiguous bare filename is disambiguated by symbol — this repo has both
cli/plan.pyandcore/plan.py, and 13 citations said onlyplan.py; the surrounding prose names a symbol that exists in exactly one of them; - a citation naming no symbol at all is now bounds-checked: the cited range must lie inside the file and contain at least one non-blank line.
That last check alone found 34 stale citations — pointing past EOF or at nothing but blank lines — which every previous release shipped. All repaired. smoke_test.py now fails if any citation is unchecked.
595 citations — 353 symbol-anchored, 242 bounds-checked, 0 unchecked, 0 stale
The settings.json lost update is closed in both directions
v2.5.3 compared the file's digest before renaming, which leaves the window between that check and the rename. There is now a post-write verification: the file is read back and compared byte-for-byte against what was written, so a peer write landing after the rename is detected too and the merge is redone.
Measured with a peer write forced into both windows in a single run: our five hook events registered, and both of the peer's keys survived.
PLAN.md and MEMORY.md no longer go stale
A retry count is the wrong shape for this failure — the destination is unavailable for as long as another process holds it open, which is a duration. write_atomic gained a wall-clock budget_s; the two derived artifacts use 3 s.
150 write rounds against three readers at 100 % duty cycle:
| retry policy | stale renders | reader samples | empty reads |
|---|---|---|---|
| 12 fixed tries (0.78 s) | 2 / 150 | 307,688 | 0 |
| 3 s wall-clock budget | 0 / 150 | 202,914 | 0 |
PROGRESS.md keeps the short count deliberately: its writer raises, so the caller finds out and can act.
Both exes are executed before release
--help exercises argparse and the frozen bootstrap; the GUI is then started against a real project and is still alive 12 s later. Previously only the installer exe was run and the dashboard was PE-header inspected only.
Verification
Eight gates green: compileall, tomllib, smoke_test.py, test_plan_carryover.py (14/14), test_surfaces.py (§1–§6), i18n_check.py, citation_check.py, version-site agreement.
Independent harnesses: 42/42 (every v2.5.2 finding's own repro), 12/12 (real installer exe install + uninstall), 6/6 (this release's four claims). PE subsystem verified — installer 3/CONSOLE, dashboard 2/GUI. Released assets hash-verified against the locally tested build.
New gates so none of these can come back: smoke_test.py fails on any unchecked citation, asserts the derived writers pass a wall-clock budget and that the budget is honoured; test_surfaces.py §6 drives a peer write into both the pre-rename and post-rename windows.
v2.5.3 — v2.5.2's Known limits, cleared
No new audit. This release takes the six residuals v2.5.2 recorded in its Known limits section rather than fixed, and closes five outright. The sixth — the installer's settings.json TOCTOU — cannot be closed by locking, so it is closed by detection instead.
Two of the six turned out to be worse than they were written up as.
The three "deliberate literal twins" were not twins — and two still truncated
v2.5.2 shipped _atomic_write in core/progress.py and _atomic_write_text in core/plan.py and llm/memory_writer.py, documented on both sides as intentional copies. They were not the same function:
retries os.replace |
on final failure | |
|---|---|---|
core/progress.py |
5× | re-raises |
core/plan.py |
none | falls back to a plain truncating write_text |
llm/memory_writer.py |
none | falls back to a plain truncating write_text |
That fallback kept the artifact by making it torn — reintroducing, for that one call, exactly the defect the function existed to remove. It was the "20 empty reads in 28,141 samples" residual.
core/atomic.py is now the single implementation, with an explicit contract: replace completely, or raise. Never truncate, never silently fall back. core may be imported by llm, so the split never had a dependency reason. PROGRESS.md's writer propagates the raise (it is the handoff contract); PLAN.md's and MEMORY.md's catch it and keep the previous complete file, since both are projections of already-committed state and regenerate on the next write. core/plan.py also gained the logger it had never had — which is why every failure in it previously had to be either raised or swallowed.
The plan mutators still accepted an unscoped call
plans.id is global to the DB file, so an unscoped UPDATE/DELETE reaches whatever row owns that id — including another project's. v2.5.2 recorded this as a known limit on the grounds that "the pre-v2.5 signature stays callable". All 11 call sites in the tree already passed project_id as a keyword, so requiring it cost nothing: it is now mandatory and keyword-only, and the WHERE clause is unconditional.
A fail-closed config.json is now visible
Suspending the plugin on an unusable config is right for a privacy control, but v2.5.2's only trace was a line in ~/.claude/hooks/cc-memory/logs/ — a file nobody reads until they already suspect something. A merge-conflicted config.json, the exact accident that file's own note warns about, therefore presented as "cc-memory quietly stopped working". core.modes.config_fault() reports why and SessionStart prints one line naming it. A project the user genuinely listed stays completely silent — that silence is the feature — and test_surfaces.py §5 asserts both halves.
The installer's lost update is now detected
v2.5.2 narrowed the window from the whole install (~0.5 s) to one dict merge and shipped the rest as unfixable without a lock protocol both sides honour. Narrowing is not detecting: the read now takes a content digest, the write refuses to rename if the file no longer matches, and the whole read-merge-write is retried on the newer contents (bounded at 4). A concurrent writer can no longer be clobbered — it can only make the installer redo the merge. Uninstall is protected identically.
The exes are RUN now, not just inspected
Every release so far asserted the subsystem from the PyInstaller flag and the PE optional header and shipped without the binary having been executed once. A 12-check harness now installs from the real cc-memory-installer.exe into a sandboxed HOME, verifies the flat tree imports, and uninstalls — 12/12.
It immediately found something: main() silently ignored unrecognised arguments. --project D:\repo performed a plain install and did not initialise that project; a typo'd --unistall performed an install — the opposite of what was typed — and exited 0. Unknown arguments are now refused with the usage text and rc=2.
Doc citation coverage nearly doubled
tools/citation_check.py could only anchor a citation when the symbol was defined in the cited file, so the commonest shape in these docs — a call site, `db.tag_progress_session(...)` (`user_prompt.py:117`) — went unchecked: 370 of 594. It now anchors cross-file citations on the text of the cited range: 342 of 595 checked, up from 224.
Two of its own bugs were fixed on the way, both found by measurement rather than review: the anchor first matched any English word ≥6 characters occurring in the file (the word guardian appears at five lines of core/plan.py, so a correct citation that missed those was reported as rot), and --fix used substring replacement, which turned memory_writer.py:83-83 into memory_writer.py:55-83 — a range that never existed. It splices by character offset, right to left, so a line carrying four citations repairs correctly.
Verification
All eight gates green: compileall, tomllib, smoke_test.py, test_plan_carryover.py (14/14), test_surfaces.py (§1–§6), i18n_check.py (3 in-sync), citation_check.py (0 stale), version-site agreement. Independent harnesses: 42/42 (every v2.5.2 finding's own repro) and 12/12 (the real exe). PE subsystem verified (installer 3/CONSOLE, dashboard 2/GUI).
New gates so these cannot come back: smoke_test.py asserts all three artifact writers are core.atomic.write_atomic, that no module re-grows a private copy, that a refused replace raises with the previous complete file intact, and that the three plan mutators refuse an unscoped call. test_surfaces.py gains §6 (settings.json compare-and-swap).
Known limits
- 253 of 595 citations still cannot be anchored to any symbol and are unchecked.
--fixrepairs a stale cross-file citation to the occurrence nearest the stale number — a stated assumption, not a proof. - The
settings.jsonCAS still has a microsecond window between its final digest check and the rename. Inherent without OS locking; what changed is that a lost update is now detected and redone rather than silent. write_atomicraising means PLAN.md / MEMORY.md can be one write stale under sustained contention — the deliberate trade against a torn file.- Only the installer exe is executed by the harness; the dashboard exe is still PE-header-verified only.
v2.5.2 — persistent prompt injection, privacy-parser holes, concurrency data loss
A third audit, on angles the first two never used: time, concurrency, cross-surface agreement, and hostile input. Six read-only scan agents, seven fix agents on disjoint files, then an independent maintainer harness re-running each finding's own repro (41/41).
The headline: a persistent prompt-injection channel
Stored memory content was interpolated verbatim into the SessionStart injection and into PROGRESS.md. Measured on one stored memory, through the real MCP writer and the real SessionStart hook:
| probe | before | after |
|---|---|---|
complete <system-reminder> blocks in hook stdout |
8 (the plugin emits 1) | 1 |
=== CC-MEMORY: Context Restored === banners |
6 | 1 |
| complete blocks inside PROGRESS.md | 4 | 0 |
forged ## 7. headings in PROGRESS.md |
4 | 1 (the real one) |
memory_add is a model-invokable MCP tool, so a single indirect injection — a malicious README, a fetched page, a dependency's source — became a permanent memory, re-injected as authoritative context at the start of every later session, inside a block whose own text orders the next Claude to trust it.
Markers are now escaped, not deleted, on the write path and on all four render paths (PROGRESS.md, the injection, PLAN.md, MEMORY.md) — because rows written by v2.5.1 and earlier are already armed in users' databases. PLAN.md and MEMORY.md were the same channel: an armed plan step forged 2 ← ACTIVE markers with one active step; an armed topic name forged a block into MEMORY.md.
Privacy: two ways the opt-out switched itself off
- A UTF-8 BOM on
config.json— PowerShell'sOut-Filedefault on the primary platform — madejson.loadraise into a swallow-and-continue. One BOM added, nothing else changed:memory.dbcreated, 3 observations stored, PROGRESS.md written for an opted-out project. - One unexpandable
~userentry voided every entry after it.Path.expanduser()raisesRuntimeError, which is neitherOSErrornorValueError, so it escaped the handler whose own comment promised "one malformed entry must not disable the rest of the opt-out list". Order-dependent, so it looked intermittent. - The MCP server ignored
excluded_projectsentirely — the seventh caller of a control v2.5.1 had just finished wiring into the six hooks, and the one loaded by default from the shipped manifest with every call chosen by the model. config.jsonnow fails CLOSED: a file that exists and cannot be used excludes every project and logs why. Absent/empty is not that case.<private>is honoured on both progress ingresses now; PROGRESS.md is not gitignored, so a leak there reached the user's repository.
Data loss
- Two PreCompacts in the same second destroyed a session archive — 12 real compactions left 3 files on disk, 9 transcripts gone with no error anywhere while the rows still render in
/cc-mem sessions. Now millisecond stems plus anO_CREAT|O_EXCLclaim of the exact path. .plan_historyoverwrote itself with no concurrency at all — 4 sequential replacements in 23 ms left 1 file. Its docstring called it "append-only … last-resort backstop"; neither clause was true.- PROGRESS.md / MEMORY.md / PLAN.md could be read as 0 bytes (4,867 empty reads in 16,071 samples). All three now write to a temp file and
os.replace. - A lone surrogate anywhere in the extracted text aborted the whole compaction — archive, session row, memories and handoff.
- The installer discarded concurrent edits to the global
settings.json(6/6 lost updates) and truncated it non-atomically.
Resource hygiene
MemoryDB._connect leaked one sqlite connection per operation — with conn: commits but does not close. Measured: 4 live after the constructor, 5 after one upsert_project, 25 after 20 inserts, in three processes that hold a MemoryDB for their whole lifetime. Now 0. Honest cost, measured: +340 % per operation from the WAL checkpoint on last-close, which is +0.6 s against a 120 s PreCompact budget.
New gate for the thing nothing gated
tools/citation_check.py resolves the symbols named around each doc file.py:LINE citation with ast and asserts the cited range covers the definition or mentions the symbol. First run: 163 of 594 citations were stale. All repaired; the checker now runs inside smoke_test.py.
Also new: tests/test_surfaces.py §5 (config-parser shapes + the MCP opt-out) and three smoke gates (.gitignore three-copy parity, connection-handle regression, PLAN.md/MEMORY.md forgery).
Verification
All eight gates green: compileall, tomllib, smoke_test.py, test_plan_carryover.py (14/14), test_surfaces.py (§1-§5), i18n_check.py (3 in-sync), citation_check.py (0 stale), version-site agreement (5 sites). Independent maintainer harness 41/41. Exes rebuilt, PE subsystem verified (installer 3/CONSOLE, dashboard 2/GUI).
Known limits
--fixrewrites a stale citation to the symbol's definition, which is not always the call site a sentence meant; 370 of 594 citations remain unanchorable and unchecked.- The atomic-write fallback still has a truncation window when
os.replaceis refused (20 empty reads in 28,141 samples, down from 344). - The installer's
settings.jsonTOCTOU is shrunk from ~0.5 s to one dict merge, not closed. core/db.py's three plan mutators still defaultproject_id=None.
v2.5.1 — post-release audit fixes
cc-memory v2.5.1 — the post-release audit of v2.5.0, and its 23 fixes
v2.5.0 shipped an hour earlier. Six read-only agents then attacked it from angles the pre-release work had not used: regressions introduced by the fixes, a brand-new user installing from the released exe, every documentation claim re-checked against the code, all six hooks driven live, an audit of the ~1,650 lines of test code v2.5.0 added, and a whole-tree sweep of the project's own invariants.
They found 23 defects. The uncomfortable part: all seven release gates were green the entire time. Three of these are things a passing test suite cannot see.
Privacy
excluded_projectswas not an opt-out — it only blocked creation. The check lived in 2 of the 6 hooks. A project that already had amemory/directory and was listed afterwards — the natural sequence, since you add a repo to the list precisely when you realise it is sensitive — kept being captured in full: 4 tool calls → 4 observations stored with their inputs and outputs, aprogressrow,PROGRESS.mdnaming the secret files, 3,189 bytes injected into the next session. With a credential present the Stop observer also POSTs those observations to the Anthropic API, and that leg was unconditional. Every clause of the README's promise — "nomemory/, no DB, no extraction and no PROGRESS.md" — was false for a pre-existing project. There is now one implementation called as the first act of all six hooks. After: 0 observations, 0 progress rows, noPROGRESS.md, 0 bytes injected — while a non-excluded sibling is unaffected.- A standalone reinstall silently wiped
excluded_projects.config.jsonwas copied unconditionally, so installing a patch release reset the plugin's one privacy control to[]with no warning and no backup. The installer now merges the shipped defaults under your file.
The hook that runs on every session
- SessionStart could still blow its 15 s budget — and would do so forever. v2.5.0 added an absolute deadline to the LLM legs but left
load_transcript_window, which runs after the deadline check, unbounded in time. Measured 17.00 s against a 15 s host budget. A 2.11 GiB transcript loads in 3.37 s and the loop reaches its last check at ~12.6 s, so a single large prior transcript is enough — and one exists on the reference machine today. It repeated every session, because a transcript that yields no memories writes nosessionsrow and aTerminateProcesskill commits nothing. The loop now charges the predicted load cost to the budget before starting it. After: 7.26 s with a 2 GiB unsaved transcript, injection intact.
Surfaces
/ccm-loadwas dead on every standalone / exe install — the layout the README recommends to Windows users. It gated onenabledPlugins, which the standalone installer never writes, so it reported "NOT FULLY ACTIVATED" — false; all six hooks were registered and working — printed advice an exe user cannot follow, and never bootstrapped./cc-mem statuson the same machine reported5/5 registered: two shipped surfaces, opposite verdicts on one healthy install./cc-mem sql's "READ-ONLY" guard was bypassable. It refused onlyPRAGMA name = value; SQLite equally acceptsPRAGMA name(value), and several pragmas write with no argument.journal_mode(DELETE)disabled WAL,optimizecreatedsqlite_stat1,user_versionandapplication_idpersisted — allrc=0, under a banner calling the tool read-only, all reachable by the model through/cc-mempassthrough. The dashboard's twin guard had already fixed this exact class in v2.5.0, naming these pragmas verbatim in its comment; the port was never made.cmd_plan_checkbriefed the plan-guardian on a superseded plan · four hooks still broke the never-raise contract on a non-stringcwd/session_id(162-case fuzz: 10 failures → 3) · MCP answered id-less notifications withid: nullagainst its own docstring · the frame cap was off by one · the dashboard search box was the one search surface that never got LIKE-escaping ·cc-memory-plan --helpnamed a file not on your PATH.
Tests and documentation
- Two assertions in the new suite were vacuous — a
protocolVersioncheck that sent a supported version (so "negotiates" and "parrots back" were indistinguishable; mutating the negotiator to accept any string still passed), and a literalassert Truevia an operator-precedence trap. Both were debris from an agent killed mid-task. The rest is load-bearing: an independent audit ran 53 mutants and 51 went red at the intended assertion. tests/smoke_test.pywrote into your real~/.claudeon every run;test_surfaces.pyleaked a sandbox into the real%TEMP%on every successful run, hidden byignore_errors=True. Both are sandboxed now.- 22 in-document anchors in the two Chinese docs pointed nowhere (translated headings, English slugs) — invisible to the hash-based i18n checker by design. Two README shell recipes could not work as written:
M="python ~/..."fails because bash expands~before parameter expansion and does not rescan.
Stated rather than hidden
file:line citations in docs/ rot on every refactor and nothing enforces them. This pass re-derived the core/plan.py citations and fact-checked every prose claim, but citations into cc_memory/hooks/*, cli/mem.py and ui/installer.py were deliberately not re-derived — those files were being rewritten in the same round. Both architecture docs now say so at the top: treat a line number as a hint and the symbol name as the fact.
Gates
compileall · tomllib 2.5.1 · smoke_test.py 79 [OK], rc=0 · test_plan_carryover.py 14 passed · test_surfaces.py PASSED · i18n_check.py 3 in-sync · 6 canonical version sites agree. The three HIGH fixes were independently re-verified by the maintainer against the scan's own repros: 9/9.
Assets
Prebuilt Windows one-file executables (Python 3.13, PyInstaller), version 2.5.1. PE subsystem re-verified: cc-memory-installer.exe = 3 (CONSOLE), cc-memory-dashboard.exe = 2 (GUI).
Full history: CHANGELOG.md
v2.5.0 — surface audit + repair
cc-memory v2.5.0 — a readiness audit of every shipped surface, and the repair of what it found
Not a feature release. Twelve agents exercised the six user-facing surfaces by running them rather than reading them; four more then attacked the resulting fixes. Roughly 134 defects closed across 41 files. Every number below was measured, not estimated.
Three surfaces did not work at all
- MCP could not survive a non-ASCII character under this host's default codec — writes stored mojibake or failed outright, and a strict codec killed the process with no response. On the read side one emoji replaced an entire result batch with an error, and
↻is a glyph cc-memory emits itself. A 4301-digit integer or ~3000 levels of nesting — both reachable through an advertised tool argument, before validation — escaped the too-narrowexcept json.JSONDecodeErrorand killed the server.params: null, which many clients send for "no params", consumed the request id and never answered. - The web viewer answered zero requests. One idle TCP pre-connect wedged a single-threaded
HTTPServerwith no handler timeout — andwebbrowser.opentriggers exactly that. It also sentAccess-Control-Allow-Origin: *, and anything POSTed is injected into your next session (a prompt-injection channel), whilePOSTitself rewrote the wrong project'sMEMORY.md. - The standalone installer shipped no user-facing surfaces.
~/.claudeafter an install heldhooks/andsettings.jsonand nothing else — no/cc-mem, no skills, no subagents. With noplan-refiner,PLAN.mdcould never be populated: the v2.2 headline feature was dead on that layout. It also crashed after copying files on anysettings.jsonit could not parse (6 of 9 realistic shapes, including a UTF-8 BOM — which is what PowerShell's>writes).
Data integrity
- Cross-project contamination.
_find_transcript_dirmatched on a substring of the project basename; on the reference machinecorematched 131 of 179 slug directories. A fixture seeded with 5 memories finished with 32 after a 278,700-record foreign transcript was ingested. Path mangling also missed_and.(0 of 179 real directories contain either), so any such project path fell into that branch by construction. Fuzzy matching is deleted; ownership is now proved from the transcript's owncwd. - The privacy filter failed OPEN above 100 tags —
<private>content reached the Anthropic call and the database. 16,000 unterminated tags tookre.sub9,517 ms and leaked the tail. Now a linearstr.findscan: uncapped, and failing closed. - A file you marked private had its path sent to the API anyway —
is_privatewas computed after the helper that replaces aReadbody with"(file content)", destroying the marker. /cc-mem sqlexited 0 while permanently committingDROP TABLE.
Hooks
- The v2.2 live-plan anchor had never fired through its own hook.
PostToolUseexited on the observation gate before reaching the plan block, and the guardian drift counters varied silently by mode. Plan control is not observation. - Hooks with hard host timeouts did not bound their LLM wall-clock, though
call_llm's own docstring requires it.session_startoverran its 15 s budget with the shipped default config (2 credential candidates × 20 s);stop.pywas measured being killed at 24.96 s against 22 s.call_llmgains an absolutedeadlinethat clamps each leg to the time actually remaining — strictly stronger than the arithmetic, becauseurlopen(timeout=…)covers neither DNS nor the TLS handshake (a successful leg measured 1.48× its nominal timeout).
Added
tests/test_surfaces.py— the first automated coverage for the MCP server, the web viewer, and the installer'ssettings.jsonshape matrix. All three had none, which is precisely why these defects shipped. It is now a third release gate.cc_memory/core/version.py— one version string, importable under both the nested and the flat install layouts (import cc_memoryraises under flat, so the obvious approach could not work). Twelve literals across five files removed.excluded_projectsnow works — it was declared, defaulted to[], and had zero readers.config.jsonis stripped to the keys code actually reads.- A read-only Progress / Plan tab in the dashboard.
Not fixed — recorded honestly
The README What is not fixed section lists every residual limit, each measured rather than assumed.
Gates
compileall · tomllib 2.5.0 · smoke_test.py 79 [OK], rc=0 · test_plan_carryover.py 14 passed · test_surfaces.py PASSED · i18n_check.py 3 in-sync · 6 canonical version sites agree.
Assets
Prebuilt Windows one-file executables (Python 3.13, PyInstaller), version 2.5.0:
cc-memory-installer.exe— one-click install. PE subsystem verified 2 (GUI) → 3 (CONSOLE), so--cliis finally visible and returns an exit code. Verified end-to-end into a sandbox: rc=0, all five surfaces land, hook timeouts numerically identical tohooks/hooks.json.cc-memory-dashboard.exe— visual memory management GUI.
Full history: CHANGELOG.md
v2.4.3 — shipped-surface repair (/cc-mem, save-memories) + docs 5→2
cc-memory v2.4.3 — shipped-surface repair + docs consolidation
A fact-check of every documentation file against the code found that three of
the plugin's own entry points were broken, not merely mis-documented.
Fixed
/cc-memwas completely non-functional.commands/cc-mem.mdpassed
$ARGSto the CLI, but the placeholder Claude Code substitutes is
$ARGUMENTS— 50 uses across the installed marketplace command files, and
$ARGSappears nowhere. Unsubstituted it expanded to nothing, and
cli/mem.py'sadd_subparsers(..., required=True)aborted every invocation./save-memoriesraisedModuleNotFoundErroron any non-legacy install.
The skill put~/.claude/hooks/cc-memory/cc_memoryonsys.path; on a
marketplace install that directory holds onlylogs/. It now resolves the
package tree the wayccm-loaddoes (env var → marketplace path → standalone)
and fails with an actionable message instead of a traceback.- Install-layout probes were inverted repo-wide.
ui/installer.pycopies
each subpackage toTARGET_DIR/<subdir>/— a flat tree with no
cc_memory/segment — whileskills/ccm-load,commands/cc-mem.md,
cli/mem.py's legacy-install detection and the README install paths all
probed for the nested form. An exe-installed machine was therefore
invisible to every one of them. All four accept both layouts now. - The README documented an MCP "enable" step that does nothing.
mcp.auto_registeris read by no code, and nothing writes an MCP client
config. Manual stdio registration is documented instead.
Changed — docs/ consolidated 5 → 2
MEMORY_RULES.md, HANDOFF_PROTOCOL.md and PLAN_PROTOCOL.md are now chapters
of docs/CONTRACTS.md; I18N.md is now §9 of docs/ARCHITECTURE.md.
79 citations across 18 files were repointed to the new filenames and
anchors in the same change — including the runtime-emitted footers in
memory/MEMORY.md and memory/PROGRESS.md and the forced SessionStart
<system-reminder>. CHANGELOG.md deliberately keeps the old names in its
historical entries; rewriting them would falsify the record.
docs/ARCHITECTURE.zh.mdadded. The oldI18N.mdshipped a language
switcher pointing atI18N.zh.md, which never existed — a dead link the
checker cannot catch, because MISSING-TRANSLATION is not a failure state by
design. The i18n tracked set is now 2 documents instead of 5.- The v2.4.0 carryover gate is documented in prose for the first time. It
shipped with no coverage indocs/,CLAUDE.mdorREADME.md— only a
commit message. /ccm-loadnarrowed to what only it can do: the global plugin-activation
check, package-tree resolution, project bootstrap, PROGRESS.md seeding. It
had claimed to "run the health check (mem.py status)" — it never did, it
printed DB counts — and claimed/cc-mem statuswas a subset of itself,
which is backwards./save-memorieswas kept separate rather than merged:
the two are genuinely orthogonal, and merging them would force one skill
description to cover two unrelated triggers, degrading skill selection.
Documentation accuracy
Each corrected against the code: hook registration (a marketplace install does
not write settings.json's hooks key — only the standalone installer
does), the progress row's writer count (four paths, not three), the
anti-patch caller list (three writers were missing), the memory-tag inventory
(["llm","auto"] is emitted by no code path), the stdlib rule (read literally
it forbade import os/import sys, which every hook uses), the memory/
artifact listings, and the standalone install paths throughout.
Gates
smoke_test.py exit 0 · test_plan_carryover.py 14 passed · i18n 2 in-sync /
0 drift · tomllib parses · all modules compile · 6 canonical version sites
agree on 2.4.3.
Assets
Prebuilt Windows one-file executables (Python 3.13, PyInstaller), bundled
version 2.4.3:
cc-memory-installer.exe— one-click install (hooks + plugin)cc-memory-dashboard.exe— visual memory management GUI
Full history: CHANGELOG.md
v2.4.2 — bounded transcript reads (PreCompact no longer killed mid-write)
cc-memory v2.4.2 — bounded transcript reads
On a long-lived project the PreCompact hook was being killed mid-write —
losing that compaction's memories — and its extraction had quietly been reading
the wrong end of the transcript for weeks. Both trace to one root cause: the
hook loaded the entire transcript into memory before using ~12 KB of it.
The numbers
Measured on a real 2.11 GiB transcript (585,000 records):
| before | after | |
|---|---|---|
| transcript load | ~88s (extrapolated, ~25 MiB/s) | 1.66s |
| load + extraction | — | 2.63s |
| full hook run | killed at the 120s timeout | 14.33s, exit 0 |
Fixed
Hook cancelledon large projects.core.extractor.load_transcript_window
reads a bounded head + tail window (40 records + 32 MiB) instead of the whole
file.msg_countkeeps its exact meaning via a raw record scan (~1 GiB/s, 40×
cheaper than parsing). Because a timeout kill runs noexceptblock, the old
failure left no trace at all — a session row and archive on disk, but
.last_save.jsonstill describing the previous success.- Extraction was reading the oldest end of the transcript. The LLM summary
filled its 12,000-character budget starting from the first record and stopped
— exhausted after 329 of ~585,000 records, so every extraction saw only
the session's opening hours. It now fills from the newest backwards. The same
bug in the retroactive path (session_start._summarize_transcript) is fixed
too. PROGRESS.md's "Current Request" was always empty._first_user_request
scanned only the first 5 records, but a transcript opens with
queue-operation/attachmentmeta rows — the first real user message sits
at index 5.- A total LLM outage silently cost the session handoff.
RuntimeErrorwas
missing from the extractionexcepttuple, so it escaped to the outer handler
and skipped thePROGRESS.mdrewrite along with extraction. - The package could not be built or installed. A UTF-8 BOM in
pyproject.toml(since v2.4.0) madetomllibfail with
Invalid statement (at line 1, column 1), breaking every PEP 517 frontend. /cc-mem statusgave a partial install a clean bill of health.
_REQUIRED_PLUGIN_FILESomittedcore/extractor.py— the module both hooks
import at load time — so a broken install reported OK while every hook died.
Added
- Killed runs are visible.
PreCompactwrites
memory/.pre_compact_attempt.jsonbefore it starts and clears it only on a
completed run, so a surviving marker proves the last attempt died. Cleared on
the error path too — an errored run must not be reported as a killed one. - Automatic compactions are now visible.
.last_save.jsonrecords whether
the trigger wasautoormanual. Claude Code only surfaces hook execution
in its UI for a manual/compact, which made automatic runs look like the
hook never fired. It always did.
Changed
memory/.gitignoremigrates existing installs instead of only being
created once. Every generator was guarded byif not exists(), so each new
runtime artifact leaked forever. Newly covered:.pre_compact_attempt.json,
.last_inject.json,.last_consolidation.json,.consolidation.lock,
.plan_raw.md,.plan_history/,*.tmp— several of which embed verbatim
conversation or plan prose, making this a privacy leak rather than noise.- Version strings resynchronised across all canonical declarations (they had
drifted to three different values), andCHANGELOG.mdbackfilled for v2.4.0
and v2.4.1, which shipped without entries.
Also released today
v2.3.4, v2.4.0, and v2.4.1 existed only as commits — they are now tagged
and released so the history is consistent.
Assets
Prebuilt Windows one-file executables (Python 3.13, PyInstaller), bundled
version 2.4.2:
cc-memory-installer.exe— one-click install (hooks + plugin)cc-memory-dashboard.exe— visual memory management GUI
Full history: CHANGELOG.md
v2.4.1 — carryover matches bare titles
Patch release. Fixes a false refusal in the v2.4.0 carryover gate, caught on the gate's second real-world replacement: updating a plan in place (status and progress notes only, identical step titles) was REFUSED.
check_carryover built its match candidates as title + " " + notes only, so a step carrying a long progress note dropped the trigram Jaccard against the outgoing bare title below the 0.5 threshold — an identical title failed to auto-carry. Each incoming step now contributes two candidates: the bare title and title + notes.
Regression pinned as tests/test_plan_carryover.py §4b (suite is now 14 checks).
Tagged and released retroactively in the v2.4.2 release pass — this version shipped as commit efc22e5 on 2026-07-29 without a tag.
See CHANGELOG.md.