From 321268754a73b6b78be52300d35f2f2c701aac8b Mon Sep 17 00:00:00 2001 From: Randalix Date: Fri, 23 Oct 2026 15:00:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(edit-history):=20review=20round=202=20?= =?UTF-8?q?=E2=80=94=20whole=20lines=20around=20skipped=20ones,=20full-bud?= =?UTF-8?q?get=20reads,=20orphan=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Forward: after a line longer than the budget, the next line is read from its start instead of being skipped with it when it crosses the chunk end. - Backward: the line before a skipped long line is read whole; a floor that turned out to be a line start no longer stalls. - A chunk the shared forward budget cut short is not applied (it read as one over-long line) but read in a later pass, so a first open of a long main transcript reaches the subagents in the same sync. - Load older reads a full budget block of up to three transcripts per click, taking turns, instead of splitting one budget (a normal Write longer than the share was skipped for good). - A subagent transcript that vanished loses its cursor, so Load older does not stay on forever. - POST /api/cleanup-state forgets the history of every record it drops; a startup sweep deletes edit-history files no session or record owns and stale tmp files of other processes. - load() rebuilds entries from known fields (string diff lines, per-edit cap re-applied, ids counted against the byte cap), bounds pending Bash fields and agent ids, and does not parse a file over 32 MiB. - No re-read after Codeman's own keep-alive turn. - Tests: regression tests for each of the above (they fail on the previous commit); the remove-during-write test now meets the write under way; the sync and route tests remove their stores before the temp dir, so no late save recreates it in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 2 +- docs/api-reference.md | 6 +- docs/architecture-invariants.md | 6 +- docs/wiki/Working-With-Files.md | 12 +- src/edit-history-parse.ts | 34 +++-- src/edit-history-store.ts | 186 ++++++++++++++++++------ src/web/edit-history-sync.ts | 71 +++++++-- src/web/public/app.js | 5 +- src/web/routes/system-routes.ts | 1 + src/web/server.ts | 9 +- test/edit-history-store.test.ts | 142 ++++++++++++++++++ test/edit-history-sync.test.ts | 120 ++++++++++++++- test/edit-history.browser.test.ts | 4 +- test/routes/edit-history-routes.test.ts | 18 ++- test/routes/system-routes.test.ts | 13 ++ 15 files changed, 540 insertions(+), 89 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4ff60e9f..93e38db0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -232,7 +232,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph **Git status indicator** (`showGitStatus`, per-device, default OFF; `src/git-workspace-status.ts`, `routes/git-status-routes.ts`, `git-status-ui.js`): the bottom-bar indicator and its panel read `GET /api/sessions/:id/git-status` and `/git-diff`, read-only and offline (it never fetches and never writes). ⚠️ git never runs on a repository at or inside a Docker case workspace (walk-up, scan and diff alike, and a cached repository list is re-checked against the CURRENT Docker roots), since a container could plant a clean filter that runs on the host; remote and Docker sessions answer `unsupported`. ⚠️ `git-diff` takes `repo`/`path`/`kind` only as keys matched against the workspace's own repository list (`findWorkspaceRepo()`) and that repository's current status, never as paths. ⚠️ Keep `--no-optional-locks`, `core.fsmonitor=false` and `log.showSignature=false` on every call and `--no-ext-diff --no-textconv` on diffs; clean filters still run, which is why the Docker rule exists. ⚠️ An enclosing repository at `$HOME` or above is ignored (`isUnrelatedAncestor()`), and is identified by one cached `rev-parse` before any full status runs. Everything git supplies renders via `textContent`. Tests: `test/git-workspace-status.test.ts`, `test/routes/git-status-routes.test.ts`, and `test/git-status.browser.test.ts` (browser suite, not in the gate). -**Edit history panel** (`showEditHistory`, per-device, default OFF; `src/edit-history-parse.ts`, `edit-history-store.ts`, `edit-history-local-source.ts`, `web/edit-history-sync.ts`, `routes/edit-history-routes.ts`, `edit-history-ui.js`): which files a LOCAL claude session edited (main agent and subagents, labelled), each edit's diff, and images a Bash call produced. Read from the session's transcripts by byte offset, never from git. `GET /api/sessions/:id/edits[?sync=1]`, `GET …/edits/:entryId` (unified diff), `POST …/edits/older`. ⚠️ **Reads only on request:** the panel sends `?sync=1` when it opens and, while it is open on that session, after an edit-capable `transcript:tool_end` or the session's `session:idle` (debounced; `transcript:complete` never fires for interactive claude, its transcript has no `result` line). The server never syncs on its own: no hook, no timer, no second watcher; `server.ts` hands the reader to the routes and only calls `remove` (via `forgetEditHistory`) and `flushAll` itself (wiring-guard test). The main transcript is the path the session's `TranscriptWatcher` follows (reported by the hooks), else `.jsonl` under the session's `CLAUDE_CONFIG_DIR` override or the server's (`resolveClaudeTranscript`); a config dir set outside Codeman is found with the next hook, until then `unavailable` / `no-transcript`. ⚠️ The store holds every diff, secrets included: `dataPath('edit-history')/.json`, 0600 via tmp + rename, capped per session (500 entries, 4 MiB diff text; damaged files are re-bounded on load), deleted with the session's persisted record (`forgetEditHistory`: kill, persisted-only DELETE, stale cleanup; a pinned killed session keeps it), blocked in `isSensitivePath`, never fed to `/api/search`. Images use the existing file surfaces only: thumbnails via `file-raw` inside the workspace; an image outside it has no thumbnail and opens in the file preview (attachment route, `notify: false`); no route here serves a file. A Bash image counts only when the file's mtime lies in the call's window (±2 s) and no entry already covers that file version. Remote, Docker and non-claude sessions answer `unsupported`. Tests: `test/edit-history-*.test.ts`, `test/routes/edit-history-routes.test.ts`, `test/edit-history.browser.test.ts` (browser suite). +**Edit history panel** (`showEditHistory`, per-device, default OFF; `src/edit-history-parse.ts`, `edit-history-store.ts`, `edit-history-local-source.ts`, `web/edit-history-sync.ts`, `routes/edit-history-routes.ts`, `edit-history-ui.js`): which files a LOCAL claude session edited (main agent and subagents, labelled), each edit's diff, and images a Bash call produced. Read from the session's transcripts by byte offset, never from git. `GET /api/sessions/:id/edits[?sync=1]`, `GET …/edits/:entryId` (unified diff), `POST …/edits/older`. ⚠️ **Reads only on request:** the panel sends `?sync=1` when it opens and, while it is open on that session, after an edit-capable `transcript:tool_end` or the session's `session:idle` (debounced; `transcript:complete` never fires for interactive claude, its transcript has no `result` line). The server never syncs on its own: no hook, no timer, no second watcher; `server.ts` hands the reader to the routes and only calls `remove` (via `forgetEditHistory`), `sweep` (startup) and `flushAll` itself (wiring-guard test). The main transcript is the path the session's `TranscriptWatcher` follows (reported by the hooks), else `.jsonl` under the session's `CLAUDE_CONFIG_DIR` override or the server's (`resolveClaudeTranscript`); a config dir set outside Codeman is found with the next hook, until then `unavailable` / `no-transcript`. ⚠️ The store holds every diff, secrets included: `dataPath('edit-history')/.json`, 0600 via tmp + rename, capped per session (500 entries, 4 MiB diff text; damaged files are re-bounded on load), deleted with the session's persisted record (`forgetEditHistory`: kill, persisted-only DELETE, stale cleanup at startup and via `POST /api/cleanup-state`; a pinned killed session keeps it; the startup `sweep` removes files no session owns), blocked in `isSensitivePath`, never fed to `/api/search`. Images use the existing file surfaces only: thumbnails via `file-raw` inside the workspace; an image outside it has no thumbnail and opens in the file preview (attachment route, `notify: false`); no route here serves a file. A Bash image counts only when the file's mtime lies in the call's window (±2 s) and no entry already covers that file version. Remote, Docker and non-claude sessions answer `unsupported`. Tests: `test/edit-history-*.test.ts`, `test/routes/edit-history-routes.test.ts`, `test/edit-history.browser.test.ts` (browser suite). **Raw file bodies are streamed and range-aware**: `file-raw`, the attachments `/raw` route, `GET /api/download` and the rendered-HTML route `GET /html-view//...` share `sendFileBody()`, advertise `Accept-Ranges: bytes` and answer `Range` with `206` + `Content-Range` (single-range, parser in `src/web/http-range.ts`); without it `