Skip to content

v0.9.4

Choose a tag to compare

@wdhwg001 wdhwg001 released this 01 Sep 23:46
· 79 commits to main since this release

Bash reads become reads and bash writes become writes in recover. Plan binding now matches Claude Code's own rule, list names a forked clone's origin, status shows what is actually moving, and regex searches with metacharacters stop paying full price.

recover: bash content anchors

A session steered toward shell-based file editing used to starve recover of anchors. Every bash write was a content-less boundary, and a file written only through the shell had no recoverable history at all. The deterministic subset of shell commands now replays as first-class content. Every admission gate errs toward refusing, so a wrong anchor never enters the replay.

Writes anchor per segment, which admits the dominant real shape: write the file with a heredoc, then run it, in one compound command. The admissible forms are a quoted-delimiter heredoc through cat or tee (the body sits byte-verbatim in the transcript), a literal echo or printf, and truncate -s 0. An unquoted delimiter is accepted only when the body is free of expansions. A compound command must also produce a clean result echo, meaning empty stderr and no interruption. Only the last segment owns the exit code, and a failing write always complains on stderr, so a clean echo is real evidence. If any other part of the command touches the same resolved path, the anchor is refused.

Reads anchor only as single simple commands (cat, head -n N, sed -n 'A,Bp') under a completeness gate. A window that starts at line 1 and hits EOF counts as the whole file. A byte-known >> append is placed only onto a buffer that is already complete and newline-terminated. Anywhere else it is disclosed as a bash_append_unplaced boundary.

Some forms stay out on purpose. tail cannot be placed in a line-keyed buffer, sed -i yielded zero literal cases in measurement, and variable targets, interpreter heredocs, ssh heredocs, and here-strings are all refused. Coverage reports count bash-read-anchor and bash-write-anchor, and each replayed segment names its provenance as bash-heredoc, bash-cat, or bash-write.

On a real shell-first transcript, a python tool written through the heredoc-then-run shape went from no recoverable history to all 33 lines recovered verbatim.

plan: a correctness fix

csift bound a session to its plan only through the plan_mode attachment. Claude Code itself binds by the first record carrying a valid slug. On a forked clone, where attachments are stripped but slug records survive, csift answered "no plan" for a session whose plan Claude Code will re-inject. Both binding laws now apply, in that precedence order. Rows carry binding_source (plan_mode or slug-only) and a minted_at_compaction flag, and the plansDirectory setting from settings.json is honored.

list: clone lineage

A transcript whose first timestamped record is a compaction boundary was minted by copying another session at that compaction. That is the signature of a background-job fork: uuids preserved, timestamps that predate the file, slug stripped. The rule produced zero false positives on a real 61-file project directory. The list row now annotates the fork and names the origin session, and JSON gains is_clone, clone_of, and clone_boundary_uuid. One corollary is documented alongside. Until you scope it away, a clone double-counts its inherited records on every spanning surface.

status: live lanes and open tasks, not floods

Child lanes gain a generating state. A lane whose newest record is younger than 300 seconds, and whose last assistant stop_reason is not end_turn, is treated as mid-generation. The threshold comes from measurement. Record gaps inside a live lane reach 295 seconds at p99.9, while dead lanes sit 31+ hours out. The old 15-second mtime window misread one lane in 17, and stop_reason alone would have called 73% of dead lanes live, so the two conditions gate together and the mtime-based active state is retired.

Settled lanes now fold to a single count, so live work stays visible instead of drowning under hundreds of finished rows. A new tasks section reads the harness task list, printing open tasks with their blockers (in_progress first) and folding completed ones to a count.

search: required-needle prefilter

A pattern with metacharacters now derives a necessity-only literal set from its parsed structure. An alternation contributes a gate only when every branch demands a safe needle, so the gate can never drop a true match. The motivating query, TodoWrite.*legacy|legacy.*TodoWrite over every session, runs 1.70x faster in wall time with about half the CPU, and the output is byte-identical. A plain pattern containing spaces now anchors on its longest whitespace-free run.

files + recover: the file-history snapshot instrument

Claude Code rewrites its settings files in-process, with no tool record, on /model, /config, and plugin toggles. Measured on the corpus, half of all settings.json mutations are invisible to the tool stream. One such write silently deleted a freshly-edited key while recover replayed the file with the key still present, calling a state that never existed on disk "100% complete". Claude Code's own per-prompt snapshot version sequence is now read as an instrument against exactly this.

At every version change, recover compares the replayed buffer to mtime-verified snapshot content and rebases on divergence, recording an authoritative external_write boundary. A version jump with no tool write in between discloses the same boundary even when the content is gone. files emits external write timeline rows carrying the version transition and the uncovered interval. The scope is hard-limited to the settings family (.claude/settings*.json) on purpose. The tracked set spans 1701 corpus paths against 11 settings-family ones, and reporting all of them would flood every timeline. The version counter also resets mid-session (148 real cases), which makes @vN store names collide across a reset, so versions are segmented into generations and unverified blobs are refused.

search: role selectors speak LLM-visibility

Putting user.unsent under -t user broke a 0.7-era consumer. A superseded draft 12 seconds before the real submit poisoned a last-human-touch hook. Selectors now come in three forms. A bare role like -t user selects only the role's LLM-visible leaves. A glob like -t 'user.*' selects everything under the prefix. Intermediate prefixes and full leaf paths keep their full sets, so -t harness.compaction still reaches the boundary. Exactly two leaves are invisible, and both were verified with instruments. user.unsent is invisible because Claude Code's own preservedMessages accounting excludes every draft uuid. harness.compaction.boundary is a metrics-only record with no message at all. The practical effect is that -t user restores the 0.7 contract.

SKILL and docs

The staleness guard is now mechanical. Run csift --version at first use after any compaction, and a mismatch means the in-context copy is a stale echo. The frontmatter description gains the corpus-first trigger, which means live sessions need /reload-skills once. Two stale claims are fixed, and SPEC carries the full v0.9.4 ledger.

Verification

The full suite is green on all 8 release targets, at 1001 unit and 521 end-to-end tests on the unix lanes. Line coverage is 95.5%, above the prior baseline. Scoped mutation testing ran over every touched file, and every surviving mutant was either killed by a differential test or classified as a documented equivalent.

The gate work caught two real risks before release. cat <<< words was briefly classified as a file read, and here-strings now refuse. A boundary record that merely references a uuid through logicalParentUuid could have joined as a clone origin, and now cannot. The Windows lanes caught a separator-naive test assertion, with product behavior correct on both platforms. All binaries were built with path-privacy remapping and audited for path leaks.