Skip to content

Commit a555c74

Browse files
authored
Merge pull request #406 from getagentseal/fix/pre-release-cleanup
fix: pre-release cleanup - opencode refactor, watchdog backoff, forge dedup
2 parents 16deaa6 + 14026a8 commit a555c74

12 files changed

Lines changed: 77 additions & 576 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## Unreleased
3+
## 0.9.11 - 2026-05-27
44

55
### Added (CLI)
66
- **MCP project profile advisor.** `codeburn optimize` now flags MCP servers
@@ -31,12 +31,42 @@
3131
- **Claude toolSequence missing from session cache.** `apiCallToCachedCall` was
3232
not forwarding the `toolSequence` field, so all cached Claude sessions lost
3333
their tool ordering data.
34+
- **Forge dedup key instability.** The fallback deduplication key used the raw
35+
message array index, which shifts when messages are deleted between scans.
36+
Now uses a composite of model name and token counts. Also fixed a variable
37+
reference before its declaration that would crash at runtime when no tool
38+
call ID was present.
39+
- **Session cache rejected `subagentTypes` field.** The cache validator did not
40+
recognize the `subagentTypes` array, causing entries with this field to be
41+
silently dropped and reparsed on every run.
42+
- **Conflicting date flags on `status` accepted silently.** Passing `--day`
43+
with `--from`/`--to`, or `--days` with any other date flag, produced
44+
undefined behavior. Now exits with a clear error message.
45+
46+
### Changed (CLI)
47+
- **OpenCode provider uses shared SQLite parser.** Delegates to
48+
`sqlite-session-parser.ts` (same module KiloCode uses), reducing the
49+
provider from 498 to 66 lines with no behavior change.
3450

