From be79ba520a6e141279fbd84264c7fe78935af5e4 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 11:03:10 -0400 Subject: [PATCH 1/3] feat(app): retry a failed run, switching to Automatic when the model caused it Why: packages/channel/src/channel.ts#class Channel | an ask with `automatic` and no model must replace the chat's model with the channel's choice, so Retry can leave a model that fails --- apps/app/src/composer.tsx | 61 ++++++++++++++++++++++++++++++++ apps/app/src/conversation.tsx | 3 ++ packages/channel/src/channel.ts | 5 +-- packages/channel/src/protocol.ts | 2 ++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/apps/app/src/composer.tsx b/apps/app/src/composer.tsx index d56e93c1..a7908256 100644 --- a/apps/app/src/composer.tsx +++ b/apps/app/src/composer.tsx @@ -40,6 +40,10 @@ const LEADING = /^\s*@ace\b/i; const UNPICKED: { model?: string; effort?: Effort } = {}; const key = (model: ModelRef) => `${model.provider}/${model.modelId}`; +// Errors a provider gives for the model or its key, which another try with them would repeat. +const MODEL_FAULT = + /\b(401|403|404)\b|api key|unauthori[sz]ed|authenticat|permission|does not exist|not found|access to/i; +const RETRY = "Try again."; type Props = { channel: Listing; @@ -57,6 +61,8 @@ type Props = { onDraftLoaded: () => void; /** Messages withdrawn from the chat's queue; the user's own come back into the draft. */ returned: QueuedMessage[]; + /** The error of the chat's latest run, when it failed and nothing has run since. */ + failure?: string; onProviders?: () => void; accessory?: ReactNode; }; @@ -89,6 +95,7 @@ export function Composer( draft, onDraftLoaded, returned, + failure, onProviders, accessory, }: Props, @@ -265,6 +272,42 @@ export function Composer( } } + // Retrying a model's own failure on that model would fail again, so it hands the choice back. + const reset = !!failure && MODEL_FAULT.test(failure) && model !== "auto"; + async function retry() { + if (sending.current) return; + sending.current = true; + setPending(true); + try { + const [provider, ...rest] = model.split("/"); + await host.channel(channel.id, { + op: "ask", + chat: id, + author: user, + text: RETRY, + ...(reset + ? { automatic: true } + : { + ...(levels ? { effort } : {}), + ...(picked && picked !== "auto" + ? { model: { provider: provider!, modelId: rest.join("/") } } + : {}), + }), + }); + if (reset) { + changePick(UNPICKED); + toast("Switched this chat to Automatic", { + description: `${model} failed, so Retry let Ace choose the model.`, + }); + } + } catch (error) { + toast.error("Could not retry", { description: (error as Error).message }); + } finally { + sending.current = false; + setPending(false); + } + } + return ( <> void send(serialize(doc), mode, attachments)} /> + {failure && !blocked && !busy && ( + void retry()} + > + Retry + + } + > + {reset + ? `The last run failed on ${model}. Retry switches this chat to Automatic.` + : "The last run failed."} + + )} {mode === "ace" && !offline && models?.length === 0 && ( item.kind === "reply"); const question = reply?.kind === "reply" && pendingQuestion(reply.tools, transcript.busy); + const last = transcript.items.at(-1); + const failure = !transcript.busy && last?.kind === "reply" ? last.error : undefined; const senders = useMemo(() => sources(channels, channel.host), [channels, channel.host]); const events = useMemo( () => @@ -299,6 +301,7 @@ export function Conversation( draft={draft} onDraftLoaded={onDraftLoaded} returned={transcript.returned} + failure={failure} onProviders={onProviders} accessory={chat?.usage && } /> diff --git a/packages/channel/src/channel.ts b/packages/channel/src/channel.ts index b7b52272..09802540 100644 --- a/packages/channel/src/channel.ts +++ b/packages/channel/src/channel.ts @@ -652,7 +652,7 @@ export class Channel { room.checkAttachments(request); await this.#mayInvoke(request.author); const conversation = await this.#conversation(request.chat); - await this.#select(conversation, request, request.model, request.effort); + await this.#select(conversation, request, request.model, request.effort, request.automatic); const submission = await conversation.submit({ type: "input", content: room.content(request.author, request.text, request), @@ -678,11 +678,12 @@ export class Channel { attached: room.Attached, requested: ModelRef | undefined, effort?: Effort, + automatic?: boolean, ) { const agent = await conversation.agent(context); const current = agent.model; // A chat's inherited model can lose its provider key; only an explicit request must keep it. - const kept = !requested && current && await usable(this.#options.models, current) + const kept = !requested && !automatic && current && await usable(this.#options.models, current) ? current : undefined; const model = requested || kept || await ( diff --git a/packages/channel/src/protocol.ts b/packages/channel/src/protocol.ts index a0f0bb9c..00a40c4c 100644 --- a/packages/channel/src/protocol.ts +++ b/packages/channel/src/protocol.ts @@ -221,6 +221,8 @@ export type Request = images?: Image[]; files?: TextFile[]; model?: ModelRef; + /** Without `model`, replace the chat's model with the channel's choice, as for a new chat. */ + automatic?: boolean; effort?: Effort; requestId?: string; } From 7d69eeb340e8b4f4afedf3ef901aba255534ccc9 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 11:03:17 -0400 Subject: [PATCH 2/3] chore: confirm lane notes Retry touched --- ...YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md | 6 +++--- ...ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md | 6 +++--- ...4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md | 6 +++--- ...XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md | 6 +++--- ...4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md | 6 +++--- ...4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md | 6 +++--- ...ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md | 6 +++--- ...3CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md | 6 +++--- ...J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md | 8 ++++++++ 9 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 .lane/memory/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md b/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md index 18dd2f33..549bcfb1 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md @@ -4,9 +4,9 @@ anchor: function Composer created: 2026-10-15T02:50:44Z norm: '1' sig: 6e0e6c5c8afbd01a -body_hash: f265dfc748a0c278 -raw_hash: dd0efcf54d33e479 -vouched: 2026-10-22T14:39:32Z +body_hash: 75fc838bf3451b29 +raw_hash: 1948feaaac06338f +vouched: 2026-10-22T15:03:16Z lines: 99-282 --- diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md b/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md index 8f827d78..6e37e455 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md @@ -4,9 +4,9 @@ anchor: function Composer created: 2026-10-15T12:44:15Z norm: '1' sig: 6e0e6c5c8afbd01a -body_hash: f265dfc748a0c278 -raw_hash: dd0efcf54d33e479 -vouched: 2026-10-22T14:39:32Z +body_hash: 75fc838bf3451b29 +raw_hash: 1948feaaac06338f +vouched: 2026-10-22T15:03:16Z lines: 101-290 --- diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md b/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md index a71ce0c5..b93dd58e 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md @@ -4,9 +4,9 @@ anchor: function Composer created: 2026-10-15T14:16:28Z norm: '1' sig: 6e0e6c5c8afbd01a -body_hash: f265dfc748a0c278 -raw_hash: dd0efcf54d33e479 -vouched: 2026-10-22T14:39:32Z +body_hash: 75fc838bf3451b29 +raw_hash: 1948feaaac06338f +vouched: 2026-10-22T15:03:16Z lines: 104-326 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md b/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md index aa4638b4..080b98f6 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-14T17:33:22Z norm: '1' sig: c905677fa4be33c1 -body_hash: e05cfd4b5bb4c4b3 -raw_hash: aeb6b16210ed310a -vouched: 2026-10-22T14:35:52Z +body_hash: 64e6dacda54276dc +raw_hash: 6d689f07f518e3a9 +vouched: 2026-10-22T15:03:17Z lines: 114-792 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md b/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md index 0ea33294..23738cab 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T00:30:42Z norm: '1' sig: c905677fa4be33c1 -body_hash: e05cfd4b5bb4c4b3 -raw_hash: aeb6b16210ed310a -vouched: 2026-10-22T14:35:52Z +body_hash: 64e6dacda54276dc +raw_hash: 6d689f07f518e3a9 +vouched: 2026-10-22T15:03:17Z lines: 114-792 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md b/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md index ad794159..9ab111f8 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T02:34:37Z norm: '1' sig: c905677fa4be33c1 -body_hash: e05cfd4b5bb4c4b3 -raw_hash: aeb6b16210ed310a -vouched: 2026-10-22T14:35:52Z +body_hash: 64e6dacda54276dc +raw_hash: 6d689f07f518e3a9 +vouched: 2026-10-22T15:03:17Z lines: 114-794 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md b/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md index f237a86b..57c3acf1 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T10:17:55Z norm: '1' sig: c905677fa4be33c1 -body_hash: e05cfd4b5bb4c4b3 -raw_hash: aeb6b16210ed310a -vouched: 2026-10-22T14:35:52Z +body_hash: 64e6dacda54276dc +raw_hash: 6d689f07f518e3a9 +vouched: 2026-10-22T15:03:17Z lines: 121-817 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md b/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md index 932a16c9..359620a1 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-16T22:11:35Z norm: '1' sig: c905677fa4be33c1 -body_hash: e05cfd4b5bb4c4b3 -raw_hash: aeb6b16210ed310a -vouched: 2026-10-22T14:35:52Z +body_hash: 64e6dacda54276dc +raw_hash: 6d689f07f518e3a9 +vouched: 2026-10-22T15:03:17Z lines: 127-873 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md b/.lane/memory/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md new file mode 100644 index 00000000..82d12b13 --- /dev/null +++ b/.lane/memory/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md @@ -0,0 +1,8 @@ +--- +id: 01M5J26DEM7Z0EHWJN3F79ZZZS +anchor: class Channel +created: 2026-10-22T15:03:10Z +norm: '1' +--- + +an ask with `automatic` and no model must replace the chat's model with the channel's choice, so Retry can leave a model that fails From 2fe14964c12e84263ed0cc3d85298870f2a4a285 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 11:03:29 -0400 Subject: [PATCH 3/3] lane: sync fix-retry memory --- ...01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md | 4 ++++ 1 file changed, 4 insertions(+) rename .lane/{memory => attic}/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md (71%) diff --git a/.lane/memory/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md b/.lane/attic/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md similarity index 71% rename from .lane/memory/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md rename to .lane/attic/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md index 82d12b13..15d9d15a 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md +++ b/.lane/attic/packages/channel/src/channel.ts/01M5J26DEM7Z0EHWJN3F79ZZZS-an-ask-with-automatic-and-no.md @@ -3,6 +3,10 @@ id: 01M5J26DEM7Z0EHWJN3F79ZZZS anchor: class Channel created: 2026-10-22T15:03:10Z norm: '1' +sig: c905677fa4be33c1 +body_hash: 64e6dacda54276dc +raw_hash: 6d689f07f518e3a9 +lines: 157-1008 --- an ask with `automatic` and no model must replace the chat's model with the channel's choice, so Retry can leave a model that fails