fix: make worktreeRoot backward-compatible for old clients/daemons
worktreeRoot was added as a required field in 9154f8fc, which breaks
old clients parsing new daemon responses and new clients parsing old
daemon responses. Made it .optional() with .transform() fallbacks.
Also added a critical rule to CLAUDE.md: schema changes must always
be backward-compatible in both directions.
This commit is contained in:
@@ -1607,35 +1607,50 @@ export const ArtifactMessageSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const ProjectCheckoutLiteNotGitPayloadSchema = z.object({
|
||||
cwd: z.string(),
|
||||
isGit: z.literal(false),
|
||||
currentBranch: z.null(),
|
||||
remoteUrl: z.null(),
|
||||
worktreeRoot: z.null(),
|
||||
isPaseoOwnedWorktree: z.literal(false),
|
||||
mainRepoRoot: z.null(),
|
||||
});
|
||||
export const ProjectCheckoutLiteNotGitPayloadSchema = z
|
||||
.object({
|
||||
cwd: z.string(),
|
||||
isGit: z.literal(false),
|
||||
currentBranch: z.null(),
|
||||
remoteUrl: z.null(),
|
||||
worktreeRoot: z.null().optional(),
|
||||
isPaseoOwnedWorktree: z.literal(false),
|
||||
mainRepoRoot: z.null(),
|
||||
})
|
||||
.transform((value) => ({
|
||||
...value,
|
||||
worktreeRoot: null,
|
||||
}));
|
||||
|
||||
export const ProjectCheckoutLiteGitNonPaseoPayloadSchema = z.object({
|
||||
cwd: z.string(),
|
||||
isGit: z.literal(true),
|
||||
currentBranch: z.string().nullable(),
|
||||
remoteUrl: z.string().nullable(),
|
||||
worktreeRoot: z.string(),
|
||||
isPaseoOwnedWorktree: z.literal(false),
|
||||
mainRepoRoot: z.null(),
|
||||
});
|
||||
export const ProjectCheckoutLiteGitNonPaseoPayloadSchema = z
|
||||
.object({
|
||||
cwd: z.string(),
|
||||
isGit: z.literal(true),
|
||||
currentBranch: z.string().nullable(),
|
||||
remoteUrl: z.string().nullable(),
|
||||
worktreeRoot: z.string().optional(),
|
||||
isPaseoOwnedWorktree: z.literal(false),
|
||||
mainRepoRoot: z.null(),
|
||||
})
|
||||
.transform((value) => ({
|
||||
...value,
|
||||
worktreeRoot: value.worktreeRoot ?? value.cwd,
|
||||
}));
|
||||
|
||||
export const ProjectCheckoutLiteGitPaseoPayloadSchema = z.object({
|
||||
cwd: z.string(),
|
||||
isGit: z.literal(true),
|
||||
currentBranch: z.string().nullable(),
|
||||
remoteUrl: z.string().nullable(),
|
||||
worktreeRoot: z.string(),
|
||||
isPaseoOwnedWorktree: z.literal(true),
|
||||
mainRepoRoot: z.string(),
|
||||
});
|
||||
export const ProjectCheckoutLiteGitPaseoPayloadSchema = z
|
||||
.object({
|
||||
cwd: z.string(),
|
||||
isGit: z.literal(true),
|
||||
currentBranch: z.string().nullable(),
|
||||
remoteUrl: z.string().nullable(),
|
||||
worktreeRoot: z.string().optional(),
|
||||
isPaseoOwnedWorktree: z.literal(true),
|
||||
mainRepoRoot: z.string(),
|
||||
})
|
||||
.transform((value) => ({
|
||||
...value,
|
||||
worktreeRoot: value.worktreeRoot ?? value.cwd,
|
||||
}));
|
||||
|
||||
export const ProjectCheckoutLitePayloadSchema = z.union([
|
||||
ProjectCheckoutLiteNotGitPayloadSchema,
|
||||
|
||||
@@ -50,4 +50,71 @@ describe("workspace message schemas", () => {
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
test("parses legacy fetch_agents_response checkout payloads without worktreeRoot", () => {
|
||||
const result = SessionOutboundMessageSchema.safeParse({
|
||||
type: "fetch_agents_response",
|
||||
payload: {
|
||||
requestId: "req-1",
|
||||
entries: [
|
||||
{
|
||||
agent: {
|
||||
id: "agent-1",
|
||||
provider: "codex",
|
||||
cwd: "C:\\repo",
|
||||
model: null,
|
||||
features: [],
|
||||
thinkingOptionId: null,
|
||||
effectiveThinkingOptionId: null,
|
||||
createdAt: "2026-04-04T00:00:00.000Z",
|
||||
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||
lastUserMessageAt: null,
|
||||
status: "running",
|
||||
capabilities: {
|
||||
supportsStreaming: true,
|
||||
supportsSessionPersistence: true,
|
||||
supportsDynamicModes: true,
|
||||
supportsMcpServers: true,
|
||||
supportsReasoningStream: true,
|
||||
supportsToolInvocations: true,
|
||||
},
|
||||
currentModeId: null,
|
||||
availableModes: [],
|
||||
pendingPermissions: [],
|
||||
persistence: null,
|
||||
title: "Agent 1",
|
||||
labels: {},
|
||||
requiresAttention: false,
|
||||
attentionReason: null,
|
||||
},
|
||||
project: {
|
||||
projectKey: "remote:github.com/acme/repo",
|
||||
projectName: "acme/repo",
|
||||
checkout: {
|
||||
cwd: "C:\\repo",
|
||||
isGit: true,
|
||||
currentBranch: "main",
|
||||
remoteUrl: "https://github.com/acme/repo.git",
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkout = result.data.payload.entries[0]?.project.checkout;
|
||||
expect(checkout?.worktreeRoot).toBe("C:\\repo");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user