mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* fix(native-chat): count a turn from the send that opened it The live turn indicator switched on at the submission but anchored its clock at the provider turn-open, so it jumped back by exactly the dispatch latency the moment the turn opened. Measured on a real Claude session: the counter climbed to "Working for 25s", reset to "Working for 0s", then settled "Worked for 26s" — three readings of one turn, from two different instants. The host now resolves the send that opened a turn and publishes it as an additive optional `requestedAt` on the turn lifecycle row. `startedAt` keeps its exact meaning, the provider turn-open, and is never rewritten, so clients that cannot be upgraded see no change to any value they already read. Both providers write it; it is omitted when no send can be named (provider-resumed turns, replayed history). Readers take one origin, `requestedAt ?? startedAt`, for both the live counter and the settled host interval, so the two cannot disagree. The provider's own reported duration keeps outranking the host interval, unchanged. The host-to-local clock conversion is now latched once per turn rather than re-derived per render. `receivedAt - hostNow` carries that sample's one-way delivery latency as well as skew, and the reducer replaces the sample on every frame, so re-deriving imported fresh jitter and could move the anchor later — the same class of backwards jump this change removes. With the conversion fixed, an origin that improves moves the anchor earlier by exactly that much, so displayed elapsed only grows. No monotonicity guard is added; the ordering is structural. Desktop and mobile drove byte-identical copies of the timing hook, so both are collapsed onto one React-free helper in shared. Regression tests drive the origin resolution rather than an already-resolved anchor, assert in milliseconds because second-flooring hides the sub-second case, and include a deliberate host/client skew so a raw timestamp assignment cannot pass on a machine where the two clocks agree. * fix(native-chat): correlate Codex turn origins by echo * fix(native-chat): preserve causal turn timing ownership * fix(native-chat): keep settled turn timing continuous
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types'
|
|
|
|
/** Sends awaiting their echo. A send whose echo never arrives is
|
|
* retired by the journal's pending-submission recovery on exit, not from here. */
|
|
export const MAX_CODEX_PENDING_DISPATCH_ECHOES = 256
|
|
|
|
export type CodexDispatchRequestOrigin = {
|
|
requestedAt: number
|
|
sequence: number
|
|
}
|
|
|
|
/**
|
|
* Which sends this session is still waiting to hear back about, keyed by the
|
|
* client message id Codex echoes on the user message.
|
|
*
|
|
* Keyed rather than ordered on purpose: Codex coalesces a `turn/start` issued
|
|
* while a turn is running into that turn, so two sends can share one turn id and
|
|
* their echoes arrive far apart. Queue position identifies neither.
|
|
*/
|
|
export type CodexDispatchEchoes = {
|
|
/** Arms settlement for a send about to be written; false preserves older waits at capacity. */
|
|
arm: (clientMessageId: string, requestedAt?: number) => boolean
|
|
/** True once, for a send this session armed and has not yet settled. */
|
|
settle: (clientMessageId: string) => boolean
|
|
/** Drops an armed send whose write never reached the provider. */
|
|
disarm: (clientMessageId: string) => void
|
|
/** Submission origin for this exact send, retained until its echo settles it. */
|
|
requestOrigin: (clientMessageId: string) => CodexDispatchRequestOrigin | null
|
|
/** Highest causal sequence assigned to a dispatch in this session. */
|
|
latestSequence: () => number
|
|
clear: () => void
|
|
readonly size: number
|
|
}
|
|
|
|
export function createCodexDispatchEchoes(): CodexDispatchEchoes {
|
|
const armed = new Map<string, { requestedAt: number | null; sequence: number }>()
|
|
let nextSequence = 0
|
|
return {
|
|
arm(clientMessageId, requestedAt) {
|
|
const existing = armed.get(clientMessageId)
|
|
if (existing) {
|
|
if (existing.requestedAt === null && requestedAt !== undefined) {
|
|
existing.requestedAt = requestedAt
|
|
}
|
|
return true
|
|
}
|
|
if (armed.size >= MAX_CODEX_PENDING_DISPATCH_ECHOES) {
|
|
return false
|
|
}
|
|
armed.set(clientMessageId, { requestedAt: requestedAt ?? null, sequence: nextSequence++ })
|
|
return true
|
|
},
|
|
settle: (clientMessageId) => armed.delete(clientMessageId),
|
|
disarm: (clientMessageId) => void armed.delete(clientMessageId),
|
|
requestOrigin: (clientMessageId) => {
|
|
const origin = armed.get(clientMessageId)
|
|
return origin?.requestedAt === null || origin === undefined
|
|
? null
|
|
: { requestedAt: origin.requestedAt, sequence: origin.sequence }
|
|
},
|
|
latestSequence: () => nextSequence - 1,
|
|
clear: () => {
|
|
armed.clear()
|
|
nextSequence = 0
|
|
},
|
|
get size() {
|
|
return armed.size
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The user-message echo a settlement is read off, or null for any other item. */
|
|
export function readCodexDispatchEcho(
|
|
item: { type: string; id: string } & Record<string, unknown>,
|
|
identity: AgentJournalItemIdentity
|
|
): { clientMessageId: string; providerIdentity: AgentJournalItemIdentity } | null {
|
|
if (item.type !== 'userMessage' || identity.provider !== 'codex') {
|
|
return null
|
|
}
|
|
const clientMessageId = item.clientId
|
|
return typeof clientMessageId === 'string' && clientMessageId.length > 0
|
|
? { clientMessageId, providerIdentity: identity }
|
|
: null
|
|
}
|