fix(native-chat): continue a usage-limited chat on the account you switch to (#41665)

* fix(native-chat): let a chat continue on the account you switch to after a usage limit

A usage-limit reset belongs to the account that hit it. After a switch the
host kept holding the chat until the old account's reset, and desktop and
phone hid Continue until then, so Switch Account on the card did nothing.
The host now releases the hold once the chat would start on another
account, letting only that account's own reading hold it, and publishes the
earlier continue time that both clients read.

* fix(native-chat): save the account a usage limit hit, per cut

The fallback to the chat's founding account could report a switch that
never happened for a chat that had already moved, releasing it before the
new account's reset. Save the cut's account while the child that ran it is
live, use only that, and key the switch time by the cut it belongs to.

* fix(native-chat): record the account each chat runtime launched on

record.accountHome names only the founding account, so after a switch
nothing said which account a usage limit hit. Owner proof now records the
account each runtime fence launched under, every host cut carries its
turn's fence, and the hold reads the cut's account from that pair. This
replaces the sweep's saved copy, which a fast child exit could miss.

Spending the new account showed before a switch now holds the chat until
its own reset; only a reading taken after the switch releases it.

* fix(native-chat): restart the switch time when the chat moves to yet another account

A second switch for the same cut kept the first switch's time, so a reading
of the newer account taken before it was chosen could release the chat.
This commit is contained in:
Kelvin Amoaba
2026-11-12 22:49:22 +00:00
committed by GitHub
parent 51fffa8753
commit c679989e6d
16 changed files with 202 additions and 51 deletions
@@ -43,14 +43,19 @@ export function MobileNativeChatUsageLimitStatus({
const { usageLimit, openAccounts } = useContext(MobileNativeChatNoticeContext)
const failure = readWholeAgentSessionFailureFact(notice.failure)
const resetsAt = failure?.kind === 'usageLimitReached' ? failure.usageLimit?.resetsAt : undefined
const resetPassed = useResetPassed(resetsAt)
const actions =
failure?.kind === 'usageLimitReached' &&
notice.usageLimitTurnItemId !== undefined &&
usageLimit?.turnItemId === notice.usageLimitTurnItemId
? usageLimit
: null
// The line names the reset Orca waits for; Continue still keys off the cut's own reset.
// Continue applies at the cut's own reset, or earlier once the host says the chat moved accounts.
const resetPassed = useResetPassed(
resetsAt !== undefined && actions?.continueAt !== undefined
? Math.min(resetsAt, actions.continueAt)
: resetsAt
)
// The line names the reset Orca waits for.
const shownAt = actions?.autoContinue ? (actions.continueAt ?? resetsAt) : resetsAt
const shownPassed = useResetPassed(shownAt)
if (failure?.kind !== 'usageLimitReached') {
@@ -102,6 +102,7 @@ export async function acquireOwner(
sessionId: record.sessionId,
fence,
...(acquired.link ? { link: acquired.link } : {}),
...(acquired.accountHome ? { launchedAccountHome: acquired.accountHome } : {}),
...(acquired.accountRecipe
? {
accountAcquisition: {
@@ -75,6 +75,15 @@ async function heldChat() {
launchUnder: (next: UsageLimitAccountScope) => {
attributed = next
},
/** The cut's runtime launched on account A; a start of this chat would now launch on `path`. */
switchAccount: async (path: string) => {
const fence = journal.itemFence(cut.turnItemId)!
await current.store.transitionHandoff(SESSION, (record) => ({
...record,
launchedAccount: { fence, accountHome: { variable: 'CODEX_HOME', path: '/homes/a' } }
}))
current.host.deps.launchAccountHome = async () => ({ variable: 'CODEX_HOME', path })
},
setEnabled: (next: boolean) => {
enabled = next
},
@@ -134,6 +143,33 @@ it('holds again on spending inside the grace, and leaves an unknown reset once r
await settleOne(await refused.pass(room + 15_000, (now) => weekly(0, null, now)))
})
it('continues before the cut resets once the chat would start on another account with room', async () => {
const chat = await heldChat()
const start = chat.cut.resetsAt - 60_000
await chat.switchAccount('/homes/b')
expect(await chat.pass(start + 5_000, (now) => weekly(0, null, now))).toEqual([])
expect(chat.state()).toMatchObject({ state: 'scheduled', continueAt: start + 5_000 })
await settleOne(await chat.pass(start + 20_000, (now) => weekly(0, null, now)))
})
it('keeps the new account spent from a reading before the switch until a later one shows room', async () => {
const chat = await heldChat()
const start = chat.cut.resetsAt - 60_000
const newAccountReset = start + 2 * HOUR
await chat.switchAccount('/homes/b')
expect(await chat.pass(start + 5_000, () => weekly(100, newAccountReset, start + 1_000))).toEqual(
[]
)
expect(chat.state()).toMatchObject({ continueAt: newAccountReset })
expect(await chat.pass(start + 10 * 60_000, () => weekly(100, newAccountReset, start))).toEqual(
[]
)
const room = start + 20 * 60_000
expect(await chat.pass(room, (now) => weekly(0, null, now))).toEqual([])
expect(chat.state()).toMatchObject({ continueAt: room })
await settleOne(await chat.pass(room + 15_000, (now) => weekly(0, null, now)))
})
it('retires a hold when the launch moves to billing the reading does not describe', async () => {
const chat = await heldChat()
const weeklyReset = chat.cut.resetsAt + 3 * HOUR
@@ -125,7 +125,8 @@ export class StructuredAgentSessionJournalProjections {
contextId,
usageLimitCut: hostUsageLimitCut(
snapshot,
latestNativeChatUsageLimitCut(snapshot.items, snapshot.submissions, journal.itemBody)
latestNativeChatUsageLimitCut(snapshot.items, snapshot.submissions, journal.itemBody),
journal.itemFence
),
firstInputSubmissionKey,
submissionCount: journal.inputOrder.inputCount(),
@@ -10,8 +10,11 @@ import type { AgentSessionRecord } from '../../../../shared/agent-session/lifecy
import type { AgentSessionRecordStore } from '../../../runtime/agent-session/records/agent-session-record-store'
import {
agentSessionAccountHomePath,
agentSessionAccountHomesEqual,
isLegacyAgentSessionAccountHome,
type AgentSessionAccountHome
} from '../../../../shared/agent-session/lifecycle/agent-session-account-home'
import { normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path'
import { usageLimitContinueIdentity as identity } from './structured-agent-session-usage-limit-chain'
/** The polled usage of the account a chat continues on, as the host attributes it. */
@@ -75,6 +78,11 @@ export class UsageLimitAccountHolds {
private readonly scopes = new Map<string, UsageLimitAccountScope | undefined>()
/** The source witness each chat's attribution was computed under. */
private readonly witnesses = new Map<string, string | undefined>()
/** When each chat was first seen continuing on another account than the one its cut hit. */
private readonly movedSince = new Map<
string,
{ cut: string; to: AgentSessionAccountHome; at: number }
>()
/** Shared by every chat of a pass and keyed by source witness: work scales with accounts and
* their changes, not chats. */
private launchHomes = new Map<string, Promise<AgentSessionAccountHome | null>>()
@@ -90,15 +98,22 @@ export class UsageLimitAccountHolds {
this.readings = new Map()
}
/** Re-attributed each pass: the account a chat continues on follows the host's selection. */
async attribute(id: string, host: AttributionHost): Promise<void> {
/** Re-attributed each pass: the account a chat continues on follows the host's selection.
* `hit` names the cut and the account it hit, when known. */
async attribute(
id: string,
host: AttributionHost,
hit?: { cut: string; home: AgentSessionAccountHome | null },
now = Date.now()
): Promise<void> {
const record = host.store.getRecord(id)
const read = host.usageLimitAutoContinue?.accountUsage
const witness = record ? liveWitness(host, record.provider) : undefined
let scope: UsageLimitAccountScope | undefined
let movedTo: AgentSessionAccountHome | null = null
try {
// WSL readings never reach this host, so a WSL launch keeps the cut's own reset.
if (record && read && record.location.wslDistro === null) {
if (record && record.location.wslDistro === null) {
// Off WSL a launch home follows only the provider's selection, never the chat.
const target = JSON.stringify([witness, record.provider])
let launch = this.launchHomes.get(target)
@@ -107,8 +122,9 @@ export class UsageLimitAccountHolds {
this.launchHomes.set(target, launch)
}
const home = (await launch) ?? record.accountHome
movedTo = hit?.home && !sameAccountHome(home, hit.home) ? home : null
const path = agentSessionAccountHomePath(home)
if (home && path) {
if (read && home && path) {
const key = JSON.stringify([witness, record.provider, path])
let reading = this.readings.get(key)
if (!reading) {
@@ -129,6 +145,13 @@ export class UsageLimitAccountHolds {
}
this.scopes.set(id, scope)
this.witnesses.set(id, witness)
const since = this.movedSince.get(id)
if (!movedTo || !hit) {
this.movedSince.delete(id)
} else if (since?.cut !== hit.cut || !sameAccountHome(since.to, movedTo)) {
// Each destination counts from when it was chosen, so an older reading of it never releases.
this.movedSince.set(id, { cut: hit.cut, to: movedTo, at: now })
}
}
/** The witness the chat's attribution was computed under; a send binds to this one. */
@@ -155,20 +178,30 @@ export class UsageLimitAccountHolds {
}
}
}
for (const id of this.movedSince.keys()) {
if (!shown.has(id)) {
this.movedSince.delete(id)
}
}
}
clear(): void {
this.holds.clear()
this.scopes.clear()
this.witnesses.clear()
this.movedSince.clear()
this.beginPass()
}
verdict(
id: string,
cut: { turnItemId: string; resetsAt: number },
limited: { turnItemId: string; resetsAt: number },
now: number
): UsageLimitAccountVerdict {
// The account the chat moved to never hit this cut: from the move on, only its reading holds.
const moved = this.movedSince.get(id)
const movedCut = moved?.cut === identity(limited)
const cut = movedCut ? { ...limited, resetsAt: Math.min(limited.resetsAt, moved.at) } : limited
const scope = this.scopes.get(id)
let hold = this.holds.get(id)
// Nothing attributed and nothing held: the cut decides, and nothing is kept for it.
@@ -202,10 +235,11 @@ export class UsageLimitAccountHolds {
(hold.unknownReset && hold.spentSeenAt) || cut.resetsAt,
typeof covering === 'number' ? covering : cut.resetsAt
)
// Spending the new account showed before the move still holds; only a later reading releases it.
if (
account &&
reading &&
fresh(cut.resetsAt) &&
fresh(movedCut ? Number.NEGATIVE_INFINITY : cut.resetsAt) &&
now - anchor <= NATIVE_CHAT_USAGE_LIMIT_MAX_OVERDUE_MS
) {
if (typeof covering === 'number') {
@@ -220,7 +254,10 @@ export class UsageLimitAccountHolds {
hold.heldUntil = Math.max(hold.heldUntil ?? spentUntil, spentUntil)
}
hold.spentSeenAt = Math.max(hold.spentSeenAt ?? reading.updatedAt, reading.updatedAt)
} else if (reading.updatedAt > (hold.spentSeenAt ?? Number.NEGATIVE_INFINITY)) {
} else if (
fresh(cut.resetsAt) &&
reading.updatedAt > (hold.spentSeenAt ?? Number.NEGATIVE_INFINITY)
) {
// Room newer than any spending seen releases the hold at that reading, so it counts from now.
if (hold.unknownReset || (hold.heldUntil ?? Number.NEGATIVE_INFINITY) > reading.updatedAt) {
hold.heldUntil = reading.updatedAt
@@ -293,6 +330,14 @@ export class UsageLimitAccountHolds {
}
}
function sameAccountHome(left: AgentSessionAccountHome, right: AgentSessionAccountHome): boolean {
return isLegacyAgentSessionAccountHome(left) && isLegacyAgentSessionAccountHome(right)
? left.variable === right.variable &&
normalizeRuntimePathForComparison(left.path) ===
normalizeRuntimePathForComparison(right.path)
: agentSessionAccountHomesEqual(left, right)
}
/** Changes whenever a source the provider's attribution reads changes. */
function liveWitness(
host: AttributionHost,
@@ -11,6 +11,7 @@ import { readAgentJournalTurn } from '../../../../shared/agent-session/lifecycle
import { restoreJournal } from '../../agent-session-journal/replay/journal-open'
import { renderJournalStatus } from '../../agent-session-journal/projection/journal-public-read'
import { journalInputOrder } from '../../agent-session-journal/input-order/journal-input-order'
import { journalItemExecutionFence } from '../../agent-session-journal/projection/journal-item-provenance'
import type Database from '../../../sqlite/sync-database'
import {
latestNativeChatUsageLimitCut,
@@ -168,18 +169,25 @@ export function usageLimitContinueChain(
return chain
}
/** A cut as the host's sweep reads it: with the chain that led to it. */
export type HostUsageLimitCut = NativeChatUsageLimitCut & { chain: number }
/** A cut as the host's sweep reads it: with the chain that led to it and the runtime fence that
* ran its turn, which names the account it hit. */
export type HostUsageLimitCut = NativeChatUsageLimitCut & { chain: number; fence?: number }
export function hostUsageLimitCut(
snapshot: {
items: readonly AgentJournalRenderItem[]
submissions: readonly ChainSubmission[]
},
cut: NativeChatUsageLimitCut | null
cut: NativeChatUsageLimitCut | null,
fenceOf: (itemId: string) => number | undefined
): HostUsageLimitCut | null {
const fence = cut ? fenceOf(cut.turnItemId) : undefined
return cut
? { ...cut, chain: usageLimitContinueChain(snapshot.items, snapshot.submissions, cut) }
? {
...cut,
chain: usageLimitContinueChain(snapshot.items, snapshot.submissions, cut),
...(fence === undefined ? {} : { fence })
}
: null
}
@@ -189,14 +197,13 @@ export function readSavedUsageLimitCut(
id: string
): HostUsageLimitCut | null {
const load = restoreJournal(db, id)
const snapshot =
load && !load.newer && !load.damage
? renderJournalStatus(load.state, journalInputOrder(load.state))
: null
return snapshot
? hostUsageLimitCut(
snapshot,
latestNativeChatUsageLimitCut(snapshot.items, snapshot.submissions)
)
: null
if (!load || load.newer || load.damage) {
return null
}
const snapshot = renderJournalStatus(load.state, journalInputOrder(load.state))
return hostUsageLimitCut(
snapshot,
latestNativeChatUsageLimitCut(snapshot.items, snapshot.submissions),
(itemId) => journalItemExecutionFence(load.state, itemId)
)
}
@@ -132,7 +132,9 @@ export class StructuredAgentSessionUsageLimitContinue {
turnItemId: cut.turnItemId,
resetsAt: cut.resetsAt,
// While the reset is unknown, `hold.resetsAt` is a reading time, not a reset.
...(!hold.unknownReset && hold.resetsAt > cut.resetsAt ? { continueAt: hold.resetsAt } : {}),
...(!hold.unknownReset && hold.resetsAt !== cut.resetsAt
? { continueAt: hold.resetsAt }
: {}),
state
}
}
@@ -172,13 +174,7 @@ export class StructuredAgentSessionUsageLimitContinue {
if (this.running || this.disposed) {
return
}
// Off, nothing can be scheduled; the settings listener publishes the switch itself.
if (!this.enabled()) {
if (this.holds.size + this.cold.size + this.attempts.size > 0) {
this.retainShown(this.deps.host.store.listVisibleSessionIds())
}
return
}
// Off, nothing is sent, but an account switch still moves when Continue applies.
this.running = true
try {
const visible = this.deps.host.store.listVisibleSessionIds()
@@ -197,10 +193,10 @@ export class StructuredAgentSessionUsageLimitContinue {
if (!cut || previous?.identity !== identity(cut)) {
this.attempts.delete(id)
}
// Before the overdue check: after a restart only the reading still knows a later reset.
// Only a reading taken after the cut's reset counts, so attribution waits for it.
if (cut?.resetsAt !== undefined && this.deps.now() >= cut.resetsAt) {
await this.holds.attribute(id, this.deps.host)
// Before the overdue check: after a restart only the reading still knows a later reset,
// and a switched account frees the chat before it.
if (cut?.resetsAt !== undefined) {
await this.attribute(id, cut)
}
const attempt = this.attempts.get(id)
if (!cut || !this.eligible(id, cut) || attempt?.unconfirmed || (attempt?.count ?? 0) >= 5) {
@@ -226,6 +222,13 @@ export class StructuredAgentSessionUsageLimitContinue {
}
}
/** The account a cut hit is the one its turn's runtime launched under, when the record names it. */
private attribute(id: string, cut: HostUsageLimitCut): Promise<void> {
const launched = this.deps.host.store.getRecord(id)?.launchedAccount
const home = launched && launched.fence === cut.fence ? launched.accountHome : null
return this.holds.attribute(id, this.deps.host, { cut: identity(cut), home }, this.deps.now())
}
private deferUnchangedCut(id: string, cut: Cut, at: number): void {
const current = this.cold.read(id)
// Admission also refuses temporary backlog; yield the next passes without spending a failure.
@@ -243,7 +246,7 @@ export class StructuredAgentSessionUsageLimitContinue {
private async send(id: string, cut: Cut): Promise<void> {
// Earlier sends take time; moved sources resolve again, once per new witness for every chat.
if (!this.holds.attributionCurrent(id, this.deps.host)) {
await this.holds.attribute(id, this.deps.host)
await this.attribute(id, cut)
}
const record = this.deps.host.store.getRecord(id)
if (!record) {
@@ -1,5 +1,6 @@
import type { AgentSessionRecord } from '../../../../shared/agent-session/lifecycle/agent-session-record'
import type { AgentSessionProviderHandleLink } from '../../../../shared/agent-session/lifecycle/agent-session-provider-handle'
import type { AgentSessionAccountHome } from '../../../../shared/agent-session/lifecycle/agent-session-account-home'
import {
withAcquiredAgentSessionAccount,
type ClaudeAccountRecipe
@@ -21,9 +22,11 @@ export function createAgentSessionOwnerProof(
now: number
leaseTtlMs?: number
accountAcquisition?: { generation: string; recipe: ClaudeAccountRecipe }
/** The account this runtime launched under, when the provider reports one. */
launchedAccountHome?: AgentSessionAccountHome
}): Promise<AgentSessionRecord> =>
mutate(args.sessionId, (record) =>
withAcquiredAgentSessionAccount(
mutate(args.sessionId, (record) => {
const { launchedAccount: _previous, ...proved } = withAcquiredAgentSessionAccount(
proveAgentSessionOwner({
record,
fence: args.fence,
@@ -33,5 +36,12 @@ export function createAgentSessionOwnerProof(
}),
args.accountAcquisition
)
)
// A runtime that reports no account leaves no stale claim about where this fence ran.
return args.launchedAccountHome
? {
...proved,
launchedAccount: { fence: args.fence, accountHome: args.launchedAccountHome }
}
: proved
})
}
@@ -321,7 +321,9 @@ export function NativeChatNoticeRow({
) : usageLimitContinue ? (
<UsageLimitContinueNow
resetsAt={
failure?.kind === 'usageLimitReached' ? failure.usageLimit?.resetsAt : undefined
failure?.kind === 'usageLimitReached'
? (orcaStopView.usageLimitContinueAt ?? failure.usageLimit?.resetsAt)
: undefined
}
/>
) : null}
@@ -12,6 +12,8 @@ export type NativeChatOrcaStopView = {
continueNow: () => void
usageLimitTurnItemId: string | null
usageLimitAutoContinue?: NativeChatUsageLimitAutoContinueState | null
/** When Continue past the usage limit applies; earlier than its reset after an account switch. */
usageLimitContinueAt?: number
onUsageLimitHandOffStarted?: () => void
usageLimitContinueDisabled?: boolean
continueUsageLimit: (hadFocus?: boolean) => void
@@ -93,11 +93,8 @@ export function useNativeChatInterruptedContinuation(input: {
() => latestNativeChatUsageLimitCut(input.journalItems, input.submissions),
[input.journalItems, input.submissions]
)
const usageLimitAutoContinue = useNativeChatUsageLimitAutoContinue(
target,
sessionId,
usageLimitCut
)
const { auto: usageLimitAutoContinue, continueAt: usageLimitContinueAt } =
useNativeChatUsageLimitAutoContinue(target, sessionId, usageLimitCut)
const [attempt, setAttempt] = useState(NO_NATIVE_CHAT_INTERRUPTED_CONTINUE_ATTEMPT)
const [usageFailure, setUsageFailure] = useState<string | null>(null)
if (usageFailure !== null && input.pending.some((entry) => entry.phase === 'sending')) {
@@ -193,6 +190,7 @@ export function useNativeChatInterruptedContinuation(input: {
continueNow,
usageLimitTurnItemId,
usageLimitAutoContinue,
usageLimitContinueAt,
onUsageLimitHandOffStarted:
usageLimitAutoContinue?.kind === 'scheduled' ? usageLimitAutoContinue.cancel : undefined,
usageLimitContinueDisabled: usageCapability === 'unknown',
@@ -207,6 +205,7 @@ export function useNativeChatInterruptedContinuation(input: {
continueNow,
usageLimitTurnItemId,
usageLimitAutoContinue,
usageLimitContinueAt,
usageCapability,
continueUsageLimit
]
@@ -24,7 +24,11 @@ export function useNativeChatUsageLimitAutoContinue(
target: RuntimeClientTarget,
sessionId: string,
cut: NativeChatUsageLimitCut | null
): NativeChatUsageLimitAutoContinueState | null {
): {
auto: NativeChatUsageLimitAutoContinueState | null
/** When the host lets Continue apply: earlier than the cut's reset once the chat moved accounts. */
continueAt: number | undefined
} {
const capability = useStructuredAgentSessionHostCapabilityState(
target,
AGENT_SESSION_USAGE_LIMIT_AUTO_CONTINUE_RUNTIME_CAPABILITY
@@ -101,7 +105,7 @@ export function useNativeChatUsageLimitAutoContinue(
})
.finally(() => setCancelling(false))
}, [cancelling, readCutState, target, sessionId, turnItemId, resetsAt])
return useMemo<NativeChatUsageLimitAutoContinueState | null>(() => {
const auto = useMemo<NativeChatUsageLimitAutoContinueState | null>(() => {
const kind =
capability === 'supported'
? nativeChatUsageLimitAutoContinueKind(cutState, {
@@ -116,4 +120,14 @@ export function useNativeChatUsageLimitAutoContinue(
? { kind, resetsAt: continueAt, cancelDisabled: cancelling, cancel }
: { kind }
}, [capability, continueAt, cutState, schedulable, target.kind, cancelling, cancel])
return useMemo(
() => ({
auto,
continueAt:
resetsAt === undefined || continueAt === undefined
? resetsAt
: Math.min(resetsAt, continueAt)
}),
[auto, resetsAt, continueAt]
)
}
@@ -2,7 +2,8 @@
export type AgentSessionUsageLimitContinue = {
turnItemId: string
resetsAt: number
/** The later reset the account still shows spent, which Orca waits for; absent: `resetsAt`. */
/** When Orca continues instead of `resetsAt`: later while the account still shows spent,
* earlier once the chat would continue on another account. Absent: `resetsAt`. */
continueAt?: number
state: 'scheduled' | 'cancelled' | 'failed' | 'off'
}
@@ -86,6 +86,26 @@ export function agentPinsAccountHome(
: agent.accountLocatorKind === home.kind
}
/** The account a chat's runtime at `fence` launched under; a chat that follows the selection
* moves on, so its founding `accountHome` no longer says where it ran. */
export type AgentSessionLaunchedAccount = { fence: number; accountHome: AgentSessionAccountHome }
export function isOptionalAgentSessionLaunchedAccount(
value: unknown
): value is AgentSessionLaunchedAccount | undefined {
return (
value === undefined ||
(typeof value === 'object' &&
value !== null &&
'fence' in value &&
typeof value.fence === 'number' &&
Number.isSafeInteger(value.fence) &&
value.fence > 0 &&
'accountHome' in value &&
isAgentSessionAccountHome(value.accountHome))
)
}
function isBoundedAccountString(value: unknown, max: number): value is string {
return (
typeof value === 'string' &&
@@ -30,8 +30,10 @@ import { isStructuredAgentId } from './agent-session-provider-handle-encoding'
import { isNonNegativeSafeInteger } from '../../ssh-retained-payload-limits'
import {
isAgentSessionAccountHome,
isOptionalAgentSessionLaunchedAccount,
MAX_PATH_LENGTH,
type AgentSessionAccountHome
type AgentSessionAccountHome,
type AgentSessionLaunchedAccount
} from './agent-session-account-home'
export const AGENT_SESSION_RECORD_SCHEMA_VERSION = 2 as const
@@ -119,6 +121,8 @@ export type AgentSessionRecord = {
providerHandleChain: AgentSessionProviderHandleLink[]
providerContextBoundary?: AgentSessionProviderContextBoundary
accountHome: AgentSessionAccountHome
/** Where the latest proven runtime actually launched; absent before any or from older hosts. */
launchedAccount?: AgentSessionLaunchedAccount
/** Optional host-written construction observation; checked independently of record readability. */
acquiredAccount?: unknown
/** Initial launch directory in host path syntax. Floating sessions resume here; worktree/folder IDs resolve their durable location. */
@@ -313,6 +317,7 @@ export function isPersistedAgentSessionObservationRecord(
isAgentSessionExecutionLocation(record.location) &&
isStructuredAgentId(record.provider) &&
isAgentSessionAccountHome(record.accountHome) &&
isOptionalAgentSessionLaunchedAccount(record.launchedAccount) &&
(record.launchDirectory === undefined ||
isBoundedString(record.launchDirectory, MAX_PATH_LENGTH)) &&
(record.options === undefined || isAgentSessionOptions(record.options)) &&
@@ -25,7 +25,7 @@ export function usageLimitContinueStateForCut(
return publishedForCut(published, cut)?.state
}
/** The reset Orca waits for before continuing this cut: a later one the account still shows spent. */
/** When Orca continues this cut: a later reset the account still shows spent, or earlier after a switch. */
export function usageLimitContinueAtForCut(
published: PublishedUsageLimitContinue,
cut: CutKey