fix(edit-history): bounded image probe, linear path scan (review of #693)

- probeLocalPaths: every agent-printed candidate goes through
  probePathKind() first (no pastCap, bulk scan), sequentially; only a path
  that answered 'file' gets realpath + stat. A path on a mount that stopped
  answering no longer hangs the sync, a queued sync or remove().
- PATH_TOKEN_RE: branch 4 starts only at a token boundary (`:` excluded so
  URLs stay ignored), branch 1 stops at `:`. Linear in the line length
  (30,000 `#` took ~3 s before).
- Load older yields to the event loop between the files it parses.
- GET /edits answers unsupported without loading a store.
- CLAUDE.md paragraph trimmed to the rules; probe and regex rules added to
  architecture-invariants#edit-history-panel.

Tests: stall test through the real sync and probe with a stat that never
settles; time-budget tests with 30,000-character inputs and a 139-result
base64 chunk; route test asserts no store load for unsupported sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
RandalixandClaude Opus 5.5 committed 2026-11-03 09:46:23 +01:00
1 parent bf2a705705
commit de43657676
10 files changed
+229 -35

No files matched your search

+6
View File
@@ -436,8 +436,14 @@ export class EditHistorySync {
this.dropMissing(store, requests, res);
const wanted = new Set(requests.map((r) => r.relPath));
const candidates: ImageCandidate[] = [];
let parsed = false;
for (const f of res.files) {
if (!wanted.has(f.relPath)) continue;
// Each file is up to one budget (8 MiB) of synchronous parsing: let the server breathe
// between them. A store removed meanwhile ignores the rest (it is inert once removed).
if (parsed) await new Promise<void>((resolve) => setImmediate(resolve));
parsed = true;
if (slot.gone) break;
const r = store.applyOlder({
transcriptId: res.transcriptId,
source: f.relPath,
+8 -5
View File
@@ -105,13 +105,16 @@ export function registerEditHistoryRoutes(app: FastifyInstance, ctx: SessionPort
const { sync: doSync } = req.query as { sync?: string };
const { session, view } = sessionOf(id, req);
const why = unsupportedReason(view);
// Never read: answer an empty list without loading (or creating) a store for it.
if (why) {
return {
success: true,
data: { state: 'unsupported', reason: why, backfillTruncated: false, agents: {}, entries: [] },
};
}
// The sync starts in the same tick as the session check: a deletion right after it marks
// the running sync gone (EditHistorySync.remove), so nothing writes a deleted session.
const status: EditHistoryStatus = why
? { state: 'unsupported', reason: why }
: doSync === '1'
? await sync.sync(view)
: sync.getStatus(session.id);
const status: EditHistoryStatus = doSync === '1' ? await sync.sync(view) : sync.getStatus(session.id);
return { success: true, data: listOf(await sync.getStore(session.id), session.workingDir, status) };
});