fix(server): restore wire compatibility for old clients
This commit is contained in:
@@ -237,6 +237,11 @@ export type ToolCallDetail =
|
|||||||
description?: string;
|
description?: string;
|
||||||
childSessionId?: string;
|
childSessionId?: string;
|
||||||
log: string;
|
log: string;
|
||||||
|
actions?: Array<{
|
||||||
|
index: number;
|
||||||
|
toolName: string;
|
||||||
|
summary?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "plain_text";
|
type: "plain_text";
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export class ClaudeSidechainTracker {
|
|||||||
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
||||||
)
|
)
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
|
actions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -851,6 +851,7 @@ function mapCollabAgentToolCallItem(
|
|||||||
subAgentType: "Sub-agent",
|
subAgentType: "Sub-agent",
|
||||||
...(item.prompt ? { description: item.prompt } : {}),
|
...(item.prompt ? { description: item.prompt } : {}),
|
||||||
log: "",
|
log: "",
|
||||||
|
actions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status === "failed") {
|
if (status === "failed") {
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ function deriveOpencodeTaskDetail(
|
|||||||
...(description ? { description } : {}),
|
...(description ? { description } : {}),
|
||||||
...(childSessionId ? { childSessionId } : {}),
|
...(childSessionId ? { childSessionId } : {}),
|
||||||
log,
|
log,
|
||||||
|
actions: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import { basename, resolve, sep } from "path";
|
|||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { ToolSet } from "ai";
|
import type { ToolSet } from "ai";
|
||||||
|
import {
|
||||||
|
CLIENT_CAPS,
|
||||||
|
readDeclaredClientCapabilities,
|
||||||
|
type ClientCapability,
|
||||||
|
} from "../shared/client-capabilities.js";
|
||||||
import {
|
import {
|
||||||
isLegacyEditorTargetId,
|
isLegacyEditorTargetId,
|
||||||
serializeAgentStreamEvent,
|
serializeAgentStreamEvent,
|
||||||
@@ -552,6 +557,7 @@ interface VoiceTranscriptionResultPayload {
|
|||||||
export interface SessionOptions {
|
export interface SessionOptions {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
appVersion?: string | null;
|
appVersion?: string | null;
|
||||||
|
clientCapabilities?: Record<string, unknown> | null;
|
||||||
onMessage: (msg: SessionOutboundMessage) => void;
|
onMessage: (msg: SessionOutboundMessage) => void;
|
||||||
onBinaryMessage?: (frame: Uint8Array) => void;
|
onBinaryMessage?: (frame: Uint8Array) => void;
|
||||||
onLifecycleIntent?: (intent: SessionLifecycleIntent) => void;
|
onLifecycleIntent?: (intent: SessionLifecycleIntent) => void;
|
||||||
@@ -725,6 +731,22 @@ function convertPCMToWavBuffer(
|
|||||||
return wavBuffer;
|
return wavBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ClientCapabilities {
|
||||||
|
private readonly supported: ReadonlySet<ClientCapability>;
|
||||||
|
|
||||||
|
constructor(capabilities: Iterable<ClientCapability>) {
|
||||||
|
this.supported = new Set(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHello(capabilities: Record<string, unknown> | null | undefined): ClientCapabilities {
|
||||||
|
return new ClientCapabilities(readDeclaredClientCapabilities(capabilities));
|
||||||
|
}
|
||||||
|
|
||||||
|
supports(capability: ClientCapability): boolean {
|
||||||
|
return this.supported.has(capability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session represents a single connected client session.
|
* Session represents a single connected client session.
|
||||||
* It owns all state management, orchestration logic, and message processing.
|
* It owns all state management, orchestration logic, and message processing.
|
||||||
@@ -733,6 +755,7 @@ function convertPCMToWavBuffer(
|
|||||||
export class Session {
|
export class Session {
|
||||||
private readonly clientId: string;
|
private readonly clientId: string;
|
||||||
private appVersion: string | null;
|
private appVersion: string | null;
|
||||||
|
private clientCapabilities: ClientCapabilities;
|
||||||
private readonly sessionId: string;
|
private readonly sessionId: string;
|
||||||
private readonly onMessage: (msg: SessionOutboundMessage) => void;
|
private readonly onMessage: (msg: SessionOutboundMessage) => void;
|
||||||
private readonly onBinaryMessage: ((frame: Uint8Array) => void) | null;
|
private readonly onBinaryMessage: ((frame: Uint8Array) => void) | null;
|
||||||
@@ -849,6 +872,7 @@ export class Session {
|
|||||||
const {
|
const {
|
||||||
clientId,
|
clientId,
|
||||||
appVersion,
|
appVersion,
|
||||||
|
clientCapabilities,
|
||||||
onMessage,
|
onMessage,
|
||||||
onBinaryMessage,
|
onBinaryMessage,
|
||||||
onLifecycleIntent,
|
onLifecycleIntent,
|
||||||
@@ -888,6 +912,7 @@ export class Session {
|
|||||||
} = options;
|
} = options;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.appVersion = appVersion ?? null;
|
this.appVersion = appVersion ?? null;
|
||||||
|
this.clientCapabilities = ClientCapabilities.fromHello(clientCapabilities);
|
||||||
this.sessionId = uuidv4();
|
this.sessionId = uuidv4();
|
||||||
this.onMessage = onMessage;
|
this.onMessage = onMessage;
|
||||||
this.onBinaryMessage = onBinaryMessage ?? null;
|
this.onBinaryMessage = onBinaryMessage ?? null;
|
||||||
@@ -959,6 +984,14 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateClientCapabilities(capabilities: Record<string, unknown> | null): void {
|
||||||
|
this.clientCapabilities = ClientCapabilities.fromHello(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
supports(capability: ClientCapability): boolean {
|
||||||
|
return this.clientCapabilities.supports(capability);
|
||||||
|
}
|
||||||
|
|
||||||
async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
|
async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
|
||||||
const descriptor = await this.describeWorkspaceRecordWithGitData(workspace);
|
const descriptor = await this.describeWorkspaceRecordWithGitData(workspace);
|
||||||
this.syncWorkspaceGitObservers([descriptor]);
|
this.syncWorkspaceGitObservers([descriptor]);
|
||||||
@@ -7204,7 +7237,9 @@ export class Session {
|
|||||||
seqStart: entry.seqStart,
|
seqStart: entry.seqStart,
|
||||||
seqEnd: entry.seqEnd,
|
seqEnd: entry.seqEnd,
|
||||||
sourceSeqRanges: entry.sourceSeqRanges,
|
sourceSeqRanges: entry.sourceSeqRanges,
|
||||||
collapsed: entry.collapsed,
|
collapsed: this.supports(CLIENT_CAPS.reasoningMergeEnum)
|
||||||
|
? entry.collapsed
|
||||||
|
: entry.collapsed.filter((value) => value !== "reasoning_merge"),
|
||||||
})),
|
})),
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ interface SessionConnection {
|
|||||||
session: Session;
|
session: Session;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
appVersion: string | null;
|
appVersion: string | null;
|
||||||
|
clientCapabilities: Record<string, unknown> | null;
|
||||||
connectionLogger: pino.Logger;
|
connectionLogger: pino.Logger;
|
||||||
sockets: Set<WebSocketLike>;
|
sockets: Set<WebSocketLike>;
|
||||||
externalDisconnectCleanupTimeout: ReturnType<typeof setTimeout> | null;
|
externalDisconnectCleanupTimeout: ReturnType<typeof setTimeout> | null;
|
||||||
@@ -874,14 +875,16 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
ws: WebSocketLike;
|
ws: WebSocketLike;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
appVersion: string | null;
|
appVersion: string | null;
|
||||||
|
clientCapabilities: Record<string, unknown> | null;
|
||||||
connectionLogger: pino.Logger;
|
connectionLogger: pino.Logger;
|
||||||
}): SessionConnection {
|
}): SessionConnection {
|
||||||
const { ws, clientId, appVersion, connectionLogger } = params;
|
const { ws, clientId, appVersion, clientCapabilities, connectionLogger } = params;
|
||||||
let connection: SessionConnection | null = null;
|
let connection: SessionConnection | null = null;
|
||||||
|
|
||||||
const session = new Session({
|
const session = new Session({
|
||||||
clientId,
|
clientId,
|
||||||
appVersion,
|
appVersion,
|
||||||
|
clientCapabilities,
|
||||||
onMessage: (msg) => {
|
onMessage: (msg) => {
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return;
|
return;
|
||||||
@@ -958,6 +961,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
session,
|
session,
|
||||||
clientId,
|
clientId,
|
||||||
appVersion,
|
appVersion,
|
||||||
|
clientCapabilities,
|
||||||
connectionLogger,
|
connectionLogger,
|
||||||
sockets: new Set([ws]),
|
sockets: new Set([ws]),
|
||||||
externalDisconnectCleanupTimeout: null,
|
externalDisconnectCleanupTimeout: null,
|
||||||
@@ -1027,6 +1031,14 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
existing.appVersion = newAppVersion;
|
existing.appVersion = newAppVersion;
|
||||||
existing.session.updateAppVersion(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);
|
existing.sockets.add(ws);
|
||||||
this.sessions.set(ws, existing);
|
this.sessions.set(ws, existing);
|
||||||
this.sendToClient(ws, this.createServerInfoMessage());
|
this.sendToClient(ws, this.createServerInfoMessage());
|
||||||
@@ -1047,6 +1059,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
ws,
|
ws,
|
||||||
clientId,
|
clientId,
|
||||||
appVersion: message.appVersion ?? null,
|
appVersion: message.appVersion ?? null,
|
||||||
|
clientCapabilities: message.capabilities ?? null,
|
||||||
connectionLogger,
|
connectionLogger,
|
||||||
});
|
});
|
||||||
this.sessions.set(ws, connection);
|
this.sessions.set(ws, connection);
|
||||||
|
|||||||
@@ -163,6 +163,23 @@ interface HandleCreatePaseoWorktreeRequestDependencies {
|
|||||||
) => Promise<CreatePaseoWorktreeWorkflowResult>;
|
) => Promise<CreatePaseoWorktreeWorkflowResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFirstAgentContext(
|
||||||
|
request: Extract<SessionInboundMessage, { type: "create_paseo_worktree_request" }>,
|
||||||
|
): 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(
|
export async function buildAgentSessionConfig(
|
||||||
dependencies: BuildAgentSessionConfigDependencies,
|
dependencies: BuildAgentSessionConfigDependencies,
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
@@ -492,7 +509,7 @@ export async function handleCreatePaseoWorktreeRequest(
|
|||||||
const createdWorktree = await dependencies.createPaseoWorktreeWorkflow({
|
const createdWorktree = await dependencies.createPaseoWorktreeWorkflow({
|
||||||
cwd: request.cwd,
|
cwd: request.cwd,
|
||||||
worktreeSlug: request.worktreeSlug,
|
worktreeSlug: request.worktreeSlug,
|
||||||
firstAgentContext: request.firstAgentContext,
|
firstAgentContext: normalizeFirstAgentContext(request),
|
||||||
refName: request.refName,
|
refName: request.refName,
|
||||||
action: request.action,
|
action: request.action,
|
||||||
githubPrNumber: request.githubPrNumber,
|
githubPrNumber: request.githubPrNumber,
|
||||||
|
|||||||
@@ -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<string>(Object.values(CLIENT_CAPS));
|
||||||
|
|
||||||
|
export function isClientCapability(value: string): value is ClientCapability {
|
||||||
|
return CLIENT_CAPABILITY_SET.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDeclaredClientCapabilities(
|
||||||
|
capabilities: Record<string, unknown> | null | undefined,
|
||||||
|
): ClientCapability[] {
|
||||||
|
if (!capabilities) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(capabilities).flatMap(([key, value]) =>
|
||||||
|
value === true && isClientCapability(key) ? [key] : [],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { CLIENT_CAPS } from "./client-capabilities.js";
|
||||||
import { AGENT_LIFECYCLE_STATUSES } from "./agent-lifecycle.js";
|
import { AGENT_LIFECYCLE_STATUSES } from "./agent-lifecycle.js";
|
||||||
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "../server/agent/agent-title-limits.js";
|
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "../server/agent/agent-title-limits.js";
|
||||||
import { AgentProviderSchema } from "../server/agent/provider-manifest.js";
|
import { AgentProviderSchema } from "../server/agent/provider-manifest.js";
|
||||||
@@ -414,6 +415,15 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, z.ZodTypeDef, unkno
|
|||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
childSessionId: z.string().optional(),
|
childSessionId: z.string().optional(),
|
||||||
log: z.string(),
|
log: z.string(),
|
||||||
|
actions: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
index: z.number().int().positive(),
|
||||||
|
toolName: z.string(),
|
||||||
|
summary: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal("plain_text"),
|
type: z.literal("plain_text"),
|
||||||
@@ -1374,6 +1384,8 @@ export const CreatePaseoWorktreeRequestSchema = z.object({
|
|||||||
type: z.literal("create_paseo_worktree_request"),
|
type: z.literal("create_paseo_worktree_request"),
|
||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
worktreeSlug: z.string().optional(),
|
worktreeSlug: z.string().optional(),
|
||||||
|
nameContext: z.string().optional(),
|
||||||
|
attachments: AgentAttachmentsSchema.optional(),
|
||||||
firstAgentContext: FirstAgentContextSchema.optional(),
|
firstAgentContext: FirstAgentContextSchema.optional(),
|
||||||
refName: z.string().min(1).optional(),
|
refName: z.string().min(1).optional(),
|
||||||
action: z.enum(["branch-off", "checkout"]).optional(),
|
action: z.enum(["branch-off", "checkout"]).optional(),
|
||||||
@@ -3661,6 +3673,7 @@ export const WSHelloMessageSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
voice: z.boolean().optional(),
|
voice: z.boolean().optional(),
|
||||||
pushNotifications: z.boolean().optional(),
|
pushNotifications: z.boolean().optional(),
|
||||||
|
[CLIENT_CAPS.reasoningMergeEnum]: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user