Skip to content
Open
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
15 changes: 4 additions & 11 deletions packages/cli/src/pi-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
} from '@maka/core/events';
import {
deriveTurnRecords,
isRuntimeSystemNoteKind,
STEP_LIMIT_NOTICE_TEXT,
type StoredMessage,
type SystemNoteMessage,
Expand Down Expand Up @@ -1344,14 +1345,10 @@ function tokenDelta(before: number | undefined, after: number | undefined): numb
}

function systemNoteText(message: SystemNoteMessage): string | undefined {
// Retired kinds are still decoded off legacy transcript rows, and none of
// them ever had a line here worth reading.
if (!isRuntimeSystemNoteKind(message.kind)) return undefined;
switch (message.kind) {
case 'session_start':
case 'session_resume':
return undefined;
case 'mode_change':
return 'Permission mode changed.';
case 'model_change':
return 'Model changed.';
case 'context_compacted':
return 'Context compacted to keep this task within the model window.';
case 'context_compaction_failed_open':
Expand Down Expand Up @@ -1408,10 +1405,6 @@ function systemNoteText(message: SystemNoteMessage): string | undefined {
}
case 'step_limit':
return STEP_LIMIT_NOTICE_TEXT;
case 'error':
return 'Session recorded an error.';
case 'abort':
return 'Session was stopped.';
}
}

Expand Down
46 changes: 43 additions & 3 deletions packages/core/src/runtime-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ import {
type OrchestrationMode,
} from './orchestration.js';
import { isToolMode, type ToolMode } from './tool-mode.js';
import type { PersistedBackendKind } from './session.js';
import {
isRuntimeSystemNoteKind,
type PersistedBackendKind,
type RuntimeSystemNoteKind,
} from './session.js';
import { decodeTurnOrigin, type TurnOrigin } from './turn-origin.js';
import type { UserQuestionRequest } from './user-question.js';
import {
Expand Down Expand Up @@ -217,6 +221,22 @@ export interface RuntimeEventFunctionResponseContent {
modelProjection?: DurableToolResultProjection;
}

/**
* A note the runtime wrote about what happened during an invocation — context
* was compacted, the step cap was reached, the turn was aborted.
*
* It is a transcript row, not a model-facing payload: nothing replays it to a
* provider. It lives here because it is a fact of the invocation, and the
* invocation's events are the only record of those. Notes that happen between
* turns have no invocation, so they stay Session transcript rows.
*/
export interface RuntimeEventSystemNoteContent {
kind: 'system_note';
note: RuntimeSystemNoteKind;
/** Shape depends on `note`, exactly as it does on the transcript row. */
data?: unknown;
}

export interface RuntimeEventErrorContent {
kind: 'error';
code?: string;
Expand Down Expand Up @@ -337,6 +357,7 @@ export type RuntimeEventContent =
| RuntimeEventFunctionCallContent
| RuntimeEventFunctionResponseContent
| RuntimeEventErrorContent
| RuntimeEventSystemNoteContent
| RuntimeEventInvocationOpenedContent;

export const RUNTIME_EVENT_CONTENT_KINDS = [
Expand All @@ -345,6 +366,7 @@ export const RUNTIME_EVENT_CONTENT_KINDS = [
'function_call',
'function_response',
'error',
'system_note',
'invocation_opened',
] as const;
export type RuntimeEventContentKind = (typeof RUNTIME_EVENT_CONTENT_KINDS)[number];
Expand All @@ -365,6 +387,12 @@ export interface RuntimeEventTokenUsage extends TokenUsageFields {}
*/
export interface RuntimeEventPermissionDecision extends PermissionResponse {
toolName?: string;
/**
* What the prompt told the user they were approving. Normally read off the
* paired request; carried here when the decision is the only surviving
* evidence that the prompt happened.
*/
hint?: string;
}

export const TOOL_BOUNDARY_PROTOCOL_V1 = 't1_after_preflight_v1' as const;
Expand Down Expand Up @@ -696,6 +724,10 @@ const ERROR_CONTENT_SHAPE = defineObjectShape<RuntimeEventErrorContent>()(
['kind', 'message'],
['code', 'reason', 'details'],
);
const SYSTEM_NOTE_CONTENT_SHAPE = defineObjectShape<RuntimeEventSystemNoteContent>()(
['kind', 'note'],
['data'],
);
const INVOCATION_OPENED_CONTENT_SHAPE = defineObjectShape<RuntimeEventInvocationOpenedContent>()(
['kind', 'protocol', 'route', 'configuration', 'root', 'source'],
['lineage'],
Expand Down Expand Up @@ -803,7 +835,7 @@ const PERMISSION_CLOSURE_ACCEPTED_SHAPE =
defineObjectShape<RuntimeEventPermissionClosureAccepted>()(['requestId', 'reason'], []);
const RUNTIME_PERMISSION_DECISION_SHAPE = defineObjectShape<RuntimeEventPermissionDecision>()(
['requestId', 'decision'],
['rememberForTurn', 'reviewer', 'rationale', 'riskLevel', 'toolName'],
['rememberForTurn', 'reviewer', 'rationale', 'riskLevel', 'toolName', 'hint'],
);
const UTF8 = new TextEncoder();
const RUNTIME_TOOL_DISPATCH_SHAPE = defineObjectShape<RuntimeEventToolDispatch>()(
Expand Down Expand Up @@ -1022,6 +1054,12 @@ function isRuntimeEventContent(value: unknown): value is RuntimeEventContent {
typeof value.message === 'string' &&
(value.details === undefined || isStringArray(value.details) || isRecord(value.details))
);
case 'system_note':
return (
hasExactShape(value, SYSTEM_NOTE_CONTENT_SHAPE) &&
typeof value.note === 'string' &&
isRuntimeSystemNoteKind(value.note)
);
case 'invocation_opened':
return isRuntimeInvocationOpened(value);
default:
Expand Down Expand Up @@ -1266,7 +1304,8 @@ function isRuntimeEventPermissionDecision(value: unknown): value is RuntimeEvent
(value.toolName === undefined ||
(typeof value.toolName === 'string' &&
value.toolName.length > 0 &&
UTF8.encode(value.toolName).byteLength <= INTERACTION_TOOL_NAME_MAX_BYTES))
UTF8.encode(value.toolName).byteLength <= INTERACTION_TOOL_NAME_MAX_BYTES)) &&
isOptionalString(value.hint)
);
}

Expand Down Expand Up @@ -1485,6 +1524,7 @@ export function runtimeEventHasModelVisibleContent(event: RuntimeEvent): boolean
case 'function_response':
return true;
case 'error':
case 'system_note':
case 'invocation_opened':
return false;
}
Expand Down
92 changes: 47 additions & 45 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export interface SessionHeader {
/** Immutable Connection entity identity. Optional only on legacy Session records. */
llmConnectionId?: string;
llmConnectionSlug: string;
/** True after first UserMessage is flushed. Storage self-heals (§5.2). */
/** True once the Session's first UserMessage is durable. One-way. */
connectionLocked: boolean;
/** Sticky session default model id, captured when the session is created. */
model: string;
Expand Down Expand Up @@ -789,20 +789,12 @@ 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',
'context_provider_dropping',
'context_window_suggestion',
'context_window_overrun',
'context_reported_window_exceeded',
'context_overflow_after_compaction',
'step_limit',
]);

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

