From 92510e5139e809a1700e1ba0aefbf00250db6c2d Mon Sep 17 00:00:00 2001 From: Hyunwoo Jung Date: Sat, 8 Aug 2026 15:00:48 +0900 Subject: [PATCH] fix(cli): accept the app's `effort` key in MessageMetaSchema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-message effort picker never reaches the SDK. happy-app sends the value under `meta.effort` (sync.ts:728), the CLI parses the message with `UserMessageSchema.safeParse` (apiSession.ts:552), and `MessageMetaSchema` does not declare `effort` — so Zod, which strips unknown keys by default, deletes it before anything downstream can see it. runClaude then gates on the key that is already gone: if (message.meta?.hasOwnProperty('effort')) { // always false const incoming = (message.meta as Record).effort; That cast is the tell: the key is read through `Record` precisely because it is absent from the type, so typecheck passes while the branch that applies the picked effort is unreachable. Every user message falls through to the `else` and keeps the previous effort. Reproduced against this file, before and after: app sends {"sentFrom":"app","effort":"high"} parsed {"sentFrom":"app"} <- effort dropped guard meta?.hasOwnProperty('effort') = false with this change: parsed {"sentFrom":"app","effort":"high"} guard meta?.hasOwnProperty('effort') = true The existing coverage cannot catch it: runClaude.test.ts builds `meta` as a TypeScript object literal and never goes through the schema, so the wire path is the only place the mismatch exists. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- packages/happy-cli/src/api/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/happy-cli/src/api/types.ts b/packages/happy-cli/src/api/types.ts index 313ed60242..e611791814 100644 --- a/packages/happy-cli/src/api/types.ts +++ b/packages/happy-cli/src/api/types.ts @@ -194,7 +194,8 @@ export const MessageMetaSchema = z.object({ customSystemPrompt: z.string().nullable().optional(), // Custom system prompt for this message (null = reset) appendSystemPrompt: z.string().nullable().optional(), // Append to system prompt for this message (null = reset) allowedTools: z.array(z.string()).nullable().optional(), // Allowed tools for this message (null = reset) - disallowedTools: z.array(z.string()).nullable().optional() // Disallowed tools for this message (null = reset) + disallowedTools: z.array(z.string()).nullable().optional(), // Disallowed tools for this message (null = reset) + effort: z.string().nullable().optional() // Effort level for this message (null = reset). happy-app sends this key; without it Zod strips the value before runClaude reads it. }) export type MessageMeta = z.infer