diff --git a/src/main/native-chat/agent-session-wire/acquisition/structured-agent-session-acquisition.ts b/src/main/native-chat/agent-session-wire/acquisition/structured-agent-session-acquisition.ts index 229f9d18d36..3fd364a11d2 100644 --- a/src/main/native-chat/agent-session-wire/acquisition/structured-agent-session-acquisition.ts +++ b/src/main/native-chat/agent-session-wire/acquisition/structured-agent-session-acquisition.ts @@ -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: { diff --git a/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-account-hold.test.ts b/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-account-hold.test.ts index 11f834686cc..5e160b2cf1f 100644 --- a/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-account-hold.test.ts +++ b/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-account-hold.test.ts @@ -75,12 +75,13 @@ async function heldChat() { launchUnder: (next: UsageLimitAccountScope) => { attributed = next }, - /** The cut ran on account A; a start of this chat would now launch on `path`. */ + /** The cut's runtime launched on account A; a start of this chat would now launch on `path`. */ switchAccount: async (path: string) => { - await current.store.recordUsageLimitCutAccount(SESSION, { - turnItemId: cut.turnItemId, - accountHome: { variable: 'CODEX_HOME', path: '/homes/a' } - }) + 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) => { @@ -151,6 +152,24 @@ it('continues before the cut resets once the chat would start on another account 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 diff --git a/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-continue.test.ts b/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-continue.test.ts index 4a09fd4aea5..34f1bbd5249 100644 --- a/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-continue.test.ts +++ b/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-usage-limit-continue.test.ts @@ -119,7 +119,6 @@ it('continues every chat whose limit reset in the same pass', async () => { sessions: new Map(ids.map((id) => [id, {}])), readSavedCut: () => null, readLoadedCut: cut, - cutAccountHome: async () => null, serialize: (_id, run) => run(), // Each send is answered as superseded, which spends no budget; only who was sent matters here. send: async (params) => { diff --git a/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts b/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts index 2ebdcfae18f..fe4d0cd83e4 100644 --- a/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts +++ b/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts @@ -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(), diff --git a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-host-background.ts b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-host-background.ts index 5c78bce3544..1a35b18449f 100644 --- a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-host-background.ts +++ b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-host-background.ts @@ -14,18 +14,6 @@ export function start( sessions: context.sessions, readLoadedCut: delivery.readUsageLimitCut, readSavedCut: (id) => readSavedUsageLimitCut(context.deps.journalDatabase.db, id), - cutAccountHome: async (id, turnItemId) => { - // Only the child that ran the cut's turn knows its account, so save it while that child is live. - const session = context.sessions.get(id) - const child = session?.child - if (child?.accountHome && session?.journal.itemFence(turnItemId) === child.fence) { - const accountHome = child.accountHome - await context.deps.store.recordUsageLimitCutAccount(id, { turnItemId, accountHome }) - return accountHome - } - const saved = context.deps.store.getRecord(id)?.usageLimitCutAccount - return saved?.turnItemId === turnItemId ? saved.accountHome : null - }, serialize: context.serialize, send: (params) => host.send({ callerKey: 'trusted-local:usage-limit-continue' }, params), publish: delivery.publishStatus, diff --git a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-account-hold.ts b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-account-hold.ts index ee0a31b5f14..6e11e26b409 100644 --- a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-account-hold.ts +++ b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-account-hold.ts @@ -195,10 +195,8 @@ export class UsageLimitAccountHolds { ): 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 cut = - moved?.cut === identity(limited) - ? { ...limited, resetsAt: Math.min(limited.resetsAt, moved.at) } - : limited + 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. @@ -232,10 +230,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') { @@ -250,7 +249,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 diff --git a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-chain.ts b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-chain.ts index ee1e8927915..cf16f2a024f 100644 --- a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-chain.ts +++ b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-chain.ts @@ -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) + ) } diff --git a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-continue.ts b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-continue.ts index 6b5ac4a6748..9cd0ecac485 100644 --- a/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-continue.ts +++ b/src/main/native-chat/agent-session-wire/recovery/structured-agent-session-usage-limit-continue.ts @@ -16,7 +16,6 @@ import type { sendStructuredAgentSessionTurn } from '../operations/structured-ag import { RestartContinuationSupersededError } from './structured-agent-session-restart-continuation' import { UsageLimitAccountHolds } from './structured-agent-session-usage-limit-account-hold' import { UsageLimitColdCuts } from './structured-agent-session-usage-limit-cold-cuts' -import type { AgentSessionAccountHome } from '../../../../shared/agent-session/lifecycle/agent-session-account-home' type Cut = HostUsageLimitCut & { resetsAt: number; turnEndedAt: number } type Attempt = { identity: string; count: number; at: number; unconfirmed?: boolean } @@ -39,8 +38,6 @@ type Deps = { params: Parameters[2] ) => ReturnType readLoadedCut: (id: string) => HostUsageLimitCut | null - /** The account the cut's turn ran on, when known. */ - cutAccountHome: (id: string, turnItemId: string) => Promise publish: (id: string) => void now: () => number } @@ -225,9 +222,11 @@ export class StructuredAgentSessionUsageLimitContinue { } } - private async attribute(id: string, cut: HostUsageLimitCut): Promise { - const home = await this.deps.cutAccountHome(id, cut.turnItemId).catch(() => null) - await this.holds.attribute(id, this.deps.host, { cut: identity(cut), home }, this.deps.now()) + /** 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 { + 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 { diff --git a/src/main/runtime/agent-session/ownership/agent-session-owner-proof.ts b/src/main/runtime/agent-session/ownership/agent-session-owner-proof.ts index c0278d40d9d..cd54e93b6c3 100644 --- a/src/main/runtime/agent-session/ownership/agent-session-owner-proof.ts +++ b/src/main/runtime/agent-session/ownership/agent-session-owner-proof.ts @@ -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 => - 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 + }) } diff --git a/src/main/runtime/agent-session/records/agent-session-record-store.ts b/src/main/runtime/agent-session/records/agent-session-record-store.ts index 66e8ca16597..5dc57674663 100644 --- a/src/main/runtime/agent-session/records/agent-session-record-store.ts +++ b/src/main/runtime/agent-session/records/agent-session-record-store.ts @@ -1,6 +1,6 @@ import { agentSessionRecordPredecessorIds } from './agent-session-record-lineage' import { clearSuccessor } from './agent-session-sender-identity-revision' -import * as usageLimit from '../../../../shared/agent-session/lifecycle/agent-session-usage-limit-continue' +import * as usageLimitContinue from '../../../../shared/agent-session/lifecycle/agent-session-usage-limit-continue' import { createAgentSessionOwnerProof } from '../ownership/agent-session-owner-proof' import { backfillAgentSessionPermissionModes } from '../ownership/agent-session-permission-backfill' import { createAgentSessionConversationReceipts } from '../agent-session-conversation-receipts' @@ -256,11 +256,10 @@ export class AgentSessionRecordStore extends AgentSessionRecordTabs { return this.mutate(args.sessionId, (record) => evictAgentSessionOwner({ ...args, record })) } - recordUsageLimitCutAccount = (id: string, cut: usageLimit.UsageLimitCutAccount) => - this.mutate(id, (record) => usageLimit.recordAgentSessionUsageLimitCutAccount(record, cut)) - - cancelUsageLimitContinue = (id: string, cut: usageLimit.UsageLimitContinueIdentity) => - this.mutate(id, (record) => usageLimit.cancelAgentSessionUsageLimitContinue(record, cut)) + cancelUsageLimitContinue = (id: string, cut: usageLimitContinue.UsageLimitContinueIdentity) => + this.mutate(id, (record) => + usageLimitContinue.cancelAgentSessionUsageLimitContinue(record, cut) + ) transitionHandoff = ( sessionId: string, @@ -312,7 +311,7 @@ export class AgentSessionRecordStore extends AgentSessionRecordTabs { /** The journal commit revealing a stopped cut co-writes its withdrawal; a saved one needs none. */ usageLimitContinueCancelReceipt = ( id: string, - cut: usageLimit.UsageLimitContinueIdentity + cut: usageLimitContinue.UsageLimitContinueIdentity ): JournalOperationReceipt | undefined => { const saved = this.getRecord(id)?.usageLimitContinueCancel return saved?.turnItemId === cut.turnItemId && saved.resetsAt === cut.resetsAt @@ -320,7 +319,10 @@ export class AgentSessionRecordStore extends AgentSessionRecordTabs { : this.transactions.receipt((draft) => { const record = draft.records.get(id) if (record) { - draft.records.set(id, usageLimit.cancelAgentSessionUsageLimitContinue(record, cut)) + draft.records.set( + id, + usageLimitContinue.cancelAgentSessionUsageLimitContinue(record, cut) + ) } }) } diff --git a/src/shared/agent-session/lifecycle/agent-session-account-home.ts b/src/shared/agent-session/lifecycle/agent-session-account-home.ts index 96d257eebf1..90678a19b9f 100644 --- a/src/shared/agent-session/lifecycle/agent-session-account-home.ts +++ b/src/shared/agent-session/lifecycle/agent-session-account-home.ts @@ -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' && diff --git a/src/shared/agent-session/lifecycle/agent-session-record.ts b/src/shared/agent-session/lifecycle/agent-session-record.ts index e6b67e63599..fa42e1d26a9 100644 --- a/src/shared/agent-session/lifecycle/agent-session-record.ts +++ b/src/shared/agent-session/lifecycle/agent-session-record.ts @@ -1,4 +1,4 @@ -import { hasValidUsageLimitRecordFields } from './agent-session-usage-limit-continue' +import { isOptionalUsageLimitContinueCancel } from './agent-session-usage-limit-continue' import type { AgentSessionDeathEvidence } from './agent-session-death-evidence' import { isAgentSessionRewindRecord, type AgentSessionRewindRecord } from './agent-session-rewind' import { isAgentSessionLaunchArgs } from './agent-session-launch-args' @@ -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. */ @@ -128,8 +132,6 @@ export type AgentSessionRecord = { /** The permission mode the user chose; Orca's, so a provider's options answer never replaces it. */ permissionMode?: string usageLimitContinueCancel?: { turnItemId: string; resetsAt: number } - /** The account the latest usage-limit cut hit, saved while the child that ran it was live. */ - usageLimitCutAccount?: { turnItemId: string; accountHome: AgentSessionAccountHome } rewind?: AgentSessionRewindRecord conversationCommand?: AgentSessionConversationCommandRecord /** The conversation's one name: Orca's automatic name or the user's rename. Session History, @@ -315,12 +317,13 @@ 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)) && (record.permissionMode === undefined || isBoundedString(record.permissionMode, MAX_ID_LENGTH)) && - hasValidUsageLimitRecordFields(record) && + isOptionalUsageLimitContinueCancel(record.usageLimitContinueCancel) && (record.rewind === undefined || isAgentSessionRewindRecord(record.rewind)) && (record.providerContextBoundary === undefined || isAgentSessionProviderContextBoundary(record.providerContextBoundary)) && diff --git a/src/shared/agent-session/lifecycle/agent-session-usage-limit-continue.ts b/src/shared/agent-session/lifecycle/agent-session-usage-limit-continue.ts index 323c36ed132..38e1978b2a5 100644 --- a/src/shared/agent-session/lifecycle/agent-session-usage-limit-continue.ts +++ b/src/shared/agent-session/lifecycle/agent-session-usage-limit-continue.ts @@ -1,9 +1,4 @@ import type { AgentSessionRecord } from './agent-session-record' -import { - agentSessionAccountHomesEqual, - isAgentSessionAccountHome, - type AgentSessionAccountHome -} from './agent-session-account-home' export type UsageLimitContinueIdentity = { turnItemId: string; resetsAt: number } @@ -23,45 +18,6 @@ export function isOptionalUsageLimitContinueCancel( ) } -/** The account whose usage limit ended a turn; that account's reset is the one that holds it. */ -export type UsageLimitCutAccount = { turnItemId: string; accountHome: AgentSessionAccountHome } - -export function isOptionalUsageLimitCutAccount( - value: unknown -): value is UsageLimitCutAccount | undefined { - return ( - value === undefined || - (typeof value === 'object' && - value !== null && - 'turnItemId' in value && - typeof value.turnItemId === 'string' && - value.turnItemId.length > 0 && - 'accountHome' in value && - isAgentSessionAccountHome(value.accountHome)) - ) -} - -export function hasValidUsageLimitRecordFields(record: { - usageLimitContinueCancel?: unknown - usageLimitCutAccount?: unknown -}): boolean { - return ( - isOptionalUsageLimitContinueCancel(record.usageLimitContinueCancel) && - isOptionalUsageLimitCutAccount(record.usageLimitCutAccount) - ) -} - -export function recordAgentSessionUsageLimitCutAccount( - record: AgentSessionRecord, - cut: UsageLimitCutAccount -): AgentSessionRecord { - const saved = record.usageLimitCutAccount - return saved?.turnItemId === cut.turnItemId && - agentSessionAccountHomesEqual(saved.accountHome, cut.accountHome) - ? record - : { ...record, usageLimitCutAccount: cut, updatedAt: Date.now() } -} - export function cancelAgentSessionUsageLimitContinue( record: AgentSessionRecord, cut: UsageLimitContinueIdentity