mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(server): restore wire compatibility for old clients
This commit is contained in:
@@ -237,6 +237,11 @@ export type ToolCallDetail =
|
||||
description?: string;
|
||||
childSessionId?: string;
|
||||
log: string;
|
||||
actions?: Array<{
|
||||
index: number;
|
||||
toolName: string;
|
||||
summary?: string;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
type: "plain_text";
|
||||
|
||||
@@ -99,6 +99,7 @@ export class ClaudeSidechainTracker {
|
||||
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
||||
)
|
||||
.join("\n"),
|
||||
actions: [],
|
||||
};
|
||||
|
||||
return [
|
||||
|
||||
@@ -851,6 +851,7 @@ function mapCollabAgentToolCallItem(
|
||||
subAgentType: "Sub-agent",
|
||||
...(item.prompt ? { description: item.prompt } : {}),
|
||||
log: "",
|
||||
actions: [],
|
||||
};
|
||||
|
||||
if (status === "failed") {
|
||||
|
||||
@@ -111,6 +111,7 @@ function deriveOpencodeTaskDetail(
|
||||
...(description ? { description } : {}),
|
||||
...(childSessionId ? { childSessionId } : {}),
|
||||
log,
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, unknown> | 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<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.
|
||||
* 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<string, unknown> | null): void {
|
||||
this.clientCapabilities = ClientCapabilities.fromHello(capabilities);
|
||||
}
|
||||
|
||||
supports(capability: ClientCapability): boolean {
|
||||
return this.clientCapabilities.supports(capability);
|
||||
}
|
||||
|
||||
async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
|
||||
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,
|
||||
},
|
||||
|
||||
@@ -264,6 +264,7 @@ interface SessionConnection {
|
||||
session: Session;
|
||||
clientId: string;
|
||||
appVersion: string | null;
|
||||
clientCapabilities: Record<string, unknown> | null;
|
||||
connectionLogger: pino.Logger;
|
||||
sockets: Set<WebSocketLike>;
|
||||
externalDisconnectCleanupTimeout: ReturnType<typeof setTimeout> | null;
|
||||
@@ -874,14 +875,16 @@ export class VoiceAssistantWebSocketServer {
|
||||
ws: WebSocketLike;
|
||||
clientId: string;
|
||||
appVersion: string | null;
|
||||
clientCapabilities: Record<string, unknown> | 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);
|
||||
|
||||
@@ -163,6 +163,23 @@ interface HandleCreatePaseoWorktreeRequestDependencies {
|
||||
) => 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(
|
||||
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,
|
||||
|
||||
23
packages/server/src/shared/client-capabilities.ts
Normal file
23
packages/server/src/shared/client-capabilities.ts
Normal file
@@ -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 { 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<ToolCallDetail, z.ZodTypeDef, unkno
|
||||
description: z.string().optional(),
|
||||
childSessionId: z.string().optional(),
|
||||
log: z.string(),
|
||||
actions: z
|
||||
.array(
|
||||
z.object({
|
||||
index: z.number().int().positive(),
|
||||
toolName: z.string(),
|
||||
summary: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("plain_text"),
|
||||
@@ -1374,6 +1384,8 @@ export const CreatePaseoWorktreeRequestSchema = z.object({
|
||||
type: z.literal("create_paseo_worktree_request"),
|
||||
cwd: z.string(),
|
||||
worktreeSlug: z.string().optional(),
|
||||
nameContext: z.string().optional(),
|
||||
attachments: AgentAttachmentsSchema.optional(),
|
||||
firstAgentContext: FirstAgentContextSchema.optional(),
|
||||
refName: z.string().min(1).optional(),
|
||||
action: z.enum(["branch-off", "checkout"]).optional(),
|
||||
@@ -3661,6 +3673,7 @@ export const WSHelloMessageSchema = z.object({
|
||||
.object({
|
||||
voice: z.boolean().optional(),
|
||||
pushNotifications: z.boolean().optional(),
|
||||
[CLIENT_CAPS.reasoningMergeEnum]: z.boolean().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
|
||||
Reference in New Issue
Block a user