mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
* fix(native-chat): an open chat moves to the account you switch to before its next message A running chat's agent read its account only when it started, and nothing compared it with the account a start would use now, so open chats kept answering on the old account after a switch. The agent a chat runs now records the account its launch used, and the host's replace-before-the- next-send check replaces an idle one whose account is no longer what a start would use. A chat mid-reply finishes on the old account first. Claude chats follow; agents whose chats keep the account they were created on are never compared. A chat's model-list reads and saves now use the account it runs on, so a moved chat no longer writes its new account's list under its first account (STA-11281). Tracked in STA-11211. * fix(native-chat): declare the launch-account read for the runtime layer that wires the host * fix(claude): find custom chat history before managed-account resume * test(claude): assign the complete custom account home * feat(native-chat): offer account switching and hand-off at usage limits * fix: track the provider in account-list reload effects * fix(native-chat): target recovery actions at the chat host and transcript * fix(native-chat): verify account recovery for each chat on its host * refactor(native-chat): retain the existing image-support result * fix(native-chat): withdraw account recovery after failed eligibility reads * fix(native-chat): leave unavailable recovery facts absent * Align Japanese usage-limit recovery copy with catalog convention * Preserve complete chat context through usage-limit recovery * Keep Continue callback focus optional * Keep recovery fixtures and account effects type safe * Keep chat recovery on native sources and native accounts * Validate native recovery files after path admission * Use the optional cursor shape in recovery fixture * fix(native-chat): exclude source CLI family from limit handoff * Resolve recovery context in the owning workspace directory * Add the continue-at-usage-limit-reset setting and its chat row lines * Continue chats automatically after usage limits reset * Fix automatic continuation fairness, lifecycle and host settings * fix: bind automatic continuation withdrawal to accepted actions * fix: withdraw source auto continuation after handoff starts * Read unopened chats with the saved-record reader for usage-limit continue Unopened chats were read through the raw-row replay, which no longer sees chats saved as keyed records, so a usage-limited chat that was not open was never continued after its reset. Read them the way the rest of the app reads a chat without opening it. Also use the shared visible-tab check, keep the ordinary rewind result without a sequence as main does, and adapt two tests to the current queued-message and saved-status behavior. * Keep a usage-limited turn idle after any rewind restores it Rewind completion read the resulting conversation through the raw-row replay, which no longer sees chats saved as keyed records, and a forked rewind wrote its completion before the new history's records. So a limited turn that a rewind brought back was continued automatically after its reset. The completion now reads the saved result the way the app reopens a chat, and the forked rewind writes its completion after the new history in the same transaction. * Type the usage-limit auto-continue test mock without an assertion * Fix type errors from main's settings, failure-wording and refusal changes * Keep ordinary settings replies when the paired host changes mid-call, and list the cancel method in wire contracts Settings reads and writes that don't carry the automatic-continue choice go through the common runtime call again, so a reply that completes after the paired host is removed still merges, as before this change. Only a write of that choice stays pinned to the host it was made for. The structured method count and the cross-version method manifest now include the cancel method. * Keep a stopped turn's automatic continue withdrawn when its usage limit arrives later A person could Stop a running Codex turn, have the turn then end with a usage-limit failure, and still get an automatic "Continue where you left off." after the reset, because Stop had nothing to cancel yet. The journal write that first reveals a limited turn the latest personal Stop owns now saves the same exact cancellation in that transaction, and a Stop accepted just after the limit landed withdraws it too. Resume, deleting a queued card and restarting keep it withdrawn; manual Continue and later turns are unaffected. * Bound the automatic-continue sweep and keep notice rows from re-rendering The 10-second sweep now does nothing while the setting is off, republishes only chats with a scheduled continuation, reads at most three unopened chats per pass, and keeps going when one chat's saved history can't be read (that chat is retried after the others instead of stopping every chat on the host). Settings writes republish only when the switch changes. Working out the usage-limit cut no longer indexes the whole conversation on every streamed row. The chat's notice rows no longer re-render for status changes they don't show. Adds operation-count and render-count tests for these bounds. * Test that streamed journal commits after a Stop scan no history and build no command index Drives real Codex rows through the host at 20 and 200 history rows: each streamed commit runs the Stop's commit hook and the status projection's cut reader with no newest-turn scan, no render and no full command index; the next turn end scans once and renders nothing. * Leave a usage limit manual once its reset passed more than a day ago Turning automatic continue on later no longer sends into chats whose limit reset days ago. Eligibility, which the row's summary, the sweep and the pre-send recheck all read, now requires the reset to be within the last 24 hours; older limits show the usual manual actions and are never continued unasked. * Show only manual actions for a usage limit Orca can never continue A limit more than a day past its reset is never continued, but its row still showed the setting link, inviting people to turn on a switch that could not help that chat. One shared check now decides whether a cut can ever be scheduled; the status summary and the sweep's eligibility both use it, so past the bound the row keeps only Continue Now, Hand Off and Add Account. A row already on screen when the day passes drops the line on the shared minute clock; the host alone decides sends. * List every journal row kind in the Stop cut filter The type-aware lint requires exhaustive switches; epoch, tombstone, submission and queue rows still cannot reveal a cut. * Type the status-subscription mock instead of casting its handler The changed-code casting gate rejects type assertions; the mock now carries the real subscription signature, so the recorded handler is typed. * Open store-less conversations without the Stop withdrawal hook Acquisition-only opens have no record store, so a person's Stop followed by a limited turn end made the journal commit throw and drop the turn end. Install the withdrawal only when a store exists; without one there is no continuation to withdraw. Also narrow the status summary before reading it and type the sweep test's send mock.
253 lines
9.0 KiB
TypeScript
253 lines
9.0 KiB
TypeScript
import { expect } from 'vitest'
|
||
import { AGENT_SESSION_CURRENT_REQUESTS_CLIENT_CAPABILITY } from '../../../src/shared/agent-session/requests/agent-session-current-requests-capability'
|
||
|
||
export const SESSION = 'session-alpha'
|
||
export const WORKSPACE = 'workspace-1'
|
||
export const THREAD = '019fd532-7c11-7a90-b6de-4e1a2c3d5f60'
|
||
export const NOW = 1_800_000_000_000
|
||
export const ATTENTION_READ = {
|
||
sessionId: SESSION,
|
||
observedCursor: { epoch: 'attention-epoch', sequence: 7 }
|
||
} as const
|
||
export const REWIND_METHOD = 'agentSession.rewind'
|
||
export const CONVERSATION_OUTLINE_METHOD = 'agentSession.conversationOutline'
|
||
export const ITEM_DETAIL_METHOD = 'agentSession.item'
|
||
export const STATUS_FEED_METHOD = 'agentSession.subscribeStatus'
|
||
export const TURN_COMPLETION_FEED_METHOD = 'agentSession.subscribeTurnCompletions'
|
||
|
||
// Declare each method’s expected host call and result; registration or a non-method_not_found error cannot prove execution.
|
||
// Omit result only for streams that legitimately open without a reply.
|
||
export const STRUCTURED_CALLS: {
|
||
method: string
|
||
hostMethod: string | null
|
||
clientCapability?: string
|
||
result?: Record<string, unknown>
|
||
}[] = [
|
||
{ method: 'agentSession.createSupport', hostMethod: null, result: { supported: true } },
|
||
{
|
||
method: 'agentSession.create',
|
||
hostMethod: 'attach',
|
||
result: { ok: true, replayed: false, value: { sessionId: SESSION } }
|
||
},
|
||
{
|
||
method: 'agentSession.ensure',
|
||
hostMethod: 'attach',
|
||
result: { ok: true, replayed: false, value: { sessionId: SESSION } }
|
||
},
|
||
{
|
||
method: 'agentSession.conversationCommand',
|
||
hostMethod: 'conversationCommand',
|
||
result: { ok: true, value: { command: 'compact', state: 'completed' } }
|
||
},
|
||
{ method: 'agentSession.send', hostMethod: 'send', result: { ok: true, replayed: false } },
|
||
{ method: 'agentSession.cancel', hostMethod: 'cancel', result: { ok: true, replayed: false } },
|
||
// Only capability-gated clients call these methods, even before advertisement.
|
||
{
|
||
method: 'agentSession.queuedMessageSend',
|
||
hostMethod: 'queuedMessageSend',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
{
|
||
method: 'agentSession.queuedMessageDelete',
|
||
hostMethod: 'queuedMessageDelete',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
{
|
||
method: 'agentSession.queuedMessageMove',
|
||
hostMethod: 'queuedMessageMove',
|
||
result: { ok: true, value: { messageId: 'queued-1', status: 'moved' } }
|
||
},
|
||
{
|
||
method: 'agentSession.queuedMessagesResume',
|
||
hostMethod: 'queuedMessagesResume',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
// Editing a queued card in place. Clients call these only on a host advertising
|
||
// `agent-session.queued-message-edit.v1`; an older host answers `method_not_found`.
|
||
{
|
||
method: 'agentSession.queuedMessageUpdate',
|
||
hostMethod: 'queuedMessageUpdate',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
{
|
||
method: 'agentSession.queuedMessageEditHold',
|
||
hostMethod: 'queuedMessageEditHold',
|
||
result: { status: 'held' }
|
||
},
|
||
{
|
||
method: REWIND_METHOD,
|
||
hostMethod: 'rewind',
|
||
result: { ok: true, replayed: false, value: { itemId: 'item-1', epoch: 'rewound-epoch' } }
|
||
},
|
||
{ method: 'agentSession.close', hostMethod: 'close', result: { ok: true } },
|
||
{
|
||
method: 'agentSession.sleepWorkspace',
|
||
hostMethod: 'sleepWorkspace',
|
||
result: { stoppedSessionIds: [] }
|
||
},
|
||
{
|
||
method: 'agentSession.respondToApproval',
|
||
hostMethod: 'respondToPrompt',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
{
|
||
method: 'agentSession.respondToQuestion',
|
||
hostMethod: 'respondToPrompt',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
{
|
||
method: 'agentSession.setOption',
|
||
hostMethod: 'setOption',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
{
|
||
method: 'agentSession.threadGoal',
|
||
hostMethod: 'changeThreadGoal',
|
||
result: { ok: true, replayed: false }
|
||
},
|
||
{
|
||
method: 'agentSession.handoffStatus',
|
||
hostMethod: 'handoffStatus',
|
||
result: { owner: 'native' }
|
||
},
|
||
{
|
||
method: 'agentSession.options',
|
||
hostMethod: 'readOptions',
|
||
result: { current: { model: 'gpt-live' } }
|
||
},
|
||
{
|
||
method: 'agentSession.continuationContext',
|
||
hostMethod: 'continuationContext',
|
||
result: {
|
||
status: 'current',
|
||
capturedText: 'Saved context',
|
||
transcriptPath: '/handoff/chat.txt'
|
||
}
|
||
},
|
||
{
|
||
method: 'agentSession.modelCatalog',
|
||
hostMethod: 'modelCatalog',
|
||
result: { origin: 'unknown' }
|
||
},
|
||
{
|
||
method: 'agentSession.commands',
|
||
hostMethod: 'readCommands',
|
||
result: { commands: [{ name: 'clear', kind: 'command' }] }
|
||
},
|
||
{
|
||
method: 'agentSession.reveal',
|
||
hostMethod: 'revealSession',
|
||
result: { ok: true, sessionId: SESSION, workspaceId: WORKSPACE, agent: 'codex', readable: true }
|
||
},
|
||
// The stub has no record, so readVisual must return its typed refusal; older hosts return method_not_found.
|
||
{
|
||
method: 'agentSession.readVisual',
|
||
hostMethod: null,
|
||
result: { ok: false, error: 'session_not_found' }
|
||
},
|
||
// A no-op on a host that starts an agent only for work; it still builds the host.
|
||
{ method: 'agentSession.hold', hostMethod: null, result: { held: true } },
|
||
// Restart-resume methods negotiate absence through explicit method_not_found, never silence.
|
||
{
|
||
method: 'agentSession.restartResumable',
|
||
hostMethod: 'restartResumableList',
|
||
result: { sessions: [] }
|
||
},
|
||
{
|
||
method: 'agentSession.restartResumableDismiss',
|
||
hostMethod: 'restartResumableDismiss',
|
||
result: { dismissed: 0 }
|
||
},
|
||
// Reattaching alone is nothing now, so this answers that nothing was resumed.
|
||
{ method: 'agentSession.restartResume', hostMethod: null, result: { results: [] } },
|
||
{
|
||
method: 'agentSession.restartContinue',
|
||
hostMethod: 'restartContinueAll',
|
||
result: { resumed: [], continued: [] }
|
||
},
|
||
// Continue on a reply an Orca stop cut off. Clients call it only on a host advertising it.
|
||
{
|
||
method: 'agentSession.continueInterrupted',
|
||
hostMethod: 'continueInterrupted',
|
||
result: { sessionId: SESSION, outcome: 'superseded' }
|
||
},
|
||
// Cancel a scheduled automatic continuation. Clients call it only on a host advertising
|
||
// `agent-session.usage-limit-auto-continue.v1`; an older host answers `method_not_found`.
|
||
{
|
||
method: 'agentSession.cancelUsageLimitContinue',
|
||
hostMethod: 'cancelUsageLimitContinue',
|
||
result: { ok: true }
|
||
},
|
||
// Name a chat again after naming failed. Clients call it only on a host advertising
|
||
// `agent-session.structured.regenerate-name.v1`; an older host answers `method_not_found`.
|
||
{
|
||
method: 'agentSession.regenerateName',
|
||
hostMethod: 'regenerateConversationName',
|
||
result: { accepted: true }
|
||
},
|
||
// Save the user's chat name. Clients call it only on a host advertising
|
||
// `agent-session.structured.rename.v1`; an older host answers `method_not_found`.
|
||
{
|
||
method: 'agentSession.rename',
|
||
hostMethod: 'setUserConversationName',
|
||
result: { name: 'Release notes', applied: true }
|
||
},
|
||
{ method: 'agentSession.release', hostMethod: null, result: { released: true } },
|
||
{
|
||
method: 'agentSession.history',
|
||
hostMethod: 'history',
|
||
result: { ok: true, page: { items: [] } }
|
||
},
|
||
{
|
||
method: 'agentSession.currentRequests',
|
||
hostMethod: 'readCurrentRequests',
|
||
clientCapability: AGENT_SESSION_CURRENT_REQUESTS_CLIENT_CAPABILITY,
|
||
result: { json: '[]', nextCursor: null }
|
||
},
|
||
{
|
||
method: CONVERSATION_OUTLINE_METHOD,
|
||
hostMethod: 'journalSnapshot',
|
||
result: { sessionId: SESSION, entries: [], omittedEntries: 0 }
|
||
},
|
||
{
|
||
method: ITEM_DETAIL_METHOD,
|
||
hostMethod: 'journalSnapshot',
|
||
result: { item: null }
|
||
},
|
||
// A subscription that opens with nothing to say answers with no reply at all,
|
||
// so reaching the host is the only signal that the gate opened.
|
||
{ method: 'agentSession.subscribe', hostMethod: 'subscribe' },
|
||
// The status feed opens with a snapshot of every session, so its first reply is the contract.
|
||
{
|
||
method: STATUS_FEED_METHOD,
|
||
hostMethod: 'subscribeStatus',
|
||
result: { type: 'snapshot', sessions: [] }
|
||
},
|
||
// Completion is an edge, so an empty opening stream is valid; the host-call delta proves admission.
|
||
{
|
||
method: TURN_COMPLETION_FEED_METHOD,
|
||
hostMethod: 'subscribeTurnCompletions'
|
||
},
|
||
// Attention acknowledgement uses the runtime store; only its reply proves admission.
|
||
{
|
||
method: 'agentSession.acknowledgeAttention',
|
||
hostMethod: null,
|
||
result: { acknowledged: true }
|
||
},
|
||
// Teardown runs through the runtime's subscription registry rather than the
|
||
// host, so its reply is the only signal that the gate opened.
|
||
{ method: 'agentSession.unsubscribe', hostMethod: null, result: { unsubscribed: true } },
|
||
// Before #25845 agents came from the installed host; later builds read registrations directly. Both must list Codex.
|
||
{
|
||
method: 'agentSession.agents',
|
||
hostMethod: null,
|
||
result: { agents: expect.arrayContaining([expect.objectContaining({ agent: 'codex' })]) }
|
||
},
|
||
// Availability uses a runtime resource; its first generation proves admission.
|
||
{
|
||
method: 'agentSession.subscribeAvailability',
|
||
hostMethod: null,
|
||
result: { type: 'availability', agents: [] }
|
||
}
|
||
]
|