mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -45,6 +45,12 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for full setup, build sync requir
|
||||
- **NEVER assume a timeout means the service needs restarting** — timeouts can be transient.
|
||||
- **NEVER add auth checks to tests** — agent providers handle their own auth.
|
||||
- **Always run typecheck after every change.**
|
||||
- **NEVER make breaking changes to WebSocket or message schemas.** The mobile app in the App Store always lags behind the daemon, and daemons in the wild lag behind new app releases. Both directions must work. Every schema change MUST be backward-compatible:
|
||||
- New fields: always `.optional()` with a sensible default or `.transform()` fallback.
|
||||
- Never change a field from optional to required.
|
||||
- Never remove a field — deprecate it (keep accepting it, stop sending it).
|
||||
- Never narrow a field's type (e.g. `string` → `enum`, `nullable` → non-null).
|
||||
- Test with: "does a 6-month-old client still parse this?" and "does a 6-month-old daemon still send something this client accepts?"
|
||||
|
||||
## Debugging
|
||||
|
||||
|
||||
@@ -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