blob: 8943659c1412fe226fa5cb8940720fa9537f1835 [file]
import { parseLocalMemoryMarkdown, type LocalMemoryEntryPreview } from '@maka/core';
import type {
MemoryBackupSnapshot,
MemoryBundleSnapshot,
MemoryDocumentSnapshot,
} from '@maka/storage/memory-bundle-store';
import {
MEMORY_DOCUMENT_CHUNK_MAX_BYTES,
MEMORY_ENTRY_PAGE_MAX_ITEMS,
MEMORY_RESULT_MAX_BYTES,
type MemoryDocumentName,
type MemoryEntriesPage,
type MemoryEntryProjection,
type MemoryQueryInput,
type MemoryQueryResult,
} from '../protocol/index.js';
export class MemoryProjectionError extends Error {}
export function projectMemoryQuery(
input: MemoryQueryInput,
snapshot: MemoryBundleSnapshot,
agentReadEnabled: boolean,
backups: readonly MemoryBackupSnapshot[],
): MemoryQueryResult {
switch (input.kind) {
case 'state':
return projectState(snapshot, agentReadEnabled, backups);
case 'entries_start':
return projectEntries(snapshot, input.view, 0);
case 'entries_continue':
return input.revision === snapshot.revision
? projectEntries(snapshot, input.view, input.cursor)
: {
kind: 'revision_changed',
expectedRevision: input.revision,
actualRevision: snapshot.revision,
};
case 'document_start':
return projectDocument(snapshot, input.document, 0);
case 'document_continue': {
const document = snapshot[input.document];
return document.revision === input.revision
? projectDocument(snapshot, input.document, input.cursor)
: {
kind: 'revision_changed',
expectedRevision: input.revision,
actualRevision: document.revision,
};
}
}
}
function projectState(
snapshot: MemoryBundleSnapshot,
agentReadEnabled: boolean,
backups: readonly MemoryBackupSnapshot[],
): MemoryQueryResult {
const memory = parseSnapshot(snapshot.memory);
const pending = parseSnapshot(snapshot.pending);
const status =
snapshot.memory.kind === 'safe_mode' || snapshot.pending.kind === 'safe_mode'
? 'safe_mode'
: snapshot.memory.kind === 'missing'
? 'missing'
: 'ok';
return {
kind: 'state',
revision: snapshot.revision,
memoryRevision: snapshot.memory.revision,
pendingRevision: snapshot.pending.revision,
agentReadEnabled,
status,
entryCount: memory?.entries.length ?? 0,
activeEntryCount: memory?.activeEntries.length ?? 0,
archivedEntryCount: memory?.archivedEntries.length ?? 0,
proposalCount:
pending?.entries.filter(
(entry) => entry.status === 'draft' || entry.status === 'review_required',
).length ?? 0,
backups: backups.map(projectBackup),
};
}
function projectBackup(backup: MemoryBackupSnapshot) {
const parsed =
backup.document.kind === 'document'
? parseLocalMemoryMarkdown(decodeDocument(backup.document))
: undefined;
return {
kind: backup.kind,
revision: backup.revision,
updatedAt: boundedCount(backup.updatedAt),
sizeBytes: backup.document.byteLength,
entryCount: parsed?.entries.length ?? 0,
activeEntryCount: parsed?.activeEntries.length ?? 0,
archivedEntryCount: parsed?.archivedEntries.length ?? 0,
safeMode: backup.document.kind === 'safe_mode' || parsed?.safeMode === true,
...(backup.document.kind === 'safe_mode'
? { reason: backup.document.reason }
: parsed?.reason
? { reason: parsed.reason }
: {}),
};
}
function projectEntries(
snapshot: MemoryBundleSnapshot,
view: 'active' | 'archived' | 'proposals',
offset: number,
): MemoryQueryResult {
const document = view === 'proposals' ? snapshot.pending : snapshot.memory;
if (document.kind === 'missing') {
return { kind: 'missing', document: view === 'proposals' ? 'pending' : 'memory' };
}
if (document.kind === 'safe_mode') {
return projectSafeMode(document, view === 'proposals' ? 'pending' : 'memory');
}
const parsed = parseLocalMemoryMarkdown(decodeDocument(document));
const source =
view === 'active'
? parsed.activeEntries
: view === 'archived'
? parsed.archivedEntries
: parsed.entries.filter(
(entry) => entry.status === 'draft' || entry.status === 'review_required',
);
if (offset > source.length) {
throw new MemoryProjectionError('Memory entries cursor is out of range');
}
return entriesPage(snapshot.revision, view, source, offset);
}
function entriesPage(
revision: MemoryBundleSnapshot['revision'],
view: 'active' | 'archived' | 'proposals',
source: readonly LocalMemoryEntryPreview[],
offset: number,
): MemoryEntriesPage {
const items: MemoryEntryProjection[] = [];
const limit = Math.min(source.length, offset + MEMORY_ENTRY_PAGE_MAX_ITEMS);
for (let index = offset; index < limit; index += 1) {
const entry = source[index];
if (!entry) break;
const candidate = [...items, projectEntry(entry)];
const nextOffset = offset + candidate.length;
const page = {
kind: 'entries_page' as const,
view,
revision,
items: candidate,
nextCursor: nextOffset < source.length ? nextOffset : null,
};
if (Buffer.byteLength(JSON.stringify(page), 'utf8') > MEMORY_RESULT_MAX_BYTES) break;
items.push(candidate.at(-1)!);
}
if (items.length === 0 && offset < source.length) {
throw new Error('A legal Memory entry exceeded the page result byte limit');
}
const nextOffset = offset + items.length;
return {
kind: 'entries_page',
view,
revision,
items,
nextCursor: nextOffset < source.length ? nextOffset : null,
};
}
function projectEntry(entry: LocalMemoryEntryPreview): MemoryEntryProjection {
return {
id: entry.id,
source: entry.source,
status: entry.status,
title: boundedUtf8(entry.title, 512),
content: boundedUtf8(entry.content, 4 * 1024),
scope: entry.scope ?? 'workspace',
...(entry.sessionId ? { sessionId: entry.sessionId } : {}),
...(entry.proposalId ? { proposalId: entry.proposalId } : {}),
...(entry.sourceTurnId ? { sourceTurnId: entry.sourceTurnId } : {}),
...projectTimestamps(entry),
tags: entry.tags.map((tag) => boundedUtf8(tag, 64)),
};
}
type MemoryTimestampProjection = Pick<
MemoryEntryProjection,
'createdAt' | 'updatedAt' | 'proposedAt' | 'confirmedAt' | 'archivedAt' | 'rejectedAt'
>;
function projectTimestamps(entry: LocalMemoryEntryPreview): Partial<MemoryTimestampProjection> {
return {
...(isProtocolCount(entry.createdAt) ? { createdAt: entry.createdAt } : {}),
...(isProtocolCount(entry.updatedAt) ? { updatedAt: entry.updatedAt } : {}),
...(isProtocolCount(entry.proposedAt) ? { proposedAt: entry.proposedAt } : {}),
...(isProtocolCount(entry.confirmedAt) ? { confirmedAt: entry.confirmedAt } : {}),
...(isProtocolCount(entry.archivedAt) ? { archivedAt: entry.archivedAt } : {}),
...(isProtocolCount(entry.rejectedAt) ? { rejectedAt: entry.rejectedAt } : {}),
};
}
function isProtocolCount(value: number | undefined): value is number {
return value !== undefined && Number.isSafeInteger(value) && value >= 0;
}
function boundedUtf8(value: string, maxBytes: number): string {
if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
let bounded = '';
let bytes = 0;
for (const character of value) {
const characterBytes = Buffer.byteLength(character, 'utf8');
if (bytes + characterBytes > maxBytes) break;
bounded += character;
bytes += characterBytes;
}
return bounded;
}
function boundedCount(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.round(value)));
}
function projectDocument(
snapshot: MemoryBundleSnapshot,
name: MemoryDocumentName,
offset: number,
): MemoryQueryResult {
const document = snapshot[name];
if (document.kind === 'missing') return { kind: 'missing', document: name };
if (document.kind === 'safe_mode') return projectSafeMode(document, name);
if (offset > document.byteLength) {
throw new MemoryProjectionError('Memory document cursor is out of range');
}
const chunk = Buffer.from(document.bytes).subarray(
offset,
Math.min(document.byteLength, offset + MEMORY_DOCUMENT_CHUNK_MAX_BYTES),
);
const nextOffset = offset + chunk.byteLength;
return {
kind: 'document_page',
document: name,
revision: document.revision,
totalBytes: document.byteLength,
offset,
chunkBase64: chunk.toString('base64'),
nextCursor: nextOffset < document.byteLength ? nextOffset : null,
};
}
function projectSafeMode(
document: Extract<MemoryDocumentSnapshot, { kind: 'safe_mode' }>,
name: MemoryDocumentName,
): MemoryQueryResult {
return {
kind: 'safe_mode',
document: name,
revision: document.revision,
reason: document.reason,
byteLength: document.byteLength,
};
}
function parseSnapshot(document: MemoryDocumentSnapshot) {
return document.kind === 'document'
? parseLocalMemoryMarkdown(decodeDocument(document))
: undefined;
}
function decodeDocument(document: Extract<MemoryDocumentSnapshot, { kind: 'document' }>): string {
return new TextDecoder('utf-8', { fatal: true }).decode(document.bytes);
}