authorgravatar for basmilius@hotmail.comBas Milius <basmilius@hotmail.com> 2026-04-30 02:44:44+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-29 17:44:44-07:00
log44b39fe2ec47a58ae4efd97fabe99c24c376792d
treee844ea2e486350d338e402d0d4da747659cf8e83
parent02903f2db870a8fba8e7d6eadc48cfccb68754da
signature Signed by PGP key B5690EEEBB952194

fix(server): key AskUserQuestion answers by question text (#2404)


2 files changed, 36 insertions(+), 1 deletions(-)

apps/server/src/provider/Layers/ClaudeAdapter.test.ts+31
......@@ -3296,6 +3296,9 @@ describe("ClaudeAdapterLive", () => {
32963296 assert.equal(typeof requestId, "string");
32973297 assert.equal(requestedEvent.value.payload.questions.length, 1);
32983298 assert.equal(requestedEvent.value.payload.questions[0]?.question, "Which framework?");
3299 // Regression for #2388: `id` must equal the full question text so the
3300 // UI's draft-answer key matches what the SDK looks up downstream.
3301 assert.equal(requestedEvent.value.payload.questions[0]?.id, "Which framework?");
32993302 assert.deepEqual(requestedEvent.value.providerRefs, {
33003303 providerItemId: ProviderItemId.make("tool-ask-1"),
33013304 });
......@@ -3330,6 +3333,34 @@ describe("ClaudeAdapterLive", () => {
33303333 assert.deepEqual(updatedInput.answers, { "Which framework?": "React" });
33313334 // Original questions should be passed through.
33323335 assert.deepEqual(updatedInput.questions, askInput.questions);
3336
3337 // Compatibility check for #2388: the answers shape we hand to the SDK
3338 // must produce a non-empty rendered tool_result on BOTH SDK iteration
3339 // patterns we have seen, so we don't regress the issue and we don't
3340 // break users still on the older Claude CLI.
3341 const sdkAnswers = updatedInput.answers as Record<string, unknown>;
3342 const sdkQuestions = updatedInput.questions as ReadonlyArray<{
3343 readonly question: string;
3344 }>;
3345
3346 // Claude CLI 2.1.119 — key-agnostic Object.entries iteration. Any key
3347 // works here, but it must at least round-trip into a non-empty string.
3348 const v119Rendered = Object.entries(sdkAnswers)
3349 .map(([key, value]) => `"${key}"="${String(value)}"`)
3350 .join(", ");
3351 assert.equal(v119Rendered, '"Which framework?"="React"');
3352
3353 // Claude CLI 2.1.121 — lookup by full question text. This is the path
3354 // that regressed in #2388 when the answers were keyed by `header`.
3355 const v121Rendered = sdkQuestions
3356 .map(({ question }) => {
3357 const answer = sdkAnswers[question];
3358 return answer === undefined ? null : `"${question}"="${String(answer)}"`;
3359 })
3360 .filter((entry): entry is string => entry !== null)
3361 .join(", ");
3362 assert.notEqual(v121Rendered, "", "Expected non-empty SDK 2.1.121 tool_result (#2388)");
3363 assert.equal(v121Rendered, '"Which framework?"="React"');
33333364 }).pipe(
33343365 Effect.provideService(Random.Random, makeDeterministicRandomService()),
33353366 Effect.provide(harness.layer),
apps/server/src/provider/Layers/ClaudeAdapter.ts+5-1
......@@ -2563,10 +2563,14 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
25632563 const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4);
25642564
25652565 // Parse questions from the SDK's AskUserQuestion input.
2566 // `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks
2567 // up answers by question text in `mapToolResultToToolResultBlockParam`,
2568 // so the key the UI uses to keep its draft answer must match the SDK's
2569 // expected lookup key. See https://github.com/pingdotgg/t3code/issues/2388
25662570 const rawQuestions = Array.isArray(toolInput.questions) ? toolInput.questions : [];
25672571 const questions: Array<UserInputQuestion> = rawQuestions.map(
25682572 (q: Record<string, unknown>, idx: number) => ({
2569 id: typeof q.header === "string" ? q.header : `q-${idx}`,
2573 id: typeof q.question === "string" && q.question.length > 0 ? q.question : `q-${idx}`,
25702574 header: typeof q.header === "string" ? q.header : `Question ${idx + 1}`,
25712575 question: typeof q.question === "string" ? q.question : "",
25722576 options: Array.isArray(q.options)