From 179af864ebab35180652691a11a96d9914c9455f Mon Sep 17 00:00:00 2001 From: Birfy Date: Mon, 19 Oct 2026 10:10:09 +0800 Subject: [PATCH 1/2] Start a Bot made by another Bot on its maker's model (#410) --- .../410-inherit-creator-model.fixed.md | 1 + core/src/host/roster.js | 12 +++++-- core/src/host/tools.js | 2 +- core/test/roster.test.js | 26 +++++++++++++++ docs/subsystems/core-host.md | 1 + patches/ds-bot/0002-flybot-api-host.patch | 25 ++++++++++++-- patches/ds-bot/0009-identity-schema.patch | 33 +++++++++++++++++++ 7 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 changelog.d/410-inherit-creator-model.fixed.md diff --git a/changelog.d/410-inherit-creator-model.fixed.md b/changelog.d/410-inherit-creator-model.fixed.md new file mode 100644 index 000000000..005c4f182 --- /dev/null +++ b/changelog.d/410-inherit-creator-model.fixed.md @@ -0,0 +1 @@ +A Bot that the Main Bot (or a tree parent) creates without naming a model now starts on its creator's model and thinking level before its first request, so it no longer fails first on an unconfigured default model ([#410](https://github.com/ThinkFlowLab/FlyBot/issues/410)). diff --git a/core/src/host/roster.js b/core/src/host/roster.js index 441dcc292..051b59098 100644 --- a/core/src/host/roster.js +++ b/core/src/host/roster.js @@ -70,8 +70,16 @@ export function install(rt) { } else if (cleanName === '') throw new Error('A friend\'s Bot needs a name') const cleanAvatarValue = cleanAvatar(avatar) // The model is fixed at creation, so a later change of the DSH default leaves it alone. - const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : await rt.defaultModel() - const effortId = modelRef !== undefined && effort !== undefined ? await rt.resolveEffort(modelRef, effort) : undefined + // A Bot made by another Bot (create_bot, a tree child) with no model named starts on its + // maker's model and thinking level (#410), set before its first request, so it never + // tries an unconfigured profile default first. A friend's Bot is never a maker here. + const maker = !model && !pickModel && !isRemote && createdBy ? rt.botOf(createdBy) : undefined + const inherited = maker && !maker.remote ? await rt.desiredModel(maker) : undefined + const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : inherited ?? await rt.defaultModel() + const inheritedEffort = effort === undefined && inherited !== undefined && inherited === maker.model ? maker.effort : undefined + const effortId = modelRef === undefined ? undefined + : effort !== undefined ? await rt.resolveEffort(modelRef, effort) + : inheritedEffort !== undefined ? await rt.resolveEffort(modelRef, inheritedEffort).catch(() => undefined) : undefined const dir = workDir === undefined || workDir === null ? undefined : await rt.cleanWorkDir(workDir) // Every Bot binds to the team's one owner (#47), resolved before the Session exists // so a failure here leaves nothing to clean up. diff --git a/core/src/host/tools.js b/core/src/host/tools.js index d63ae8b88..053d8220c 100644 --- a/core/src/host/tools.js +++ b/core/src/host/tools.js @@ -254,7 +254,7 @@ export function install(rt) { role: { type: 'string', required: true, description: 'Two-to-four character role label shown under the name' }, instructions: { type: 'string', required: true, description: 'What this Bot is responsible for and how it should work' }, brief: { type: 'string', description: 'Why the user wants it, passed to the new Bot as its first context' }, - model: { type: 'string', description: 'Model the Bot runs on, as provider/model or a model id. Only when the user names one; otherwise the DSH default model.' }, + model: { type: 'string', description: 'Model the Bot runs on, as provider/model or a model id. Only when the user names one; otherwise the new Bot starts on your own model.' }, work_dir: WORK_DIR_PARAM, }, output, diff --git a/core/test/roster.test.js b/core/test/roster.test.js index 430c0989a..7db887e08 100644 --- a/core/test/roster.test.js +++ b/core/test/roster.test.js @@ -119,6 +119,32 @@ test('other Bots start on the default model and greet at once', async (t) => { assert.deepEqual(sent.map(entry => entry.sessionId), [writer.id]) }) +test('a Bot made by the Main Bot starts on the Main Bot\'s model and level before its first request (#410)', async (t) => { + const reasoning = { 'deepseek/deepseek-v4-pro': { reasoning: { efforts: [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }], defaultEffort: 'high' } } } + // The profile default is the first served model (flash); Chief runs on pro. + const { call, stop, sent, selected, ctx } = start(undefined, { models: ['deepseek-v4-flash', 'deepseek-v4-pro'], reasoning }) + t.after(stop) + const chief = (await call('state', {})).value.mainBotId + await call('update-bot', { id: chief, model: 'deepseek/deepseek-v4-pro', effort: 'low' }) + await until(() => sent.some(entry => entry.sessionId === chief)) + await ctx.registeredTools.get('create_bot').execute({ name: 'Writer', role: 'Writer', instructions: 'Write.' }, { agent: { id: chief } }) + const writer = (await call('state', {})).value.bots.find(entry => entry.name === 'Writer') + assert.equal(writer.model, 'deepseek/deepseek-v4-pro') + assert.equal(writer.effort, 'low') + await until(() => sent.some(entry => entry.sessionId === writer.id)) + // No model selection for the new Bot ever names the profile default. + const own = selected.filter(request => request.sessionId === writer.id) + assert.ok(own.length > 0) + assert.ok(own.every(request => request.model === 'deepseek-v4-pro'), JSON.stringify(own)) + // A model the Main Bot names still wins; a Bot the user makes keeps the default. + await ctx.registeredTools.get('create_bot').execute({ name: 'Editor', role: 'Editor', instructions: 'Edit.', model: 'deepseek/deepseek-v4-flash' }, { agent: { id: chief } }) + const editor = (await call('state', {})).value.bots.find(entry => entry.name === 'Editor') + assert.equal(editor.model, 'deepseek/deepseek-v4-flash') + assert.equal(editor.effort, undefined) + const typed = (await call('create-bot', { name: 'Typist' })).value + assert.equal(typed.model, 'deepseek/deepseek-v4-flash') +}) + test('a new name must be one "@" can call; a name saved before the rule still saves', async (t) => { const { call, stop } = start(undefined, { models: ['deepseek-v4-flash'] }) t.after(stop) diff --git a/docs/subsystems/core-host.md b/docs/subsystems/core-host.md index 55e1a68ab..27572fc44 100644 --- a/docs/subsystems/core-host.md +++ b/docs/subsystems/core-host.md @@ -114,6 +114,7 @@ credentials and team mode remain separate work under #55/#198. - Another member's own face-room line arrives through `flybot.rooms.postHuman` (#180): a quiet `postToRoom` post carrying `source.human { authorId, name }`. `group_relay` turns it into a round reply `{ kind: 'user', other: true, name, authorId }` (not the grey `notice`), so it draws as a left-hand bubble from that person; it asks no Bot to answer. `postNotice` stays for system events (joined, left, started). Covered by `core/test/rounds.test.js`. - `flybot.chatApprovals.changed()` saves the team (moving the revision) so `bot.pendingCount` reaches the browser at once when a hold arrives or is decided (#226). Covered by `core/test/flybot.test.js`. - The DSH composer still prompts the group Session (`user/message` → `group_relay`). Face-to-face turn rule (#167, changed in #287 and #295), `openingSpeakers`: a **plain** line (no `@` naming a Bot) starts one round in which **every** local Bot may answer, the admin first then the room's order, capped at `FACE_PLAIN_SPEAKERS` (6), one at a time and each after the earlier replies; that round is `single` (`isPlainFaceLine`): replies' own `@`s start no follow-up turns inside it. **Routed by intent (#299):** a plain line that is not `@all` (`route` on the round) also tells each Bot in its prompt (`routeNote`) to answer only when the line is theirs — it names them or their role, no other member's role fits better, or it follows up on their own last answer — and to reply `[PASS]` otherwise (the default; no "add something new" licence); the first Bot that posts, works on it or waits for approval ends a non-broadcast routed round (`answeredAlready`): the Bots after it are marked passed without a model call or a line, so three Bots never post three similar answers. The owner's own typed line reaches a face round through the Session inbox (the round's opener is the quiet `name: text` notice), so `drainRoomInbox` routes it (`routeFor`, `single`, `lastFirst`; lines queued together by the newest one) when it starts the round; a friend's attested line routes at the opener. A line to everyone (`ROOM_EVERYONE`) is not cut; a pass (`isPass`) posts, logs and shows nothing. A line to everyone (`ROOM_EVERYONE`: 你们/大家/everyone/introduce yourselves…) says each may answer. The Bot that spoke last before the line (`recentSpeaker`, a follow-up) is asked first and named in the note. When every Bot passes on the **owner's own** line (not a friend's attested one, whose home answers it; not a follow-up to a friend's Bot), `fallbackDue` appends one turn so the first Bot that passed answers with no pass option. `everyone` in the post's source marks a friend's `@all` (not routed). Covered by `core/test/rounds.test.js`. `@Name`, the label the picker shows (`Friend's Chief`, `Writer · Friend's Bot`) or a handle picks that local Bot (`face-mentions.js`); `@all` is everyone; a line that only @s a friend's Bot starts none here; A friend's line (`flybot.rooms.postHuman`, only when the server attests `typed_by_owner`, passed as `attested`) names my Bot by label or handle (`callIds` in the post's source, read by `openingSpeakers` before the quiet rule), or is plain or `@all` and then carries `plain: true`, so my Bots answer it in the same single pass (#295); a line that only names a friend's Bot, or one from a server that cannot attest, stays quiet; a bare `@Chief` from a friend whose own Chief is in the room calls nobody here. A Bot's post in a face room still starts only the Bots it @s, so Bots in two homes cannot set each other off. The reply is held / sent under the usual approval and Auto-send rules. An `@` that names nobody in the room adds one quiet notice line (`No one in this room is called @X …`). A face-room line the send path already posted (`rt.notePostedLine`) is not appended or sent again when the round reads it from the Session. Covered by `core/test/rounds.test.js`, `core/test/face-mentions.test.js` and `plugins/social/test/social.test.js`. +- New Bot model (#410): `roster.js` `createBot` fixes a Bot's model at creation. A Bot made by another Bot (`create_bot`, a tree child via `flybot.bot.create` with `createdBy`) and given no model starts on its maker's model (`desiredModel`) and, on that same model, its thinking level, before the kickoff, so its first request never goes to an unconfigured profile default. A model the caller names wins; a Bot the user makes in the UI keeps the client's start model (Main Bot's, #230). Covered by `core/test/roster.test.js`. - Group Session models (#289): `applyModel` does not trust `appliedModel` while a Bot is `modelStale`. A failed model selection in a group Session (from `applyModel` or from `room-participant.js` `open()`) marks the Bot stale; the next apply (model-list change, restart, or the Bot's next turn in a known group Session) selects every Session again and clears the mark once all took it. Covered by `core/test/models.test.js`. ### Face-to-face participant look diff --git a/patches/ds-bot/0002-flybot-api-host.patch b/patches/ds-bot/0002-flybot-api-host.patch index 9f749b44a..63fcb5d68 100644 --- a/patches/ds-bot/0002-flybot-api-host.patch +++ b/patches/ds-bot/0002-flybot-api-host.patch @@ -106,7 +106,7 @@ diff --git a/src/host/roster.js b/src/host/roster.js state.workspaceId = workspace.id await rt.save() return workspace.id -@@ -58,16 +58,24 @@ export function install(rt) { +@@ -58,16 +58,32 @@ export function install(rt) { // `pickModel` leaves the model to the user: the chat opens on a model card, and the // greeting waits until a model is chosen. So does a Bot made when no model is served. @@ -126,8 +126,18 @@ diff --git a/src/host/roster.js b/src/host/roster.js + } else if (cleanName === '') throw new Error('A friend\'s Bot needs a name') const cleanAvatarValue = cleanAvatar(avatar) // The model is fixed at creation, so a later change of the DSH default leaves it alone. - const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : await rt.defaultModel() - const effortId = modelRef !== undefined && effort !== undefined ? await rt.resolveEffort(modelRef, effort) : undefined +- const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : await rt.defaultModel() +- const effortId = modelRef !== undefined && effort !== undefined ? await rt.resolveEffort(modelRef, effort) : undefined ++ // A Bot made by another Bot (create_bot, a tree child) with no model named starts on its ++ // maker's model and thinking level (#410), set before its first request, so it never ++ // tries an unconfigured profile default first. A friend's Bot is never a maker here. ++ const maker = !model && !pickModel && !isRemote && createdBy ? rt.botOf(createdBy) : undefined ++ const inherited = maker && !maker.remote ? await rt.desiredModel(maker) : undefined ++ const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : inherited ?? await rt.defaultModel() ++ const inheritedEffort = effort === undefined && inherited !== undefined && inherited === maker.model ? maker.effort : undefined ++ const effortId = modelRef === undefined ? undefined ++ : effort !== undefined ? await rt.resolveEffort(modelRef, effort) ++ : inheritedEffort !== undefined ? await rt.resolveEffort(modelRef, inheritedEffort).catch(() => undefined) : undefined const dir = workDir === undefined || workDir === null ? undefined : await rt.cleanWorkDir(workDir) + // Every Bot binds to the team's one owner (#47), resolved before the Session exists + // so a failure here leaves nothing to clean up. @@ -829,6 +839,15 @@ diff --git a/src/host/tools.js b/src/host/tools.js return `Stopped ${target.name} in ${running.map(whereOf).join(' and ')}. It is told why in a new turn, whose reply comes back to you.` }, })) +@@ -175,7 +175,7 @@ export function install(rt) { + role: { type: 'string', required: true, description: 'Two-to-four character role label shown under the name' }, + instructions: { type: 'string', required: true, description: 'What this Bot is responsible for and how it should work' }, + brief: { type: 'string', description: 'Why the user wants it, passed to the new Bot as its first context' }, +- model: { type: 'string', description: 'Model the Bot runs on, as provider/model or a model id. Only when the user names one; otherwise the DSH default model.' }, ++ model: { type: 'string', description: 'Model the Bot runs on, as provider/model or a model id. Only when the user names one; otherwise the new Bot starts on your own model.' }, + work_dir: WORK_DIR_PARAM, + }, + output, @@ -257,7 +257,7 @@ export function install(rt) { } return { bots, unknown } diff --git a/patches/ds-bot/0009-identity-schema.patch b/patches/ds-bot/0009-identity-schema.patch index 566742f0f..229bf66c7 100644 --- a/patches/ds-bot/0009-identity-schema.patch +++ b/patches/ds-bot/0009-identity-schema.patch @@ -3,6 +3,39 @@ FlyBot patch 0009-identity-schema on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Gen diff --git a/test/roster.test.js b/test/roster.test.js --- a/test/roster.test.js +++ b/test/roster.test.js +@@ -119,6 +119,32 @@ test('other Bots start on the default model and greet at once', async (t) => { + assert.deepEqual(sent.map(entry => entry.sessionId), [writer.id]) + }) + ++test('a Bot made by the Main Bot starts on the Main Bot\'s model and level before its first request (#410)', async (t) => { ++ const reasoning = { 'deepseek/deepseek-v4-pro': { reasoning: { efforts: [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }], defaultEffort: 'high' } } } ++ // The profile default is the first served model (flash); Chief runs on pro. ++ const { call, stop, sent, selected, ctx } = start(undefined, { models: ['deepseek-v4-flash', 'deepseek-v4-pro'], reasoning }) ++ t.after(stop) ++ const chief = (await call('state', {})).value.mainBotId ++ await call('update-bot', { id: chief, model: 'deepseek/deepseek-v4-pro', effort: 'low' }) ++ await until(() => sent.some(entry => entry.sessionId === chief)) ++ await ctx.registeredTools.get('create_bot').execute({ name: 'Writer', role: 'Writer', instructions: 'Write.' }, { agent: { id: chief } }) ++ const writer = (await call('state', {})).value.bots.find(entry => entry.name === 'Writer') ++ assert.equal(writer.model, 'deepseek/deepseek-v4-pro') ++ assert.equal(writer.effort, 'low') ++ await until(() => sent.some(entry => entry.sessionId === writer.id)) ++ // No model selection for the new Bot ever names the profile default. ++ const own = selected.filter(request => request.sessionId === writer.id) ++ assert.ok(own.length > 0) ++ assert.ok(own.every(request => request.model === 'deepseek-v4-pro'), JSON.stringify(own)) ++ // A model the Main Bot names still wins; a Bot the user makes keeps the default. ++ await ctx.registeredTools.get('create_bot').execute({ name: 'Editor', role: 'Editor', instructions: 'Edit.', model: 'deepseek/deepseek-v4-flash' }, { agent: { id: chief } }) ++ const editor = (await call('state', {})).value.bots.find(entry => entry.name === 'Editor') ++ assert.equal(editor.model, 'deepseek/deepseek-v4-flash') ++ assert.equal(editor.effort, undefined) ++ const typed = (await call('create-bot', { name: 'Typist' })).value ++ assert.equal(typed.model, 'deepseek/deepseek-v4-flash') ++}) ++ + test('a new name must be one "@" can call; a name saved before the rule still saves', async (t) => { + const { call, stop } = start(undefined, { models: ['deepseek-v4-flash'] }) + t.after(stop) @@ -526,3 +526,34 @@ test('a write that fails at save time archives the Session it made', async (t) = const saved = JSON.parse(readFileSync(join(home, 'state.json'), 'utf8')) assert.equal(Object.values(saved.bots).some(bot => bot.name === 'Phantom'), false) From edf652781442f711f14eea2a5c824a656e8497ed Mon Sep 17 00:00:00 2001 From: Birfy Date: Mon, 19 Oct 2026 10:23:36 +0800 Subject: [PATCH 2/2] Inherit the maker's level against its effective model (#410) --- core/src/host/roster.js | 5 +++-- docs/subsystems/core-host.md | 2 +- patches/ds-bot/0002-flybot-api-host.patch | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/host/roster.js b/core/src/host/roster.js index 051b59098..0d92db0bf 100644 --- a/core/src/host/roster.js +++ b/core/src/host/roster.js @@ -71,12 +71,13 @@ export function install(rt) { const cleanAvatarValue = cleanAvatar(avatar) // The model is fixed at creation, so a later change of the DSH default leaves it alone. // A Bot made by another Bot (create_bot, a tree child) with no model named starts on its - // maker's model and thinking level (#410), set before its first request, so it never + // maker's effective model and thinking level (#410; the level resolved against that + // model, as applyModel does for the maker), set before its first request, so it never // tries an unconfigured profile default first. A friend's Bot is never a maker here. const maker = !model && !pickModel && !isRemote && createdBy ? rt.botOf(createdBy) : undefined const inherited = maker && !maker.remote ? await rt.desiredModel(maker) : undefined const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : inherited ?? await rt.defaultModel() - const inheritedEffort = effort === undefined && inherited !== undefined && inherited === maker.model ? maker.effort : undefined + const inheritedEffort = effort === undefined && inherited !== undefined ? maker.effort : undefined const effortId = modelRef === undefined ? undefined : effort !== undefined ? await rt.resolveEffort(modelRef, effort) : inheritedEffort !== undefined ? await rt.resolveEffort(modelRef, inheritedEffort).catch(() => undefined) : undefined diff --git a/docs/subsystems/core-host.md b/docs/subsystems/core-host.md index 27572fc44..5e4d9b88e 100644 --- a/docs/subsystems/core-host.md +++ b/docs/subsystems/core-host.md @@ -114,7 +114,7 @@ credentials and team mode remain separate work under #55/#198. - Another member's own face-room line arrives through `flybot.rooms.postHuman` (#180): a quiet `postToRoom` post carrying `source.human { authorId, name }`. `group_relay` turns it into a round reply `{ kind: 'user', other: true, name, authorId }` (not the grey `notice`), so it draws as a left-hand bubble from that person; it asks no Bot to answer. `postNotice` stays for system events (joined, left, started). Covered by `core/test/rounds.test.js`. - `flybot.chatApprovals.changed()` saves the team (moving the revision) so `bot.pendingCount` reaches the browser at once when a hold arrives or is decided (#226). Covered by `core/test/flybot.test.js`. - The DSH composer still prompts the group Session (`user/message` → `group_relay`). Face-to-face turn rule (#167, changed in #287 and #295), `openingSpeakers`: a **plain** line (no `@` naming a Bot) starts one round in which **every** local Bot may answer, the admin first then the room's order, capped at `FACE_PLAIN_SPEAKERS` (6), one at a time and each after the earlier replies; that round is `single` (`isPlainFaceLine`): replies' own `@`s start no follow-up turns inside it. **Routed by intent (#299):** a plain line that is not `@all` (`route` on the round) also tells each Bot in its prompt (`routeNote`) to answer only when the line is theirs — it names them or their role, no other member's role fits better, or it follows up on their own last answer — and to reply `[PASS]` otherwise (the default; no "add something new" licence); the first Bot that posts, works on it or waits for approval ends a non-broadcast routed round (`answeredAlready`): the Bots after it are marked passed without a model call or a line, so three Bots never post three similar answers. The owner's own typed line reaches a face round through the Session inbox (the round's opener is the quiet `name: text` notice), so `drainRoomInbox` routes it (`routeFor`, `single`, `lastFirst`; lines queued together by the newest one) when it starts the round; a friend's attested line routes at the opener. A line to everyone (`ROOM_EVERYONE`) is not cut; a pass (`isPass`) posts, logs and shows nothing. A line to everyone (`ROOM_EVERYONE`: 你们/大家/everyone/introduce yourselves…) says each may answer. The Bot that spoke last before the line (`recentSpeaker`, a follow-up) is asked first and named in the note. When every Bot passes on the **owner's own** line (not a friend's attested one, whose home answers it; not a follow-up to a friend's Bot), `fallbackDue` appends one turn so the first Bot that passed answers with no pass option. `everyone` in the post's source marks a friend's `@all` (not routed). Covered by `core/test/rounds.test.js`. `@Name`, the label the picker shows (`Friend's Chief`, `Writer · Friend's Bot`) or a handle picks that local Bot (`face-mentions.js`); `@all` is everyone; a line that only @s a friend's Bot starts none here; A friend's line (`flybot.rooms.postHuman`, only when the server attests `typed_by_owner`, passed as `attested`) names my Bot by label or handle (`callIds` in the post's source, read by `openingSpeakers` before the quiet rule), or is plain or `@all` and then carries `plain: true`, so my Bots answer it in the same single pass (#295); a line that only names a friend's Bot, or one from a server that cannot attest, stays quiet; a bare `@Chief` from a friend whose own Chief is in the room calls nobody here. A Bot's post in a face room still starts only the Bots it @s, so Bots in two homes cannot set each other off. The reply is held / sent under the usual approval and Auto-send rules. An `@` that names nobody in the room adds one quiet notice line (`No one in this room is called @X …`). A face-room line the send path already posted (`rt.notePostedLine`) is not appended or sent again when the round reads it from the Session. Covered by `core/test/rounds.test.js`, `core/test/face-mentions.test.js` and `plugins/social/test/social.test.js`. -- New Bot model (#410): `roster.js` `createBot` fixes a Bot's model at creation. A Bot made by another Bot (`create_bot`, a tree child via `flybot.bot.create` with `createdBy`) and given no model starts on its maker's model (`desiredModel`) and, on that same model, its thinking level, before the kickoff, so its first request never goes to an unconfigured profile default. A model the caller names wins; a Bot the user makes in the UI keeps the client's start model (Main Bot's, #230). Covered by `core/test/roster.test.js`. +- New Bot model (#410): `roster.js` `createBot` fixes a Bot's model at creation. A Bot made by another Bot (`create_bot`, a tree child via `flybot.bot.create` with `createdBy`) and given no model starts on its maker's effective model (`desiredModel`) and its thinking level resolved against that model (dropped when that model does not serve it), before the kickoff, so its first request never goes to an unconfigured profile default. A model the caller names wins; a Bot the user makes in the UI keeps the client's start model (Main Bot's, #230). Covered by `core/test/roster.test.js`. - Group Session models (#289): `applyModel` does not trust `appliedModel` while a Bot is `modelStale`. A failed model selection in a group Session (from `applyModel` or from `room-participant.js` `open()`) marks the Bot stale; the next apply (model-list change, restart, or the Bot's next turn in a known group Session) selects every Session again and clears the mark once all took it. Covered by `core/test/models.test.js`. ### Face-to-face participant look diff --git a/patches/ds-bot/0002-flybot-api-host.patch b/patches/ds-bot/0002-flybot-api-host.patch index 63fcb5d68..64ec793b6 100644 --- a/patches/ds-bot/0002-flybot-api-host.patch +++ b/patches/ds-bot/0002-flybot-api-host.patch @@ -106,7 +106,7 @@ diff --git a/src/host/roster.js b/src/host/roster.js state.workspaceId = workspace.id await rt.save() return workspace.id -@@ -58,16 +58,32 @@ export function install(rt) { +@@ -58,16 +58,33 @@ export function install(rt) { // `pickModel` leaves the model to the user: the chat opens on a model card, and the // greeting waits until a model is chosen. So does a Bot made when no model is served. @@ -129,12 +129,13 @@ diff --git a/src/host/roster.js b/src/host/roster.js - const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : await rt.defaultModel() - const effortId = modelRef !== undefined && effort !== undefined ? await rt.resolveEffort(modelRef, effort) : undefined + // A Bot made by another Bot (create_bot, a tree child) with no model named starts on its -+ // maker's model and thinking level (#410), set before its first request, so it never ++ // maker's effective model and thinking level (#410; the level resolved against that ++ // model, as applyModel does for the maker), set before its first request, so it never + // tries an unconfigured profile default first. A friend's Bot is never a maker here. + const maker = !model && !pickModel && !isRemote && createdBy ? rt.botOf(createdBy) : undefined + const inherited = maker && !maker.remote ? await rt.desiredModel(maker) : undefined + const modelRef = model ? await rt.resolveModel(model) : pickModel ? undefined : inherited ?? await rt.defaultModel() -+ const inheritedEffort = effort === undefined && inherited !== undefined && inherited === maker.model ? maker.effort : undefined ++ const inheritedEffort = effort === undefined && inherited !== undefined ? maker.effort : undefined + const effortId = modelRef === undefined ? undefined + : effort !== undefined ? await rt.resolveEffort(modelRef, effort) + : inheritedEffort !== undefined ? await rt.resolveEffort(modelRef, inheritedEffort).catch(() => undefined) : undefined