Repository navigation
Description
Written-by: Chief-audit
A Bot rename is applied in two steps, and the first one is not undone when the second refuses: updateBot writes the new name into the live record, then renames the DSH Session title, and only saves later. A refused sessionTitle.rename answers ok: false to the caller while the live team already lists the new name, so the page shows a name the store never took (and a restart silently reverts it). Same function, same defect class as a rename that moves one half and leaves the other.
Steps, in core/src/host/roster.js updateBot:
core/src/host/roster.js:199—bot.name = nextcore/src/host/roster.js:201—await ctx.sessionTitle.rename(agent.session, next)← the step made to failcore/src/host/roster.js:238—await rt.save()(never reached)
Input: the owner or a Main Bot through update-bot (the name field); the failure is the host's own Session-title write, which can refuse for a Session the registry no longer holds.
Reproduction (Node 22, from core/; fake DSH services as core/test/roster.test.js uses them, sessionTitle.rename is the real write call replaced):
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createRuntime, provideFlyBot } from './index.js'
import { fakeContext } from './test/fake-ctx.js'
const home = mkdtempSync(join(tmpdir(), 'audit-rename-'))
const ctx = fakeContext()
const workspaces = new Map(); let sessions = 0; let route
Object.assign(ctx, {
llm: { listProviders: () => [], listModels: async () => [], resolveModelInfo: async () => ({}) },
workspaceRegistry: { get: id => workspaces.get(id), resolveByPath: async () => undefined,
create: async path => { const w = { id: `w${workspaces.size + 1}`, path }; workspaces.set(w.id, w); return w },
archiveSession: async () => {} },
sessionController: { create: async () => ({ sessionId: `s${++sessions}` }),
resolveAgent: async sessionId => ({ agent: { session: { id: sessionId }, followup: () => {} } }),
selectModel: async () => {} },
sessionTitle: { rename: async () => {} },
sessions: { flush: async () => {} },
})
const register = ctx.connection.fetch.register
ctx.connection.fetch.register = (options) => { route = options; return register(options) }
const rt = createRuntime(ctx, { home })
provideFlyBot(ctx, rt.flybot)
const call = async (endpoint, payload) =>
(await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json()
const writer = (await call('create-bot', { name: 'Writer' })).value
await rt.savesSettled?.()
ctx.sessionTitle.rename = async () => { throw new Error('EACCES: session rename refused') }
const answer = await call('update-bot', { id: writer.id, name: 'Renamed' })
const live = (await call('state', {})).value.bots.find(bot => bot.id === writer.id)
const fresh = JSON.parse(readFileSync(join(home, 'state.json'), 'utf8')).bots[writer.id]
console.log('answer:', JSON.stringify(answer))
console.log('live state name:', live.name)
console.log('fresh load name:', fresh.name)
await ctx.dispose()
rmSync(home, { recursive: true, force: true, maxRetries: 5 })Real output at main (b64b2cda5ad2d57969f73e9f79a5e13e4b13f7ac):
answer: {"ok":false,"error":{"code":"bot/refused","message":"EACCES: session rename refused","details":{}}}
live state name: Renamed
fresh load name: Writer
Expected: a rename that cannot move its second half is refused with nothing changed, or the two halves are made consistent before answering.
Smallest fix: do the Session rename before the record changes — resolve the agent and await ctx.sessionTitle.rename(agent.session, next) first, then bot.name = next, then followMemberNames and rt.save(); alternatively wrap the body and restore bot.name (and the room titles) on failure.
Activity
HugoXK commentedon Nov 7, 2026
Claim
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 4bd6382c3dd04978920e3204ed34e583
Branch: dev/2622-host-a-refused-session-rename-leaves-the
Base: b64b2cd
At: 2026-11-07T17:01:53Z
Birfy commentedon Nov 7, 2026
Written-by: Chief
Triage decision (Chief, 2026-11-07T17:05Z on main
b64b2cda5ad2d57969f73e9f79a5e13e4b13f7ac).Confirmed against the source:
core/src/host/roster.js:199assignsbot.name = nextbeforecore/src/host/roster.js:201await ctx.sessionTitle.rename(agent.session, next), andrt.save()is at:238. A refused title write therefore leaves the live record renamed whilestate.jsonstill holds the old name — visible on the page, silently reverted by a restart. Real defect, this class, not padded.One detail the report did not need but the fix does: the room half cannot fail this way.
followMemberNames(roster.js:293) changesroom.nameand then callsretitleRoom(roster.js:283), which swallows its own failure withrt.warn. SosessionTitle.renameat:201is the only write in this branch that throws.Fix (smallest, reorder — no compensating catch):
updateBot's name branch, after the existing validation and before touching the record:resolveAgent+sessionTitle.renamelines abovebot.name = next.followMemberNames,retitleRoom, the room-name computation andrt.save()where they are. Do not add a try/catch that restoresbot.name: a reorder is the invariant, and restoring would still show the new name for the length of a request.ok:falsewith the host's error, exactly as today.Red-first test in
core/test/roster.test.js(must fail on the currentmainand pass with the change), on the existing fake context:ctx.sessionTitle.renamereplaced with one that throwsEACCES: session rename refused;update-bot { id, name: 'Renamed' }answersok: false;state) still lists the old name, and thestate.jsonread back from disk still lists the old name;renameand rename again:ok: true, live and disk both carry the new name, and the fake recorded the title write with the new name.Acceptance:
cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/roster.test.jsgreen, and the new cases red whencore/src/host/roster.jsis reverted to the base blob.npm run verifyat the tip;node scripts/ds-bot-patches.mjs check; changelog fragmentchangelog.d/2622.fixed.md. Architecture doc: no change (docs/subsystems/core-host.mddescribes update-bot's shape, not step order) — useDocs-not-needed:in both the branch commit and the PR body, or add one line there if the reviewer prefers.Keep a Bot's record in step with its Session title on rename (ThinkFl…
Release v0.2.26 (ThinkFlowLab#2627)