blob: f7ef242473cb27740c043e17111bc9c9f84ce310 [file]
import { TOOL_OUTPUT_DELTA_MAX_CHARS } from '@maka/core/events';
import {
assertExactKeys,
requireCount,
requireEntityId,
requireExactRecord,
requireId,
requireRecord,
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import {
decodeSessionInteractionProjection,
type SessionInteractionProjection,
} from './interaction.js';
import {
decodeSessionMessageQueueProjection,
type SessionMessageQueueProjection,
} from './message.js';
import { defineOperation } from './operation-spec.js';
import { decodeTurnSnapshot, type TurnSnapshot } from './turn.js';
import { decodeGoalProjection, type GoalProjection } from './goal.js';
export const SESSION_CONTINUITY_SCHEMA_VERSION = 3 as const;
export const SESSION_CONTINUITY_SNAPSHOT_MAX_BYTES = 56 * 1024;
export const SESSION_LIVE_DELTA_MAX_BYTES = 16 * 1024;
// Core emits at most 8,192 UTF-16 code units per tool output event. A code unit
// needs at most three UTF-8 bytes (an astral pair needs four bytes total).
export const SESSION_TOOL_OUTPUT_DELTA_MAX_BYTES = 3 * TOOL_OUTPUT_DELTA_MAX_CHARS;
export const SESSION_TOOL_NAME_MAX_BYTES = 256;
export const SESSION_SUBSCRIPTION_FRAME_MAX_BYTES = 64 * 1024 - 1;
export type SessionLifecycleStatus =
| 'active'
| 'running'
| 'waiting_for_user'
| 'blocked'
| 'review'
| 'done'
| 'archived'
| 'aborted';
export interface SessionContinuityIdentity {
sessionId: string;
metadataRevision: number;
status: SessionLifecycleStatus;
createdAt: number;
lastUsedAt: number;
isArchived: boolean;
archivedAt?: number;
}
export interface SessionContinuitySnapshot {
schemaVersion: typeof SESSION_CONTINUITY_SCHEMA_VERSION;
session: SessionContinuityIdentity;
projectionRevision: number;
rootTurn: TurnSnapshot | null;
goal: GoalProjection | null;
queue: SessionMessageQueueProjection;
interactions: SessionInteractionProjection;
}
export interface SubscriptionOpenInput {
sessionId: string;
}
export interface SubscriptionOpenResult {
hostEpoch: string;
subscriptionId: string;
nextSequence: number;
snapshot: SessionContinuitySnapshot;
}
export interface SubscriptionCloseInput {
subscriptionId: string;
}
export interface SubscriptionCloseResult {
subscriptionId: string;
}
interface SubscriptionEnvelope {
hostEpoch: string;
subscriptionId: string;
sequence: number;
}
export interface SessionProjectionFrame extends SubscriptionEnvelope {
kind: 'subscription.session_projection';
snapshot: SessionContinuitySnapshot;
}
export interface SessionAssistantDelta {
kind: 'text' | 'thinking';
turnId: string;
runId: string;
messageId: string;
text: string;
}
export interface SessionDeltaFrame extends SubscriptionEnvelope {
kind: 'subscription.session_delta';
sessionId: string;
delta: SessionAssistantDelta;
}
interface SessionToolEventIdentity {
id: string;
turnId: string;
ts: number;
toolUseId: string;
}
export type SessionToolEvent =
| (SessionToolEventIdentity & {
type: 'tool_start';
toolName: string;
operationId?: string;
activityKind?:
| 'read'
| 'search'
| 'websearch'
| 'webfetch'
| 'edit'
| 'command'
| 'explore'
| 'browser'
| 'tool';
displayName?: string;
stepId?: string;
})
| (SessionToolEventIdentity & {
type: 'tool_output_delta';
seq: number;
stream: 'stdout' | 'stderr';
chunk: string;
redacted: boolean;
createdAt: number;
})
| (SessionToolEventIdentity & {
type: 'tool_progress';
chunk: string;
})
| (SessionToolEventIdentity & {
type: 'tool_result';
operationId?: string;
status: 'completed' | 'errored';
durationMs?: number;
});
export interface SessionEventFrame extends SubscriptionEnvelope {
kind: 'subscription.session_event';
sessionId: string;
runId: string;
event: SessionToolEvent;
}
export interface SubscriptionClosedFrame extends SubscriptionEnvelope {
kind: 'subscription.closed';
reason: 'slow_consumer' | 'session_removed';
}
export type SubscriptionFrame =
| SessionProjectionFrame
| SessionDeltaFrame
| SessionEventFrame
| SubscriptionClosedFrame;
const SUBSCRIPTION_OPEN_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'not_found',
'operation_conflict',
'internal_failure',
] as const;
const SUBSCRIPTION_CLOSE_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'not_found',
'internal_failure',
] as const;
export const SESSION_CONTINUITY_OPERATION_SPECS = {
'subscription.open': defineOperation({
mode: 'control',
availability: 'ready',
errors: SUBSCRIPTION_OPEN_ERRORS,
decodeInput: decodeSubscriptionOpenInput,
decodeOutput: decodeSubscriptionOpenResult,
}),
'subscription.close': defineOperation({
mode: 'control',
availability: 'ready',
errors: SUBSCRIPTION_CLOSE_ERRORS,
decodeInput: decodeSubscriptionCloseInput,
decodeOutput: decodeSubscriptionCloseResult,
}),
} as const;
export function decodeSubscriptionFrame(value: unknown): SubscriptionFrame {
requireEncodedByteLimit(value, 'subscription frame', SESSION_SUBSCRIPTION_FRAME_MAX_BYTES);
const record = requireRecord(value, 'subscription frame');
const envelope = decodeEnvelope(record);
let frame: SubscriptionFrame;
if (record.kind === 'subscription.session_projection') {
assertExactKeys(record, 'Session projection frame', [
'kind',
'hostEpoch',
'subscriptionId',
'sequence',
'snapshot',
]);
const snapshot = decodeSessionContinuitySnapshot(record.snapshot);
assertQueueEpoch(snapshot, envelope.hostEpoch);
frame = { kind: record.kind, ...envelope, snapshot };
} else if (record.kind === 'subscription.session_delta') {
assertExactKeys(record, 'Session delta frame', [
'kind',
'hostEpoch',
'subscriptionId',
'sequence',
'sessionId',
'delta',
]);
frame = {
kind: record.kind,
...envelope,
sessionId: requireEntityId(record.sessionId, 'sessionId'),
delta: decodeAssistantDelta(record.delta),
};
} else if (record.kind === 'subscription.session_event') {
assertExactKeys(record, 'Session event frame', [
'kind',
'hostEpoch',
'subscriptionId',
'sequence',
'sessionId',
'runId',
'event',
]);
frame = {
kind: record.kind,
...envelope,
sessionId: requireEntityId(record.sessionId, 'sessionId'),
runId: requireEntityId(record.runId, 'runId'),
event: decodeSessionToolEvent(record.event),
};
} else if (record.kind === 'subscription.closed') {
assertExactKeys(record, 'subscription closed frame', [
'kind',
'hostEpoch',
'subscriptionId',
'sequence',
'reason',
]);
if (record.reason !== 'slow_consumer' && record.reason !== 'session_removed') {
throw invalidProtocolFrame('Invalid subscription close reason');
}
frame = { kind: record.kind, ...envelope, reason: record.reason };
} else {
throw invalidProtocolFrame('Unknown subscription frame kind');
}
return frame;
}
export function isSubscriptionFrameKind(value: unknown): value is SubscriptionFrame['kind'] {
return (
value === 'subscription.session_projection' ||
value === 'subscription.session_delta' ||
value === 'subscription.session_event' ||
value === 'subscription.closed'
);
}
export function decodeSessionContinuitySnapshot(value: unknown): SessionContinuitySnapshot {
requireEncodedByteLimit(
value,
'Session continuity snapshot',
SESSION_CONTINUITY_SNAPSHOT_MAX_BYTES,
);
const record = requireExactRecord(value, 'Session continuity snapshot', [
'schemaVersion',
'session',
'projectionRevision',
'rootTurn',
'goal',
'queue',
'interactions',
]);
if (record.schemaVersion !== SESSION_CONTINUITY_SCHEMA_VERSION) {
throw invalidProtocolFrame('Unsupported Session continuity snapshot schema');
}
const session = decodeSessionContinuityIdentity(record.session);
const rootTurn = record.rootTurn === null ? null : decodeTurnSnapshot(record.rootTurn);
if (rootTurn !== null && rootTurn.sessionId !== session.sessionId) {
throw invalidProtocolFrame('Session continuity root Turn belongs to a different Session');
}
const interactions = decodeSessionInteractionProjection(record.interactions, session.sessionId);
const goal = record.goal === null ? null : decodeGoalProjection(record.goal);
if (goal !== null && goal.sessionId !== session.sessionId) {
throw invalidProtocolFrame('Session continuity Goal belongs to a different Session');
}
return {
schemaVersion: SESSION_CONTINUITY_SCHEMA_VERSION,
session,
projectionRevision: requirePositiveCount(record.projectionRevision, 'projectionRevision'),
rootTurn,
goal,
queue: decodeSessionMessageQueueProjection(record.queue),
interactions,
};
}
function decodeSubscriptionOpenInput(value: unknown): SubscriptionOpenInput {
const record = requireExactRecord(value, 'subscription.open input', ['sessionId']);
return { sessionId: requireEntityId(record.sessionId, 'sessionId') };
}
function decodeSubscriptionOpenResult(value: unknown): SubscriptionOpenResult {
const record = requireExactRecord(value, 'subscription.open result', [
'hostEpoch',
'subscriptionId',
'nextSequence',
'snapshot',
]);
const hostEpoch = requireId(record.hostEpoch, 'hostEpoch');
const snapshot = decodeSessionContinuitySnapshot(record.snapshot);
assertQueueEpoch(snapshot, hostEpoch);
return {
hostEpoch,
subscriptionId: requireId(record.subscriptionId, 'subscriptionId'),
nextSequence: requirePositiveCount(record.nextSequence, 'nextSequence'),
snapshot,
};
}
function decodeSubscriptionCloseInput(value: unknown): SubscriptionCloseInput {
const record = requireExactRecord(value, 'subscription.close input', ['subscriptionId']);
return { subscriptionId: requireId(record.subscriptionId, 'subscriptionId') };
}
function decodeSubscriptionCloseResult(value: unknown): SubscriptionCloseResult {
const record = requireExactRecord(value, 'subscription.close result', ['subscriptionId']);
return { subscriptionId: requireId(record.subscriptionId, 'subscriptionId') };
}
function decodeEnvelope(record: Record<string, unknown>): SubscriptionEnvelope {
return {
hostEpoch: requireId(record.hostEpoch, 'hostEpoch'),
subscriptionId: requireId(record.subscriptionId, 'subscriptionId'),
sequence: requirePositiveCount(record.sequence, 'sequence'),
};
}
function decodeAssistantDelta(value: unknown): SessionAssistantDelta {
const record = requireExactRecord(value, 'Session assistant delta', [
'kind',
'turnId',
'runId',
'messageId',
'text',
]);
if (record.kind !== 'text' && record.kind !== 'thinking') {
throw invalidProtocolFrame('Invalid Session assistant delta kind');
}
return {
kind: record.kind,
turnId: requireEntityId(record.turnId, 'turnId'),
runId: requireEntityId(record.runId, 'runId'),
messageId: requireEntityId(record.messageId, 'messageId'),
text: requireUtf8BoundedString(
record.text,
'Session assistant delta text',
SESSION_LIVE_DELTA_MAX_BYTES,
),
};
}
function decodeSessionToolEvent(value: unknown): SessionToolEvent {
const record = requireRecord(value, 'Session tool event');
const identity = {
id: requireId(record.id, 'Session tool event id'),
turnId: requireEntityId(record.turnId, 'turnId'),
ts: requireCount(record.ts, 'Session tool event timestamp'),
toolUseId: requireId(record.toolUseId, 'toolUseId'),
};
if (record.type === 'tool_start') {
const allowed = [
'type',
'id',
'turnId',
'ts',
'toolUseId',
'toolName',
'operationId',
'activityKind',
'displayName',
'stepId',
];
assertAllowedKeys(record, 'Session tool start event', allowed);
assertRequiredKeys(record, 'Session tool start event', [
'type',
'id',
'turnId',
'ts',
'toolUseId',
'toolName',
]);
return {
type: record.type,
...identity,
toolName: requireUtf8BoundedString(
record.toolName,
'Session tool name',
SESSION_TOOL_NAME_MAX_BYTES,
),
...(record.operationId === undefined
? {}
: { operationId: requireEntityId(record.operationId, 'operationId') }),
...(record.activityKind === undefined
? {}
: { activityKind: requireToolActivityKind(record.activityKind) }),
...(record.displayName === undefined
? {}
: {
displayName: requireUtf8BoundedString(
record.displayName,
'Session tool display name',
SESSION_TOOL_NAME_MAX_BYTES,
),
}),
...(record.stepId === undefined ? {} : { stepId: requireEntityId(record.stepId, 'stepId') }),
};
}
if (record.type === 'tool_output_delta') {
assertExactKeys(record, 'Session tool output delta event', [
'type',
'id',
'turnId',
'ts',
'toolUseId',
'seq',
'stream',
'chunk',
'redacted',
'createdAt',
]);
if (record.stream !== 'stdout' && record.stream !== 'stderr') {
throw invalidProtocolFrame('Invalid Session tool output stream');
}
if (typeof record.redacted !== 'boolean') {
throw invalidProtocolFrame('Invalid Session tool output redaction');
}
return {
type: record.type,
...identity,
seq: requireCount(record.seq, 'Session tool output sequence'),
stream: record.stream,
chunk: requireUtf8BoundedString(
record.chunk,
'Session tool output chunk',
SESSION_TOOL_OUTPUT_DELTA_MAX_BYTES,
),
redacted: record.redacted,
createdAt: requireCount(record.createdAt, 'Session tool output timestamp'),
};
}
if (record.type === 'tool_progress') {
assertExactKeys(record, 'Session tool progress event', [
'type',
'id',
'turnId',
'ts',
'toolUseId',
'chunk',
]);
return {
type: record.type,
...identity,
chunk: requireUtf8BoundedString(
record.chunk,
'Session tool progress chunk',
SESSION_LIVE_DELTA_MAX_BYTES,
),
};
}
if (record.type === 'tool_result') {
const allowed = [
'type',
'id',
'turnId',
'ts',
'toolUseId',
'operationId',
'status',
'durationMs',
];
assertAllowedKeys(record, 'Session tool result event', allowed);
assertRequiredKeys(record, 'Session tool result event', [
'type',
'id',
'turnId',
'ts',
'toolUseId',
'status',
]);
if (record.status !== 'completed' && record.status !== 'errored') {
throw invalidProtocolFrame('Invalid Session tool result status');
}
return {
type: record.type,
...identity,
...(record.operationId === undefined
? {}
: { operationId: requireEntityId(record.operationId, 'operationId') }),
status: record.status,
...(record.durationMs === undefined
? {}
: {
durationMs: requireCount(record.durationMs, 'Session tool result duration'),
}),
};
}
throw invalidProtocolFrame('Invalid Session tool event type');
}
function decodeSessionContinuityIdentity(value: unknown): SessionContinuityIdentity {
const record = requireRecord(value, 'Session continuity identity');
assertAllowedKeys(record, 'Session continuity identity', [
'sessionId',
'metadataRevision',
'status',
'createdAt',
'lastUsedAt',
'isArchived',
'archivedAt',
]);
assertRequiredKeys(record, 'Session continuity identity', [
'sessionId',
'metadataRevision',
'status',
'createdAt',
'lastUsedAt',
'isArchived',
]);
if (typeof record.isArchived !== 'boolean') {
throw invalidProtocolFrame('Invalid Session archived state');
}
return {
sessionId: requireEntityId(record.sessionId, 'sessionId'),
metadataRevision: requirePositiveCount(record.metadataRevision, 'metadataRevision'),
status: requireSessionLifecycleStatus(record.status),
createdAt: requireCount(record.createdAt, 'createdAt'),
lastUsedAt: requireCount(record.lastUsedAt, 'lastUsedAt'),
isArchived: record.isArchived,
...(record.archivedAt === undefined
? {}
: { archivedAt: requireCount(record.archivedAt, 'archivedAt') }),
};
}
function assertQueueEpoch(snapshot: SessionContinuitySnapshot, hostEpoch: string): void {
if (snapshot.queue.hostEpoch !== hostEpoch) {
throw invalidProtocolFrame('Session queue projection belongs to a different Host Epoch');
}
}
function assertAllowedKeys(
record: Record<string, unknown>,
label: string,
keys: readonly string[],
): void {
const allowed = new Set(keys);
if (Object.keys(record).some((key) => !allowed.has(key))) {
throw invalidProtocolFrame(`Unknown ${label} field`);
}
}
function assertRequiredKeys(
record: Record<string, unknown>,
label: string,
keys: readonly string[],
): void {
if (keys.some((key) => !Object.hasOwn(record, key))) {
throw invalidProtocolFrame(`Invalid ${label} fields`);
}
}
function requirePositiveCount(value: unknown, label: string): number {
const count = requireCount(value, label);
if (count === 0) throw invalidProtocolFrame(`Invalid ${label}`);
return count;
}
function requireUtf8BoundedString(value: unknown, label: string, maxBytes: number): string {
if (
typeof value !== 'string' ||
value.length === 0 ||
Buffer.byteLength(value, 'utf8') > maxBytes
) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value;
}
function requireEncodedByteLimit(value: unknown, label: string, maxBytes: number): void {
let encoded: string | undefined;
try {
encoded = JSON.stringify(value);
} catch {
throw invalidProtocolFrame(`Invalid ${label}`);
}
if (encoded === undefined || Buffer.byteLength(encoded, 'utf8') > maxBytes) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
}
function requireToolActivityKind(
value: unknown,
): Extract<SessionToolEvent, { type: 'tool_start' }>['activityKind'] {
if (
value === 'read' ||
value === 'search' ||
value === 'websearch' ||
value === 'webfetch' ||
value === 'edit' ||
value === 'command' ||
value === 'explore' ||
value === 'browser' ||
value === 'tool'
)
return value;
throw invalidProtocolFrame('Invalid Session tool activity kind');
}
function requireSessionLifecycleStatus(value: unknown): SessionLifecycleStatus {
if (
value === 'active' ||
value === 'running' ||
value === 'waiting_for_user' ||
value === 'blocked' ||
value === 'review' ||
value === 'done' ||
value === 'archived' ||
value === 'aborted'
)
return value;
throw invalidProtocolFrame('Invalid Session lifecycle status');
}