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
This commit is contained in:
Mohamed Boudra
2026-04-06 08:34:58 +07:00
parent d3e5d6929d
commit 8d4d445c85
4 changed files with 36 additions and 33 deletions
@@ -70,8 +70,8 @@ describe("openProjectDirectly", () => {
requestId: "request-1", requestId: "request-1",
error: null, error: null,
workspace: { workspace: {
id: 1, id: "1",
projectId: 1, projectId: "1",
projectDisplayName: "project", projectDisplayName: "project",
projectRootPath: WORKSPACE_ID, projectRootPath: WORKSPACE_ID,
workspaceDirectory: WORKSPACE_ID, workspaceDirectory: WORKSPACE_ID,
+1 -1
View File
@@ -127,7 +127,7 @@ export type DaemonEvent =
} }
| { | {
type: "workspace_update"; type: "workspace_update";
workspaceId: number; workspaceId: string;
payload: Extract<SessionOutboundMessage, { type: "workspace_update" }>["payload"]; payload: Extract<SessionOutboundMessage, { type: "workspace_update" }>["payload"];
} }
| { | {
+24 -21
View File
@@ -317,12 +317,12 @@ type WorkspaceUpdatesSubscriptionState = {
subscriptionId: string; subscriptionId: string;
filter?: WorkspaceUpdatesFilter; filter?: WorkspaceUpdatesFilter;
isBootstrapping: boolean; isBootstrapping: boolean;
pendingUpdatesByWorkspaceId: Map<number, WorkspaceUpdatePayload>; pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
}; };
type FetchWorkspacesCursor = { type FetchWorkspacesCursor = {
sort: FetchWorkspacesRequestSort[]; sort: FetchWorkspacesRequestSort[];
values: Record<string, string | number | null>; values: Record<string, string | number | null>;
id: number; id: string;
}; };
class SessionRequestError extends Error { class SessionRequestError extends Error {
@@ -2729,8 +2729,8 @@ export class Session {
labels, labels,
); );
const resolvedWorkspace = const resolvedWorkspace =
typeof msg.workspaceId === "number" msg.workspaceId
? await this.workspaceRegistry.get(msg.workspaceId) ? await this.workspaceRegistry.get(Number(msg.workspaceId))
: (await this.findWorkspaceByDirectory(sessionConfig.cwd)) ?? : (await this.findWorkspaceByDirectory(sessionConfig.cwd)) ??
(await this.findOrCreateWorkspaceForDirectory(sessionConfig.cwd)); (await this.findOrCreateWorkspaceForDirectory(sessionConfig.cwd));
if (!resolvedWorkspace) { if (!resolvedWorkspace) {
@@ -4093,7 +4093,7 @@ export class Session {
workspaces: Iterable<WorkspaceDescriptorPayload>, workspaces: Iterable<WorkspaceDescriptorPayload>,
): Promise<void> { ): Promise<void> {
for (const workspace of workspaces) { for (const workspace of workspaces) {
const persistedWorkspace = await this.workspaceRegistry.get(workspace.id); const persistedWorkspace = await this.workspaceRegistry.get(Number(workspace.id));
if (!persistedWorkspace) { if (!persistedWorkspace) {
continue; continue;
} }
@@ -5181,17 +5181,19 @@ export class Session {
if (cachedShortstat !== undefined) { if (cachedShortstat !== undefined) {
diffStat = cachedShortstat; diffStat = cachedShortstat;
} else { } else {
warmCheckoutShortstatInBackground(workspace.directory); warmCheckoutShortstatInBackground(workspace.directory, undefined, () => {
void this.emitWorkspaceUpdateForCwd(workspace.directory);
});
} }
return { return {
id: workspace.id, id: String(workspace.id),
projectId: workspace.projectId, projectId: String(workspace.projectId),
projectDisplayName: resolvedProjectRecord?.displayName ?? String(workspace.projectId), projectDisplayName: resolvedProjectRecord?.displayName ?? String(workspace.projectId),
projectRootPath: resolvedProjectRecord?.directory ?? workspace.directory, projectRootPath: resolvedProjectRecord?.directory ?? workspace.directory,
workspaceDirectory: workspace.directory, workspaceDirectory: workspace.directory,
projectKind: resolvedProjectRecord?.kind ?? "directory", projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git",
workspaceKind: workspace.kind, workspaceKind: workspace.kind === "checkout" ? "local_checkout" : workspace.kind,
name: workspace.displayName, name: workspace.displayName,
status: "done", status: "done",
activityAt: null, activityAt: null,
@@ -5313,7 +5315,7 @@ export class Session {
} }
return spec.direction === "asc" ? base : -base; return spec.direction === "asc" ? base : -base;
} }
return left.id - right.id; return Number(left.id) - Number(right.id);
} }
private encodeFetchWorkspacesCursor( private encodeFetchWorkspacesCursor(
@@ -5355,7 +5357,7 @@ export class Session {
id?: unknown; 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"); throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
} }
if (!payload.values || typeof payload.values !== "object") { if (!payload.values || typeof payload.values !== "object") {
@@ -5403,7 +5405,7 @@ export class Session {
return { return {
sort: cursorSort, sort: cursorSort,
values: payload.values as Record<string, string | number | null>, values: payload.values as Record<string, string | number | null>,
id: payload.id, id: String(payload.id),
}; };
} }
@@ -5422,7 +5424,7 @@ export class Session {
} }
return spec.direction === "asc" ? base : -base; return spec.direction === "asc" ? base : -base;
} }
return workspace.id - cursor.id; return Number(workspace.id) - Number(cursor.id);
} }
private matchesWorkspaceFilter(input: { private matchesWorkspaceFilter(input: {
@@ -5511,7 +5513,7 @@ export class Session {
} }
private flushBootstrappedWorkspaceUpdates(options?: { private flushBootstrappedWorkspaceUpdates(options?: {
snapshotLatestActivityByWorkspaceId?: Map<number, number>; snapshotLatestActivityByWorkspaceId?: Map<string, number>;
}): void { }): void {
const subscription = this.workspaceUpdatesSubscription; const subscription = this.workspaceUpdatesSubscription;
if (!subscription || !subscription.isBootstrapping) { if (!subscription || !subscription.isBootstrapping) {
@@ -5687,7 +5689,7 @@ export class Session {
const persistedWorkspace = await this.findWorkspaceByDirectory(normalizedCwd); const persistedWorkspace = await this.findWorkspaceByDirectory(normalizedCwd);
const all = await this.listWorkspaceDescriptorsSnapshot(); const all = await this.listWorkspaceDescriptorsSnapshot();
const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry] as const)); 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) { for (const nextWorkspaceId of workspaceIdsToEmit) {
const workspace = descriptorsByWorkspaceId.get(nextWorkspaceId); const workspace = descriptorsByWorkspaceId.get(nextWorkspaceId);
@@ -5738,7 +5740,7 @@ export class Session {
for (const workspaceCwd of uniqueWorkspaceCwds) { for (const workspaceCwd of uniqueWorkspaceCwds) {
const persistedWorkspace = await this.findWorkspaceByDirectory(workspaceCwd); 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 = const nextWorkspace =
workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter }) workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
? workspace ? workspace
@@ -5749,7 +5751,7 @@ export class Session {
if (persistedWorkspace) { if (persistedWorkspace) {
this.bufferOrEmitWorkspaceUpdate(subscription, { this.bufferOrEmitWorkspaceUpdate(subscription, {
kind: "remove", kind: "remove",
id: persistedWorkspace.id, id: String(persistedWorkspace.id),
}); });
} }
continue; continue;
@@ -5844,7 +5846,7 @@ export class Session {
const payload = await this.listFetchWorkspacesEntries(request); const payload = await this.listFetchWorkspacesEntries(request);
await this.primeWorkspaceGitWatchFingerprints(payload.entries); await this.primeWorkspaceGitWatchFingerprints(payload.entries);
const snapshotLatestActivityByWorkspaceId = new Map<number, number>(); const snapshotLatestActivityByWorkspaceId = new Map<string, number>();
for (const entry of payload.entries) { for (const entry of payload.entries) {
const parsedLatestActivity = entry.activityAt const parsedLatestActivity = entry.activityAt
? Date.parse(entry.activityAt) ? Date.parse(entry.activityAt)
@@ -5973,7 +5975,8 @@ export class Session {
request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>, request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>,
): Promise<void> { ): Promise<void> {
try { try {
const existing = await this.workspaceRegistry.get(request.workspaceId); const numericWorkspaceId = Number(request.workspaceId);
const existing = await this.workspaceRegistry.get(numericWorkspaceId);
if (!existing) { if (!existing) {
throw new Error(`Workspace not found: ${request.workspaceId}`); throw new Error(`Workspace not found: ${request.workspaceId}`);
} }
@@ -5981,7 +5984,7 @@ export class Session {
throw new Error("Use worktree archive for Paseo worktrees"); throw new Error("Use worktree archive for Paseo worktrees");
} }
const archivedAt = new Date().toISOString(); const archivedAt = new Date().toISOString();
await this.archiveWorkspaceRecord(request.workspaceId, archivedAt); await this.archiveWorkspaceRecord(numericWorkspaceId, archivedAt);
await this.emitWorkspaceUpdateForCwd(existing.directory); await this.emitWorkspaceUpdateForCwd(existing.directory);
this.emit({ this.emit({
type: "archive_workspace_response", type: "archive_workspace_response",
+9 -9
View File
@@ -658,7 +658,7 @@ export const FetchWorkspacesRequestMessageSchema = z.object({
filter: z filter: z
.object({ .object({
query: z.string().optional(), query: z.string().optional(),
projectId: z.number().int().optional(), projectId: z.union([z.string(), z.number()]).transform(String).optional(),
idPrefix: z.string().optional(), idPrefix: z.string().optional(),
}) })
.optional(), .optional(),
@@ -757,7 +757,7 @@ export type GitSetupOptions = z.infer<typeof GitSetupOptionsSchema>;
export const CreateAgentRequestMessageSchema = z.object({ export const CreateAgentRequestMessageSchema = z.object({
type: z.literal("create_agent_request"), type: z.literal("create_agent_request"),
config: AgentSessionConfigSchema, config: AgentSessionConfigSchema,
workspaceId: z.number().int().optional(), workspaceId: z.union([z.string(), z.number()]).transform(String).optional(),
worktreeName: z.string().optional(), worktreeName: z.string().optional(),
initialPrompt: z.string().optional(), initialPrompt: z.string().optional(),
clientMessageId: z.string().optional(), clientMessageId: z.string().optional(),
@@ -1102,7 +1102,7 @@ export const OpenProjectRequestSchema = z.object({
export const ArchiveWorkspaceRequestSchema = z.object({ export const ArchiveWorkspaceRequestSchema = z.object({
type: z.literal("archive_workspace_request"), type: z.literal("archive_workspace_request"),
workspaceId: z.number().int(), workspaceId: z.union([z.string(), z.number()]).transform(String),
requestId: z.string(), requestId: z.string(),
}); });
@@ -1733,13 +1733,13 @@ export const WorkspaceServicePayloadSchema = z.object({
}); });
export const WorkspaceDescriptorPayloadSchema = z.object({ export const WorkspaceDescriptorPayloadSchema = z.object({
id: z.number().int(), id: z.union([z.string(), z.number()]).transform(String),
projectId: z.number().int(), projectId: z.union([z.string(), z.number()]).transform(String),
projectDisplayName: z.string(), projectDisplayName: z.string(),
projectRootPath: z.string(), projectRootPath: z.string(),
workspaceDirectory: z.string(), workspaceDirectory: z.string(),
projectKind: z.enum(["git", "directory"]), projectKind: z.enum(["git", "non_git", "directory"]),
workspaceKind: z.enum(["checkout", "worktree"]), workspaceKind: z.enum(["local_checkout", "checkout", "worktree"]),
name: z.string(), name: z.string(),
status: WorkspaceStateBucketSchema, status: WorkspaceStateBucketSchema,
activityAt: z.string().nullable(), activityAt: z.string().nullable(),
@@ -1841,7 +1841,7 @@ export const WorkspaceUpdateMessageSchema = z.object({
}), }),
z.object({ z.object({
kind: z.literal("remove"), 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"), type: z.literal("archive_workspace_response"),
payload: z.object({ payload: z.object({
requestId: z.string(), requestId: z.string(),
workspaceId: z.number().int(), workspaceId: z.union([z.string(), z.number()]).transform(String),
archivedAt: z.string().nullable(), archivedAt: z.string().nullable(),
error: z.string().nullable(), error: z.string().nullable(),
}), }),