From e3e1c08525776ccae6d6a5bfcfd286cebc8647ef Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 2 May 2026 19:47:50 +0700 Subject: [PATCH] fix(server): restore wire compatibility for old clients --- .../src/server/agent/agent-sdk-types.ts | 5 +++ .../providers/claude/sidechain-tracker.ts | 1 + .../agent/providers/codex/tool-call-mapper.ts | 1 + .../opencode/tool-call-detail-parser.ts | 1 + packages/server/src/server/session.ts | 37 ++++++++++++++++++- .../server/src/server/websocket-server.ts | 15 +++++++- .../server/src/server/worktree-session.ts | 19 +++++++++- .../server/src/shared/client-capabilities.ts | 23 ++++++++++++ packages/server/src/shared/messages.ts | 13 +++++++ 9 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 packages/server/src/shared/client-capabilities.ts diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index b2d7f94aa..81d7a86be 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -237,6 +237,11 @@ export type ToolCallDetail = description?: string; childSessionId?: string; log: string; + actions?: Array<{ + index: number; + toolName: string; + summary?: string; + }>; } | { type: "plain_text"; diff --git a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts index 59f138f3f..7cea3f8b5 100644 --- a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts +++ b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts @@ -99,6 +99,7 @@ export class ClaudeSidechainTracker { action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`, ) .join("\n"), + actions: [], }; return [ diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index 80ec64c94..2ac389ba9 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -851,6 +851,7 @@ function mapCollabAgentToolCallItem( subAgentType: "Sub-agent", ...(item.prompt ? { description: item.prompt } : {}), log: "", + actions: [], }; if (status === "failed") { diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts index 517afc381..9c5a5bbcc 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts @@ -111,6 +111,7 @@ function deriveOpencodeTaskDetail( ...(description ? { description } : {}), ...(childSessionId ? { childSessionId } : {}), log, + actions: [], }; } diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index ebf011eba..28c350cf7 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -8,6 +8,11 @@ import { basename, resolve, sep } from "path"; import { homedir } from "node:os"; import { z } from "zod"; import type { ToolSet } from "ai"; +import { + CLIENT_CAPS, + readDeclaredClientCapabilities, + type ClientCapability, +} from "../shared/client-capabilities.js"; import { isLegacyEditorTargetId, serializeAgentStreamEvent, @@ -552,6 +557,7 @@ interface VoiceTranscriptionResultPayload { export interface SessionOptions { clientId: string; appVersion?: string | null; + clientCapabilities?: Record | null; onMessage: (msg: SessionOutboundMessage) => void; onBinaryMessage?: (frame: Uint8Array) => void; onLifecycleIntent?: (intent: SessionLifecycleIntent) => void; @@ -725,6 +731,22 @@ function convertPCMToWavBuffer( return wavBuffer; } +class ClientCapabilities { + private readonly supported: ReadonlySet; + + constructor(capabilities: Iterable) { + this.supported = new Set(capabilities); + } + + static fromHello(capabilities: Record | null | undefined): ClientCapabilities { + return new ClientCapabilities(readDeclaredClientCapabilities(capabilities)); + } + + supports(capability: ClientCapability): boolean { + return this.supported.has(capability); + } +} + /** * Session represents a single connected client session. * It owns all state management, orchestration logic, and message processing. @@ -733,6 +755,7 @@ function convertPCMToWavBuffer( export class Session { private readonly clientId: string; private appVersion: string | null; + private clientCapabilities: ClientCapabilities; private readonly sessionId: string; private readonly onMessage: (msg: SessionOutboundMessage) => void; private readonly onBinaryMessage: ((frame: Uint8Array) => void) | null; @@ -849,6 +872,7 @@ export class Session { const { clientId, appVersion, + clientCapabilities, onMessage, onBinaryMessage, onLifecycleIntent, @@ -888,6 +912,7 @@ export class Session { } = options; this.clientId = clientId; this.appVersion = appVersion ?? null; + this.clientCapabilities = ClientCapabilities.fromHello(clientCapabilities); this.sessionId = uuidv4(); this.onMessage = onMessage; this.onBinaryMessage = onBinaryMessage ?? null; @@ -959,6 +984,14 @@ export class Session { } } + updateClientCapabilities(capabilities: Record | null): void { + this.clientCapabilities = ClientCapabilities.fromHello(capabilities); + } + + supports(capability: ClientCapability): boolean { + return this.clientCapabilities.supports(capability); + } + async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise { const descriptor = await this.describeWorkspaceRecordWithGitData(workspace); this.syncWorkspaceGitObservers([descriptor]); @@ -7204,7 +7237,9 @@ export class Session { seqStart: entry.seqStart, seqEnd: entry.seqEnd, sourceSeqRanges: entry.sourceSeqRanges, - collapsed: entry.collapsed, + collapsed: this.supports(CLIENT_CAPS.reasoningMergeEnum) + ? entry.collapsed + : entry.collapsed.filter((value) => value !== "reasoning_merge"), })), error: null, }, diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 1da1d9bfb..c8d362a05 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -264,6 +264,7 @@ interface SessionConnection { session: Session; clientId: string; appVersion: string | null; + clientCapabilities: Record | null; connectionLogger: pino.Logger; sockets: Set; externalDisconnectCleanupTimeout: ReturnType | null; @@ -874,14 +875,16 @@ export class VoiceAssistantWebSocketServer { ws: WebSocketLike; clientId: string; appVersion: string | null; + clientCapabilities: Record | null; connectionLogger: pino.Logger; }): SessionConnection { - const { ws, clientId, appVersion, connectionLogger } = params; + const { ws, clientId, appVersion, clientCapabilities, connectionLogger } = params; let connection: SessionConnection | null = null; const session = new Session({ clientId, appVersion, + clientCapabilities, onMessage: (msg) => { if (!connection) { return; @@ -958,6 +961,7 @@ export class VoiceAssistantWebSocketServer { session, clientId, appVersion, + clientCapabilities, connectionLogger, sockets: new Set([ws]), externalDisconnectCleanupTimeout: null, @@ -1027,6 +1031,14 @@ export class VoiceAssistantWebSocketServer { existing.appVersion = newAppVersion; existing.session.updateAppVersion(newAppVersion); } + const newClientCapabilities = message.capabilities ?? null; + if ( + JSON.stringify(existing.clientCapabilities ?? null) !== + JSON.stringify(newClientCapabilities ?? null) + ) { + existing.clientCapabilities = newClientCapabilities; + existing.session.updateClientCapabilities(newClientCapabilities); + } existing.sockets.add(ws); this.sessions.set(ws, existing); this.sendToClient(ws, this.createServerInfoMessage()); @@ -1047,6 +1059,7 @@ export class VoiceAssistantWebSocketServer { ws, clientId, appVersion: message.appVersion ?? null, + clientCapabilities: message.capabilities ?? null, connectionLogger, }); this.sessions.set(ws, connection); diff --git a/packages/server/src/server/worktree-session.ts b/packages/server/src/server/worktree-session.ts index 719a8e6ad..5a28c1d76 100644 --- a/packages/server/src/server/worktree-session.ts +++ b/packages/server/src/server/worktree-session.ts @@ -163,6 +163,23 @@ interface HandleCreatePaseoWorktreeRequestDependencies { ) => Promise; } +function normalizeFirstAgentContext( + request: Extract, +): FirstAgentContext | undefined { + if (request.firstAgentContext) { + return request.firstAgentContext; + } + + if (request.attachments || request.nameContext) { + return { + attachments: request.attachments ?? [], + ...(request.nameContext ? { prompt: request.nameContext } : {}), + }; + } + + return undefined; +} + export async function buildAgentSessionConfig( dependencies: BuildAgentSessionConfigDependencies, config: AgentSessionConfig, @@ -492,7 +509,7 @@ export async function handleCreatePaseoWorktreeRequest( const createdWorktree = await dependencies.createPaseoWorktreeWorkflow({ cwd: request.cwd, worktreeSlug: request.worktreeSlug, - firstAgentContext: request.firstAgentContext, + firstAgentContext: normalizeFirstAgentContext(request), refName: request.refName, action: request.action, githubPrNumber: request.githubPrNumber, diff --git a/packages/server/src/shared/client-capabilities.ts b/packages/server/src/shared/client-capabilities.ts new file mode 100644 index 000000000..a0137c5ed --- /dev/null +++ b/packages/server/src/shared/client-capabilities.ts @@ -0,0 +1,23 @@ +export const CLIENT_CAPS = { + reasoningMergeEnum: "reasoning_merge_enum", +} as const; + +export type ClientCapability = (typeof CLIENT_CAPS)[keyof typeof CLIENT_CAPS]; + +const CLIENT_CAPABILITY_SET = new Set(Object.values(CLIENT_CAPS)); + +export function isClientCapability(value: string): value is ClientCapability { + return CLIENT_CAPABILITY_SET.has(value); +} + +export function readDeclaredClientCapabilities( + capabilities: Record | null | undefined, +): ClientCapability[] { + if (!capabilities) { + return []; + } + + return Object.entries(capabilities).flatMap(([key, value]) => + value === true && isClientCapability(key) ? [key] : [], + ); +} diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 59272bb6e..838b2da99 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { CLIENT_CAPS } from "./client-capabilities.js"; import { AGENT_LIFECYCLE_STATUSES } from "./agent-lifecycle.js"; import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "../server/agent/agent-title-limits.js"; import { AgentProviderSchema } from "../server/agent/provider-manifest.js"; @@ -414,6 +415,15 @@ const ToolCallDetailPayloadSchema: z.ZodType