mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
Preserve unverifiable timing across older host upgrade
This commit is contained in:
@@ -14,58 +14,60 @@ function userItem(itemId: string): AgentJournalRenderItem {
|
||||
}
|
||||
|
||||
describe('authoritative unknown turn duration at the shared status consumer', () => {
|
||||
it('does not convert a running turn to local Worked for after unverifiable recovery', () => {
|
||||
const user: AgentJournalRenderItem = {
|
||||
itemId: 'orca:u1',
|
||||
revision: 0,
|
||||
sequence: 1,
|
||||
observedAt: 1_000,
|
||||
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hello' }] }
|
||||
}
|
||||
const recoveredTurn: AgentJournalRenderItem = {
|
||||
itemId: 'legacy:codex:s:turn-lifecycle%3At1',
|
||||
revision: 2,
|
||||
sequence: 2,
|
||||
observedAt: 60_000,
|
||||
body: {
|
||||
kind: 'turn',
|
||||
turnId: 't1',
|
||||
userItemId: user.itemId,
|
||||
state: 'unverifiable',
|
||||
startedAt: 1_000
|
||||
it.each([1_000, undefined])(
|
||||
'does not convert a running turn to local Worked for after unverifiable recovery (start %s)',
|
||||
(startedAt) => {
|
||||
const user: AgentJournalRenderItem = {
|
||||
itemId: 'orca:u1',
|
||||
revision: 0,
|
||||
sequence: 1,
|
||||
observedAt: 1_000,
|
||||
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hello' }] }
|
||||
}
|
||||
}
|
||||
const keys = new Set([user.itemId])
|
||||
const running = reduceNativeChatTurnTiming(
|
||||
{},
|
||||
{
|
||||
const recoveredTurn: AgentJournalRenderItem = {
|
||||
itemId: 'legacy:codex:s:turn-lifecycle%3At1',
|
||||
revision: 2,
|
||||
sequence: 2,
|
||||
observedAt: 60_000,
|
||||
body: {
|
||||
kind: 'turn',
|
||||
turnId: 't1',
|
||||
state: 'unverifiable',
|
||||
...(startedAt === undefined ? {} : { startedAt, userItemId: user.itemId })
|
||||
}
|
||||
}
|
||||
const keys = new Set([user.itemId])
|
||||
const running = reduceNativeChatTurnTiming(
|
||||
{},
|
||||
{
|
||||
activeTurnKey: user.itemId,
|
||||
validTurnKeys: keys,
|
||||
isWorking: true,
|
||||
workingStartedAt: 1_000,
|
||||
now: 1_000
|
||||
}
|
||||
)
|
||||
const locallyStopped = reduceNativeChatTurnTiming(running, {
|
||||
activeTurnKey: user.itemId,
|
||||
validTurnKeys: keys,
|
||||
isWorking: true,
|
||||
workingStartedAt: 1_000,
|
||||
now: 1_000
|
||||
isWorking: false,
|
||||
workingStartedAt: null,
|
||||
now: 60_000
|
||||
})
|
||||
const options = {
|
||||
activeTurnKey: user.itemId,
|
||||
isWorking: false,
|
||||
hasCurrentTurnResponse: true,
|
||||
settledByTurn: selectStructuredAgentSettledTurns([user, recoveredTurn])
|
||||
}
|
||||
)
|
||||
const locallyStopped = reduceNativeChatTurnTiming(running, {
|
||||
activeTurnKey: user.itemId,
|
||||
validTurnKeys: keys,
|
||||
isWorking: false,
|
||||
workingStartedAt: null,
|
||||
now: 60_000
|
||||
})
|
||||
const options = {
|
||||
activeTurnKey: user.itemId,
|
||||
isWorking: false,
|
||||
hasCurrentTurnResponse: true,
|
||||
settledByTurn: selectStructuredAgentSettledTurns([user, recoveredTurn])
|
||||
}
|
||||
|
||||
const mounted = selectNativeChatTurnStatuses(locallyStopped, options)
|
||||
const reloaded = selectNativeChatTurnStatuses({}, options)
|
||||
expect(mounted).toEqual(reloaded)
|
||||
expect(mounted.active).toBeNull()
|
||||
expect(mounted.completedByTurn).toEqual({})
|
||||
})
|
||||
const mounted = selectNativeChatTurnStatuses(locallyStopped, options)
|
||||
const reloaded = selectNativeChatTurnStatuses({}, options)
|
||||
expect(mounted).toEqual(reloaded)
|
||||
expect(mounted.active).toBeNull()
|
||||
expect(mounted.completedByTurn).toEqual({})
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['running', 'completed', 'interrupted'] as const)(
|
||||
'suppresses local completion for a host-recorded %s turn without an endpoint',
|
||||
|
||||
@@ -56,11 +56,11 @@ function readTiming(item: AgentJournalRenderItem): StructuredAgentTurnTiming | n
|
||||
* reached through its alias. Rows from older hosts carry no key and fall back
|
||||
* to the nearest user message before them in journal order — the submission
|
||||
* row is written ahead of dispatch, so it always precedes the provider's
|
||||
* turn-start. Rows without `startedAt` (conversation commands) are skipped. */
|
||||
* turn-start. Untimed rows are skipped unless explicitly unverifiable (null). */
|
||||
export function selectStructuredAgentTurnTimings(
|
||||
items: readonly AgentJournalRenderItem[],
|
||||
submissions: readonly AgentJournalSubmission[] = []
|
||||
): ReadonlyMap<string, StructuredAgentTurnTiming> {
|
||||
): ReadonlyMap<string, StructuredAgentTurnTiming | null> {
|
||||
const itemIds = new Set(items.map((item) => item.itemId))
|
||||
const aliases = new Map<string, string>()
|
||||
// Codex folds a send issued mid-turn into the running turn under the SAME provider
|
||||
@@ -70,18 +70,19 @@ export function selectStructuredAgentTurnTimings(
|
||||
aliases.set(submission.providerItemId, agentJournalSubmissionKey(submission.clientMessageId))
|
||||
}
|
||||
}
|
||||
const timings = new Map<string, StructuredAgentTurnTiming>()
|
||||
const timings = new Map<string, StructuredAgentTurnTiming | null>()
|
||||
let precedingUserItemId: string | null = null
|
||||
for (const item of items) {
|
||||
if (item.body.kind === 'message' && item.body.role === 'user') {
|
||||
precedingUserItemId = item.itemId
|
||||
continue
|
||||
}
|
||||
const turn = readAgentJournalTurn(item.body)
|
||||
const timing = readTiming(item)
|
||||
if (!timing) {
|
||||
if (!timing && turn?.state !== 'unverifiable') {
|
||||
continue
|
||||
}
|
||||
const key = readAgentJournalTurn(item.body)?.userItemId
|
||||
const key = turn?.userItemId
|
||||
const userItemId =
|
||||
key === undefined ? precedingUserItemId : itemIds.has(key) ? key : (aliases.get(key) ?? null)
|
||||
if (userItemId !== null) {
|
||||
@@ -108,7 +109,7 @@ export function selectStructuredAgentRunningTurnTiming(
|
||||
|
||||
/** Whole seconds a settled turn ran, or null when the host never observed its end. */
|
||||
export function completedStructuredAgentTurnSeconds(
|
||||
timing: StructuredAgentTurnTiming | undefined
|
||||
timing: StructuredAgentTurnTiming | null | undefined
|
||||
): number | null {
|
||||
if (!timing || (timing.state !== 'completed' && timing.state !== 'interrupted')) {
|
||||
return null
|
||||
@@ -152,7 +153,9 @@ export function selectStructuredAgentSettledTurns(
|
||||
const workedSeconds = completedStructuredAgentTurnSeconds(timing)
|
||||
settled.set(
|
||||
userItemId,
|
||||
workedSeconds === null ? null : { startedAt: timing.startedAt, workedSeconds }
|
||||
workedSeconds === null || timing === null
|
||||
? null
|
||||
: { startedAt: timing.startedAt, workedSeconds }
|
||||
)
|
||||
}
|
||||
return settled
|
||||
|
||||
Reference in New Issue
Block a user