Skip to content

fix: seventeen reliability fixes - #583

Merged
kevintseng merged 21 commits into
mainfrom
codex/rebuild-m4-integration
Oct 5, 2026
Merged

kevintseng merged 21 commits into
mainfrom
codex/rebuild-m4-integration

Conversation

@kevintseng

Copy link
Copy Markdown
Collaborator

Seventeen reliability fixes, each from its own branch, merged on top of main (which already has the credential redaction from #569 and the exact project names from #558). Every branch is kept as a parent of the merge commit.

What changes for users:

Where two fixes meet, both are kept: an import compares the redacted text when it decides nothing was added, and the guard check reports both an unreadable store and an invalid pattern.

npm run verify is green on this tree (316 test files, 5836 tests).

Closes #514
Closes #534
Closes #568
Closes #560
Closes #525
Closes #566
Closes #511
Closes #408
Closes #403
Closes #551
Closes #506
Closes #561
Closes #552
Closes #550
Closes #555
Closes #529
Closes #477

Two live sessions under one principal: a message sent to session A with
target_kind "session" was counted as waiting by session B too. B's
prompt hook reminded it, and its Stop hook blocked, but B is not allowed
to take the message in, so B could not stop cleanly.

Hooks that know their own session id now count a session-targeted
message only when it is addressed to that session or to a session it
replaced with /clear. A hook that cannot name its session keeps the
previous behaviour.

Refs #566
A recall whose query was exactly a memory's name could return that memory
below partial matches, because recency, frequency and confidence moved it
down. A small limit, or a size-capped response, could leave it out
entirely.

A memory whose name or title equals the query now comes first, ignoring
letter case (any script), surrounding spaces and Unicode normalisation
form. This applies to the keyword search, to the stored-text scan used
when the index is out of date, and to archived matches.

Refs #525
A memory tagged project:<name>, in a repository whose project id is
<name>~<hash>, was stored under a separate project that no briefing for
that repository reads, so later sessions never saw it.

remember (the MCP tool and memesh remember) now stores such a tag as the
project id when it names the call's own project, and reports the rewrite
in retagged. When the write updates or supersedes a memory already filed
under the plain name, the tag stays as written, as before; memesh kg
rename-project moves those. A tag naming any other project is unchanged.

Refs #511
Memories tagged project:<name>, in a graph that also holds the project
id project:<name>~<hash>, never reach that project's sessions, and
nothing said so.

memesh doctor now has a project-identity-split row that counts the
active memories only the plain name holds and points at memesh kg
rename-project, which lists the names and moves them (preview, then
--apply, which backs the database up first). The row names no project,
because doctor output is copied into public feedback reports, and it
says that a shared name does not prove one project. A failure reading
the tags is reported as its own warning, never as a failed database.

Refs #408
An observation holding a lone UTF-16 surrogate is stored as U+FFFD.
import --merge append compared the incoming text with what was stored, so
it never matched, and every re-run of the same file added the observation
again. The shared observation writer had the same gap.

Both now compare text in the form the database stores it.

Refs #561
A Codex host acceptance that carried the message body kept the waiting
reminder quiet in that thread for good. When the MeMesh host connection
that accepted it ended before the message was run (an older release's
codex exec run that exited, then resumed), the thread was never reminded.

Such an acceptance now silences the reminder only while the accepting
host connection is live. A notice-only acceptance never silenced it.

Refs #514
When the one-time search-index rebuild failed (for example on a full
disk), it waited 24 hours before retrying. Meanwhile the title backfill
removed index rows one by one with today's text splitting, which the old
index cannot take, so every open failed with "database disk image is
malformed". memesh reindex --fts and memesh doctor --fix, the ways out,
failed with it, and doctor suggested moving the database away.

The title backfill now waits until the index is rebuilt. The database
opens, recall finds memories by scanning their stored text, and memesh
reindex --fts rebuilds the index at once.

Refs #568
When the guards could not be read (for example a damaged database
file), the guard check before a Bash command recorded "no active guard
matched this command" and printed nothing. Every guard had stopped
working, and the record looked like a command no guard applies to.

It now records an error outcome whose reason starts with "guards
unreadable:" and says so on stderr. One guard with corrupt metadata is
still skipped while the others load, as before.

Refs #560
memesh doctor counted each hook's recorded errors and never read them,
so capture liveness showed as healthy while a hook was failing. A
capture hook that failed on every run was reported as one that decided
there was nothing to save.

A hook with an error among its five most recent runs (those its trigger
applied to) now makes the verdict PASS_WITH_CONCERNS, ahead of that
silence message. The row names the hook, how many of its recent runs
failed and the latest reason, quoted only when it is a reason the hooks
are known to record, with only the date of when it happened. The
SessionStart line names the hook too.

A run that records an error and then its normal outcome is still
counted as two runs.

Refs #555
memesh export-schema described the message tool without target_kind,
intended_session and fallback_to_principal, which the live tool accepts,
so a client built from the export could not send to one session or fall
back to its principal.

The export now carries all three. A test compares the field names of
every exported tool with the live MCP tool; project on remember, recall
and learn stays MCP-only, as HTTP does not take it.

Refs #534
A Claude Code transcript can open with long metadata lines, such as a
file-history snapshot of more than 100 KB. Transcript discovery looked
for the session's working directory only in the first 64 KB and 40
lines, so the project's own sessions were skipped without a message,
and a work package found nothing to read.

The working directory is now taken from the first line that records
one at the top level, wherever it is in the file. A "cwd" inside
message text or a tool input is not taken for it, and a later line
cannot claim the session for another project.

Refs #552
When another process held the database's write lock, a Claude Code
session start (the daily noise compression) or a Stop (the note
ingestion) waited up to 30 s for it. That ran past the 10 s hook
limit, so Claude Code ended the hook with a timeout.

Both now give up after one 2 s wait, the wait the other hooks use. This
also holds right after an upgrade, when one-time migrations are still
pending and would otherwise each wait in turn. The skipped step runs on
the next session start or Stop; a skipped note ingestion is recorded as
an error.

Refs #506
The one-time rename of `lesson` and `mistake` memories to
`lesson_learned` ran only once. A memory written with the old type after
that, for example by an older plugin version still running in another
session, kept it for good, and anything filtering or counting by
`lesson_learned` missed it.

When such a memory exists, the rename now runs again the next time the
database is opened.

Refs #551
Every `message` response gave `created_at` in SQLite's form,
`2026-09-21 20:35:59`: UTC, but with no `T` and no `Z`. A client that
parsed it read it as local time, hours off.

Sent messages, events, fetched payloads, receipts and facts now give
`2026-09-21T20:35:59Z`, and a native acceptance's `accepted_at` does
too. `memesh message storage report` and `prune` read a `--cutoff` in
SQLite's form as UTC instead of local time. The format is documented in
the API reference.

Refs #403
A transcript prepare that found nothing returned only none_available, so
a user could not tell an empty project from sessions that were skipped.

It now carries skipped_sessions, a count per reason: no recorded cwd,
another project's cwd, too large, unreadable, the scan limits, already
proposed, changed since the scan, no visible turns. It names no session
and carries no content.

Refs #552
Importing the same file again with --merge append reported every memory
it named as appended and marked it untrusted, even when the file added
no text to it, so restoring the same backup twice demoted your own
memories.

An entry that adds nothing (no new observation or tag, no title change,
no namespace move, and not an archived memory being restored) is now
counted as skipped and left as it is. Tags and the title are compared in
the form the database stores them, as observations are.

Refs #561
memesh import --notes exited 0 even when every note file it found was
refused, so a script checking the exit code took a run that imported
nothing for a success.

It now exits 1 in that case, still printing the refusals; --json still
writes the full result. A partial import, or a re-run where every note
is unchanged, still exits 0.

Refs #550
…ey start

- The stand-in npm and codex that qa:live-journey, test:packaged and
  test:packaged:upgrade put on PATH now carry their own module type, so a
  package.json with "type": "module" above the temporary directory no longer
  breaks them with "require is not defined in ES module scope".
- A live-journey report from a clean checkout no longer says dist_stale: true
  because git switch left the source newer than the unchanged committed
  build. Only a dirty tree is judged by file times. A failed git status now
  stops the run instead of reading as a clean tree.

Refs #477
…e --project

- `memesh kg backfill-relations` now writes nothing unless given --apply.
  The preview reads through a read-only handle, so it does not even decay
  or record attempted orphans. --apply backs up the database to backups/
  beside it first, then writes, and prints how to restore it. --apply with
  --dry-run is refused.
- With --project X, both ends of every proposed relation carry X. Before,
  the tag, session and name-token rules picked partners from every project.
- A memory tagged with several projects is anchored in the project asked
  for, or, without --project, in each of its projects up to
  --max-per-source, instead of whichever tag was read last.

Refs #529
… cache

- A memory in two projects whose newest anchor is the same entity in both
  is now proposed that link once, so the preview no longer lists it twice
  and it no longer takes two of --max-per-source.
- The preview's --reset-idempotency path and the --json output of --apply
  are described as they behave.

Refs #529
Each branch fixes one reported issue; this merge combines them on top of
the credential redaction (#569) and exact project name (#558) changes.

- #514 remind a Codex thread again once the host that accepted it ended
- #534 export-schema describes the message tool's session fields
- #568 open a database whose index rebuild is waiting to retry
- #560 the Bash guard check reports a guard store it cannot read
- #525 recall returns an exact name or title match first
- #566 a session message reminds only the session it is for
- #511 a plain-name tag for your own project is stored as the project id
- #408 doctor warns when memories sit under a plain project name
- #403 message timestamps are ISO 8601 UTC
- #551 a lesson written with the old type is renamed after the rename
- #506 hooks stop waiting 30 s for a held database lock
- #561 an import that adds nothing leaves the memory alone
- #552 work_package finds a session's recorded directory anywhere in the
  transcript and says which sessions it left out and why
- #550 import --notes exits 1 when every note is refused
- #555 doctor reports a hook that keeps failing, counting runs
- #529 kg backfill-relations previews by default and backs up before --apply
- #477 release checks no longer fail on environment traps

Where two fixes meet, both are kept: an import compares the redacted text
when it decides nothing was added, and the guard check reports both an
unreadable store and an invalid pattern.
@kevintseng
kevintseng merged commit b8124ec into main Oct 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment