Commit graph

13 commits

Author SHA1 Message Date
iamtoruk
39075edd50 parser: keep promptSource on lines over 32 KB
parseLargeJsonl dropped promptSource for exactly the lines SDK-generated
prompts live on, so the recurring-context detector regex-scanned the ends of
the raw line for it. Add the field to LARGE_ROOT_FIELDS (tiny scalar,
add-only, isSidechain already there) and delete the workaround: it read only
2 KB from each end, so a flag further in was missed.

No cache change: optimize scans the raw JSONL each run, so promptSource never
has to persist on CachedFile.

Fixes #1030. With #994 this closes #1023.
2026-08-18 12:00:29 -07:00
iamtoruk
29b531fced optimize: keep sidechain tool calls in the junk-read and read:edit signals
Only duplicate-reads has a structural reason to skip them: a subagent
starts on a fresh context, so re-reading what its parent read is a
necessary read, not a repeat. Reading node_modules or editing without
reading is the same waste whoever does it, and the CLAUDE.md rule both
findings suggest binds subagents too - filtering them there discarded
most of the evidence on a subagent-heavy corpus.
2026-08-18 09:38:11 -07:00
iamtoruk
eb17a03610 merge origin/main into pr994 2026-08-18 08:50:09 -07:00
iamtoruk
d5d42e4d26 fix(optimize): disambiguate connector namespaces against local MCP config
The prefix alone could not tell a claude.ai connector from a local MCP
server that happens to be named claude_ai_*, so the latter lost its
removal command and its apply plan. A namespace is now a connector only
when no readable local config claims that exact name; localMcpServerNames
supplies the set from the same files the remove plan edits (settings.json,
.mcp.json, and ~/.claude.json top-level plus per-project mcpServers).

