mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 14:34:32 +00:00
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.
This commit is contained in:
parent
8d8848d805
commit
e3a55cdf20
3 changed files with 60 additions and 6 deletions
|
|
@ -3,7 +3,7 @@
|
|||
## Unreleased
|
||||
|
||||
### Added
|
||||
- **`optimize` spots the same long block pasted at the start of many sessions.** The new `recurring-context` detector groups sessions by their opening block — normalized for whitespace and ANSI, hashed over the first 2 KB — and reports a block of at least 1.5 KB that opens 5 or more sessions, with the top three by tokens, their session counts and the project each is confined to. It is a habit, not an apply-able fix: CodeBurn will not move your own text into `CLAUDE.md` for you, so the finding asks Claude to give the block a permanent home (a `CLAUDE.md` rule, or a file read on demand) and hand back a one-line pointer to open sessions with instead. Savings count the repeats only, never the first paste, and are marked `estimated`: provider usage is counted per API call, where the pasted block is mixed in with the system prompt, tool schemas and `CLAUDE.md`, so the block is sized from its own bytes. Injected system reminders and slash-command wrappers are not pastes and are skipped, and the opening block is read from the session scan that already runs, so nothing extra is read from disk.
|
||||
- **`optimize` spots the same long block pasted at the start of many sessions.** The new `recurring-context` detector groups sessions by their opening block — normalized for whitespace and ANSI, hashed over the first 2 KB — and reports a block of at least 1.5 KB that opens 5 or more sessions, with the top three by tokens, their session counts and the project each is confined to. It is a habit, not an apply-able fix: CodeBurn will not move your own text into `CLAUDE.md` for you, so the finding asks Claude to give the block a permanent home (a `CLAUDE.md` rule, or a file read on demand) and hand back a one-line pointer to open sessions with instead. Savings count the repeats only, never the first paste, and are marked `estimated`: provider usage is counted per API call, where the pasted block is mixed in with the system prompt, tool schemas and `CLAUDE.md`, so the block is sized from its own bytes. Injected system reminders and slash-command wrappers are not pastes and are skipped, and neither is a prompt a program wrote — an SDK session or a subagent task — read from the entry's flags, or off the ends of the raw line when the entry is too large for the parser to keep them. The opening block comes from the session scan that already runs, so nothing extra is read from disk.
|
||||
- **Optimize findings say what to do with them and where their number came from.** Every finding now carries a class and a basis, and every surface groups by it: `Fix now (apply-able)` for findings `codeburn optimize --apply` can write itself, `Habits` for the behavioural ones, `FYI` for informational ones whose cost may be justified. A finding only counts as apply-able when a plan can actually be built for that instance, so an `mcp-deferral-off` caused by Vertex policy or a shell-profile override is grouped as a habit rather than promising a fix that does not exist. Alongside it, each finding is marked `measured` (summed from provider-counted usage) or `estimated` (a schema-size or recovery-fraction model), with the split reported in the header as `N measured · M estimated` in place of the blanket "Estimates only." footer. Sessions whose cost the provider never reported are kept out of the `cost-outliers` peer comparison, and a provider that only ever estimates gets the finding marked `estimated` rather than dropped. `--format json` gains `class` and `basis` per finding plus `summary.measuredSavingsUSD` (existing fields unchanged), and the new `docs/optimize.md` covers what is scanned, exactly what `--apply` may write, and how to read the health grade.
|
||||
|
||||
### Added (CLI)
|
||||
|
|
|
|||
|
|
@ -527,8 +527,8 @@ export type ApiCallMeta = {
|
|||
}
|
||||
|
||||
/// One session's opening paste. `hash` groups sessions that open with the
|
||||
/// same block; `chars` is the block's full length (the transcript text is
|
||||
/// only capped for the hash, not for the size).
|
||||
/// same block; `chars` is the block's length, a floor for a block long
|
||||
/// enough that the parser capped its text.
|
||||
export type SessionOpener = {
|
||||
hash: string
|
||||
chars: number
|
||||
|
|
@ -637,9 +637,29 @@ function normalizeOpener(text: string): string {
|
|||
return stripAnsi(text).replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
const MACHINE_PROMPT_HEAD_BYTES = 2048
|
||||
const MACHINE_PROMPT_PATTERN = /"promptSource"\s*:\s*"sdk"|"isSidechain"\s*:\s*true/
|
||||
|
||||
/// True when a program wrote this prompt rather than a person pasting it: an
|
||||
/// SDK caller, or a parent agent writing a subagent's task. Either repeats by
|
||||
/// design and has no home in CLAUDE.md. A user entry over the parser's
|
||||
/// large-line threshold — routine for generated prompts — comes back without
|
||||
/// its root flags, so those are read off the raw line instead: the ends of it,
|
||||
/// since the fields sit either side of the message that made the line large.
|
||||
function isMachineWrittenPrompt(entry: Record<string, unknown>, line: string | Buffer): boolean {
|
||||
if (entry['promptSource'] === 'sdk' || entry['isSidechain'] === true) return true
|
||||
const edge = (start: number, end: number): string =>
|
||||
typeof line === 'string' ? line.slice(start, end) : line.subarray(start, end).toString('utf-8')
|
||||
const head = edge(0, MACHINE_PROMPT_HEAD_BYTES)
|
||||
const tail = edge(Math.max(MACHINE_PROMPT_HEAD_BYTES, line.length - MACHINE_PROMPT_HEAD_BYTES), line.length)
|
||||
return MACHINE_PROMPT_PATTERN.test(head) || MACHINE_PROMPT_PATTERN.test(tail)
|
||||
}
|
||||
|
||||
/// A session's opening block, or null when it is too small to matter or is
|
||||
/// not a paste at all: system reminders carry CLAUDE.md and hook output,
|
||||
/// slash command wrappers refer to a file that already exists.
|
||||
/// slash command wrappers refer to a file that already exists. `chars` is a
|
||||
/// floor: the parser caps the text of a very large user entry, so a huge
|
||||
/// block is sized at that cap rather than its true length.
|
||||
function toSessionOpener(text: string, project: string): SessionOpener | null {
|
||||
if (text.length < RECURRING_CONTEXT_MIN_CHARS) return null
|
||||
const head = text.trimStart()
|
||||
|
|
@ -716,7 +736,7 @@ export async function scanJsonlFile(
|
|||
userMessages.push(msgContent.slice(0, OPTIMIZE_TEXT_CAP))
|
||||
if (!sawUserText) {
|
||||
sawUserText = true
|
||||
const opener = toSessionOpener(msgContent, project)
|
||||
const opener = isMachineWrittenPrompt(entry, line) ? null : toSessionOpener(msgContent, project)
|
||||
if (opener) openers.push(opener)
|
||||
}
|
||||
} else if (Array.isArray(msgContent)) {
|
||||
|
|
@ -729,7 +749,7 @@ export async function scanJsonlFile(
|
|||
remaining -= text.length
|
||||
if (!sawUserText) {
|
||||
sawUserText = true
|
||||
const opener = toSessionOpener(block.text, project)
|
||||
const opener = isMachineWrittenPrompt(entry, line) ? null : toSessionOpener(block.text, project)
|
||||
if (opener) openers.push(opener)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -434,6 +434,40 @@ describe('detectRecurringContext', () => {
|
|||
expect(detectRecurringContext(await openersFor(repeat(6, `<command-name>/brief</command-name>${BRIEF}`)))).toBeNull()
|
||||
})
|
||||
|
||||
it('skips prompts a program wrote: SDK sessions and subagent transcripts', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
const now = new Date().toISOString()
|
||||
const openers: SessionOpener[] = []
|
||||
for (const [i, entry] of [{ promptSource: 'sdk' }, { isSidechain: true }].entries()) {
|
||||
for (let j = 0; j < 6; j++) {
|
||||
const filePath = join(root, `machine-${i}-${j}.jsonl`)
|
||||
writeFile(filePath, JSON.stringify({ type: 'user', timestamp: now, ...entry, message: { content: BRIEF } }))
|
||||
openers.push(...(await scanJsonlFile(filePath, 'my-app', undefined)).openers)
|
||||
}
|
||||
}
|
||||
expect(openers).toEqual([])
|
||||
expect(detectRecurringContext(openers)).toBeNull()
|
||||
})
|
||||
|
||||
// Over 32 KB the JSONL parser returns a reduced entry without the root
|
||||
// flags, so the markers have to be read off the raw line.
|
||||
it('skips machine-written prompts too large for the parser to keep flags on', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
const now = new Date().toISOString()
|
||||
const huge = BRIEF + 'x'.repeat(40_000)
|
||||
const openers: SessionOpener[] = []
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const filePath = join(root, `huge-${i}.jsonl`)
|
||||
// Field order matters: the flags land past the head, behind the very
|
||||
// message that made the line large.
|
||||
writeFile(filePath, JSON.stringify({
|
||||
isSidechain: false, type: 'user', message: { content: huge }, timestamp: now, promptSource: 'sdk',
|
||||
}))
|
||||
openers.push(...(await scanJsonlFile(filePath, 'my-app', undefined)).openers)
|
||||
}
|
||||
expect(openers).toEqual([])
|
||||
})
|
||||
|
||||
it('only counts the first message of a session as its opener', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
const now = new Date().toISOString()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue