Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 75d4701

Browse files
authored
feat(optimize): flag low-worth expensive sessions
Adds a low-worth detector to codeburn optimize that flags expensive sessions with weak delivery signals (no edits, repeated retries, or no one-shot edits) when no git/gh delivery command is observed. Priority order is low-worth → context-bloat → outliers; each later detector excludes sessions named by an earlier one so the same session is never listed in three findings. Detection: floor, for no-edit, 3+ retries, regex matches git commit/push and gh pr create/merge but excludes commit-tree/commit-graph and dry-run. Three impact tiers consistent with getagentseal#246. Token-savings uses full session tokens for no-edit sessions and the retry fraction for edit-with-retry sessions. Supersedes getagentseal#241 with review fixes. Original implementation by @ozymandiashh.
1 parent f92d57d commit 75d4701

4 files changed

Lines changed: 471 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
and suggests starting fresh with only the current goal, relevant files,
2222
failing output, and constraints. Sessions flagged here are excluded from
2323
the cost-outlier finding so the same session is not listed twice.
24+
- **Worth-it score detector.** New `optimize` finding flags expensive sessions
25+
with weak delivery signals: no edit turns, repeated retries, or edit work
26+
that never landed in one shot, when no `git`/`gh` delivery command is
27+
observed. Framed as a conservative review candidate, not proof of waste.
28+
Sessions flagged here take priority and are excluded from both the
29+
context-bloat and cost-outlier findings so the same session is not listed
30+
more than once.
2431

2532
### Fixed (CLI)
2633
- **Windows Claude project paths.** Claude Code project rollups now prefer

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ Scans your sessions and your `~/.claude/` setup for waste patterns:
189189
- Bloated `CLAUDE.md` files (with `@-import` expansion counted)
190190
- Cache creation overhead and junk directory reads
191191
- Context-heavy sessions where effective input/cache tokens swamp output
192+
- Possibly low-worth expensive sessions with no edit turns or repeated retries
193+
when no `git`/`gh` delivery command is observed
192194

193195
Each finding shows the estimated token and dollar savings plus a ready-to-paste fix: a `CLAUDE.md` line, an environment variable, or a `mv` command to archive unused items. Findings are ranked by urgency (impact weighted against observed waste) and rolled up into an A to F setup health grade. Repeat runs classify each finding as new, improving, or resolved against a 48-hour recent window.
194196

src/optimize.ts

Lines changed: 197 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ const CONTEXT_BLOAT_HIGH_MIN_CANDIDATES = 10
8989
const CONTEXT_BLOAT_GROWTH_RATIO = 2
9090
const CONTEXT_BLOAT_GROWTH_MAX_GAP_MS = 7 * 24 * 60 * 60 * 1000
9191
const CONTEXT_BLOAT_RATIO_DISPLAY_CAP = 1000
92+
const WORTH_IT_MIN_COST_USD = 2
93+
const WORTH_IT_NO_EDIT_MIN_COST_USD = 3
94+
const WORTH_IT_MIN_RETRIES = 3
95+
const WORTH_IT_RETRY_WITH_EDIT_MIN_RETRIES = 2
96+
const WORTH_IT_PREVIEW = 5
97+
const WORTH_IT_LOW_MAX_CANDIDATES = 2
98+
const WORTH_IT_LOW_MAX_TOTAL_COST_USD = 10
99+
const WORTH_IT_HIGH_MIN_CANDIDATES = 10
100+
const WORTH_IT_HIGH_TOTAL_COST_USD = 50
92101

93102
// ============================================================================
94103
// Scoring constants
@@ -1235,6 +1244,179 @@ function formatContextRatio(ratio: number): string {
12351244
return ratio.toFixed(1)
12361245
}
12371246

