blob: 6b7fec2e35b8232b0ae44d7c242a4a3ac2045826 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import type { StoredMessage } from '@maka/core/session';
import {
SESSION_TRANSCRIPT_PAGE_MAX_MESSAGES,
SESSION_TRANSCRIPT_RANGE_MAX_BYTES,
SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES,
type SessionTranscriptBootstrap,
type SessionTranscriptFragment,
type SessionTranscriptPage,
type SessionTranscriptPageDirection,
type SessionTranscriptPageInput,
type SessionTranscriptPageSource,
type TurnSnapshot,
} from '../protocol/index.js';
import {
ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES,
ACTIVE_TRANSCRIPT_OVERLAY_MAX_MESSAGES,
type SessionTranscriptReader,
} from './session-transcript-reader.js';
import { projectSharedSessionTranscriptMessage } from './shared-session-transcript.js';
type SessionTranscriptProjection = 'owner' | 'shared';
interface TranscriptCursorState {
readonly version: 1;
readonly subscriptionId: string;
readonly sessionId: string;
readonly source: SessionTranscriptPageSource;
readonly direction: SessionTranscriptPageDirection;
readonly throughSequence: number | null;
readonly position: number;
readonly byteOffset: number | null;
readonly rangeBoundarySequence: number | null;
}
export interface SubscriberTranscriptState {
readonly sessionId: string;
readonly subscriptionId: string;
readonly openedThroughSequence: number | null;
overlayMessages: readonly Buffer[] | undefined;
readonly cursorSecret: Buffer;
durableThroughSequence: number | null;
readonly projection: SessionTranscriptProjection;
}
export interface ActiveTranscriptAssistantStream {
readonly turnId: string;
readonly messageId: string;
readonly kind: 'text' | 'thinking';
readonly text: string;
}
interface SelectedFragments {
readonly fragments: readonly SessionTranscriptFragment[];
readonly rawBytes: number;
readonly next: { position: number; byteOffset: number | null } | null;
}
export async function createSessionTranscriptBootstrap(input: {
reader: SessionTranscriptReader;
sessionId: string;
subscriptionId: string;
throughSequence: number | null;
rootTurn: TurnSnapshot | null;
activeAssistantStreams: Iterable<ActiveTranscriptAssistantStream>;
maxBytes: number;
maxEncodedBytes?: number;
preparedOverlayMessages?: readonly Buffer[];
projection: SessionTranscriptProjection;
}): Promise<{ bootstrap: SessionTranscriptBootstrap; state: SubscriberTranscriptState }> {
const projection = input.projection;
const preparedOverlayMessages =
input.preparedOverlayMessages ?? (await prepareSessionTranscriptOverlay(input));
const overlayMessages =
projection === 'shared'
? preparedOverlayMessages.flatMap((message) =>
projectEncodedSharedMessage(message, input.sessionId),
)
: preparedOverlayMessages;
const cursorSecret = randomBytes(32);
let rawBudget = input.maxBytes;
for (;;) {
const overlayBudget = Math.min(8 * 1024, Math.max(1, Math.floor(rawBudget / 2)));
const selectedOverlay = selectOverlay(
overlayMessages,
'older',
overlayMessages.length - 1,
null,
overlayBudget,
);
const durableBudget = rawBudget - selectedOverlay.rawBytes;
const durableRequest = {
direction: 'older',
throughSequence: input.throughSequence,
maxBytes: durableBudget,
maxMessages: SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES,
} as const;
const durableStorage =
projection === 'shared'
? await readSharedDurablePage(input.reader, input.sessionId, durableRequest)
: await input.reader.readDurablePage(input.sessionId, durableRequest);
if (durableStorage.throughSequence !== input.throughSequence) {
throw new Error('Session transcript durable watermark changed during bootstrap');
}
const state: SubscriberTranscriptState = {
sessionId: input.sessionId,
subscriptionId: input.subscriptionId,
openedThroughSequence: input.throughSequence,
durableThroughSequence: input.throughSequence,
overlayMessages,
cursorSecret,
projection,
};
const durableSelection = storageSelection(durableStorage);
const rangeEdges = await readRangeEdges({
reader: input.reader,
state,
direction: 'older',
throughSequence: input.throughSequence,
selected: durableSelection,
});
const bootstrap: SessionTranscriptBootstrap = {
throughSequence: input.throughSequence,
overlayMessageCount: overlayMessages.length,
durable: pageFromSelection(
state,
'durable',
'older',
rangeEdges.selected,
input.throughSequence,
rangeEdges.rangeBoundarySequence,
rangeEdges.protectedTurnSequence,
),
overlay: pageFromSelection(state, 'overlay', 'older', selectedOverlay),
};
const encodedBytes = Buffer.byteLength(JSON.stringify(bootstrap), 'utf8');
if (input.maxEncodedBytes === undefined || encodedBytes <= input.maxEncodedBytes) {
return { state, bootstrap };
}
if (rawBudget <= 2) {
throw new Error('Session transcript bootstrap cannot fit the subscription open result');
}
const excess = encodedBytes - input.maxEncodedBytes;
rawBudget = Math.max(2, rawBudget - Math.max(1, Math.ceil((excess * 3) / 4)));
}
}
export async function prepareSessionTranscriptOverlay(input: {
reader: SessionTranscriptReader;
sessionId: string;
throughSequence: number | null;
rootTurn: TurnSnapshot | null;
activeAssistantStreams: Iterable<ActiveTranscriptAssistantStream>;
}): Promise<readonly Buffer[]> {
const activeAssistantStreams = [...input.activeAssistantStreams];
const activeMessageIds = [...new Set(activeAssistantStreams.map((stream) => stream.messageId))];
const activeOverlay = await input.reader.readActiveOverlay(input.sessionId, input.rootTurn);
const durableActiveMessages = await input.reader.readDurableMessagesById(input.sessionId, {
messageIds: activeMessageIds,
throughSequence: input.throughSequence,
maxBytes: ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES,
maxMessages: ACTIVE_TRANSCRIPT_OVERLAY_MAX_MESSAGES,
});
const overlayMessages = mergeActiveAssistantStreams(
activeOverlay,
activeAssistantStreams,
durableActiveMessages,
).map((message) => Buffer.from(JSON.stringify(message), 'utf8'));
assertOverlayRetainedBound(overlayMessages);
return overlayMessages;
}
export async function readSessionTranscriptPage(input: {
reader: SessionTranscriptReader;
state: SubscriberTranscriptState;
request: SessionTranscriptPageInput;
}): Promise<SessionTranscriptPage> {
const { state, request } = input;
if (
request.throughSequence !== null &&
(state.durableThroughSequence === null ||
request.throughSequence > state.durableThroughSequence)
) {
throw new TranscriptPageRequestError('Transcript watermark is not known to this subscription');
}
if (request.source === 'overlay' && request.throughSequence !== state.openedThroughSequence) {
throw new TranscriptPageRequestError('Transcript overlay watermark changed');
}
if (request.source === 'overlay' && state.overlayMessages === undefined) {
throw new TranscriptPageRequestError('Transcript overlay has been released');
}
const position = resolvePosition(state, request);
if (position === null) return emptyPage(state, request);
if (request.source === 'overlay') {
const overlayMessages = state.overlayMessages!;
const selected = selectOverlay(
overlayMessages,
request.direction,
position.position,
position.byteOffset,
request.maxBytes,
continuationMessageLimit(position),
);
return pageFromSelection(
state,
'overlay',
request.direction,
selected,
request.throughSequence,
);
}
if (request.throughSequence === null) return emptyPage(state, request);
const durableRequest = {
direction: request.direction,
throughSequence: request.throughSequence,
position: position.position,
...(position.byteOffset === null ? {} : { byteOffset: position.byteOffset }),
maxBytes: request.maxBytes,
maxMessages: continuationMessageLimit(position),
} as const;
const storage =
state.projection === 'shared'
? await readSharedDurablePage(
input.reader,
state.sessionId,
durableRequest,
position.rangeBoundarySequence,
)
: await input.reader.readDurablePage(state.sessionId, durableRequest);
const selected = selectionThroughRangeBoundary(
storageSelection(storage),
request.direction,
position.rangeBoundarySequence,
);
const rangeEdges = await readRangeEdges({
reader: input.reader,
state,
direction: request.direction,
throughSequence: request.throughSequence,
selected,
});
return pageFromSelection(
state,
'durable',
request.direction,
rangeEdges.selected,
request.throughSequence,
rangeEdges.rangeBoundarySequence,
rangeEdges.protectedTurnSequence,
);
}
async function readRangeEdges(input: {
reader: SessionTranscriptReader;
state: SubscriberTranscriptState;
direction: SessionTranscriptPageDirection;
throughSequence: number | null;
selected: SelectedFragments;
}): Promise<{
readonly selected: SelectedFragments;
readonly rangeBoundarySequence: number | null;
readonly protectedTurnSequence: number | null;
}> {
if (input.throughSequence === null) {
return {
selected: input.selected,
rangeBoundarySequence: null,
protectedTurnSequence: null,
};
}
const selectedSequences = input.selected.fragments.flatMap((fragment) =>
fragment.kind === 'durable' ? [fragment.sequence] : [],
);
if (selectedSequences.length === 0) {
return {
selected: input.selected,
rangeBoundarySequence: null,
protectedTurnSequence: null,
};
}
const boundaryCandidate =
input.direction === 'older' ? Math.min(...selectedSequences) : Math.max(...selectedSequences);
const scanPosition =
input.direction === 'older' ? Math.max(...selectedSequences) : Math.min(...selectedSequences);
const rangeRecords: Array<{
readonly sequence: number;
readonly turnId: string | undefined;
readonly bytes: number;
}> = [];
let targetTurnId: string | undefined;
let targetStart: number | null = null;
let candidateReached = false;
let hiddenBytes = 0;
let reachedFarEdge = false;
let position: number | null = scanPosition;
while (position !== null && !reachedFarEdge) {
const scanned = await input.reader.readDurableRecords(input.state.sessionId, {
direction: input.direction,
throughSequence: input.throughSequence,
position,
maxStoredBytes: SESSION_TRANSCRIPT_RANGE_MAX_BYTES,
maxMessages: SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES,
});
for (const record of scanned.records) {
const message =
input.state.projection === 'shared'
? projectSharedSessionTranscriptMessage(record.message, input.state.sessionId)
: record.message;
if (!message) {
hiddenBytes += Buffer.byteLength(JSON.stringify(record.message), 'utf8');
if (hiddenBytes > SESSION_TRANSCRIPT_RANGE_MAX_BYTES) {
throw new RangeError('Session transcript projection scan exceeds its capacity limit');
}
continue;
}
const turnId = messageTurnId(message);
if (targetStart !== null && turnId !== targetTurnId) {
reachedFarEdge = true;
break;
}
rangeRecords.push({
sequence: record.sequence,
turnId,
bytes: Buffer.byteLength(JSON.stringify(message), 'utf8'),
});
if (!candidateReached && record.sequence === boundaryCandidate) {
candidateReached = true;
if (turnId === undefined) {
reachedFarEdge = true;
break;
}
targetTurnId = turnId;
targetStart = rangeRecords.length - 1;
while (targetStart > 0 && rangeRecords[targetStart - 1]?.turnId === targetTurnId) {
targetStart -= 1;
}
}
if (targetStart !== null) {
const targetRecords = rangeRecords.slice(targetStart);
const targetBytes = targetRecords.reduce((sum, target) => sum + target.bytes, 0);
if (
targetRecords.length > SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES ||
targetBytes > SESSION_TRANSCRIPT_RANGE_MAX_BYTES
) {
if (targetStart === 0) {
return {
selected: input.selected,
rangeBoundarySequence: null,
protectedTurnSequence: null,
};
}
reachedFarEdge = true;
break;
}
}
}
if (reachedFarEdge || scanned.nextPosition === null) {
position = scanned.nextPosition;
break;
}
if (scanned.nextPosition === position) {
throw new Error('Session transcript projection scan did not advance');
}
position = scanned.nextPosition;
}
if (!candidateReached || rangeRecords.length === 0) {
throw new Error('Session transcript range did not reach its authoritative Turn');
}
let retainedEnd = 0;
let retainedMessages = 0;
let retainedBytes = 0;
while (retainedEnd < rangeRecords.length) {
const groupStart = retainedEnd;
const groupTurnId = rangeRecords[groupStart]!.turnId;
let groupEnd = groupStart + 1;
if (groupTurnId !== undefined) {
while (groupEnd < rangeRecords.length && rangeRecords[groupEnd]?.turnId === groupTurnId) {
groupEnd += 1;
}
}
const group = rangeRecords.slice(groupStart, groupEnd);
const groupBytes = group.reduce((sum, record) => sum + record.bytes, 0);
if (
group.length > SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES ||
groupBytes > SESSION_TRANSCRIPT_RANGE_MAX_BYTES
) {
if (groupStart > 0) break;
return {
selected: input.selected,
rangeBoundarySequence: null,
protectedTurnSequence: null,
};
}
if (
retainedMessages + group.length > SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES ||
retainedBytes + groupBytes > SESSION_TRANSCRIPT_RANGE_MAX_BYTES
) {
break;
}
retainedMessages += group.length;
retainedBytes += groupBytes;
retainedEnd = groupEnd;
}
const retainedRecords = rangeRecords.slice(0, retainedEnd);
const boundary = retainedRecords.at(-1)?.sequence;
if (boundary === undefined) {
throw new RangeError('Session transcript Turn range exceeds its capacity limit');
}
const selected =
retainedEnd === rangeRecords.length
? input.selected
: (() => {
const retainedSequences = new Set(retainedRecords.map((record) => record.sequence));
const fragments = input.selected.fragments.filter(
(fragment) => fragment.kind === 'durable' && retainedSequences.has(fragment.sequence),
);
return {
fragments,
rawBytes: fragments.reduce(
(sum, fragment) => sum + Buffer.byteLength(fragment.data, 'base64'),
0,
),
next: { position: rangeRecords[retainedEnd]!.sequence, byteOffset: null },
};
})();
const turnRecords = retainedRecords.filter((record) => record.turnId !== undefined);
const protectedTurnSequence =
input.direction === 'older' ? turnRecords[0]?.sequence : turnRecords.at(-1)?.sequence;
return {
selected,
rangeBoundarySequence: boundary,
protectedTurnSequence: protectedTurnSequence ?? boundary,
};
}
function messageTurnId(message: StoredMessage): string | undefined {
const turnId = 'turnId' in message ? message.turnId : undefined;
return typeof turnId === 'string' ? turnId : undefined;
}
async function readSharedDurablePage(
reader: SessionTranscriptReader,
sessionId: string,
request: Parameters<SessionTranscriptReader['readDurablePage']>[1],
rangeBoundarySequence: number | null = null,
): ReturnType<SessionTranscriptReader['readDurablePage']> {
const position =
request.position ??
(request.direction === 'older' ? (request.throughSequence ?? undefined) : 0);
const fragments: Awaited<
ReturnType<SessionTranscriptReader['readDurablePage']>
>['fragments'][number][] = [];
let rawBytes = 0;
let hiddenBytes = 0;
let next: { position: number; byteOffset: number | null } | null = null;
let scanPosition = position;
let throughSequence = request.throughSequence ?? null;
while (scanPosition !== undefined && next === null) {
const scanned = await reader.readDurableRecords(sessionId, {
direction: request.direction,
...(request.throughSequence === undefined
? {}
: { throughSequence: request.throughSequence }),
position: scanPosition,
maxStoredBytes: ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES,
maxMessages: SESSION_TRANSCRIPT_PAGE_MAX_MESSAGES,
});
throughSequence = scanned.throughSequence;
let recordIndex = 0;
for (; recordIndex < scanned.records.length; recordIndex += 1) {
const record = scanned.records[recordIndex]!;
const projected = projectSharedSessionTranscriptMessage(record.message, sessionId);
if (!projected) {
hiddenBytes += Buffer.byteLength(JSON.stringify(record.message), 'utf8');
if (hiddenBytes > SESSION_TRANSCRIPT_RANGE_MAX_BYTES) {
throw new RangeError('Session transcript projection scan exceeds its capacity limit');
}
continue;
}
const bytes = Buffer.from(JSON.stringify(projected), 'utf8');
const continuationOffset =
record.sequence === position && request.byteOffset !== undefined
? request.byteOffset
: null;
const selected = selectBuffer(
bytes,
request.direction,
continuationOffset,
request.maxBytes - rawBytes,
);
if (!selected) {
next = { position: record.sequence, byteOffset: null };
break;
}
fragments.push({
sequence: record.sequence,
byteOffset: selected.byteOffset,
totalBytes: bytes.byteLength,
payloadDigest: null,
data: selected.data,
});
rawBytes += selected.data.byteLength;
if (!selected.complete) {
next = { position: record.sequence, byteOffset: selected.nextOffset };
break;
}
if (record.sequence === rangeBoundarySequence) {
const following = scanned.records[recordIndex + 1]?.sequence ?? scanned.nextPosition;
next = following === null ? null : { position: following, byteOffset: null };
break;
}
if (fragments.length === request.maxMessages || rawBytes === request.maxBytes) {
const following = scanned.records[recordIndex + 1]?.sequence ?? scanned.nextPosition;
next = following === null ? null : { position: following, byteOffset: null };
break;
}
}
if (next !== null) break;
if (scanned.nextPosition === null) break;
if (scanned.nextPosition === scanPosition) {
throw new Error('Session transcript projection scan did not advance');
}
scanPosition = scanned.nextPosition;
}
return {
throughSequence,
fragments,
rawBytes,
next,
};
}
function projectEncodedSharedMessage(bytes: Buffer, sessionId: string): Buffer[] {
const projected = projectSharedSessionTranscriptMessage(
JSON.parse(bytes.toString('utf8')),
sessionId,
);
return projected ? [Buffer.from(JSON.stringify(projected), 'utf8')] : [];
}
export function updateSubscriberTranscriptHighWater(
state: SubscriberTranscriptState,
throughSequence: number | null,
): boolean {
if (throughSequence === null || throughSequence === state.durableThroughSequence) return false;
if (state.durableThroughSequence !== null && throughSequence < state.durableThroughSequence) {
throw new Error('Session transcript durable watermark moved backwards');
}
state.durableThroughSequence = throughSequence;
return true;
}
export class TranscriptPageRequestError extends Error {
readonly name = 'TranscriptPageRequestError';
}
function resolvePosition(
state: SubscriberTranscriptState,
request: SessionTranscriptPageInput,
): {
position: number;
byteOffset: number | null;
rangeBoundarySequence: number | null;
} | null {
if (request.cursor !== null) {
const cursor = decodeCursor(request.cursor, state.cursorSecret);
if (
cursor.subscriptionId !== state.subscriptionId ||
cursor.sessionId !== state.sessionId ||
cursor.source !== request.source ||
cursor.direction !== request.direction ||
cursor.throughSequence !== request.throughSequence
) {
throw new TranscriptPageRequestError('Transcript cursor does not match request');
}
return {
position: cursor.position,
byteOffset: cursor.byteOffset,
rangeBoundarySequence: cursor.rangeBoundarySequence,
};
}
if (request.source === 'overlay') {
const overlayMessages = state.overlayMessages;
if (overlayMessages === undefined) return null;
const anchor = request.anchorSequence;
const position =
request.direction === 'older' ? (anchor ?? overlayMessages.length) - 1 : (anchor ?? -1) + 1;
return position < 0 || position >= overlayMessages.length
? null
: { position, byteOffset: null, rangeBoundarySequence: null };
}
if (request.throughSequence === null) return null;
const position =
request.direction === 'older'
? (request.anchorSequence ?? request.throughSequence + 1) - 1
: (request.anchorSequence ?? -1) + 1;
return position < 0 || position > request.throughSequence
? null
: { position, byteOffset: null, rangeBoundarySequence: null };
}
function continuationMessageLimit(position: {
position: number;
rangeBoundarySequence: number | null;
}): number {
return position.rangeBoundarySequence === null
? SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES
: Math.min(
SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES,
Math.abs(position.position - position.rangeBoundarySequence) + 1,
);
}
function storageSelection(
storage: Awaited<ReturnType<SessionTranscriptReader['readDurablePage']>>,
): SelectedFragments {
return {
fragments: storage.fragments.map((fragment) => ({
kind: 'durable' as const,
sequence: fragment.sequence,
byteOffset: fragment.byteOffset,
totalBytes: fragment.totalBytes,
payloadDigest: fragment.payloadDigest,
data: fragment.data.toString('base64'),
})),
rawBytes: storage.rawBytes,
next: storage.next,
};
}
function selectionThroughRangeBoundary(
selected: SelectedFragments,
direction: SessionTranscriptPageDirection,
rangeBoundarySequence: number | null,
): SelectedFragments {
if (rangeBoundarySequence === null) return selected;
// RuntimeEvent-backed message sequences are sparse, so a continuation's
// message limit cannot infer how many records remain from sequence distance.
const firstOmittedIndex = selected.fragments.findIndex(
(fragment) =>
fragment.kind === 'durable' &&
(direction === 'older'
? fragment.sequence < rangeBoundarySequence
: fragment.sequence > rangeBoundarySequence),
);
if (firstOmittedIndex === -1) return selected;
const firstOmitted = selected.fragments[firstOmittedIndex]!;
if (firstOmitted.kind !== 'durable') {
throw new Error('Session transcript durable range contained an overlay fragment');
}
const fragments = selected.fragments.slice(0, firstOmittedIndex);
return {
fragments,
rawBytes: fragments.reduce(
(sum, fragment) => sum + Buffer.from(fragment.data, 'base64').byteLength,
0,
),
next: { position: firstOmitted.sequence, byteOffset: null },
};
}
function selectOverlay(
messages: readonly Buffer[],
direction: SessionTranscriptPageDirection,
position: number,
byteOffset: number | null,
maxBytes: number,
maxMessages = SESSION_TRANSCRIPT_PAGE_MAX_MESSAGES,
): SelectedFragments {
const fragments: SessionTranscriptFragment[] = [];
let rawBytes = 0;
let index = position;
let offset = byteOffset;
while (
index >= 0 &&
index < messages.length &&
rawBytes < maxBytes &&
fragments.length < maxMessages
) {
const message = messages[index]!;
const selected = selectBuffer(message, direction, offset, maxBytes - rawBytes);
if (!selected) break;
fragments.push({
kind: 'overlay',
messageIndex: index,
byteOffset: selected.byteOffset,
totalBytes: message.byteLength,
data: selected.data.toString('base64'),
});
rawBytes += selected.data.byteLength;
if (!selected.complete) {
return {
fragments,
rawBytes,
next: { position: index, byteOffset: selected.nextOffset },
};
}
index += direction === 'older' ? -1 : 1;
offset = null;
}
return {
fragments,
rawBytes,
next: index >= 0 && index < messages.length ? { position: index, byteOffset: null } : null,
};
}
function selectBuffer(
bytes: Buffer,
direction: SessionTranscriptPageDirection,
byteOffset: number | null,
budget: number,
): {
byteOffset: number;
data: Buffer;
complete: boolean;
nextOffset: number;
} | null {
if (budget < 1) return null;
if (direction === 'older') {
const end = byteOffset ?? bytes.byteLength;
if (end < 1 || end > bytes.byteLength)
throw new TranscriptPageRequestError('Invalid cursor byte offset');
const start = Math.max(0, end - budget);
return {
byteOffset: start,
data: bytes.subarray(start, end),
complete: start === 0,
nextOffset: start,
};
}
const start = byteOffset ?? 0;
if (start < 0 || start >= bytes.byteLength) {
throw new TranscriptPageRequestError('Invalid cursor byte offset');
}
const end = Math.min(bytes.byteLength, start + budget);
return {
byteOffset: start,
data: bytes.subarray(start, end),
complete: end === bytes.byteLength,
nextOffset: end,
};
}
function pageFromSelection(
state: SubscriberTranscriptState,
source: SessionTranscriptPageSource,
direction: SessionTranscriptPageDirection,
selected: SelectedFragments,
throughSequence: number | null = state.openedThroughSequence,
rangeBoundarySequence: number | null = null,
protectedTurnSequence: number | null = null,
): SessionTranscriptPage {
const cursorRangeBoundarySequence =
selected.next !== null &&
rangeBoundarySequence !== null &&
(direction === 'older'
? selected.next.position < rangeBoundarySequence
: selected.next.position > rangeBoundarySequence)
? null
: rangeBoundarySequence;
return {
kind: 'page',
sessionId: state.sessionId,
source,
direction,
throughSequence,
rawBytes: selected.rawBytes,
fragments: selected.fragments,
rangeBoundarySequence,
protectedTurnSequence,
nextCursor: selected.next
? encodeCursor(
{
version: 1,
subscriptionId: state.subscriptionId,
sessionId: state.sessionId,
source,
direction,
throughSequence,
rangeBoundarySequence: cursorRangeBoundarySequence,
...selected.next,
},
state.cursorSecret,
)
: null,
};
}
function emptyPage(
state: SubscriberTranscriptState,
request: SessionTranscriptPageInput,
): SessionTranscriptPage {
return {
kind: 'page',
sessionId: state.sessionId,
source: request.source,
direction: request.direction,
throughSequence: request.throughSequence,
rawBytes: 0,
fragments: [],
rangeBoundarySequence: null,
protectedTurnSequence: null,
nextCursor: null,
};
}
function encodeCursor(cursor: TranscriptCursorState, secret: Buffer): string {
const payload = Buffer.from(JSON.stringify(cursor), 'utf8').toString('base64url');
return `${payload}.${signCursor(payload, secret).toString('base64url')}`;
}
function decodeCursor(value: string, secret: Buffer): TranscriptCursorState {
let decoded: unknown;
try {
const parts = value.split('.');
if (parts.length !== 2) throw new Error('invalid cursor envelope');
const [payload, signatureValue] = parts as [string, string];
const bytes = Buffer.from(payload, 'base64url');
const signature = Buffer.from(signatureValue, 'base64url');
const expected = signCursor(payload, secret);
if (
bytes.toString('base64url') !== payload ||
signature.toString('base64url') !== signatureValue ||
signature.byteLength !== expected.byteLength ||
!timingSafeEqual(signature, expected)
) {
throw new Error('invalid cursor signature');
}
decoded = JSON.parse(bytes.toString('utf8')) as unknown;
} catch (cause) {
throw new TranscriptPageRequestError('Invalid transcript cursor', { cause });
}
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded)) {
throw new TranscriptPageRequestError('Invalid transcript cursor');
}
const cursor = decoded as Record<string, unknown>;
const keys = [
'version',
'subscriptionId',
'sessionId',
'source',
'direction',
'throughSequence',
'position',
'byteOffset',
'rangeBoundarySequence',
];
if (
Object.keys(cursor).length !== keys.length ||
keys.some((key) => !Object.hasOwn(cursor, key))
) {
throw new TranscriptPageRequestError('Invalid transcript cursor fields');
}
if (
cursor.version !== 1 ||
typeof cursor.subscriptionId !== 'string' ||
typeof cursor.sessionId !== 'string' ||
(cursor.source !== 'durable' && cursor.source !== 'overlay') ||
(cursor.direction !== 'older' && cursor.direction !== 'newer') ||
(cursor.throughSequence !== null && !isCount(cursor.throughSequence)) ||
!isCount(cursor.position) ||
(cursor.byteOffset !== null && !isCount(cursor.byteOffset)) ||
(cursor.rangeBoundarySequence !== null && !isCount(cursor.rangeBoundarySequence))
) {
throw new TranscriptPageRequestError('Invalid transcript cursor values');
}
return cursor as unknown as TranscriptCursorState;
}
function signCursor(payload: string, secret: Buffer): Buffer {
return createHmac('sha256', secret).update(payload, 'utf8').digest();
}
function mergeActiveAssistantStreams(
overlay: readonly StoredMessage[],
prefixes: Iterable<ActiveTranscriptAssistantStream>,
durable: readonly StoredMessage[],
): StoredMessage[] {
const merged = [...overlay];
const indices = new Map(merged.map((message, index) => [message.id, index]));
const durableById = new Map<string, StoredMessage>();
for (const message of durable) durableById.set(message.id, message);
for (const prefix of prefixes) {
let index = indices.get(prefix.messageId);
const durableMessage = durableById.get(prefix.messageId);
if (index === undefined) {
if (!durableMessage) {
throw new Error('Active assistant prefix has no matching transcript message');
}
index = merged.length;
indices.set(prefix.messageId, index);
merged.push(durableMessage);
} else if (durableMessage) {
const projected = merged[index];
if (projected?.type !== 'assistant' || durableMessage.type !== 'assistant') {
throw new Error('Active assistant prefix has no matching transcript message');
}
merged[index] = reconcileAssistantMessage(durableMessage, projected);
}
const message = merged[index];
if (message?.type !== 'assistant' || message.turnId !== prefix.turnId) {
throw new Error('Active assistant prefix has no matching transcript message');
}
if (prefix.kind === 'text') {
merged[index] = { ...message, text: reconcileAssistantText(message.text, prefix.text) };
continue;
}
if (!message.thinking) {
throw new Error('Active thinking prefix has no matching transcript content');
}
merged[index] = {
...message,
thinking: {
...message.thinking,
text: reconcileAssistantText(message.thinking.text, prefix.text),
},
};
}
return merged;
}
function assertOverlayRetainedBound(messages: readonly Buffer[]): void {
if (messages.length > ACTIVE_TRANSCRIPT_OVERLAY_MAX_MESSAGES) {
throw new Error('Active Session transcript overlay exceeds its message limit');
}
let retainedBytes = 0;
for (const message of messages) {
retainedBytes += message.byteLength;
if (retainedBytes > ACTIVE_TRANSCRIPT_OVERLAY_MAX_BYTES) {
throw new Error('Active Session transcript overlay exceeds its byte limit');
}
}
}
function reconcileAssistantMessage(
durable: Extract<StoredMessage, { type: 'assistant' }>,
projected: Extract<StoredMessage, { type: 'assistant' }>,
): Extract<StoredMessage, { type: 'assistant' }> {
const thinking =
durable.thinking && projected.thinking
? {
...projected.thinking,
text: reconcileAssistantText(durable.thinking.text, projected.thinking.text),
}
: (projected.thinking ?? durable.thinking);
return {
...projected,
text: reconcileAssistantText(durable.text, projected.text),
...(thinking ? { thinking } : {}),
};
}
function reconcileAssistantText(projected: string, active: string): string {
if (active.startsWith(projected)) return active;
if (projected.startsWith(active)) return projected;
return projected;
}
function isCount(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}