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",
error: null,
workspace: {
id: 1,
projectId: 1,
id: "1",
projectId: "1",
projectDisplayName: "project",
projectRootPath: WORKSPACE_ID,
workspaceDirectory: WORKSPACE_ID,
+1 -1
View File
@@ -127,7 +127,7 @@ export type DaemonEvent =
}
| {
type: "workspace_update";
workspaceId: number;
workspaceId: string;
payload: Extract<SessionOutboundMessage, { type: "workspace_update" }>["payload"];
}
| {
+24 -21
View File
@@ -317,12 +317,12 @@ type WorkspaceUpdatesSubscriptionState = {
subscriptionId: string;
filter?: WorkspaceUpdatesFilter;
isBootstrapping: boolean;
pendingUpdatesByWorkspaceId: Map<number, WorkspaceUpdatePayload>;
pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
};
type FetchWorkspacesCursor = {
sort: FetchWorkspacesRequestSort[];
values: Record<string, string | number | null>;
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<WorkspaceDescriptorPayload>,
): Promise<void> {
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<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 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<number, number>;
snapshotLatestActivityByWorkspaceId?: Map<string, number>;
}): 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<number, number>();
const snapshotLatestActivityByWorkspaceId = new Map<string, number>();
for (const entry of payload.entries) {
const parsedLatestActivity = entry.activityAt
? Date.parse(entry.activityAt)
@@ -5973,7 +5975,8 @@ export class Session {
request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>,
): Promise<void> {
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",
+9 -9
View File
@@ -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<typeof GitSetupOptionsSchema>;
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(),
}),