1247+
// ============================================================================
1248+
// Worth-it / low-worth-session detector helpers
1249+
// ============================================================================
1250+
1251+
// Use (\s|$|--) instead of \b after commit/push so `git commit-tree` and
1252+
// `git commit-graph` are not treated as deliveries. The `--` clause keeps
1253+
// `git commit --amend` matching as a real delivery command.
1254+
const DELIVERY_COMMAND_PATTERNS = [
1255+
/(?:^|[;&|]\s*)git\s+(?:commit|push)(?=\s|$|--)(?![^;&|]*--dry-run)/,
1256+
/(?:^|[;&|]\s*)gh\s+pr\s+(?:create|merge)(?=\s|$|--)(?![^;&|]*--dry-run)/,
1257+
]
1258+
1259+
function sessionDeliveryCommand(session: ProjectSummary['sessions'][number]): string | null {
1260+
const commands = Object.keys(session.bashBreakdown)
1261+
return commands.find(command => DELIVERY_COMMAND_PATTERNS.some(pattern => pattern.test(command))) ?? null
1262+
}
1263+
1264+
function hasCategoryBreakdownData(session: ProjectSummary['sessions'][number]): boolean {
1265+
return Object.values(session.categoryBreakdown).some(category =>
1266+
category.turns > 0
1267+
|| category.costUSD > 0
1268+
|| category.retries > 0
1269+
|| category.editTurns > 0
1270+
|| category.oneShotTurns > 0
1271+
)
1272+
}
1273+
1274+
function sessionEditTurns(session: ProjectSummary['sessions'][number]): number {
1275+
if (hasCategoryBreakdownData(session)) {
1276+
return Object.values(session.categoryBreakdown).reduce((sum, c) => sum + c.editTurns, 0)
1277+
}
1278+
return session.turns.filter(turn => turn.hasEdits).length
1279+
}
1280+
1281+
function sessionOneShotTurns(session: ProjectSummary['sessions'][number]): number {
1282+
if (hasCategoryBreakdownData(session)) {
1283+
return Object.values(session.categoryBreakdown).reduce((sum, c) => sum + c.oneShotTurns, 0)
1284+
}
1285+
return session.turns.filter(turn => turn.hasEdits && turn.retries === 0).length
1286+
}
1287+
1288+
function sessionRetryCount(session: ProjectSummary['sessions'][number]): number {
1289+
if (hasCategoryBreakdownData(session)) {
1290+
return Object.values(session.categoryBreakdown).reduce((sum, c) => sum + c.retries, 0)
1291+
}
1292+
return session.turns.reduce((sum, turn) => sum + turn.retries, 0)
1293+
}
1294+
1295+
function sessionTotalTurns(session: ProjectSummary['sessions'][number]): number {
1296+
if (hasCategoryBreakdownData(session)) {
1297+
return Object.values(session.categoryBreakdown).reduce((sum, c) => sum + c.turns, 0)
1298+
}
1299+
return session.turns.length
1300+
}
1301+
1302+
// Token-savings estimate for a low-worth candidate. Two regimes:
1303+
// - No-edit sessions: full session tokens are at risk (the session produced
1304+
// no apparent output to weigh against the spend).
1305+
// - Sessions with edits but with retries / no one-shot: only the retry
1306+
// fraction is counted as recoverable. Edits may still have been useful;
1307+
// we credit the model with that and only flag the retry overhead.
1308+
// Ratio is bounded to [0, 1] so retry-heavy sessions with weird turn counts
1309+
// can't claim more than the full session token total.
1310+
function estimateLowWorthRecoverableTokens(
1311+
session: ProjectSummary['sessions'][number],
1312+
editTurns: number,
1313+
retries: number,
1314+
): number {
1315+
const tokens = sessionTokenTotal(session)
1316+
if (editTurns === 0) return tokens
1317+
const totalTurns = sessionTotalTurns(session)
1318+
if (totalTurns === 0) return 0
1319+
const fraction = Math.min(1, Math.max(0, retries / totalTurns))
1320+
return Math.round(tokens * fraction)
1321+
}
1322+
1323+
export type LowWorthCandidate = {
1324+
project: string
1325+
sessionId: string
1326+
date: string
1327+
cost: number
1328+
tokens: number
1329+
reasons: string[]
1330+
}
1331+
1332+
export function findLowWorthCandidates(projects: ProjectSummary[]): LowWorthCandidate[] {
1333+
const candidates: LowWorthCandidate[] = []
1334+
1335+
for (const project of projects) {
1336+
for (const session of project.sessions) {
1337+
if (session.totalCostUSD < WORTH_IT_MIN_COST_USD) continue
1338+
if (sessionDeliveryCommand(session)) continue
1339+
1340+
const editTurns = sessionEditTurns(session)
1341+
const oneShotTurns = sessionOneShotTurns(session)
1342+
const retries = sessionRetryCount(session)
1343+
const reasons: string[] = []
1344+
1345+
if (editTurns === 0 && session.totalCostUSD >= WORTH_IT_NO_EDIT_MIN_COST_USD) {
1346+
reasons.push('no edit turns')
1347+
}
1348+
if (retries >= WORTH_IT_MIN_RETRIES) {
1349+
reasons.push(`${retries} retries`)
1350+
}
1351+
if (
1352+
editTurns > 0
1353+
&& oneShotTurns === 0
1354+
&& retries >= WORTH_IT_RETRY_WITH_EDIT_MIN_RETRIES
1355+
) {
1356+
reasons.push('no one-shot edit turns')
1357+
}
1358+
1359+
if (reasons.length === 0) continue
1360+
1361+
candidates.push({
1362+
project: project.project,
1363+
sessionId: session.sessionId,
1364+
date: session.firstTimestamp.slice(0, 10),
1365+
cost: session.totalCostUSD,
1366+
tokens: estimateLowWorthRecoverableTokens(session, editTurns, retries),
1367+
reasons,
1368+
})
1369+
}
1370+
}
1371+
1372+
candidates.sort((a, b) =>
1373+
b.cost - a.cost
1374+
|| a.date.localeCompare(b.date)
1375+
|| a.project.localeCompare(b.project)
1376+
|| a.sessionId.localeCompare(b.sessionId)
1377+
)
1378+
return candidates
1379+
}
1380+
1381+
export function detectLowWorthSessions(projects: ProjectSummary[]): WasteFinding | null {
1382+
const candidates = findLowWorthCandidates(projects)
1383+
if (candidates.length === 0) return null
1384+
1385+
const preview = candidates.slice(0, WORTH_IT_PREVIEW)
1386+
const list = preview
1387+
.map(s => `${s.project}/${s.sessionId} on ${s.date}: ${formatCost(s.cost)} (${s.reasons.join(', ')})`)
1388+
.join('; ')
1389+
const extra = candidates.length > preview.length ? `; +${candidates.length - preview.length} more` : ''
1390+
// Per-candidate `tokens` is already the recoverable estimate (full session
1391+
// for no-edit, retry-fraction for edit-with-retries). Sum across candidates.
1392+
const tokensSaved = Math.round(candidates.reduce((sum, s) => sum + s.tokens, 0))
1393+
const totalCost = candidates.reduce((sum, s) => sum + s.cost, 0)
1394+
1395+
// Three tiers consistent with detectContextBloat: high at >=10 candidates
1396+
// or >=$50 total spend at risk; low at <=2 candidates AND <$10 total;
1397+
// medium in between.
1398+
let impact: Impact
1399+
if (candidates.length >= WORTH_IT_HIGH_MIN_CANDIDATES || totalCost >= WORTH_IT_HIGH_TOTAL_COST_USD) {
1400+
impact = 'high'
1401+
} else if (candidates.length <= WORTH_IT_LOW_MAX_CANDIDATES && totalCost < WORTH_IT_LOW_MAX_TOTAL_COST_USD) {
1402+
impact = 'low'
1403+
} else {
1404+
impact = 'medium'
1405+
}
1406+
1407+
return {
1408+
title: `${candidates.length} possibly low-worth expensive session${candidates.length === 1 ? '' : 's'}`,
1409+
explanation: `Sessions with meaningful spend but weak delivery signals: ${list}${extra}. This is a review candidate, not proof of waste: CodeBurn flags missing edit turns, repeated retries, and sessions without git delivery commands so you can decide whether the work was worth its cost before it becomes a habit.`,
1410+
impact,
1411+
tokensSaved,
1412+
fix: {
1413+
type: 'paste',
1414+
label: 'Set a delivery checkpoint at the start of the next expensive thread:',
1415+
text: 'Before continuing, name the deliverable in one sentence (PR title, file changed, command output you expect). Stop and check with me if (a) you spend more than 10 minutes without an edit, or (b) the same approach fails twice. Do not retry past two attempts on any single fix.',
1416+
},
1417+
}
1418+
}
1419+
12381420
export type ContextBloatCandidate = {
12391421
project: string
12401422
sessionId: string
@@ -1302,8 +1484,9 @@ export function findContextBloatCandidates(projects: ProjectSummary[]): ContextB
13021484
return candidates
13031485
}
13041486

