blob: abf0ee426099ad62863a1c042a866803fb1820a2 [file]
import {
LOCAL_MEMORY_MAX_BYTES,
type LocalMemoryEntryPreview,
type LocalMemoryEntryStatus,
type LocalMemoryScope,
type LocalMemorySource,
} from '@maka/core';
import {
requireCount,
requireEntityId,
requireExactRecord,
requireRecord,
requireString,
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
export const MEMORY_DOCUMENT_CHUNK_MAX_BYTES = 32 * 1024;
export const MEMORY_RESULT_MAX_BYTES = 48 * 1024;
export const MEMORY_ENTRY_PAGE_MAX_ITEMS = 64;
export const MEMORY_SEMANTIC_CONTENT_MAX_BYTES = 24 * 1024;
const QUERY_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'invalid_request',
'persistence_failed',
'internal_failure',
] as const;
const MUTATE_ERRORS = [...QUERY_ERRORS, 'commit_outcome_unknown'] as const;
const REVISION = /^sha256:[a-f0-9]{64}$/;
export type MemoryDocumentName = 'memory' | 'pending';
export type MemoryRevision = `sha256:${string}`;
export type MemoryBackupKind = 'save' | 'reset' | 'restore';
export type MemoryEntriesView = 'active' | 'archived' | 'proposals';
export type MemoryQueryInput =
| { readonly kind: 'state' }
| {
readonly kind: 'entries_start';
readonly view: MemoryEntriesView;
}
| {
readonly kind: 'entries_continue';
readonly view: MemoryEntriesView;
readonly revision: MemoryRevision;
readonly cursor: number;
}
| {
readonly kind: 'document_start';
readonly document: MemoryDocumentName;
}
| {
readonly kind: 'document_continue';
readonly document: MemoryDocumentName;
readonly revision: MemoryRevision;
readonly cursor: number;
};
export interface MemoryBackupProjection {
readonly kind: MemoryBackupKind;
readonly revision: MemoryRevision;
readonly updatedAt: number;
readonly sizeBytes: number;
readonly entryCount: number;
readonly activeEntryCount: number;
readonly archivedEntryCount: number;
readonly safeMode: boolean;
readonly reason?: string;
}
export interface MemoryStateProjection {
readonly kind: 'state';
readonly revision: MemoryRevision;
readonly memoryRevision: MemoryRevision | null;
readonly pendingRevision: MemoryRevision | null;
readonly agentReadEnabled: boolean;
readonly status: 'ok' | 'missing' | 'safe_mode';
readonly entryCount: number;
readonly activeEntryCount: number;
readonly archivedEntryCount: number;
readonly proposalCount: number;
readonly backups: readonly MemoryBackupProjection[];
}
export type MemoryEntryProjection = Pick<
LocalMemoryEntryPreview,
| 'id'
| 'source'
| 'status'
| 'title'
| 'content'
| 'scope'
| 'sessionId'
| 'proposalId'
| 'sourceTurnId'
| 'createdAt'
| 'updatedAt'
| 'proposedAt'
| 'confirmedAt'
| 'archivedAt'
| 'rejectedAt'
| 'tags'
>;
export interface MemoryEntriesPage {
readonly kind: 'entries_page';
readonly view: MemoryEntriesView;
readonly revision: MemoryRevision;
readonly items: readonly MemoryEntryProjection[];
readonly nextCursor: number | null;
}
export interface MemoryDocumentPage {
readonly kind: 'document_page';
readonly document: MemoryDocumentName;
readonly revision: MemoryRevision;
readonly totalBytes: number;
readonly offset: number;
readonly chunkBase64: string;
readonly nextCursor: number | null;
}
export type MemoryQueryResult =
| MemoryStateProjection
| MemoryEntriesPage
| MemoryDocumentPage
| {
readonly kind: 'revision_changed';
readonly expectedRevision: MemoryRevision;
readonly actualRevision: MemoryRevision | null;
}
| {
readonly kind: 'blocked';
readonly reason: 'disabled' | 'incognito_active';
}
| {
readonly kind: 'safe_mode';
readonly document: MemoryDocumentName;
readonly revision: MemoryRevision;
readonly reason: 'invalid_utf8' | 'oversize';
readonly byteLength: number;
}
| {
readonly kind: 'missing';
readonly document: MemoryDocumentName;
};
export type MemoryScopeInput =
| { readonly kind: 'workspace' }
| { readonly kind: 'session'; readonly sessionId: string };
export type MemoryMutateInput =
| {
readonly kind: 'propose';
readonly expectedRevision: MemoryRevision;
readonly title: string;
readonly content: string;
readonly scope: MemoryScopeInput;
readonly sourceTurnId?: string;
}
| {
readonly kind: 'remember';
readonly expectedRevision: MemoryRevision;
readonly title: string;
readonly content: string;
readonly scope: MemoryScopeInput;
}
| {
readonly kind: 'approve';
readonly expectedRevision: MemoryRevision;
readonly proposalId: string;
}
| {
readonly kind: 'reject';
readonly expectedRevision: MemoryRevision;
readonly proposalId: string;
}
| {
readonly kind: 'set_status';
readonly expectedRevision: MemoryRevision;
readonly entryId: string;
readonly status: 'active' | 'archived';
readonly archiveReason?: string;
}
| {
readonly kind: 'reset';
readonly expectedRevision: MemoryRevision;
}
| {
readonly kind: 'restore_backup';
readonly expectedRevision: MemoryRevision;
readonly backupKind: MemoryBackupKind;
readonly expectedBackupRevision: MemoryRevision;
}
| {
readonly kind: 'replace_begin';
readonly expectedRevision: MemoryRevision;
readonly totalBytes: number;
readonly contentSha256: MemoryRevision;
}
| {
readonly kind: 'replace_chunk';
readonly uploadId: string;
readonly offset: number;
readonly chunkBase64: string;
}
| {
readonly kind: 'replace_commit';
readonly uploadId: string;
}
| {
readonly kind: 'replace_abort';
readonly uploadId: string;
};
export type MemoryMutationRejectionReason =
| 'disabled'
| 'incognito_active'
| 'invalid_content'
| 'invalid_scope'
| 'invalid_state'
| 'not_found'
| 'not_pending'
| 'oversize'
| 'safe_mode'
| 'upload_not_found'
| 'upload_incomplete'
| 'upload_conflict'
| 'backup_not_found';
export type MemoryMutateResult =
| {
readonly kind: 'upload_opened';
readonly uploadId: string;
readonly nextOffset: 0;
}
| {
readonly kind: 'chunk_accepted';
readonly uploadId: string;
readonly nextOffset: number;
}
| {
readonly kind: 'upload_aborted';
readonly uploadId: string;
}
| {
readonly kind: 'committed' | 'unchanged';
readonly revision: MemoryRevision;
readonly memoryRevision: MemoryRevision | null;
readonly pendingRevision: MemoryRevision | null;
}
| {
readonly kind: 'revision_conflict';
readonly expectedRevision: MemoryRevision;
readonly actualRevision: MemoryRevision;
}
| {
readonly kind: 'backup_revision_conflict';
readonly backupKind: MemoryBackupKind;
readonly expectedRevision: MemoryRevision;
readonly actualRevision: MemoryRevision;
}
| {
readonly kind: 'rejected';
readonly reason: MemoryMutationRejectionReason;
};
export const MEMORY_OPERATION_SPECS = {
'memory.query': defineOperation<
MemoryQueryInput,
MemoryQueryResult,
(typeof QUERY_ERRORS)[number]
>({
mode: 'query',
availability: 'ready',
errors: QUERY_ERRORS,
decodeInput: decodeMemoryQueryInput,
decodeOutput: decodeMemoryQueryResult,
}),
'memory.mutate': defineOperation<
MemoryMutateInput,
MemoryMutateResult,
(typeof MUTATE_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: MUTATE_ERRORS,
decodeInput: decodeMemoryMutateInput,
decodeOutput: decodeMemoryMutateResult,
}),
} as const;
export function decodeMemoryQueryInput(value: unknown): MemoryQueryInput {
const input = requireRecord(value, 'Memory query input');
switch (input.kind) {
case 'state':
requireExactRecord(input, 'Memory state query', ['kind']);
return { kind: 'state' };
case 'entries_start': {
const exact = requireExactRecord(input, 'Memory entries start query', ['kind', 'view']);
return { kind: 'entries_start', view: requireEntriesView(exact.view) };
}
case 'entries_continue': {
const exact = requireExactRecord(input, 'Memory entries continuation', [
'kind',
'view',
'revision',
'cursor',
]);
return {
kind: 'entries_continue',
view: requireEntriesView(exact.view),
revision: requireRevision(exact.revision, 'Memory bundle revision'),
cursor: requireCount(exact.cursor, 'Memory entries cursor'),
};
}
case 'document_start': {
const exact = requireExactRecord(input, 'Memory document start query', ['kind', 'document']);
return {
kind: 'document_start',
document: requireDocumentName(exact.document),
};
}
case 'document_continue': {
const exact = requireExactRecord(input, 'Memory document continuation', [
'kind',
'document',
'revision',
'cursor',
]);
return {
kind: 'document_continue',
document: requireDocumentName(exact.document),
revision: requireRevision(exact.revision, 'Memory document revision'),
cursor: requireCount(exact.cursor, 'Memory document cursor'),
};
}
default:
throw invalidProtocolFrame('Invalid Memory query kind');
}
}
export function decodeMemoryQueryResult(value: unknown): MemoryQueryResult {
assertMemoryResultSize(value);
const result = requireRecord(value, 'Memory query result');
switch (result.kind) {
case 'state':
return decodeState(result);
case 'entries_page':
return decodeEntriesPage(result);
case 'document_page':
return decodeDocumentPage(result);
case 'revision_changed': {
const exact = requireExactRecord(result, 'Memory revision change', [
'kind',
'expectedRevision',
'actualRevision',
]);
return {
kind: 'revision_changed',
expectedRevision: requireRevision(exact.expectedRevision, 'expected Memory revision'),
actualRevision:
exact.actualRevision === null
? null
: requireRevision(exact.actualRevision, 'actual Memory revision'),
};
}
case 'blocked': {
const exact = requireExactRecord(result, 'Memory blocked result', ['kind', 'reason']);
if (exact.reason !== 'disabled' && exact.reason !== 'incognito_active') {
throw invalidProtocolFrame('Invalid Memory block reason');
}
return { kind: 'blocked', reason: exact.reason };
}
case 'safe_mode': {
const exact = requireExactRecord(result, 'Memory safe-mode result', [
'kind',
'document',
'revision',
'reason',
'byteLength',
]);
if (exact.reason !== 'invalid_utf8' && exact.reason !== 'oversize') {
throw invalidProtocolFrame('Invalid Memory safe-mode reason');
}
return {
kind: 'safe_mode',
document: requireDocumentName(exact.document),
revision: requireRevision(exact.revision, 'Memory document revision'),
reason: exact.reason,
byteLength: requireCount(exact.byteLength, 'Memory document byte length'),
};
}
case 'missing': {
const exact = requireExactRecord(result, 'Memory missing result', ['kind', 'document']);
return { kind: 'missing', document: requireDocumentName(exact.document) };
}
default:
throw invalidProtocolFrame('Invalid Memory query result kind');
}
}
export function decodeMemoryMutateInput(value: unknown): MemoryMutateInput {
const input = requireRecord(value, 'Memory mutation input');
switch (input.kind) {
case 'propose':
return decodePropose(input);
case 'remember':
return decodeRemember(input);
case 'approve':
case 'reject': {
const exact = requireExactRecord(input, `Memory ${input.kind} mutation`, [
'kind',
'expectedRevision',
'proposalId',
]);
return {
kind: input.kind,
expectedRevision: requireRevision(
exact.expectedRevision,
'expected Memory bundle revision',
),
proposalId: requireEntityId(exact.proposalId, 'Memory proposal id'),
};
}
case 'set_status': {
const keys = ['kind', 'expectedRevision', 'entryId', 'status'];
if (input.archiveReason !== undefined) keys.push('archiveReason');
const exact = requireExactRecord(input, 'Memory status mutation', keys);
if (exact.status !== 'active' && exact.status !== 'archived') {
throw invalidProtocolFrame('Invalid Memory entry status');
}
return {
kind: 'set_status',
expectedRevision: requireRevision(
exact.expectedRevision,
'expected Memory bundle revision',
),
entryId: requireEntityId(exact.entryId, 'Memory entry id'),
status: exact.status,
...(exact.archiveReason === undefined
? {}
: {
archiveReason: requireUtf8String(exact.archiveReason, 'Memory archive reason', 512),
}),
};
}
case 'reset': {
const exact = requireExactRecord(input, 'Memory reset mutation', [
'kind',
'expectedRevision',
]);
return {
kind: 'reset',
expectedRevision: requireRevision(
exact.expectedRevision,
'expected Memory bundle revision',
),
};
}
case 'restore_backup': {
const exact = requireExactRecord(input, 'Memory backup restore mutation', [
'kind',
'expectedRevision',
'backupKind',
'expectedBackupRevision',
]);
return {
kind: 'restore_backup',
expectedRevision: requireRevision(
exact.expectedRevision,
'expected Memory bundle revision',
),
backupKind: requireBackupKind(exact.backupKind),
expectedBackupRevision: requireRevision(
exact.expectedBackupRevision,
'expected Memory backup revision',
),
};
}
case 'replace_begin': {
const exact = requireExactRecord(input, 'Memory replace begin mutation', [
'kind',
'expectedRevision',
'totalBytes',
'contentSha256',
]);
const totalBytes = requireCount(exact.totalBytes, 'Memory replacement byte length');
if (totalBytes > LOCAL_MEMORY_MAX_BYTES) {
throw invalidProtocolFrame('Memory replacement exceeds its byte limit');
}
return {
kind: 'replace_begin',
expectedRevision: requireRevision(
exact.expectedRevision,
'expected Memory bundle revision',
),
totalBytes,
contentSha256: requireRevision(exact.contentSha256, 'Memory replacement content revision'),
};
}
case 'replace_chunk': {
const exact = requireExactRecord(input, 'Memory replace chunk mutation', [
'kind',
'uploadId',
'offset',
'chunkBase64',
]);
return {
kind: 'replace_chunk',
uploadId: requireEntityId(exact.uploadId, 'Memory upload id'),
offset: requireCount(exact.offset, 'Memory upload offset'),
chunkBase64: requireBase64Chunk(exact.chunkBase64),
};
}
case 'replace_commit':
case 'replace_abort': {
const exact = requireExactRecord(input, `Memory ${input.kind} mutation`, [
'kind',
'uploadId',
]);
return {
kind: input.kind,
uploadId: requireEntityId(exact.uploadId, 'Memory upload id'),
};
}
default:
throw invalidProtocolFrame('Invalid Memory mutation kind');
}
}
export function decodeMemoryMutateResult(value: unknown): MemoryMutateResult {
const result = requireRecord(value, 'Memory mutation result');
switch (result.kind) {
case 'upload_opened': {
const exact = requireExactRecord(result, 'Memory upload opened result', [
'kind',
'uploadId',
'nextOffset',
]);
if (exact.nextOffset !== 0) throw invalidProtocolFrame('Invalid Memory upload offset');
return {
kind: 'upload_opened',
uploadId: requireEntityId(exact.uploadId, 'Memory upload id'),
nextOffset: 0,
};
}
case 'chunk_accepted': {
const exact = requireExactRecord(result, 'Memory chunk accepted result', [
'kind',
'uploadId',
'nextOffset',
]);
return {
kind: 'chunk_accepted',
uploadId: requireEntityId(exact.uploadId, 'Memory upload id'),
nextOffset: requireCount(exact.nextOffset, 'Memory upload offset'),
};
}
case 'upload_aborted': {
const exact = requireExactRecord(result, 'Memory upload aborted result', [
'kind',
'uploadId',
]);
return {
kind: 'upload_aborted',
uploadId: requireEntityId(exact.uploadId, 'Memory upload id'),
};
}
case 'committed':
case 'unchanged': {
const exact = requireExactRecord(result, 'Memory committed result', [
'kind',
'revision',
'memoryRevision',
'pendingRevision',
]);
return {
kind: result.kind,
revision: requireRevision(exact.revision, 'Memory bundle revision'),
memoryRevision: requireNullableRevision(exact.memoryRevision, 'MEMORY.md revision'),
pendingRevision: requireNullableRevision(exact.pendingRevision, 'PENDING.md revision'),
};
}
case 'revision_conflict': {
const exact = requireExactRecord(result, 'Memory revision conflict', [
'kind',
'expectedRevision',
'actualRevision',
]);
return {
kind: 'revision_conflict',
expectedRevision: requireRevision(
exact.expectedRevision,
'expected Memory bundle revision',
),
actualRevision: requireRevision(exact.actualRevision, 'actual Memory bundle revision'),
};
}
case 'backup_revision_conflict': {
const exact = requireExactRecord(result, 'Memory backup revision conflict', [
'kind',
'backupKind',
'expectedRevision',
'actualRevision',
]);
return {
kind: 'backup_revision_conflict',
backupKind: requireBackupKind(exact.backupKind),
expectedRevision: requireRevision(
exact.expectedRevision,
'expected Memory backup revision',
),
actualRevision: requireRevision(exact.actualRevision, 'actual Memory backup revision'),
};
}
case 'rejected': {
const exact = requireExactRecord(result, 'Memory rejected mutation', ['kind', 'reason']);
return { kind: 'rejected', reason: requireRejectionReason(exact.reason) };
}
default:
throw invalidProtocolFrame('Invalid Memory mutation result kind');
}
}
function decodePropose(
input: Record<string, unknown>,
): Extract<MemoryMutateInput, { kind: 'propose' }> {
const keys = ['kind', 'expectedRevision', 'title', 'content', 'scope'];
if (input.sourceTurnId !== undefined) keys.push('sourceTurnId');
const exact = requireExactRecord(input, 'Memory propose mutation', keys);
return {
kind: 'propose',
expectedRevision: requireRevision(exact.expectedRevision, 'expected Memory bundle revision'),
title: requireUtf8String(exact.title, 'Memory title', 512),
content: requireUtf8String(exact.content, 'Memory content', MEMORY_SEMANTIC_CONTENT_MAX_BYTES),
scope: decodeScope(exact.scope),
...(exact.sourceTurnId === undefined
? {}
: { sourceTurnId: requireEntityId(exact.sourceTurnId, 'source turn id') }),
};
}
function decodeRemember(
input: Record<string, unknown>,
): Extract<MemoryMutateInput, { kind: 'remember' }> {
const exact = requireExactRecord(input, 'Memory remember mutation', [
'kind',
'expectedRevision',
'title',
'content',
'scope',
]);
return {
kind: 'remember',
expectedRevision: requireRevision(exact.expectedRevision, 'expected Memory bundle revision'),
title: requireUtf8String(exact.title, 'Memory title', 512),
content: requireUtf8String(exact.content, 'Memory content', MEMORY_SEMANTIC_CONTENT_MAX_BYTES),
scope: decodeScope(exact.scope),
};
}
function decodeScope(value: unknown): MemoryScopeInput {
const scope = requireRecord(value, 'Memory scope');
if (scope.kind === 'workspace') {
requireExactRecord(scope, 'workspace Memory scope', ['kind']);
return { kind: 'workspace' };
}
if (scope.kind === 'session') {
const exact = requireExactRecord(scope, 'session Memory scope', ['kind', 'sessionId']);
return {
kind: 'session',
sessionId: requireEntityId(exact.sessionId, 'Memory session id'),
};
}
throw invalidProtocolFrame('Invalid Memory scope');
}
function decodeState(input: Record<string, unknown>): MemoryStateProjection {
const exact = requireExactRecord(input, 'Memory state result', [
'kind',
'revision',
'memoryRevision',
'pendingRevision',
'agentReadEnabled',
'status',
'entryCount',
'activeEntryCount',
'archivedEntryCount',
'proposalCount',
'backups',
]);
if (exact.status !== 'ok' && exact.status !== 'missing' && exact.status !== 'safe_mode') {
throw invalidProtocolFrame('Invalid Memory state status');
}
if (
typeof exact.agentReadEnabled !== 'boolean' ||
!Array.isArray(exact.backups) ||
exact.backups.length > 3
) {
throw invalidProtocolFrame('Invalid Memory state projection');
}
return {
kind: 'state',
revision: requireRevision(exact.revision, 'Memory bundle revision'),
memoryRevision: requireNullableRevision(exact.memoryRevision, 'MEMORY.md revision'),
pendingRevision: requireNullableRevision(exact.pendingRevision, 'PENDING.md revision'),
agentReadEnabled: exact.agentReadEnabled,
status: exact.status,
entryCount: requireCount(exact.entryCount, 'Memory entry count'),
activeEntryCount: requireCount(exact.activeEntryCount, 'active Memory entry count'),
archivedEntryCount: requireCount(exact.archivedEntryCount, 'archived Memory entry count'),
proposalCount: requireCount(exact.proposalCount, 'Memory proposal count'),
backups: exact.backups.map(decodeBackup),
};
}
function decodeEntriesPage(input: Record<string, unknown>): MemoryEntriesPage {
const exact = requireExactRecord(input, 'Memory entries page', [
'kind',
'view',
'revision',
'items',
'nextCursor',
]);
if (!Array.isArray(exact.items) || exact.items.length > MEMORY_ENTRY_PAGE_MAX_ITEMS) {
throw invalidProtocolFrame('Invalid Memory entry page items');
}
return {
kind: 'entries_page',
view: requireEntriesView(exact.view),
revision: requireRevision(exact.revision, 'Memory bundle revision'),
items: exact.items.map(decodeEntry),
nextCursor:
exact.nextCursor === null ? null : requireCount(exact.nextCursor, 'Memory entries cursor'),
};
}
function decodeDocumentPage(input: Record<string, unknown>): MemoryDocumentPage {
const exact = requireExactRecord(input, 'Memory document page', [
'kind',
'document',
'revision',
'totalBytes',
'offset',
'chunkBase64',
'nextCursor',
]);
return {
kind: 'document_page',
document: requireDocumentName(exact.document),
revision: requireRevision(exact.revision, 'Memory document revision'),
totalBytes: requireCount(exact.totalBytes, 'Memory document byte length'),
offset: requireCount(exact.offset, 'Memory document offset'),
chunkBase64: requireBase64Chunk(exact.chunkBase64, true),
nextCursor:
exact.nextCursor === null ? null : requireCount(exact.nextCursor, 'Memory document cursor'),
};
}
function decodeEntry(value: unknown): MemoryEntryProjection {
const entry = requireRecord(value, 'Memory entry');
const required = ['id', 'source', 'status', 'title', 'content', 'scope', 'tags'];
const optional = [
'sessionId',
'proposalId',
'sourceTurnId',
'createdAt',
'updatedAt',
'proposedAt',
'confirmedAt',
'archivedAt',
'rejectedAt',
];
assertKnownOptionalKeys(entry, 'Memory entry', required, optional);
if (!Array.isArray(entry.tags) || entry.tags.length > 8) {
throw invalidProtocolFrame('Invalid Memory entry tags');
}
const source = requireMemorySource(entry.source);
const status = requireMemoryStatus(entry.status);
const scope = requireMemoryScope(entry.scope);
return {
id: requireUtf8String(entry.id, 'Memory entry id', 512),
source,
status,
title: requireUtf8String(entry.title, 'Memory entry title', 512),
content: requireUtf8String(entry.content, 'Memory entry content', 4 * 1024),
scope,
...(entry.sessionId === undefined
? {}
: { sessionId: requireUtf8String(entry.sessionId, 'Memory session id', 512) }),
...(entry.proposalId === undefined
? {}
: { proposalId: requireUtf8String(entry.proposalId, 'Memory proposal id', 512) }),
...(entry.sourceTurnId === undefined
? {}
: { sourceTurnId: requireUtf8String(entry.sourceTurnId, 'source turn id', 512) }),
...decodeOptionalTimestamps(entry),
tags: entry.tags.map((tag) => requireUtf8String(tag, 'Memory tag', 64)),
};
}
function decodeOptionalTimestamps(
entry: Record<string, unknown>,
): Partial<
Pick<
MemoryEntryProjection,
'createdAt' | 'updatedAt' | 'proposedAt' | 'confirmedAt' | 'archivedAt' | 'rejectedAt'
>
> {
const result: Record<string, number> = {};
for (const key of [
'createdAt',
'updatedAt',
'proposedAt',
'confirmedAt',
'archivedAt',
'rejectedAt',
] as const) {
if (entry[key] !== undefined) result[key] = requireCount(entry[key], `Memory ${key}`);
}
return result;
}
function decodeBackup(value: unknown): MemoryBackupProjection {
const backup = requireRecord(value, 'Memory backup');
const required = [
'kind',
'revision',
'updatedAt',
'sizeBytes',
'entryCount',
'activeEntryCount',
'archivedEntryCount',
'safeMode',
];
assertKnownOptionalKeys(backup, 'Memory backup', required, ['reason']);
if (typeof backup.safeMode !== 'boolean') {
throw invalidProtocolFrame('Invalid Memory backup safe-mode state');
}
return {
kind: requireBackupKind(backup.kind),
revision: requireRevision(backup.revision, 'Memory backup revision'),
updatedAt: requireCount(backup.updatedAt, 'Memory backup timestamp'),
sizeBytes: requireCount(backup.sizeBytes, 'Memory backup size'),
entryCount: requireCount(backup.entryCount, 'Memory backup entry count'),
activeEntryCount: requireCount(backup.activeEntryCount, 'Memory backup active entry count'),
archivedEntryCount: requireCount(
backup.archivedEntryCount,
'Memory backup archived entry count',
),
safeMode: backup.safeMode,
...(backup.reason === undefined
? {}
: { reason: requireUtf8String(backup.reason, 'Memory backup reason', 512) }),
};
}
function requireDocumentName(value: unknown): MemoryDocumentName {
if (value !== 'memory' && value !== 'pending') {
throw invalidProtocolFrame('Invalid Memory document name');
}
return value;
}
function requireEntriesView(value: unknown): MemoryEntriesView {
if (value !== 'active' && value !== 'archived' && value !== 'proposals') {
throw invalidProtocolFrame('Invalid Memory entries view');
}
return value;
}
function requireBackupKind(value: unknown): MemoryBackupKind {
if (value !== 'save' && value !== 'reset' && value !== 'restore') {
throw invalidProtocolFrame('Invalid Memory backup kind');
}
return value;
}
function requireMemoryScope(value: unknown): LocalMemoryScope {
if (value !== 'workspace' && value !== 'session') {
throw invalidProtocolFrame('Invalid Memory entry scope');
}
return value;
}
function requireMemorySource(value: unknown): LocalMemorySource {
if (value !== 'user_authored' && value !== 'chat_extracted' && value !== 'unknown') {
throw invalidProtocolFrame('Invalid Memory entry source');
}
return value;
}
function requireMemoryStatus(value: unknown): LocalMemoryEntryStatus {
if (
value !== 'draft' &&
value !== 'review_required' &&
value !== 'active' &&
value !== 'archived' &&
value !== 'rejected' &&
value !== 'unknown'
) {
throw invalidProtocolFrame('Invalid Memory entry status');
}
return value;
}
function requireRejectionReason(value: unknown): MemoryMutationRejectionReason {
const allowed: readonly MemoryMutationRejectionReason[] = [
'disabled',
'incognito_active',
'invalid_content',
'invalid_scope',
'invalid_state',
'not_found',
'not_pending',
'oversize',
'safe_mode',
'upload_not_found',
'upload_incomplete',
'upload_conflict',
'backup_not_found',
];
if (typeof value !== 'string' || !allowed.includes(value as MemoryMutationRejectionReason)) {
throw invalidProtocolFrame('Invalid Memory mutation rejection reason');
}
return value as MemoryMutationRejectionReason;
}
function requireRevision(value: unknown, label: string): MemoryRevision {
if (typeof value !== 'string' || !REVISION.test(value)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as MemoryRevision;
}
function requireNullableRevision(value: unknown, label: string): MemoryRevision | null {
return value === null ? null : requireRevision(value, label);
}
function requireUtf8String(value: unknown, label: string, maxBytes: number): string {
const text = requireString(value, label, maxBytes);
if (Buffer.byteLength(text, 'utf8') > maxBytes) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return text;
}
function requireBase64Chunk(value: unknown, allowEmpty = false): string {
if (allowEmpty && value === '') return '';
const encoded = requireString(
value,
'Memory chunk',
Math.ceil(MEMORY_DOCUMENT_CHUNK_MAX_BYTES / 3) * 4,
);
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
throw invalidProtocolFrame('Invalid Memory chunk');
}
const decoded = Buffer.from(encoded, 'base64');
if (
decoded.byteLength > MEMORY_DOCUMENT_CHUNK_MAX_BYTES ||
decoded.toString('base64') !== encoded
) {
throw invalidProtocolFrame('Invalid Memory chunk');
}
return encoded;
}
function assertKnownOptionalKeys(
record: Record<string, unknown>,
label: string,
required: readonly string[],
optional: readonly string[],
): void {
const allowed = new Set([...required, ...optional]);
if (
required.some((key) => !Object.hasOwn(record, key)) ||
Object.keys(record).some((key) => !allowed.has(key))
) {
throw invalidProtocolFrame(`Invalid ${label} fields`);
}
}
function assertMemoryResultSize(value: unknown): void {
if (Buffer.byteLength(JSON.stringify(value), 'utf8') > MEMORY_RESULT_MAX_BYTES) {
throw invalidProtocolFrame('Memory result exceeds byte limit');
}
}