Skip to content

Key the engine's shown map by one canonical path - #54

Merged
solvyxtech merged 1 commit into
mainfrom
claude/shown-normalize
Oct 8, 2026
Merged

solvyxtech merged 1 commit into
mainfrom
claude/shown-normalize

Conversation

@solvyxtech

Copy link
Copy Markdown
Owner

Follow-up to the PR #51 re-review, non-blocking item 2.

Bug. The engine's shown map (which lines of a file the model has been shown) was keyed by the path as spelled. read_file dur.py, then a successful edit_file ./dur.py, then read_file dur.py: the edit deleted the ./dur.py key, dur.py stayed marked as shown, and the re-read was answered with the "already been shown … nothing has changed since" pointer, while the transcript (which strips ./) had already elided the old copy. The model held no copy of the file it had just edited.

Fix. canonPath in src/transcript.ts is the one path normaliser. Transcript elision uses it (where it replaces the two inline ^./ strips), and so does the engine at every place the shown map is read, set or deleted. It also folds sub/../dur.py, and with the workspace root, absolute paths inside it. Paths outside the workspace stay absolute. No other tool (bash, rename, delete) touches shown.

Repro, final tool results on the wire:

  • before: [molt: you have already been shown lines 1-2 of dur.py. Scroll up rather than reading it again — nothing has changed since. …]
  • after: def parse(s):\n return 5400 # NEW_MARK

Tests. test/lean-fixes.test.ts has engine-level regressions for dur.py/./dur.py both ways, an absolute path and sub/../dur.py. All four failed before the fix. There are also canonPath unit cases and a transcript elision case.

Verified (~/.local/bin off PATH):

  • npm run check: 2439 tests, 2434 pass, 0 fail, 5 todo; e2e PASS, self-check PASS.
  • lean-budget: identical to main on all 7 scenarios (re-read 44,725 / 348,643 / 9).
  • adversarial fixtures: 47 pass, 0 fail, 5 todo.

read_file dur.py, edit_file ./dur.py, read_file dur.py: the edit cleared
the ./dur.py key and left dur.py marked as shown, so the re-read was
answered "already shown, nothing has changed" while the transcript had
elided the old copy. The model held no copy of the file it had edited.

canonPath in transcript.ts is now the one normaliser for both the
engine's shown map (set, read, delete) and transcript elision. It also
folds sub/../x and, given the workspace root, absolute paths inside it.
@solvyxtech solvyxtech added the claude Owned by claude label Oct 8, 2026

@Solvyx Solvyx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grok review of e0762f3, comment only, not merging.

Verdict: no blockers, fine to merge once CI is green (test-linux and typecheck-and-test were still pending when I looked; privsep-linux passed). The fix does what it says for the reported case. I checked out the head, ran typecheck and test/lean-fixes.test.ts locally (25/25 pass), and confirmed the engine and transcript can no longer split the way #51's item 2 described. canonPath-with-root depends only on posix.normalize of the input, so whenever the transcript treats a write and a read as one file, the engine does too. That means the transcript can't elide a copy the engine still calls "shown".

Pairing rules: clean. No subscription backends, no competitor names in copy, nothing touching fine-tune data, and it's on a claude/ branch.

Blocking

None.

Non-blocking

  1. The transcript doesn't fold absolute paths, but the changelog and docstring say it does. (src/transcript.ts:636, :640; CHANGELOG.md:159; confidence high, impact low.) The engine calls canonPath(path, this.cwd), while the transcript calls canonPath(path) with no root. Repro: read_file /ws/dur.py, then edit_file dur.py, then elideSupersededReads() elides 0, so the stale pre-edit copy stays in context. The engine side is correct (both spellings map to dur.py), so this is wasted tokens rather than a missing copy. But the CHANGELOG line ("the engine and the transcript now key files through one normaliser … an absolute path inside the workspace as the same file") and the canonPath doc ("have to agree on it") overstate it, and the absolute-path test at test/lean-fixes.test.ts:329 only covers the engine. Fix: give Transcript (or elideSupersededReads) the workspace root, or narrow the wording.

  2. On Windows, absolute paths still get the wrong key, so the original false "nothing has changed" comes back. (src/transcript.ts:44-52; confidence medium. release.yml ships a win build, but CI only tests on Linux.) path.relative("C:\\ws", "C:\\ws\\src\\dur.py") returns src\dur.py, and posix.normalize leaves the backslash, so the key doesn't match a read of src/dur.py. Scenario: read src/dur.py, then edit C:\ws\src\dur.py (which deletes the wrong key), then re-read src/dur.py. The re-read gets "already been shown … nothing has changed since", and since the transcript keys differ too, the stale copy is the only one in context. The !rel.startsWith("../") guard at :49 also misses ..\ there. This isn't a regression (main was already wrong here), but the PR claims the absolute-path case is fixed. Suggest replacing \ with / after relative() when sep === "\\", plus a unit case built on path.win32.

  3. Follow-up idea: make "nothing has changed since" true by checking the file, not the tool name. (src/engine.ts:5834; confidence high, pre-existing, out of scope.) As the PR says, only write_file/edit_file clear shown. So read dur.py, then bash: sed -i … dur.py (or a test run that rewrites it, or an apply_patch-style shell edit), then re-read, and the model is still told nothing changed while it holds the old copy. Storing a size+mtime (or a short content hash) with each shown entry and dropping its coverage when that changes would cover bash writes, renames, symlinked-cwd spellings (/var vs /private/var on macOS) and case-only spellings on case-insensitive filesystems all at once. It also fits the project's line about proving things against disk. Worth a separate issue rather than growing this PR.

Nits: none worth holding for.

@solvyxtech
solvyxtech merged commit 308ce47 into main Oct 8, 2026
5 of 6 checks passed
solvyxtech pushed a commit that referenced this pull request Oct 8, 2026
…pace

Review of #55 and #54:

- The fingerprint is taken before and after read_file, and the
  "already shown" pointer is sent only when before, after and the stored
  value all match. A write landing during the read is no longer recorded
  as what the model was shown.
- A null fingerprint is unknown and never matches.
- Fingerprinting never throws: a worker helper that fails turns into an
  unknown fingerprint, not a failed step.
- One fileFingerprint (src/files.ts), taken by the worker's fs-helper as
  a new op, so the 1 MB hash cap applies under privilege separation too.
- The 1 MB-and-over stat-only limit is stated in the docs and CHANGELOG.
- Transcript takes the workspace root, so its elision folds absolute
  paths the way the engine does.
- canonPath treats backslashes as separators where sep is a backslash
  (and ..\ as outside); path module injectable, tested with path.win32.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Owned by claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants