payload: add-only stale marker for read-only stale menubar serves (#771)

isSessionHydrationComplete() (parser.ts, PR #937) already reports when a
read-only serve skipped or staled real on-disk changes; thread it through
buildMenubarPayload as an optional stale field, present and true only on a
stale serve, always absent otherwise, so older/newer CLI-app pairs stay
compatible. Mirrors the field into the desktop renderer types and the macOS
menubar's Codable payload model (data layer only, no view change).

Drafted with minimax/MiniMax-M3 via local gateway.
This commit is contained in:
iamtoruk 2026-08-22 11:38:46 -07:00
parent 9cfa957e3e
commit affd40e230
6 changed files with 35 additions and 3 deletions

View file

@ -127,6 +127,9 @@ export type ClaudeConfigSelector = {
export type MenubarPayload = {
generated: string
// Optional: older CLIs omit it. Present and true only on a stale read-only
// serve; absent otherwise. Absence must always be read as "assume fresh."
stale?: boolean
current: {
label: string
cost: number

View file

@ -4,6 +4,11 @@ import Foundation
/// `current` is scoped to the requested period; the whole payload reflects that slice.
struct MenubarPayload: Codable, Sendable {
let generated: String
/// Present and `true` only when this payload was assembled from a
/// read-only stale serve. Absent never `false` on a fresh payload;
/// absence must be read as "assume fresh," including for payloads from
/// a CLI version that predates this field.
let stale: Bool?
let current: CurrentBlock
let optimize: OptimizeBlock
let history: HistoryBlock
@ -15,8 +20,10 @@ struct MenubarPayload: Codable, Sendable {
optimize: OptimizeBlock,
history: HistoryBlock,
combined: CombinedUsage?,
claudeConfigs: ClaudeConfigSelector? = nil) {
claudeConfigs: ClaudeConfigSelector? = nil,
stale: Bool? = nil) {
self.generated = generated
self.stale = stale
self.current = current
self.optimize = optimize
self.history = history
@ -25,12 +32,13 @@ struct MenubarPayload: Codable, Sendable {
}
enum CodingKeys: String, CodingKey {
case generated, current, optimize, history, combined, claudeConfigs
case generated, stale, current, optimize, history, combined, claudeConfigs
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
generated = try c.decode(String.self, forKey: .generated)
stale = try c.decodeIfPresent(Bool.self, forKey: .stale)
current = try c.decode(CurrentBlock.self, forKey: .current)
optimize = try c.decode(OptimizeBlock.self, forKey: .optimize)
history = try c.decode(HistoryBlock.self, forKey: .history)

View file

@ -178,6 +178,12 @@ export type ClaudeConfigSelector = {
export type MenubarPayload = {
generated: string
/// Optional. Present and `true` only when this payload was assembled from a
/// read-only stale serve (see `isSessionHydrationComplete` in `parser.ts`).
/// Omitted — never `false` — on a fresh/complete payload, so absence always
/// means "assume fresh," including for payloads from a CLI version that
/// predates this field.
stale?: boolean
current: {
label: string
cost: number
@ -507,6 +513,7 @@ export function buildMenubarPayload(
breakdowns?: BreakdownArrays,
claudeConfigs?: ClaudeConfigSelector,
granularHistory?: GranularHistory,
stale?: boolean,
): MenubarPayload {
const payload: MenubarPayload = {
generated: new Date().toISOString(),
@ -557,5 +564,8 @@ export function buildMenubarPayload(
if (claudeConfigs && claudeConfigs.options.length > 1) {
payload.claudeConfigs = claudeConfigs
}
if (stale) {
payload.stale = true
}
return payload
}

View file

@ -944,5 +944,5 @@ export async function buildMenubarPayloadForRange(periodInfo: PeriodInfo, opts:
const optimize = opts.optimize === false ? null : await scanAndDetect(scanProjects, scanRange, opts.provider)
const granularRange = opts.daysSelection?.range ?? scanRange
const granularHistory = opts.timeline === false ? undefined : buildGranularHistory(scanProjects, granularRange)
return buildMenubarPayload(currentData, providers, optimize, dailyHistory, retryTax, routingWaste, breakdowns, claudeConfigs, granularHistory)
return buildMenubarPayload(currentData, providers, optimize, dailyHistory, retryTax, routingWaste, breakdowns, claudeConfigs, granularHistory, isSessionHydrationComplete() ? undefined : true)
}

View file

@ -413,4 +413,14 @@ describe('buildMenubarPayload', () => {
],
})
})
it('sets stale:true when the caller reports an incomplete hydration', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null, undefined, undefined, undefined, undefined, undefined, undefined, true)
expect(payload.stale).toBe(true)
})
it('omits stale on a normal fresh build', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
expect(payload.stale).toBeUndefined()
})
})

View file

@ -23,5 +23,6 @@ describe('buildMenubarPayloadForRange', () => {
expect(payload.current.codexCredits).toBeGreaterThanOrEqual(0)
// optimize:false => scanAndDetect skipped => empty optimize block regardless of data
expect(payload.optimize).toEqual({ findingCount: 0, savingsUSD: 0, topFindings: [] })
expect(payload.stale).toBeUndefined()
})
})