From 3c078ef8a85cf337e684e2e02862fb5f82c83394 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 16:30:39 -0400 Subject: [PATCH 1/3] fix: save the composer's model pick on the chat so every view names the next run's model The composer's pick lived in browser storage until a send, so details, `ace info` and other clients named a different model. A pick now selects the chat's model and effort through a new `select` request; channels announce the change to watchers, and info names what Automatic resolves to, which a default model change moves. Why: apps/app/src/composer.tsx#function Composer | the chat holds the model and effort pick (select op); the composer must not keep its own browser copy, or details and the CLI disagree with it --- apps/app/src/channel.tsx | 10 ++- apps/app/src/composer.tsx | 76 ++++++++++++------- apps/app/src/conversation.tsx | 1 + apps/app/src/transcript.ts | 21 ++++- apps/host/src/cli.ts | 19 ++++- apps/host/src/worker.ts | 2 +- packages/channel/src/channel.ts | 57 +++++++++++--- packages/channel/src/protocol.ts | 19 +++++ .../src/components/composer/chat-composer.tsx | 2 +- 9 files changed, 160 insertions(+), 47 deletions(-) diff --git a/apps/app/src/channel.tsx b/apps/app/src/channel.tsx index 07903c94..155dc6dd 100644 --- a/apps/app/src/channel.tsx +++ b/apps/app/src/channel.tsx @@ -115,7 +115,9 @@ function where(channel: Listing): DetailsFact[] { function facts(channel: Listing, info?: ChannelInfo): DetailsFact[] { const root = info?.chats.find((value) => value.id === chat); - const model = root?.model || channel.model; + const chosen = root?.model || channel.model; + // A chat without a model runs what Automatic picks, which a default model change moves. + const model = chosen || info?.automatic; // "off" also stands for an always-thinking provider's default, so it isn't shown as a level. const effort = root?.model && root.effort !== "off" ? root.effort : undefined; const usage = info?.chats.reduce( @@ -136,7 +138,11 @@ function facts(channel: Listing, info?: ChannelInfo): DetailsFact[] { ...(model ? [{ label: "Model", - value: effort ? `${model.modelId} · ${EFFORTS[effort]}` : model.modelId, + value: effort + ? `${model.modelId} · ${EFFORTS[effort]}` + : chosen + ? model.modelId + : `${model.modelId} (Automatic)`, title: `${model.provider}/${model.modelId}${effort ? `, ${EFFORTS[effort]} effort` : ""}`, }] : []), diff --git a/apps/app/src/composer.tsx b/apps/app/src/composer.tsx index cee8e5a6..e51793c1 100644 --- a/apps/app/src/composer.tsx +++ b/apps/app/src/composer.tsx @@ -47,7 +47,6 @@ export const EFFORTS: Record = { }; const ACE = /(^|\s)@ace\b/i; 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. @@ -61,6 +60,8 @@ type Props = { user: string; current?: ModelRef; currentEffort?: Effort; + /** What the channel picks for a chat on Automatic. */ + automatic?: ModelRef; busy: boolean; /** What a pending send waits on, as its submit control's label. */ waiting: string; @@ -100,6 +101,7 @@ export function Composer( user, current, currentEffort, + automatic, busy, waiting, shared, @@ -118,7 +120,7 @@ export function Composer( const typed = useRef(0); const [pending, setPending] = useState(false); const [attached, setAttached] = useState([]); - // Kept beside the channel's draft so a deliberate mode and an unsent model pick survive relaunch. + // Kept beside the channel's draft so a deliberate mode survives relaunch. const scope = `/channels/${channel.id}`; const [stored, changeMode] = useLocalStorage(`ace:mode:${scope}`, "ace"); // The channel refuses a blocked teammate's ask, stop, and kill too. @@ -204,14 +206,14 @@ export function Composer( active = false; }; }, [channel.id, mode, status, settingsVersion, offline, ready]); - const [{ model: picked, effort: pickedEffort }, changePick] = useLocalStorage( - `ace:pick:${scope}/${id}`, - UNPICKED, - ); - const model = picked || (current ? key(current) : "auto"); + // The chat holds the pick, so details, the CLI and other clients name what its next run uses. + // A pick shows until the chat's info reports a change, which follows the pick's own answer. + const chosen = `${current ? key(current) : "auto"} ${currentEffort || "off"}`; + const [picking, setPicking] = useState<{ model: string; effort?: Effort; from: string }>(); + if (picking && picking.from !== chosen) setPicking(undefined); + const model = picking?.model || (current ? key(current) : "auto"); const levels = models?.find((value) => key(value) === model)?.efforts; - const requestedEffort = pickedEffort - || (current && key(current) === model ? currentEffort : undefined) || "off"; + const requestedEffort = picking?.effort || currentEffort || "off"; const effort = requestedEffort === "off" || levels?.includes(requestedEffort) ? requestedEffort : "off"; @@ -230,7 +232,11 @@ export function Composer( choices.unshift({ id: model, name: model.split("/").slice(1).join("/"), vendor: "" }); } // Automatic stays offered so a chat whose model lost its key can hand the choice back. - choices.unshift({ id: "auto", name: "Automatic", vendor: "" }); + choices.unshift({ + id: "auto", + name: automatic ? `Automatic (${automatic.modelId})` : "Automatic", + vendor: "", + }); // Formatting tools crowd a phone's composer; they stay one tap away. const phone = useMedia("(width < 40rem)"); @@ -246,6 +252,33 @@ export function Composer( setAttached(kept); } + async function pick(next: string, nextEffort?: Effort) { + const from = chosen; + setPicking({ model: next, effort: nextEffort, from }); + const [provider, ...rest] = next.split("/"); + try { + const result = await host.channel<{ model: ModelRef; effort: Effort }>(channel.id, { + op: "select", + chat: id, + author: user, + ...(next === "auto" + ? { automatic: true } + : { model: { provider: provider!, modelId: rest.join("/") } }), + ...(nextEffort ? { effort: nextEffort } : {}), + }); + setPicking({ model: key(result.model), effort: result.effort, from }); + } catch (error) { + setPicking(undefined); + const { message } = error as Error; + toast.error("Could not change the model", { + description: message, + ...(onProviders && /provider key/i.test(message) + ? { action: { label: "Providers", onClick: onProviders } } + : {}), + }); + } + } + async function send(text: string, mode: string, attached: Attachment[]) { if (sending.current) return; // Posted, a blocked teammate's "@ace" would read as an ask nobody answers. @@ -269,18 +302,14 @@ export function Composer( try { const encoded = await attachments(attached); if (invoke) { - const [provider, ...rest] = model.split("/"); + // The chat holds the pick; asking without a model lets the channel replace one whose + // provider lost its key. await host.channel(channel.id, { op: "ask", - ...(levels ? { effort } : {}), chat: id, author: user, text: body, ...encoded, - // An unpicked model lets the channel replace a chat model whose provider lost its key. - ...(picked && picked !== "auto" - ? { model: { provider: provider!, modelId: rest.join("/") } } - : {}), }); } else { await host.channel(channel.id, { @@ -315,23 +344,14 @@ export function Composer( 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("/") } } - : {}), - }), + ...(reset ? { automatic: true } : {}), }); if (reset) { - changePick(UNPICKED); toast("Switched this chat to Automatic", { description: `${model} failed, so Retry let Ace choose the model.`, }); @@ -358,10 +378,10 @@ export function Composer( mentions={mentions} models={choices} model={model} - onModelChange={(value) => changePick({ model: value })} + onModelChange={(value) => void pick(value)} efforts={efforts} effort={effort} - onEffortChange={(value) => changePick({ model: picked, effort: value as Effort })} + onEffortChange={(value) => void pick(model, value as Effort)} attachments={attached} onAttachmentsChange={attach} busy={busy} diff --git a/apps/app/src/conversation.tsx b/apps/app/src/conversation.tsx index ec2c374b..9e011818 100644 --- a/apps/app/src/conversation.tsx +++ b/apps/app/src/conversation.tsx @@ -320,6 +320,7 @@ export function Conversation( user={user} current={model} currentEffort={chat?.effort} + automatic={transcript.info?.automatic} busy={transcript.busy} waiting={waiting} shared={transcript.shared} diff --git a/apps/app/src/transcript.ts b/apps/app/src/transcript.ts index 951644e2..3df4232d 100644 --- a/apps/app/src/transcript.ts +++ b/apps/app/src/transcript.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import type { Image, QueuedMessage, Reactions, TextFile } from "@ace/channel/protocol"; import { DISCONNECTED } from "@ace/host/gateway-client"; @@ -234,6 +234,8 @@ export function apply(state: Transcript, event: Event): Transcript { } case "live": return { ...state, live: true }; + case "agent": + return state; case "metadata": { const shared = event.shared ?? state.shared; const desktop = event.desktop ?? state.desktop; @@ -316,6 +318,15 @@ export function useTranscript( /** The channel's worker ended the watch and a re-watch is waiting for it to come back. */ const [restarting, setRestarting] = useState(false); const [attempt, setAttempt] = useState(0); + const refresh = useRef<() => void>(undefined); + // A default model change moves what Automatic picks, which info names. + const settingsVersion = useSyncExternalStore(host.subscribe, () => host.settingsVersion); + const settingsSeen = useRef(settingsVersion); + useEffect(() => { + if (settingsVersion === settingsSeen.current) return; + settingsSeen.current = settingsVersion; + refresh.current?.(); + }, [settingsVersion]); const [wasOffline, setWasOffline] = useState(offline); if (offline !== wasOffline) { setWasOffline(offline); @@ -352,11 +363,12 @@ export function useTranscript( // A re-watch replays the whole chat; the shown transcript stays until the replay is // complete so the timeline, and what is expanded in it, stays mounted. let replay: Transcript | undefined = EMPTY; - const refresh = () => + const read = () => host.channel(channel, { op: "info" }).then((value) => { if (current()) setInfo(value); }, () => {}); - void refresh(); + refresh.current = () => void read(); + void read(); host.channel(channel, { op: "watch", queue: true, @@ -364,6 +376,7 @@ export function useTranscript( progress: true, actions: true, typing: true, + agent: true, ...(chat === undefined ? {} : { chat }), }, { event(event) { @@ -375,7 +388,7 @@ export function useTranscript( setRestarting(false); replay = undefined; } - if (event.kind === "run") void refresh(); + if (event.kind === "run" || event.kind === "agent") void read(); }, // A released watch belongs to an old generation; a reconnecting host restarts every watch. closed(reason) { diff --git a/apps/host/src/cli.ts b/apps/host/src/cli.ts index 530b262a..6f6def2b 100755 --- a/apps/host/src/cli.ts +++ b/apps/host/src/cli.ts @@ -36,7 +36,17 @@ import { type WindowInfo, } from "./protocol"; import { checkModel, setModel } from "./settings"; -import { archive, fork, isRunning, kill, models, parseModel, project, remove } from "./manage"; +import { + archive, + defaultModel, + fork, + isRunning, + kill, + models, + parseModel, + project, + remove, +} from "./manage"; import { move } from "./move"; const HELP = `ace — channels on this host @@ -502,7 +512,12 @@ async function main() { case "model": { if (ref) await setModel(ref === "auto" ? null : parseModel(ref)); const saved = preference(); - console.log(saved ? `${saved.provider}/${saved.modelId}` : "Automatic"); + if (saved) console.log(`${saved.provider}/${saved.modelId}`); + else { + // Without a usable key Automatic has nothing to pick yet. + const picked = await defaultModel().catch(() => undefined); + console.log(picked ? `Automatic (${picked.provider}/${picked.modelId})` : "Automatic"); + } if (config.model) console.log(`Overridden by ACE_MODEL=${config.model}`); return; } diff --git a/apps/host/src/worker.ts b/apps/host/src/worker.ts index 1c8727ef..ffb7fee3 100644 --- a/apps/host/src/worker.ts +++ b/apps/host/src/worker.ts @@ -21,7 +21,7 @@ import { tabs } from "./tabs"; const RETIRE_AFTER = 10 * 60_000; /** An archived channel keeps its history as it was archived. */ -const ARCHIVED = new Set(["say", "ask", "chat", "react", "delete", "edit"]); +const ARCHIVED = new Set(["say", "ask", "select", "chat", "react", "delete", "edit"]); seal(); diff --git a/packages/channel/src/channel.ts b/packages/channel/src/channel.ts index 8126f447..456a8937 100644 --- a/packages/channel/src/channel.ts +++ b/packages/channel/src/channel.ts @@ -156,6 +156,7 @@ type Listener = { captions: boolean; actions: boolean; typing: boolean; + agent: boolean; reacted?: boolean; }; @@ -360,6 +361,8 @@ export class Channel { return this.#say(request); case "ask": return this.#ask(request); + case "select": + return this.#admitted(request.chat, () => this.#pick(request)); case "chat": return this.#chat(request); case "answer": { @@ -483,7 +486,19 @@ export class Channel { if (lane) Object.assign(chat, { lane, cwd }); return chat; })); - return { id, name, summary, revision, project, owner, ...this.#settings, chats }; + // Without a usable key there is nothing to name; asking says so. + const automatic = await this.#automatic().catch(() => undefined); + return { + id, + name, + summary, + revision, + project, + owner, + ...this.#settings, + chats, + ...(automatic ? { automatic } : {}), + }; } /** The newest response's own token count; providers report what that request carried. */ @@ -661,11 +676,15 @@ export class Channel { return { submission: submission.id }; } - async #ask(request: Extract) { - const chat = request.chat || ROOT_CONVERSATION_ID; + #ask(request: Extract) { + return this.#admitted(request.chat, () => this.#admit(request)); + } + + async #admitted(id: ChatId | undefined, admit: () => Promise): Promise { + const chat = id || ROOT_CONVERSATION_ID; const previous = this.#admissions.get(chat) || Promise.resolve(); // Selection and submission must stay together across concurrent clients. - const pending = previous.catch(() => {}).then(() => this.#admit(request)); + const pending = previous.catch(() => {}).then(admit); this.#admissions.set(chat, pending); try { return await pending; @@ -689,6 +708,13 @@ export class Channel { return { submission: submission.id }; } + async #pick(request: Extract) { + this.#author(request.author); + await this.#mayInvoke(request.author); + const conversation = await this.#conversation(request.chat); + return this.#select(conversation, {}, request.model, request.effort, request.automatic); + } + async #mayInvoke(author: string) { const settings = await this.#harness.snapshot(SettingsDoc, context); // Hosts rewrite every client's author, so an agent author is another channel's agent on this @@ -706,16 +732,14 @@ export class Channel { requested: ModelRef | undefined, effort?: Effort, automatic?: boolean, - ) { + ): Promise<{ model: ModelRef; effort: Effort }> { 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 && !automatic && current && await usable(this.#options.models, current) ? current : undefined; - const model = requested || kept || await ( - this.#options.defaultModel?.() || choose(this.#options.models) - ); + const model = requested || kept || await this.#automatic(); const selected = this.#options.models.getModel(model.provider, model.modelId); if (!selected) { throw new Error(`Unknown model ${model.provider}/${model.modelId}`); @@ -746,8 +770,22 @@ export class Channel { `The ${name} API key is missing. Add it in Provider settings to invoke an agent. Chat is still available.`, ); } - if (same) return; + if (same) return { model, effort: thinkingLevel }; await conversation.configure({ model, thinkingLevel }, context); + const event: Event = { kind: "agent", chat: conversation.id, model, effort: thinkingLevel }; + for (const listener of this.#listeners) { + if (listener.chat !== conversation.id || !listener.agent) continue; + try { + listener.send(event); + } catch { + this.#listeners.delete(listener); + } + } + return { model, effort: thinkingLevel }; + } + + #automatic(): Promise { + return this.#options.defaultModel?.() || choose(this.#options.models); } async #chat(request: Extract) { @@ -967,6 +1005,7 @@ export class Channel { captions: !!request.captions, actions, typing: !!request.typing, + agent: !!request.agent, }; this.#listeners.add(listener); // Read after the stream's snapshot, so every deletion it holds is here; later ones arrive live. diff --git a/packages/channel/src/protocol.ts b/packages/channel/src/protocol.ts index 5ad6aaad..9681405f 100644 --- a/packages/channel/src/protocol.ts +++ b/packages/channel/src/protocol.ts @@ -55,6 +55,8 @@ export type ChannelInfo = Metadata & { /** Channel-wide native desktop tools gate, including the owner's agents and child chats. */ desktop: boolean; chats: Chat[]; + /** What a chat without a model, or one handed back with Automatic, runs next; absent without a key. */ + automatic?: ModelRef; }; export type Chat = { @@ -238,6 +240,19 @@ export type Request = effort?: Effort; requestId?: string; } + /** + * Set the model and effort the chat's next run uses, as `ask` would, without posting. Answers + * with the chat's resulting model and effort. + */ + | { + op: "select"; + chat?: ChatId; + author: string; + model?: ModelRef; + /** Without `model`, replace the chat's model with the channel's choice, as for a new chat. */ + automatic?: boolean; + effort?: Effort; + } | { op: "chat"; author: string; model?: ModelRef } /** Answer a pending `question` tool call: each question's chosen option indices or written answer. */ | { op: "answer"; chat?: ChatId; call: string; author: string; answers: Answer[] } @@ -280,6 +295,8 @@ export type Request = actions?: boolean; /** Include typing events; older clients do not understand them. */ typing?: boolean; + /** Include agent events; older clients do not understand them. */ + agent?: boolean; }; export type Event = @@ -368,6 +385,8 @@ export type Event = */ | { kind: "recap"; chat: ChatId; calls: string[]; text: string } | { kind: "run"; chat: ChatId; state: "start" | "end" } + /** The chat's model or effort changed, so the next run uses these. */ + | { kind: "agent"; chat: ChatId; model: ModelRef; effort: Effort } /** * Channel-wide metadata, sent initially and whenever its name, summary, or settings change. * `revision` covers only the name and summary. Older hosts omit `shared` and `desktop`; diff --git a/packages/ui/src/components/composer/chat-composer.tsx b/packages/ui/src/components/composer/chat-composer.tsx index b799fbc4..252eaee5 100644 --- a/packages/ui/src/components/composer/chat-composer.tsx +++ b/packages/ui/src/components/composer/chat-composer.tsx @@ -1533,7 +1533,7 @@ function ComposerModel({ state }: { state: ComposerState }) { /> } > - + {selected?.name} From eeb3e22050d55833f5f21cce44895a6de148ca7f Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 16:30:53 -0400 Subject: [PATCH 2/3] chore: confirm and retire lane notes for the model pick --- ...YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md | 0 ...ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md | 6 +++--- ...4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md | 6 +++--- ...JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md | 8 ++++++++ ...YQRMMB6JT09W33MJMPKSMF-a-re-watch-keeps-showing-the.md | 6 +++--- ...YYQEXJX6JQ04TBRZ4DW1X8-a-watch-rejected-with-discon.md | 6 +++--- ...YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md | 6 +++--- ...J0BZWP8SCBK522YT2HMZDZ-ace-info-prints-agent-access.md | 6 +++--- ...YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.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 +++--- 14 files changed, 44 insertions(+), 36 deletions(-) rename .lane/{memory => attic}/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md (100%) create mode 100644 .lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md b/.lane/attic/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md similarity index 100% rename from .lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md rename to .lane/attic/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md 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 afd5a25d..1f6ec00d 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: 7a4597684360db67 -raw_hash: 52e4e5fdab485c4d -vouched: 2026-10-22T17:52:41Z +body_hash: 69f69de77ff20470 +raw_hash: 555034163125d546 +vouched: 2026-10-22T20:30:45Z 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 b8160bdf..eb6b6bad 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: 7a4597684360db67 -raw_hash: 52e4e5fdab485c4d -vouched: 2026-10-22T17:52:41Z +body_hash: 69f69de77ff20470 +raw_hash: 555034163125d546 +vouched: 2026-10-22T20:30:45Z lines: 104-326 --- diff --git a/.lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md b/.lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md new file mode 100644 index 00000000..aecb1ce3 --- /dev/null +++ b/.lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md @@ -0,0 +1,8 @@ +--- +id: 01M5JMY120BQ7Q19708TEHREZF +anchor: function Composer +created: 2026-10-22T20:30:39Z +norm: '1' +--- + +the chat holds the model and effort pick (select op); the composer must not keep its own browser copy, or details and the CLI disagree with it diff --git a/.lane/memory/apps/app/src/transcript.ts/01M4YQRMMB6JT09W33MJMPKSMF-a-re-watch-keeps-showing-the.md b/.lane/memory/apps/app/src/transcript.ts/01M4YQRMMB6JT09W33MJMPKSMF-a-re-watch-keeps-showing-the.md index 25d1fddb..675de253 100644 --- a/.lane/memory/apps/app/src/transcript.ts/01M4YQRMMB6JT09W33MJMPKSMF-a-re-watch-keeps-showing-the.md +++ b/.lane/memory/apps/app/src/transcript.ts/01M4YQRMMB6JT09W33MJMPKSMF-a-re-watch-keeps-showing-the.md @@ -4,9 +4,9 @@ anchor: function useTranscript created: 2026-10-15T02:55:20Z norm: '1' sig: 3961cde46070e655 -body_hash: 6eead60056a515f0 -raw_hash: e83ea96acc31564a -vouched: 2026-10-22T15:28:03Z +body_hash: 715b52c056e02eac +raw_hash: 0c5f8df4fea08fca +vouched: 2026-10-22T20:30:45Z lines: 174-275 --- diff --git a/.lane/memory/apps/app/src/transcript.ts/01M4YYQEXJX6JQ04TBRZ4DW1X8-a-watch-rejected-with-discon.md b/.lane/memory/apps/app/src/transcript.ts/01M4YYQEXJX6JQ04TBRZ4DW1X8-a-watch-rejected-with-discon.md index da801330..b4a1fefb 100644 --- a/.lane/memory/apps/app/src/transcript.ts/01M4YYQEXJX6JQ04TBRZ4DW1X8-a-watch-rejected-with-discon.md +++ b/.lane/memory/apps/app/src/transcript.ts/01M4YYQEXJX6JQ04TBRZ4DW1X8-a-watch-rejected-with-discon.md @@ -4,9 +4,9 @@ anchor: function useTranscript created: 2026-10-15T04:57:01Z norm: '1' sig: 3961cde46070e655 -body_hash: 6eead60056a515f0 -raw_hash: e83ea96acc31564a -vouched: 2026-10-22T15:28:03Z +body_hash: 715b52c056e02eac +raw_hash: 0c5f8df4fea08fca +vouched: 2026-10-22T20:30:45Z lines: 197-314 --- diff --git a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md index 83b8803d..1b38bc22 100644 --- a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md +++ b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md @@ -4,9 +4,9 @@ anchor: function main created: 2026-10-15T04:47:23Z norm: '1' sig: 7d5d3f63a3bddde4 -body_hash: 7770844bad8c4705 -raw_hash: 5a2b69597fdf7f65 -vouched: 2026-10-22T15:24:37Z +body_hash: eb91c91d476265b2 +raw_hash: ec1c6c19cf199017 +vouched: 2026-10-22T20:30:45Z lines: 253-482 --- diff --git a/.lane/memory/apps/host/src/cli.ts/01M5J0BZWP8SCBK522YT2HMZDZ-ace-info-prints-agent-access.md b/.lane/memory/apps/host/src/cli.ts/01M5J0BZWP8SCBK522YT2HMZDZ-ace-info-prints-agent-access.md index e1c59e30..b29ed811 100644 --- a/.lane/memory/apps/host/src/cli.ts/01M5J0BZWP8SCBK522YT2HMZDZ-ace-info-prints-agent-access.md +++ b/.lane/memory/apps/host/src/cli.ts/01M5J0BZWP8SCBK522YT2HMZDZ-ace-info-prints-agent-access.md @@ -4,9 +4,9 @@ anchor: function main created: 2026-10-22T14:31:16Z norm: '1' sig: 7d5d3f63a3bddde4 -body_hash: 7770844bad8c4705 -raw_hash: 5a2b69597fdf7f65 -vouched: 2026-10-22T15:24:37Z +body_hash: eb91c91d476265b2 +raw_hash: ec1c6c19cf199017 +vouched: 2026-10-22T20:30:45Z lines: 332-614 --- diff --git a/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md b/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md index f5c9e29a..eb3ac832 100644 --- a/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md +++ b/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-15T02:58:00Z norm: '1' sig: 1a43097a4e1ad24c -body_hash: 3d68c7dfc3bf4c24 -raw_hash: eac2665348c286fe -vouched: 2026-10-22T14:47:34Z +body_hash: 5e79b1f5e63ad3c5 +raw_hash: 1f3f06a6f1a6594b +vouched: 2026-10-22T20:30:45Z lines: 1-194 --- 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 bd7d01bf..debf8dc3 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: 97ca0ac50b13c167 -raw_hash: 4b1f599e8b95d697 -vouched: 2026-10-22T15:29:33Z +body_hash: 4bdc53b3fed0ba8d +raw_hash: 7726f4f136727a5a +vouched: 2026-10-22T20:30:45Z 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 5da58be6..8a800956 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: 97ca0ac50b13c167 -raw_hash: 4b1f599e8b95d697 -vouched: 2026-10-22T15:29:33Z +body_hash: 4bdc53b3fed0ba8d +raw_hash: 7726f4f136727a5a +vouched: 2026-10-22T20:30:46Z 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 e8b331be..906a011c 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: 97ca0ac50b13c167 -raw_hash: 4b1f599e8b95d697 -vouched: 2026-10-22T15:29:33Z +body_hash: 4bdc53b3fed0ba8d +raw_hash: 7726f4f136727a5a +vouched: 2026-10-22T20:30:46Z 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 9be8c60c..ea6ec4d6 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: 97ca0ac50b13c167 -raw_hash: 4b1f599e8b95d697 -vouched: 2026-10-22T15:29:33Z +body_hash: 4bdc53b3fed0ba8d +raw_hash: 7726f4f136727a5a +vouched: 2026-10-22T20:30:46Z 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 9df3eae3..7458954e 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: 97ca0ac50b13c167 -raw_hash: 4b1f599e8b95d697 -vouched: 2026-10-22T15:29:33Z +body_hash: 4bdc53b3fed0ba8d +raw_hash: 7726f4f136727a5a +vouched: 2026-10-22T20:30:46Z lines: 127-873 --- From 89b3f1e86ba52522d687d742570b178268c1b1e3 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 16:30:55 -0400 Subject: [PATCH 3/3] lane: sync fix-model-next memory --- ...01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md b/.lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md index aecb1ce3..1defe677 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M5JMY120BQ7Q19708TEHREZF-the-chat-holds-the-model-and.md @@ -3,6 +3,10 @@ id: 01M5JMY120BQ7Q19708TEHREZF anchor: function Composer created: 2026-10-22T20:30:39Z norm: '1' +sig: 6e0e6c5c8afbd01a +body_hash: 69f69de77ff20470 +raw_hash: 555034163125d546 +lines: 97-433 --- the chat holds the model and effort pick (select op); the composer must not keep its own browser copy, or details and the CLI disagree with it