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 @@ -45,32 +45,30 @@ test('restarts a paginated catalog read instead of mixing revisions', async () =
]);

assert.deepEqual(
(await client.listSessions({ isArchived: false })).map(({ id }) => id),
(await client.listSessions()).map(({ id }) => id),
['fresh-1', 'fresh-2'],
);
assert.deepEqual(requests, [
{
operation: 'session.catalog.query',
input: { kind: 'list_start', filter: { isArchived: false } },
input: { kind: 'list_start' },
},
{
operation: 'session.catalog.query',
input: {
kind: 'list_continue',
filter: { isArchived: false },
revision: revisionOne,
cursor: 'stale-cursor',
},
},
{
operation: 'session.catalog.query',
input: { kind: 'list_start', filter: { isArchived: false } },
input: { kind: 'list_start' },
},
{
operation: 'session.catalog.query',
input: {
kind: 'list_continue',
filter: { isArchived: false },
revision: revisionTwo,
cursor: 'fresh-cursor',
},
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ test('drives the renderer Session catalog facade through real UDS framing', asyn

const created = await ipc.invoke('sessions:create', undefined);
assert.deepEqual((await ipc.invoke('sessions:list')) as unknown[], [created]);
for (const staleFilter of [
{ isArchived: false },
{ isFlagged: true },
{ labelSlug: 'paged' },
]) {
await assert.rejects(
ipc.invoke('sessions:list', staleFilter),
/Invalid Session list filter/,
);
}
assert.equal(
(await ipc.invoke('sessions:setPermissionMode', 'session-ipc', 'execute') as {
permissionMode: string;
Expand Down
7 changes: 2 additions & 5 deletions apps/desktop/src/main/runtime-host-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ import {
type ProjectDirectoryRoot,
type QueueRetractInput,
type QueueRetractResult,
type SessionCatalogFilter,
SESSION_TRANSCRIPT_BOOTSTRAP_MAX_BYTES,
type SessionCatalogChangedFrame,
type ScheduledTaskChangedFrame,
Expand Down Expand Up @@ -529,12 +528,10 @@ export class DesktopRuntimeHostClient {
}
}

async listSessions(
filter?: SessionCatalogFilter,
): Promise<SessionCatalogProjection[]> {
async listSessions(): Promise<SessionCatalogProjection[]> {
this.#assertOpen();
try {
return (await readRuntimeHostSessions(this.connection, filter)).map(requireSessionProjection);
return (await readRuntimeHostSessions(this.connection)).map(requireSessionProjection);
} catch (error) {
if (error instanceof DesktopRuntimeHostClientError) throw error;
if (!(error instanceof RuntimeHostCatalogReadError)) throw error;
Expand Down
37 changes: 23 additions & 14 deletions apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { isThinkingLevel } from '@maka/core/model-thinking';
import { type CreateSessionRequestInput, type SessionListFilter } from '@maka/core/runtime-inputs';
import { type SessionChangedEvent, type SessionChangedReason, type SessionSummary } from '@maka/core/session';
import type {
SessionCatalogFilter,
SessionCatalogProjection,
SessionCreateInput,
WorkspaceTarget,
Expand Down Expand Up @@ -70,7 +69,7 @@ export function registerRuntimeHostSessionCatalogIpc(
const listSessions = async (filter?: SessionListFilter): Promise<DesktopHostSessionSummary[]> => {
await recoveryTask;
const parentSessionId = normalizeParentSessionFilter(filter?.subagentParentSessionId);
const sessions = await deps.client.listSessions(toHostCatalogFilter(filter));
const sessions = await deps.client.listSessions();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return sessions
.filter((session) => !pendingCleanup.has(session.id))
.filter((session) =>
Expand All @@ -81,8 +80,8 @@ export function registerRuntimeHostSessionCatalogIpc(
const actionIds = (sessionId: string, options: unknown) =>
resolveSessionActionIds(() => listSessions(), sessionId, options);

handleReconnectableRead(ipcMain, 'sessions:list', (_event, filter?: SessionListFilter) =>
listSessions(filter),
handleReconnectableRead(ipcMain, 'sessions:list', (_event, filter?: unknown) =>
listSessions(normalizeSessionListFilter(filter)),
);
ipcMain.handle('sessions:cleanupSessionCopy', async (_event, sessionId: string) => {
await deps.sessionCopyCleanup.cleanup(sessionId);
Expand Down Expand Up @@ -245,16 +244,6 @@ async function updateConfiguration(
return toDesktopHostSessionSummary(session);
}

function toHostCatalogFilter(filter: SessionListFilter | undefined): SessionCatalogFilter | undefined {
if (!filter) return undefined;
const result: SessionCatalogFilter = {
...(filter.isArchived === undefined ? {} : { isArchived: filter.isArchived }),
...(filter.isFlagged === undefined ? {} : { isFlagged: filter.isFlagged }),
...(filter.labelSlug === undefined ? {} : { labelSlug: filter.labelSlug }),
};
return Object.keys(result).length === 0 ? undefined : result;
}

function normalizeParentSessionFilter(value: unknown): string | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'string' || value.length === 0) {
Expand All @@ -263,6 +252,26 @@ function normalizeParentSessionFilter(value: unknown): string | undefined {
return value;
}

function normalizeSessionListFilter(value: unknown): SessionListFilter | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error('Invalid Session list filter');
}
const record = value as Record<string, unknown>;
if (Object.keys(record).some((key) => key !== 'subagentParentSessionId')) {
throw new Error('Invalid Session list filter keys');
}
return {
...(record.subagentParentSessionId === undefined
? {}
: {
subagentParentSessionId: normalizeParentSessionFilter(
record.subagentParentSessionId,
),
}),
};
}

function normalizeModelTarget(input: CreateSessionRequestInput | undefined): SessionModelTarget {
const slug = normalizeOptionalString(input?.llmConnectionSlug, 'model connection');
const model = normalizeOptionalString(input?.model, 'model');
Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/runtime-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,6 @@ export interface ReviseBeforeTurnInput {
}

export interface SessionListFilter {
isArchived?: boolean;
isFlagged?: boolean;
labelSlug?: string;
/** Return linked subagent sessions owned by this parent session. */
subagentParentSessionId?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,32 @@ test('catalog paging stops before the encoded 48 KiB result boundary', async ()
);
});

test('rejects a legacy cursor that carries a Session catalog filter', async () => {
const fixture = createFixture();
const cursor = Buffer.from(
JSON.stringify({
version: 1,
activityAt: 1,
sessionId: 'session-1',
filter: { isArchived: false },
}),
'utf8',
).toString('base64url');

const outcome = await fixture.coordinator.handlers['session.catalog.query'](
{
kind: 'list_continue',
revision: 'sha256:test',
cursor,
},
context,
);

assert.equal(outcome.ok, false);
if (outcome.ok) assert.fail('Legacy filtered cursor must be rejected');
assert.equal(outcome.error.code, 'invalid_request');
});

function createFixture(
options: {
readonly labels?: readonly string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,54 @@ describe('Session catalog protocol', () => {
);
});

test('accepts only filter-free Session catalog list inputs', () => {
const revision = `sha256:${'a'.repeat(64)}` as const;
assert.deepEqual(
decodeClientFrame({
requestId: 'request-list',
operation: 'session.catalog.query',
input: { kind: 'list_start' },
}),
{
requestId: 'request-list',
operation: 'session.catalog.query',
input: { kind: 'list_start' },
},
);
assert.deepEqual(
decodeClientFrame({
requestId: 'request-continue',
operation: 'session.catalog.query',
input: { kind: 'list_continue', revision, cursor: 'cursor-1' },
}),
{
requestId: 'request-continue',
operation: 'session.catalog.query',
input: { kind: 'list_continue', revision, cursor: 'cursor-1' },
},
);
for (const filter of [{ isArchived: false }, { isFlagged: true }, { labelSlug: 'paged' }]) {
assert.throws(
() =>
decodeClientFrame({
requestId: 'request-reject',
operation: 'session.catalog.query',
input: { kind: 'list_start', filter },
}),
isProtocolError,
);
assert.throws(
() =>
decodeClientFrame({
requestId: 'request-reject',
operation: 'session.catalog.query',
input: { kind: 'list_continue', revision, cursor: 'cursor-1', filter },
}),
isProtocolError,
);
}
});

test('accepts the complete 80-code-point Session name range', () => {
const name = '🦊'.repeat(80);
const decoded = decodeClientFrame({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,41 +434,6 @@ test('two Clients share stable Session creation, CAS configuration, and catalog
bulk.length + catalogBeforeBulk.sessions.length,
);

const filteredStart = await desktop.request('session.catalog.query', {
kind: 'list_start',
filter: { labelSlug: 'paged' },
});
assert.equal(filteredStart.kind, 'page');
if (filteredStart.kind !== 'page' || !filteredStart.nextCursor) {
assert.fail('Filtered Session catalog must provide a continuation');
}
assert.equal(filteredStart.sessions.length, 32);
const filteredContinuation = await tui.request('session.catalog.query', {
kind: 'list_continue',
revision: filteredStart.revision,
cursor: filteredStart.nextCursor,
});
assert.equal(filteredContinuation.kind, 'page');
if (filteredContinuation.kind !== 'page') {
assert.fail('Filtered Session catalog continuation must return a page');
}
assert.equal(filteredContinuation.sessions.length, 2);
assert.equal(
[...filteredStart.sessions, ...filteredContinuation.sessions].every((session) =>
requireSessionProjection(session).labels.includes('paged'),
),
true,
);
await assert.rejects(
desktop.request('session.catalog.query', {
kind: 'list_continue',
filter: { isFlagged: true },
revision: filteredStart.revision,
cursor: filteredStart.nextCursor,
}),
operationError('invalid_request'),
);

const staleStart = await desktop.request('session.catalog.query', {
kind: 'list_start',
});
Expand All @@ -489,16 +454,6 @@ test('two Clients share stable Session creation, CAS configuration, and catalog
cursor: staleStart.nextCursor,
});
assert.equal(staleContinuation.kind, 'revision_changed');
const flagged = await tui.request('session.catalog.query', {
kind: 'list_start',
filter: { isFlagged: true },
});
assert.equal(flagged.kind, 'page');
if (flagged.kind !== 'page') assert.fail('Flagged Session query must return a page');
assert.deepEqual(
flagged.sessions.map((session) => session.id).sort(),
[created.id, bulkSession.id, oversizedSessionId].sort(),
);

await subscription.close();
const retirementSubscription = await tui.openSessionSubscription({
Expand Down
4 changes: 0 additions & 4 deletions packages/runtime-host/src/client/catalog-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
type ConnectionCatalogQueryResult,
type RelayModelProfile,
type RelayModelProfiles,
type SessionCatalogFilter,
type SessionCatalogItem,
type SkillCatalogWorkspaceContext,
type SkillCatalogInvocableItem,
Expand Down Expand Up @@ -165,14 +164,12 @@ export async function readRuntimeHostInvocableSkills(

export async function readRuntimeHostSessions(
connection: RuntimeHostCatalogConnection,
filter?: SessionCatalogFilter,
): Promise<SessionCatalogItem[]> {
const { pages } = await collectStablePages(
'session',
async () => {
const result = await connection.request('session.catalog.query', {
kind: 'list_start',
...(filter ? { filter } : {}),
});
return result.kind === 'page' ? result : null;
},
Expand All @@ -181,7 +178,6 @@ export async function readRuntimeHostSessions(
kind: 'list_continue',
revision,
cursor,
...(filter ? { filter } : {}),
});
return result.kind === 'page' ? result : null;
},
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-host/src/protocol/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 23 as const;
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 24 as const;
// Transcript pages amortize storage and network round trips with a 512 KiB raw
// payload. Base64 expansion plus the bounded fragment envelope must still fit in
// one transport message; narrower domains retain their own encoded limits.
Expand Down
Loading
Loading