mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-16 12:39:21 +00:00
fix(hermes): use sessions.cwd, flag estimated cost, propagate SQLITE_BUSY
- Read the populated sessions.cwd column for project grouping; fall back to scraping the transcript, then the profile name. The current Hermes build no longer writes "Current working directory:" lines into messages. - Set costIsEstimated when the figure is the LiteLLM fallback (no recorded cost). - Re-throw SQLITE_BUSY from the discovery and read paths so a transient lock on the live state.db is retried instead of being cached as an empty result. - Tests: add the cwd column to fixtures; cover cwd grouping, the estimated flag, and tool-result tool_name extraction.
This commit is contained in:
parent
645b3c2bdd
commit
e80da3139a
2 changed files with 88 additions and 5 deletions
|
|
@ -11,6 +11,7 @@ type HermesSessionRow = {
|
|||
id: string
|
||||
source: string | null
|
||||
model: string | null
|
||||
cwd: string | null
|
||||
billing_provider: string | null
|
||||
input_tokens: number | null
|
||||
output_tokens: number | null
|
||||
|
|
@ -294,6 +295,7 @@ async function discoverFromDb(dbPath: string, profile: string): Promise<SessionS
|
|||
provider: 'hermes',
|
||||
}))
|
||||
} catch (err) {
|
||||
if (isSqliteBusyError(err)) throw err
|
||||
process.stderr.write(`codeburn: error querying Hermes database: ${err instanceof Error ? err.message : err}\n`)
|
||||
return []
|
||||
} finally {
|
||||
|
|
@ -329,6 +331,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
|
|||
`SELECT id,
|
||||
${nullableColumn(columns, 'source')},
|
||||
${nullableColumn(columns, 'model')},
|
||||
${nullableColumn(columns, 'cwd')},
|
||||
${nullableColumn(columns, 'billing_provider')},
|
||||
${numberColumn(columns, 'input_tokens')},
|
||||
${numberColumn(columns, 'output_tokens')},
|
||||
|
|
@ -374,7 +377,13 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
|
|||
|
||||
const model = row.model ?? 'unknown'
|
||||
const { tools, toolSequence, bashCommands } = collectTools(messages)
|
||||
const projectInfo = inferProject(messages, sanitizeProject(profile))
|
||||
// Hermes records the session's working directory in sessions.cwd.
|
||||
// Prefer it; fall back to scraping a "Current working directory:" line
|
||||
// from the transcript (older builds), then to the profile name.
|
||||
const cwd = row.cwd?.trim()
|
||||
const projectInfo = cwd
|
||||
? { project: sanitizeProject(cwd), projectPath: cwd }
|
||||
: inferProject(messages, sanitizeProject(profile))
|
||||
const timestamp = parseTimestamp(row.started_at)
|
||||
const dedupKey = `hermes:${profile}:${row.id}`
|
||||
if (seenKeys.has(dedupKey)) return
|
||||
|
|
@ -391,10 +400,14 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
|
|||
cacheReadTokens,
|
||||
0,
|
||||
)
|
||||
const costUSD =
|
||||
const recordedCost =
|
||||
(row.actual_cost_usd ?? 0) > 0 ? row.actual_cost_usd!
|
||||
: (row.estimated_cost_usd ?? 0) > 0 ? row.estimated_cost_usd!
|
||||
: calculatedCost
|
||||
: null
|
||||
// When Hermes stored no cost (e.g. subscription-billed sessions), the
|
||||
// figure is our LiteLLM-priced estimate from the session token totals.
|
||||
const costUSD = recordedCost ?? calculatedCost
|
||||
const costIsEstimated = recordedCost === null
|
||||
|
||||
result = {
|
||||
provider: 'hermes',
|
||||
|
|
@ -407,6 +420,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
|
|||
reasoningTokens,
|
||||
webSearchRequests: 0,
|
||||
costUSD,
|
||||
costIsEstimated,
|
||||
tools,
|
||||
bashCommands,
|
||||
timestamp,
|
||||
|
|
@ -420,6 +434,9 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
|
|||
projectPath: projectInfo.projectPath,
|
||||
}
|
||||
} catch (err) {
|
||||
// A transient lock on the live state.db must propagate so the caller
|
||||
// retries, not get swallowed into an empty (negatively cached) result.
|
||||
if (isSqliteBusyError(err)) throw err
|
||||
process.stderr.write(`codeburn: error querying Hermes database: ${err instanceof Error ? err.message : err}\n`)
|
||||
return
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ function createHermesDb(homeDir: string): string {
|
|||
id TEXT PRIMARY KEY,
|
||||
source TEXT,
|
||||
model TEXT,
|
||||
cwd TEXT,
|
||||
billing_provider TEXT,
|
||||
billing_base_url TEXT,
|
||||
billing_mode TEXT,
|
||||
|
|
@ -120,6 +121,7 @@ function insertSession(db: TestDb, values: {
|
|||
id: string
|
||||
source?: string
|
||||
model?: string
|
||||
cwd?: string | null
|
||||
billingProvider?: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
|
|
@ -135,14 +137,15 @@ function insertSession(db: TestDb, values: {
|
|||
}): void {
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (
|
||||
id, source, model, billing_provider, input_tokens, output_tokens,
|
||||
id, source, model, cwd, billing_provider, input_tokens, output_tokens,
|
||||
cache_read_tokens, cache_write_tokens, reasoning_tokens, estimated_cost_usd,
|
||||
actual_cost_usd, api_call_count, tool_call_count, started_at, title
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
values.id,
|
||||
values.source ?? 'cli',
|
||||
values.model ?? 'gpt-5.5',
|
||||
values.cwd ?? null,
|
||||
values.billingProvider ?? 'openai-codex',
|
||||
values.inputTokens,
|
||||
values.outputTokens,
|
||||
|
|
@ -477,4 +480,67 @@ skipUnlessSqlite('hermes provider', () => {
|
|||
projectPath: 'C:\\AI_LAB\\OPENCLAW',
|
||||
})
|
||||
})
|
||||
|
||||
it('groups by the sessions.cwd column when present, ahead of message scraping', async () => {
|
||||
const dbPath = createHermesDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
insertSession(db, {
|
||||
id: 'cwd-session',
|
||||
cwd: '/Users/me/projects/codeburn',
|
||||
inputTokens: 30,
|
||||
outputTokens: 10,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
reasoningTokens: 0,
|
||||
startedAt: 1779549200,
|
||||
})
|
||||
db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
|
||||
.run('cwd-session', 'user', 'Current working directory: /tmp/decoy\nbuild it', 1779549201)
|
||||
})
|
||||
|
||||
const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=cwd-session`)
|
||||
expect(calls[0]).toMatchObject({
|
||||
project: 'Users-me-projects-codeburn',
|
||||
projectPath: '/Users/me/projects/codeburn',
|
||||
})
|
||||
})
|
||||
|
||||
it('flags estimated cost only when Hermes recorded none', async () => {
|
||||
const dbPath = createHermesDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
insertSession(db, {
|
||||
id: 'no-cost',
|
||||
inputTokens: 100, outputTokens: 20, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0,
|
||||
startedAt: 1779549200,
|
||||
})
|
||||
insertSession(db, {
|
||||
id: 'recorded-cost',
|
||||
actualCost: 1.23,
|
||||
inputTokens: 100, outputTokens: 20, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0,
|
||||
startedAt: 1779549300,
|
||||
})
|
||||
})
|
||||
|
||||
const noCost = await collectCalls(tmpDir, `${dbPath}#hermes-session=no-cost`)
|
||||
expect(noCost[0]!.costIsEstimated).toBe(true)
|
||||
|
||||
const recorded = await collectCalls(tmpDir, `${dbPath}#hermes-session=recorded-cost`)
|
||||
expect(recorded[0]).toMatchObject({ costUSD: 1.23, costIsEstimated: false })
|
||||
})
|
||||
|
||||
it('counts tool-result messages by their tool_name', async () => {
|
||||
const dbPath = createHermesDb(tmpDir)
|
||||
withTestDb(dbPath, (db) => {
|
||||
insertSession(db, {
|
||||
id: 'tool-result-session',
|
||||
inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0,
|
||||
startedAt: 1779549200,
|
||||
})
|
||||
db.prepare('INSERT INTO messages (session_id, role, content, tool_name, timestamp) VALUES (?, ?, ?, ?, ?)')
|
||||
.run('tool-result-session', 'tool', null, 'read_file', 1779549201)
|
||||
})
|
||||
|
||||
const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=tool-result-session`)
|
||||
expect(calls[0]!.tools).toContain('Read')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue