Merge branch 'brennanb2025/native-chat-1a-session-store' into brennanb2025/native-chat-structured-base

This commit is contained in:
Brennan Benson
2026-08-09 12:29:13 -07:00
20 changed files with 4117 additions and 1 deletions
@@ -0,0 +1,266 @@
import { describe, expect, it } from 'vitest'
import {
adjudicateAgentSessionRestart,
agentSessionLeaseAdmitsWriter,
classifyObservedAgentSessionSpawnToken,
evaluateAgentSessionAcquisition,
isProvenAliveProbe,
isProvenDeadProbe,
type AgentSessionOwnerProbe
} from './agent-session-lease-adjudication'
import type { AgentSessionLease } from './agent-session-record'
const OWNER = {
hostId: 'local',
pid: 4242,
processStartTimeMs: 1_700_000_000_000,
spawnToken: 'spawn-a'
}
function lease(overrides: Partial<AgentSessionLease> = {}): AgentSessionLease {
return {
sessionId: 'session-alpha-1',
runtimeKind: 'native',
runtimeFence: 7,
handoffStage: null,
provenHandleLinkId: 'link-1',
ownerProcess: OWNER,
reservedSpawnToken: 'spawn-a',
leaseDeadlineAt: 1_000,
lastRenewedAt: 500,
handoffOperationId: null,
journalCheckpoint: null,
claimKeyId: 'key-1',
claimStatus: 'live',
unreconciled: false,
deathEvidence: null,
...overrides
}
}
const MATCHED: AgentSessionOwnerProbe = { outcome: 'identity-matched', matchedOn: ['spawn-token'] }
const INDETERMINATE: AgentSessionOwnerProbe = { outcome: 'indeterminate', reason: 'no answer' }
function acquire(
leaseState: AgentSessionLease,
probe: AgentSessionOwnerProbe,
handoffOperationId: string | null = null
) {
return evaluateAgentSessionAcquisition({
lease: leaseState,
expectedFence: leaseState.runtimeFence,
handoffOperationId,
probe
})
}
describe('proof classification', () => {
it('treats a pid match with nothing PID-reuse-safe as no proof at all', () => {
// A bare pid match is exactly the case that mints a second writer after pid reuse.
expect(isProvenAliveProbe({ outcome: 'identity-matched', matchedOn: [] })).toBe(false)
expect(isProvenAliveProbe(MATCHED)).toBe(true)
expect(isProvenDeadProbe(INDETERMINATE)).toBe(false)
expect(isProvenAliveProbe(INDETERMINATE)).toBe(false)
})
it.each([
['exit-observed', { outcome: 'exit-observed' } as AgentSessionOwnerProbe],
['pid-absent', { outcome: 'pid-absent' } as AgentSessionOwnerProbe],
[
'identity-mismatch',
{ outcome: 'identity-mismatch', field: 'spawn-token' } as AgentSessionOwnerProbe
]
])('accepts %s as proof of death', (_name, probe) => {
expect(isProvenDeadProbe(probe)).toBe(true)
})
it('never counts a reservation probe or an indeterminate answer as death', () => {
expect(isProvenDeadProbe({ outcome: 'reservation-unused' })).toBe(false)
expect(isProvenDeadProbe(INDETERMINATE)).toBe(false)
})
})
describe('acquisition compare-and-swap', () => {
it('refuses a stale fence and grants at exactly fence + 1', () => {
const held = lease({ ownerProcess: null, claimStatus: 'released', reservedSpawnToken: null })
expect(
evaluateAgentSessionAcquisition({
lease: held,
expectedFence: held.runtimeFence - 1,
handoffOperationId: null,
probe: MATCHED
})
).toEqual({ decision: 'refused', code: 'agent_session_checkpoint_stale' })
expect(acquire(held, MATCHED)).toEqual({ decision: 'granted', nextFence: 8 })
})
it('refuses the loser of a concurrent swap: only one caller sees the pre-state fence', () => {
const before = lease({ ownerProcess: null, claimStatus: 'released', reservedSpawnToken: null })
const winner = acquire(before, MATCHED)
expect(winner).toEqual({ decision: 'granted', nextFence: 8 })
// The loser still holds the pre-swap fence, which is no longer current.
const after = lease({ ...before, runtimeFence: 8, claimStatus: 'reserved' })
expect(
evaluateAgentSessionAcquisition({
lease: after,
expectedFence: 7,
handoffOperationId: null,
probe: MATCHED
})
).toEqual({ decision: 'refused', code: 'agent_session_checkpoint_stale' })
})
it('never grants a second owner on expiry alone', () => {
// The recorded owner is long past its deadline; nothing here may consult that deadline.
const expired = lease({ leaseDeadlineAt: 1, lastRenewedAt: 1 })
expect(acquire(expired, INDETERMINATE)).toEqual({
decision: 'refused',
code: 'agent_session_ownership_unknown'
})
expect(acquire(expired, MATCHED)).toEqual({
decision: 'refused',
code: 'agent_session_conflict'
})
expect(acquire(expired, { outcome: 'pid-absent' })).toEqual({
decision: 'granted',
nextFence: 8
})
})
it('refuses while unreconciled even with proof the owner is dead', () => {
expect(acquire(lease({ unreconciled: true }), { outcome: 'pid-absent' })).toEqual({
decision: 'refused',
code: 'execution_owner_reconciling'
})
})
it('keeps a conflicted claim conflicted regardless of proof', () => {
expect(acquire(lease({ claimStatus: 'conflicted' }), { outcome: 'exit-observed' })).toEqual({
decision: 'refused',
code: 'agent_session_conflict'
})
})
it.each([
['recovering', 'agent_session_ownership_unknown'],
['manual-recovery', 'agent_session_ownership_unknown'],
['preparing', 'agent_session_conflict']
] as const)('refuses acquisition in stage %s', (handoffStage, code) => {
expect(acquire(lease({ handoffStage }), { outcome: 'pid-absent' })).toEqual({
decision: 'refused',
code
})
})
it('refuses a different handoff operation and replays the matching one', () => {
const mid = lease({
handoffStage: 'old-owner-stopped',
handoffOperationId: 'op-1',
ownerProcess: null,
claimStatus: 'reserved'
})
expect(acquire(mid, { outcome: 'reservation-unused' }, 'op-2')).toEqual({
decision: 'refused',
code: 'agent_session_operation_conflict'
})
expect(acquire(mid, { outcome: 'reservation-unused' }, 'op-1')).toEqual({
decision: 'retry-reservation',
fence: 7
})
})
it('refuses a reservation whose spawn may have won the race with the crash', () => {
const reserved = lease({ ownerProcess: null, claimStatus: 'reserved', handoffStage: null })
expect(acquire(reserved, INDETERMINATE)).toEqual({
decision: 'refused',
code: 'agent_session_ownership_unknown'
})
expect(acquire(reserved, { outcome: 'reservation-unused' })).toEqual({
decision: 'granted',
nextFence: 8
})
})
})
describe('restart reconciliation', () => {
it('re-adopts a proven-live owner without moving the fence', () => {
expect(
adjudicateAgentSessionRestart({ lease: lease(), probe: MATCHED, observedAt: 9_000 })
).toEqual({ disposition: 'readopt' })
})
it('bumps the fence exactly once for a proven-dead owner and records the evidence', () => {
const result = adjudicateAgentSessionRestart({
lease: lease(),
probe: { outcome: 'identity-mismatch', field: 'process-start-time' },
observedAt: 9_000
})
expect(result).toEqual({
disposition: 'evicted',
nextFence: 8,
evidence: {
kind: 'identity-mismatch',
detail: 'mismatched process-start-time',
observedAt: 9_000
}
})
})
it('sends an unverifiable owner to recovery instead of evicting it', () => {
expect(
adjudicateAgentSessionRestart({
lease: lease({ leaseDeadlineAt: 1 }),
probe: INDETERMINATE,
observedAt: 9_000
})
).toEqual({ disposition: 'recovering', stage: 'recovering', reason: 'no answer' })
})
it('keeps a pre-restart conflict conflicted', () => {
expect(
adjudicateAgentSessionRestart({
lease: lease({ claimStatus: 'conflicted' }),
probe: { outcome: 'pid-absent' },
observedAt: 9_000
})
).toEqual({ disposition: 'conflicted', reason: 'claim conflicted before restart' })
})
it('frees a reservation only when nothing ever spawned', () => {
const reserved = lease({ ownerProcess: null, claimStatus: 'reserved' })
expect(
adjudicateAgentSessionRestart({
lease: reserved,
probe: { outcome: 'reservation-unused' },
observedAt: 9_000
})
).toMatchObject({ disposition: 'evicted', nextFence: 8 })
expect(
adjudicateAgentSessionRestart({ lease: reserved, probe: INDETERMINATE, observedAt: 9_000 })
).toMatchObject({ disposition: 'recovering' })
})
})
describe('writer admission and orphan spawn tokens', () => {
it('admits a writer only when reconciled, settled, live, and holding a process', () => {
expect(agentSessionLeaseAdmitsWriter(lease())).toBe(true)
expect(agentSessionLeaseAdmitsWriter(lease({ unreconciled: true }))).toBe(false)
expect(agentSessionLeaseAdmitsWriter(lease({ handoffStage: 'new-owner-proving' }))).toBe(false)
expect(agentSessionLeaseAdmitsWriter(lease({ claimStatus: 'reserved' }))).toBe(false)
expect(agentSessionLeaseAdmitsWriter(lease({ ownerProcess: null }))).toBe(false)
})
it('calls a spawn token with no matching lease an orphan', () => {
const leases = [lease(), lease({ sessionId: 'session-beta-1', reservedSpawnToken: 'spawn-b' })]
expect(classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-a', leases })).toBe('owned')
expect(classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-b', leases })).toBe('owned')
expect(classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-z', leases })).toBe('orphan')
})
it('still recognises an owner whose reservation token was cleared after proving', () => {
const proved = lease({ reservedSpawnToken: null })
expect(
classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-a', leases: [proved] })
).toBe('owned')
})
})
@@ -0,0 +1,225 @@
/**
* Single-writer lease adjudication.
*
* Every decision here fails closed: expiry alone never grants a second owner, an unverifiable
* process counts as possibly alive, and a stage that cannot prove an owner ends in manual
* recovery rather than handing the session to the other runtime. This is the opposite polarity
* from daemon adoption checks, which fail open on a missing start time — a wrong answer there
* refuses an adoption, a wrong answer here creates two writers on one provider session.
*/
import type {
AgentSessionDeathEvidence,
AgentSessionHandoffStage,
AgentSessionLease
} from './agent-session-record'
export type AgentSessionIdentityMatchField = 'process-start-time' | 'spawn-token'
export type AgentSessionOwnerProbe =
/** Orca watched this exact process exit. */
| { outcome: 'exit-observed' }
/** The recorded pid is not present on the host. */
| { outcome: 'pid-absent' }
/** The pid is present but is a different process. */
| { outcome: 'identity-mismatch'; field: AgentSessionIdentityMatchField | 'command-line' }
/** The pid is present and at least one identity element was verified. */
| { outcome: 'identity-matched'; matchedOn: readonly AgentSessionIdentityMatchField[] }
/** No process carries the reserved spawn token and the provider saw no activity after it. */
| { outcome: 'reservation-unused' }
/** The host could not answer — restricted container, no start time, no token echo. */
| { outcome: 'indeterminate'; reason: string }
export type AgentSessionLeaseRefusalCode =
| 'agent_session_checkpoint_stale'
| 'agent_session_conflict'
| 'agent_session_ownership_unknown'
| 'agent_session_operation_conflict'
| 'execution_owner_reconciling'
export type AgentSessionAcquisitionDecision =
| { decision: 'granted'; nextFence: number }
/** The same handoff operation re-entering its own reservation; no new fence, no new spawn. */
| { decision: 'retry-reservation'; fence: number }
| { decision: 'refused'; code: AgentSessionLeaseRefusalCode }
export type AgentSessionRestartAdjudication =
| { disposition: 'readopt' }
| { disposition: 'evicted'; nextFence: number; evidence: AgentSessionDeathEvidence }
| { disposition: 'recovering'; stage: AgentSessionHandoffStage; reason: string }
| { disposition: 'conflicted'; reason: string }
/** Stages that can legally admit a new owner at all; the rest have an owner or no evidence. */
const STAGES_ADMITTING_NEW_OWNER: ReadonlySet<AgentSessionHandoffStage> = new Set([
'old-owner-stopped',
'new-owner-proving'
])
export function isProvenDeadProbe(probe: AgentSessionOwnerProbe): boolean {
return (
probe.outcome === 'exit-observed' ||
probe.outcome === 'pid-absent' ||
probe.outcome === 'identity-mismatch'
)
}
/**
* A matched pid is only proof of life when something PID-reuse-safe matched with it. A bare pid
* match on a host that can produce neither a start time nor a token echo is indeterminate.
*/
export function isProvenAliveProbe(probe: AgentSessionOwnerProbe): boolean {
return probe.outcome === 'identity-matched' && probe.matchedOn.length > 0
}
function deathEvidenceFor(
probe: AgentSessionOwnerProbe,
observedAt: number
): AgentSessionDeathEvidence | null {
if (probe.outcome === 'exit-observed') {
return { kind: 'exit-observed', detail: 'observed process exit', observedAt }
}
if (probe.outcome === 'pid-absent') {
return { kind: 'pid-absent', detail: 'recorded pid absent on host', observedAt }
}
if (probe.outcome === 'identity-mismatch') {
return { kind: 'identity-mismatch', detail: `mismatched ${probe.field}`, observedAt }
}
return null
}
/** True when the recorded owner may write right now. Used by every mutating path in later parts. */
export function agentSessionLeaseAdmitsWriter(lease: AgentSessionLease): boolean {
return (
!lease.unreconciled &&
lease.handoffStage === null &&
lease.claimStatus === 'live' &&
lease.ownerProcess !== null
)
}
export function isAgentSessionFenceCurrent(lease: AgentSessionLease, fence: number): boolean {
return Number.isSafeInteger(fence) && fence === lease.runtimeFence
}
/**
* Compare-and-swap acquisition. `probe` describes what the host could prove about the recorded
* owner; it is only consulted when a recorded owner or an unused reservation stands in the way.
*/
export function evaluateAgentSessionAcquisition(args: {
lease: AgentSessionLease
expectedFence: number
handoffOperationId: string | null
probe: AgentSessionOwnerProbe
}): AgentSessionAcquisitionDecision {
const { lease, expectedFence, handoffOperationId, probe } = args
if (lease.unreconciled) {
return { decision: 'refused', code: 'execution_owner_reconciling' }
}
if (!isAgentSessionFenceCurrent(lease, expectedFence)) {
return { decision: 'refused', code: 'agent_session_checkpoint_stale' }
}
if (lease.claimStatus === 'conflicted') {
return { decision: 'refused', code: 'agent_session_conflict' }
}
if (lease.handoffStage === 'recovering' || lease.handoffStage === 'manual-recovery') {
// Why: no stage expires into an owner; recovery is resolved by proof or by the user.
return { decision: 'refused', code: 'agent_session_ownership_unknown' }
}
if (lease.handoffStage === 'preparing') {
// Why: the old owner is quiesced but alive and still authoritative.
return { decision: 'refused', code: 'agent_session_conflict' }
}
if (lease.handoffStage !== null && !STAGES_ADMITTING_NEW_OWNER.has(lease.handoffStage)) {
return { decision: 'refused', code: 'agent_session_conflict' }
}
if (lease.handoffStage !== null && lease.handoffOperationId !== null) {
if (handoffOperationId !== lease.handoffOperationId) {
// Why: the retry key is operation id + fence + stage; a different id is a different intent.
return { decision: 'refused', code: 'agent_session_operation_conflict' }
}
if (lease.ownerProcess === null && lease.handoffStage === 'old-owner-stopped') {
// Why: an idempotent re-run of a reservation that already exists at this fence.
return { decision: 'retry-reservation', fence: lease.runtimeFence }
}
}
if (lease.ownerProcess !== null) {
if (!isProvenDeadProbe(probe)) {
// Why: a lapsed deadline means Orca stopped hearing from the owner, not that the child
// stopped editing files and spending tokens.
return {
decision: 'refused',
code: isProvenAliveProbe(probe)
? 'agent_session_conflict'
: 'agent_session_ownership_unknown'
}
}
return { decision: 'granted', nextFence: lease.runtimeFence + 1 }
}
if (lease.claimStatus === 'reserved' && probe.outcome !== 'reservation-unused') {
// Why: a reservation with no proven process is not a free lease — the crash may have lost
// the race with the spawn rather than beaten it.
return { decision: 'refused', code: 'agent_session_ownership_unknown' }
}
return { decision: 'granted', nextFence: lease.runtimeFence + 1 }
}
/**
* Host-restart reconciliation for one persisted lease. Every lease is unreconciled at load and
* grants no writer until this returns.
*/
export function adjudicateAgentSessionRestart(args: {
lease: AgentSessionLease
probe: AgentSessionOwnerProbe
observedAt: number
}): AgentSessionRestartAdjudication {
const { lease, probe, observedAt } = args
if (lease.claimStatus === 'conflicted') {
// Why: the conflict outlives the process that observed it; resolving to free would hand the
// provider session to whichever side restarted first.
return { disposition: 'conflicted', reason: 'claim conflicted before restart' }
}
if (lease.ownerProcess === null) {
if (probe.outcome === 'reservation-unused') {
return {
disposition: 'evicted',
nextFence: lease.runtimeFence + 1,
evidence: { kind: 'pid-absent', detail: 'reservation never spawned', observedAt }
}
}
return {
disposition: 'recovering',
stage: 'recovering',
reason: 'reservation with no proven process'
}
}
if (isProvenAliveProbe(probe)) {
// Why: re-adoption is not a new generation, so the fence does not move.
return { disposition: 'readopt' }
}
const evidence = deathEvidenceFor(probe, observedAt)
if (evidence) {
return { disposition: 'evicted', nextFence: lease.runtimeFence + 1, evidence }
}
return {
disposition: 'recovering',
stage: 'recovering',
reason:
probe.outcome === 'indeterminate' ? probe.reason : 'process identity could not be verified'
}
}
/**
* A process carrying an Orca spawn token with no matching lease is an orphan: stop it, never
* adopt it. Neither age nor CPU is evidence — only a token match justifies acting on a process.
*/
export function classifyObservedAgentSessionSpawnToken(args: {
spawnToken: string
leases: readonly AgentSessionLease[]
}): 'owned' | 'orphan' {
const owned = args.leases.some(
(lease) =>
lease.reservedSpawnToken === args.spawnToken ||
lease.ownerProcess?.spawnToken === args.spawnToken
)
return owned ? 'owned' : 'orphan'
}
@@ -0,0 +1,174 @@
import { describe, expect, it } from 'vitest'
import {
AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS,
AGENT_SESSION_OPERATION_FUTURE_SKEW_MS
} from './agent-session-host-authority'
import {
agentSessionOperationExpiry,
agentSessionOperationKey,
evaluateAgentSessionOperation,
isAgentSessionOperationRow,
pruneAgentSessionOperationRows,
type AgentSessionOperationRow
} from './agent-session-operation-ledger'
const NOW = 1_800_000_000_000
function operationId(timestamp: number, suffix = 'a'.repeat(32)): string {
return `${String(timestamp).padStart(13, '0')}-${suffix}`
}
function evaluate(
rows: Map<string, AgentSessionOperationRow>,
overrides: Partial<{
callerKey: string
operationId: string
fingerprint: string
now: number
perClientLimit: number
globalLimit: number
}> = {}
) {
return evaluateAgentSessionOperation({
rows,
callerKey: 'client-1',
operationId: operationId(NOW),
fingerprint: 'fp-1',
now: NOW,
...overrides
})
}
function admit(
rows: Map<string, AgentSessionOperationRow>,
overrides: Parameters<typeof evaluate>[1] = {}
): AgentSessionOperationRow {
const decision = evaluate(rows, overrides)
if (decision.decision !== 'admit') {
throw new Error(`expected admit, got ${decision.decision}`)
}
rows.set(agentSessionOperationKey(decision.row.callerKey, decision.row.operationId), decision.row)
return decision.row
}
describe('operation admission', () => {
it('admits a fresh id once and replays the identical retry', () => {
const rows = new Map<string, AgentSessionOperationRow>()
const row = admit(rows)
const replay = evaluate(rows)
expect(replay).toEqual({ decision: 'replay', row })
})
it('refuses the same id carrying different parameters', () => {
const rows = new Map<string, AgentSessionOperationRow>()
admit(rows)
expect(evaluate(rows, { fingerprint: 'fp-2' })).toEqual({
decision: 'refused',
code: 'agent_session_operation_conflict'
})
})
it('scopes ids per caller so two clients cannot collide or replay each other', () => {
const rows = new Map<string, AgentSessionOperationRow>()
admit(rows)
expect(evaluate(rows, { callerKey: 'client-2' }).decision).toBe('admit')
})
it('refuses a malformed or future-dated id', () => {
const rows = new Map<string, AgentSessionOperationRow>()
expect(evaluate(rows, { operationId: 'not-an-operation-id' })).toEqual({
decision: 'refused',
code: 'agent_session_operation_invalid'
})
// Why: a future-dated id would look new again after its own tombstone is collected.
expect(
evaluate(rows, {
operationId: operationId(NOW + AGENT_SESSION_OPERATION_FUTURE_SKEW_MS + 1)
})
).toEqual({ decision: 'refused', code: 'agent_session_operation_invalid' })
expect(
evaluate(rows, { operationId: operationId(NOW + AGENT_SESSION_OPERATION_FUTURE_SKEW_MS) })
.decision
).toBe('admit')
})
it('refuses an id older than the admission window instead of treating it as new', () => {
const rows = new Map<string, AgentSessionOperationRow>()
const stale = operationId(NOW - AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS - 1)
expect(evaluate(rows, { operationId: stale })).toEqual({
decision: 'refused',
code: 'agent_session_operation_expired'
})
expect(
evaluate(rows, { operationId: operationId(NOW - AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS) })
.decision
).toBe('admit')
})
it('refuses new ids at the per-client and global caps rather than evicting tombstones', () => {
const rows = new Map<string, AgentSessionOperationRow>()
admit(rows, { operationId: operationId(NOW, 'b'.repeat(32)) })
expect(evaluate(rows, { perClientLimit: 1 })).toEqual({
decision: 'refused',
code: 'agent_session_operation_capacity'
})
// A different caller is still refused once the global cap is reached.
expect(evaluate(rows, { callerKey: 'client-2', globalLimit: 1 })).toEqual({
decision: 'refused',
code: 'agent_session_operation_capacity'
})
expect(evaluate(rows, { callerKey: 'client-2', perClientLimit: 1 }).decision).toBe('admit')
})
})
describe('retention', () => {
it('keeps a tombstone strictly longer than its id can be admitted as new', () => {
const expiry = agentSessionOperationExpiry(NOW, NOW)
const lastAdmissibleAt = NOW + AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS
expect(expiry).toBeGreaterThan(lastAdmissibleAt)
// Why: a retry landing in that gap would become a second spawn instead of a replay.
const rows = new Map<string, AgentSessionOperationRow>()
admit(rows)
expect(pruneAgentSessionOperationRows(rows, lastAdmissibleAt).size).toBe(1)
expect(evaluate(pruneAgentSessionOperationRows(rows, lastAdmissibleAt)).decision).toBe('replay')
})
it('anchors retention to the later of recording and stamping', () => {
const late = agentSessionOperationExpiry(NOW + 10_000, NOW)
expect(late).toBe(agentSessionOperationExpiry(NOW + 10_000, NOW + 10_000))
expect(late).toBeGreaterThan(agentSessionOperationExpiry(NOW, NOW))
})
it('drops only rows past their own expiry', () => {
const rows = new Map<string, AgentSessionOperationRow>()
const row = admit(rows)
expect(pruneAgentSessionOperationRows(rows, row.expiresAt).size).toBe(0)
expect(pruneAgentSessionOperationRows(rows, row.expiresAt - 1).size).toBe(1)
})
})
describe('persisted row validation', () => {
it('accepts every recorded outcome shape', () => {
const rows = new Map<string, AgentSessionOperationRow>()
const row = admit(rows)
expect(isAgentSessionOperationRow(row)).toBe(true)
expect(
isAgentSessionOperationRow({ ...row, outcome: { status: 'succeeded', sessionId: 's-1' } })
).toBe(true)
expect(isAgentSessionOperationRow({ ...row, outcome: { status: 'unknown' } })).toBe(true)
expect(isAgentSessionOperationRow({ ...row, outcome: { status: 'failed', code: 'x' } })).toBe(
true
)
})
it('rejects rows a later build could misread', () => {
const rows = new Map<string, AgentSessionOperationRow>()
const row = admit(rows)
expect(isAgentSessionOperationRow({ ...row, operationId: 'garbage' })).toBe(false)
expect(isAgentSessionOperationRow({ ...row, callerKey: '' })).toBe(false)
expect(isAgentSessionOperationRow({ ...row, expiresAt: 1.5 })).toBe(false)
expect(isAgentSessionOperationRow({ ...row, outcome: { status: 'succeeded' } })).toBe(false)
expect(isAgentSessionOperationRow({ ...row, outcome: null })).toBe(false)
expect(isAgentSessionOperationRow(null)).toBe(false)
})
})
@@ -0,0 +1,173 @@
/**
* Durable client-operation ledger.
*
* `terminal.ensureAgentSession` / `terminal.createAgentSession` already enforce timestamped
* operation ids with fingerprint conflict detection, age expiry, capacity limits, and tombstone
* retention — but in memory, so a host restart turns "replay this create" into "spawn another
* agent". These are the same rules over rows that survive a restart; the store writes a row in
* the same atomic transaction as the lease reservation.
*/
import {
AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS,
AGENT_SESSION_OPERATION_FUTURE_SKEW_MS,
parseAgentSessionOperationTimestamp
} from './agent-session-host-authority'
export const AGENT_SESSION_DURABLE_OPERATION_PER_CLIENT_LIMIT = 512
export const AGENT_SESSION_DURABLE_OPERATION_GLOBAL_LIMIT = 4_096
export type AgentSessionOperationOutcome =
| { status: 'pending' }
| { status: 'succeeded'; sessionId: string }
| { status: 'failed'; code: string }
/** The effect may or may not have happened; replay this answer instead of spawning again. */
| { status: 'unknown' }
export type AgentSessionOperationRow = {
callerKey: string
operationId: string
fingerprint: string
operationTimestamp: number
recordedAt: number
expiresAt: number
outcome: AgentSessionOperationOutcome
}
export type AgentSessionOperationRefusalCode =
| 'agent_session_operation_invalid'
| 'agent_session_operation_conflict'
| 'agent_session_operation_expired'
| 'agent_session_operation_capacity'
export type AgentSessionOperationDecision =
| { decision: 'replay'; row: AgentSessionOperationRow }
| { decision: 'admit'; row: AgentSessionOperationRow }
| { decision: 'refused'; code: AgentSessionOperationRefusalCode }
/** NUL cannot occur in a caller key or operation id, so no pair can forge another pair's key. */
const OPERATION_KEY_SEPARATOR = '\u0000'
export function agentSessionOperationKey(callerKey: string, operationId: string): string {
return `${callerKey}${OPERATION_KEY_SEPARATOR}${operationId}`
}
/**
* Retention floor. The tombstone must outlive the window in which its id could still be admitted
* as new, plus the accepted future skew — otherwise a retry arriving in the gap becomes a second
* spawn instead of a replay.
*/
export function agentSessionOperationExpiry(
operationTimestamp: number,
recordedAt: number
): number {
return (
Math.max(recordedAt, operationTimestamp) +
AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS +
AGENT_SESSION_OPERATION_FUTURE_SKEW_MS
)
}
export function pruneAgentSessionOperationRows(
rows: ReadonlyMap<string, AgentSessionOperationRow>,
now: number
): Map<string, AgentSessionOperationRow> {
const kept = new Map<string, AgentSessionOperationRow>()
for (const [key, row] of rows) {
if (row.expiresAt > now) {
kept.set(key, row)
}
}
return kept
}
/**
* Decide what a mutating call with this operation id means against the persisted ledger. Callers
* must prune first; a row that is present is a row that is still authoritative.
*/
export function evaluateAgentSessionOperation(args: {
rows: ReadonlyMap<string, AgentSessionOperationRow>
callerKey: string
operationId: string
fingerprint: string
now: number
perClientLimit?: number
globalLimit?: number
}): AgentSessionOperationDecision {
const { rows, callerKey, operationId, fingerprint, now } = args
const operationTimestamp = parseAgentSessionOperationTimestamp(operationId)
if (
operationTimestamp === null ||
operationTimestamp > now + AGENT_SESSION_OPERATION_FUTURE_SKEW_MS
) {
// Why: a future-dated id could look new again after its tombstone is collected.
return { decision: 'refused', code: 'agent_session_operation_invalid' }
}
const key = agentSessionOperationKey(callerKey, operationId)
const existing = rows.get(key)
if (existing) {
return existing.fingerprint === fingerprint
? { decision: 'replay', row: existing }
: { decision: 'refused', code: 'agent_session_operation_conflict' }
}
if (now - operationTimestamp > AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS) {
// Why: once a tombstone could have expired, an unseen replay must never be reinterpreted as
// permission to start another fresh agent.
return { decision: 'refused', code: 'agent_session_operation_expired' }
}
const perClientLimit = args.perClientLimit ?? AGENT_SESSION_DURABLE_OPERATION_PER_CLIENT_LIMIT
const globalLimit = args.globalLimit ?? AGENT_SESSION_DURABLE_OPERATION_GLOBAL_LIMIT
let callerCount = 0
for (const row of rows.values()) {
if (row.callerKey === callerKey) {
callerCount += 1
}
}
if (callerCount >= perClientLimit || rows.size >= globalLimit) {
// Why: tombstones cannot be evicted early without making an old replay capable of spawning
// again; reject new ids until retained rows age out.
return { decision: 'refused', code: 'agent_session_operation_capacity' }
}
return {
decision: 'admit',
row: {
callerKey,
operationId,
fingerprint,
operationTimestamp,
recordedAt: now,
expiresAt: agentSessionOperationExpiry(operationTimestamp, now),
outcome: { status: 'pending' }
}
}
}
const OPERATION_ID_MAX_LENGTH = 128
export function isAgentSessionOperationRow(value: unknown): value is AgentSessionOperationRow {
if (typeof value !== 'object' || value === null) {
return false
}
const row = value as Partial<AgentSessionOperationRow>
const outcome = row.outcome as AgentSessionOperationOutcome | undefined
const outcomeValid =
typeof outcome === 'object' &&
outcome !== null &&
((outcome.status === 'pending' && true) ||
(outcome.status === 'succeeded' && typeof outcome.sessionId === 'string') ||
(outcome.status === 'failed' && typeof outcome.code === 'string') ||
outcome.status === 'unknown')
return (
typeof row.callerKey === 'string' &&
row.callerKey.length > 0 &&
typeof row.operationId === 'string' &&
row.operationId.length <= OPERATION_ID_MAX_LENGTH &&
parseAgentSessionOperationTimestamp(row.operationId) !== null &&
typeof row.fingerprint === 'string' &&
row.fingerprint.length > 0 &&
Number.isSafeInteger(row.operationTimestamp) &&
Number.isSafeInteger(row.recordedAt) &&
Number.isSafeInteger(row.expiresAt) &&
outcomeValid
)
}
@@ -0,0 +1,294 @@
import { describe, expect, it } from 'vitest'
import {
agentSessionProviderHandleChainHead,
agentSessionProviderHandleKey,
agentSessionProviderHandleRoot,
agentSessionProviderHandlesEqual,
appendAgentSessionProviderHandleLink,
findAgentSessionProviderHandleLink,
isAgentSessionProviderHandle,
isAgentSessionProviderHandleChain,
MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS,
type AgentSessionProviderHandle,
type AgentSessionProviderHandleLink
} from './agent-session-provider-handle'
const CLAUDE: AgentSessionProviderHandle = {
provider: 'claude',
sessionId: 'sess-1',
leafUuid: '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('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({ ...CLAUDE, leafUuid: 'leaf-2' })
expect(branchA).not.toEqual(branchB)
expect(agentSessionProviderHandleRoot(CLAUDE)).toEqual(
agentSessionProviderHandleRoot({ ...CLAUDE, leafUuid: 'leaf-2' })
)
})
it('keys a Codex handle by thread id alone', () => {
const codex: AgentSessionProviderHandle = { provider: 'codex', threadId: 'thread-1' }
expect(agentSessionProviderHandleKey(codex)).toBe('codex:"thread-1"')
expect(agentSessionProviderHandleRoot(codex)).toBe('codex:"thread-1"')
expect(
agentSessionProviderHandlesEqual(codex, { provider: 'codex', threadId: 'thread-2' })
).toBe(false)
})
it('distinguishes a null leaf from an empty-string leaf and rejects malformed handles', () => {
expect(isAgentSessionProviderHandle({ ...CLAUDE, leafUuid: null })).toBe(true)
expect(isAgentSessionProviderHandle({ ...CLAUDE, leafUuid: '' })).toBe(false)
expect(isAgentSessionProviderHandle({ provider: 'claude', sessionId: '' })).toBe(false)
expect(isAgentSessionProviderHandle({ provider: 'gemini', sessionId: 'x' })).toBe(false)
expect(isAgentSessionProviderHandle({ ...CLAUDE, sessionId: ' sess-1 ' })).toBe(false)
})
it('uses collision-free keys when Claude ids contain delimiters', () => {
const left: AgentSessionProviderHandle = {
provider: 'claude',
sessionId: 'a#b',
leafUuid: 'c'
}
const right: AgentSessionProviderHandle = {
provider: 'claude',
sessionId: 'a',
leafUuid: '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: { provider: 'claude', sessionId: 'sess-2', leafUuid: '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: { provider: 'claude', sessionId: 'sess-2', leafUuid: '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: { ...CLAUDE, leafUuid: '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: { provider: 'codex', threadId: '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: { ...CLAUDE, leafUuid: '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: { ...CLAUDE, leafUuid: `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: { ...CLAUDE, leafUuid: '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: { ...CLAUDE, leafUuid: '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: { ...CLAUDE, leafUuid: '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: { ...CLAUDE, leafUuid: 'leaf-2' },
mintedAtFence: 2
})
])
).toBe(false)
expect(
isAgentSessionProviderHandleChain([
link(),
link({
linkId: 'link-2',
origin: 'resumed',
handle: { provider: 'claude', sessionId: 'sess-2', leafUuid: 'leaf-2' },
mintedAtFence: 2
})
])
).toBe(false)
})
})
+212
View File
@@ -0,0 +1,212 @@
/**
* Durable provider handle chain for an agent session.
*
* Handles are keyed per provider because the two structured lanes disagree about what
* identifies a conversation: a Claude session id alone can name two sibling branches inside one
* transcript, so the leaf uuid is part of the key; a Codex thread id is the whole key. Resumes
* extend the chain, forks start a new identity root, and the chain records which is which so a
* fork is never presented as a resume.
*/
export const AGENT_SESSION_PROVIDER_HANDLE_PROVIDERS = ['claude', 'codex'] as const
export type AgentSessionHandleProvider = (typeof AGENT_SESSION_PROVIDER_HANDLE_PROVIDERS)[number]
export type AgentSessionProviderHandle =
| { provider: 'claude'; sessionId: string; leafUuid: string | null }
| { provider: 'codex'; threadId: string }
export type AgentSessionProviderHandleOrigin = 'created' | 'adopted' | 'resumed' | 'forked'
export type AgentSessionProviderHandleLink = {
/** Stable id so a lease can name the exact link its owner proved. */
linkId: string
handle: AgentSessionProviderHandle
origin: AgentSessionProviderHandleOrigin
/** Runtime fence in force when this link was minted; never decreases along the chain. */
mintedAtFence: number
observedAt: number
/** Key of the link a fork was seeded from. Only set when `origin` is `forked`. */
forkedFromKey?: string
}
export type AgentSessionProviderHandleChain = readonly AgentSessionProviderHandleLink[]
/** Bounded so one session cannot grow an unbounded persisted record. */
export const MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS = 256
const MAX_HANDLE_FIELD_LENGTH = 512
const LINK_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
function isHandleField(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value.length <= MAX_HANDLE_FIELD_LENGTH &&
value === value.trim()
)
}
export function isAgentSessionProviderHandle(value: unknown): value is AgentSessionProviderHandle {
if (typeof value !== 'object' || value === null) {
return false
}
const handle = value as Partial<AgentSessionProviderHandle> & Record<string, unknown>
if (handle.provider === 'claude') {
return (
isHandleField(handle.sessionId) &&
(handle.leafUuid === null || isHandleField(handle.leafUuid))
)
}
return handle.provider === 'codex' && isHandleField(handle.threadId)
}
/** Stable string identity for one handle. Two handles with the same key name the same writer target. */
export function agentSessionProviderHandleKey(handle: AgentSessionProviderHandle): string {
return handle.provider === 'claude'
? `claude:${JSON.stringify([handle.sessionId, handle.leafUuid])}`
: `codex:${JSON.stringify(handle.threadId)}`
}
/**
* Identity root: the part that a resume must preserve. A resume that changes the root is a fork,
* whatever the provider called it.
*/
export function agentSessionProviderHandleRoot(handle: AgentSessionProviderHandle): string {
return handle.provider === 'claude'
? `claude:${JSON.stringify(handle.sessionId)}`
: `codex:${JSON.stringify(handle.threadId)}`
}
export function agentSessionProviderHandlesEqual(
left: AgentSessionProviderHandle,
right: AgentSessionProviderHandle
): boolean {
return agentSessionProviderHandleKey(left) === agentSessionProviderHandleKey(right)
}
export function agentSessionProviderHandleChainHead(
chain: AgentSessionProviderHandleChain
): AgentSessionProviderHandleLink | null {
return chain.at(-1) ?? null
}
export function findAgentSessionProviderHandleLink(
chain: AgentSessionProviderHandleChain,
linkId: string
): AgentSessionProviderHandleLink | null {
return chain.find((link) => link.linkId === linkId) ?? null
}
export function isAgentSessionProviderHandleLink(
value: unknown
): value is AgentSessionProviderHandleLink {
if (typeof value !== 'object' || value === null) {
return false
}
const link = value as Partial<AgentSessionProviderHandleLink>
const originValid =
link.origin === 'created' ||
link.origin === 'adopted' ||
link.origin === 'resumed' ||
link.origin === 'forked'
return (
typeof link.linkId === 'string' &&
LINK_ID_PATTERN.test(link.linkId) &&
isAgentSessionProviderHandle(link.handle) &&
originValid &&
Number.isSafeInteger(link.mintedAtFence) &&
(link.mintedAtFence as number) >= 0 &&
Number.isSafeInteger(link.observedAt) &&
(link.origin === 'forked'
? isHandleField(link.forkedFromKey)
: link.forkedFromKey === undefined)
)
}
export function isAgentSessionProviderHandleChain(
value: unknown
): value is AgentSessionProviderHandleLink[] {
if (!Array.isArray(value) || value.length > MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS) {
return false
}
let validated: AgentSessionProviderHandleLink[] = []
try {
for (const link of value) {
if (!isAgentSessionProviderHandleLink(link)) {
return false
}
const next = appendAgentSessionProviderHandleLink(validated, link)
// A persisted chain must name every link exactly once; retry elision belongs at append time.
if (next.length !== validated.length + 1) {
return false
}
validated = next
}
return true
} catch {
return false
}
}
/**
* Append one link, rejecting anything that would let a fork masquerade as a resume or let a
* late writer rewrite the chain under an older fence.
*/
export function appendAgentSessionProviderHandleLink(
chain: AgentSessionProviderHandleChain,
link: AgentSessionProviderHandleLink
): AgentSessionProviderHandleLink[] {
if (!isAgentSessionProviderHandleLink(link)) {
throw new Error('agent_session_provider_handle_invalid')
}
const head = agentSessionProviderHandleChainHead(chain)
if (!head) {
if (link.origin !== 'created' && link.origin !== 'adopted') {
throw new Error('agent_session_provider_handle_invalid')
}
return [link]
}
if (link.handle.provider !== head.handle.provider) {
throw new Error('agent_session_provider_handle_provider_mismatch')
}
if (link.mintedAtFence < head.mintedAtFence) {
throw new Error('agent_session_provider_handle_stale_fence')
}
if (link.origin === 'created' || link.origin === 'adopted') {
throw new Error('agent_session_provider_handle_invalid')
}
const sameRoot =
agentSessionProviderHandleRoot(link.handle) === agentSessionProviderHandleRoot(head.handle)
if (link.origin === 'resumed' && !sameRoot) {
// Why: a resume that lands on another identity root forked; recording it as a resume would
// make Orca claim continuity the provider never gave.
throw new Error('agent_session_provider_handle_forked')
}
if (link.origin === 'forked') {
if (sameRoot) {
throw new Error('agent_session_provider_handle_invalid')
}
if (link.forkedFromKey !== agentSessionProviderHandleKey(head.handle)) {
throw new Error('agent_session_provider_handle_invalid')
}
}
if (
link.origin === 'resumed' &&
agentSessionProviderHandlesEqual(link.handle, head.handle) &&
link.mintedAtFence === head.mintedAtFence
) {
// Why: re-proving the same handle at the same fence is a retry, not a new identity.
return [...chain]
}
if (findAgentSessionProviderHandleLink(chain, link.linkId)) {
// Why: the lease names its exact proof by link id; reuse would make that reference ambiguous.
throw new Error('agent_session_provider_handle_invalid')
}
if (chain.length >= MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS) {
// Why: dropping older links would erase fork provenance, so refuse and let the caller roll
// the journal epoch instead of silently losing where this conversation came from.
throw new Error('agent_session_provider_handle_chain_overflow')
}
return [...chain, link]
}
+283
View File
@@ -0,0 +1,283 @@
/**
* Durable agent-session record and its single-writer lease.
*
* The record is the session's identity — where it runs, which provider it talks to, which account
* home is pinned to it — and is independent of any terminal tab. The lease is the separate
* question of which process is currently allowed to write to it.
*/
import type { ExecutionHostId } from './execution-host'
import {
isAgentSessionProviderHandleChain,
type AgentSessionHandleProvider,
type AgentSessionProviderHandleLink
} from './agent-session-provider-handle'
export const AGENT_SESSION_RECORD_SCHEMA_VERSION = 1 as const
export type AgentSessionWorkspaceKind = 'git-worktree' | 'folder'
/**
* Where the provider process actually runs. WSL is called out separately from the execution host
* id because a WSL workspace is served by the local host but is a distinct filesystem, account
* root, and process namespace — two sessions there must never collide with their native twins.
*/
export type AgentSessionExecutionLocation = {
executionHostId: ExecutionHostId
/** Distro name when the provider runs inside WSL; null for native and remote hosts. */
wslDistro: string | null
workspaceId: string
workspaceKind: AgentSessionWorkspaceKind
}
/** Account root pinned at launch by the account selector, so a resume cannot drift to another login. */
export type AgentSessionAccountHome = {
variable: 'CLAUDE_CONFIG_DIR' | 'CODEX_HOME'
/** Host-resolved absolute path in the execution host's own path syntax. */
path: string
}
export type AgentSessionOwnerRuntimeKind = 'native' | 'tui'
export type AgentSessionHandoffStage =
| 'preparing'
| 'old-owner-stopped'
| 'new-owner-proving'
| 'recovering'
| 'manual-recovery'
/**
* PID-reuse-safe process identity. `spawnToken` is the only element available on every platform:
* process start time costs a CIM query on Windows and is absent in some containers, so a runtime
* that cannot echo the token back qualifies for manual recovery only.
*/
export type AgentSessionProcessIdentity = {
hostId: string
pid: number
processStartTimeMs: number | null
spawnToken: string
}
export type AgentSessionJournalCheckpoint = { epoch: number; sequence: number }
/**
* Mirrors the in-memory claim registry's reserved / live / conflicted states so a conflict
* survives a restart. `released` has no registry equivalent: the registry expresses "no owner" by
* deleting the entry, and a durable record that outlives its owner needs a name for that.
*/
export type AgentSessionClaimStatus = 'reserved' | 'live' | 'conflicted' | 'released'
export type AgentSessionDeathEvidence = {
kind: 'exit-observed' | 'pid-absent' | 'identity-mismatch'
detail: string
observedAt: number
}
export type AgentSessionLease = {
sessionId: string
runtimeKind: AgentSessionOwnerRuntimeKind
/** Durable monotonic integer; only acquisition CAS and proven eviction move it. */
runtimeFence: number
handoffStage: AgentSessionHandoffStage | null
/** Link id of the provider handle this owner proved; the full chain lives on the record. */
provenHandleLinkId: string | null
/** Null between the durable reservation and the observed spawn. */
ownerProcess: AgentSessionProcessIdentity | null
/** Reserved before any process exists, then matched against the child's environment. */
reservedSpawnToken: string | null
leaseDeadlineAt: number
lastRenewedAt: number
handoffOperationId: string | null
journalCheckpoint: AgentSessionJournalCheckpoint | null
/** Key id that minted the HMAC claim this lease was granted under. */
claimKeyId: string
claimStatus: AgentSessionClaimStatus
/** True from load until the host adjudicates it; no writer is granted while set. */
unreconciled: boolean
deathEvidence: AgentSessionDeathEvidence | null
}
export type AgentSessionRecord = {
schemaVersion: typeof AGENT_SESSION_RECORD_SCHEMA_VERSION
sessionId: string
location: AgentSessionExecutionLocation
provider: AgentSessionHandleProvider
providerHandleChain: AgentSessionProviderHandleLink[]
accountHome: AgentSessionAccountHome
lease: AgentSessionLease
createdAt: number
updatedAt: number
}
const MAX_ID_LENGTH = 512
const MAX_PATH_LENGTH = 4096
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/
function isBoundedString(value: unknown, max: number): value is string {
return typeof value === 'string' && value.length > 0 && value.length <= max
}
export function isAgentSessionId(value: unknown): value is string {
return typeof value === 'string' && SESSION_ID_PATTERN.test(value)
}
/** NUL cannot occur in a host id, distro name, or workspace id, so no component can forge a join. */
const SCOPE_KEY_SEPARATOR = '\u0000'
/**
* Scope key for host-and-workspace isolation. Native, WSL, and SSH copies of one workspace id are
* different sessions; collapsing them would let one host adjudicate another host's lease.
*/
export function agentSessionScopeKey(location: AgentSessionExecutionLocation): string {
return [location.executionHostId, location.wslDistro ?? '', location.workspaceId].join(
SCOPE_KEY_SEPARATOR
)
}
export function agentSessionExecutionLocationsEqual(
left: AgentSessionExecutionLocation,
right: AgentSessionExecutionLocation
): boolean {
return (
agentSessionScopeKey(left) === agentSessionScopeKey(right) &&
left.workspaceKind === right.workspaceKind
)
}
export function isAgentSessionExecutionLocation(
value: unknown
): value is AgentSessionExecutionLocation {
if (typeof value !== 'object' || value === null) {
return false
}
const location = value as Partial<AgentSessionExecutionLocation>
return (
isBoundedString(location.executionHostId, MAX_ID_LENGTH) &&
(location.wslDistro === null || isBoundedString(location.wslDistro, MAX_ID_LENGTH)) &&
isBoundedString(location.workspaceId, MAX_ID_LENGTH) &&
(location.workspaceKind === 'git-worktree' || location.workspaceKind === 'folder')
)
}
export function isAgentSessionProcessIdentity(
value: unknown
): value is AgentSessionProcessIdentity {
if (typeof value !== 'object' || value === null) {
return false
}
const identity = value as Partial<AgentSessionProcessIdentity>
return (
isBoundedString(identity.hostId, MAX_ID_LENGTH) &&
Number.isSafeInteger(identity.pid) &&
(identity.pid as number) > 0 &&
(identity.processStartTimeMs === null ||
(Number.isSafeInteger(identity.processStartTimeMs) &&
(identity.processStartTimeMs as number) >= 0)) &&
isBoundedString(identity.spawnToken, MAX_ID_LENGTH)
)
}
function isAgentSessionAccountHome(value: unknown): value is AgentSessionAccountHome {
if (typeof value !== 'object' || value === null) {
return false
}
const home = value as Partial<AgentSessionAccountHome>
return (
(home.variable === 'CLAUDE_CONFIG_DIR' || home.variable === 'CODEX_HOME') &&
isBoundedString(home.path, MAX_PATH_LENGTH)
)
}
function isAgentSessionJournalCheckpoint(value: unknown): value is AgentSessionJournalCheckpoint {
if (typeof value !== 'object' || value === null) {
return false
}
const checkpoint = value as Partial<AgentSessionJournalCheckpoint>
return (
Number.isSafeInteger(checkpoint.epoch) &&
(checkpoint.epoch as number) >= 0 &&
Number.isSafeInteger(checkpoint.sequence) &&
(checkpoint.sequence as number) >= 0
)
}
function isAgentSessionDeathEvidence(value: unknown): value is AgentSessionDeathEvidence {
if (typeof value !== 'object' || value === null) {
return false
}
const evidence = value as Partial<AgentSessionDeathEvidence>
return (
(evidence.kind === 'exit-observed' ||
evidence.kind === 'pid-absent' ||
evidence.kind === 'identity-mismatch') &&
isBoundedString(evidence.detail, MAX_ID_LENGTH) &&
Number.isSafeInteger(evidence.observedAt) &&
(evidence.observedAt as number) >= 0
)
}
function isAgentSessionLease(value: unknown): value is AgentSessionLease {
if (typeof value !== 'object' || value === null) {
return false
}
const lease = value as Partial<AgentSessionLease>
return (
isAgentSessionId(lease.sessionId) &&
(lease.runtimeKind === 'native' || lease.runtimeKind === 'tui') &&
Number.isSafeInteger(lease.runtimeFence) &&
(lease.runtimeFence as number) >= 0 &&
(lease.handoffStage === null ||
lease.handoffStage === 'preparing' ||
lease.handoffStage === 'old-owner-stopped' ||
lease.handoffStage === 'new-owner-proving' ||
lease.handoffStage === 'recovering' ||
lease.handoffStage === 'manual-recovery') &&
(lease.provenHandleLinkId === null || isBoundedString(lease.provenHandleLinkId, 128)) &&
(lease.ownerProcess === null || isAgentSessionProcessIdentity(lease.ownerProcess)) &&
(lease.reservedSpawnToken === null ||
isBoundedString(lease.reservedSpawnToken, MAX_ID_LENGTH)) &&
Number.isSafeInteger(lease.leaseDeadlineAt) &&
Number.isSafeInteger(lease.lastRenewedAt) &&
(lease.handoffOperationId === null ||
isBoundedString(lease.handoffOperationId, MAX_ID_LENGTH)) &&
(lease.journalCheckpoint === null ||
isAgentSessionJournalCheckpoint(lease.journalCheckpoint)) &&
isBoundedString(lease.claimKeyId, MAX_ID_LENGTH) &&
(lease.claimStatus === 'reserved' ||
lease.claimStatus === 'live' ||
lease.claimStatus === 'conflicted' ||
lease.claimStatus === 'released') &&
typeof lease.unreconciled === 'boolean' &&
(lease.deathEvidence === null || isAgentSessionDeathEvidence(lease.deathEvidence))
)
}
export function isAgentSessionRecord(value: unknown): value is AgentSessionRecord {
if (typeof value !== 'object' || value === null) {
return false
}
const record = value as Partial<AgentSessionRecord>
const shapeValid =
record.schemaVersion === AGENT_SESSION_RECORD_SCHEMA_VERSION &&
isAgentSessionId(record.sessionId) &&
isAgentSessionExecutionLocation(record.location) &&
(record.provider === 'claude' || record.provider === 'codex') &&
isAgentSessionProviderHandleChain(record.providerHandleChain) &&
isAgentSessionAccountHome(record.accountHome) &&
isAgentSessionLease(record.lease) &&
record.lease.sessionId === record.sessionId &&
Number.isSafeInteger(record.createdAt) &&
Number.isSafeInteger(record.updatedAt)
if (!shapeValid) {
return false
}
const validated = record as AgentSessionRecord
const head = validated.providerHandleChain.at(-1)
return (
validated.providerHandleChain.every((link) => link.handle.provider === validated.provider) &&
(validated.lease.claimStatus !== 'live' ||
(validated.lease.ownerProcess !== null &&
head?.linkId === validated.lease.provenHandleLinkId &&
head.mintedAtFence === validated.lease.runtimeFence))
)
}