| import { isCollaborationMode, type CollaborationMode } from '@maka/core'; |
| import { isOrchestrationMode, type OrchestrationMode } from '@maka/core'; |
| import { isPermissionMode, type PermissionMode } from '@maka/core'; |
| import { isSessionStartMode, type SessionStartMode } from '@maka/core'; |
| import { |
| isSessionBlockedReason, |
| isSessionStatus, |
| type SessionBlockedReason, |
| type SessionStatus, |
| type SessionSubagentProjection, |
| } from '@maka/core'; |
| import { isThinkingLevel, type ThinkingLevel } from '@maka/core'; |
| import type { ExecutionBoundarySummary } from '@maka/core'; |
| export type { ExecutionBoundarySummary } from '@maka/core'; |
| import { |
| assertAllowedKeys, |
| requireCount, |
| requireEncodedByteLimit, |
| requireEntityId, |
| requireExactRecord, |
| requireRecord, |
| requireShapedRecord, |
| requireUtf8String, |
| } from './codec.js'; |
| import { invalidProtocolFrame } from './errors.js'; |
| import { defineOperation } from './operation-spec.js'; |
| |
| export type { SessionSubagentProjection } from '@maka/core'; |
| |
| export const SESSION_CATALOG_PAGE_MAX_ITEMS = 32; |
| export const SESSION_CATALOG_RESULT_MAX_BYTES = 48 * 1024; |
| export const SESSION_CATALOG_CURSOR_MAX_BYTES = 512; |
| export const SESSION_CATALOG_CWD_MAX_BYTES = 4 * 1024; |
| export const SESSION_CATALOG_NAME_MAX_BYTES = 320; |
| export const SESSION_CATALOG_LABEL_MAX_ITEMS = 32; |
| export const SESSION_CATALOG_LABEL_MAX_BYTES = 128; |
| export const SESSION_CATALOG_PREVIEW_MAX_BYTES = 4 * 1024; |
| export const SESSION_CATALOG_MODEL_MAX_BYTES = 512; |
| export const SESSION_CATALOG_CONNECTION_SLUG_MAX_BYTES = 256; |
| export const SESSION_CATALOG_PROJECT_ID_MAX_BYTES = 256; |
| |
| const QUERY_ERRORS = [ |
| 'host_not_ready', |
| 'host_draining', |
| 'operation_unavailable', |
| 'invalid_request', |
| 'persistence_failed', |
| 'internal_failure', |
| ] as const; |
| const CREATE_ERRORS = [...QUERY_ERRORS, 'operation_conflict', 'commit_outcome_unknown'] as const; |
| const METADATA_UPDATE_ERRORS = [...QUERY_ERRORS, 'not_found', 'commit_outcome_unknown'] as const; |
| const CONFIGURATION_UPDATE_ERRORS = [ |
| ...METADATA_UPDATE_ERRORS, |
| 'session_busy', |
| 'operation_conflict', |
| ] as const; |
| const READ_MARKER_ERRORS = [...METADATA_UPDATE_ERRORS, 'operation_conflict'] as const; |
| const EXECUTION_BOUNDARY_QUERY_ERRORS = [...QUERY_ERRORS, 'not_found'] as const; |
| |
| const PROJECTION_REQUIRED_FIELDS = [ |
| 'id', |
| 'revision', |
| 'cwd', |
| 'createdAt', |
| 'lastUsedAt', |
| 'name', |
| 'isFlagged', |
| 'isArchived', |
| 'labels', |
| 'labelsTruncated', |
| 'hasUnread', |
| 'status', |
| 'backend', |
| 'llmConnectionSlug', |
| 'connectionLocked', |
| 'model', |
| 'permissionMode', |
| 'collaborationMode', |
| 'orchestrationMode', |
| ] as const; |
| const PROJECTION_FIELDS = [ |
| ...PROJECTION_REQUIRED_FIELDS, |
| 'projectId', |
| 'lastMessageAt', |
| 'lastMessagePreview', |
| 'blockedReason', |
| 'statusUpdatedAt', |
| 'parentSessionId', |
| 'branchOfTurnId', |
| 'subagent', |
| 'revisionRootSessionId', |
| 'revisionParentSessionId', |
| 'revisionOfTurnId', |
| 'revisionIndex', |
| 'revisionState', |
| 'thinkingLevel', |
| 'lastReadMessageId', |
| ] as const; |
| |
| export type SessionCatalogRevision = `sha256:${string}`; |
| |
| export interface SessionCatalogFilter { |
| readonly isArchived?: boolean; |
| readonly isFlagged?: boolean; |
| readonly labelSlug?: string; |
| } |
| |
| export type SessionCatalogQueryInput = |
| | { readonly kind: 'list_start'; readonly filter?: SessionCatalogFilter } |
| | { |
| readonly kind: 'list_continue'; |
| readonly filter?: SessionCatalogFilter; |
| readonly revision: SessionCatalogRevision; |
| readonly cursor: string; |
| } |
| | { readonly kind: 'get'; readonly sessionId: string }; |
| |
| export type SessionModelTarget = |
| | { readonly kind: 'default' } |
| | { |
| readonly kind: 'explicit'; |
| readonly connectionSlug: string; |
| readonly model: string; |
| }; |
| |
| export interface SessionCreateInput { |
| readonly sessionId: string; |
| readonly cwd: string; |
| readonly mode?: SessionStartMode; |
| readonly projectId?: string | null; |
| readonly name?: string; |
| readonly labels?: readonly string[]; |
| readonly modelTarget: SessionModelTarget; |
| readonly thinkingLevel?: ThinkingLevel; |
| readonly permissionMode?: PermissionMode; |
| readonly collaborationMode?: CollaborationMode; |
| readonly orchestrationMode?: OrchestrationMode; |
| } |
| |
| export interface SessionMetadataPatch { |
| readonly name?: string; |
| readonly labels?: readonly string[]; |
| readonly isFlagged?: boolean; |
| readonly projectId?: string | null; |
| } |
| |
| export interface SessionMetadataUpdateInput { |
| readonly sessionId: string; |
| readonly expectedRevision: number; |
| readonly patch: SessionMetadataPatch; |
| } |
| |
| export interface SessionConfiguration { |
| readonly modelTarget: SessionModelTarget; |
| readonly thinkingLevel: ThinkingLevel | null; |
| readonly permissionMode: PermissionMode; |
| readonly collaborationMode: CollaborationMode; |
| readonly orchestrationMode: OrchestrationMode; |
| } |
| |
| export interface SessionConfigurationUpdateInput { |
| readonly sessionId: string; |
| readonly expectedRevision: number; |
| readonly configuration: SessionConfiguration; |
| } |
| |
| export interface SessionCwdRelocateInput { |
| readonly sessionId: string; |
| readonly expectedRevision: number; |
| readonly cwd: string; |
| readonly projectId?: string | null; |
| } |
| |
| export interface SessionReadMarkerSetInput { |
| readonly sessionId: string; |
| readonly readThroughMessageId: string; |
| } |
| |
| export interface SessionExecutionBoundaryQueryInput { |
| readonly sessionId: string; |
| } |
| |
| export interface SessionCatalogProjection { |
| readonly id: string; |
| readonly revision: number; |
| readonly cwd: string; |
| readonly projectId?: string | null; |
| readonly createdAt: number; |
| readonly lastUsedAt: number; |
| readonly name: string; |
| readonly isFlagged: boolean; |
| readonly isArchived: boolean; |
| readonly labels: readonly string[]; |
| readonly labelsTruncated: boolean; |
| readonly hasUnread: boolean; |
| readonly lastReadMessageId?: string; |
| readonly lastMessageAt?: number; |
| readonly lastMessagePreview?: string; |
| readonly status: SessionStatus; |
| readonly blockedReason?: SessionBlockedReason; |
| readonly statusUpdatedAt?: number; |
| readonly parentSessionId?: string; |
| readonly branchOfTurnId?: string; |
| readonly subagent?: SessionSubagentProjection; |
| readonly revisionRootSessionId?: string; |
| readonly revisionParentSessionId?: string; |
| readonly revisionOfTurnId?: string; |
| readonly revisionIndex?: number; |
| readonly revisionState?: 'preparing' | 'committed'; |
| readonly backend: 'ai-sdk' | 'fake' | 'pi-agent'; |
| readonly llmConnectionSlug: string; |
| readonly connectionLocked: boolean; |
| readonly model: string; |
| readonly thinkingLevel?: ThinkingLevel; |
| readonly permissionMode: PermissionMode; |
| readonly collaborationMode: CollaborationMode; |
| readonly orchestrationMode: OrchestrationMode; |
| } |
| |
| export interface UnsupportedLegacySessionCatalogRecord { |
| readonly kind: 'unsupported_legacy_record'; |
| readonly id: string; |
| readonly revision: number; |
| readonly reason: 'not_wire_representable'; |
| } |
| |
| export type SessionCatalogItem = SessionCatalogProjection | UnsupportedLegacySessionCatalogRecord; |
| |
| export type SessionCatalogQueryResult = |
| | { |
| readonly kind: 'page'; |
| readonly revision: SessionCatalogRevision; |
| readonly sessions: readonly SessionCatalogItem[]; |
| readonly nextCursor: string | null; |
| } |
| | { |
| readonly kind: 'revision_changed'; |
| readonly expectedRevision: SessionCatalogRevision; |
| readonly actualRevision: SessionCatalogRevision; |
| } |
| | { |
| readonly kind: 'session'; |
| readonly session: SessionCatalogItem | null; |
| }; |
| |
| export type SessionUpdateResult = |
| | { readonly kind: 'committed'; readonly session: SessionCatalogItem } |
| | { |
| readonly kind: 'revision_conflict'; |
| readonly expectedRevision: number; |
| readonly actualRevision: number; |
| }; |
| |
| export const SESSION_CATALOG_OPERATION_SPECS = { |
| 'session.catalog.query': defineOperation< |
| SessionCatalogQueryInput, |
| SessionCatalogQueryResult, |
| (typeof QUERY_ERRORS)[number] |
| >({ |
| mode: 'query', |
| availability: 'ready', |
| errors: QUERY_ERRORS, |
| decodeInput: decodeSessionCatalogQueryInput, |
| decodeOutput: decodeSessionCatalogQueryResult, |
| }), |
| 'session.create': defineOperation< |
| SessionCreateInput, |
| SessionCatalogItem, |
| (typeof CREATE_ERRORS)[number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: CREATE_ERRORS, |
| decodeInput: decodeSessionCreateInput, |
| decodeOutput: decodeSessionCatalogItem, |
| assertOutputForInput: (input, output) => assertSessionIdentity(input.sessionId, output), |
| }), |
| 'session.metadata.update': defineOperation< |
| SessionMetadataUpdateInput, |
| SessionUpdateResult, |
| (typeof METADATA_UPDATE_ERRORS)[number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: METADATA_UPDATE_ERRORS, |
| decodeInput: decodeSessionMetadataUpdateInput, |
| decodeOutput: decodeSessionUpdateResult, |
| assertOutputForInput: assertUpdateOutputIdentity, |
| }), |
| 'session.configuration.update': defineOperation< |
| SessionConfigurationUpdateInput, |
| SessionUpdateResult, |
| (typeof CONFIGURATION_UPDATE_ERRORS)[number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: CONFIGURATION_UPDATE_ERRORS, |
| decodeInput: decodeSessionConfigurationUpdateInput, |
| decodeOutput: decodeSessionUpdateResult, |
| assertOutputForInput: assertUpdateOutputIdentity, |
| }), |
| 'session.cwd.relocate': defineOperation< |
| SessionCwdRelocateInput, |
| SessionUpdateResult, |
| (typeof CONFIGURATION_UPDATE_ERRORS)[number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: CONFIGURATION_UPDATE_ERRORS, |
| decodeInput: decodeSessionCwdRelocateInput, |
| decodeOutput: decodeSessionUpdateResult, |
| assertOutputForInput: assertUpdateOutputIdentity, |
| }), |
| 'session.read_marker.set': defineOperation< |
| SessionReadMarkerSetInput, |
| SessionCatalogItem, |
| (typeof READ_MARKER_ERRORS)[number] |
| >({ |
| mode: 'command', |
| availability: 'ready', |
| errors: READ_MARKER_ERRORS, |
| decodeInput: decodeSessionReadMarkerSetInput, |
| decodeOutput: decodeSessionCatalogItem, |
| assertOutputForInput: (input, output) => assertSessionIdentity(input.sessionId, output), |
| }), |
| 'session.execution_boundary.query': defineOperation< |
| SessionExecutionBoundaryQueryInput, |
| ExecutionBoundarySummary, |
| (typeof EXECUTION_BOUNDARY_QUERY_ERRORS)[number] |
| >({ |
| mode: 'query', |
| availability: 'ready', |
| errors: EXECUTION_BOUNDARY_QUERY_ERRORS, |
| decodeInput: decodeSessionExecutionBoundaryQueryInput, |
| decodeOutput: decodeExecutionBoundarySummary, |
| }), |
| } as const; |
| |
| export function decodeSessionExecutionBoundaryQueryInput( |
| value: unknown, |
| ): SessionExecutionBoundaryQueryInput { |
| const input = requireExactRecord(value, 'Session execution boundary query input', ['sessionId']); |
| return { sessionId: requireEntityId(input.sessionId, 'sessionId') }; |
| } |
| |
| export function decodeExecutionBoundarySummary(value: unknown): ExecutionBoundarySummary { |
| const boundary = requireRecord(value, 'Session execution boundary summary'); |
| if (boundary.kind === 'managed') { |
| const exact = requireExactRecord(boundary, 'Managed execution boundary summary', [ |
| 'kind', |
| 'access', |
| 'revision', |
| ]); |
| if (exact.access !== 'read_only' && exact.access !== 'writable') { |
| throw invalidProtocolFrame('Invalid managed execution boundary access'); |
| } |
| return { |
| kind: 'managed', |
| access: exact.access, |
| revision: requireCount(exact.revision, 'Execution boundary revision'), |
| }; |
| } |
| if (boundary.kind === 'bypass' || boundary.kind === 'external') { |
| const exact = requireExactRecord(boundary, 'Execution boundary summary', ['kind', 'revision']); |
| return { |
| kind: boundary.kind, |
| revision: requireCount(exact.revision, 'Execution boundary revision'), |
| }; |
| } |
| throw invalidProtocolFrame('Invalid execution boundary summary'); |
| } |
| |
| export function decodeSessionCatalogQueryInput(value: unknown): SessionCatalogQueryInput { |
| const input = requireRecord(value, 'Session catalog query input'); |
| if (input.kind === 'list_start') { |
| const exact = requireShapedRecord( |
| input, |
| 'Session catalog list start input', |
| ['kind'], |
| ['filter'], |
| ); |
| return { |
| kind: 'list_start', |
| ...(Object.hasOwn(exact, 'filter') ? { filter: decodeFilter(exact.filter) } : {}), |
| }; |
| } |
| if (input.kind === 'list_continue') { |
| const exact = requireShapedRecord( |
| input, |
| 'Session catalog list continuation input', |
| ['kind', 'revision', 'cursor'], |
| ['filter'], |
| ); |
| return { |
| kind: 'list_continue', |
| ...(Object.hasOwn(exact, 'filter') ? { filter: decodeFilter(exact.filter) } : {}), |
| revision: catalogRevision(exact.revision), |
| cursor: requireUtf8String( |
| exact.cursor, |
| 'Session catalog cursor', |
| SESSION_CATALOG_CURSOR_MAX_BYTES, |
| ), |
| }; |
| } |
| if (input.kind === 'get') { |
| const exact = requireExactRecord(input, 'Session catalog get input', ['kind', 'sessionId']); |
| return { kind: 'get', sessionId: requireEntityId(exact.sessionId, 'sessionId') }; |
| } |
| throw invalidProtocolFrame('Invalid Session catalog query kind'); |
| } |
| |
| export function decodeSessionCreateInput(value: unknown): SessionCreateInput { |
| const input = requireShapedRecord( |
| value, |
| 'Session create input', |
| ['sessionId', 'cwd', 'modelTarget'], |
| [ |
| 'mode', |
| 'projectId', |
| 'name', |
| 'labels', |
| 'thinkingLevel', |
| 'permissionMode', |
| 'collaborationMode', |
| 'orchestrationMode', |
| ], |
| ); |
| return { |
| sessionId: requireEntityId(input.sessionId, 'sessionId'), |
| cwd: requireUtf8String(input.cwd, 'Session cwd', SESSION_CATALOG_CWD_MAX_BYTES), |
| ...(Object.hasOwn(input, 'mode') ? { mode: sessionStartMode(input.mode) } : {}), |
| ...(Object.hasOwn(input, 'projectId') |
| ? { |
| projectId: |
| input.projectId === null |
| ? null |
| : boundedText( |
| input.projectId, |
| 'Session project id', |
| SESSION_CATALOG_PROJECT_ID_MAX_BYTES, |
| ), |
| } |
| : {}), |
| ...(Object.hasOwn(input, 'name') ? { name: sessionName(input.name) } : {}), |
| ...(Object.hasOwn(input, 'labels') ? { labels: labels(input.labels) } : {}), |
| modelTarget: modelTarget(input.modelTarget), |
| ...(Object.hasOwn(input, 'thinkingLevel') |
| ? { thinkingLevel: thinkingLevel(input.thinkingLevel) } |
| : {}), |
| ...(Object.hasOwn(input, 'permissionMode') |
| ? { permissionMode: permissionMode(input.permissionMode) } |
| : {}), |
| ...(Object.hasOwn(input, 'collaborationMode') |
| ? { collaborationMode: collaborationMode(input.collaborationMode) } |
| : {}), |
| ...(Object.hasOwn(input, 'orchestrationMode') |
| ? { orchestrationMode: orchestrationMode(input.orchestrationMode) } |
| : {}), |
| }; |
| } |
| |
| function sessionStartMode(value: unknown): SessionStartMode { |
| if (!isSessionStartMode(value)) throw invalidProtocolFrame('Invalid Session start mode'); |
| return value; |
| } |
| |
| export function decodeSessionMetadataUpdateInput(value: unknown): SessionMetadataUpdateInput { |
| const input = requireExactRecord(value, 'Session metadata update input', [ |
| 'sessionId', |
| 'expectedRevision', |
| 'patch', |
| ]); |
| const patch = requireShapedRecord( |
| input.patch, |
| 'Session metadata patch', |
| [], |
| ['name', 'labels', 'isFlagged', 'projectId'], |
| ); |
| if (Object.keys(patch).length === 0) { |
| throw invalidProtocolFrame('Session metadata patch is empty'); |
| } |
| return { |
| sessionId: requireEntityId(input.sessionId, 'sessionId'), |
| expectedRevision: positiveRevision(input.expectedRevision, 'expected Session revision'), |
| patch: { |
| ...(Object.hasOwn(patch, 'name') ? { name: sessionName(patch.name) } : {}), |
| ...(Object.hasOwn(patch, 'labels') ? { labels: labels(patch.labels) } : {}), |
| ...(Object.hasOwn(patch, 'isFlagged') |
| ? { isFlagged: boolean(patch.isFlagged, 'Session flagged state') } |
| : {}), |
| ...(Object.hasOwn(patch, 'projectId') |
| ? { |
| projectId: |
| patch.projectId === null |
| ? null |
| : boundedText( |
| patch.projectId, |
| 'Session project id', |
| SESSION_CATALOG_PROJECT_ID_MAX_BYTES, |
| ), |
| } |
| : {}), |
| }, |
| }; |
| } |
| |
| export function decodeSessionConfigurationUpdateInput( |
| value: unknown, |
| ): SessionConfigurationUpdateInput { |
| const input = requireExactRecord(value, 'Session configuration update input', [ |
| 'sessionId', |
| 'expectedRevision', |
| 'configuration', |
| ]); |
| const configuration = requireExactRecord(input.configuration, 'Session configuration', [ |
| 'modelTarget', |
| 'thinkingLevel', |
| 'permissionMode', |
| 'collaborationMode', |
| 'orchestrationMode', |
| ]); |
| return { |
| sessionId: requireEntityId(input.sessionId, 'sessionId'), |
| expectedRevision: positiveRevision(input.expectedRevision, 'expected Session revision'), |
| configuration: { |
| modelTarget: modelTarget(configuration.modelTarget), |
| thinkingLevel: |
| configuration.thinkingLevel === null ? null : thinkingLevel(configuration.thinkingLevel), |
| permissionMode: permissionMode(configuration.permissionMode), |
| collaborationMode: collaborationMode(configuration.collaborationMode), |
| orchestrationMode: orchestrationMode(configuration.orchestrationMode), |
| }, |
| }; |
| } |
| |
| export function decodeSessionCwdRelocateInput(value: unknown): SessionCwdRelocateInput { |
| const input = requireShapedRecord( |
| value, |
| 'Session cwd relocate input', |
| ['sessionId', 'expectedRevision', 'cwd'], |
| ['projectId'], |
| ); |
| return { |
| sessionId: requireEntityId(input.sessionId, 'sessionId'), |
| expectedRevision: positiveRevision(input.expectedRevision, 'expected Session revision'), |
| cwd: requireUtf8String(input.cwd, 'Session cwd', SESSION_CATALOG_CWD_MAX_BYTES), |
| ...(Object.hasOwn(input, 'projectId') |
| ? { |
| projectId: |
| input.projectId === null |
| ? null |
| : boundedText( |
| input.projectId, |
| 'Session project id', |
| SESSION_CATALOG_PROJECT_ID_MAX_BYTES, |
| ), |
| } |
| : {}), |
| }; |
| } |
| |
| export function decodeSessionReadMarkerSetInput(value: unknown): SessionReadMarkerSetInput { |
| const input = requireExactRecord(value, 'Session read marker input', [ |
| 'sessionId', |
| 'readThroughMessageId', |
| ]); |
| return { |
| sessionId: requireEntityId(input.sessionId, 'sessionId'), |
| readThroughMessageId: requireEntityId(input.readThroughMessageId, 'readThroughMessageId'), |
| }; |
| } |
| |
| export function decodeSessionCatalogQueryResult(value: unknown): SessionCatalogQueryResult { |
| const result = requireRecord(value, 'Session catalog query result'); |
| if (result.kind === 'revision_changed') { |
| const exact = requireExactRecord(result, 'Session catalog revision changed result', [ |
| 'kind', |
| 'expectedRevision', |
| 'actualRevision', |
| ]); |
| return { |
| kind: 'revision_changed', |
| expectedRevision: catalogRevision(exact.expectedRevision), |
| actualRevision: catalogRevision(exact.actualRevision), |
| }; |
| } |
| if (result.kind === 'session') { |
| const exact = requireExactRecord(result, 'Session catalog item result', ['kind', 'session']); |
| return { |
| kind: 'session', |
| session: exact.session === null ? null : decodeSessionCatalogItem(exact.session), |
| }; |
| } |
| if (result.kind !== 'page') throw invalidProtocolFrame('Invalid Session catalog result kind'); |
| const page = requireExactRecord(result, 'Session catalog page result', [ |
| 'kind', |
| 'revision', |
| 'sessions', |
| 'nextCursor', |
| ]); |
| if (!Array.isArray(page.sessions) || page.sessions.length > SESSION_CATALOG_PAGE_MAX_ITEMS) { |
| throw invalidProtocolFrame('Session catalog page exceeds item limit'); |
| } |
| const decoded: SessionCatalogQueryResult = { |
| kind: 'page', |
| revision: catalogRevision(page.revision), |
| sessions: page.sessions.map(decodeSessionCatalogItem), |
| nextCursor: |
| page.nextCursor === null |
| ? null |
| : requireUtf8String( |
| page.nextCursor, |
| 'Session catalog next cursor', |
| SESSION_CATALOG_CURSOR_MAX_BYTES, |
| ), |
| }; |
| requireEncodedByteLimit(decoded, 'Session catalog page', SESSION_CATALOG_RESULT_MAX_BYTES); |
| return decoded; |
| } |
| |
| export function decodeSessionUpdateResult(value: unknown): SessionUpdateResult { |
| const result = requireRecord(value, 'Session update result'); |
| if (result.kind === 'committed') { |
| const exact = requireExactRecord(result, 'Session committed update result', [ |
| 'kind', |
| 'session', |
| ]); |
| return { kind: 'committed', session: decodeSessionCatalogItem(exact.session) }; |
| } |
| if (result.kind === 'revision_conflict') { |
| const exact = requireExactRecord(result, 'Session revision conflict result', [ |
| 'kind', |
| 'expectedRevision', |
| 'actualRevision', |
| ]); |
| return { |
| kind: 'revision_conflict', |
| expectedRevision: positiveRevision(exact.expectedRevision, 'expected Session revision'), |
| actualRevision: positiveRevision(exact.actualRevision, 'actual Session revision'), |
| }; |
| } |
| throw invalidProtocolFrame('Invalid Session update result kind'); |
| } |
| |
| export function decodeSessionCatalogProjection(value: unknown): SessionCatalogProjection { |
| const record = requireRecord(value, 'Session catalog projection'); |
| assertAllowedKeys(record, 'Session catalog projection', PROJECTION_FIELDS); |
| if (PROJECTION_REQUIRED_FIELDS.some((field) => !Object.hasOwn(record, field))) { |
| throw invalidProtocolFrame('Invalid Session catalog projection fields'); |
| } |
| const projection: SessionCatalogProjection = { |
| id: requireEntityId(record.id, 'Session id'), |
| revision: positiveRevision(record.revision, 'Session revision'), |
| cwd: requireUtf8String(record.cwd, 'Session cwd', SESSION_CATALOG_CWD_MAX_BYTES), |
| ...optionalProjectId(record), |
| createdAt: timestamp(record.createdAt, 'Session createdAt'), |
| lastUsedAt: timestamp(record.lastUsedAt, 'Session lastUsedAt'), |
| name: sessionName(record.name), |
| isFlagged: boolean(record.isFlagged, 'Session flagged state'), |
| isArchived: boolean(record.isArchived, 'Session archived state'), |
| labels: labels(record.labels), |
| labelsTruncated: boolean(record.labelsTruncated, 'Session labels truncated state'), |
| hasUnread: boolean(record.hasUnread, 'Session unread state'), |
| ...optionalEntityId(record, 'lastReadMessageId'), |
| ...optionalTimestamp(record, 'lastMessageAt'), |
| ...optionalText(record, 'lastMessagePreview', SESSION_CATALOG_PREVIEW_MAX_BYTES), |
| status: sessionStatus(record.status), |
| ...optionalBlockedReason(record), |
| ...optionalTimestamp(record, 'statusUpdatedAt'), |
| ...optionalEntityId(record, 'parentSessionId'), |
| ...optionalEntityId(record, 'branchOfTurnId'), |
| ...optionalSubagent(record), |
| ...optionalEntityId(record, 'revisionRootSessionId'), |
| ...optionalEntityId(record, 'revisionParentSessionId'), |
| ...optionalEntityId(record, 'revisionOfTurnId'), |
| ...optionalRevisionIndex(record), |
| ...optionalRevisionState(record), |
| backend: backend(record.backend), |
| llmConnectionSlug: requireUtf8String( |
| record.llmConnectionSlug, |
| 'Session connection slug', |
| SESSION_CATALOG_CONNECTION_SLUG_MAX_BYTES, |
| ), |
| connectionLocked: boolean(record.connectionLocked, 'Session connection lock'), |
| model: requireUtf8String(record.model, 'Session model', SESSION_CATALOG_MODEL_MAX_BYTES), |
| ...optionalThinkingLevel(record), |
| permissionMode: permissionMode(record.permissionMode), |
| collaborationMode: collaborationMode(record.collaborationMode), |
| orchestrationMode: orchestrationMode(record.orchestrationMode), |
| }; |
| requireEncodedByteLimit( |
| projection, |
| 'Session catalog projection', |
| SESSION_CATALOG_RESULT_MAX_BYTES, |
| ); |
| return projection; |
| } |
| |
| export function decodeSessionCatalogItem(value: unknown): SessionCatalogItem { |
| const record = requireRecord(value, 'Session catalog item'); |
| if (record.kind !== 'unsupported_legacy_record') { |
| return decodeSessionCatalogProjection(record); |
| } |
| const exact = requireExactRecord(record, 'unsupported legacy Session catalog record', [ |
| 'kind', |
| 'id', |
| 'revision', |
| 'reason', |
| ]); |
| if (exact.reason !== 'not_wire_representable') { |
| throw invalidProtocolFrame('Invalid unsupported legacy Session catalog reason'); |
| } |
| return { |
| kind: 'unsupported_legacy_record', |
| id: requireEntityId(exact.id, 'Session id'), |
| revision: positiveRevision(exact.revision, 'Session revision'), |
| reason: exact.reason, |
| }; |
| } |
| |
| function decodeFilter(value: unknown): SessionCatalogFilter { |
| const filter = requireShapedRecord( |
| value, |
| 'Session catalog filter', |
| [], |
| ['isArchived', 'isFlagged', 'labelSlug'], |
| ); |
| return { |
| ...(Object.hasOwn(filter, 'isArchived') |
| ? { isArchived: boolean(filter.isArchived, 'Session archived filter') } |
| : {}), |
| ...(Object.hasOwn(filter, 'isFlagged') |
| ? { isFlagged: boolean(filter.isFlagged, 'Session flagged filter') } |
| : {}), |
| ...(Object.hasOwn(filter, 'labelSlug') |
| ? { |
| labelSlug: boundedText( |
| filter.labelSlug, |
| 'Session label filter', |
| SESSION_CATALOG_LABEL_MAX_BYTES, |
| ), |
| } |
| : {}), |
| }; |
| } |
| |
| function modelTarget(value: unknown): SessionModelTarget { |
| const target = requireRecord(value, 'Session model target'); |
| if (target.kind === 'default') { |
| requireExactRecord(target, 'default Session model target', ['kind']); |
| return { kind: 'default' }; |
| } |
| if (target.kind === 'explicit') { |
| const exact = requireExactRecord(target, 'explicit Session model target', [ |
| 'kind', |
| 'connectionSlug', |
| 'model', |
| ]); |
| return { |
| kind: 'explicit', |
| connectionSlug: requireUtf8String( |
| exact.connectionSlug, |
| 'Session connection slug', |
| SESSION_CATALOG_CONNECTION_SLUG_MAX_BYTES, |
| ), |
| model: requireUtf8String(exact.model, 'Session model', SESSION_CATALOG_MODEL_MAX_BYTES), |
| }; |
| } |
| throw invalidProtocolFrame('Invalid Session model target'); |
| } |
| |
| function labels(value: unknown): readonly string[] { |
| if (!Array.isArray(value) || value.length > SESSION_CATALOG_LABEL_MAX_ITEMS) { |
| throw invalidProtocolFrame('Invalid Session labels'); |
| } |
| const decoded = value.map((label) => |
| boundedText(label, 'Session label', SESSION_CATALOG_LABEL_MAX_BYTES), |
| ); |
| if (new Set(decoded).size !== decoded.length) { |
| throw invalidProtocolFrame('Duplicate Session label'); |
| } |
| return decoded; |
| } |
| |
| function optionalProjectId( |
| record: Record<string, unknown>, |
| ): Pick<SessionCatalogProjection, 'projectId'> | Record<string, never> { |
| if (!Object.hasOwn(record, 'projectId')) return {}; |
| return { |
| projectId: |
| record.projectId === null |
| ? null |
| : boundedText(record.projectId, 'Session project id', SESSION_CATALOG_PROJECT_ID_MAX_BYTES), |
| }; |
| } |
| |
| function optionalTimestamp<Field extends 'lastMessageAt' | 'statusUpdatedAt'>( |
| record: Record<string, unknown>, |
| field: Field, |
| ): Pick<SessionCatalogProjection, Field> | Record<string, never> { |
| return Object.hasOwn(record, field) |
| ? ({ [field]: timestamp(record[field], `Session ${field}`) } as Pick< |
| SessionCatalogProjection, |
| Field |
| >) |
| : {}; |
| } |
| |
| function optionalText<Field extends 'lastMessagePreview'>( |
| record: Record<string, unknown>, |
| field: Field, |
| maxBytes: number, |
| ): Pick<SessionCatalogProjection, Field> | Record<string, never> { |
| return Object.hasOwn(record, field) |
| ? ({ [field]: boundedText(record[field], `Session ${field}`, maxBytes) } as Pick< |
| SessionCatalogProjection, |
| Field |
| >) |
| : {}; |
| } |
| |
| function optionalEntityId< |
| Field extends |
| | 'parentSessionId' |
| | 'branchOfTurnId' |
| | 'lastReadMessageId' |
| | 'revisionRootSessionId' |
| | 'revisionParentSessionId' |
| | 'revisionOfTurnId', |
| >( |
| record: Record<string, unknown>, |
| field: Field, |
| ): Pick<SessionCatalogProjection, Field> | Record<string, never> { |
| return Object.hasOwn(record, field) |
| ? ({ [field]: requireEntityId(record[field], `Session ${field}`) } as Pick< |
| SessionCatalogProjection, |
| Field |
| >) |
| : {}; |
| } |
| |
| function optionalBlockedReason( |
| record: Record<string, unknown>, |
| ): Pick<SessionCatalogProjection, 'blockedReason'> | Record<string, never> { |
| if (!Object.hasOwn(record, 'blockedReason')) return {}; |
| if (!isSessionBlockedReason(record.blockedReason)) { |
| throw invalidProtocolFrame('Invalid Session blocked reason'); |
| } |
| return { blockedReason: record.blockedReason }; |
| } |
| |
| function optionalSubagent( |
| record: Record<string, unknown>, |
| ): Pick<SessionCatalogProjection, 'subagent'> | Record<string, never> { |
| if (!Object.hasOwn(record, 'subagent')) return {}; |
| const subagent = requireShapedRecord( |
| record.subagent, |
| 'Session subagent projection', |
| ['parentSessionId'], |
| ['agentId', 'agentName', 'profile'], |
| ); |
| return { |
| subagent: { |
| parentSessionId: requireEntityId(subagent.parentSessionId, 'subagent parentSessionId'), |
| ...(Object.hasOwn(subagent, 'agentId') |
| ? { agentId: boundedText(subagent.agentId, 'subagent agentId', 512) } |
| : {}), |
| ...(Object.hasOwn(subagent, 'agentName') |
| ? { agentName: boundedText(subagent.agentName, 'subagent agentName', 512) } |
| : {}), |
| ...(Object.hasOwn(subagent, 'profile') |
| ? { profile: boundedText(subagent.profile, 'subagent profile', 512) } |
| : {}), |
| }, |
| }; |
| } |
| |
| function optionalRevisionIndex( |
| record: Record<string, unknown>, |
| ): Pick<SessionCatalogProjection, 'revisionIndex'> | Record<string, never> { |
| if (!Object.hasOwn(record, 'revisionIndex')) return {}; |
| return { revisionIndex: positiveRevision(record.revisionIndex, 'Session revision index') }; |
| } |
| |
| function optionalRevisionState( |
| record: Record<string, unknown>, |
| ): Pick<SessionCatalogProjection, 'revisionState'> | Record<string, never> { |
| if (!Object.hasOwn(record, 'revisionState')) return {}; |
| if (record.revisionState !== 'preparing' && record.revisionState !== 'committed') { |
| throw invalidProtocolFrame('Invalid Session revision state'); |
| } |
| return { revisionState: record.revisionState }; |
| } |
| |
| function optionalThinkingLevel( |
| record: Record<string, unknown>, |
| ): Pick<SessionCatalogProjection, 'thinkingLevel'> | Record<string, never> { |
| if (!Object.hasOwn(record, 'thinkingLevel')) return {}; |
| return { thinkingLevel: thinkingLevel(record.thinkingLevel) }; |
| } |
| |
| function sessionStatus(value: unknown): SessionStatus { |
| if (!isSessionStatus(value)) throw invalidProtocolFrame('Invalid Session status'); |
| return value; |
| } |
| |
| function backend(value: unknown): SessionCatalogProjection['backend'] { |
| if (value !== 'ai-sdk' && value !== 'fake' && value !== 'pi-agent') { |
| throw invalidProtocolFrame('Invalid Session backend'); |
| } |
| return value; |
| } |
| |
| function thinkingLevel(value: unknown): ThinkingLevel { |
| if (!isThinkingLevel(value)) throw invalidProtocolFrame('Invalid Session thinking level'); |
| return value; |
| } |
| |
| function permissionMode(value: unknown): PermissionMode { |
| if (!isPermissionMode(value)) throw invalidProtocolFrame('Invalid Session permission mode'); |
| return value; |
| } |
| |
| function collaborationMode(value: unknown): CollaborationMode { |
| if (!isCollaborationMode(value)) { |
| throw invalidProtocolFrame('Invalid Session collaboration mode'); |
| } |
| return value; |
| } |
| |
| function orchestrationMode(value: unknown): OrchestrationMode { |
| if (!isOrchestrationMode(value)) { |
| throw invalidProtocolFrame('Invalid Session orchestration mode'); |
| } |
| return value; |
| } |
| |
| function sessionName(value: unknown): string { |
| return boundedText(value, 'Session name', SESSION_CATALOG_NAME_MAX_BYTES); |
| } |
| |
| function timestamp(value: unknown, label: string): number { |
| const result = requireCount(value, label); |
| return result; |
| } |
| |
| function positiveRevision(value: unknown, label: string): number { |
| const revision = requireCount(value, label); |
| if (revision < 1) throw invalidProtocolFrame(`Invalid ${label}`); |
| return revision; |
| } |
| |
| function catalogRevision(value: unknown): SessionCatalogRevision { |
| if (typeof value !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value)) { |
| throw invalidProtocolFrame('Invalid Session catalog revision'); |
| } |
| return value as SessionCatalogRevision; |
| } |
| |
| function boundedText(value: unknown, label: string, maxBytes: number): string { |
| const text = requireUtf8String(value, label, maxBytes); |
| if (text.trim() !== text || /[\u0000-\u001f\u007f]/.test(text)) { |
| throw invalidProtocolFrame(`Invalid ${label}`); |
| } |
| return text; |
| } |
| |
| function boolean(value: unknown, label: string): boolean { |
| if (typeof value !== 'boolean') throw invalidProtocolFrame(`Invalid ${label}`); |
| return value; |
| } |
| |
| function assertUpdateOutputIdentity( |
| input: SessionMetadataUpdateInput | SessionConfigurationUpdateInput | SessionCwdRelocateInput, |
| output: SessionUpdateResult, |
| ): void { |
| if (output.kind === 'committed') assertSessionIdentity(input.sessionId, output.session); |
| if (output.kind === 'revision_conflict' && output.expectedRevision !== input.expectedRevision) { |
| throw invalidProtocolFrame('Session revision conflict does not match request'); |
| } |
| } |
| |
| function assertSessionIdentity( |
| sessionId: string, |
| output: Pick<SessionCatalogProjection, 'id'>, |
| ): void { |
| if (output.id !== sessionId) { |
| throw invalidProtocolFrame('Session operation result identity does not match request'); |
| } |
| } |