Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ test('keeps a bounded contiguous window while moving between history and the tai
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 4,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrapPage,
overlay: { ...page(null), source: 'overlay' },
Expand Down Expand Up @@ -294,6 +295,7 @@ test('delivers a mid-session tail append even while a history window is resident
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 4,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrapPage,
overlay: { ...page(null), source: 'overlay' },
Expand Down Expand Up @@ -364,6 +366,7 @@ test('does not resurrect a discarded replica when a tail re-anchor is in flight'
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 4,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrapPage,
overlay: { ...page(null), source: 'overlay' },
Expand Down Expand Up @@ -446,6 +449,7 @@ test('does not resurrect a discarded replica when a history load is in flight',
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 4,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrapPage,
overlay: { ...page(null), source: 'overlay' },
Expand Down Expand Up @@ -518,6 +522,7 @@ test('does not drive a discarded replica terminal when a contiguous catch-up is
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 4,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrapPage,
overlay: { ...page(null, 4), source: 'overlay' },
Expand Down Expand Up @@ -572,6 +577,7 @@ test('loads a history target with newer messages available below it', async () =
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 4,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrapPage,
overlay: { ...transcriptPage('older', null, 4), source: 'overlay' },
Expand Down Expand Up @@ -624,6 +630,7 @@ test('keeps an oversized transcript sparse while moving between indexed prompts'
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 15,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrapPage,
overlay: { ...transcriptPage('older', null, 15), source: 'overlay' },
Expand Down Expand Up @@ -727,6 +734,7 @@ test('keeps history resident when an active overlay uses its own cache budget',
events: { async *[Symbol.asyncIterator]() {} },
transcriptBootstrap: {
throughSequence: 1,
durableCoverage: 'complete',
overlayMessageCount: 1,
durable: bootstrapPage,
overlay: { ...transcriptPage('older', null, 1), source: 'overlay' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ function subscription(
activeAssistantStreams: [],
transcriptBootstrap: {
throughSequence: null,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: emptyTranscriptPage(sessionId, 'durable'),
overlay: emptyTranscriptPage(sessionId, 'overlay'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ test('fences transcript range failures across same-source replica recovery', asy
events,
transcriptBootstrap: {
throughSequence: 1,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: bootstrap,
overlay: { ...bootstrap, source: 'overlay', nextCursor: null },
Expand Down Expand Up @@ -887,6 +888,7 @@ test('finishes transcript open and replays a stale range request after replaceme
events,
transcriptBootstrap: {
throughSequence: 0,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: {
kind: 'page',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function runtimeHostSessionFixture(input: {
activeAssistantStreams: input.activeAssistantStreams ?? [],
transcriptBootstrap: input.transcriptBootstrap ?? {
throughSequence: null,
durableCoverage: 'complete',
overlayMessageCount: 0,
durable: emptyPage(sessionId, 'durable'),
overlay: emptyPage(sessionId, 'overlay'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,13 @@ export class RuntimeHostSessionSubscriptionOwner {
}

function subscriptionClosedError(
reason: "slow_consumer" | "session_removed",
reason: "access_revoked" | "slow_consumer" | "session_removed",
): Error {
return reason === "session_removed"
return reason !== "slow_consumer"
? new SessionRemovedSubscriptionError(
"Runtime Host Session was removed while it was observed",
reason === "access_revoked"
? "Runtime Host Session access was revoked"
: "Runtime Host Session was removed while it was observed",
)
: new RuntimeHostSubscriptionError(
"slow_consumer",
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,17 @@ export function userFacingText(message: Pick<UserMessage, 'text' | 'displayText'
return message.displayText ?? message.text;
}

const USER_VISIBLE_SESSION_SYSTEM_NOTES = new Set([
'context_compacted',
'context_compaction_failed_open',
'step_limit',
]);

/** Closed policy for system notes that are part of the user-visible transcript. */
export function isUserVisibleSessionSystemNote(kind: string): boolean {
return USER_VISIBLE_SESSION_SYSTEM_NOTES.has(kind);
}

export interface AssistantMessage {
type: 'assistant';
id: string;
Expand Down
112 changes: 112 additions & 0 deletions packages/runtime-host/src/__tests__/artifact-coordinator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,118 @@ test('Artifact query streams complete content in bounded ordered chunks', async
}
});

test('Session Guests can read only shared attachment Artifacts from their granted Session', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-artifact-shared-read-'));
const capability = await resolveStorageRoot({ path: root, kind: 'interactive' });
const owner = await tryAcquireInteractiveRootOwner(capability);
assert.ok(owner);
try {
const store = await openInteractiveArtifactStoreForWrite(owner.lease);
await store.recover();
await store.create({
id: 'shared-image',
sessionId: 'session-1',
turnId: 'turn-1',
name: 'shared.png',
kind: 'image',
mimeType: 'image/png',
source: 'user_upload',
content: Buffer.from('image'),
now: 1,
});
await store.create({
id: 'private-artifact',
sessionId: 'session-1',
turnId: 'turn-1',
name: 'private.txt',
kind: 'file',
source: 'provider_request_capture',
content: Buffer.from('private'),
now: 2,
});
let active = true;
let revokeAfterAuthorization = false;
const coordinator = new HostArtifactCoordinator(
store,
() => assert.fail('successful read must not request Host drain'),
new SessionAdmissionGate(),
{ probeSessionRemoval: async () => ({ kind: 'present' }) },
Date.now,
{
activeSessionGrant: () => {
if (!active) return;
if (revokeAfterAuthorization) {
revokeAfterAuthorization = false;
queueMicrotask(() => {
active = false;
});
}
return {
kind: 'session_observation',
grantId: 'grant-1',
principalId: 'guest-1',
sessionId: 'session-1',
createdAt: '2026-08-30T00:00:00.000Z',
};
},
},
);
const guest = {
...connectionContext,
principal: 'guest-1',
principalKind: 'session_guest' as const,
};

const visible = await coordinator.handlers['artifact.query'](
{ kind: 'get', sessionId: 'session-1', artifactId: 'shared-image' },
guest,
);
assert.equal(visible.ok && visible.result.kind === 'artifact', true);
assert.equal(
(
await coordinator.handlers['artifact.query'](
{ kind: 'get', sessionId: 'session-1', artifactId: 'private-artifact' },
guest,
)
).ok,
false,
);
assert.equal(
(
await coordinator.handlers['artifact.query'](
{ kind: 'list_start', sessionId: 'session-1' },
guest,
)
).ok,
false,
);

revokeAfterAuthorization = true;
assert.equal(
(
await coordinator.handlers['artifact.query'](
{ kind: 'get', sessionId: 'session-1', artifactId: 'shared-image' },
guest,
)
).ok,
false,
);
assert.equal(
(
await coordinator.handlers['artifact.query'](
{ kind: 'get', sessionId: 'session-1', artifactId: 'shared-image' },
guest,
)
).ok,
false,
);
store.close();
} finally {
await owner.close();
await rm(root, { recursive: true, force: true });
}
});

function digest(bytes: Uint8Array): `sha256:${string}` {
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
RUNTIME_HOST_PROTOCOL_VERSION,
SESSION_CATALOG_LIVE_RUN_STATE_SCHEMA_VERSION,
decodeCollaborationInvitationCode,
type RequestFrame,
} from '../protocol/index.js';
import { openRuntimeHostAccessAuthority } from '../server/access-authority.js';
Expand Down Expand Up @@ -70,6 +71,7 @@ test('one Local IPC owner and one authenticated WebSocket Client control the sam
});
let local: RuntimeHostConnection | undefined;
let remote: RuntimeHostConnection | undefined;
let guest: RuntimeHostConnection | undefined;
try {
local = requireConnection(await connectRuntimeHost({ rootPath: root, protocol: PROTOCOL }));
const issued = await local.request('access.credential.issue', {
Expand Down Expand Up @@ -217,6 +219,58 @@ test('one Local IPC owner and one authenticated WebSocket Client control the sam
modelTarget: { kind: 'default' },
});
assert.ok(!('kind' in created));
const preparedGuest = await local.request('collaboration.invitation.prepare', {
sessionId: 'shared-session',
grantKinds: ['session_observation'],
});
const guestInvitation = decodeCollaborationInvitationCode(preparedGuest.invitationCode);
const pendingGuest = await connectRemoteRuntimeHost({
url,
credential: guestInvitation.credential,
clientInstanceId: 'guest-client',
expectedRootId: capability.rootId,
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
protocol: PROTOCOL,
});
assert.equal(pendingGuest.kind, 'connected', JSON.stringify(pendingGuest));
if (pendingGuest.kind !== 'connected') assert.fail('Session Guest did not connect');
await pendingGuest.connection.request('access.credential.finalize', {});
await pendingGuest.connection.close();
const activeGuest = await connectRemoteRuntimeHost({
url,
credential: guestInvitation.credential,
clientInstanceId: 'guest-client',
expectedRootId: capability.rootId,
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
protocol: PROTOCOL,
});
assert.equal(activeGuest.kind, 'connected', JSON.stringify(activeGuest));
if (activeGuest.kind !== 'connected') assert.fail('Session Guest did not reconnect');
guest = activeGuest.connection;
const sharedCatalog = await guest.request('session.shared.query', {});
assert.equal(sharedCatalog.session?.id, 'shared-session');
assert.equal('workspace' in sharedCatalog.session!, false);
await assert.rejects(
guest.request('session.catalog.query', { kind: 'list_start' }),
(error: unknown) =>
error instanceof RuntimeHostOperationError && error.code === 'unauthorized',
);
const guestSubscription = await guest.openSessionSubscription({
sessionId: 'shared-session',
transcript: { kind: 'none' },
});
const observationGrant = preparedGuest.grants.find(
(grant) => grant.kind === 'session_observation',
)!;
await local.request('collaboration.grant.revoke', {
grantId: observationGrant.grantId,
});
const closed = await guestSubscription[Symbol.asyncIterator]().next();
assert.equal(closed.done, false);
assert.equal(closed.value?.kind, 'subscription.closed');
if (closed.value?.kind === 'subscription.closed') {
assert.equal(closed.value.reason, 'access_revoked');
}
assert.deepEqual(
await remote.request('session.catalog.query', {
kind: 'get',
Expand Down Expand Up @@ -409,7 +463,7 @@ test('one Local IPC owner and one authenticated WebSocket Client control the sam
{ credentialId: candidate.credentialId, revoked: true },
);
} finally {
await Promise.allSettled([remote?.close(), local?.close()]);
await Promise.allSettled([guest?.close(), remote?.close(), local?.close()]);
await host.close().catch(() => undefined);
await rm(join(resolveRootControlNamespace(), capability.rootId), {
recursive: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,7 @@ function transcriptBootstrapFor(sessionId: string) {
const contents = Buffer.from('t'.repeat(16 * 1024));
return {
throughSequence: 0,
durableCoverage: 'complete' as const,
overlayMessageCount: 0,
durable: {
kind: 'page' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,40 @@ export function transcriptReader(
}
return { throughSequence, fragments, rawBytes, next };
},
readDurableRecords: async (_sessionId, request) => {
const throughSequence =
request.throughSequence === undefined
? durable.length === 0
? null
: durable.length - 1
: request.throughSequence;
if (throughSequence === null) {
return { throughSequence: null, records: [], nextPosition: null };
}
const position = request.position ?? (request.direction === 'older' ? throughSequence : 0);
const candidates = durable
.map((message, sequence) => ({ sequence, message }))
.filter(
({ sequence }) =>
sequence <= throughSequence &&
(request.direction === 'older' ? sequence <= position : sequence >= position),
)
.sort((left, right) =>
request.direction === 'older'
? right.sequence - left.sequence
: left.sequence - right.sequence,
);
const records = candidates.slice(0, request.maxMessages);
const last = records.at(-1);
return {
throughSequence,
records,
nextPosition:
last && records.length < candidates.length
? last.sequence + (request.direction === 'older' ? -1 : 1)
: null,
};
},
readDurableMessagesById: async (_sessionId, request) =>
request.throughSequence === null
? []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5162,6 +5162,7 @@ function operationContext(
hostEpoch,
connectionId,
principal: 'local_os_user' as const,
principalKind: 'local_owner' as const,
acquireResidency,
};
}
Expand Down
Loading