From 8d4d445c857be250dcc23e0eee2f4c428c7ac013 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 6 Apr 2026 08:34:58 +0700 Subject: [PATCH] fix: migrate workspace/project IDs to string and widen schema enums - Change workspace id/projectId from z.number() to z.union([z.string(), z.number()]).transform(String) for backward compat - Add "non_git" to projectKind, "local_checkout" to workspaceKind enums - Session converts numeric IDs to strings at the descriptor boundary - Update tests and daemon-client event types to match --- .../app/src/hooks/use-open-project.test.ts | 4 +- packages/server/src/client/daemon-client.ts | 2 +- packages/server/src/server/session.ts | 45 ++++++++++--------- packages/server/src/shared/messages.ts | 18 ++++---- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/app/src/hooks/use-open-project.test.ts b/packages/app/src/hooks/use-open-project.test.ts index 285dd4a73..3bf87a915 100644 --- a/packages/app/src/hooks/use-open-project.test.ts +++ b/packages/app/src/hooks/use-open-project.test.ts @@ -70,8 +70,8 @@ describe("openProjectDirectly", () => { requestId: "request-1", error: null, workspace: { - id: 1, - projectId: 1, + id: "1", + projectId: "1", projectDisplayName: "project", projectRootPath: WORKSPACE_ID, workspaceDirectory: WORKSPACE_ID, diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index 009d256f7..073146093 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -127,7 +127,7 @@ export type DaemonEvent = } | { type: "workspace_update"; - workspaceId: number; + workspaceId: string; payload: Extract["payload"]; } | { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index db41ca7f1..47c9fce81 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -317,12 +317,12 @@ type WorkspaceUpdatesSubscriptionState = { subscriptionId: string; filter?: WorkspaceUpdatesFilter; isBootstrapping: boolean; - pendingUpdatesByWorkspaceId: Map; + pendingUpdatesByWorkspaceId: Map; }; type FetchWorkspacesCursor = { sort: FetchWorkspacesRequestSort[]; values: Record; - id: number; + id: string; }; class SessionRequestError extends Error { @@ -2729,8 +2729,8 @@ export class Session { labels, ); const resolvedWorkspace = - typeof msg.workspaceId === "number" - ? await this.workspaceRegistry.get(msg.workspaceId) + msg.workspaceId + ? await this.workspaceRegistry.get(Number(msg.workspaceId)) : (await this.findWorkspaceByDirectory(sessionConfig.cwd)) ?? (await this.findOrCreateWorkspaceForDirectory(sessionConfig.cwd)); if (!resolvedWorkspace) { @@ -4093,7 +4093,7 @@ export class Session { workspaces: Iterable, ): Promise { for (const workspace of workspaces) { - const persistedWorkspace = await this.workspaceRegistry.get(workspace.id); + const persistedWorkspace = await this.workspaceRegistry.get(Number(workspace.id)); if (!persistedWorkspace) { continue; } @@ -5181,17 +5181,19 @@ export class Session { if (cachedShortstat !== undefined) { diffStat = cachedShortstat; } else { - warmCheckoutShortstatInBackground(workspace.directory); + warmCheckoutShortstatInBackground(workspace.directory, undefined, () => { + void this.emitWorkspaceUpdateForCwd(workspace.directory); + }); } return { - id: workspace.id, - projectId: workspace.projectId, + id: String(workspace.id), + projectId: String(workspace.projectId), projectDisplayName: resolvedProjectRecord?.displayName ?? String(workspace.projectId), projectRootPath: resolvedProjectRecord?.directory ?? workspace.directory, workspaceDirectory: workspace.directory, - projectKind: resolvedProjectRecord?.kind ?? "directory", - workspaceKind: workspace.kind, + projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git", + workspaceKind: workspace.kind === "checkout" ? "local_checkout" : workspace.kind, name: workspace.displayName, status: "done", activityAt: null, @@ -5313,7 +5315,7 @@ export class Session { } return spec.direction === "asc" ? base : -base; } - return left.id - right.id; + return Number(left.id) - Number(right.id); } private encodeFetchWorkspacesCursor( @@ -5355,7 +5357,7 @@ export class Session { id?: unknown; }; - if (!Array.isArray(payload.sort) || typeof payload.id !== "number") { + if (!Array.isArray(payload.sort) || (typeof payload.id !== "number" && typeof payload.id !== "string")) { throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor"); } if (!payload.values || typeof payload.values !== "object") { @@ -5403,7 +5405,7 @@ export class Session { return { sort: cursorSort, values: payload.values as Record, - id: payload.id, + id: String(payload.id), }; } @@ -5422,7 +5424,7 @@ export class Session { } return spec.direction === "asc" ? base : -base; } - return workspace.id - cursor.id; + return Number(workspace.id) - Number(cursor.id); } private matchesWorkspaceFilter(input: { @@ -5511,7 +5513,7 @@ export class Session { } private flushBootstrappedWorkspaceUpdates(options?: { - snapshotLatestActivityByWorkspaceId?: Map; + snapshotLatestActivityByWorkspaceId?: Map; }): void { const subscription = this.workspaceUpdatesSubscription; if (!subscription || !subscription.isBootstrapping) { @@ -5687,7 +5689,7 @@ export class Session { const persistedWorkspace = await this.findWorkspaceByDirectory(normalizedCwd); const all = await this.listWorkspaceDescriptorsSnapshot(); const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry] as const)); - const workspaceIdsToEmit = persistedWorkspace ? [persistedWorkspace.id] : []; + const workspaceIdsToEmit = persistedWorkspace ? [String(persistedWorkspace.id)] : []; for (const nextWorkspaceId of workspaceIdsToEmit) { const workspace = descriptorsByWorkspaceId.get(nextWorkspaceId); @@ -5738,7 +5740,7 @@ export class Session { for (const workspaceCwd of uniqueWorkspaceCwds) { const persistedWorkspace = await this.findWorkspaceByDirectory(workspaceCwd); - const workspace = persistedWorkspace ? descriptorsByWorkspaceId.get(persistedWorkspace.id) : null; + const workspace = persistedWorkspace ? descriptorsByWorkspaceId.get(String(persistedWorkspace.id)) : null; const nextWorkspace = workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter }) ? workspace @@ -5749,7 +5751,7 @@ export class Session { if (persistedWorkspace) { this.bufferOrEmitWorkspaceUpdate(subscription, { kind: "remove", - id: persistedWorkspace.id, + id: String(persistedWorkspace.id), }); } continue; @@ -5844,7 +5846,7 @@ export class Session { const payload = await this.listFetchWorkspacesEntries(request); await this.primeWorkspaceGitWatchFingerprints(payload.entries); - const snapshotLatestActivityByWorkspaceId = new Map(); + const snapshotLatestActivityByWorkspaceId = new Map(); for (const entry of payload.entries) { const parsedLatestActivity = entry.activityAt ? Date.parse(entry.activityAt) @@ -5973,7 +5975,8 @@ export class Session { request: Extract, ): Promise { try { - const existing = await this.workspaceRegistry.get(request.workspaceId); + const numericWorkspaceId = Number(request.workspaceId); + const existing = await this.workspaceRegistry.get(numericWorkspaceId); if (!existing) { throw new Error(`Workspace not found: ${request.workspaceId}`); } @@ -5981,7 +5984,7 @@ export class Session { throw new Error("Use worktree archive for Paseo worktrees"); } const archivedAt = new Date().toISOString(); - await this.archiveWorkspaceRecord(request.workspaceId, archivedAt); + await this.archiveWorkspaceRecord(numericWorkspaceId, archivedAt); await this.emitWorkspaceUpdateForCwd(existing.directory); this.emit({ type: "archive_workspace_response", diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 65fe21fd3..006abb9ef 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -658,7 +658,7 @@ export const FetchWorkspacesRequestMessageSchema = z.object({ filter: z .object({ query: z.string().optional(), - projectId: z.number().int().optional(), + projectId: z.union([z.string(), z.number()]).transform(String).optional(), idPrefix: z.string().optional(), }) .optional(), @@ -757,7 +757,7 @@ export type GitSetupOptions = z.infer; export const CreateAgentRequestMessageSchema = z.object({ type: z.literal("create_agent_request"), config: AgentSessionConfigSchema, - workspaceId: z.number().int().optional(), + workspaceId: z.union([z.string(), z.number()]).transform(String).optional(), worktreeName: z.string().optional(), initialPrompt: z.string().optional(), clientMessageId: z.string().optional(), @@ -1102,7 +1102,7 @@ export const OpenProjectRequestSchema = z.object({ export const ArchiveWorkspaceRequestSchema = z.object({ type: z.literal("archive_workspace_request"), - workspaceId: z.number().int(), + workspaceId: z.union([z.string(), z.number()]).transform(String), requestId: z.string(), }); @@ -1733,13 +1733,13 @@ export const WorkspaceServicePayloadSchema = z.object({ }); export const WorkspaceDescriptorPayloadSchema = z.object({ - id: z.number().int(), - projectId: z.number().int(), + id: z.union([z.string(), z.number()]).transform(String), + projectId: z.union([z.string(), z.number()]).transform(String), projectDisplayName: z.string(), projectRootPath: z.string(), workspaceDirectory: z.string(), - projectKind: z.enum(["git", "directory"]), - workspaceKind: z.enum(["checkout", "worktree"]), + projectKind: z.enum(["git", "non_git", "directory"]), + workspaceKind: z.enum(["local_checkout", "checkout", "worktree"]), name: z.string(), status: WorkspaceStateBucketSchema, activityAt: z.string().nullable(), @@ -1841,7 +1841,7 @@ export const WorkspaceUpdateMessageSchema = z.object({ }), z.object({ kind: z.literal("remove"), - id: z.number().int(), + id: z.union([z.string(), z.number()]).transform(String), }), ]), }); @@ -1892,7 +1892,7 @@ export const ArchiveWorkspaceResponseMessageSchema = z.object({ type: z.literal("archive_workspace_response"), payload: z.object({ requestId: z.string(), - workspaceId: z.number().int(), + workspaceId: z.union([z.string(), z.number()]).transform(String), archivedAt: z.string().nullable(), error: z.string().nullable(), }),