1305-
export function detectContextBloat(projects: ProjectSummary[]): WasteFinding | null {
1487+
export function detectContextBloat(projects: ProjectSummary[], excludedSessionIds?: ReadonlySet<string>): WasteFinding | null {
13061488
const candidates = findContextBloatCandidates(projects)
1489+
.filter(c => !excludedSessionIds?.has(c.sessionId))
13071490
if (candidates.length === 0) return null
13081491

13091492
const preview = candidates.slice(0, CONTEXT_BLOAT_PREVIEW)
@@ -1530,16 +1713,26 @@ export async function scanAndDetect(
15301713
const mcpCoverage = aggregateMcpCoverage(projects)
15311714

15321715
const findings: WasteFinding[] = []
1533-
const contextBloatSessionIds = new Set(findContextBloatCandidates(projects).map(c => c.sessionId))
1716+
// Priority order for the per-session findings: low-worth → context-bloat →
1717+
// outliers. Each later detector excludes sessions already named by an
1718+
// earlier one so a single session is not listed in three findings.
1719+
const lowWorthSessionIds = new Set(findLowWorthCandidates(projects).map(c => c.sessionId))
1720+
const contextBloatVisibleIds = new Set(
1721+
findContextBloatCandidates(projects)
1722+
.filter(c => !lowWorthSessionIds.has(c.sessionId))
1723+
.map(c => c.sessionId),
1724+
)
1725+
const outlierExclusions = new Set([...lowWorthSessionIds, ...contextBloatVisibleIds])
15341726
const syncDetectors: Array<() => WasteFinding | null> = [
15351727
() => detectCacheBloat(apiCalls, projects, dateRange),
15361728
() => detectLowReadEditRatio(toolCalls),
15371729
() => detectJunkReads(toolCalls, dateRange),
15381730
() => detectDuplicateReads(toolCalls, dateRange),
15391731
() => detectUnusedMcp(toolCalls, projects, projectCwds, mcpCoverage),
15401732
() => detectMcpToolCoverage(projects, mcpCoverage),
1541-
() => detectContextBloat(projects),
1542-
() => detectSessionOutliers(projects, contextBloatSessionIds),
1733+
() => detectLowWorthSessions(projects),
1734+
() => detectContextBloat(projects, lowWorthSessionIds),
1735+
() => detectSessionOutliers(projects, outlierExclusions),
15431736
() => detectBloatedClaudeMd(projectCwds),
15441737
() => detectBashBloat(),
15451738
]

0 commit comments

Comments
 (0)