From 8d567bb9e842dd2bb222359ca208f9506cdd9320 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:22:44 +1100 Subject: [PATCH 1/5] fix(muse): failed Muse text generation shows Muse's reason Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 17 +++++++++++++++++ .../provider-muse/src/server/textGeneration.ts | 10 ++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/provider-muse/src/server/textGeneration.test.ts b/packages/provider-muse/src/server/textGeneration.test.ts index 7876cac59a50..e66cdad1933f 100644 --- a/packages/provider-muse/src/server/textGeneration.test.ts +++ b/packages/provider-muse/src/server/textGeneration.test.ts @@ -226,6 +226,23 @@ it.layer(NodeServices.layer)("Muse text generation", (it) => { }), ); + it.effect("keeps Muse's reason when the turn fails", () => + Effect.gen(function* () { + const test = fixture((emit) => + emit("turn/completed", { + terminal: "failed", + error: { message: "You've reached your usage limit." }, + }), + ); + const service = yield* test.make; + expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({ + _tag: "TextGenerationError", + detail: "Muse Code text generation failed: You've reached your usage limit.", + }); + expect(test.host.close).toHaveBeenCalledOnce(); + }), + ); + it.effect("settles a completed turn even when its start acknowledgement never arrives", () => Effect.gen(function* () { const test = fixture(finish('{"title":"Fix login form"}'), false); diff --git a/packages/provider-muse/src/server/textGeneration.ts b/packages/provider-muse/src/server/textGeneration.ts index f122d1df756e..4295e7524c10 100644 --- a/packages/provider-muse/src/server/textGeneration.ts +++ b/packages/provider-muse/src/server/textGeneration.ts @@ -9,6 +9,7 @@ import type { MuseSettings } from "../settings.ts"; import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { extractJsonObject } from "@t3tools/shared/schemaJson"; +import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; @@ -71,6 +72,7 @@ const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); const decodeJson = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown)); type Operation = keyof ProviderTextGeneration; +class MuseTurnFailure extends Error {} async function generateMuseText( host: MuseSdkHost, @@ -137,7 +139,9 @@ async function generateMuseText( } } else { rejectCompletion( - new Error(completed.error?.message ?? `Muse turn ${completed.terminal}.`), + completed.error + ? new MuseTurnFailure(completed.error.message) + : new Error(`Muse turn ${completed.terminal}.`), ); } } else if ( @@ -267,7 +271,9 @@ export const makeMuseTextGeneration = Effect.fn("makeMuseTextGeneration")(functi new TextGenerationError({ operation, detail: - "Muse Code text generation failed. Check Muse login and availability on this T3 server host.", + Cause.isUnknownError(cause) && cause.cause instanceof MuseTurnFailure + ? `Muse Code text generation failed: ${cause.cause.message}` + : "Muse Code text generation failed. Check Muse login and availability on this T3 server host.", cause, }), ), From 03d02e627eda8cff772139f6580bb00ef7f0d50e Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:32:23 +1100 Subject: [PATCH 2/5] fix(muse): failed Muse text generation explains Muse's error category Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 47 ++++++++++++++----- .../src/server/textGeneration.ts | 39 ++++++++++++--- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/packages/provider-muse/src/server/textGeneration.test.ts b/packages/provider-muse/src/server/textGeneration.test.ts index e66cdad1933f..5306a0ffd88b 100644 --- a/packages/provider-muse/src/server/textGeneration.test.ts +++ b/packages/provider-muse/src/server/textGeneration.test.ts @@ -226,20 +226,41 @@ it.layer(NodeServices.layer)("Muse text generation", (it) => { }), ); - it.effect("keeps Muse's reason when the turn fails", () => + it.effect("explains a failed turn by Muse's error category", () => Effect.gen(function* () { - const test = fixture((emit) => - emit("turn/completed", { - terminal: "failed", - error: { message: "You've reached your usage limit." }, - }), - ); - const service = yield* test.make; - expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({ - _tag: "TextGenerationError", - detail: "Muse Code text generation failed: You've reached your usage limit.", - }); - expect(test.host.close).toHaveBeenCalledOnce(); + const message = "API error 429: raw provider text"; + const cases = [ + { + error: { kind: "modelError", quota: { limitKind: "tierLimit", resetsAtMs: 1 } }, + detail: "Muse Code usage limit reached. Try again after it resets.", + }, + { + error: { kind: "authRequired" }, + detail: "Muse Code is not signed in. Run `muse login` on this T3 server host.", + }, + { + error: { kind: "modelError" }, + detail: "Muse Code's model request failed. Try again or choose another Muse model.", + }, + { + error: { kind: "stepLimit" }, + detail: + "Muse Code text generation failed. Check Muse login and availability on this T3 server host.", + }, + ]; + for (const { error, detail } of cases) { + const test = fixture((emit) => + emit("turn/completed", { + terminal: "failed", + error: { ...error, message, retryable: false }, + }), + ); + const service = yield* test.make; + const failure = yield* service.generateThreadTitle(titleInput).pipe(Effect.flip); + expect(failure).toMatchObject({ _tag: "TextGenerationError", detail }); + expect(failure.detail).not.toContain(message); + expect(test.host.close).toHaveBeenCalledOnce(); + } }), ); diff --git a/packages/provider-muse/src/server/textGeneration.ts b/packages/provider-muse/src/server/textGeneration.ts index 4295e7524c10..9162e59396c3 100644 --- a/packages/provider-muse/src/server/textGeneration.ts +++ b/packages/provider-muse/src/server/textGeneration.ts @@ -49,7 +49,13 @@ const TurnCompleted = Schema.Struct({ sessionId: Schema.String, turnId: Schema.String, terminal: Schema.String, - error: Schema.optionalKey(Schema.Struct({ message: Schema.String })), + error: Schema.optionalKey( + Schema.Struct({ + kind: Schema.optionalKey(Schema.String), + message: Schema.String, + quota: Schema.optionalKey(Schema.Unknown), + }), + ), }); const ApprovalRequested = Schema.Struct({ sessionId: Schema.String, @@ -72,7 +78,29 @@ const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); const decodeJson = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown)); type Operation = keyof ProviderTextGeneration; -class MuseTurnFailure extends Error {} +type TurnError = NonNullable<(typeof TurnCompleted.Type)["error"]>; +class MuseTurnFailure extends Error { + readonly turnError: TurnError; + constructor(turnError: TurnError) { + super(turnError.message); + this.turnError = turnError; + } +} + +function museFailureDetail(cause: unknown): string { + const error = + Cause.isUnknownError(cause) && cause.cause instanceof MuseTurnFailure + ? cause.cause.turnError + : undefined; + // Muse sends `quota` only for quota exhaustion, which it still labels `modelError`. + if (error?.quota !== undefined) + return "Muse Code usage limit reached. Try again after it resets."; + if (error?.kind === "authRequired") + return "Muse Code is not signed in. Run `muse login` on this T3 server host."; + if (error?.kind === "modelError") + return "Muse Code's model request failed. Try again or choose another Muse model."; + return "Muse Code text generation failed. Check Muse login and availability on this T3 server host."; +} async function generateMuseText( host: MuseSdkHost, @@ -140,7 +168,7 @@ async function generateMuseText( } else { rejectCompletion( completed.error - ? new MuseTurnFailure(completed.error.message) + ? new MuseTurnFailure(completed.error) : new Error(`Muse turn ${completed.terminal}.`), ); } @@ -270,10 +298,7 @@ export const makeMuseTextGeneration = Effect.fn("makeMuseTextGeneration")(functi (cause) => new TextGenerationError({ operation, - detail: - Cause.isUnknownError(cause) && cause.cause instanceof MuseTurnFailure - ? `Muse Code text generation failed: ${cause.cause.message}` - : "Muse Code text generation failed. Check Muse login and availability on this T3 server host.", + detail: museFailureDetail(cause), cause, }), ), From 419e93bd75f1d1427be01c82f7baaa2c19befbe7 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:59:10 +1100 Subject: [PATCH 3/5] fix(muse): proxy quota refusals read as a usage limit in text generation Co-Authored-By: Claude Opus 5.5 --- packages/provider-muse/src/server/textGeneration.test.ts | 9 +++++++-- packages/provider-muse/src/server/textGeneration.ts | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/provider-muse/src/server/textGeneration.test.ts b/packages/provider-muse/src/server/textGeneration.test.ts index 5306a0ffd88b..f2d19a1d29fe 100644 --- a/packages/provider-muse/src/server/textGeneration.test.ts +++ b/packages/provider-muse/src/server/textGeneration.test.ts @@ -234,6 +234,11 @@ it.layer(NodeServices.layer)("Muse text generation", (it) => { error: { kind: "modelError", quota: { limitKind: "tierLimit", resetsAtMs: 1 } }, detail: "Muse Code usage limit reached. Try again after it resets.", }, + { + // Credential proxies can refuse with a 429 quota message and no quota object. + error: { kind: "modelError", message: "upstream 429: quota exceeded for this key" }, + detail: "Muse Code usage limit reached. Try again after it resets.", + }, { error: { kind: "authRequired" }, detail: "Muse Code is not signed in. Run `muse login` on this T3 server host.", @@ -252,13 +257,13 @@ it.layer(NodeServices.layer)("Muse text generation", (it) => { const test = fixture((emit) => emit("turn/completed", { terminal: "failed", - error: { ...error, message, retryable: false }, + error: { message, ...error, retryable: false }, }), ); const service = yield* test.make; const failure = yield* service.generateThreadTitle(titleInput).pipe(Effect.flip); expect(failure).toMatchObject({ _tag: "TextGenerationError", detail }); - expect(failure.detail).not.toContain(message); + expect(failure.detail).not.toContain("message" in error ? error.message : message); expect(test.host.close).toHaveBeenCalledOnce(); } }), diff --git a/packages/provider-muse/src/server/textGeneration.ts b/packages/provider-muse/src/server/textGeneration.ts index 9162e59396c3..e0706d8c3b8e 100644 --- a/packages/provider-muse/src/server/textGeneration.ts +++ b/packages/provider-muse/src/server/textGeneration.ts @@ -17,6 +17,7 @@ import * as Schema from "effect/Schema"; import { createMuseSdkHost, createMuseSdkHostEffect, type MuseSdkHost } from "./sdk.ts"; import { museModelCapabilities, resolveMuseReasoningEffort } from "./modelCatalog.ts"; +import { isMuseUsageLimitFailure } from "./usageLimits.ts"; import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration"; import { investigationUnsupported } from "@t3tools/provider-core/server/textGenerationOperations"; import { @@ -93,7 +94,7 @@ function museFailureDetail(cause: unknown): string { ? cause.cause.turnError : undefined; // Muse sends `quota` only for quota exhaustion, which it still labels `modelError`. - if (error?.quota !== undefined) + if (error?.quota !== undefined || isMuseUsageLimitFailure(error?.message)) return "Muse Code usage limit reached. Try again after it resets."; if (error?.kind === "authRequired") return "Muse Code is not signed in. Run `muse login` on this T3 server host."; From b822aac9172adfb239716a7e93f8cad280001322 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:16:28 +1100 Subject: [PATCH 4/5] refactor(muse): use a tagged error for turn failures --- .../src/server/textGeneration.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/provider-muse/src/server/textGeneration.ts b/packages/provider-muse/src/server/textGeneration.ts index 743fe4f7a6cb..dacaeae8bd03 100644 --- a/packages/provider-muse/src/server/textGeneration.ts +++ b/packages/provider-muse/src/server/textGeneration.ts @@ -53,18 +53,17 @@ const ItemNotification = Schema.Struct({ truncated: Schema.optionalKey(Schema.Boolean), }), }); +const TurnError = Schema.Struct({ + kind: Schema.optionalKey(Schema.String), + message: Schema.String, + quota: Schema.optionalKey(Schema.Unknown), +}); const TurnCompleted = Schema.Struct({ sessionId: Schema.String, turnId: Schema.String, terminal: Schema.String, reason: Schema.optionalKey(Schema.String), - error: Schema.optionalKey( - Schema.Struct({ - kind: Schema.optionalKey(Schema.String), - message: Schema.String, - quota: Schema.optionalKey(Schema.Unknown), - }), - ), + error: Schema.optionalKey(TurnError), }); const ApprovalRequested = Schema.Struct({ sessionId: Schema.String, @@ -88,21 +87,18 @@ const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); const decodeJson = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown)); type Operation = keyof ProviderTextGeneration; -type TurnError = NonNullable<(typeof TurnCompleted.Type)["error"]>; -// @effect-diagnostics-next-line extendsNativeError:off - rejects a plain promise; MuseSdkError carries it as its cause. -class MuseTurnFailure extends Error { - readonly turnError: TurnError; - constructor(turnError: TurnError) { - super(turnError.message); - this.turnError = turnError; +class MuseTurnFailure extends Schema.TaggedError()("MuseTurnFailure", { + turnError: TurnError, +}) { + override get message() { + return this.turnError.message; } } +const isMuseTurnFailure = Schema.is(MuseTurnFailure); function museFailureDetail(cause: unknown): string { const error = - isMuseSdkError(cause) && cause.cause instanceof MuseTurnFailure - ? cause.cause.turnError - : undefined; + isMuseSdkError(cause) && isMuseTurnFailure(cause.cause) ? cause.cause.turnError : undefined; // Muse sends `quota` only for quota exhaustion, which it still labels `modelError`. if (error?.quota !== undefined || isMuseUsageLimitFailure(error?.message)) return "Muse Code usage limit reached. Try again after it resets."; @@ -181,7 +177,9 @@ async function generateMuseText( completed.error ?? (completed.reason !== undefined ? { message: completed.reason } : undefined); rejectCompletion( - error ? new MuseTurnFailure(error) : new Error(`Muse turn ${completed.terminal}.`), + error + ? new MuseTurnFailure({ turnError: error }) + : new Error(`Muse turn ${completed.terminal}.`), ); } } else if ( From b5a9f79c2a47dde309abb5b881efcf042585eaba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Nov 2026 12:18:09 +1100 Subject: [PATCH 5/5] test(web): command palette tests clear a leaked last-used project Cherry-picked from 5fae2b5124d8c491f242c004a6c6ef8602517719 (#35107) by Julius Marminge. Co-Authored-By: Julius Marminge Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/CommandPalette.browser.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/web/src/components/CommandPalette.browser.test.tsx b/apps/web/src/components/CommandPalette.browser.test.tsx index c7106f0cff28..8292121ac2ce 100644 --- a/apps/web/src/components/CommandPalette.browser.test.tsx +++ b/apps/web/src/components/CommandPalette.browser.test.tsx @@ -5,6 +5,8 @@ import { page, userEvent } from "vite-plus/test/browser"; import { act, render } from "../../test/render"; import { openCommandPalette } from "../commandPaletteBus"; import { useCommandPaletteStore } from "../commandPaletteStore"; +import { LAST_USED_PROJECT_STORAGE_KEY } from "../hooks/useHandleNewThread"; +import { removeLocalStorageItem } from "../hooks/useLocalStorage"; import { makeThreadFixture } from "../test-fixtures"; import type { Project, Thread } from "../types"; import { CommandPalette } from "./CommandPalette"; @@ -20,6 +22,9 @@ vi.mock("../state/entities", async (original) => ({ })); function setup(state: { projects?: ReadonlyArray; threads?: ReadonlyArray } = {}) { + // Earlier browser test files share this origin's localStorage; a remembered project + // that is not listed here hides the default "New thread in " row. + removeLocalStorageItem(LAST_USED_PROJECT_STORAGE_KEY); useCommandPaletteStore.setState(useCommandPaletteStore.getInitialState(), true); entities.projects = state.projects ?? []; entities.threads = state.threads ?? [];