3551
### Added (macOS menubar)
3652
- **Configurable menubar status period.** The menubar dropdown now lets you
3753
choose which period (Today, 7 Days, Month, All Time) is shown in the status
3854
bar. Persisted via UserDefaults. Thanks @ozymandiashh. (#302)
3955

56+
### Fixed (macOS menubar)
57+
- **Loading watchdog killed healthy CLI fetches.** The recovery loop ran every
58+
8 seconds with no backoff. Each attempt reset the generation counter,
59+
discarding in-flight CLI responses (45s timeout) before they could finish.
60+
Replaced with exponential backoff (8s to 60s, 6 attempts max) that skips
61+
recovery when a fetch is already in flight. Shows an error overlay with a
62+
Retry button after all attempts are exhausted.
63+
- **Multi-day cache key mismatch.** `selectedDay` returned the earliest date
64+
instead of nil when multiple days were selected, and
65+
`startInteractiveSelectionRefresh` did not pass the day set to the cache key
66+
constructor. Both now match `PayloadCacheKey` normalization rules.
67+
- **Dead code cleanup.** Removed `RefreshBackoff.swift`, its test file, and a
68+
broken test that called methods deleted in #393.
69+
4070
## 0.9.10 - 2026-05-20
4171

4272
### Added (CLI)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<a href="https://github.com/sponsors/iamtoruk"><img src="https://img.shields.io/badge/sponsor-♥-ea4aaa?logo=github" alt="Sponsor" /></a>
1414
</p>
1515

16-
CodeBurn tracks token usage, cost, and performance across **21 AI coding tools**. It breaks down spending by task type, model, tool, project, and provider so you can see exactly where your budget goes.
16+
CodeBurn tracks token usage, cost, and performance across **25 AI coding tools**. It breaks down spending by task type, model, tool, project, and provider so you can see exactly where your budget goes.
1717

1818
Everything runs locally. No wrapper, no proxy, no API keys. CodeBurn reads session data directly from disk and prices every call using [LiteLLM](https://github.com/BerriAI/litellm).
1919

mac/Sources/CodeBurnMenubar/AppStore.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class AppStore {
4040
var selectedDays: Set<String> = []
4141

4242
var selectedDay: String? {
43-
guard selectedDays.count == 1 else { return selectedDays.min() }
43+
guard selectedDays.count == 1 else { return nil }
4444
return selectedDays.first
4545
}
4646
private(set) var menubarPeriod: Period = Period.savedMenubarPeriod() {
@@ -294,7 +294,8 @@ final class AppStore {
294294
let period = selectedPeriod
295295
let provider = selectedProvider
296296
let day = selectedDay
297-
let key = PayloadCacheKey(period: period, provider: provider, day: day)
297+
let days = selectedDays
298+
let key = PayloadCacheKey(period: period, provider: provider, day: day, days: days)
298299
lastErrorByKey[key] = nil
299300
switchTask = Task {
300301
if provider == .all {
@@ -403,8 +404,15 @@ final class AppStore {
403404
}
404405

405406
func recoverFromStuckLoading() async {
406-
resetLoadingState()
407-
await refresh(key: currentKey, includeOptimize: false, force: true, showLoading: true)
407+
let key = currentKey
408+
guard inFlightKeys[key] == nil else { return }
409+
loadingCountsByKey[key] = nil
410+
loadingStartedAtByKey[key] = nil
411+
await refresh(key: key, includeOptimize: false, force: true, showLoading: true)
412+
}
413+
414+
func setRecoveryExhausted(for label: String) {
415+
lastErrorByKey[currentKey] = "Could not load \(label). Check that the codeburn CLI is installed and working."
408416
}
409417

410418
func refresh(includeOptimize: Bool, force: Bool = false, showLoading: Bool = false) async {

mac/Sources/CodeBurnMenubar/RefreshBackoff.swift

Lines changed: 0 additions & 47 deletions
This file was deleted.

mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,17 @@ struct MenuBarContent: View {
6060
BurnLoadingOverlay(periodLabel: store.selectionLabel)
6161
.transition(.opacity)
6262
.task {
63-
// Keep retrying until data loads or the view is removed.
64-
// The original one-shot recovery silently gave up if the
65-
// first attempt also stalled (generation mismatch, CLI
66-
// timeout, day rollover race). Looping guarantees the
67-
// user never sees a permanent spinner.
68-
while !Task.isCancelled {
69-
try? await Task.sleep(for: .seconds(8))
63+
var delay: Duration = .seconds(8)
64+
let maxDelay: Duration = .seconds(60)
65+
let maxAttempts = 6
66+
for attempt in 1...maxAttempts {
67+
try? await Task.sleep(for: delay)
7068
guard !Task.isCancelled, !store.hasCachedData else { return }
7169
await store.recoverFromStuckLoading()
70+
if attempt < maxAttempts { delay = min(delay * 2, maxDelay) }
7271
}
72+
guard !Task.isCancelled, !store.hasCachedData else { return }
73+
store.setRecoveryExhausted(for: store.selectionLabel)
7374
}
7475
}
7576
}

mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,4 @@ struct AppStoreRefreshRecoveryTests {
9191
#expect(store.shouldResetInteractiveRefreshPipeline)
9292
}
9393

94-
@Test("refresh pause message is visible and clearable")
95-
func refreshPauseMessageIsVisibleAndClearable() {
96-
let store = AppStore()
97-
98-
store.pauseAutomaticRefresh(until: Date(timeIntervalSince1970: 4_000), consecutiveStalls: 3)
99-
#expect(store.refreshPauseMessage?.contains("Refresh paused") == true)
100-
#expect(store.refreshPauseMessage?.contains("3 stalled attempts") == true)
101-
102-
store.clearRefreshPause()
103-
#expect(store.refreshPauseMessage == nil)
104-
}
10594
}

mac/Tests/CodeBurnMenubarTests/RefreshBackoffTests.swift

Lines changed: 0 additions & 57 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeburn",
3-
"version": "0.9.10",
3+
"version": "0.9.11",
44
"description": "See where your AI coding tokens go - by task, tool, model, and project",
55
"type": "module",
66
"main": "./dist/cli.js",

src/main.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,14 @@ program
462462
.option('--no-optimize', 'Skip optimize findings (menubar-json only, faster)')
463463
.action(async (opts) => {
464464
assertFormat(opts.format, ['terminal', 'menubar-json', 'json'], 'status')
465+
if (opts.day && (opts.from || opts.to)) {
466+
process.stderr.write('error: --day cannot be combined with --from or --to\n')
467+
process.exit(1)
468+
}
469+
if (opts.days && (opts.day || opts.from || opts.to)) {
470+
process.stderr.write('error: --days cannot be combined with --day, --from, or --to\n')
471+
process.exit(1)
472+
}
465473
await loadPricing()
466474
const pf = opts.provider
467475
const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude)

src/providers/forge.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,13 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
194194
const inputTokens = Math.max(0, promptTokens - cachedInputTokens)
195195
if (inputTokens === 0 && outputTokens === 0) continue
196196

197+
const model = typeof text?.model === 'string' ? text.model : 'unknown'
197198
const calls = toolCalls(text?.tool_calls)
198199
const { tools, bashCommands, firstCallId } = extractToolsAndCommands(calls)
199-
const deduplicationKey = `forge:${row.conversation_id}:${firstCallId ?? i}`
200+
const stableId = firstCallId ?? `${model}:${promptTokens}:${outputTokens}:${i}`
201+
const deduplicationKey = `forge:${row.conversation_id}:${stableId}`
200202
if (seenKeys.has(deduplicationKey)) continue
201203
seenKeys.add(deduplicationKey)
202-
203-
const model = typeof text?.model === 'string' ? text.model : 'unknown'
204204
yield {
205205
provider: 'forge',
206206
model,

0 commit comments

Comments
 (0)