mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
* fix(native-chat): bound structured chat launch * Fix post-merge test hygiene * Make structured fallback settlement exhaustive * fix(native-chat): release sessions after root exit * chore(i18n): remove legacy fallback copy * test(native-chat): remove terminal fallback census * docs(native-chat): clarify root-exit lease proof * chore(native-chat): drop unrelated formatting * fix native chat launch visibility * test(native-chat): split message rail windowing coverage * fix(native-chat): keep transport gating render-pure * fix(native-chat): coordinate launch prompt settlement * test(native-chat): align unified close ownership * fix(native-chat): correct lifecycle imports and test typing * fix(native-chat): fence restored launch cancellations * fix(native-chat): fence authoritative cancellation snapshots
194 lines
6.1 KiB
TypeScript
194 lines
6.1 KiB
TypeScript
import type { AgentJournalMessageItem, AgentJournalSubmission } from './agent-session-journal-types'
|
|
import { agentSessionRefusalOperationState } from './agent-session-refusal-retry'
|
|
import type { AgentSessionWireRefusalCode } from './agent-session-wire'
|
|
import { structuredAgentSessionPayloadFingerprint } from './structured-agent-session-mutation'
|
|
import { DISPATCH_REJECTED_CANCELLED } from './structured-agent-session-dispatch-rejection'
|
|
|
|
export type StructuredAgentSessionOutboxState = 'queued' | 'dispatching' | 'unconfirmed'
|
|
|
|
export type StructuredAgentSessionOutboxEntry = {
|
|
clientMessageId: string
|
|
sessionId: string
|
|
body: AgentJournalMessageItem
|
|
previewUris: string[]
|
|
state: StructuredAgentSessionOutboxState
|
|
queuedAt: number
|
|
lastAttemptAt: number | null
|
|
retryAfterUnknownSubmittedAt: number | null
|
|
source?: 'launch'
|
|
}
|
|
|
|
export type StructuredAgentSessionAttachment = {
|
|
path: string
|
|
previewUri: string
|
|
}
|
|
|
|
export function structuredAgentSessionSendBody(
|
|
text: string,
|
|
attachments: readonly StructuredAgentSessionAttachment[]
|
|
): AgentJournalMessageItem {
|
|
return {
|
|
kind: 'message',
|
|
role: 'user',
|
|
blocks: [
|
|
...(text.trim().length > 0 ? [{ type: 'text' as const, text: text.trimEnd() }] : []),
|
|
...attachments.map((attachment) => ({ type: 'image-ref' as const, path: attachment.path }))
|
|
]
|
|
}
|
|
}
|
|
|
|
export function createStructuredAgentSessionOutboxEntry(args: {
|
|
clientMessageId: string
|
|
sessionId: string
|
|
text: string
|
|
attachments: readonly StructuredAgentSessionAttachment[]
|
|
queuedAt: number
|
|
}): StructuredAgentSessionOutboxEntry {
|
|
return {
|
|
clientMessageId: args.clientMessageId,
|
|
sessionId: args.sessionId,
|
|
body: structuredAgentSessionSendBody(args.text, args.attachments),
|
|
previewUris: args.attachments.map((attachment) => attachment.previewUri),
|
|
state: 'queued',
|
|
queuedAt: args.queuedAt,
|
|
lastAttemptAt: null,
|
|
retryAfterUnknownSubmittedAt: null
|
|
}
|
|
}
|
|
|
|
export function updateStructuredAgentSessionOutboxEntry(
|
|
entries: readonly StructuredAgentSessionOutboxEntry[],
|
|
id: string,
|
|
update: (entry: StructuredAgentSessionOutboxEntry) => StructuredAgentSessionOutboxEntry | null
|
|
): StructuredAgentSessionOutboxEntry[] {
|
|
return entries.flatMap((entry) => {
|
|
if (entry.clientMessageId !== id) {
|
|
return [entry]
|
|
}
|
|
const next = update(entry)
|
|
return next ? [next] : []
|
|
})
|
|
}
|
|
|
|
export function requeueStructuredAgentSessionSendRefusal(
|
|
entry: StructuredAgentSessionOutboxEntry,
|
|
code: AgentSessionWireRefusalCode,
|
|
createOperationId: () => string,
|
|
retainOperationId = false
|
|
): StructuredAgentSessionOutboxEntry {
|
|
const refusalState = agentSessionRefusalOperationState('agentSession.send', code)
|
|
if (
|
|
refusalState !== 'settled-rejected' ||
|
|
retainOperationId ||
|
|
entry.state === 'unconfirmed' ||
|
|
entry.retryAfterUnknownSubmittedAt !== null
|
|
) {
|
|
return { ...entry, state: 'queued' }
|
|
}
|
|
return {
|
|
...entry,
|
|
clientMessageId: createOperationId(),
|
|
state: 'queued',
|
|
lastAttemptAt: null,
|
|
retryAfterUnknownSubmittedAt: null
|
|
}
|
|
}
|
|
|
|
export function reconcileStructuredAgentSessionOutbox(
|
|
entries: readonly StructuredAgentSessionOutboxEntry[],
|
|
submissions: readonly AgentJournalSubmission[]
|
|
): StructuredAgentSessionOutboxEntry[] {
|
|
const settled = new Map(submissions.map((entry) => [entry.clientMessageId, entry]))
|
|
return entries.flatMap((entry) => {
|
|
const submission = settled.get(entry.clientMessageId)
|
|
if (submission?.dispatchState === 'accepted') {
|
|
return []
|
|
}
|
|
if (
|
|
submission?.dispatchState === 'rejected' &&
|
|
submission.reason === DISPATCH_REJECTED_CANCELLED
|
|
) {
|
|
return []
|
|
}
|
|
if (submission?.dispatchState === 'pending') {
|
|
return entry.state === 'dispatching' ? [entry] : [{ ...entry, state: 'dispatching' as const }]
|
|
}
|
|
if (
|
|
submission?.dispatchState === 'unknown' &&
|
|
entry.retryAfterUnknownSubmittedAt !== -1 &&
|
|
entry.retryAfterUnknownSubmittedAt !== submission.submittedAt
|
|
) {
|
|
return [{ ...entry, state: 'unconfirmed' as const }]
|
|
}
|
|
return [entry]
|
|
})
|
|
}
|
|
|
|
export function parseStructuredAgentSessionOutboxEntry(
|
|
value: unknown,
|
|
sessionId: string
|
|
): StructuredAgentSessionOutboxEntry | null {
|
|
if (typeof value !== 'object' || value === null) {
|
|
return null
|
|
}
|
|
const entry = value as Partial<StructuredAgentSessionOutboxEntry>
|
|
const body = entry.body
|
|
if (
|
|
entry.sessionId !== sessionId ||
|
|
typeof entry.clientMessageId !== 'string' ||
|
|
typeof entry.queuedAt !== 'number' ||
|
|
!body ||
|
|
body.kind !== 'message' ||
|
|
body.role !== 'user' ||
|
|
!Array.isArray(body.blocks) ||
|
|
!Array.isArray(entry.previewUris) ||
|
|
!entry.previewUris.every((uri) => typeof uri === 'string') ||
|
|
!['queued', 'dispatching', 'unconfirmed'].includes(entry.state ?? '')
|
|
) {
|
|
return null
|
|
}
|
|
return {
|
|
clientMessageId: entry.clientMessageId,
|
|
sessionId,
|
|
body,
|
|
previewUris: entry.previewUris,
|
|
state: entry.state as StructuredAgentSessionOutboxState,
|
|
queuedAt: entry.queuedAt,
|
|
lastAttemptAt: typeof entry.lastAttemptAt === 'number' ? entry.lastAttemptAt : null,
|
|
retryAfterUnknownSubmittedAt:
|
|
typeof entry.retryAfterUnknownSubmittedAt === 'number'
|
|
? entry.retryAfterUnknownSubmittedAt
|
|
: null,
|
|
...(entry.source === 'launch' ? { source: 'launch' as const } : {})
|
|
}
|
|
}
|
|
|
|
export function structuredAgentSessionSendRequest(
|
|
entry: StructuredAgentSessionOutboxEntry,
|
|
expectedRuntimeFence: number
|
|
): Record<string, unknown> {
|
|
const fields = { body: entry.body }
|
|
return {
|
|
envelope: {
|
|
sessionId: entry.sessionId,
|
|
clientOperationId: entry.clientMessageId,
|
|
expectedRuntimeFence,
|
|
payloadFingerprint: structuredAgentSessionPayloadFingerprint({
|
|
method: 'agentSession.send',
|
|
sessionId: entry.sessionId,
|
|
fields
|
|
})
|
|
},
|
|
...fields
|
|
}
|
|
}
|
|
|
|
export type StructuredAgentSessionSendFailure = 'delivery-unknown' | 'failed'
|
|
|
|
export function classifyStructuredAgentSessionSendFailure(
|
|
error: unknown,
|
|
isDeliveryUnknown: (error: unknown) => boolean
|
|
): StructuredAgentSessionSendFailure {
|
|
return isDeliveryUnknown(error) ? 'delivery-unknown' : 'failed'
|
|
}
|