From 54c33391b87184e05f5b5e0397f5225e19e0f6ce Mon Sep 17 00:00:00 2001 From: testikun Date: Fri, 4 Sep 2026 16:33:15 +0800 Subject: [PATCH 01/17] feat(desktop): show Agent Graph output previews Project bounded child output and usage-derived throughput from existing RuntimeEvent facts into the Agent Graph read model, then render streaming and completed previews without changing graph execution semantics. Generated-by: OpenAI Codex --- .../main/__tests__/agent-graph-panel.test.ts | 87 ++++++++ .../src/renderer/styles/agent-graph.css | 13 ++ .../stories/agent-graph-panel.stories.tsx | 58 ++++++ .../__tests__/agent-graph-coordinator.test.ts | 25 +++ .../__tests__/agent-graph-protocol.test.ts | 24 +++ .../runtime-host/src/protocol/agent-graph.ts | 76 ++++++- .../src/server/agent-graph-coordinator.ts | 23 ++ .../__tests__/stream-graph-projection.test.ts | 60 ++++++ .../__tests__/stream-graph-read-model.test.ts | 113 ++++++++++ .../runtime/src/stream-graph-coordinator.ts | 17 +- .../runtime/src/stream-graph-projection.ts | 109 ++++++++++ .../runtime/src/stream-graph-read-model.ts | 197 ++++++++++++++++-- 12 files changed, 773 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/main/__tests__/agent-graph-panel.test.ts b/apps/desktop/src/main/__tests__/agent-graph-panel.test.ts index 6cbfb66ed8..384ba73d39 100644 --- a/apps/desktop/src/main/__tests__/agent-graph-panel.test.ts +++ b/apps/desktop/src/main/__tests__/agent-graph-panel.test.ts @@ -101,6 +101,30 @@ function snapshot( }; } +function operator( + overrides: Pick & + Partial, +): AgentGraphClientSnapshot['operators'][number] { + return { + childSessionId: `child-${overrides.operatorId}`, + provisionId: `provision-${overrides.operatorId}`, + agentId: 'reviewer', + provisionedAt: 1, + inboundEdgeIds: [], + outboundEdgeIds: [], + scheduledWorkIds: [], + readiness: [], + omitted: { + inboundEdgeIds: 0, + outboundEdgeIds: 0, + scheduledWorkIds: 0, + readiness: 0, + readinessWaits: 0, + }, + ...overrides, + }; +} + function installGraphRenderer( initial: AgentGraphClientSnapshot, historical: readonly AgentGraphClientSnapshot[] = [], @@ -308,6 +332,69 @@ async function renderPanel( } describe('AgentGraphPanel dismiss', () => { + it('renders bounded live and completed output facts without replacing child navigation', async () => { + const opened: string[] = []; + const source = snapshot({ + graphId: 'graph-output', + status: 'active', + operators: [ + operator({ + operatorId: 'operator-live', + status: 'running', + output: { + activationId: 'run-live', + preview: 'Inspecting the renderer projection', + previewTruncated: true, + phase: 'streaming', + previewUpdatedAt: 2_000, + sourceEventId: 'event-live', + messageId: 'message-live', + sampleStartedAt: 1_000, + outputTokens: 21, + sampleDurationMs: 1_000, + tokensPerSecond: 21, + }, + }), + operator({ + operatorId: 'operator-done', + status: 'completed', + output: { + activationId: 'run-done', + preview: 'Projection verified', + previewTruncated: false, + phase: 'completed', + previewUpdatedAt: 3_000, + sourceEventId: 'event-done', + sampleStartedAt: 2_000, + }, + }), + ], + }); + const harness = installGraphRenderer(source); + await act(async () => { + harness.root.render( + createElement(AgentGraphPanel, { + rootSessionId: 'session-1', + enabled: true, + locale: 'en', + onOpenSession: (sessionId: string) => opened.push(sessionId), + }), + ); + await Promise.resolve(); + }); + + assert.match(harness.container.textContent ?? '', /Live output · 21\.0 token\/s/); + assert.match(harness.container.textContent ?? '', /Inspecting the renderer projection…/); + assert.match(harness.container.textContent ?? '', /Result preview/); + const open = [...harness.container.querySelectorAll('button')].find((button) => + button.textContent?.includes('Open child task'), + ); + assert.ok(open); + await act(async () => (open as HTMLElement).click()); + assert.deepEqual(opened, ['child-operator-live']); + await act(async () => harness.root.unmount()); + }); + it('keeps the new session loading when a disposed read settles later', async () => { const sessionA = snapshot({ graphId: 'graph-a', status: 'active' }); const sessionB = snapshot({ diff --git a/apps/desktop/src/renderer/styles/agent-graph.css b/apps/desktop/src/renderer/styles/agent-graph.css index e24734583e..aa6ab5f020 100644 --- a/apps/desktop/src/renderer/styles/agent-graph.css +++ b/apps/desktop/src/renderer/styles/agent-graph.css @@ -204,6 +204,19 @@ font: var(--maka-text-supporting); } +.maka-agent-graph-output { + display: grid; + gap: var(--space-0-5); +} + +.maka-agent-graph-output-meta { + color: var(--muted-foreground); +} + +.maka-agent-graph-output-preview { + color: var(--foreground); +} + .maka-agent-graph-wait { color: var(--warning-text); } diff --git a/apps/desktop/stories/agent-graph-panel.stories.tsx b/apps/desktop/stories/agent-graph-panel.stories.tsx index dcae4eed15..f802bd6eac 100644 --- a/apps/desktop/stories/agent-graph-panel.stories.tsx +++ b/apps/desktop/stories/agent-graph-panel.stories.tsx @@ -238,6 +238,64 @@ export const BlockedOnUpstream: Story = { }, }; +// Real path: one child task is streaming while another has settled. The graph +// read model supplies bounded text and provider-reported throughput; the panel +// never reads either child Session independently. +export const OutputPreviews: Story = { + decorators: [ + withScopedMakaBridge( + graphBridge( + snapshot({ + status: 'active', + operators: [ + operator({ + operatorId: 'op-research', + status: 'running', + output: { + activationId: 'run-research', + preview: 'Comparing the three provider adapters and their retry boundaries…', + previewTruncated: false, + phase: 'streaming', + previewUpdatedAt: 2_000, + sourceEventId: 'event-research', + messageId: 'message-research', + sampleStartedAt: 1_000, + outputTokens: 42, + sampleDurationMs: 1_000, + tokensPerSecond: 42, + }, + }), + operator({ + operatorId: 'op-review', + status: 'completed', + output: { + activationId: 'run-review', + preview: 'Found one retry guard that needs a focused regression test.', + previewTruncated: false, + phase: 'completed', + previewUpdatedAt: 3_000, + sourceEventId: 'event-review', + sampleStartedAt: 2_000, + outputTokens: 18, + sampleDurationMs: 1_000, + tokensPerSecond: 18, + }, + }), + ], + }), + ), + ), + ], + render: panel, + play: async ({ canvasElement }) => { + await waitFor(() => { + expect(canvasElement.textContent).toContain(copy.liveOutput); + expect(canvasElement.textContent).toContain(copy.completedOutput); + expect(canvasElement.textContent).toContain(copy.throughput(42)); + }); + }, +}; + // Real path: a wide fan-out — many operators provisioned at once, the breadth a // small graph never shows. The read-model only elides operators past 256 (far // beyond a story's scale), so this shows a genuine many-operator list rather diff --git a/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts b/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts index b719b4533a..9199593416 100644 --- a/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts @@ -343,6 +343,19 @@ describe('Host Agent Graph coordinator', () => { test('projects only allowlisted Runtime fields onto the wire', () => { const source = graphSnapshot(); Object.assign(source.operators[0]!, { privatePrompt: 'operator-secret' }); + source.operators[0]!.output = { + activationId: 'activation-0', + preview: 'Bounded output', + previewTruncated: false, + phase: 'streaming', + previewUpdatedAt: 10, + sourceEventId: 'event-0', + sampleStartedAt: 1, + outputTokens: 18, + sampleDurationMs: 9, + tokensPerSecond: 2_000, + }; + Object.assign(source.operators[0]!.output, { privateOutput: 'output-secret' }); Object.assign(source.operators[0]!.readiness[0]!, { policyKind: 'map', privatePolicy: 'readiness-secret', @@ -351,6 +364,18 @@ describe('Host Agent Graph coordinator', () => { const projected = projectAgentGraphClientSnapshot(source); assert.equal(JSON.stringify(projected).includes('secret'), false); + assert.deepEqual(projected.operators[0]?.output, { + activationId: 'activation-0', + preview: 'Bounded output', + previewTruncated: false, + phase: 'streaming', + previewUpdatedAt: 10, + sourceEventId: 'event-0', + sampleStartedAt: 1, + outputTokens: 18, + sampleDurationMs: 9, + tokensPerSecond: 2_000, + }); assert.doesNotThrow(() => decodeAgentGraphClientSnapshot(projected)); }); diff --git a/packages/runtime-host/src/__tests__/agent-graph-protocol.test.ts b/packages/runtime-host/src/__tests__/agent-graph-protocol.test.ts index a66a4bbfbf..3ff4b2391d 100644 --- a/packages/runtime-host/src/__tests__/agent-graph-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/agent-graph-protocol.test.ts @@ -142,6 +142,17 @@ describe('Agent Graph Client protocol', () => { operators: [{ ...snapshot.operators[0], privatePrompt: 'secret' }], }), ); + assertInvalid(() => + decodeAgentGraphClientSnapshot({ + ...snapshot, + operators: [ + { + ...snapshot.operators[0], + output: { ...snapshot.operators[0]!.output, tokensPerSecond: -1 }, + }, + ], + }), + ); assertInvalid(() => decodeAgentGraphClientSnapshot({ ...snapshot, @@ -267,6 +278,19 @@ function graphSnapshot(): AgentGraphClientSnapshot { lastEventTime: 10, run: { sessionId: 'child-1', agentRunId: 'run:1', turnId: 'turn:1' }, }, + output: { + activationId: 'activation:1', + preview: 'Reviewing the graph projection.', + previewTruncated: false, + phase: 'streaming', + previewUpdatedAt: 10, + sourceEventId: 'event:1', + messageId: 'message:1', + sampleStartedAt: 1, + outputTokens: 18, + sampleDurationMs: 9, + tokensPerSecond: 2_000, + }, }, ], edges: [{ edgeId: 'edge:1', fromOperatorId: 'operator:0', toOperatorId: 'operator:1' }], diff --git a/packages/runtime-host/src/protocol/agent-graph.ts b/packages/runtime-host/src/protocol/agent-graph.ts index 02c9842c00..26de6efb2a 100644 --- a/packages/runtime-host/src/protocol/agent-graph.ts +++ b/packages/runtime-host/src/protocol/agent-graph.ts @@ -54,6 +54,7 @@ export const AGENT_GRAPH_MAX_INSPECTION_RECORDS = 32; export const AGENT_GRAPH_EPOCH_PAGE_SIZE = 32; const AGENT_GRAPH_INSTRUCTION_PREVIEW_MAX_BYTES = 2 * 1024; +const AGENT_GRAPH_OUTPUT_PREVIEW_MAX_BYTES = 2 * 1024; const AGENT_GRAPH_REASON_MAX_BYTES = 12 * 1024; const AGENT_GRAPH_RECONCILIATION_FAILURE_REASON_MAX_BYTES = 4 * 1024; @@ -177,6 +178,21 @@ export interface AgentGraphClientOperator { readonly terminalRecordId?: string; readonly run: AgentGraphClientRunRef; }; + readonly output?: AgentGraphClientOperatorOutput; +} + +export interface AgentGraphClientOperatorOutput { + readonly activationId: string; + readonly preview: string; + readonly previewTruncated: boolean; + readonly phase: 'streaming' | 'completed'; + readonly previewUpdatedAt: number; + readonly sourceEventId: string; + readonly messageId?: string; + readonly sampleStartedAt: number; + readonly outputTokens?: number; + readonly sampleDurationMs?: number; + readonly tokensPerSecond?: number; } export interface AgentGraphClientEdge { @@ -759,7 +775,7 @@ function decodeOperator(value: unknown): AgentGraphClientOperator { 'readiness', 'omitted', ], - ['currentActivation'], + ['currentActivation', 'output'], ); return { operatorId: requireOpaqueIdentity(record.operatorId, 'operatorId'), @@ -793,6 +809,57 @@ function decodeOperator(value: unknown): AgentGraphClientOperator { ...(record.currentActivation === undefined ? {} : { currentActivation: decodeCurrentActivation(record.currentActivation) }), + ...(record.output === undefined ? {} : { output: decodeOperatorOutput(record.output) }), + }; +} + +function decodeOperatorOutput(value: unknown): AgentGraphClientOperatorOutput { + const record = requireShapedRecord( + value, + 'agent graph operator output', + [ + 'activationId', + 'preview', + 'previewTruncated', + 'phase', + 'previewUpdatedAt', + 'sourceEventId', + 'sampleStartedAt', + ], + ['messageId', 'outputTokens', 'sampleDurationMs', 'tokensPerSecond'], + ); + if (record.phase !== 'streaming' && record.phase !== 'completed') { + throw invalidProtocolFrame('Invalid agent graph operator output phase'); + } + return { + activationId: requireOpaqueIdentity(record.activationId, 'activationId'), + preview: requireUtf8String( + record.preview, + 'operator output preview', + AGENT_GRAPH_OUTPUT_PREVIEW_MAX_BYTES, + ), + previewTruncated: requireBoolean(record.previewTruncated, 'previewTruncated'), + phase: record.phase, + previewUpdatedAt: requireCount(record.previewUpdatedAt, 'previewUpdatedAt'), + sourceEventId: requireOpaqueIdentity(record.sourceEventId, 'sourceEventId'), + ...(record.messageId === undefined + ? {} + : { messageId: requireOpaqueIdentity(record.messageId, 'messageId') }), + sampleStartedAt: requireCount(record.sampleStartedAt, 'sampleStartedAt'), + ...(record.outputTokens === undefined + ? {} + : { outputTokens: requireCount(record.outputTokens, 'outputTokens') }), + ...(record.sampleDurationMs === undefined + ? {} + : { sampleDurationMs: requireCount(record.sampleDurationMs, 'sampleDurationMs') }), + ...(record.tokensPerSecond === undefined + ? {} + : { + tokensPerSecond: requireNonNegativeFiniteNumber( + record.tokensPerSecond, + 'tokensPerSecond', + ), + }), }; } @@ -1348,6 +1415,13 @@ function requireBoolean(value: unknown, label: string): boolean { return value; } +function requireNonNegativeFiniteNumber(value: unknown, label: string): number { + if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { + throw invalidProtocolFrame(`Invalid ${label}`); + } + return value; +} + function requireOpaqueIdentity(value: unknown, label: string): string { if ( typeof value !== 'string' || diff --git a/packages/runtime-host/src/server/agent-graph-coordinator.ts b/packages/runtime-host/src/server/agent-graph-coordinator.ts index 166655fb6b..c463d854e0 100644 --- a/packages/runtime-host/src/server/agent-graph-coordinator.ts +++ b/packages/runtime-host/src/server/agent-graph-coordinator.ts @@ -402,6 +402,29 @@ function projectOperator( }, } : {}), + ...(operator.output + ? { + output: { + activationId: operator.output.activationId, + preview: operator.output.preview, + previewTruncated: operator.output.previewTruncated, + phase: operator.output.phase, + previewUpdatedAt: operator.output.previewUpdatedAt, + sourceEventId: operator.output.sourceEventId, + sampleStartedAt: operator.output.sampleStartedAt, + ...(operator.output.messageId ? { messageId: operator.output.messageId } : {}), + ...(operator.output.outputTokens !== undefined + ? { outputTokens: operator.output.outputTokens } + : {}), + ...(operator.output.sampleDurationMs !== undefined + ? { sampleDurationMs: operator.output.sampleDurationMs } + : {}), + ...(operator.output.tokensPerSecond !== undefined + ? { tokensPerSecond: operator.output.tokensPerSecond } + : {}), + }, + } + : {}), }; } diff --git a/packages/runtime/src/__tests__/stream-graph-projection.test.ts b/packages/runtime/src/__tests__/stream-graph-projection.test.ts index 751e6f93dd..b1046f7a7d 100644 --- a/packages/runtime/src/__tests__/stream-graph-projection.test.ts +++ b/packages/runtime/src/__tests__/stream-graph-projection.test.ts @@ -210,6 +210,66 @@ describe('committed stream graph projection', () => { assert.doesNotMatch(JSON.stringify(projection.records), /mutable-stream-chunk/); }); + test('derives bounded operator output and actual throughput from immutable runtime facts', async () => { + const run = runInvocation({ + sessionId: 'child-output', + runId: 'run-output', + turnId: 'turn-output', + status: 'completed', + createdAt: baseTs, + }); + const text = `Summary ${'界'.repeat(400)}`; + const projection = await readCommittedAgentGraphProjection({ + graphId: 'graph-output', + operators: [{ operatorId: 'writer', sessionId: run.sessionId }], + runtimeEventStore: { + async listSessionInvocations() { + return [run]; + }, + async readImmutableRuntimeEvents() { + return [ + runtimeEvent(run, { + id: 'output-start', + ts: baseTs + 500, + partial: true, + role: 'model', + author: 'agent', + content: { kind: 'text', text: 'Summary ' }, + }), + runtimeEvent(run, { + id: 'output-text', + ts: baseTs + 1_000, + role: 'model', + author: 'agent', + content: { kind: 'text', text }, + }), + runtimeEvent(run, { + id: 'output-usage', + ts: baseTs + 5_000, + actions: { tokenUsage: { input: 100, output: 90 } }, + }), + runtimeEvent(run, { + id: 'output-complete', + ts: baseTs + 5_001, + status: 'completed', + actions: { endInvocation: true }, + }), + ]; + }, + }, + }); + + const output = projection.operatorOutputs?.[0]; + assert.ok(output); + assert.equal(Array.from(output.preview).length, 280); + assert.equal(output.previewTruncated, true); + assert.equal(output.phase, 'completed'); + assert.equal(output.outputTokens, 90); + assert.equal(output.sampleDurationMs, 4_500); + assert.equal(output.tokensPerSecond, 20); + assert.equal(output.sourceEventId, 'output-text'); + }); + test('replay is deterministic for reordered delivery and idempotent duplicates', () => { const run = runInvocation({ sessionId: 'child-a', diff --git a/packages/runtime/src/__tests__/stream-graph-read-model.test.ts b/packages/runtime/src/__tests__/stream-graph-read-model.test.ts index 3f37466605..202b9c5675 100644 --- a/packages/runtime/src/__tests__/stream-graph-read-model.test.ts +++ b/packages/runtime/src/__tests__/stream-graph-read-model.test.ts @@ -376,6 +376,7 @@ describe('agent graph client read model', () => { undefined, ); + assert.ok(forward.activity); const canonicalRecords = forward.snapshot.recentActivity.map((activity, index) => graphRecordFromActivity(graphId, activity, index), ); @@ -401,6 +402,9 @@ describe('agent graph client read model', () => { }; canonicalObservation.projection.state.operators[operatorId]!.currentActivationId = 'run-0'; canonicalObservation.projection.state.operators[operatorId]!.status = 'completed'; + const forwardOutput = forward.operator.operator.output; + assert.ok(forwardOutput); + canonicalObservation.projection.operatorOutputs = [{ operatorId, ...forwardOutput }]; const rebuilt = materializeAgentGraphClientProjection({ ...baseInput, observation: canonicalObservation, @@ -419,6 +423,92 @@ describe('agent graph client read model', () => { assert.equal(rebuilt.snapshot.snapshotVersion, forward.snapshot.snapshotVersion); assert.deepEqual(rebuilt.snapshot.recentActivity, forward.snapshot.recentActivity); }); + + test('advances a bounded streaming preview and provider-reported TPS without activity records', () => { + const graphId = 'graph-output'; + const operatorId = 'operator-output'; + const childSessionId = 'child-output'; + const initial = materializeAgentGraphClientProjection( + runningInput(graphId, operatorId, childSessionId), + ); + const first = advanceMaterializedAgentGraphClientProjection( + initial.snapshot, + initial.operators[0]!, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'delta-1', + type: 'text_delta', + ts: 1_000, + messageId: 'message-1', + startOffset: 0, + text: 'Reviewing ', + }), + false, + )!; + assert.equal(first.activity, undefined); + assert.equal(first.operator.operator.output?.preview, 'Reviewing '); + + const replayed = advanceMaterializedAgentGraphClientProjection( + first.snapshot, + first.operator, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'delta-replay', + type: 'text_delta', + ts: 1_500, + messageId: 'message-1', + startOffset: 0, + text: 'Reviewing ', + }), + false, + )!; + assert.equal(replayed.operator.operator.output?.preview, 'Reviewing '); + + const second = advanceMaterializedAgentGraphClientProjection( + replayed.snapshot, + replayed.operator, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'delta-2', + type: 'text_delta', + ts: 2_000, + messageId: 'message-1', + startOffset: 10, + text: 'projection', + }), + false, + )!; + assert.equal(second.operator.operator.output?.preview, 'Reviewing projection'); + + const usage = advanceMaterializedAgentGraphClientProjection( + second.snapshot, + second.operator, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'usage-1', + type: 'token_usage', + ts: 3_000, + input: 80, + output: 20, + }), + false, + )!; + assert.equal(usage.activity?.facets[0], 'usage'); + assert.equal(usage.operator.operator.output?.outputTokens, 20); + assert.equal(usage.operator.operator.output?.sampleDurationMs, 2_000); + assert.equal(usage.operator.operator.output?.tokensPerSecond, 10); + assert.equal(usage.operator.operator.output?.sourceEventId, 'delta-2'); + + const settled = advanceMaterializedAgentGraphClientProjection( + usage.snapshot, + usage.operator, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'complete-1', + type: 'complete', + ts: 4_000, + stopReason: 'end_turn', + }), + false, + )!; + assert.equal(settled.operator.operator.output?.phase, 'completed'); + assert.equal(settled.snapshot.operators[0]?.output?.phase, 'completed'); + }); }); function emptyInput(graphId: string) { @@ -658,6 +748,29 @@ function supervisorRuntimeEvent(input: { } as unknown as AgentGraphSupervisorRuntimeEvent; } +function outputRuntimeEvent( + graphId: string, + operatorId: string, + childSessionId: string, + event: Record, +): AgentGraphSupervisorRuntimeEvent { + return { + intent: { graphId }, + claim: { + targetOperatorId: operatorId, + targetSessionId: childSessionId, + targetRunId: 'run-0', + targetTurnId: 'turn-0', + }, + event: { + sessionId: childSessionId, + runId: 'run-0', + turnId: 'turn-0', + ...event, + }, + } as unknown as AgentGraphSupervisorRuntimeEvent; +} + function graphRecordFromActivity( graphId: string, activity: AgentGraphClientActivity, diff --git a/packages/runtime/src/stream-graph-coordinator.ts b/packages/runtime/src/stream-graph-coordinator.ts index 3eb3378e11..bd4c115ac7 100644 --- a/packages/runtime/src/stream-graph-coordinator.ts +++ b/packages/runtime/src/stream-graph-coordinator.ts @@ -1200,13 +1200,15 @@ export class AgentGraphCoordinator { // stop race). Only the authoritative RuntimeEvent fold populates the // immutable terminal-history table. terminalActivities: [], - activityRecords: [ - { - recordId: advanced.activity.recordId, - eventTime: advanced.activity.eventTime, - }, - ], - incrementalRecordId: advanced.activity.recordId, + activityRecords: advanced.activity + ? [ + { + recordId: advanced.activity.recordId, + eventTime: advanced.activity.eventTime, + }, + ] + : [], + ...(advanced.activity ? { incrementalRecordId: advanced.activity.recordId } : {}), }); if (committed.snapshotVersion === advanced.snapshot.snapshotVersion) { this.#notifyClientChanged(driver, 'runtime_activity'); @@ -1732,7 +1734,6 @@ function isMaterializedGraphClientEvent( type: AgentGraphSupervisorRuntimeEvent['event']['type'], ): boolean { return ![ - 'text_delta', 'thinking_delta', 'tool_output_delta', 'tool_progress', diff --git a/packages/runtime/src/stream-graph-projection.ts b/packages/runtime/src/stream-graph-projection.ts index f0f42f6acf..c18063bdbc 100644 --- a/packages/runtime/src/stream-graph-projection.ts +++ b/packages/runtime/src/stream-graph-projection.ts @@ -202,6 +202,27 @@ export interface AgentGraphProjection { records: AgentGraphRecord[]; supervisorMetaStream: AgentGraphSupervisorMetaRecord[]; state: AgentGraphReplayState; + /** + * Bounded presentation facts derived from the same immutable RuntimeEvents. + * RuntimeEvents remain authoritative; this is not part of scheduling or + * supervisor replay. + */ + operatorOutputs?: AgentGraphOperatorOutputProjection[]; +} + +export interface AgentGraphOperatorOutputProjection { + operatorId: string; + activationId: string; + preview: string; + previewTruncated: boolean; + phase: 'streaming' | 'completed'; + previewUpdatedAt: number; + sourceEventId: string; + messageId?: string; + sampleStartedAt: number; + outputTokens?: number; + sampleDurationMs?: number; + tokensPerSecond?: number; } export interface ReadCommittedAgentGraphProjectionInput { @@ -287,10 +308,98 @@ export async function readCommittedAgentGraphProjectionWithRuns( records: projected.records, supervisorMetaStream: projected.supervisorMetaStream, state, + operatorOutputs: projectOperatorOutputs(streams), }, }; } +const AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS = 280; + +function projectOperatorOutputs( + streams: readonly AgentGraphRunStream[], +): AgentGraphOperatorOutputProjection[] { + const latestByOperator = new Map(); + for (const stream of streams) { + const current = latestByOperator.get(stream.operator.operatorId); + if ( + !current || + stream.run.openedAt > current.run.openedAt || + (stream.run.openedAt === current.run.openedAt && + compareAgentGraphIdentity(stream.run.runId, current.run.runId) > 0) + ) { + latestByOperator.set(stream.operator.operatorId, stream); + } + } + return [...latestByOperator.values()] + .map(projectOperatorOutput) + .filter((output): output is AgentGraphOperatorOutputProjection => output !== undefined) + .sort((left, right) => compareAgentGraphIdentity(left.operatorId, right.operatorId)); +} + +function projectOperatorOutput( + stream: AgentGraphRunStream, +): AgentGraphOperatorOutputProjection | undefined { + const orderedEvents = stream.events + .slice() + .sort((left, right) => left.ts - right.ts || compareAgentGraphIdentity(left.id, right.id)); + const events = orderedEvents.filter((event) => !event.partial); + const textEvents = events.filter( + (event) => event.role === 'model' && event.content?.kind === 'text', + ); + const latestText = textEvents.at(-1); + if (!latestText || latestText.content?.kind !== 'text') return undefined; + const preview = boundOutputPreview(latestText.content.text, 'head'); + if (!preview.text) return undefined; + const usageEvents = events.filter((event) => event.actions?.tokenUsage !== undefined); + const latestUsage = usageEvents.at(-1); + const outputTokens = latestUsage?.actions?.tokenUsage?.output; + const usageEndedAt = latestUsage?.ts; + const sampleStartedAt = + orderedEvents.find((event) => event.role === 'model' && event.content?.kind === 'text')?.ts ?? + textEvents[0]!.ts; + const sampleDurationMs = + outputTokens !== undefined && outputTokens > 0 && usageEndedAt !== undefined + ? Math.max(0, usageEndedAt - sampleStartedAt) + : undefined; + const tokensPerSecond = + outputTokens !== undefined && sampleDurationMs !== undefined && sampleDurationMs > 0 + ? roundTokensPerSecond((outputTokens * 1_000) / sampleDurationMs) + : undefined; + return { + operatorId: stream.operator.operatorId, + activationId: stream.run.runId, + preview: preview.text, + previewTruncated: preview.truncated, + phase: runtimeInvocationOutcome(stream.run) ? 'completed' : 'streaming', + previewUpdatedAt: latestText.ts, + sourceEventId: latestText.id, + ...(latestText.refs?.providerEventId ? { messageId: latestText.refs.providerEventId } : {}), + sampleStartedAt, + ...(outputTokens !== undefined ? { outputTokens } : {}), + ...(sampleDurationMs !== undefined ? { sampleDurationMs } : {}), + ...(tokensPerSecond !== undefined ? { tokensPerSecond } : {}), + }; +} + +function boundOutputPreview( + text: string, + edge: 'head' | 'tail', +): { text: string; truncated: boolean } { + const codePoints = Array.from(text.trim()); + if (codePoints.length <= AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS) { + return { text: codePoints.join(''), truncated: false }; + } + const visible = + edge === 'head' + ? codePoints.slice(0, AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS) + : codePoints.slice(-AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS); + return { text: visible.join(''), truncated: true }; +} + +function roundTokensPerSecond(value: number): number { + return Math.round(value * 10) / 10; +} + export function projectAgentGraphRecords(input: ProjectAgentGraphRecordsInput): { ignoredPartialEvents: number; records: AgentGraphRecord[]; diff --git a/packages/runtime/src/stream-graph-read-model.ts b/packages/runtime/src/stream-graph-read-model.ts index 5510f927b7..b05c5a5699 100644 --- a/packages/runtime/src/stream-graph-read-model.ts +++ b/packages/runtime/src/stream-graph-read-model.ts @@ -29,6 +29,7 @@ import type { } from './stream-graph-dispatch.js'; import type { AgentGraphActivationStatus, + AgentGraphOperatorOutputProjection, AgentGraphRecord, AgentGraphRecordFacet, AgentGraphSupervisorSignal, @@ -65,6 +66,7 @@ const MAX_OPERATOR_INSPECTION_EDGES = 512; const MAX_OPERATOR_INSPECTION_WORK = 256; const MAX_OPERATOR_INSPECTION_CLAIMS = 256; const MAX_INSTRUCTION_PREVIEW_CHARS = 500; +const MAX_OUTPUT_PREVIEW_CODE_POINTS = 280; export type AgentGraphClientOperatorStatus = | 'not_started' @@ -121,6 +123,21 @@ export interface AgentGraphClientOperator { terminalRecordId?: string; run: AgentGraphClientRunRef; }; + output?: AgentGraphClientOperatorOutput; +} + +export interface AgentGraphClientOperatorOutput { + activationId: string; + preview: string; + previewTruncated: boolean; + phase: 'streaming' | 'completed'; + previewUpdatedAt: number; + sourceEventId: string; + messageId?: string; + sampleStartedAt: number; + outputTokens?: number; + sampleDurationMs?: number; + tokensPerSecond?: number; } export interface AgentGraphClientEdge { @@ -291,7 +308,7 @@ export interface AgentGraphClientMaterialization { export interface AdvancedAgentGraphClientProjection { snapshot: AgentGraphClientSnapshot; operator: AgentGraphOperatorInspection; - activity: AgentGraphClientActivity; + activity?: AgentGraphClientActivity; terminalActivity?: AgentGraphClientActivity; } @@ -315,8 +332,9 @@ interface BuiltReadModel { /** * Durable, bounded graph-facing projection for untrusted presentation clients. * - * Payloads remain in Session/Runtime stores. This surface carries only - * identities, lifecycle state, wait reasons, and bounded instruction previews. + * Full payloads remain in Session/Runtime stores. This surface carries only + * identities, lifecycle state, wait reasons, bounded instruction/output + * previews, and provider-reported usage-derived throughput. */ export function buildAgentGraphClientSnapshot( input: BuildAgentGraphClientReadModelInput, @@ -352,7 +370,13 @@ export function advanceMaterializedAgentGraphClientProjection( activationHadError: boolean, ): AdvancedAgentGraphClientProjection | undefined { const projected = projectClientSessionEvent(runtime.event, activationHadError); - if (!projected) return undefined; + const output = advanceClientOperatorOutput( + inspectionInput.operator.output, + runtime.event, + runtime.claim.targetRunId, + ['completed', 'failed', 'aborted', 'cancelled'].includes(inspectionInput.operator.status), + ); + if (!projected && !output) return undefined; if ( snapshotInput.graphId !== runtime.intent.graphId || inspectionInput.graphId !== runtime.intent.graphId || @@ -361,21 +385,36 @@ export function advanceMaterializedAgentGraphClientProjection( ) { throw new Error('Agent graph runtime activity does not match its materialized projection'); } - const activity: AgentGraphClientActivity = { - recordId: clientRuntimeRecordId(runtime), - operatorId: runtime.claim.targetOperatorId, - activationId: runtime.claim.targetRunId, - eventTime: runtime.event.ts, - facets: projected.facets, - signals: projected.signals, - run: { - sessionId: runtime.claim.targetSessionId, - agentRunId: runtime.claim.targetRunId, - turnId: runtime.claim.targetTurnId, - }, - }; + const activity: AgentGraphClientActivity | undefined = projected + ? { + recordId: clientRuntimeRecordId(runtime), + operatorId: runtime.claim.targetOperatorId, + activationId: runtime.claim.targetRunId, + eventTime: runtime.event.ts, + facets: projected.facets, + signals: projected.signals, + run: { + sessionId: runtime.claim.targetSessionId, + agentRunId: runtime.claim.targetRunId, + turnId: runtime.claim.targetTurnId, + }, + } + : undefined; const snapshot = structuredClone(snapshotInput); const inspection = structuredClone(inspectionInput); + if (output) inspection.operator.output = output; + const visibleOperatorIndex = snapshot.operators.findIndex( + (operator) => operator.operatorId === inspection.operator.operatorId, + ); + if (visibleOperatorIndex >= 0 && output) { + snapshot.operators[visibleOperatorIndex] = structuredClone(inspection.operator); + } + if (!projected || !activity) { + snapshot.latestEventTime = Math.max(snapshot.latestEventTime ?? 0, runtime.event.ts); + snapshot.snapshotVersion = clientSnapshotVersion(snapshot); + inspection.snapshotVersion = snapshot.snapshotVersion; + return { snapshot, operator: inspection }; + } if ( snapshot.recentActivity.some((record) => record.recordId === activity.recordId) || inspection.recentRecords.some((record) => record.recordId === activity.recordId) @@ -487,9 +526,6 @@ export function advanceMaterializedAgentGraphClientProjection( .slice(-MAX_OPERATOR_INSPECTION_RECORDS); inspection.omitted.records = Math.max(0, inspectionRecordCount - inspection.recentRecords.length); - const visibleOperatorIndex = snapshot.operators.findIndex( - (operator) => operator.operatorId === inspection.operator.operatorId, - ); if (visibleOperatorIndex >= 0) { snapshot.operators[visibleOperatorIndex] = structuredClone(inspection.operator); } @@ -665,6 +701,12 @@ function buildReadModel(input: BuildAgentGraphClientReadModelInput): BuiltReadMo .map(clientActivity) .sort(compareClientActivity); const work = schedule.work.map(clientWork); + const outputByOperator = new Map( + (input.observation.projection.operatorOutputs ?? []).map((output) => [ + output.operatorId, + output, + ]), + ); const operators = input.observation.projection.operators.map((binding) => { const provision = provisionByOperator.get(binding.operatorId); if (!provision || provision.targetSessionId !== binding.sessionId) { @@ -683,6 +725,7 @@ function buildReadModel(input: BuildAgentGraphClientReadModelInput): BuiltReadMo const readiness = allReadiness.slice(0, MAX_OPERATOR_READINESS); const state = input.observation.projection.state.operators[binding.operatorId]; const currentActivation = state?.activations[state.currentActivationId]; + const output = outputByOperator.get(binding.operatorId); const currentClaim = currentActivation ? claims.find( (claim) => @@ -750,6 +793,9 @@ function buildReadModel(input: BuildAgentGraphClientReadModelInput): BuiltReadMo }, } : {}), + ...(currentActivation && output?.activationId === currentActivation.activationId + ? { output: clientOperatorOutput(output) } + : {}), } satisfies AgentGraphClientOperator; }); const stoppedTargets = schedule.stoppedTargets.map(clientStoppedTarget); @@ -796,6 +842,117 @@ function buildReadModel(input: BuildAgentGraphClientReadModelInput): BuiltReadMo }; } +function clientOperatorOutput( + output: AgentGraphOperatorOutputProjection, +): AgentGraphClientOperatorOutput { + return { + activationId: output.activationId, + preview: output.preview, + previewTruncated: output.previewTruncated, + phase: output.phase, + previewUpdatedAt: output.previewUpdatedAt, + sourceEventId: output.sourceEventId, + ...(output.messageId ? { messageId: output.messageId } : {}), + sampleStartedAt: output.sampleStartedAt, + ...(output.outputTokens !== undefined ? { outputTokens: output.outputTokens } : {}), + ...(output.sampleDurationMs !== undefined ? { sampleDurationMs: output.sampleDurationMs } : {}), + ...(output.tokensPerSecond !== undefined ? { tokensPerSecond: output.tokensPerSecond } : {}), + }; +} + +function advanceClientOperatorOutput( + current: AgentGraphClientOperatorOutput | undefined, + event: SessionEvent, + activationId: string, + operatorSettled: boolean, +): AgentGraphClientOperatorOutput | undefined { + if (current?.activationId === activationId && current.sourceEventId === event.id) { + return undefined; + } + const existing = current?.activationId === activationId ? current : undefined; + if ( + existing && + (event.type === 'text_delta' || event.type === 'text_complete') && + event.ts < existing.previewUpdatedAt + ) { + return undefined; + } + if (event.type === 'text_delta' || event.type === 'text_complete') { + const sameMessage = existing?.messageId === event.messageId; + const append = event.type === 'text_delta' && sameMessage; + const text = append ? foldClientOutputDelta(existing, event) : event.text; + const preview = boundClientOutputPreview( + text, + event.type === 'text_complete' ? 'head' : 'tail', + ); + if (!preview.text) return undefined; + return { + activationId, + preview: preview.text, + previewTruncated: preview.truncated || (append && existing.previewTruncated), + phase: operatorSettled || existing?.phase === 'completed' ? 'completed' : 'streaming', + previewUpdatedAt: event.ts, + sourceEventId: event.id, + ...(event.messageId ? { messageId: event.messageId } : {}), + sampleStartedAt: existing?.sampleStartedAt ?? event.ts, + ...(existing?.outputTokens !== undefined ? { outputTokens: existing.outputTokens } : {}), + ...(existing?.sampleDurationMs !== undefined + ? { sampleDurationMs: existing.sampleDurationMs } + : {}), + ...(existing?.tokensPerSecond !== undefined + ? { tokensPerSecond: existing.tokensPerSecond } + : {}), + }; + } + if (event.type === 'token_usage' && existing) { + const sampleDurationMs = Math.max(0, event.ts - existing.sampleStartedAt); + return { + ...existing, + outputTokens: event.output, + sampleDurationMs, + ...(sampleDurationMs > 0 + ? { tokensPerSecond: roundClientTokensPerSecond((event.output * 1_000) / sampleDurationMs) } + : {}), + }; + } + if ((event.type === 'complete' || event.type === 'abort') && existing) { + return { ...existing, phase: 'completed' }; + } + return undefined; +} + +function foldClientOutputDelta( + existing: AgentGraphClientOperatorOutput, + event: Extract, +): string { + if (event.startOffset === undefined || existing.previewTruncated) { + return `${existing.preview}${event.text}`; + } + if (event.startOffset <= existing.preview.length) { + return `${existing.preview.slice(0, event.startOffset)}${event.text}`; + } + return event.text; +} + +function boundClientOutputPreview( + text: string, + edge: 'head' | 'tail', +): { text: string; truncated: boolean } { + const codePoints = Array.from(edge === 'head' ? text.trim() : text); + if (codePoints.length <= MAX_OUTPUT_PREVIEW_CODE_POINTS) { + return { text: codePoints.join(''), truncated: false }; + } + const visible = + edge === 'head' + ? codePoints.slice(0, MAX_OUTPUT_PREVIEW_CODE_POINTS) + : codePoints.slice(-MAX_OUTPUT_PREVIEW_CODE_POINTS); + return { text: visible.join(''), truncated: true }; +} + +function roundClientTokensPerSecond(value: number): number { + return Math.round(value * 10) / 10; +} + function operatorStatus( runtimeStatus: AgentGraphActivationStatus | undefined, readiness: AgentGraphClientOperator['readiness'], From a92a1c84adf930b100a312e8473954aac8c53a4f Mon Sep 17 00:00:00 2001 From: testikun Date: Fri, 4 Sep 2026 16:51:57 +0800 Subject: [PATCH 02/17] test(runtime): cover Agent Graph output invalidations Replace the obsolete no-delta-write assertion with bounded streaming projection and one-invalidation-per-commit coverage. Generated-by: OpenAI Codex --- .../stream-graph-coordinator.test.ts | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts b/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts index e35b0e2d65..cb2093e75f 100644 --- a/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts +++ b/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts @@ -502,6 +502,11 @@ describe('host-managed agent graph coordinator', () => { snapshot.work[0]?.instructionPreview, 'Inspect the repository and report one concrete finding.', ); + const operatorOutput = snapshot.operators[0]?.output; + assert.ok(operatorOutput); + assert.equal(operatorOutput.phase, 'completed'); + assert.ok(Array.from(operatorOutput.preview).length <= 280); + assert.match(operatorOutput.preview, /Fake backend received:/); assert.match(snapshot.snapshotVersion, /^sha256:[a-f0-9]{64}$/); const readsBeforeInspection = delayedControlStore.projectionReadCounts(); const inspection = await coordinator.inspectOperator( @@ -541,15 +546,40 @@ describe('host-managed agent graph coordinator', () => { ); await new Promise((resolve) => setImmediate(resolve)); assert.ok(clientEvents.includes('reconciled')); - assert.equal( - clientEvents.filter((reason) => reason === 'runtime_activity').length, - 2, - 'partial text deltas must not cause projection writes or invalidations', - ); const incrementalProjectionCommits = delayedControlStore.projectionCommits.filter( (request) => request.incrementalRecordId !== undefined, ); assert.equal(incrementalProjectionCommits.length, 2); + const outputProjectionCommits = delayedControlStore.projectionCommits.filter( + (request) => + request.incrementalRecordId === undefined && + !request.replaceOperators && + request.activityRecords.length === 0 && + request.operators.some((candidate) => { + const payload = candidate.payload as { + operator?: { output?: { preview?: unknown } }; + }; + return typeof payload.operator?.output?.preview === 'string'; + }), + ); + assert.ok(outputProjectionCommits.length > 0); + assert.ok( + outputProjectionCommits.every((request) => + request.operators.every((candidate) => { + const payload = candidate.payload as { + operator?: { output?: { preview?: unknown } }; + }; + const preview = payload.operator?.output?.preview; + return typeof preview === 'string' && Array.from(preview).length <= 280; + }), + ), + 'streaming output commits must remain bounded', + ); + assert.equal( + clientEvents.filter((reason) => reason === 'runtime_activity').length, + incrementalProjectionCommits.length + outputProjectionCommits.length, + 'each materialized activity or bounded output update must invalidate exactly once', + ); assert.ok( incrementalProjectionCommits.every((request) => request.terminalActivities.length === 0), 'yielded SessionEvents must never write immutable terminal history', From 497a2789d7a38688fea822e26e72219be87e019c Mon Sep 17 00:00:00 2001 From: testikun Date: Tue, 8 Sep 2026 14:49:01 +0800 Subject: [PATCH 03/17] fix(protocol): advance Agent Graph compatibility epoch --- packages/runtime-host/src/protocol/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts index 3078a049b2..fe439c52b7 100644 --- a/packages/runtime-host/src/protocol/index.ts +++ b/packages/runtime-host/src/protocol/index.ts @@ -101,7 +101,9 @@ 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 = 133 as const; +export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 134 as const; +// 134: Agent Graph operator snapshots carry bounded output previews and metrics. +// Epoch-133 peers reject the additional `output` field on strict operator shapes. // 133: WorkHub actions carry attachments and new-Work model/permission defaults. // Epoch-132 peers reject these additional fields on strict action shapes. // 132: new Tool Result archives use versioned ledger references, not Artifact payloads. From 082bb403c0f4d34bc4cca42690eb5be091ac5c9a Mon Sep 17 00:00:00 2001 From: testikun Date: Tue, 8 Sep 2026 15:32:39 +0800 Subject: [PATCH 04/17] fix(desktop): render Agent Graph output previews --- apps/desktop/src/renderer/agent-graph-panel.tsx | 16 ++++++++++++++++ .../src/renderer/locales/agent-graph-copy.ts | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/apps/desktop/src/renderer/agent-graph-panel.tsx b/apps/desktop/src/renderer/agent-graph-panel.tsx index 7858b401d7..62dda7fa70 100644 --- a/apps/desktop/src/renderer/agent-graph-panel.tsx +++ b/apps/desktop/src/renderer/agent-graph-panel.tsx @@ -377,6 +377,22 @@ export function AgentGraphPanel(props: { {operator.agentId} {work?.instructionPreview ?? operator.operatorId} + {operator.output ? ( + + + {operator.output.phase === 'streaming' + ? copy.liveOutput + : copy.completedOutput} + {operator.output.tokensPerSecond === undefined + ? null + : ` · ${copy.throughput(operator.output.tokensPerSecond)}`} + + + {operator.output.preview} + {operator.output.previewTruncated ? '…' : ''} + + + ) : null} {wait ? {wait} : null} diff --git a/apps/desktop/src/renderer/locales/agent-graph-copy.ts b/apps/desktop/src/renderer/locales/agent-graph-copy.ts index e87370a159..7cefa80aec 100644 --- a/apps/desktop/src/renderer/locales/agent-graph-copy.ts +++ b/apps/desktop/src/renderer/locales/agent-graph-copy.ts @@ -37,6 +37,9 @@ export interface AgentGraphPanelCopy { openSession: string; operators: string; selectedResults: string; + liveOutput: string; + completedOutput: string; + throughput(tokensPerSecond: number): string; epoch: string; currentEpoch: string; historicalEpoch: string; @@ -64,6 +67,9 @@ const AGENT_GRAPH_PANEL_COPY = { openSession: '打开子任务', operators: 'Operators', selectedResults: '已选择结果', + liveOutput: '实时输出', + completedOutput: '结果预览', + throughput: (tokensPerSecond) => `${tokensPerSecond.toFixed(1)} token/s`, epoch: 'Graph 运行轮次', currentEpoch: '当前', historicalEpoch: '历史记录(只读)', @@ -110,6 +116,9 @@ const AGENT_GRAPH_PANEL_COPY = { openSession: '開啟子任務', operators: 'Operators', selectedResults: '已選取結果', + liveOutput: '即時輸出', + completedOutput: '結果預覽', + throughput: (tokensPerSecond) => `${tokensPerSecond.toFixed(1)} token/s`, epoch: 'Graph 執行輪次', currentEpoch: '目前', historicalEpoch: '歷史記錄(唯讀)', @@ -156,6 +165,9 @@ const AGENT_GRAPH_PANEL_COPY = { openSession: 'Open child task', operators: 'Operators', selectedResults: 'Selected results', + liveOutput: 'Live output', + completedOutput: 'Result preview', + throughput: (tokensPerSecond) => `${tokensPerSecond.toFixed(1)} token/s`, epoch: 'Graph run', currentEpoch: 'Current', historicalEpoch: 'History (read-only)', From 44366b7d4cd6733d3bbdcb7d922b79229b2e0d3c Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:45:35 +0800 Subject: [PATCH 05/17] fix(runtime): remove unreachable output timestamp fallback --- packages/runtime/src/stream-graph-projection.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/stream-graph-projection.ts b/packages/runtime/src/stream-graph-projection.ts index c18063bdbc..4f8bd3fbc8 100644 --- a/packages/runtime/src/stream-graph-projection.ts +++ b/packages/runtime/src/stream-graph-projection.ts @@ -354,9 +354,9 @@ function projectOperatorOutput( const latestUsage = usageEvents.at(-1); const outputTokens = latestUsage?.actions?.tokenUsage?.output; const usageEndedAt = latestUsage?.ts; - const sampleStartedAt = - orderedEvents.find((event) => event.role === 'model' && event.content?.kind === 'text')?.ts ?? - textEvents[0]!.ts; + const sampleStartedAt = orderedEvents.find( + (event) => event.role === 'model' && event.content?.kind === 'text', + )!.ts; const sampleDurationMs = outputTokens !== undefined && outputTokens > 0 && usageEndedAt !== undefined ? Math.max(0, usageEndedAt - sampleStartedAt) From 7ef35d23fc5055e009c15b254a195e9672e7bc11 Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Fri, 18 Sep 2026 10:55:42 +0900 Subject: [PATCH 06/17] fix(graph): bound queued output projection flushes --- .../runtime/src/stream-graph-coordinator.ts | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/packages/runtime/src/stream-graph-coordinator.ts b/packages/runtime/src/stream-graph-coordinator.ts index b3409fc741..b08cf827da 100644 --- a/packages/runtime/src/stream-graph-coordinator.ts +++ b/packages/runtime/src/stream-graph-coordinator.ts @@ -162,6 +162,7 @@ interface GraphDriver { stopTask?: Promise; clientProjectionTask?: Promise; clientProjectionDirty: boolean; + outputProjectionFlushScheduled: boolean; pendingOutputDeltas: Map< string, { @@ -1188,26 +1189,45 @@ export class AgentGraphCoordinator { } const next = { event, activationHadError, sampleStartedAt: event.event.ts }; driver.pendingOutputDeltas.set(operatorId, next); - const due = new Promise((resolve) => { - setTimeout(resolve, OUTPUT_DELTA_PROJECTION_INTERVAL_MS); - }); + this.#scheduleOutputProjectionFlush(driver); + } + + #scheduleOutputProjectionFlush(driver: GraphDriver): void { + if (driver.outputProjectionFlushScheduled) return; + driver.outputProjectionFlushScheduled = true; this.#queueClientProjectionUpdate(driver, async () => { - await due; - driver.pendingOutputDeltas.delete(operatorId); - await this.#advanceClientProjection( - driver, - next.event, - next.activationHadError, - next.sampleStartedAt, - ); + try { + await new Promise((resolve) => { + setTimeout(resolve, OUTPUT_DELTA_PROJECTION_INTERVAL_MS); + }); + const pending = [...driver.pendingOutputDeltas.values()]; + driver.pendingOutputDeltas.clear(); + for (const delta of pending) { + await this.#advanceClientProjection( + driver, + delta.event, + delta.activationHadError, + delta.sampleStartedAt, + ); + } + } finally { + driver.outputProjectionFlushScheduled = false; + if (driver.pendingOutputDeltas.size > 0) { + this.#scheduleOutputProjectionFlush(driver); + } + } }); } async #waitForClientProjectionUpdates(driver: GraphDriver): Promise { - await driver.clientProjectionTask?.catch(() => { - // A best-effort repair or later durable observation may repair this - // derived read side; graph authority never depends on it. - }); + while (driver.clientProjectionTask) { + const task = driver.clientProjectionTask; + await task.catch(() => { + // A best-effort repair or later durable observation may repair this + // derived read side; graph authority never depends on it. + }); + if (driver.clientProjectionTask === task) return; + } } async #repairClientProjectionBestEffort(driver: GraphDriver): Promise { @@ -1628,6 +1648,7 @@ export class AgentGraphCoordinator { closed: false, reconciliationReaders: 0, clientProjectionDirty: false, + outputProjectionFlushScheduled: false, pendingOutputDeltas: new Map(), runtimeFailureRunIds: new Set(), yieldWaiters: new Set(), From 6f5228aa5682f6d2f666394a5312367ffa5a8c11 Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:16:48 +0900 Subject: [PATCH 07/17] ci: rerun checks From be0846da91ab764b8802451374c5846c7c2c4fed Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:50:38 +0900 Subject: [PATCH 08/17] ci: rerun checks From cac39fe1b1716e463aa28519a947ef3cda74317d Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Fri, 18 Sep 2026 15:29:12 +0900 Subject: [PATCH 09/17] fix(graph): bound projection reads to queued task tail Generated-by: OpenAI Codex --- packages/runtime/src/stream-graph-coordinator.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/stream-graph-coordinator.ts b/packages/runtime/src/stream-graph-coordinator.ts index b08cf827da..beb064c147 100644 --- a/packages/runtime/src/stream-graph-coordinator.ts +++ b/packages/runtime/src/stream-graph-coordinator.ts @@ -1220,14 +1220,11 @@ export class AgentGraphCoordinator { } async #waitForClientProjectionUpdates(driver: GraphDriver): Promise { - while (driver.clientProjectionTask) { - const task = driver.clientProjectionTask; - await task.catch(() => { - // A best-effort repair or later durable observation may repair this - // derived read side; graph authority never depends on it. - }); - if (driver.clientProjectionTask === task) return; - } + const task = driver.clientProjectionTask; + await task?.catch(() => { + // A best-effort repair or later durable observation may repair this + // derived read side; graph authority never depends on it. + }); } async #repairClientProjectionBestEffort(driver: GraphDriver): Promise { From 03df76e340b2ffa2d3c9299cc95677405fd87892 Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:19:17 +0900 Subject: [PATCH 10/17] ci: rerun checks Generated-by: OpenAI Codex From f7b1dbee28a268e8b174e45320933b83d3ca358d Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:38:52 +0900 Subject: [PATCH 11/17] test(desktop): select skill draft token by keyboard Generated-by: OpenAI Codex --- apps/desktop/e2e/skill-draft-lifecycle.spec.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts index 227a8bc2a7..ee62bb612f 100644 --- a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts +++ b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts @@ -49,14 +49,15 @@ async function seedEditableTurn(page: Page): Promise { } /** Type the draft, then append the Skill chip — the order a user works in. */ -async function composeWithSkill(page: Page, text: string, name: RegExp): Promise { +async function composeWithSkill(page: Page, text: string, name: string, skillId: string): Promise { const composer = page.locator(COMPOSER_INPUT); await composer.fill(text); await composer.click(); - await composer.pressSequentially(' /'); + await composer.pressSequentially(` /${skillId}`); const option = page.getByRole('listbox', { name: /技能/ }).getByRole('option', { name }); await expect(option).toBeVisible(); - await option.click(); + await composer.press('Enter'); + await expect(page.locator(`[data-astryx-token-value="/skill:${skillId}"]`)).toBeVisible(); } async function beginRevision(page: Page): Promise { @@ -89,7 +90,7 @@ test('a successful revision retry clears both child and source drafts', async ({ await openInstalledWorkspaceSkill(page); await seedEditableTurn(page); await beginRevision(page); - await composeWithSkill(page, 'edited with skill', /Workspace Only/); + await composeWithSkill(page, 'edited with skill', 'Workspace Only', 'workspace-only'); await failWorkspaceSkillRevision(page); const enabled = await page.evaluate(() => @@ -114,9 +115,9 @@ test('cancelling a failed revision restores the complete pre-edit draft', async await seedEditableTurn(page); const composer = page.locator(COMPOSER_INPUT); - await composeWithSkill(page, 'previous unsent draft', /Project Only/); + await composeWithSkill(page, 'previous unsent draft', 'Project Only', 'project-only'); await beginRevision(page); - await composeWithSkill(page, 'edited with skill', /Workspace Only/); + await composeWithSkill(page, 'edited with skill', 'Workspace Only', 'workspace-only'); await failWorkspaceSkillRevision(page); await page.getByRole('button', { name: '取消' }).click(); From eaa079349cd2ac526847b550766e773e29d4688f Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:54:29 +0900 Subject: [PATCH 12/17] ci: rerun Storybook smoke Generated-by: OpenAI Codex From e2cb92a901396930befee567dd10266f23c59098 Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Sun, 20 Sep 2026 15:38:20 +0900 Subject: [PATCH 13/17] fix(runtime): harden agent graph output previews --- .../__tests__/stream-graph-read-model.test.ts | 83 +++++++++++++++++++ .../runtime/src/stream-graph-coordinator.ts | 6 +- .../runtime/src/stream-graph-projection.ts | 3 +- .../runtime/src/stream-graph-read-model.ts | 16 +++- packages/ui/src/model-picker-internals.tsx | 1 + 5 files changed, 100 insertions(+), 9 deletions(-) diff --git a/packages/runtime/src/__tests__/stream-graph-read-model.test.ts b/packages/runtime/src/__tests__/stream-graph-read-model.test.ts index 1526031c55..b956a7c6ea 100644 --- a/packages/runtime/src/__tests__/stream-graph-read-model.test.ts +++ b/packages/runtime/src/__tests__/stream-graph-read-model.test.ts @@ -552,6 +552,89 @@ describe('agent graph client read model', () => { assert.equal(settled.operator.operator.output?.phase, 'completed'); assert.equal(settled.snapshot.operators[0]?.output?.phase, 'completed'); }); + + test('marks a non-contiguous output fragment as truncated', () => { + const graphId = 'graph-gap'; + const operatorId = 'operator-gap'; + const childSessionId = 'child-gap'; + const initial = materializeAgentGraphClientProjection( + runningInput(graphId, operatorId, childSessionId), + ); + const first = advanceMaterializedAgentGraphClientProjection( + initial.snapshot, + initial.operators[0]!, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'gap-first', + type: 'text_delta', + ts: 1_000, + messageId: 'gap-message', + startOffset: 0, + text: 'first', + }), + false, + )!; + const fragment = advanceMaterializedAgentGraphClientProjection( + first.snapshot, + first.operator, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'gap-fragment', + type: 'text_delta', + ts: 2_000, + messageId: 'gap-message', + startOffset: 20, + text: 'tail', + }), + false, + )!; + + assert.equal(fragment.operator.operator.output?.preview, 'tail'); + assert.equal(fragment.operator.operator.output?.previewTruncated, true); + }); + + test('ignores late output from an older activation', () => { + const graphId = 'graph-late'; + const operatorId = 'operator-late'; + const childSessionId = 'child-late'; + const initial = materializeAgentGraphClientProjection( + runningInput(graphId, operatorId, childSessionId), + ); + const inspection = structuredClone(initial.operators[0]!); + inspection.operator.currentActivation = { + activationId: 'run-1', + status: 'running', + recordCount: 0, + firstEventTime: 2_000, + lastEventTime: 2_000, + run: { sessionId: childSessionId, agentRunId: 'run-1', turnId: 'turn-1' }, + }; + inspection.operator.output = { + activationId: 'run-1', + preview: 'current output', + previewTruncated: false, + phase: 'streaming', + previewUpdatedAt: 2_000, + sourceEventId: 'current-event', + messageId: 'current-message', + sampleStartedAt: 2_000, + }; + + const stale = advanceMaterializedAgentGraphClientProjection( + initial.snapshot, + inspection, + outputRuntimeEvent(graphId, operatorId, childSessionId, { + id: 'late-event', + type: 'text_delta', + ts: 3_000, + messageId: 'old-message', + startOffset: 0, + text: 'stale output', + }), + false, + ); + + assert.equal(stale, undefined); + assert.equal(inspection.operator.output.preview, 'current output'); + }); }); function emptyInput(graphId: string) { diff --git a/packages/runtime/src/stream-graph-coordinator.ts b/packages/runtime/src/stream-graph-coordinator.ts index beb064c147..f8a8c4e688 100644 --- a/packages/runtime/src/stream-graph-coordinator.ts +++ b/packages/runtime/src/stream-graph-coordinator.ts @@ -40,6 +40,7 @@ import { decodeAgentGraphIntentClaim } from '@maka/core/agent-graph-control'; import type { MakaTool } from './tool-runtime.js'; import type { SessionManager } from './session-manager.js'; import { + AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS, readCommittedAgentGraphProjection, type AgentGraphRecord, } from './stream-graph-projection.js'; @@ -61,7 +62,6 @@ import { } from './stream-graph-schedule-reconcile.js'; import { AGENT_GRAPH_CLIENT_TERMINAL_PAGE_SIZE, - MAX_OUTPUT_PREVIEW_CODE_POINTS, advanceMaterializedAgentGraphClientProjection, buildAgentGraphClientSnapshot, decodeAgentGraphTerminalCursor, @@ -1169,13 +1169,13 @@ export class AgentGraphCoordinator { current.startOffset === previous.startOffset + previous.text.length)) ) { const joined = Array.from(previous.text + current.text); - const truncated = joined.length > MAX_OUTPUT_PREVIEW_CODE_POINTS + 1; + const truncated = joined.length > AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS + 1; pending.event = { ...event, event: { ...current, text: truncated - ? joined.slice(-(MAX_OUTPUT_PREVIEW_CODE_POINTS + 1)).join('') + ? joined.slice(-(AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS + 1)).join('') : joined.join(''), startOffset: truncated ? undefined : previous.startOffset, }, diff --git a/packages/runtime/src/stream-graph-projection.ts b/packages/runtime/src/stream-graph-projection.ts index bd975f936c..ac383a3aa7 100644 --- a/packages/runtime/src/stream-graph-projection.ts +++ b/packages/runtime/src/stream-graph-projection.ts @@ -26,6 +26,7 @@ import { stableHash, stableStringify } from './request-shape.js'; import { compareAgentGraphIdentity } from './stream-graph-identity.js'; export const AGENT_GRAPH_RECORD_SCHEMA_VERSION = 1 as const; +export const AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS = 280; export const AGENT_GRAPH_RECORD_FACETS = [ 'message', @@ -313,8 +314,6 @@ export async function readCommittedAgentGraphProjectionWithRuns( }; } -const AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS = 280; - function projectOperatorOutputs( streams: readonly AgentGraphRunStream[], ): AgentGraphOperatorOutputProjection[] { diff --git a/packages/runtime/src/stream-graph-read-model.ts b/packages/runtime/src/stream-graph-read-model.ts index 831ba6c5dd..e80107401c 100644 --- a/packages/runtime/src/stream-graph-read-model.ts +++ b/packages/runtime/src/stream-graph-read-model.ts @@ -34,6 +34,7 @@ import type { AgentGraphRecordFacet, AgentGraphSupervisorSignal, } from './stream-graph-projection.js'; +import { AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS } from './stream-graph-projection.js'; import type { AgentGraphReadinessWait } from './stream-graph-readiness.js'; import { projectAgentGraphSchedule, @@ -66,7 +67,6 @@ const MAX_OPERATOR_INSPECTION_EDGES = 512; const MAX_OPERATOR_INSPECTION_WORK = 256; const MAX_OPERATOR_INSPECTION_CLAIMS = 256; const MAX_INSTRUCTION_PREVIEW_CHARS = 500; -export const MAX_OUTPUT_PREVIEW_CODE_POINTS = 280; export type AgentGraphClientOperatorStatus = | 'not_started' @@ -377,6 +377,7 @@ export function advanceMaterializedAgentGraphClientProjection( runtime.claim.targetRunId, ['completed', 'failed', 'aborted', 'cancelled'].includes(inspectionInput.operator.status), sampleStartedAt, + inspectionInput.operator.currentActivation?.activationId, ); if (!projected && !output) return undefined; if ( @@ -868,7 +869,9 @@ function advanceClientOperatorOutput( activationId: string, operatorSettled: boolean, sampleStartedAt?: number, + currentActivationId?: string, ): AgentGraphClientOperatorOutput | undefined { + if (currentActivationId && currentActivationId !== activationId) return undefined; if (current?.activationId === activationId && current.sourceEventId === event.id) { return undefined; } @@ -883,13 +886,18 @@ function advanceClientOperatorOutput( if (event.type === 'text_delta' || event.type === 'text_complete') { const sameMessage = existing?.messageId === event.messageId; const append = event.type === 'text_delta' && sameMessage; + const discontinuous = + append && + event.startOffset !== undefined && + !existing.previewTruncated && + event.startOffset > existing.preview.length; const text = append ? foldClientOutputDelta(existing, event) : event.text; const preview = boundClientOutputPreview(text); if (!preview.text) return undefined; return { activationId, preview: preview.text, - previewTruncated: preview.truncated || (append && existing.previewTruncated), + previewTruncated: preview.truncated || discontinuous || (append && existing.previewTruncated), phase: operatorSettled || existing?.phase === 'completed' ? 'completed' : 'streaming', previewUpdatedAt: event.ts, sourceEventId: event.id, @@ -936,10 +944,10 @@ function foldClientOutputDelta( function boundClientOutputPreview(text: string): { text: string; truncated: boolean } { const codePoints = Array.from(text); - if (codePoints.length <= MAX_OUTPUT_PREVIEW_CODE_POINTS) { + if (codePoints.length <= AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS) { return { text: codePoints.join(''), truncated: false }; } - const visible = codePoints.slice(-MAX_OUTPUT_PREVIEW_CODE_POINTS); + const visible = codePoints.slice(-AGENT_GRAPH_OUTPUT_PREVIEW_MAX_CODE_POINTS); return { text: visible.join(''), truncated: true }; } diff --git a/packages/ui/src/model-picker-internals.tsx b/packages/ui/src/model-picker-internals.tsx index 8a41863182..5e6acf1bb6 100644 --- a/packages/ui/src/model-picker-internals.tsx +++ b/packages/ui/src/model-picker-internals.tsx @@ -37,6 +37,7 @@ export interface ModelPickerLeadingOption { providerType?: ProviderType; disabled?: boolean; } + type ModelChoiceValueFn = (choice: ModelMenuGroup['choices'][number]) => string; export function providerMarkIcon( From d20a3d77e755bffafe1ea17488c5c731cb4e2adb Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Sun, 20 Sep 2026 15:38:47 +0900 Subject: [PATCH 14/17] fix(desktop): isolate agent graph preview text --- apps/desktop/src/renderer/agent-graph-panel.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/agent-graph-panel.tsx b/apps/desktop/src/renderer/agent-graph-panel.tsx index c1fccf36cb..1a64a5a919 100644 --- a/apps/desktop/src/renderer/agent-graph-panel.tsx +++ b/apps/desktop/src/renderer/agent-graph-panel.tsx @@ -387,8 +387,10 @@ export function AgentGraphPanel(props: { : ` · ${copy.throughput(operator.output.tokensPerSecond)}`} - {operator.output.previewTruncated ? '…' : ''} - {operator.output.preview} + {operator.output.previewTruncated ? ( + + ) : null} + {operator.output.preview} ) : null} From ca7d9afea7262f00927a41baf54dbc1dc37d0336 Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:38:54 +0900 Subject: [PATCH 15/17] test(desktop): submit disabled skill draft explicitly --- apps/desktop/e2e/skill-draft-lifecycle.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts index ee62bb612f..5085302e09 100644 --- a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts +++ b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts @@ -18,7 +18,7 @@ */ import type { Page } from '@playwright/test'; -import { expect, test, COMPOSER_INPUT } from './fixtures'; +import { awaitSendReady, expect, test, COMPOSER_INPUT } from './fixtures'; /** * Revision drafts, per session, with a Skill staged in them. @@ -74,7 +74,8 @@ async function failWorkspaceSkillRevision(page: Page): Promise { expect(disabled.ok).toBe(true); const composer = page.locator(COMPOSER_INPUT); - await composer.press('Enter'); + await awaitSendReady(page); + await page.locator('.maka-composer button[type="submit"]').click(); await expect(page.getByText('Skill 调用失败,消息未发送')).toBeVisible(); // The draft survives the rejection whole, and reads as the token rather than // as a chip: the Skill was just disabled, so it is gone from the catalog the From 8508fdfd02b25fb31e13614ddf5155ff7b284d87 Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:04:57 +0900 Subject: [PATCH 16/17] test(desktop): submit skill drafts through form --- apps/desktop/e2e/skill-draft-lifecycle.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts index 5085302e09..e3a97f3e0c 100644 --- a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts +++ b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts @@ -75,7 +75,7 @@ async function failWorkspaceSkillRevision(page: Page): Promise { const composer = page.locator(COMPOSER_INPUT); await awaitSendReady(page); - await page.locator('.maka-composer button[type="submit"]').click(); + await page.locator('form.maka-composer').evaluate((form: HTMLFormElement) => form.requestSubmit()); await expect(page.getByText('Skill 调用失败,消息未发送')).toBeVisible(); // The draft survives the rejection whole, and reads as the token rather than // as a chip: the Skill was just disabled, so it is gone from the catalog the From 6432312838c90a67529e65e2115d8988ebb1b443 Mon Sep 17 00:00:00 2001 From: testikun <320479488+testikun@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:02:05 +0900 Subject: [PATCH 17/17] chore(runtime-host): repin external session declaration --- .../external-session-cwd-limit-authority.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-host/protocol-compatible-changes/external-session-cwd-limit-authority.json b/packages/runtime-host/protocol-compatible-changes/external-session-cwd-limit-authority.json index 3f3bde03e2..71dc521e25 100644 --- a/packages/runtime-host/protocol-compatible-changes/external-session-cwd-limit-authority.json +++ b/packages/runtime-host/protocol-compatible-changes/external-session-cwd-limit-authority.json @@ -1,5 +1,5 @@ { - "epoch": 165, + "epoch": 170, "files": ["packages/runtime-host/src/protocol/external-session.ts"], "reason": "Re-exports the unchanged 4096-byte cwd limit from its core authority without changing any codec, field, accepted value, or encoded result" }