Skip to content

Releases: skymanbp/cc-memory

v2.5.6 - the plan gate guards steps; two criteria died in the gap

Choose a tag to compare

@skymanbp skymanbp released this 06 Aug 01:43

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.6 tag 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) in core/plan.py — returns every outgoing success_criteria entry whose best trigram-Jaccard against the replacement's criteria plus its goal and context is below the steps gate's own CARRYOVER_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-refinercmd_plan_set snapshots the outgoing plan before apply_refined_plan (afterwards it exists only in memory/.plan_history/) and prints the unmatched criteria, what the gate does and does not cover, and — in its last line — that context is 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.md gain 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.
  • context is 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

Choose a tag to compare

@skymanbp skymanbp released this 06 Aug 00:09

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, plus tools/i18n_check.py" after citation_check.py became 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.md named 23 of the 28 subcommands cli/mem.py defines. The five missing were sql, sessions, schema, keywords, observations — including sql, 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.md and README.zh.md still said doc citations were unenforced"Nothing enforces them today" — three releases after citation_check.py started 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

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 23:53

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.py and core/plan.py, and 13 citations said only plan.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

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 23:20

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 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. --fix repairs a stale cross-file citation to the occurrence nearest the stale number — a stated assumption, not a proof.
  • The settings.json CAS 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_atomic raising 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

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 22:07

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's Out-File default on the primary platform — made json.load raise into a swallow-and-continue. One BOM added, nothing else changed: memory.db created, 3 observations stored, PROGRESS.md written for an opted-out project.
  • One unexpandable ~user entry voided every entry after it. Path.expanduser() raises RuntimeError, which is neither OSError nor ValueError, 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_projects entirely — 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.json now 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 an O_CREAT|O_EXCL claim of the exact path.
  • .plan_history overwrote 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

  • --fix rewrites 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.replace is refused (20 empty reads in 28,141 samples, down from 344).
  • The installer's settings.json TOCTOU is shrunk from ~0.5 s to one dict merge, not closed.
  • core/db.py's three plan mutators still default project_id=None.

v2.5.1 — post-release audit fixes

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 17:26

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_projects was not an opt-out — it only blocked creation. The check lived in 2 of the 6 hooks. A project that already had a memory/ 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, a progress row, PROGRESS.md naming 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 — "no memory/, 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, no PROGRESS.md, 0 bytes injected — while a non-excluded sibling is unaffected.
  • A standalone reinstall silently wiped excluded_projects. config.json was 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 no sessions row and a TerminateProcess kill 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-load was dead on every standalone / exe install — the layout the README recommends to Windows users. It gated on enabledPlugins, 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 status on the same machine reported 5/5 registered: two shipped surfaces, opposite verdicts on one healthy install.
  • /cc-mem sql's "READ-ONLY" guard was bypassable. It refused only PRAGMA name = value; SQLite equally accepts PRAGMA name(value), and several pragmas write with no argument. journal_mode(DELETE) disabled WAL, optimize created sqlite_stat1, user_version and application_id persisted — all rc=0, under a banner calling the tool read-only, all reachable by the model through /cc-mem passthrough. 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_check briefed the plan-guardian on a superseded plan · four hooks still broke the never-raise contract on a non-string cwd/session_id (162-case fuzz: 10 failures → 3) · MCP answered id-less notifications with id: null against 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 --help named a file not on your PATH.

