export interface DurableSessionIdentity { /** Provisioned roster identity; isolates durable state between named agents. */ agentName: string; sessionId: string; tenantId: string; ownerId: string; providerId: string; runtimeSessionId: string; } export type DurableInboxStatus = 'pending' | 'processing' | 'processed'; export type DurableOutboxStatus = 'pending' | 'processing' | 'delivered'; export interface DurableInboxInput { sessionId: string; idempotencyKey: string; correlationId: string; content: string; } export interface DurableInboxEntry extends DurableInboxInput { status: DurableInboxStatus; } export interface DurableOutboxInput { sessionId: string; idempotencyKey: string; correlationId: string; channelId: string; kind: string; content: string; } export interface DurableOutboxEntry extends DurableOutboxInput { status: DurableOutboxStatus; } export interface DurableCheckpointInput { sessionId: string; checkpointId: string; cursor: string; summary: string; compactionEpoch: number; } export interface DurableCheckpoint extends DurableCheckpointInput {} export interface DurableHandoffInput { sessionId: string; handoffId: string; destination: string; correlationId: string; checkpointId: string; status: 'pending' | 'accepted'; } export interface DurableHandoff extends DurableHandoffInput {} export interface DurableSessionSnapshot { identity: DurableSessionIdentity; inbox: DurableInboxEntry[]; outbox: DurableOutboxEntry[]; checkpoint?: DurableCheckpoint; handoffs: DurableHandoff[]; } export interface DurableHandoffRecovery { identity: DurableSessionIdentity; checkpoint: DurableCheckpoint; handoff: DurableHandoff; } export interface DurableEnqueueResult { accepted: boolean; status: TStatus; } /** * A durable-state port. Implementations must atomically claim and complete work * records. Recovery may requeue interrupted inbox work, but never an * externally visible outbox effect: an ambiguous provider result remains * claimed until a separately authorized reconciliation proves it safe. */ export interface DurableSessionStore { create(identity: DurableSessionIdentity): Promise; snapshot(sessionId: string): Promise; enqueueInbox(input: DurableInboxInput): Promise>; claimInbox(sessionId: string): Promise; completeInbox(sessionId: string, idempotencyKey: string): Promise; releaseInbox(sessionId: string, idempotencyKey: string): Promise; enqueueOutbox(input: DurableOutboxInput): Promise>; claimOutbox(sessionId: string): Promise; claimOutboxByKey(sessionId: string, idempotencyKey: string): Promise; completeOutbox(sessionId: string, idempotencyKey: string): Promise; releaseOutbox(sessionId: string, idempotencyKey: string): Promise; checkpoint(input: DurableCheckpointInput): Promise; findCheckpoint(sessionId: string, checkpointId: string): Promise; handoff(input: DurableHandoffInput): Promise; findHandoff(handoffId: string): Promise; requeueInFlight(sessionId: string): Promise; } export class DurableSessionNotFoundError extends Error { constructor(sessionId: string) { super(`Durable Tess session not found: ${sessionId}`); this.name = 'DurableSessionNotFoundError'; } } export class DurableSessionCoordinator { constructor(private readonly store: DurableSessionStore) {} async create(identity: DurableSessionIdentity): Promise { this.assertIdentity(identity); await this.store.create(identity); } async receive(input: DurableInboxInput): Promise> { this.assertRecord(input.sessionId, input.idempotencyKey, input.correlationId, input.content); return this.store.enqueueInbox(input); } async enqueueOutbox( input: DurableOutboxInput, ): Promise> { this.assertRecord( input.sessionId, input.idempotencyKey, input.correlationId, input.channelId, input.content, ); if (input.kind.trim().length === 0) throw new Error('Durable outbox kind is required'); return this.store.enqueueOutbox(input); } async checkpoint(input: DurableCheckpointInput): Promise { this.assertRecord(input.sessionId, input.checkpointId, input.cursor, input.summary); if (!Number.isInteger(input.compactionEpoch) || input.compactionEpoch < 0) { throw new Error('Durable checkpoint compaction epoch must be a non-negative integer'); } await this.store.checkpoint(input); } async handoff(input: DurableHandoffInput): Promise { this.assertRecord(input.sessionId, input.handoffId, input.destination, input.correlationId); if (input.checkpointId.trim().length === 0) throw new Error('Durable handoff checkpoint is required'); await this.store.handoff(input); } /** Read durable state without changing claim status; safe during normal operation. */ async snapshot(sessionId: string): Promise { const snapshot = await this.store.snapshot(sessionId); if (!snapshot) throw new DurableSessionNotFoundError(sessionId); return snapshot; } /** Requeue interrupted inbox work during recovery; preserve ambiguous outbox claims. */ async recover(sessionId: string): Promise { await this.store.requeueInFlight(sessionId); return this.snapshot(sessionId); } async drainInbox( sessionId: string, handler: (entry: DurableInboxEntry) => Promise, ): Promise { for (;;) { const entry = await this.store.claimInbox(sessionId); if (!entry) return; try { await handler(entry); } catch (error: unknown) { await this.store.releaseInbox(sessionId, entry.idempotencyKey); throw error; } // If this write fails after the handler succeeded, leave the record // processing. A recovery path can retry it with its stable idempotency key. await this.store.completeInbox(sessionId, entry.idempotencyKey); } } async dispatchOutbox( sessionId: string, dispatcher: (entry: DurableOutboxEntry) => Promise, ): Promise { for (;;) { const entry = await this.store.claimOutbox(sessionId); if (!entry) return; // A provider failure can be ambiguous: it may occur after the receiver // accepted the idempotency key. Preserve the claim for reconciliation. await dispatcher(entry); // Do not requeue an effect after it has been applied but before its // terminal state could be persisted; recovery preserves the claim. await this.store.completeOutbox(sessionId, entry.idempotencyKey); } } async dispatchOutboxEntry( sessionId: string, idempotencyKey: string, dispatcher: (entry: DurableOutboxEntry) => Promise, ): Promise { const entry = await this.store.claimOutboxByKey(sessionId, idempotencyKey); if (!entry) return; // A provider failure can be ambiguous, so this stays processing until // separately authorized reconciliation proves it safe to resolve. await dispatcher(entry); await this.store.completeOutbox(sessionId, entry.idempotencyKey); } async resumeHandoff(handoffId: string): Promise { const handoff = await this.store.findHandoff(handoffId); if (!handoff) throw new Error(`Durable Tess handoff not found: ${handoffId}`); const snapshot = await this.snapshot(handoff.sessionId); const checkpoint = await this.store.findCheckpoint(handoff.sessionId, handoff.checkpointId); if (!checkpoint) { throw new Error(`Durable Tess handoff checkpoint is unavailable: ${handoff.checkpointId}`); } return { identity: snapshot.identity, checkpoint, handoff }; } private assertIdentity(identity: DurableSessionIdentity): void { this.assertRecord( identity.agentName, identity.sessionId, identity.tenantId, identity.ownerId, identity.providerId, ); if (identity.runtimeSessionId.trim().length === 0) { throw new Error('Durable runtime session identity is required'); } } private assertRecord(...values: string[]): void { if (values.some((value: string): boolean => value.trim().length === 0)) { throw new Error('Durable session records require non-empty fields'); } } } interface InMemorySessionState { identity: DurableSessionIdentity; inbox: Map; outbox: Map; checkpoints: Map; handoffs: Map; } /** Reference store for deterministic domain tests; production uses the gateway DB adapter. */ export class InMemoryDurableSessionStore implements DurableSessionStore { private readonly sessions = new Map(); async create(identity: DurableSessionIdentity): Promise { const existing = this.sessions.get(identity.sessionId); if (existing) { if (!identitiesEqual(existing.identity, identity)) { throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`); } return; } this.sessions.set(identity.sessionId, { identity: copyIdentity(identity), inbox: new Map(), outbox: new Map(), checkpoints: new Map(), handoffs: new Map(), }); } async snapshot(sessionId: string): Promise { const state = this.sessions.get(sessionId); if (!state) return null; const checkpoint = latestCheckpoint(state.checkpoints); return { identity: copyIdentity(state.identity), inbox: [...state.inbox.values()].map(copyInbox), outbox: [...state.outbox.values()].map(copyOutbox), ...(checkpoint ? { checkpoint: copyCheckpoint(checkpoint) } : {}), handoffs: [...state.handoffs.values()].map(copyHandoff), }; } async enqueueInbox(input: DurableInboxInput): Promise> { const state = this.require(input.sessionId); const existing = state.inbox.get(input.idempotencyKey); if (existing) { if (!sameInbox(existing, input)) { throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: existing.status }; } state.inbox.set(input.idempotencyKey, { ...input, status: 'pending' }); return { accepted: true, status: 'pending' }; } async claimInbox(sessionId: string): Promise { const state = this.require(sessionId); const entry = [...state.inbox.values()].find( (candidate: DurableInboxEntry): boolean => candidate.status === 'pending', ); if (!entry) return null; entry.status = 'processing'; return copyInbox(entry); } async completeInbox(sessionId: string, idempotencyKey: string): Promise { this.requireEntry(this.require(sessionId).inbox, idempotencyKey, 'inbox').status = 'processed'; } async releaseInbox(sessionId: string, idempotencyKey: string): Promise { this.requireEntry(this.require(sessionId).inbox, idempotencyKey, 'inbox').status = 'pending'; } async enqueueOutbox( input: DurableOutboxInput, ): Promise> { const state = this.require(input.sessionId); const existing = state.outbox.get(input.idempotencyKey); if (existing) { if (!sameOutbox(existing, input)) { throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: existing.status }; } state.outbox.set(input.idempotencyKey, { ...input, status: 'pending' }); return { accepted: true, status: 'pending' }; } async claimOutbox(sessionId: string): Promise { const state = this.require(sessionId); const entry = [...state.outbox.values()].find( (candidate: DurableOutboxEntry): boolean => candidate.status === 'pending', ); if (!entry) return null; entry.status = 'processing'; return copyOutbox(entry); } async claimOutboxByKey( sessionId: string, idempotencyKey: string, ): Promise { const entry = this.require(sessionId).outbox.get(idempotencyKey); if (!entry || entry.status !== 'pending') return null; entry.status = 'processing'; return copyOutbox(entry); } async completeOutbox(sessionId: string, idempotencyKey: string): Promise { this.requireEntry(this.require(sessionId).outbox, idempotencyKey, 'outbox').status = 'delivered'; } async releaseOutbox(sessionId: string, idempotencyKey: string): Promise { this.requireEntry(this.require(sessionId).outbox, idempotencyKey, 'outbox').status = 'pending'; } async checkpoint(input: DurableCheckpointInput): Promise { const checkpoints = this.require(input.sessionId).checkpoints; const existing = checkpoints.get(input.checkpointId); if (existing && !sameCheckpoint(existing, input)) { throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`); } if (!existing) checkpoints.set(input.checkpointId, { ...input }); } async findCheckpoint(sessionId: string, checkpointId: string): Promise { const checkpoint = this.require(sessionId).checkpoints.get(checkpointId); return checkpoint ? copyCheckpoint(checkpoint) : null; } async handoff(input: DurableHandoffInput): Promise { const state = this.require(input.sessionId); if (!state.checkpoints.has(input.checkpointId)) { throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); } const existing = state.handoffs.get(input.handoffId); if (existing && !sameHandoff(existing, input)) { throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`); } if (!existing) state.handoffs.set(input.handoffId, { ...input }); } async findHandoff(handoffId: string): Promise { for (const state of this.sessions.values()) { const handoff = state.handoffs.get(handoffId); if (handoff) return copyHandoff(handoff); } return null; } async requeueInFlight(sessionId: string): Promise { const state = this.require(sessionId); for (const entry of state.inbox.values()) { if (entry.status === 'processing') entry.status = 'pending'; } } private require(sessionId: string): InMemorySessionState { const state = this.sessions.get(sessionId); if (!state) throw new DurableSessionNotFoundError(sessionId); return state; } private requireEntry( records: Map, idempotencyKey: string, kind: string, ): T { const entry = records.get(idempotencyKey); if (!entry) throw new Error(`Durable Tess ${kind} entry not found: ${idempotencyKey}`); return entry; } } function identitiesEqual(left: DurableSessionIdentity, right: DurableSessionIdentity): boolean { return ( left.agentName === right.agentName && left.sessionId === right.sessionId && left.tenantId === right.tenantId && left.ownerId === right.ownerId && left.providerId === right.providerId && left.runtimeSessionId === right.runtimeSessionId ); } function sameInbox(left: DurableInboxEntry, right: DurableInboxInput): boolean { return ( left.sessionId === right.sessionId && left.idempotencyKey === right.idempotencyKey && left.correlationId === right.correlationId && left.content === right.content ); } function sameOutbox(left: DurableOutboxEntry, right: DurableOutboxInput): boolean { return ( left.sessionId === right.sessionId && left.idempotencyKey === right.idempotencyKey && left.correlationId === right.correlationId && left.channelId === right.channelId && left.kind === right.kind && left.content === right.content ); } function sameCheckpoint(left: DurableCheckpoint, right: DurableCheckpointInput): boolean { return ( left.sessionId === right.sessionId && left.checkpointId === right.checkpointId && left.cursor === right.cursor && left.summary === right.summary && left.compactionEpoch === right.compactionEpoch ); } function latestCheckpoint( checkpoints: Map, ): DurableCheckpoint | undefined { return [...checkpoints.values()].sort( (left: DurableCheckpoint, right: DurableCheckpoint): number => right.compactionEpoch - left.compactionEpoch, )[0]; } function sameHandoff(left: DurableHandoff, right: DurableHandoffInput): boolean { return ( left.sessionId === right.sessionId && left.handoffId === right.handoffId && left.destination === right.destination && left.correlationId === right.correlationId && left.checkpointId === right.checkpointId && left.status === right.status ); } function copyIdentity(identity: DurableSessionIdentity): DurableSessionIdentity { return { ...identity }; } function copyInbox(entry: DurableInboxEntry): DurableInboxEntry { return { ...entry }; } function copyOutbox(entry: DurableOutboxEntry): DurableOutboxEntry { return { ...entry }; } function copyCheckpoint(checkpoint: DurableCheckpoint): DurableCheckpoint { return { ...checkpoint }; } function copyHandoff(handoff: DurableHandoff): DurableHandoff { return { ...handoff }; }