mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 00:02:35 +00:00
fix(codex): retain exact cancellable prompt turn ids
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
boundPayload,
|
||||
digestPayload
|
||||
} from '../native-chat/agent-session-journal/journal-payload-bounds'
|
||||
import { AGENT_SESSION_ID_MAX_LENGTH } from '../../shared/agent-session-wire'
|
||||
|
||||
export const CODEX_JOURNAL_PROMPT_ID_COMPONENT_MAX_BYTES = 256
|
||||
export const CODEX_JOURNAL_PROMPT_OPTION_ID_MAX_BYTES = 1024
|
||||
@@ -13,7 +14,7 @@ export const CODEX_PROMPT_MAX_ANSWER_BYTES = 64 * 1024
|
||||
export const MAX_CODEX_PROMPT_REGISTRY_ENTRIES = 128
|
||||
export const MAX_CODEX_PROMPT_JOURNAL_BINDINGS = 256
|
||||
export const MAX_CODEX_PROMPT_REGISTRY_BYTES = 4 * 1024 * 1024
|
||||
const CODEX_PROMPT_TURN_ID_RESERVED_BYTES = 512
|
||||
const CODEX_PROMPT_TURN_ID_RESERVED_BYTES = AGENT_SESSION_ID_MAX_LENGTH * 3
|
||||
|
||||
type CodexPromptRegistryEntryBounds = {
|
||||
threadId: string
|
||||
@@ -49,7 +50,7 @@ export function codexPromptTurnIdentity(turnId: string): {
|
||||
turnId: string | null
|
||||
turnIdDigest?: string
|
||||
} {
|
||||
return Buffer.byteLength(turnId, 'utf8') <= CODEX_PROMPT_TURN_ID_RESERVED_BYTES
|
||||
return turnId.length <= AGENT_SESSION_ID_MAX_LENGTH
|
||||
? { turnId }
|
||||
: { turnId: null, turnIdDigest: digestPayload(turnId) }
|
||||
}
|
||||
|
||||
@@ -89,11 +89,15 @@ export class CodexPromptRegistry {
|
||||
return null
|
||||
}
|
||||
const turnId = readString(request.params, 'turnId')
|
||||
const turnIdentity = turnId ? codexPromptTurnIdentity(turnId) : { turnId: null }
|
||||
if (turnId && turnIdentity.turnId === null) {
|
||||
return null
|
||||
}
|
||||
const prompt: CodexPendingPrompt = {
|
||||
requestId: request.id,
|
||||
method: request.method,
|
||||
threadId,
|
||||
...(turnId ? codexPromptTurnIdentity(turnId) : { turnId: null }),
|
||||
...turnIdentity,
|
||||
codexItemId,
|
||||
promptKey: readString(request.params, 'approvalId') ?? codexItemId,
|
||||
questionIds,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key'
|
||||
import { AGENT_SESSION_ID_MAX_LENGTH } from '../../shared/agent-session-wire'
|
||||
import type {
|
||||
AgentJournalItemBody,
|
||||
AgentJournalItemIdentity
|
||||
@@ -301,6 +302,30 @@ describe('Codex live prompt ownership', () => {
|
||||
expect(codex.connections[0]?.closed).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps a wire-valid multibyte prompt turn id as the exact interrupt target', async () => {
|
||||
const promptTurnId = '界'.repeat(171)
|
||||
expect(promptTurnId.length).toBeLessThanOrEqual(AGENT_SESSION_ID_MAX_LENGTH)
|
||||
expect(Buffer.byteLength(promptTurnId, 'utf8')).toBeGreaterThan(AGENT_SESSION_ID_MAX_LENGTH)
|
||||
const codex = fakeCodex({
|
||||
'turn/interrupt': () => completeTurn(codex, 'thread-child', promptTurnId)
|
||||
})
|
||||
const adapter = await acquired(codex)
|
||||
registerPrompt(adapter, codex, 'child-prompt', 'thread-child', promptTurnId)
|
||||
|
||||
await expect(
|
||||
adapter.cancelTurn({
|
||||
sessionId: 'session-1',
|
||||
turnId: 'root-turn',
|
||||
fence: 7,
|
||||
prompt: { itemId: 'child-prompt' }
|
||||
})
|
||||
).resolves.toEqual({ cancelled: true })
|
||||
expect(codex.connections[0]?.calls.at(-1)).toEqual({
|
||||
method: 'turn/interrupt',
|
||||
params: { threadId: 'thread-child', turnId: promptTurnId }
|
||||
})
|
||||
})
|
||||
|
||||
it('settles a grouped prompt and its running turn before reporting cancellation', async () => {
|
||||
const codex = fakeCodex({
|
||||
'turn/interrupt': () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AGENT_SESSION_ID_MAX_LENGTH } from '../../shared/agent-session-wire'
|
||||
import {
|
||||
applyCodexPromptAnswer,
|
||||
CodexPromptRegistry,
|
||||
@@ -122,7 +123,7 @@ describe('CodexPromptRegistry', () => {
|
||||
expect(registry.find('other-thread-item')?.requestId).toBe(3)
|
||||
})
|
||||
|
||||
it('bounds an oversized backfilled turn id and still clears its prompt', () => {
|
||||
it('retains a bounded cleanup identity for an unaddressable backfilled turn id', () => {
|
||||
const registry = new CodexPromptRegistry()
|
||||
const turnId = 'turn-'.padEnd(MAX_CODEX_PROMPT_REGISTRY_BYTES + 1, 'x')
|
||||
registry.register({
|
||||
@@ -138,20 +139,34 @@ describe('CodexPromptRegistry', () => {
|
||||
expect(registry.find('journal-root')).toBeNull()
|
||||
})
|
||||
|
||||
it('bounds an oversized request turn id and still clears its prompt', () => {
|
||||
it('reserves enough bytes for a wire-valid multibyte backfilled turn id', () => {
|
||||
const registry = new CodexPromptRegistry()
|
||||
const turnId = 'turn-'.padEnd(MAX_CODEX_PROMPT_REGISTRY_BYTES + 1, 'x')
|
||||
registry.register({
|
||||
id: 1,
|
||||
method: 'item/commandExecution/requestApproval',
|
||||
params: { itemId: 'root-item', threadId: 'thread-1' }
|
||||
})
|
||||
const reservedBytes = registry.bytes
|
||||
const turnId = '界'.repeat(AGENT_SESSION_ID_MAX_LENGTH)
|
||||
|
||||
registry.bindJournalItemId('journal-root', 'thread-1', 'root-item', turnId)
|
||||
|
||||
expect(registry.find('journal-root')?.turnId).toBe(turnId)
|
||||
expect(registry.bytes).toBe(reservedBytes)
|
||||
expect(registry.bytes).toBeLessThanOrEqual(MAX_CODEX_PROMPT_REGISTRY_BYTES)
|
||||
})
|
||||
|
||||
it('rejects a request turn id beyond the wire identity bound', () => {
|
||||
const registry = new CodexPromptRegistry()
|
||||
const turnId = 'x'.repeat(AGENT_SESSION_ID_MAX_LENGTH + 1)
|
||||
const prompt = registry.register({
|
||||
id: 1,
|
||||
method: 'item/commandExecution/requestApproval',
|
||||
params: { itemId: 'root-item', threadId: 'thread-1', turnId }
|
||||
})
|
||||
|
||||
expect(prompt).not.toBeNull()
|
||||
expect(registry.bytes).toBeLessThanOrEqual(MAX_CODEX_PROMPT_REGISTRY_BYTES)
|
||||
registry.bindJournalItemId('journal-root', 'thread-1', 'root-item')
|
||||
registry.clearTurn('thread-1', turnId)
|
||||
expect(registry.find('journal-root')).toBeNull()
|
||||
expect(prompt).toBeNull()
|
||||
expect(registry.bytes).toBe(0)
|
||||
})
|
||||
|
||||
it('addresses a prompt by its journal item id once bound, and forgets both', () => {
|
||||
|
||||
@@ -75,6 +75,8 @@ export type AgentSessionTurnActivity = {
|
||||
text: string
|
||||
}
|
||||
|
||||
export const AGENT_SESSION_ID_MAX_LENGTH = 512
|
||||
|
||||
/** Backward paging is the client's normal read; 40 matches the page size the
|
||||
* mobile list renders without a visible fill-in. */
|
||||
export const AGENT_SESSION_HISTORY_DEFAULT_LIMIT = 40
|
||||
|
||||
@@ -2,11 +2,12 @@ import { z } from 'zod'
|
||||
import { isAgentSessionId } from '../agent-session-record'
|
||||
import { normalizeExecutionHostId } from '../execution-host'
|
||||
import {
|
||||
AGENT_SESSION_ID_MAX_LENGTH,
|
||||
AGENT_SESSION_HISTORY_DIRECTIONS,
|
||||
AGENT_SESSION_HISTORY_MAX_LIMIT
|
||||
} from '../agent-session-wire'
|
||||
|
||||
export const MAX_ID_LENGTH = 512
|
||||
export const MAX_ID_LENGTH = AGENT_SESSION_ID_MAX_LENGTH
|
||||
|
||||
// Four Claude questions with all four generated choices occupy 610 chars when fully percent-encoded.
|
||||
export const MAX_RESPONSE_OPTION_ID_LENGTH = 1024
|
||||
|
||||
Reference in New Issue
Block a user