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
21 changes: 21 additions & 0 deletions packages/core/src/__tests__/session-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { SESSION_STATUSES, isSessionStatus } from '../session.js';

describe('session status contract', () => {
it('accepts only the current ordered session status collection', () => {
assert.deepEqual(SESSION_STATUSES, [
'active',
'running',
'waiting_for_user',
'blocked',
'archived',
'aborted',
]);
for (const status of SESSION_STATUSES) {
assert.equal(isSessionStatus(status), true);
}
assert.equal(isSessionStatus('review'), false);
assert.equal(isSessionStatus('done'), false);
});
});
10 changes: 0 additions & 10 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,12 @@ export { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from './explore-ag
* alongside `isArchived`; consolidating those two onto one authority is its own
* change (#2984, PR 3) because it rewrites stored rows.
*
* `review` and `done` have no writer in current source, but they stay: this
* list is read back out of storage, and narrowing it is a data migration, not a
* cleanup. `resolveLegacyStatus` in the JSONL importer (removed in #2656) let
* both values through into real SQLite stores verbatim, and `normalizeSession
* Header` throws on an unrecognised status for the WHOLE header — so one stored
* row carrying `done` fails an entire catalog page, not just its own row.
* Removing them needs a schema migration or a tolerant read, which is its own
* change with its own review.
*/
export const SESSION_STATUSES = [
'active',
'running',
'waiting_for_user',
'blocked',
'review',
'done',
'archived',
'aborted',
] as const;
Expand Down
25 changes: 25 additions & 0 deletions packages/runtime-host/src/__tests__/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,27 @@ describe('Runtime Host bootstrap protocol', () => {
assert.throws(() => decodeHostFrame(oversized), isInvalidFrame);
});

test('normalizes legacy Session statuses in continuity snapshots', () => {
for (const status of ['review', 'done']) {
const decoded = decodeSessionContinuitySnapshot({
...continuitySnapshot('epoch-1'),
session: { ...continuitySnapshot('epoch-1').session, status },
});
assert.equal(decoded.session.status, 'active');
}
});

test('rejects unknown Session statuses in continuity snapshots', () => {
assert.throws(
() =>
decodeSessionContinuitySnapshot({
...continuitySnapshot('epoch-1'),
session: { ...continuitySnapshot('epoch-1').session, status: 'unknown' },
}),
isInvalidSessionStatus,
);
});

test('decodes only privacy-normalized bounded subscription live frames', () => {
const envelope = {
kind: 'subscription.session_event' as const,
Expand Down Expand Up @@ -1308,6 +1329,10 @@ function isInvalidFrame(error: unknown): boolean {
return error instanceof RuntimeHostProtocolError && error.code === 'invalid_frame';
}

function isInvalidSessionStatus(error: unknown): boolean {
return error instanceof RuntimeHostProtocolError && error.message === 'Invalid Session status';
}

function queuedMessage(
text = 'adjust this turn',
placement: 'current_turn' | 'next_turn' = 'current_turn',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,21 @@ describe('Session catalog protocol', () => {
);
});

test('normalizes legacy Session statuses in catalog projections', () => {
for (const status of ['review', 'done']) {
const decoded = decodeSessionCatalogItem({ ...projection(), status });
if ('kind' in decoded) assert.fail('Expected a Session catalog projection');
assert.equal(decoded.status, 'active');
}
});

test('rejects unknown Session statuses in catalog projections', () => {
assert.throws(
() => decodeSessionCatalogItem({ ...projection(), status: 'unknown' }),
isInvalidSessionStatus,
);
});

test('bounds pages and preserves revision-pinned continuation results', () => {
const sessions = Array.from({ length: SESSION_CATALOG_PAGE_MAX_ITEMS }, (_, index) =>
projection({ id: `session-${index}` }),
Expand Down Expand Up @@ -425,3 +440,7 @@ function projection(overrides: Partial<SessionCatalogProjection> = {}): SessionC
function isProtocolError(error: unknown): boolean {
return error instanceof RuntimeHostProtocolError;
}

function isInvalidSessionStatus(error: unknown): boolean {
return error instanceof RuntimeHostProtocolError && error.message === 'Invalid Session status';
}
9 changes: 2 additions & 7 deletions packages/runtime-host/src/protocol/session-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { isPermissionMode, type PermissionMode } from '@maka/core/permission';
import { isSessionStartMode, type SessionStartMode } from '@maka/core/explore-agent';
import {
isSessionBlockedReason,
isSessionStatus,
isSessionToolProfile,
type SessionBlockedReason,
type SessionStatus,
Expand All @@ -26,6 +25,7 @@ import {
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineHostPathOperation, defineOperation } from './operation-spec.js';
import { decodeSessionStatus } from './session-status.js';
import {
decodeWorkspaceProjection,
decodeWorkspaceTarget,
Expand Down Expand Up @@ -644,7 +644,7 @@ export function decodeSessionCatalogProjection(value: unknown): SessionCatalogPr
...optionalEntityId(record, 'lastReadMessageId'),
...optionalTimestamp(record, 'lastMessageAt'),
...optionalText(record, 'lastMessagePreview', SESSION_CATALOG_PREVIEW_MAX_BYTES),
status: sessionStatus(record.status),
status: decodeSessionStatus(record.status),
...optionalBlockedReason(record),
...optionalTimestamp(record, 'statusUpdatedAt'),
...optionalEntityId(record, 'parentSessionId'),
Expand Down Expand Up @@ -867,11 +867,6 @@ function optionalThinkingLevel(
return { thinkingLevel: thinkingLevel(record.thinkingLevel) };
}

function sessionStatus(value: unknown): SessionStatus {
if (!isSessionStatus(value)) throw invalidProtocolFrame('Invalid Session status');
return value;
}

function backend(value: unknown): SessionCatalogProjection['backend'] {
if (value !== 'ai-sdk' && value !== 'fake') {
throw invalidProtocolFrame('Invalid Session backend');
Expand Down
10 changes: 3 additions & 7 deletions packages/runtime-host/src/protocol/session-continuity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { TOOL_ACTIVITY_KINDS, TOOL_OUTPUT_DELTA_MAX_CHARS } from '@maka/core/eve
import type { ToolResultPreviewContent } from '@maka/core/events';
import { decodeToolResultPreviewContent } from '@maka/core/tool-result-preview';
import type { ToolActivityKind } from '@maka/core/events';
import { isSessionStatus, type SessionStatus } from '@maka/core/session';
import type { SessionStatus } from '@maka/core/session';
import {
assertExactKeys,
requireCount,
Expand All @@ -12,6 +12,7 @@ import {
requireRecord,
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { decodeSessionStatus } from './session-status.js';
import {
decodeSessionInteractionProjection,
type SessionInteractionProjection,
Expand Down Expand Up @@ -883,7 +884,7 @@ function decodeSessionContinuityIdentity(value: unknown): SessionContinuityIdent
return {
sessionId: requireEntityId(record.sessionId, 'sessionId'),
metadataRevision: requirePositiveCount(record.metadataRevision, 'metadataRevision'),
status: requireSessionLifecycleStatus(record.status),
status: decodeSessionStatus(record.status),
createdAt: requireCount(record.createdAt, 'createdAt'),
lastUsedAt: requireCount(record.lastUsedAt, 'lastUsedAt'),
isArchived: record.isArchived,
Expand Down Expand Up @@ -963,11 +964,6 @@ function requireToolActivityKind(value: unknown): ToolActivityKind {
throw invalidProtocolFrame('Invalid Session tool activity kind');
}

function requireSessionLifecycleStatus(value: unknown): SessionLifecycleStatus {
if (isSessionStatus(value)) return value;
throw invalidProtocolFrame('Invalid Session lifecycle status');
}

function requireAgentGraphChangedReason(value: unknown): AgentGraphChangedReason {
if (
value === 'observation' ||
Expand Down
13 changes: 13 additions & 0 deletions packages/runtime-host/src/protocol/session-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { isSessionStatus, type SessionStatus } from '@maka/core/session';
import { invalidProtocolFrame } from './errors.js';

/**
* Accept status values written by legacy Runtime Hosts without expanding the
* current wire status shape. This is decode-only compatibility: all outgoing
* values remain current SessionStatus values.
*/
export function decodeSessionStatus(value: unknown): SessionStatus {
const normalized = value === 'review' || value === 'done' ? 'active' : value;
if (!isSessionStatus(normalized)) throw invalidProtocolFrame('Invalid Session status');
return normalized;
}
Loading
Loading