From 4a6c1bac114dedfe83f54bd887a54a72133889d3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 5 Oct 2026 15:10:19 -0700 Subject: [PATCH 1/2] fix(server): ACP mode states with null descriptions are no longer dropped Grok advertises its session modes in `initialize._meta.modeState`, which reaches us untyped. A hand-written guard checked it and rejected a mode whose `description` was `null`. The ACP schema allows that value, so one such entry silently threw away the whole mode list. Both initialize guards (mode state and model state) are now `Schema.is` over the effect-acp schemas: the v1 `SessionModeState` (modes are gone from ACP v2) and the compat `SessionModelState` the session RPCs already decode with. Valid states that passed before still pass. Malformed entries are still rejected, and so is a non-object `_meta` on the state, which the old guards never checked. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/provider/acp/AcpRuntimeModel.test.ts | 52 +++++++++++++++++++ .../src/provider/acp/AcpRuntimeModel.ts | 41 +++------------ 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts index f2d999520e02..1869747cfc5b 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts @@ -13,6 +13,7 @@ import { parsePermissionRequest, parseSessionModeState, parseSessionUpdateEvent, + sessionModelStateFromInitialize, sessionUpdateCountsAsLoadReplayActivity, sessionUpdateIsReplay, syntheticLoadSessionResponseFromInitialize, @@ -216,6 +217,57 @@ describe("AcpRuntimeModel", () => { expect(response.modes?.availableModes).toHaveLength(2); }); + it("keeps initialize mode state whose modes have null descriptions", () => { + const response = syntheticLoadSessionResponseFromInitialize({ + protocolVersion: 1, + _meta: { + modeState: { + currentModeId: "code", + availableModes: [ + { id: "ask", name: "Ask", description: null }, + { id: "code", name: "Code", description: "Edit files", _meta: null }, + ], + _meta: null, + }, + }, + } satisfies EffectAcpSchema.InitializeResponse); + + expect(parseSessionModeState(response)).toEqual({ + currentModeId: "code", + availableModes: [ + { id: "ask", name: "Ask" }, + { id: "code", name: "Code", description: "Edit files" }, + ], + }); + }); + + it("reads initialize model state and ignores malformed model state", () => { + const modelState = { + currentModelId: "grok-4.7", + availableModels: [ + { + modelId: "grok-4.7", + name: "Grok 4.7", + description: null, + _meta: { reasoningEfforts: [{ id: "high", default: true }] }, + }, + { modelId: "grok-4.6", name: "Grok 4.6" }, + ], + }; + + expect(sessionModelStateFromInitialize({ protocolVersion: 1, _meta: { modelState } })).toEqual( + modelState, + ); + expect( + sessionModelStateFromInitialize({ + protocolVersion: 1, + _meta: { + modelState: { currentModelId: "grok-4.7", availableModels: [{ modelId: "grok-4.7" }] }, + }, + }), + ).toBeUndefined(); + }); + it("projects typed ACP tool call updates into runtime events", () => { const created = parseSessionUpdateEvent({ sessionId: "session-1", diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.ts b/apps/server/src/provider/acp/AcpRuntimeModel.ts index 522d8783dec3..694f761ff8f7 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.ts @@ -3,7 +3,9 @@ import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; -import type * as EffectAcpSchema from "effect-acp/compat"; +import * as Schema from "effect/Schema"; +import * as EffectAcpSchema from "effect-acp/compat"; +import * as EffectAcpSchemaV1 from "effect-acp/schema-v1"; import { deriveToolActivityPresentation, mergeToolActivityData, @@ -19,39 +21,10 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -function isSessionModelState(value: unknown): value is EffectAcpSchema.SessionModelState { - if (!isRecord(value) || typeof value.currentModelId !== "string") { - return false; - } - if (!Array.isArray(value.availableModels)) { - return false; - } - return value.availableModels.every( - (model) => - isRecord(model) && - typeof model.modelId === "string" && - typeof model.name === "string" && - (model.description === undefined || - model.description === null || - typeof model.description === "string"), - ); -} - -function isSessionModeState(value: unknown): value is EffectAcpSchema.SessionModeState { - if (!isRecord(value) || typeof value.currentModeId !== "string") { - return false; - } - if (!Array.isArray(value.availableModes)) { - return false; - } - return value.availableModes.every( - (mode) => - isRecord(mode) && - typeof mode.id === "string" && - typeof mode.name === "string" && - (mode.description === undefined || typeof mode.description === "string"), - ); -} +// Guards for the untyped `initialize._meta` states some agents (Grok) advertise. +// Modes were removed from ACP v2, so the v1 wire schema is the source of truth. +const isSessionModelState = Schema.is(EffectAcpSchema.SessionModelState); +const isSessionModeState = Schema.is(EffectAcpSchemaV1.SessionModeState); export interface AcpSessionMode { readonly id: string; From a8a5184d54a69cad6231ab991478a5d5af59b699 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 5 Oct 2026 15:30:40 -0700 Subject: [PATCH 2/2] test(server): pin the stricter ACP model state _meta check Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/server/src/provider/acp/AcpRuntimeModel.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts index 1869747cfc5b..27cb3234b375 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts @@ -266,6 +266,12 @@ describe("AcpRuntimeModel", () => { }, }), ).toBeUndefined(); + expect( + sessionModelStateFromInitialize({ + protocolVersion: 1, + _meta: { modelState: { ...modelState, _meta: "not an object" } }, + }), + ).toBeUndefined(); }); it("projects typed ACP tool call updates into runtime events", () => {