Files
orca/src/shared/agent-session-provider-handle.test.ts
T
Brennan Benson eaaae0196f feat(native-chat): record fresh sessions after failed restoration (#25747)
* feat(native-chat): record a fresh provider conversation that replaced one the agent could not restore

A chat whose saved conversation the agent cannot reopen can now continue in a
fresh one: the handle chain records the new conversation as a creation that
replaces the lost one (which, why, and when), keeping every earlier link.

Rows keep a shape older builds read: the stored chain starts at the latest
replacement and carries the earlier links inside it.

* refactor(native-chat): store a replaced conversation flat; refuse it where older builds read the row

Older builds only read Claude and Codex records, so the nested stored form
protected rows no replacement can reach while adding a cap mismatch after a
downgrade. Store the chain as held, refuse a replacement in a Claude or Codex
chain until one has a stored shape older builds read, and refuse a supersession
key on a replacement that names no creation in the chain.

* test(native-chat): prove replacement rows survive downgrade and re-upgrade
2026-10-05 23:22:24 -07:00

697 lines
25 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
agentSessionProviderHandleChainHead,
agentSessionProviderHandleKey,
agentSessionProviderHandleRoot,
agentSessionProviderHandlesEqual,
appendAgentSessionProviderHandleLink,
findAgentSessionProviderHandleLink,
isAgentSessionProviderHandle,
isAgentSessionHandleProvider,
isAgentSessionProviderHandleChain,
MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS,
type AgentSessionProviderHandle,
type AgentSessionProviderHandleLink
} from './agent-session-provider-handle'
import { claudeProviderHandle, codexProviderHandle } from './agent-session-provider-handle-encoding'
const CLAUDE: AgentSessionProviderHandle = claudeProviderHandle('sess-1', 'leaf-1')
function link(overrides: Partial<AgentSessionProviderHandleLink> = {}) {
return {
linkId: 'link-1',
handle: CLAUDE,
origin: 'created',
mintedAtFence: 1,
observedAt: 1_000,
...overrides
} as AgentSessionProviderHandleLink
}
describe('handle identity', () => {
it('rejects unknown persisted provider names instead of defaulting to Codex', () => {
expect(isAgentSessionHandleProvider('codex')).toBe(true)
expect(isAgentSessionHandleProvider('claude')).toBe(true)
expect(isAgentSessionHandleProvider('gemini')).toBe(false)
expect(isAgentSessionHandleProvider(undefined)).toBe(false)
})
it('keys a Claude handle by session id AND leaf, so two branches are two handles', () => {
// Concurrent resumes branch one transcript silently; the session id alone cannot name a writer.
const branchA = agentSessionProviderHandleKey(CLAUDE)
const branchB = agentSessionProviderHandleKey(claudeProviderHandle('sess-1', 'leaf-2'))
expect(branchA).not.toEqual(branchB)
expect(agentSessionProviderHandleRoot(CLAUDE)).toEqual(
agentSessionProviderHandleRoot(claudeProviderHandle('sess-1', 'leaf-2'))
)
})
it('keys a Codex handle by thread id alone', () => {
const codex: AgentSessionProviderHandle = codexProviderHandle('thread-1')
expect(agentSessionProviderHandleKey(codex)).toBe('codex:"thread-1"')
expect(agentSessionProviderHandleRoot(codex)).toBe('codex:"thread-1"')
expect(agentSessionProviderHandlesEqual(codex, codexProviderHandle('thread-2'))).toBe(false)
})
it('distinguishes a null leaf from an empty-string leaf and rejects malformed handles', () => {
expect(isAgentSessionProviderHandle(claudeProviderHandle('sess-1', null))).toBe(true)
expect(isAgentSessionProviderHandle({ ...CLAUDE, resumeCursor: '' })).toBe(false)
expect(isAgentSessionProviderHandle({ ...CLAUDE, nativeId: '' })).toBe(false)
expect(isAgentSessionProviderHandle({ ...CLAUDE, nativeId: ' sess-1 ' })).toBe(false)
// A stored typed shape is not an in-memory handle: it must be decoded first.
expect(
isAgentSessionProviderHandle({ provider: 'claude', sessionId: 'x', leafUuid: null })
).toBe(false)
expect(isAgentSessionProviderHandle({ provider: 'gemini', sessionId: 'x' })).toBe(false)
})
it('uses collision-free keys when Claude ids contain delimiters', () => {
const left = claudeProviderHandle('a#b', 'c')
const right = claudeProviderHandle('a', 'b#c')
expect(agentSessionProviderHandleKey(left)).not.toBe(agentSessionProviderHandleKey(right))
expect(agentSessionProviderHandlesEqual(left, right)).toBe(false)
})
})
describe('chain append', () => {
it('starts only from a created or adopted link', () => {
expect(appendAgentSessionProviderHandleLink([], link())).toEqual([link()])
expect(appendAgentSessionProviderHandleLink([], link({ origin: 'adopted' }))).toHaveLength(1)
expect(() => appendAgentSessionProviderHandleLink([], link({ origin: 'resumed' }))).toThrow(
'agent_session_provider_handle_invalid'
)
})
it('refuses to record a fork as a resume', () => {
// --fork-session keeps the original item ids; calling it a resume would claim continuity the
// provider never gave.
const chain = [link()]
expect(() =>
appendAgentSessionProviderHandleLink(
chain,
link({
linkId: 'link-2',
origin: 'resumed',
handle: claudeProviderHandle('sess-2', 'leaf-9'),
mintedAtFence: 2
})
)
).toThrow('agent_session_provider_handle_forked')
})
it('records a fork only with a new root and the seed it came from', () => {
const chain = [link()]
const forked = link({
linkId: 'link-2',
origin: 'forked',
handle: claudeProviderHandle('sess-2', 'leaf-9'),
mintedAtFence: 2,
forkedFromKey: agentSessionProviderHandleKey(CLAUDE)
})
expect(appendAgentSessionProviderHandleLink(chain, forked)).toHaveLength(2)
expect(() =>
appendAgentSessionProviderHandleLink(chain, { ...forked, forkedFromKey: 'claude:other' })
).toThrow('agent_session_provider_handle_invalid')
expect(() =>
appendAgentSessionProviderHandleLink(chain, {
...forked,
handle: CLAUDE,
forkedFromKey: agentSessionProviderHandleKey(CLAUDE)
})
).toThrow('agent_session_provider_handle_invalid')
})
it('rejects a link minted under an older fence', () => {
const chain = [link({ mintedAtFence: 5 })]
expect(() =>
appendAgentSessionProviderHandleLink(
chain,
link({
linkId: 'link-2',
origin: 'resumed',
handle: claudeProviderHandle('sess-1', 'leaf-2'),
mintedAtFence: 4
})
)
).toThrow('agent_session_provider_handle_stale_fence')
})
it('rejects a provider change mid-chain', () => {
expect(() =>
appendAgentSessionProviderHandleLink(
[link()],
link({
linkId: 'link-2',
origin: 'resumed',
handle: codexProviderHandle('thread-1'),
mintedAtFence: 2
})
)
).toThrow('agent_session_provider_handle_provider_mismatch')
})
it('treats re-proving the same handle at the same fence as a retry, not a new link', () => {
const chain = [link({ mintedAtFence: 3 })]
const retried = appendAgentSessionProviderHandleLink(
chain,
link({ linkId: 'link-2', origin: 'resumed', mintedAtFence: 3 })
)
expect(retried).toHaveLength(1)
expect(retried[0]?.linkId).toBe('link-1')
// A later fence on the same handle is a genuine re-acquisition and does append.
expect(
appendAgentSessionProviderHandleLink(
chain,
link({ linkId: 'link-2', origin: 'resumed', mintedAtFence: 4 })
)
).toHaveLength(2)
})
it('rejects reuse of a stable link id for a different proof', () => {
expect(() =>
appendAgentSessionProviderHandleLink(
[link()],
link({
origin: 'resumed',
handle: claudeProviderHandle('sess-1', 'leaf-2'),
mintedAtFence: 2
})
)
).toThrow('agent_session_provider_handle_invalid')
})
it('refuses to grow past the cap rather than dropping fork provenance', () => {
const chain: AgentSessionProviderHandleLink[] = [link()]
for (let index = 1; index < MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS; index += 1) {
chain.push(
link({
linkId: `link-${index + 1}`,
origin: 'resumed',
handle: claudeProviderHandle('sess-1', `leaf-${index + 1}`),
mintedAtFence: index + 1
})
)
}
expect(chain).toHaveLength(MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS)
expect(() =>
appendAgentSessionProviderHandleLink(
chain,
link({
linkId: 'link-overflow',
origin: 'resumed',
handle: claudeProviderHandle('sess-1', 'leaf-overflow'),
mintedAtFence: 999
})
)
).toThrow('agent_session_provider_handle_chain_overflow')
expect(isAgentSessionProviderHandleChain(chain)).toBe(true)
expect(agentSessionProviderHandleChainHead(chain)?.linkId).toBe(
`link-${MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS}`
)
})
it('never mutates the chain it was given', () => {
const chain = [link()]
appendAgentSessionProviderHandleLink(
chain,
link({
linkId: 'link-2',
origin: 'resumed',
handle: claudeProviderHandle('sess-1', 'leaf-2'),
mintedAtFence: 2
})
)
expect(chain).toHaveLength(1)
})
})
describe('chain lookup and validation', () => {
it('finds a link by id and reports the head', () => {
const chain = appendAgentSessionProviderHandleLink(
[link()],
link({
linkId: 'link-2',
origin: 'resumed',
handle: claudeProviderHandle('sess-1', 'leaf-2'),
mintedAtFence: 2
})
)
expect(findAgentSessionProviderHandleLink(chain, 'link-1')?.origin).toBe('created')
expect(findAgentSessionProviderHandleLink(chain, 'missing')).toBeNull()
expect(agentSessionProviderHandleChainHead(chain)?.linkId).toBe('link-2')
expect(agentSessionProviderHandleChainHead([])).toBeNull()
})
it('rejects a persisted chain that is over the cap or holds a malformed link', () => {
expect(isAgentSessionProviderHandleChain([{ ...link(), mintedAtFence: -1 }])).toBe(false)
expect(isAgentSessionProviderHandleChain([{ ...link(), linkId: 'not a link id!' }])).toBe(false)
expect(isAgentSessionProviderHandleChain([{ ...link(), forkedFromKey: 'claude:seed' }])).toBe(
false
)
expect(
isAgentSessionProviderHandleChain(
Array.from({ length: MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS + 1 }, (_value, index) =>
link({ linkId: `link-${index}` })
)
)
).toBe(false)
})
it('rejects persisted chains that bypass append invariants', () => {
expect(
isAgentSessionProviderHandleChain([
link(),
link({
linkId: 'link-2',
origin: 'created',
mintedAtFence: 2
})
])
).toBe(false)
expect(
isAgentSessionProviderHandleChain([
link(),
link({
origin: 'resumed',
handle: claudeProviderHandle('sess-1', 'leaf-2'),
mintedAtFence: 2
})
])
).toBe(false)
expect(
isAgentSessionProviderHandleChain([
link(),
link({
linkId: 'link-2',
origin: 'resumed',
handle: claudeProviderHandle('sess-2', 'leaf-2'),
mintedAtFence: 2
})
])
).toBe(false)
})
})
describe('adopted chain heads', () => {
// What a resume-from-history builds: the create seeds an `adopted` head, then the provider's own
// proof lands on top of it.
const adopted = (overrides: Partial<AgentSessionProviderHandleLink> = {}) =>
link({
linkId: 'claude-1-sess-1-empty',
origin: 'adopted',
handle: claudeProviderHandle('sess-1', null),
...overrides
})
it('appends a Claude resume that lands on the adopted root with a leaf', () => {
// The adopted head names no leaf; the provider answers with one. Same root, so it is a resume.
const resumed = link({
linkId: 'claude-1-sess-1-leaf-1',
origin: 'resumed',
handle: CLAUDE,
mintedAtFence: 1
})
const chain = appendAgentSessionProviderHandleLink([adopted()], resumed)
expect(chain.map((entry) => entry.origin)).toEqual(['adopted', 'resumed'])
expect(agentSessionProviderHandleChainHead(chain)).toBe(resumed)
})
it('elides a Claude re-proof of the identical adopted handle at the same fence', () => {
const chain = [adopted()]
const elided = appendAgentSessionProviderHandleLink(
chain,
link({
linkId: 'claude-1-sess-1-empty-retry',
origin: 'resumed',
handle: claudeProviderHandle('sess-1', null),
mintedAtFence: 1
})
)
expect(elided).toEqual(chain)
})
it('appends a Codex resume only once the fence has moved', () => {
const codexAdopted = link({
linkId: 'codex-1-thread-1',
origin: 'adopted',
handle: codexProviderHandle('thread-1')
})
const reproved = link({
linkId: 'codex-1-thread-1-retry',
origin: 'resumed',
handle: codexProviderHandle('thread-1'),
mintedAtFence: 1
})
// Codex's thread id is the whole key, so a same-fence re-proof can only ever be a retry.
expect(appendAgentSessionProviderHandleLink([codexAdopted], reproved)).toEqual([codexAdopted])
expect(
appendAgentSessionProviderHandleLink([codexAdopted], { ...reproved, mintedAtFence: 2 })
).toHaveLength(2)
})
it('refuses a second origin link on top of an adopted head', () => {
// Nothing re-origins a chain: a create landing here would erase where the conversation came from.
for (const origin of ['created', 'adopted'] as const) {
expect(() =>
appendAgentSessionProviderHandleLink(
[adopted()],
link({ linkId: 'claude-2-sess-1-leaf-1', origin, handle: CLAUDE, mintedAtFence: 2 })
)
).toThrow('agent_session_provider_handle_invalid')
}
})
it('refuses a resume that landed on another conversation entirely', () => {
expect(() =>
appendAgentSessionProviderHandleLink(
[adopted()],
link({
linkId: 'claude-1-sess-9-leaf-9',
origin: 'resumed',
handle: claudeProviderHandle('sess-9', 'leaf-9'),
mintedAtFence: 1
})
)
).toThrow('agent_session_provider_handle_forked')
})
it('accepts an adopted head as a persisted chain', () => {
expect(isAgentSessionProviderHandleChain([adopted()])).toBe(true)
})
})
describe('superseding a creation the provider never saved', () => {
const unsaved: AgentSessionProviderHandle = codexProviderHandle('thread-unsaved')
const created = link({ linkId: 'codex-1-thread-unsaved', handle: unsaved })
function replacement(overrides: Partial<AgentSessionProviderHandleLink> = {}) {
return link({
linkId: 'codex-3-thread-new',
handle: codexProviderHandle('thread-new'),
mintedAtFence: 3,
supersedesKey: agentSessionProviderHandleKey(unsaved),
...overrides
})
}
it('replaces the unsaved creation instead of standing beside it', () => {
const chain = appendAgentSessionProviderHandleLink([created], replacement())
expect(chain).toEqual([replacement()])
expect(isAgentSessionProviderHandleChain(chain)).toBe(true)
// An unused chat reopened across many restarts stays one link long.
const again = appendAgentSessionProviderHandleLink(
chain,
replacement({
linkId: 'codex-5-thread-newer',
handle: codexProviderHandle('thread-newer'),
mintedAtFence: 5,
supersedesKey: agentSessionProviderHandleKey(codexProviderHandle('thread-new'))
})
)
expect(again).toHaveLength(1)
expect(again[0]?.handle).toEqual(codexProviderHandle('thread-newer'))
})
it('never supersedes a conversation a resume, fork or adoption proved', () => {
const resumed = link({ linkId: 'codex-2-thread-unsaved', origin: 'resumed', handle: unsaved })
expect(() => appendAgentSessionProviderHandleLink([created, resumed], replacement())).toThrow(
'agent_session_provider_handle_invalid'
)
expect(() =>
appendAgentSessionProviderHandleLink([{ ...created, origin: 'adopted' }], replacement())
).toThrow('agent_session_provider_handle_invalid')
})
it('names exactly the head it replaces, on a new root, under a current fence', () => {
expect(() =>
appendAgentSessionProviderHandleLink(
[created],
replacement({ supersedesKey: 'codex:"thread-other"' })
)
).toThrow('agent_session_provider_handle_invalid')
expect(() =>
appendAgentSessionProviderHandleLink([created], replacement({ handle: unsaved }))
).toThrow('agent_session_provider_handle_invalid')
expect(() =>
appendAgentSessionProviderHandleLink([created], replacement({ linkId: created.linkId }))
).toThrow('agent_session_provider_handle_invalid')
expect(() =>
appendAgentSessionProviderHandleLink(
[{ ...created, mintedAtFence: 4 }],
replacement({ mintedAtFence: 3 })
)
).toThrow('agent_session_provider_handle_stale_fence')
})
it('carries supersession only on a creation, and never as a persisted second link', () => {
expect(
appendAgentSessionProviderHandleLink([], replacement({ origin: 'created' }))
).toHaveLength(1)
expect(() =>
appendAgentSessionProviderHandleLink(
[created],
replacement({ origin: 'resumed', handle: unsaved })
)
).toThrow('agent_session_provider_handle_invalid')
expect(isAgentSessionProviderHandleChain([created, replacement()])).toBe(false)
})
})
describe('a transport shared code has never heard of', () => {
const acp = (nativeId: string, resumeCursor?: string): AgentSessionProviderHandle => ({
transport: 'acp',
agent: 'grok',
nativeId,
...(resumeCursor === undefined ? {} : { resumeCursor })
})
it('chains resumes and forks by its native id alone', () => {
const created = link({ handle: acp('s-1', '{"cwd":"/a"}') })
const resumed = link({
linkId: 'link-2',
origin: 'resumed',
handle: acp('s-1', '{"cwd":"/b"}'),
mintedAtFence: 2
})
const chain = appendAgentSessionProviderHandleLink([created], resumed)
expect(chain).toHaveLength(2)
expect(isAgentSessionProviderHandleChain(chain)).toBe(true)
expect(() =>
appendAgentSessionProviderHandleLink(chain, {
...resumed,
linkId: 'link-3',
handle: acp('s-2')
})
).toThrow('agent_session_provider_handle_forked')
const forked = appendAgentSessionProviderHandleLink(chain, {
...resumed,
linkId: 'link-3',
origin: 'forked',
handle: acp('s-2'),
forkedFromKey: agentSessionProviderHandleKey(resumed.handle)
})
expect(forked.at(-1)?.handle.nativeId).toBe('s-2')
})
it('records a same-fence resume that only moved the resume cursor instead of eliding it', () => {
const created = link({ handle: acp('s-1', 'a') })
const moved = link({ linkId: 'link-2', origin: 'resumed', handle: acp('s-1', 'b') })
expect(appendAgentSessionProviderHandleLink([created], moved)).toHaveLength(2)
expect(
appendAgentSessionProviderHandleLink([created], { ...moved, handle: acp('s-1', 'a') })
).toHaveLength(1)
})
it('never joins a chain of another transport or agent, even with the same id', () => {
const chain = [link({ handle: codexProviderHandle('shared-id') })]
for (const handle of [
acp('shared-id'),
{ transport: 'codex-app-server', agent: 'grok', nativeId: 'shared-id' }
]) {
expect(() =>
appendAgentSessionProviderHandleLink(
chain,
link({ linkId: 'link-2', origin: 'resumed', handle, mintedAtFence: 2 })
)
).toThrow()
}
})
})
describe('replacing a conversation the provider could not restore', () => {
const acp = (nativeId: string): AgentSessionProviderHandle => ({
transport: 'acp',
agent: 'grok',
nativeId
})
const created = link({ handle: acp('s-1') })
const resumed = link({
linkId: 'link-2',
origin: 'resumed',
handle: acp('s-1'),
mintedAtFence: 2
})
const lost = {
key: agentSessionProviderHandleKey(acp('s-1')),
reason: 'restore-failed',
replacedAt: 3_000
}
function fresh(overrides: Partial<AgentSessionProviderHandleLink> = {}) {
return link({
linkId: 'link-3',
handle: acp('s-2'),
mintedAtFence: 3,
observedAt: 3_000,
replaces: lost,
...overrides
})
}
it('follows a reopened conversation and keeps every link before it', () => {
const chain = appendAgentSessionProviderHandleLink([created, resumed], fresh())
expect(chain).toEqual([created, resumed, fresh()])
expect(agentSessionProviderHandleChainHead(chain)?.handle.nativeId).toBe('s-2')
expect(isAgentSessionProviderHandleChain(chain)).toBe(true)
// The fresh conversation then resumes like any other.
const reopened = appendAgentSessionProviderHandleLink(
chain,
link({ linkId: 'link-4', origin: 'resumed', handle: acp('s-2'), mintedAtFence: 4 })
)
expect(reopened).toHaveLength(4)
expect(isAgentSessionProviderHandleChain(reopened)).toBe(true)
})
it('names exactly the head it took over from, on a new root', () => {
for (const bad of [
fresh({ replaces: { ...lost, key: agentSessionProviderHandleKey(acp('s-9')) } }),
fresh({ handle: acp('s-1') }),
fresh({ linkId: 'link-2' }),
fresh({ replaces: undefined })
]) {
expect(() => appendAgentSessionProviderHandleLink([created, resumed], bad)).toThrow(
'agent_session_provider_handle_invalid'
)
}
expect(() =>
appendAgentSessionProviderHandleLink([created, resumed], fresh({ mintedAtFence: 1 }))
).toThrow('agent_session_provider_handle_stale_fence')
})
it('is only ever a creation, and never opens a chain', () => {
expect(() => appendAgentSessionProviderHandleLink([], fresh())).toThrow(
'agent_session_provider_handle_invalid'
)
for (const origin of ['resumed', 'adopted', 'forked'] as const) {
expect(() =>
appendAgentSessionProviderHandleLink(
[created, resumed],
fresh({ origin, forkedFromKey: lost.key })
)
).toThrow('agent_session_provider_handle_invalid')
}
})
it('reads a reason a later build records, but no malformed one', () => {
const later = fresh({ replaces: { ...lost, reason: 'transport-retired' } })
expect(appendAgentSessionProviderHandleLink([created, resumed], later)).toHaveLength(3)
for (const replaces of [
{ ...lost, reason: '' },
{ ...lost, reason: 'x'.repeat(65) },
{ ...lost, replacedAt: -1 },
{ key: lost.key, reason: lost.reason }
]) {
expect(() =>
appendAgentSessionProviderHandleLink(
[created, resumed],
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: deliberately malformed stored data the guard must refuse.
fresh({ replaces: replaces as AgentSessionProviderHandleLink['replaces'] })
)
).toThrow('agent_session_provider_handle_invalid')
}
})
it('stays lost when a fresh conversation the agent never saved is superseded in turn', () => {
const chain = appendAgentSessionProviderHandleLink([created, resumed], fresh())
const superseding = link({
linkId: 'link-5',
handle: acp('s-3'),
mintedAtFence: 5,
supersedesKey: agentSessionProviderHandleKey(acp('s-2'))
})
const superseded = appendAgentSessionProviderHandleLink(chain, superseding)
expect(superseded).toEqual([created, resumed, { ...superseding, replaces: lost }])
expect(isAgentSessionProviderHandleChain(superseded)).toBe(true)
// Unused across many restarts, it still does not grow.
const again = appendAgentSessionProviderHandleLink(superseded, {
...superseding,
linkId: 'link-6',
handle: acp('s-4'),
mintedAtFence: 6,
supersedesKey: agentSessionProviderHandleKey(acp('s-3'))
})
expect(again).toHaveLength(3)
expect(again.at(-1)?.replaces).toEqual(lost)
// A supersession cannot rewrite what was lost, nor land back on it.
expect(() =>
appendAgentSessionProviderHandleLink(chain, {
...superseding,
replaces: { ...lost, replacedAt: 9 }
})
).toThrow('agent_session_provider_handle_invalid')
expect(() =>
appendAgentSessionProviderHandleLink(chain, { ...superseding, handle: acp('s-1') })
).toThrow('agent_session_provider_handle_invalid')
})
it('refuses a supersession key that names no creation in the chain', () => {
expect(() =>
appendAgentSessionProviderHandleLink(
[created, resumed],
fresh({ supersedesKey: agentSessionProviderHandleKey(acp('s-9')) })
)
).toThrow('agent_session_provider_handle_invalid')
})
it('is refused in a Claude or Codex chain, whose rows older builds read', () => {
for (const [first, lost, next] of [
[CLAUDE, CLAUDE, claudeProviderHandle('sess-2', null)],
[codexProviderHandle('t-1'), codexProviderHandle('t-1'), codexProviderHandle('t-2')]
] as const) {
expect(() =>
appendAgentSessionProviderHandleLink(
[link({ handle: first })],
fresh({
handle: next,
replaces: {
key: agentSessionProviderHandleKey(lost),
reason: 'restore-failed',
replacedAt: 3_000
}
})
)
).toThrow('agent_session_provider_handle_invalid')
}
})
it('still supersedes only a creation the provider never saved', () => {
const chain = appendAgentSessionProviderHandleLink([created, resumed], fresh())
const proven = appendAgentSessionProviderHandleLink(
chain,
link({ linkId: 'link-4', origin: 'resumed', handle: acp('s-2'), mintedAtFence: 4 })
)
expect(() =>
appendAgentSessionProviderHandleLink(
proven,
link({
linkId: 'link-5',
handle: acp('s-3'),
mintedAtFence: 5,
supersedesKey: agentSessionProviderHandleKey(acp('s-2'))
})
)
).toThrow('agent_session_provider_handle_invalid')
})
})