A prefixed name that local config does own stays local: normal removal
command, apply plan, class fix, full savings attribution. Because the
transcript still cannot rule out a same-name connector, the finding adds a
manual note about it instead of asserting the server is one. Config that
cannot be read contributes no names, which leaves every prefixed namespace
on the conservative connector path.
2026-08-18 08:39:53 -07:00
iamtoruk
e3a55cdf20 optimize: count only human pastes as recurring context
An SDK session's opening prompt and a subagent's task prompt are written by
a program: they repeat by design and have no home in CLAUDE.md. Both are
flagged on the entry, but a user entry over the parser's large-line
threshold comes back without its root flags - routine for generated prompts,
which are exactly the long ones - so the markers are read off the ends of
the raw line, where the fields sit either side of the oversized message.
2026-08-18 03:27:10 -07:00
iamtoruk
8d8848d805 optimize: detect recurring context pasted at the start of sessions
Groups sessions by their opening block (whitespace/ANSI-normalized, hashed
over the first 2 KB) and flags a block of at least 1.5 KB that opens five or
more sessions. Class nudge: CodeBurn will not move the user's own text into
CLAUDE.md, so the fix asks Claude to give the block a permanent home. Only
the repeats count as savings, sized from the block's bytes because provider
usage is per API call and cannot isolate the paste. The opener comes from
the session scan that already runs, so nothing extra is read.
2026-08-18 03:13:40 -07:00
Dongmin,Yu
cbb09d0170
fix(optimize): scope transcript-derived findings to the selected provider (#1003)
* fix(optimize): scope transcript-derived findings to the selected provider

scanSessions() always ran discoverAllSessions('claude'), so every finding it
feeds was computed from Claude transcripts regardless of --provider, while the
header (sessions, calls, cost) came from the already-filtered projects. Under
--provider codex the two described different providers, and the Claude-derived
numbers read as the selected provider's.

Skip the scan when the filter excludes Claude, and skip the detectors it feeds
rather than handing them an empty scan: emptiness reads as "never invoked", so
an empty scan turned every skill, agent and command into a reported ghost.
Findings derived from projects (MCP tool coverage, capability reliability,
low-worth sessions, context bloat, outliers, model recommendations) already
filter correctly and still run.

The result cache key now carries the provider, since the provider decides
whether the scan runs at all.

* fix(optimize): thread the provider through the apply, aggregator and TUI scans

The previous commit fixed one of four scanAndDetect callers. The other three
carry a provider filter and dropped it:

- act/optimize-apply.ts: `optimize --apply` branches in main.ts before the
  code that threads it, so a Codex-scoped run planned applies off Claude
  findings. This is the worst of the three because `unused-skills` is
  appliable and its plan moves directories out of ~/.claude/skills.
- usage-aggregator.ts: AggregateOpts.provider was already honoured for the
  usage half but not for the optimize half, so the menubar, desktop and web
  surfaces carried the same mismatch.
- dashboard.tsx: `p` cycles activeProvider and `o` opens optimize off the
  same state, so the TUI could show Claude findings under a Codex view.
  activeProvider joins the callback deps; reloadData already clears
  optimizeResult on a provider switch, so no extra invalidation is needed.

Covered by a dry-run test on the apply path: under `provider: 'codex'` a fake
home holding an uninvoked skill must plan nothing, and under 'claude' the same
fixture must still plan the archive.
2026-08-18 04:30:11 +03:00
Aditya Vikram Singh
7a3b4af9e9 fix(optimize): isolate sidechains from behavior 2026-08-14 02:51:54 +05:30
Tiago Santos
75c32e6d65
fix: fix and improve test isolation and collision with environment (#530)
* fix: fix and improve test isolation and collision with environment

* docs: remove unnecessary comment

* test(env-isolation): clear CODEBURN_FORCE_MACOS_MAJOR and pin TZ

Two env vars read in src/ were not isolated: CODEBURN_FORCE_MACOS_MAJOR
(now cleared so it cannot leak between tests) and TZ (now pinned to UTC,
since clearing it falls back to the OS zone and would shift date buckets
versus a clean CI runner).

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-06-20 13:42:10 +02:00
iamtoruk
2fb078bdfb Fix V8 OOM crash on 30-day period with Buffer-based line reader and large-line parser
Three-layer fix for V8 heap exhaustion when parsing heavy session data:

1. Buffer-based readSessionLines (fs-utils.ts): Replace readline with raw
   Buffer streaming using Buffer.indexOf(0x0a). Eliminates ConsString trees
   that caused OOM when regex-flattening 100MB+ lines. Two-state machine
   (ACCUMULATING/SCANNING) skips old lines at ~2KB cost instead of 200MB.

2. Large-line streaming parser (parser.ts): Hand-written JSON scanner for
   lines >32KB extracts only cost/token/tool fields without JSON.parse,
   avoiding full object graph allocation. Dual string/Buffer paths.

3. Dashboard memory management (dashboard.tsx): Disable auto-refresh for
   heavy periods (30d/month/all), clear old dataset before reload via
   nextTick to allow GC, prevent overlapping reloads with mutex, lazy
   optimize scanning on keypress instead of useEffect.

Also fixes three race conditions in dashboard reload deduplication:
- Early return after nextTick bypassing finally block (permanent mutex lock)
- A->B->A period switching dropping final reload (stale pending)
- Stale pendingReloadRef not cleared when in-flight matches request
2026-05-15 23:15:26 -07:00
Łukasz Majcher
5e49f17e64 fix: switch scanJsonlFile and parseSessionFile to readSessionLines to prevent OOM
readViaStream (used for files ≥8 MB) reconstructs the full file as a
single string via chunks.join('\n'), giving the same peak allocation as
readFile. Callers then call content.split('\n'), creating a second copy.
With FILE_READ_CONCURRENCY=16 and files up to 128 MB this can exhaust
the V8 heap (~6 GB theoretical peak).

readSessionLines already exists as a proper async generator that yields
one line at a time. Switch both hot-path callers to iterate it directly
so the full file string is never held in memory.

Adds two tests: a spy test confirming readSessionLines is called (not
readSessionFile), and a 500-entry correctness test.

Fixes #131
2026-04-22 10:11:13 +00:00
Ninym
bd71377fdd docs(optimize): remove references to non-existent .claudeignore
Claude Code does not document or implement a .claudeignore feature.
The junk-reads detector's fix is now a CLAUDE.md instruction asking
Claude to avoid generated/dependency directories. The separate
detectMissingClaudeignore finding and its tests are removed; checking
for the presence of a non-existent file has no signal.

Closes #61.
2026-04-17 08:32:07 +02:00
AgentSeal
c02f63235a test(optimize): add 34 filesystem-mocking tests
Covers previously untested detectors and helpers with real temp-dir
fixtures (not stubs) to verify behavior against actual file I/O.

New coverage:
- detectMissingClaudeignore: project with/without junk dirs and
  .claudeignore, impact scaling.
- detectBloatedClaudeMd: plain oversized file, @-import expansion,
  circular import safety, email/npm-scope @-token filtering.
- loadMcpConfigs: project reads, colon-to-underscore normalization,
  malformed JSON tolerance.
- detectUnusedMcp: 24-hour grace period, config vs invocation merge.
- detectBashBloat: env var unset, configured under/over limit.
- detectGhostCommands: path prefixes are not commands, <command-name>
  tag parsing.
- scanJsonlFile: missing file, tool_use parsing, malformed line
  skipping, date-range filter.
- scanAndDetect: empty projects returns healthy result.
- estimateContextBudget: system base, MCP tools, memory files.
- discoverProjectCwd: empty dir, no jsonl, cwd extraction.

Uses vi.mock to redirect os.homedir() to a disposable temp directory,
so tests do not read the tester's real ~/.claude. 34 tests, <30ms
wall time. Total suite now 160 tests.
2026-04-16 09:33:03 -07:00