export interface AssistantMessage {
Expand Down Expand Up @@ -1147,27 +1139,49 @@ export interface TurnRecord {
partialOutputRetained: boolean;
}

/**
* The notes the runtime writes: things that happened inside one invocation and
* are part of what that invocation did. Their record is its RuntimeEvent ledger.
*/
export const RUNTIME_SYSTEM_NOTE_KINDS = [
'context_compacted',
'context_compaction_failed_open',
'context_provider_dropping',
'context_window_suggestion',
'context_window_overrun',
'context_reported_window_exceeded',
'context_overflow_after_compaction',
'step_limit',
] as const;

/**
* Notes only legacy transcripts carry, still decoded so those rows stay
* readable. Nothing writes them: the Session header and the invocation's
* opening and terminal facts already own what each of them said.
*/
export const RETIRED_SYSTEM_NOTE_KINDS = [
'session_start',
'session_resume',
'mode_change',
'model_change',
'error',
'abort',
] as const;

export type RuntimeSystemNoteKind = (typeof RUNTIME_SYSTEM_NOTE_KINDS)[number];
export type SystemNoteKind = RuntimeSystemNoteKind | (typeof RETIRED_SYSTEM_NOTE_KINDS)[number];

export function isRuntimeSystemNoteKind(kind: string): kind is RuntimeSystemNoteKind {
return (RUNTIME_SYSTEM_NOTE_KINDS as readonly string[]).includes(kind);
}

export interface SystemNoteMessage {
type: 'system_note';
id: string;
/** Session-level notes omit turnId. */
/** Retired session-level notes omit turnId. */
turnId?: string;
ts: number;
kind:
| 'session_start'
| 'session_resume'
| 'mode_change'
| 'model_change'
| 'context_compacted'
| 'context_compaction_failed_open'
| 'context_provider_dropping'
| 'context_window_suggestion'
| 'context_window_overrun'
| 'context_reported_window_exceeded'
| 'context_overflow_after_compaction'
| 'step_limit'
| 'error'
| 'abort';
kind: SystemNoteKind;
/** Shape depends on `kind`. */
data?: unknown;
}
Expand Down Expand Up @@ -1402,21 +1416,9 @@ const ASSISTANT_THINKING_SHAPE = defineObjectShape<AssistantThinking>()(
['text'],
['signature', 'providerOptions', 'parts'],
);
const SYSTEM_NOTE_KINDS = new Set([
'session_start',
'session_resume',
'mode_change',
'model_change',
'context_compacted',
'context_compaction_failed_open',
'context_provider_dropping',
'context_window_suggestion',
'context_window_overrun',
'context_reported_window_exceeded',
'context_overflow_after_compaction',
'step_limit',
'error',
'abort',
const SYSTEM_NOTE_KINDS = new Set<string>([
...RUNTIME_SYSTEM_NOTE_KINDS,
...RETIRED_SYSTEM_NOTE_KINDS,
]);

export function decodeCanonicalMessage(value: unknown): StoredMessage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ test('cancels managed approval owners and joiners with the canonical provider id
header: sessionHeader(),
connection: llmConnection(),
modelId: 'model-1',
appendMessage: async () => undefined,
readExecutionBoundary: async () =>
createManagedExecutionBoundary(createWorkspaceWritePermissionProfile(), 0),
newId: nextId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
stopReplacedWorkHubRoot,
} from '../server/execution-composition.js';
import { waitFor as pollFor } from '@maka/core/test-only/async-primitives';
import { readLedgerMessages } from './fixtures/ledger-transcript.js';

const require = createRequire(import.meta.url);
const FAKE_CONNECTION_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
Expand Down Expand Up @@ -462,6 +463,9 @@ test('production recovery preserves legacy Automation history and closes an orph
const composition = await createExecutionRuntimeHostComposition(compositionContext(owner));
try {
await composition.recover();
// The legacy transcript itself, as the converter reads it: recovery must
// leave a pre-ledger Automation's origin intact for the import that
// follows on the Session's first read.
const history = await stores.sessionStore.readMessages(historical.id);
assert.deepEqual(history[0]?.type === 'user' ? history[0].origin : undefined, {
kind: 'legacy_automation',
Expand Down Expand Up @@ -1725,7 +1729,7 @@ async function assertUniqueGraphExecutionFacts(
): Promise<void> {
const [runs, messages, runtimeEvents] = await Promise.all([
stores.runtimeEventStore.listSessionInvocations(claim.targetSessionId),
stores.sessionStore.readMessages(claim.targetSessionId),
readLedgerMessages(stores.runtimeEventStore, claim.targetSessionId),
stores.runtimeEventStore.readImmutableRuntimeEvents(claim.targetSessionId, claim.targetRunId),
]);
assert.deepEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
} from '../protocol/index.js';
import { SessionAdmissionGate } from '../server/session-admission-gate.js';
import { FramedTransport } from '../transport/framed-transport.js';
import { readLedgerMessages } from './fixtures/ledger-transcript.js';

import {
CONNECTION_EFFECT_MODEL_IDS,
Expand Down Expand Up @@ -773,7 +774,7 @@ test('startup recovery canonically closes pending linked child admissions withou
assert.equal(terminal.fact.failureClass, 'app_restarted');
}
const userMessages: StoredMessage[] = (
await stores.sessionStore.readMessages(recovered.sessionId)
await readLedgerMessages(stores.runtimeEventStore, recovered.sessionId)
).filter((message) => message.type === 'user' && message.turnId === recovered.turnId);
assert.equal(userMessages.length, recovered.kind === 'linked_child_provider_retry' ? 0 : 1);
if (recovered.kind !== 'linked_child_provider_retry') {
Expand Down
Loading
Loading