Repository navigation
v0.5.0-ev.1
Pre-releaseKnown issue: on a host with many sync repos (seen with 36; the threshold depends on the host), the per-prompt changes hook (claude-memsync changes --hook, which the memory-notices plugin runs) can miss its 1.5 s deadline on every call. It records its progress only when it finishes, so it never catches up: each prompt waits about 1.5 s and no change notice is shown. Syncing is not affected. On such hosts, keep the memory-notices plugin disabled until the fix in the next release. To check a host, time one call: time (echo '{"session_id":"check","cwd":"/","hook_event_name":"UserPromptSubmit"}' | claude-memsync changes --hook). If it takes about 1.5 s and prints "hook deadline exceeded" to stderr, the host is affected. A call that returns at once may have found another hook running, so run it twice.
Upgrading from v0.4.0-ev.1:
- Routing now fails closed. A project folder whose name passes through a directory with more than 256 entries (on macOS, the per-user temp dir) is synced only when what lies below that directory is a single name without
-. Anything else is left unresolved rather than guessed, so some folders under very large directories now show as "ambiguous, not synced" inclaude-memsync statusinstead of syncing. To sync such a folder, pin it with afolderrule placed before thepathandremoterules inroutes.json, then restart the daemon. - A
privateOnlyrepo needs a token the daemon can use. With no token source at all, orrecheckTokenCmd: falsewith only a command, the repo is held at daemon start. A configured token source that gives no token (a failing command, an empty token file, an unset variable) now also holds the repo; inv0.4.0-ev.1such a repo started anyway. Hosts bootstrapped without--check-appand without a token file therefore hold everyprivateOnlyrepo until a check App or token file is configured (for a token file: create it, then re-run bootstrap, which writesgithubTokenFileinto eachconfig.jsononly if the file exists) and the service is restarted (claude-memsync stop, thenclaude-memsync start).claude-memsync statusshows the hold. A hold outlives switchingprivateOnlyoff; status then gives the manual release step (stop the service, delete the repo's.state/loop-failed, start it). - A folder that routes to deny now has none of its memory files mirrored, and
related suggestnever lists or opens its transcripts. - Each release now also carries four budget archives,
claude-utils-<tag>-budget-<os>-<arch>.tar.gz(linux and darwin, amd64 and arm64), covered bySHA256SUMS. Unpack the matching product archive, then run the test binaries in the budget archive withMEMSYNC_BUDGET_BIN_DIR=<product dir> MEMSYNC_BUDGET_TAG=<tag> ./claude-memsync-budget.test -test.run '^TestBudgetB' -test.v -test.timeout 900s(and the same for./route-budget.test). No Go or checkout is needed. The archive'sREADMEanddocs/releases.mdhave the details.
Changelog:
- A denied folder is fully contained: a folder that routes to deny has none of its memory files mirrored, and
related suggest(and the daemon's suggestion refresh) never lists or opens its transcripts and counts it toward no signal; tests pin both, including a deny pin listed ahead of a broader rule. - Auto maintenance no longer escapes a deadline: after a commit, fetch, merge or rebase git may start
maintenance run --auto --detach(or a detachedgc --auto), which leaves the process group via setsid and survives proc's group kill andrelated fetch's 60 s timeout. Everygitthat can start it (the sync loops' and CLI'sgitwtcalls, andrelated fetch) now carries-c gc.auto=0 -c maintenance.auto=false, and apushto a remote that is a local path orfile://URL also passes--receive-packwithreceive.autogc=false(a local transport drops-candGIT_CONFIG_*before receive-pack; a push to an ssh or https remote runs receive-pack on the server and is not ours to configure). To keep sync dirs from growing without bound, each daemon loop runsgit gc --autoitself at most once per 24 hours per sync dir (recorded in.state/maintained, so a restart does not repeat it; foreground,gc.autoDetach=false; one at a time across all loops; stopped with the daemon's context and killed with its process group at the daemon's stop or after 10 minutes; skipped while the loop is held; the changes hook never runs it). Read clones are not housekept. Upstream candidate. - The interrupt guard's
stop()waits for its goroutine:guardChildren's stop func now joins the goroutine (idempotent; it does not wait for one already exiting the process or swallowing a signal the sync flow owns), so nothing of the guard reads package state after it returns. Fixes a data race seen inTestInterruptDuringSyncFlow_SecondSignalKillsGitAndExits130under-race; harmless in production. Upstream candidate. - privateOnly needs a token the daemon can use: with no token source at all (or
recheckTokenCmd: falsewith only a command) aprivateOnlyrepo is now held at daemon start instead of starting on the init-time check. Release note: hosts bootstrapped without--check-appand without a token file now hold every privateOnly repo until a check App or token file is configured (for a token file: create it, then re-run bootstrap, which writesgithubTokenFileinto eachconfig.jsononly if the file exists) and the service restarted (claude-memsync stop, thenclaude-memsync start(they runlaunchctlon macOS andsystemctl --useron Linux));claude-memsync statusshows it. A hold also outlives switchingprivateOnlyoff: status saysheld, released by hand only: … held from an earlier runand gives the manual release step (stop the service, delete the repo's.state/loop-failed, start it). Fork-only. - Private-only check holds a repo, not the daemon: a repo whose private-only check fails at daemon start (not private, or the check cannot run, for example a GitHub App with no installation) is held: its loop does not start,
statusshowswaiting for the private-only check, re-checking with backoff, and the check is retried from 10 s, doubling to every 30 min; the loop starts, with its push guard, only after a passing check. A configured token source that gives no token (failing command, empty file) now fails the check instead of reading as "no token configured", and a push guard built then blocks pushes until the source works and the API says private; a held repo is released only by an API answer, never by a skipped check. Other repos sync meanwhile, and the daemon no longer exits, so a service manager no longer restarts it every few seconds. Upstream candidate. - Agents hear about a held repo:
changes --hookleads its output with one line per repo the private-only check holds (memsync: repo <name> is HELD by the private-only check since <time>: <reason>. …), once per session and hold, from local state files only, and also when the hook's deadline defers the change notices; text from state files is cut to one line with control and invisible format characters removed, the hold record and the per-session marker are read bounded and as regular files only (a FIFO or symlink is skipped without waiting), and the marker is written without following links. Upstream candidate. - A performance budget for the hook and the daemon, enforced by tests:
go test -tags budgetmeasures the changes hook (p95 under 200 ms over 20 runs), the daemon's start-up pass (under 10 s), idle CPU (under 1% of a core) and resident memory (under 64 MB) on a synthetic tree with a 10k-entry temp dir and 1,000 project folders, plus counted gates that load cannot hide: directory reads and bytes stay flat as unrelated directories grow, at most one of 20 overlapping hooks does the work, a hunggitnever outlives the hook's deadline, a hook with nothing new starts nogitprocess, and start-up spawns at most onegitper existing project directory. Timing gates print the load average and report INVALID above 8 instead of passing or failing. On an Apple-silicon Mac the start-up pass went from 18.7 s to 2.7 s and the hook p95 from 474 ms to 74 ms. Upstream candidate. - Resolution reads
.gitdirectly, and asksgiton any doubt: whether a directory is inside a work tree, its common dir and its HEAD come from the files (a.gitdirectory or agitdir:file withcommondir, loose and packed refs), withgitas the fallback for anything unusual (symlinked paths, submodules, bare orcore.worktreerepos, includes, reftable, unreadable or corrupt files, another owner). A directory is only ever called "not a repo" aftergitconfirms it, once per routing generation; a missing directory costs nothing. Start-up resolves folders side by side, at most 4gitchildren at a time, each with a 10 s timeout, and the daemon's loops start after at most 5 s while slow folders finish in the background. Upstream candidate. - Routing fails closed under a directory too large to list: a project folder whose name passes through a directory with more than 256 entries (on macOS, the per-user temp dir) resolves only when the part below it is a single name without
-; anything else is left unresolved, never guessed, because-may stand for/,.,_, a space or a character the folder name cannot spell, and a wrong guess would sync one project's notes into another project's repo. Such a folder is logged once and listed byclaude-memsync statusas "ambiguous, not synced" when it looks live (modified within 14 days). A path that resolves in a huge directory keeps its on-disk spelling on case-folding hosts. Behaviour change. Upstream candidate. - The changes hook runs one at a time and never leaves
gitbehind: overlapping hooks skip at once when another holds the hook lock in the first repo's state dir, and at the hook's deadline everygitit started (including related-repo calls) is killed with its process group; expiries are logged at most once a minute with a count. Interactive commands kill theirgitchildren on Ctrl-C or SIGTERM (a command that is stopping the service keeps its "first signal lets the change finish" behaviour). Upstream candidate. - The budget gates run against the shipped binaries: each release now also carries
claude-utils-<tag>-budget-<os>-<arch>.tar.gz(linux and darwin) with the gates as test binaries, covered bySHA256SUMS. WithMEMSYNC_BUDGET_BIN_DIRandMEMSYNC_BUDGET_TAGset they test the release's ownclaude-memsyncandclaude-memmerge(version checked, HOME isolated) with no Go and no checkout. Every archive's contents, including the printable strings of every binary, are now scanned with gitleaks beforeSHA256SUMSis written, after a control that proves the scan can see inside binaries. Seedocs/releases.md. Fork-only. - Routing no longer re-reads a huge directory for every stale folder: finding the directory a project folder name stands for read every directory along its path, so on a Mac with hundreds of folders for deleted
$TMPDIRcheckouts the daemon's startup reconcile re-read the same very large temp directory for each one (about 10 s apiece, in parallel across the loops) and never finished. The router now reads each directory once per routing generation (untilroutes.jsonreloads or the pull tick), re-reading one only when its modification time or size changes, shares the read between concurrent callers, and walks once per folder instead of twice. May also help the changes hook overrunning its deadline (#12). Upstream candidate.
Verify every archive against SHA256SUMS before extracting; see docs/releases.md.