mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
fix(native-chat): a message the host keeps refusing past its window comes back
A host that refuses a send before looking up its id (a host with structured chats turned off) refuses every resend the same way. Since a message past the host's window is no longer handed back on wake, such a message was resent forever and held up everything queued behind it. Now, past the host's window for the id, an answer the host itself gave that settles nothing (a refusal returned or thrown, or a call turned away) hands the message back with "couldn't confirm" words, unless the journal shows its row. Only a lost connection keeps it resending, since that says nothing about the host. Derived from that answer and the id's own time; nothing new is stored. Also corrects the host-window docstring, which still said an answer that never comes ends it.
This commit is contained in:
@@ -63,7 +63,7 @@ describe('a queued draft handed off under a fresh submission id', () => {
|
||||
}
|
||||
},
|
||||
'draft',
|
||||
{ firstAttempt: false, answersProve: true, journalHasRow: false }
|
||||
{ firstAttempt: false, answersProve: true, journalHasRow: false, outlivedHostWindow: false }
|
||||
)
|
||||
).toEqual({ kind: 'recorded' })
|
||||
})
|
||||
|
||||
@@ -90,7 +90,8 @@ function refused(
|
||||
const FIRST: StructuredAgentSessionOutboxSettlementContext = {
|
||||
firstAttempt: true,
|
||||
answersProve: true,
|
||||
journalHasRow: false
|
||||
journalHasRow: false,
|
||||
outlivedHostWindow: false
|
||||
}
|
||||
const RESEND_PROVING: StructuredAgentSessionOutboxSettlementContext = {
|
||||
...FIRST,
|
||||
@@ -617,6 +618,48 @@ describe('applying a settlement', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("past the host's window, a host answer that settles nothing", () => {
|
||||
const PAST = { ...RESEND_OLD_HOST, outlivedHostWindow: true }
|
||||
const hostAnswers: [string, StructuredAgentSessionSendAnswer][] = [
|
||||
['a refusal it returned', refused('agent_session_journal_unreadable')],
|
||||
[
|
||||
'a refusal it threw',
|
||||
{
|
||||
kind: 'thrown',
|
||||
refusal: { kind: 'refused', code: 'structured_agent_session_unsupported' },
|
||||
rpcCode: undefined
|
||||
}
|
||||
],
|
||||
['a call it turned away', { kind: 'thrown', refusal: undefined, rpcCode: 'method_not_found' }]
|
||||
]
|
||||
|
||||
it.each(hostAnswers)(
|
||||
'%s: comes back to check the chat, or is the row it shows',
|
||||
(_label, answer) => {
|
||||
expect(settleStructuredAgentSessionSendAnswer(answer, ID, RESEND_OLD_HOST).kind).toBe(
|
||||
'unanswered'
|
||||
)
|
||||
expect(settleStructuredAgentSessionSendAnswer(answer, ID, PAST)).toEqual({
|
||||
kind: 'returned',
|
||||
words: ['sendOutcomeLost']
|
||||
})
|
||||
expect(
|
||||
settleStructuredAgentSessionSendAnswer(answer, ID, { ...PAST, journalHasRow: true })
|
||||
).toEqual({ kind: 'recorded' })
|
||||
}
|
||||
)
|
||||
|
||||
it('but a lost connection is no answer from the host, so it still goes again', () => {
|
||||
expect(
|
||||
settleStructuredAgentSessionSendAnswer(
|
||||
{ kind: 'thrown', refusal: undefined, rpcCode: 'runtime_timeout' },
|
||||
ID,
|
||||
PAST
|
||||
)
|
||||
).toEqual({ kind: 'unanswered' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('the host window bounds every entry', () => {
|
||||
const pastWindow = MADE_AT + AGENT_SESSION_MAX_OPERATION_REPLAY_AGE_MS + 1
|
||||
const reading = {
|
||||
|
||||
@@ -57,6 +57,9 @@ export type StructuredAgentSessionOutboxSettlementContext = {
|
||||
answersProve: boolean
|
||||
/** The loaded journal holds a row for this id. */
|
||||
journalHasRow: boolean
|
||||
/** The host's window for this id has closed, by the id's own time
|
||||
* (`structuredAgentSessionEntryOutlivedHostWindow`). */
|
||||
outlivedHostWindow: boolean
|
||||
}
|
||||
|
||||
/** Words for a message Orca can no longer settle with the host: an earlier attempt may already be
|
||||
@@ -135,10 +138,34 @@ function thrownSettlement(
|
||||
}
|
||||
|
||||
/** The host's answer to one attempt, as one of the three ends. */
|
||||
/** Whether the host itself answered: a refusal it returned or threw, or a code it turned the call
|
||||
* away with. Anything else is the transport, which says nothing about the host. */
|
||||
function answeredByHost(answer: StructuredAgentSessionSendAnswer): boolean {
|
||||
return (
|
||||
answer.kind === 'result' ||
|
||||
answer.refusal !== undefined ||
|
||||
agentSessionRpcErrorFailure(answer.rpcCode).kind === 'refused'
|
||||
)
|
||||
}
|
||||
|
||||
/** The host's answer to one attempt, as one of the three ends. Past the host's window for the id,
|
||||
* an answer the host gave that settles nothing never will (a host that refuses before looking the
|
||||
* id up refuses it every time), so the journal decides; lost contact still only goes again. */
|
||||
export function settleStructuredAgentSessionSendAnswer(
|
||||
answer: StructuredAgentSessionSendAnswer,
|
||||
clientMessageId: string,
|
||||
context: StructuredAgentSessionOutboxSettlementContext
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
const settlement = settleAnswer(answer, clientMessageId, context)
|
||||
return settlement.kind === 'unanswered' && context.outlivedHostWindow && answeredByHost(answer)
|
||||
? settledByJournal(context)
|
||||
: settlement
|
||||
}
|
||||
|
||||
function settleAnswer(
|
||||
answer: StructuredAgentSessionSendAnswer,
|
||||
clientMessageId: string,
|
||||
context: StructuredAgentSessionOutboxSettlementContext
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
if (answer.kind === 'thrown') {
|
||||
return thrownSettlement(answer, context)
|
||||
@@ -177,8 +204,9 @@ export function structuredAgentSessionEntryHostWindowEndsAt(
|
||||
return madeAt === null ? null : madeAt + AGENT_SESSION_MAX_OPERATION_REPLAY_AGE_MS
|
||||
}
|
||||
|
||||
/** Whether the host can no longer settle this id, so a host that keeps failing the request, or an
|
||||
* answer that never comes, can't hold the entry forever. */
|
||||
/** Whether the host can no longer settle this id. Past it, a host answer that settles nothing
|
||||
* hands the entry back for the journal to decide, and a live journal settles one only an owed
|
||||
* answer would; lost contact alone keeps it, since it says nothing about the host. */
|
||||
export function structuredAgentSessionEntryOutlivedHostWindow(
|
||||
entry: Pick<StructuredAgentSessionOutboxEntry, 'clientMessageId'>,
|
||||
now: number
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('outbox queue delivery', () => {
|
||||
}
|
||||
},
|
||||
'client-1',
|
||||
{ firstAttempt: true, answersProve: false, journalHasRow: false }
|
||||
{ firstAttempt: true, answersProve: false, journalHasRow: false, outlivedHostWindow: false }
|
||||
)
|
||||
const stopped = stopStructuredAgentSessionOutbox([staged], [], 'client-1', 'stop-1').entries
|
||||
for (const entries of [stopped, [staged]]) {
|
||||
|
||||
Reference in New Issue
Block a user