From dcdeaab98de56f248fa13c0235c169cd650fdb12 Mon Sep 17 00:00:00 2001 From: Randalix Date: Fri, 23 Oct 2026 14:30:05 +0200 Subject: [PATCH] =?UTF-8?q?fix(edit-history):=20review=20round=201=20?= =?UTF-8?q?=E2=80=94=20record-bound=20deletion,=20load-time=20bounds,=20ol?= =?UTF-8?q?der=20reads=20per=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The history is deleted when the session's persisted record goes (forgetEditHistory): kill via cleanupSession (a pinned record is only demoted and keeps it), the persisted-only DELETE, and stale cleanup at startup. After remove() neither a sync in flight nor a later request writes the file again; writes are serialized. - load() re-bounds a damaged file: cursors, pending Bash calls (MAX_PENDING_BASH) and subagent labels (MAX_AGENTS, oldest unused dropped first). - Load older reads one block of every transcript, budget split between them, and only the files it asked for; a block short of budget reads its end. A line longer than the budget is skipped going back as well (floorMidLine). - Concurrent first callers share one store; a forward request folded with an older one still reads forward. - Images outside the workspace get no thumbnail (one attachment id per render); a click opens the shared preview, which registers once with notify: false. - Docs: per-edit diff cap, deletion rule, load-time bounds, thumbnails, follow triggers. Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 2 +- docs/api-reference.md | 6 +- docs/architecture-invariants.md | 8 +- docs/wiki/Working-With-Files.md | 21 +-- src/edit-history-local-source.ts | 22 ++- src/edit-history-store.ts | 182 ++++++++++++++++++++----- src/web/edit-history-sync.ts | 100 ++++++++++---- src/web/ports/session-port.ts | 5 + src/web/public/edit-history-ui.js | 59 +++----- src/web/routes/session-routes.ts | 3 +- src/web/server.ts | 22 +-- test/edit-history-local-source.test.ts | 18 +++ test/edit-history-store.test.ts | 67 ++++++++- test/edit-history-sync.test.ts | 101 +++++++++++++- test/edit-history.browser.test.ts | 39 ++++-- test/mocks/mock-route-context.ts | 1 + test/routes/session-routes.test.ts | 10 ++ 17 files changed, 508 insertions(+), 158 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 15dd8e25..e7024dec 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 a `transcript:complete` (debounced). 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`/`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), deleted with the session, blocked in `isSensitivePath`, never fed to `/api/search`. Images use the existing file surfaces only (workspace `file-raw`; outside it the attachment route with `notify: false` and its extension allowlist); 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 a `transcript:complete` (debounced). 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). **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 `