import { sha256 } from './sha256' function canonicalize(value: unknown): string { if (value === null || typeof value !== 'object') { return JSON.stringify(value ?? null) } if (Array.isArray(value)) { return `[${value.map(canonicalize).join(',')}]` } const entries = Object.entries(value as Record) .filter(([, entry]) => entry !== undefined) .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalize(entry)}`).join(',')}}` } export function structuredAgentSessionPayloadFingerprint(input: { method: string sessionId: string fields: Record }): string { const bytes = sha256( new TextEncoder().encode( canonicalize({ method: input.method, sessionId: input.sessionId, fields: input.fields }) ) ) return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('') } export function structuredAgentSessionCreateFingerprint(input: { sessionId: string worktree: string agent: 'claude' | 'codex' resumeFrom?: { providerSessionId: string } }): string { return structuredAgentSessionPayloadFingerprint({ method: 'agentSession.create', sessionId: input.sessionId, fields: { worktree: input.worktree, agent: input.agent, // `canonicalize` drops undefined, so a plain create keeps the digest it has always had. // Adopting a conversation is a different intent and must not replay as a blank create. resumeFrom: input.resumeFrom } }) } export function showStructuredAgentSessionChoice(input: { hostCapability: boolean workspaceSupport: boolean agent: string }): boolean { return ( input.hostCapability && input.workspaceSupport && (input.agent === 'claude' || input.agent === 'codex') ) } export function createStructuredAgentSessionOperationId( randomUuid: () => string, now: number = Date.now() ): string { const timestamp = Math.trunc(now).toString() const entropy = randomUuid().replaceAll('-', '').toLowerCase() if (!/^\d{13}$/.test(timestamp) || !/^[0-9a-f]{32}$/.test(entropy)) { throw new Error('Unable to create a durable operation id') } return `${timestamp}-${entropy}` }