fix(native-chat): restore rewind refusal delivery after merge

This commit is contained in:
Merge Sim
2026-09-09 17:13:21 -07:00
parent 6521a4cc13
commit b9d3fb0fa1
4 changed files with 65 additions and 136 deletions
@@ -86,7 +86,9 @@ vi.mock('./use-structured-agent-session', async () => {
isWorking: false,
isMonitoringBackgroundTasks: mocks.monitoringBackgroundTasks,
supportsBackgroundTaskStop: mocks.supportsBackgroundTaskStop,
supportsBackgroundTaskStopAll: mocks.supportsBackgroundTaskStopAll,
backgroundTasksView: {
supportsBackgroundTaskStopAll: mocks.supportsBackgroundTaskStopAll
},
backgroundTasks: mocks.backgroundTasks,
turnId: null,
cancel: vi.fn(),
@@ -7,7 +7,11 @@
import { useCallback, useRef, useState } from 'react'
import * as conversationCommands from './structured-conversation-command-send'
import type { AgentSessionMutationResult } from '../../../../shared/agent-session-wire'
import type {
AgentSessionMutationResult,
AgentSessionWireRefusal
} from '../../../../shared/agent-session-wire'
import { hasRuntimeRpcErrorCode } from '../../../../shared/runtime-rpc-error-code'
import { agentSessionRefusalOperationState } from '../../../../shared/agent-session-refusal-retry'
import { structuredAgentSessionPayloadFingerprint } from '../../../../shared/structured-agent-session-mutation'
import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
@@ -18,7 +22,8 @@ export type StructuredAgentSessionMutate = <T>(
method: string,
fingerprintMethod: string,
fields: Record<string, unknown>,
operationIdOverride?: string | null
operationIdOverride?: string | null,
onFailure?: (refusal?: AgentSessionWireRefusal) => void
) => Promise<T | null>
export function useStructuredAgentSessionMutate(args: {
@@ -37,7 +42,8 @@ export function useStructuredAgentSessionMutate(args: {
method: string,
fingerprintMethod: string,
fields: Record<string, unknown>,
operationIdOverride?: string | null
operationIdOverride?: string | null,
onFailure?: (refusal?: AgentSessionWireRefusal) => void
): Promise<T | null> => {
if (stateRef.current.fence === null) {
return null
@@ -63,7 +69,17 @@ export function useStructuredAgentSessionMutate(args: {
...fields
})
} catch (error) {
if (stateRef.current.fence === targetFence) {
if (onFailure) {
onFailure(
hasRuntimeRpcErrorCode(error, 'method_not_found')
? {
code: 'structured_agent_session_unsupported',
message: '',
rewindReason: 'unsupported'
}
: undefined
)
} else if (stateRef.current.fence === targetFence) {
setWriteError(error instanceof Error ? error.message : 'Request was not sent')
}
return null
@@ -75,12 +91,14 @@ export function useStructuredAgentSessionMutate(args: {
) {
operationIds.current.delete(key)
}
if (stateRef.current.fence === targetFence) {
if (onFailure) {
onFailure(result.refusal)
} else if (stateRef.current.fence === targetFence) {
setWriteError(result.refusal.message)
}
return null
}
if (stateRef.current.fence !== targetFence) {
if (stateRef.current.fence !== targetFence && fingerprintMethod !== 'agentSession.rewind') {
return null
}
if (!conversationCommands.isUnconfirmedConversationCommand(fingerprintMethod, result.value)) {
@@ -1,104 +0,0 @@
import { translate } from '@/i18n/i18n'
import { useCallback, useEffect, useRef, useState } from 'react'
import type {
AgentSessionMutationResult,
AgentSessionWireRefusal
} from '../../../../shared/agent-session-wire'
import { hasRuntimeRpcErrorCode } from '../../../../shared/runtime-rpc-error-code'
import { agentSessionRefusalOperationState } from '../../../../shared/agent-session-refusal-retry'
import { structuredAgentSessionPayloadFingerprint } from '../../../../shared/structured-agent-session-mutation'
import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client'
import { structuredSessionOperationId } from './use-structured-agent-session-outbox'
import * as conversationCommands from './structured-conversation-command-send'
export function useStructuredAgentSessionMutation(
sessionId: string,
target: RuntimeClientTarget,
fence: number | null
) {
const stateRef = useRef({ fence })
const [writeError, setWriteError] = useState<string | null>(null)
const operationIds = useRef(new Map<string, string>())
useEffect(() => {
stateRef.current = { fence }
}, [fence])
const mutate = useCallback(
async <T>(
method: string,
fingerprintMethod: string,
fields: Record<string, unknown>,
operationIdOverride?: string | null,
onFailure?: (refusal?: AgentSessionWireRefusal) => void
): Promise<T | null> => {
if (stateRef.current.fence === null) {
return null
}
const targetFence = stateRef.current.fence
const key = `${sessionId}:${fingerprintMethod}:${JSON.stringify(fields)}`
const clientOperationId =
operationIdOverride ?? operationIds.current.get(key) ?? structuredSessionOperationId()
operationIds.current.set(key, clientOperationId)
let result: AgentSessionMutationResult<T>
try {
result = await callStructuredAgentSession<AgentSessionMutationResult<T>>(target, method, {
envelope: {
sessionId,
clientOperationId,
expectedRuntimeFence: targetFence,
payloadFingerprint: structuredAgentSessionPayloadFingerprint({
method: fingerprintMethod,
sessionId,
fields
})
},
...fields
})
} catch (error) {
if (onFailure) {
onFailure(
hasRuntimeRpcErrorCode(error, 'method_not_found')
? {
code: 'structured_agent_session_unsupported',
message: '',
rewindReason: 'unsupported'
}
: undefined
)
} else if (stateRef.current.fence === targetFence) {
setWriteError(
error instanceof Error
? error.message
: translate('components.native-chat.requestNotSent', 'Request was not sent')
)
}
return null
}
if (!result.ok) {
if (
agentSessionRefusalOperationState(fingerprintMethod, result.refusal.code) ===
'settled-rejected'
) {
operationIds.current.delete(key)
}
if (onFailure) {
onFailure(result.refusal)
} else if (stateRef.current.fence === targetFence) {
setWriteError(result.refusal.message)
}
return null
}
if (stateRef.current.fence !== targetFence && fingerprintMethod !== 'agentSession.rewind') {
return null
}
if (!conversationCommands.isUnconfirmedConversationCommand(fingerprintMethod, result.value)) {
operationIds.current.delete(key)
}
setWriteError(null)
return result.value
},
[sessionId, target]
)
return { mutate, writeError }
}
@@ -191,31 +191,44 @@ export function useStructuredAgentSession(args: {
)
const prompts = pendingStructuredSessionPrompts(state.items)
const rewind = useNativeChatRewind({
sessionId,
hostBlockedReason: summary?.rewindBlockedReason,
state,
support:
conversationSupport?.sessionId === sessionId && conversationSupport.fence === state.fence
? conversationSupport.rewind
: undefined,
supportResolved:
conversationSupport?.sessionId === sessionId && conversationSupport.fence === state.fence,
blocked: Boolean(
turnId ||
prompts.length ||
isMonitoringBackgroundTasks ||
outboxController.outbox.length ||
commandPending.current
),
send: (fields) =>
mutate<AgentSessionRewindResult>(
'agentSession.rewind',
'agentSession.rewind',
fields,
undefined
)
})
const rewindSupportResolved =
conversationSupport?.sessionId === sessionId && conversationSupport.fence === state.fence
const rewindSupport = rewindSupportResolved ? conversationSupport.rewind : undefined
const rewindBlocked = Boolean(
turnId ||
prompts.length ||
isMonitoringBackgroundTasks ||
outboxController.outbox.length ||
commandPending.current
)
const rewindInput = useMemo<Parameters<typeof useNativeChatRewind>[0]>(
() => ({
sessionId,
hostBlockedReason: summary?.rewindBlockedReason,
state,
support: rewindSupport,
supportResolved: rewindSupportResolved,
blocked: rewindBlocked,
send: (fields, onFailure) =>
mutate<AgentSessionRewindResult>(
'agentSession.rewind',
'agentSession.rewind',
fields,
undefined,
onFailure
)
}),
[
sessionId,
summary?.rewindBlockedReason,
state,
rewindSupport,
rewindSupportResolved,
rewindBlocked,
mutate
]
)
const rewind = useNativeChatRewind(rewindInput)
const { outbox } = outboxController
const messages = useStructuredAgentSessionMessages(state.items, outbox, state.submissions)
return {