mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(native-chat): separate bounded journal key forms
This commit is contained in:
@@ -147,6 +147,34 @@ describe('key encoding', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('bounded component domain separation', () => {
|
||||
const oversizedTurnId = 'a'.repeat(MAX_JOURNAL_KEY_COMPONENT_CHARS + 1)
|
||||
const digestFormMimic = boundJournalKeyComponent(oversizedTurnId)
|
||||
const keyFor = (turnId: string) =>
|
||||
agentJournalItemKey({ provider: 'codex', threadId: THREAD, turnId, ordinal: 0 })
|
||||
|
||||
it('separates an oversized component from the raw string matching its digest form', () => {
|
||||
const oversizedKey = keyFor(oversizedTurnId)
|
||||
expect(oversizedKey).toBe(`codex:${THREAD}:${digestFormMimic}:0`)
|
||||
expect(oversizedKey).not.toBe(keyFor(digestFormMimic))
|
||||
})
|
||||
|
||||
it('keeps both persisted key spellings stable through parse and re-key', () => {
|
||||
for (const turnId of [oversizedTurnId, digestFormMimic]) {
|
||||
const key = keyFor(turnId)
|
||||
const parsed = parseAgentJournalItemKey(key)
|
||||
expect(parsed).not.toBeNull()
|
||||
expect(agentJournalItemKey(parsed as AgentJournalItemIdentity)).toBe(key)
|
||||
}
|
||||
expect(parseAgentJournalItemKey(keyFor(digestFormMimic))).toEqual({
|
||||
provider: 'codex',
|
||||
threadId: THREAD,
|
||||
turnId: digestFormMimic,
|
||||
ordinal: 0
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('oversized identity bounding on Unicode boundaries', () => {
|
||||
// 39 UTF-16 units of ASCII put the astral character's surrogate pair across
|
||||
// the 40-unit diagnostic-head cut. Pre-fix the head ended in a lone high
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { agentJournalSubmissionKey } from '../../../shared/agent-session-journal-item-key'
|
||||
import {
|
||||
agentJournalItemKey,
|
||||
agentJournalSubmissionKey,
|
||||
boundJournalKeyComponent,
|
||||
MAX_JOURNAL_KEY_COMPONENT_CHARS
|
||||
} from '../../../shared/agent-session-journal-item-key'
|
||||
import type { AgentJournalMessageItem } from '../../../shared/agent-session-journal-types'
|
||||
import { structuredAgentSessionPayloadFingerprint } from '../../../shared/structured-agent-session-mutation'
|
||||
import {
|
||||
@@ -418,3 +423,24 @@ describe('malformed persisted item keys', () => {
|
||||
expect(renderJournalState(state).items[0]?.itemId).toBe('%')
|
||||
})
|
||||
})
|
||||
|
||||
describe('bounded item-key collisions', () => {
|
||||
it('keeps an oversized turn and its raw digest-form mimic as separate items', () => {
|
||||
const oversizedTurnId = 'a'.repeat(MAX_JOURNAL_KEY_COMPONENT_CHARS + 1)
|
||||
const digestFormMimic = boundJournalKeyComponent(oversizedTurnId)
|
||||
const keyFor = (turnId: string) =>
|
||||
agentJournalItemKey({ provider: 'codex', threadId: 'thread-1', turnId, ordinal: 0 })
|
||||
const oversizedKey = keyFor(oversizedTurnId)
|
||||
const mimicKey = keyFor(digestFormMimic)
|
||||
|
||||
const state = fold([
|
||||
{ kind: 'item', itemId: oversizedKey, revision: 1, body: text('oversized'), ...base(1) },
|
||||
{ kind: 'item', itemId: mimicKey, revision: 1, body: text('mimic'), ...base(2) }
|
||||
])
|
||||
|
||||
expect(renderJournalState(state).items.map((item) => item.itemId)).toEqual([
|
||||
oversizedKey,
|
||||
mimicKey
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,10 @@ import type {
|
||||
AgentJournalItemIdentity,
|
||||
AgentSessionJournalIdentity
|
||||
} from '../../../shared/agent-session-journal-types'
|
||||
import {
|
||||
boundJournalKeyComponent,
|
||||
MAX_JOURNAL_KEY_COMPONENT_CHARS
|
||||
} from '../../../shared/agent-session-journal-item-key'
|
||||
import { readJournalBlob } from './journal-blob-store'
|
||||
import { JOURNAL_LOG_FILE, JOURNAL_SNAPSHOT_FILE } from './journal-log-file'
|
||||
import { loadJournal } from './journal-open'
|
||||
@@ -91,6 +95,37 @@ describe('sequences', () => {
|
||||
expect(journal.snapshot().items).toHaveLength(1)
|
||||
expect(journal.snapshot().items[0]?.revision).toBe(3)
|
||||
})
|
||||
|
||||
it('preserves an oversized identity and its raw digest-form mimic across reopen', async () => {
|
||||
const oversizedTurnId = 'a'.repeat(MAX_JOURNAL_KEY_COMPONENT_CHARS + 1)
|
||||
const digestFormMimic = boundJournalKeyComponent(oversizedTurnId)
|
||||
const identityFor = (turnId: string): AgentJournalItemIdentity => ({
|
||||
provider: 'codex',
|
||||
threadId: 'thread-1',
|
||||
turnId,
|
||||
ordinal: 0
|
||||
})
|
||||
const oversizedIdentity = identityFor(oversizedTurnId)
|
||||
const mimicIdentity = identityFor(digestFormMimic)
|
||||
const journal = await open()
|
||||
|
||||
const oversized = await journal.appendItem(oversizedIdentity, body('oversized'), { fence: 1 })
|
||||
const mimic = await journal.appendItem(mimicIdentity, body('mimic'), { fence: 1 })
|
||||
expect(oversized.itemId).not.toBe(mimic.itemId)
|
||||
expect([oversized.revision, mimic.revision]).toEqual([1, 1])
|
||||
|
||||
const reopened = await open()
|
||||
expect(reopened.snapshot().items.map((entry) => entry.body)).toEqual([
|
||||
body('oversized'),
|
||||
body('mimic')
|
||||
])
|
||||
|
||||
await reopened.appendTombstone(oversizedIdentity, { fence: 1 })
|
||||
const afterTombstoneReopen = await open()
|
||||
expect(afterTombstoneReopen.snapshot().items.map((entry) => entry.body)).toEqual([
|
||||
body('mimic')
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('fences', () => {
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
import type { AgentJournalItemIdentity } from './agent-session-journal-types'
|
||||
|
||||
const KEY_DELIMITER = ':'
|
||||
const VERBATIM_BOUNDED_COMPONENT_TAG = '%FF'
|
||||
const BOUNDED_COMPONENT_PATTERN = /^[\s\S]{0,40}~orca-oversized~(?:[1-9]\d*)~[0-9a-f]{16}$/
|
||||
const PARSED_JOURNAL_ITEM_KEY = Symbol('parsedJournalItemKey')
|
||||
|
||||
type ParsedJournalItemIdentity = AgentJournalItemIdentity & {
|
||||
readonly [PARSED_JOURNAL_ITEM_KEY]?: string
|
||||
}
|
||||
|
||||
/** Longest raw component a key may embed. Real provider ids are tens of bytes;
|
||||
* anything larger would push the composed key past wire page budgets, so it
|
||||
@@ -92,7 +99,17 @@ function fnv1a32(value: string, seed: number): number {
|
||||
}
|
||||
|
||||
function encodePart(value: string | number): string {
|
||||
return encodeURIComponent(boundJournalKeyComponent(String(value)))
|
||||
const raw = String(value)
|
||||
const bounded = boundJournalKeyComponent(raw)
|
||||
const encoded = encodeURIComponent(bounded)
|
||||
// `%FF` is not valid UTF-8 and cannot be emitted by encodeURIComponent.
|
||||
return raw === bounded && isBoundedComponentRepresentation(raw)
|
||||
? `${VERBATIM_BOUNDED_COMPONENT_TAG}${encoded}`
|
||||
: encoded
|
||||
}
|
||||
|
||||
function isBoundedComponentRepresentation(value: string): boolean {
|
||||
return BOUNDED_COMPONENT_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,6 +123,11 @@ function encodePart(value: string | number): string {
|
||||
* plus the uuid. Text never participates.
|
||||
*/
|
||||
export function agentJournalItemKey(identity: AgentJournalItemIdentity): string {
|
||||
// Parsed pre-tag digest keys must keep addressing their persisted revision chain.
|
||||
const parsedKey = (identity as ParsedJournalItemIdentity)[PARSED_JOURNAL_ITEM_KEY]
|
||||
if (parsedKey !== undefined) {
|
||||
return parsedKey
|
||||
}
|
||||
if (identity.provider === 'codex') {
|
||||
return [
|
||||
'codex',
|
||||
@@ -136,40 +158,77 @@ export function agentJournalSubmissionKey(clientMessageId: string): string {
|
||||
/**
|
||||
* Inverse of {@link agentJournalItemKey}. Clients hold item KEYS, but an upsert
|
||||
* needs the identity behind one — answering an approval re-appends the same
|
||||
* item at the next revision. Every component is percent-encoded, and
|
||||
* `encodeURIComponent` escapes the delimiter, so the split is unambiguous.
|
||||
* item at the next revision. Components are percent-encoded; raw strings that
|
||||
* imitate a bounded component carry a reserved encoded-domain tag.
|
||||
*/
|
||||
export function parseAgentJournalItemKey(key: string): AgentJournalItemIdentity | null {
|
||||
// Persisted keys can be corrupted: a malformed percent sequence must fail
|
||||
// the parse, never throw through journal replay or open.
|
||||
const parts: string[] = []
|
||||
let preserveExactKey = false
|
||||
for (const part of key.split(KEY_DELIMITER)) {
|
||||
try {
|
||||
parts.push(decodeURIComponent(part))
|
||||
} catch {
|
||||
const decoded = decodePart(part)
|
||||
if (!decoded) {
|
||||
return null
|
||||
}
|
||||
parts.push(decoded.value)
|
||||
preserveExactKey ||= decoded.tagged || isBoundedComponentRepresentation(decoded.value)
|
||||
}
|
||||
const [provider, ...rest] = parts
|
||||
if (provider === 'codex' && rest.length === 3) {
|
||||
const ordinal = Number(rest[2])
|
||||
return Number.isSafeInteger(ordinal) && ordinal >= 0
|
||||
? { provider, threadId: rest[0] as string, turnId: rest[1] as string, ordinal }
|
||||
? parsedIdentity(
|
||||
{ provider, threadId: rest[0] as string, turnId: rest[1] as string, ordinal },
|
||||
key,
|
||||
preserveExactKey
|
||||
)
|
||||
: null
|
||||
}
|
||||
if (provider === 'claude' && rest.length === 2) {
|
||||
return { provider, sessionId: rest[0] as string, uuid: rest[1] as string }
|
||||
return parsedIdentity(
|
||||
{ provider, sessionId: rest[0] as string, uuid: rest[1] as string },
|
||||
key,
|
||||
preserveExactKey
|
||||
)
|
||||
}
|
||||
if (provider === 'orca' && rest.length === 1) {
|
||||
return { provider, clientMessageId: rest[0] as string }
|
||||
return parsedIdentity({ provider, clientMessageId: rest[0] as string }, key, preserveExactKey)
|
||||
}
|
||||
if (provider === 'legacy' && rest.length === 3) {
|
||||
return {
|
||||
provider,
|
||||
agent: rest[0] as string,
|
||||
sessionId: rest[1] as string,
|
||||
recordId: rest[2] as string
|
||||
}
|
||||
return parsedIdentity(
|
||||
{
|
||||
provider,
|
||||
agent: rest[0] as string,
|
||||
sessionId: rest[1] as string,
|
||||
recordId: rest[2] as string
|
||||
},
|
||||
key,
|
||||
preserveExactKey
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function decodePart(part: string): { value: string; tagged: boolean } | null {
|
||||
const tagged = part.startsWith(VERBATIM_BOUNDED_COMPONENT_TAG)
|
||||
try {
|
||||
const value = decodeURIComponent(
|
||||
tagged ? part.slice(VERBATIM_BOUNDED_COMPONENT_TAG.length) : part
|
||||
)
|
||||
return !tagged || isBoundedComponentRepresentation(value) ? { value, tagged } : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function parsedIdentity<T extends AgentJournalItemIdentity>(
|
||||
identity: T,
|
||||
key: string,
|
||||
preserveExactKey: boolean
|
||||
): T {
|
||||
if (preserveExactKey) {
|
||||
Object.defineProperty(identity, PARSED_JOURNAL_ITEM_KEY, { value: key })
|
||||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user