Tests and documentation

  • Two assertions in the new suite were vacuous — a protocolVersion check that sent a supported version (so "negotiates" and "parrots back" were indistinguishable; mutating the negotiator to accept any string still passed), and a literal assert True via 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.py wrote into your real ~/.claude on every run; test_surfaces.py leaked a sandbox into the real %TEMP% on every successful run, hidden by ignore_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

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 13:36

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-narrow except json.JSONDecodeError and 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 HTTPServer with no handler timeout — and webbrowser.open triggers exactly that. It also sent Access-Control-Allow-Origin: *, and anything POSTed is injected into your next session (a prompt-injection channel), while POST itself rewrote the wrong project's MEMORY.md.
  • The standalone installer shipped no user-facing surfaces. ~/.claude after an install held hooks/ and settings.json and nothing else — no /cc-mem, no skills, no subagents. With no plan-refiner, PLAN.md could never be populated: the v2.2 headline feature was dead on that layout. It also crashed after copying files on any settings.json it 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_dir matched on a substring of the project basename; on the reference machine core matched 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 own cwd.
  • The privacy filter failed OPEN above 100 tags — <private> content reached the Anthropic call and the database. 16,000 unterminated tags took re.sub 9,517 ms and leaked the tail. Now a linear str.find scan: uncapped, and failing closed.
  • A file you marked private had its path sent to the API anywayis_private was computed after the helper that replaces a Read body with "(file content)", destroying the marker.
  • /cc-mem sql exited 0 while permanently committing DROP TABLE.

Hooks

  • The v2.2 live-plan anchor had never fired through its own hook. PostToolUse exited 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_start overran its 15 s budget with the shipped default config (2 credential candidates × 20 s); stop.py was measured being killed at 24.96 s against 22 s. call_llm gains an absolute deadline that clamps each leg to the time actually remaining — strictly stronger than the arithmetic, because urlopen(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's settings.json shape 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_memory raises under flat, so the obvious approach could not work). Twelve literals across five files removed.
  • excluded_projects now works — it was declared, defaulted to [], and had zero readers. config.json is 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 --cli is finally visible and returns an exit code. Verified end-to-end into a sandbox: rc=0, all five surfaces land, hook timeouts numerically identical to hooks/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

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 02:28

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-mem was completely non-functional. commands/cc-mem.md passed
    $ARGS to the CLI, but the placeholder Claude Code substitutes is
    $ARGUMENTS — 50 uses across the installed marketplace command files, and
    $ARGS appears nowhere. Unsubstituted it expanded to nothing, and
    cli/mem.py's add_subparsers(..., required=True) aborted every invocation.
  • /save-memories raised ModuleNotFoundError on any non-legacy install.
    The skill put ~/.claude/hooks/cc-memory/cc_memory on sys.path; on a
    marketplace install that directory holds only logs/. It now resolves the
    package tree the way ccm-load does (env var → marketplace path → standalone)
    and fails with an actionable message instead of a traceback.
  • Install-layout probes were inverted repo-wide. ui/installer.py copies
    each subpackage to TARGET_DIR/<subdir>/ — a flat tree with no
    cc_memory/ segment — while skills/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_register is 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.md added. The old I18N.md shipped a language
    switcher pointing at I18N.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 in docs/, CLAUDE.md or README.md — only a
    commit message.
  • /ccm-load narrowed 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 status was a subset of itself,
    which is backwards. /save-memories was 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)

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 01:05

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 cancelled on large projects. core.extractor.load_transcript_window
    reads a bounded head + tail window (40 records + 32 MiB) instead of the whole
    file. msg_count keeps its exact meaning via a raw record scan (~1 GiB/s, 40×
    cheaper than parsing). Because a timeout kill runs no except block, the old
    failure left no trace at all — a session row and archive on disk, but
    .last_save.json still 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 / attachment meta rows — the first real user message sits
    at index 5.
  • A total LLM outage silently cost the session handoff. RuntimeError was
    missing from the extraction except tuple, so it escaped to the outer handler
    and skipped the PROGRESS.md rewrite along with extraction.
  • The package could not be built or installed. A UTF-8 BOM in
    pyproject.toml (since v2.4.0) made tomllib fail with
    Invalid statement (at line 1, column 1), breaking every PEP 517 frontend.
  • /cc-mem status gave a partial install a clean bill of health.
    _REQUIRED_PLUGIN_FILES omitted core/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. PreCompact writes
    memory/.pre_compact_attempt.json before 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.json records whether
    the trigger was auto or manual. 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/.gitignore migrates existing installs instead of only being
    created once. Every generator was guarded by if 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), and CHANGELOG.md backfilled 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

Choose a tag to compare

@skymanbp skymanbp released this 05 Aug 01:05

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.