Files
orca/src/shared/agent-session-usage-limit.ts
T
Brennan Benson 4a33cd8e73 fix(native-chat): a usage limit reads as a pause with its reset time and a Continue, not a failure (#30396)
* fix(native-chat): a usage limit reads as a pause with its reset time and a Continue, not a failure

Claude, Codex and Grok chats that hit the account's usage limit now end the
turn on one warning row that names the limit and, in the reader's clock,
when it resets, with Continue to pick the turn back up. Each adapter reads
its protocol's typed limit signal into a `usageLimitReached` failure fact;
older clients keep the host's sentence.

* fix(native-chat): keep the failure words and chat session files within their line budgets

Moves the attachment-problem sentences to their own file and hands the composer's send straight to Continue.

* fix(native-chat): drop a check the shown branch already rules out

* fix(i18n): ship the usage-limit failure words in the runtime catalog

* fix(native-chat): reconcile usage failures by owning turn and typed cause

* fix(codex): keep snapshot import type-only

* fix(claude): open refusal turns without opening empty placeholders

* fix(native-chat): bind usage Continue to the latest idle failed turn

* test(native-chat): isolate stale Continue from ordinary delivery progress

* test(native-chat): use a braced journal assertion

* test(native-chat): build failure rows through their word factory

* fix(claude): retain terminal failure evidence through result reconciliation

* fix(native-chat): complete usage Continue through the composer sender

* test(native-chat): provide the composer options surface in Continue coverage

* test(native-chat): use typed failure words in renderer fixtures

* test(native-chat): match the remote send payload apart from transport options

* fix(native-chat): replace the previous sender notice on Continue

* fix(native-chat): clear superseded Continue notices before rendering

* fix(native-chat): preserve ordinary composer send arguments

* fix(claude): resolve terminal ownership and preserve resultless quota failures

* fix(claude): keep repeated terminal detail unchanged and complete test fixtures

* fix(claude): resolve deferred terminal replays after owning journal writes

* refactor(native-chat): share tombstone append within the sink line budget

* fix(claude): retain both input and echo correlation on the live turn

* fix(claude): preserve unowned result completion after terminal resolution

* fix(claude): allow unassociated successful goal completion

* fix(claude): keep terminal completion on the live turn

* fix(claude): keep typed notices separate from quota failures

* fix(claude): preserve context-limit failures as ordinary errors

* fix(native-chat): keep quota failures visible in collapsed turns

* fix(native-chat): return keyboard Continue focus to the composer

* test(native-chat): keep quota fixture context stable

* test(native-chat): hoist quota context to module scope

* fix(i18n): use one Chinese word for the agent in both Continue tooltips
2026-10-19 14:43:16 -07:00

60 lines
2.4 KiB
TypeScript

// Which of the agent's usage limits a turn hit and when it resets, as the provider reported it.
// Carried on a `usageLimitReached` failure fact; every reader words it in its own clock.
/** `session`: the rolling few-hour limit. `weekly`: the account's weekly limit. `weeklyModel`: a
* weekly limit on one model. `credits`: paid usage beyond the plan ran out. */
export const AGENT_SESSION_USAGE_LIMIT_WINDOWS = [
'session',
'weekly',
'weeklyModel',
'credits'
] as const
export type AgentSessionUsageLimitWindow = (typeof AGENT_SESSION_USAGE_LIMIT_WINDOWS)[number]
export type AgentSessionUsageLimit = {
window?: AgentSessionUsageLimitWindow
/** Epoch ms. Absent when the provider gave none, or any limit it hit had none. */
resetsAt?: number
}
/** Past this, a reset time is not one a person waits for, so the words leave it out. */
const MAX_RESET_WAIT_MS = 31 * 24 * 60 * 60 * 1000
/** A reset time worth saying, as epoch ms: finite, positive and within a month of `now`. */
export function usageLimitResetsAt(value: unknown, now: number): number | undefined {
return typeof value === 'number' &&
Number.isFinite(value) &&
value > 0 &&
value - now <= MAX_RESET_WAIT_MS
? value
: undefined
}
/** A limit as a reader meets it; undefined when it names nothing this build knows. */
export function readAgentSessionUsageLimit(value: unknown): AgentSessionUsageLimit | undefined {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return undefined
}
const record: Record<string, unknown> = { ...value }
const window = AGENT_SESSION_USAGE_LIMIT_WINDOWS.find((known) => known === record.window)
const resetsAt =
typeof record.resetsAt === 'number' && Number.isFinite(record.resetsAt) && record.resetsAt > 0
? record.resetsAt
: undefined
return window || resetsAt
? { ...(window ? { window } : {}), ...(resetsAt ? { resetsAt } : {}) }
: undefined
}
/** A reset time in the reader's own clock: the time alone today, else with its day. */
export function formatUsageLimitResetClock(resetsAt: number, now: number = Date.now()): string {
const at = new Date(resetsAt)
const today = at.toDateString() === new Date(now).toDateString()
return new Intl.DateTimeFormat(
undefined,
today
? { hour: 'numeric', minute: '2-digit' }
: { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }
).format(at)
}