From e08616e95df69c76691cbc11400f046cf93e741a Mon Sep 17 00:00:00 2001 From: Venius24 Date: Thu, 29 Oct 2026 15:35:49 +0500 Subject: [PATCH] fix(orchestration): name the provider's words when a worker's preamble is rejected (#35791) When a structured worker's dispatch preamble is definitively rejected, the dispatch_preamble_undelivered error now appends the provider's failure detail ("Details: ...") unless the rejection sentence already quotes it. The quoted text is bounded and normalized by the same quotedProviderDetail helper the rejection writer uses. Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> --- ...stration-structured-worker-session.test.ts | 41 ++++++++++++++++++- ...orchestration-structured-worker-session.ts | 12 +++++- .../agent-session-failure-quoted-detail.ts | 16 ++++---- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts index b548b68fbc2..4cc5745b2fb 100644 --- a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts @@ -7,6 +7,8 @@ import type { AgentJournalSubmission } from '../../../../shared/agent-session-jo import type { AgentMessageSource } from '../../../../shared/agent-session/agent-session-message-source' import { getAppEnvironment } from '../../../../shared/app-environment' import { DISPATCH_REJECTED_WRITE_FAILED } from '../../../../shared/structured-agent-session-dispatch-rejection' +import { agentSessionFailureFact } from '../../../../shared/agent-session-failure' +import { codexDispatchRejection } from '../../../codex/codex-structured-turn-end-settlement' const hostRef: { current: unknown } = { current: null } const createSpy = vi.fn() @@ -231,7 +233,7 @@ describe('structured worker session', () => { describe('structured worker dispatch preamble', () => { type PreambleHost = Parameters[0]['host'] - type Settled = Pick + type Settled = Pick function submissionOf(settled: Settled): AgentJournalSubmission { return { @@ -377,6 +379,43 @@ describe('structured worker dispatch preamble', () => { expect(isUnknownWorkerStartOutcome(error, 'dispatch_input')).toBe(false) }) + it('names a provider log detail the rejection sentence leaves out', async () => { + const error = await send( + hostWithSubmission({ + dispatchState: 'rejected', + ...codexDispatchRejection( + agentSessionFailureFact('providerRejected', { + detail: { text: 'thread/start: unsupported service tier', audience: 'log' }, + neverReachedAgent: true + }) + ) + }) + ).catch((thrown: unknown) => thrown) + expect(error).toMatchObject({ + code: 'dispatch_preamble_undelivered', + message: + "The dispatch preamble was not delivered (providerRejected): Codex didn't accept this message. Details: thread/start: unsupported service tier." + }) + }) + + it('does not repeat provider words the rejection sentence already quotes', async () => { + const error = await send( + hostWithSubmission({ + dispatchState: 'rejected', + ...codexDispatchRejection( + agentSessionFailureFact('providerRejected', { + detail: { text: 'Unsupported service tier.', audience: 'person' }, + neverReachedAgent: true + }) + ) + }) + ).catch((thrown: unknown) => thrown) + expect(error).toMatchObject({ + message: + "The dispatch preamble was not delivered (providerRejected): Codex didn't accept this message: Unsupported service tier." + }) + }) + it('ends the message with one period whether the reason is a sentence or a marker', async () => { const sentence = await send( hostWithSubmission({ diff --git a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts index abe08f34a5b..86d828e6e18 100644 --- a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts +++ b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts @@ -4,6 +4,11 @@ import { randomUUID } from 'node:crypto' import { isDefinitiveAgentSessionCreateRefusal } from '../../../../shared/agent-session-definitive-refusal' import type { AgentMessageSource } from '../../../../shared/agent-session/agent-session-message-source' +import { + agentSessionFailureDetailText, + readAgentSessionFailureFact +} from '../../../../shared/agent-session-failure' +import { quotedProviderDetail } from '../../../../shared/agent-session-failure-quoted-detail' import type { AgentJournalMessageItem, AgentJournalSubmission @@ -240,12 +245,17 @@ export function preambleDispatchState( return submission.dispatchState } if (submission?.dispatchState === 'rejected') { + // Name the failure detail unless the reason already quotes it. + const detail = agentSessionFailureDetailText(readAgentSessionFailureFact(submission.rejection)) + const quoted = detail ? quotedProviderDetail(detail) : '' + const detailClause = + quoted && !submission.reason?.includes(quoted) ? ` Details: ${quoted}.` : '' // Definitive rejection gets a typed non-delivery code so the coordinator can retry without a duplicate turn. throw new OrchestrationError( 'dispatch_preamble_undelivered', `The dispatch preamble was not delivered${ submission.rejection ? ` (${submission.rejection.kind})` : '' - }: ${reasonClause(submission.reason)}.` + }: ${reasonClause(submission.reason)}.${detailClause}` ) } // Only accepted proves delivery; adapter/transport unknown requires an outcome_unknown receipt that tells the coordinator to inspect. diff --git a/src/shared/agent-session-failure-quoted-detail.ts b/src/shared/agent-session-failure-quoted-detail.ts index 3c37d0358ba..aacc86a4399 100644 --- a/src/shared/agent-session-failure-quoted-detail.ts +++ b/src/shared/agent-session-failure-quoted-detail.ts @@ -8,6 +8,14 @@ import type { /** Person-facing provider text is quoted, but bounded so the sentence stays one. */ const MAX_QUOTED_DETAIL_CHARS = 512 +/** Provider text as a failure sentence quotes it. */ +export function quotedProviderDetail(text: string): string { + return text + .slice(0, MAX_QUOTED_DETAIL_CHARS) + .trim() + .replace(/[.\s]+$/, '') +} + /** The quoted lead when the provider wrote the detail for a person; the plain lead otherwise. */ export function quotingPersonDetail( say: AgentSessionFailureSay, @@ -16,12 +24,6 @@ export function quotingPersonDetail( detail: ProviderDiagnostic | undefined, values: AgentSessionFailureCopyValues = {} ): string { - const quoted = - detail?.audience === 'person' - ? detail.text - .slice(0, MAX_QUOTED_DETAIL_CHARS) - .trim() - .replace(/[.\s]+$/, '') - : '' + const quoted = detail?.audience === 'person' ? quotedProviderDetail(detail.text) : '' return quoted ? say(quotedLead, { ...values, detail: quoted }) : say(lead, values) }