mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
A structured-chat turn used to end by tombstoning its running lifecycle item, which threw away the only durable record of when the turn ended. Completed "Worked for" labels therefore depended on the renderer having observed the turn finish, and vanished on reopen. The lifecycle item is now revised in place, never tombstoned: - running, with startedAt, at the provider's turn start - completed or interrupted, with completedAt, at the provider's terminal frame, a user stop, or a child exit the host observed - unverifiable, with no end, when a cold acquire finds a running row from a generation whose exit nobody observed Both timestamps are the execution host's clock at receipt, captured before the deferred sink, so the completed value is identical on every client and needs no client clock. Codex history restore uses the provider's own second-granular endpoints for turns that predate this change. Desktop and mobile read settled durations off the journal through one shared selector, and anchor the live counter on the host start with the client's local receipt so a skewed client clock never leaks into the label. Locally observed durations remain the fallback for hosts that still tombstone. Timestamps live inside the existing turnLifecycle field, which old clients strip, and every working-state consumer keys on state === 'running', so no capability negotiation is needed.
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { CodexAppServerConnection } from './codex-app-server-connection'
|
|
import type { CodexJournalTranslationAdmission } from './codex-structured-journal-contracts'
|
|
import { createCodexStructuredNotificationRetry } from './codex-structured-notification-retry'
|
|
import type { CodexSession } from './codex-structured-session-state'
|
|
|
|
type Translate = Parameters<typeof createCodexStructuredNotificationRetry>[0]['translate']
|
|
|
|
function sessionWith(connection: CodexAppServerConnection): CodexSession {
|
|
return { connection, ended: false } as CodexSession
|
|
}
|
|
|
|
function translateMock(
|
|
...results: CodexJournalTranslationAdmission[]
|
|
): ReturnType<typeof vi.fn<Translate>> {
|
|
const translate = vi.fn<Translate>()
|
|
for (const result of results) {
|
|
translate.mockReturnValueOnce(result)
|
|
}
|
|
return translate
|
|
}
|
|
|
|
describe('createCodexStructuredNotificationRetry', () => {
|
|
it('replays a backpressured turn boundary with its original receipt time', async () => {
|
|
vi.useFakeTimers()
|
|
try {
|
|
const connection = {
|
|
pauseReading: vi.fn(),
|
|
resumeReading: vi.fn()
|
|
} as unknown as CodexAppServerConnection
|
|
const session = sessionWith(connection)
|
|
const translate = translateMock({ accepted: false, reason: 'backpressure' })
|
|
translate.mockReturnValue({ accepted: true })
|
|
const retries = createCodexStructuredNotificationRetry({
|
|
sessionFor: () => session,
|
|
translate
|
|
})
|
|
|
|
const admission = retries.handle('session-1', 'turn/started', { turn: { id: 't' } }, 1_000)
|
|
expect(admission).toEqual({ accepted: false, reason: 'backpressure' })
|
|
await vi.advanceTimersByTimeAsync(50)
|
|
|
|
expect(translate).toHaveBeenCalledTimes(2)
|
|
expect(translate.mock.calls.map((call) => call[4])).toEqual([1_000, 1_000])
|
|
expect(connection.resumeReading).not.toHaveBeenCalled()
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
|
|
it('queues a later notification behind a pending one without inventing a receipt time', () => {
|
|
const connection = {
|
|
pauseReading: vi.fn(),
|
|
resumeReading: vi.fn()
|
|
} as unknown as CodexAppServerConnection
|
|
const session = sessionWith(connection)
|
|
const translate = translateMock()
|
|
translate.mockReturnValue({ accepted: false, reason: 'backpressure' })
|
|
const retries = createCodexStructuredNotificationRetry({
|
|
sessionFor: () => session,
|
|
translate
|
|
})
|
|
|
|
retries.handle('session-1', 'turn/started', { turn: { id: 't' } }, 1_000)
|
|
retries.handle('session-1', 'item/completed', { item: { id: 'i' } })
|
|
retries.clear('session-1', connection)
|
|
|
|
// Every attempt replays the head of the queue with its own receipt time;
|
|
// the later notification never jumps ahead of it.
|
|
expect(translate.mock.calls.length).toBeGreaterThan(0)
|
|
expect(translate.mock.calls.map((call) => [call[2], call[4]])).toEqual(
|
|
translate.mock.calls.map(() => ['turn/started', 1_000])
|
|
)
|
|
})
|
|
})
|