From 40f39e72f94bb4596f45e935a165d9fa478b85fa Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Thu, 12 Nov 2026 22:16:17 +0000 Subject: [PATCH] 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. --- .../structured-agent-session-acquisition.ts | 1 + ...t-session-usage-limit-account-hold.test.ts | 29 +++++++++--- ...agent-session-usage-limit-continue.test.ts | 1 - ...agent-session-status-journal-projection.ts | 3 +- ...tructured-agent-session-host-background.ts | 12 ----- ...-agent-session-usage-limit-account-hold.ts | 14 +++--- ...uctured-agent-session-usage-limit-chain.ts | 35 +++++++++------ ...ured-agent-session-usage-limit-continue.ts | 11 +++-- .../ownership/agent-session-owner-proof.ts | 16 +++++-- .../records/agent-session-record-store.ts | 18 ++++---- .../lifecycle/agent-session-account-home.ts | 20 +++++++++ .../lifecycle/agent-session-record.ts | 13 +++--- .../agent-session-usage-limit-continue.ts | 44 ------------------- 13 files changed, 112 insertions(+), 105 deletions(-) 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