Repository navigation
Description
What happened?
Deleting a Bot or deleting a group chat never removes the scheduled tasks that DSH's schedule service holds for that Bot's or room's Session:
- The task rows stay
status: 'active'and keep delivering when due (CHANGELOG.md: "A task sends its message to the chat when it is due"), into a Session whose Bot or room no longer exists. The harness's session controller resumes the Session when the task delivers (packages/api/session-controller/src/agent.ts,resolveAgent()→resolve()→resume()), with no archived check, so the deleted Bot's model keeps running on schedule and burns tokens invisibly. - The Settings list hides them:
schedulesView()drops every task whose owner is no longer instate.bots/state.rooms(ds-bot/src/host/schedules.js:91, theknown()filter, applied at:98), so the task vanishes from Settings → Scheduled tasks instead of being offered for removal. schedule-deleterefuses them:ds-bot/src/host/schedules.js:121throws'No such task'whenownerOf(body.sessionId)isundefined, which it is once the Bot or room is gone. After the deletion there is no way left in DS Bot to see or remove the task.- The archive that
deleteBot/deleteRoomattempt does not stop the tasks either:ds-bot/src/host/roster.js:391and:332callarchiveSession(...)without{ stopActivity: true }and swallow errors with.catch(() => {}). DSH reports armed reminders as session activity (packages/schedule/schedule/src/index.ts, theworkspace/session-activitylistener inarchiveAdmission()), so the archive is refused withWorkspaceActiveSessionErrorbefore anything is written — and the refusal is silently dropped, so the Session is not even archived.
Expected: the module doc says this module "lists the team's tasks for Settings, creates and removes them there" (ds-bot/src/host/schedules.js:1-7), so a task's lifecycle should end with its Bot or room. DSH models exactly this cleanup: "an idle Session whose reminders are still armed refuses the archive, and an archiving stop deletes those rows instead of letting them deliver into a closed Session" (packages/schedule/schedule/src/index.ts, archiveAdmission()), and archiveSession(id, { stopActivity: true }) asks the workspace/session-stop providers, where the schedule service removes the Session's task rows. DS Bot already follows this invariant for conversation parts — moveSchedules() (ds-bot/src/host/schedules.js:129) moves a part's active tasks to the fresh Session before the old one ends (ds-bot/src/host/parts.js:732).
Root cause: deleteBot (ds-bot/src/host/roster.js:391) and deleteRoom (ds-bot/src/host/roster.js:332) archive the Session without stopActivity and never touch the schedule service, so DSH's workspace/session-stop hook that deletes the task rows is never triggered.
Steps to reproduce
In the UI: Settings → Scheduled tasks (or Scheduled task in the composer's + menu) → create a task for a Bot or a group chat → delete that Bot (Settings → Bots) or that group chat. The task is gone from the list but stays armed in DSH.
Scripted reproduction (the repo's test context test/fake-ctx.js plus an in-memory schedule service with the same shape as test/schedules.test.js; no API key, no model). Save as repro-schedules-orphan.mjs in ds-bot/:
// Repro: deleting a Bot (or a group chat) orphans its scheduled tasks — they stay
// active in DSH's schedule service (and keep firing on the session), but
// vanish from Settings and `schedule-delete` refuses to remove them.
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { apply } from './index.js'
import { fakeContext } from './test/fake-ctx.js'
const TEAM = {
version: 1,
mainBotIds: ['m1'],
revision: 1,
bots: {
m1: { id: 'm1', sessionId: 'm1', name: 'Chief', role: 'Chief of Staff', instructions: '', createdAt: 1 },
w1: { id: 'w1', sessionId: 'w2', name: 'Writer', role: '', instructions: '', createdAt: 2, segments: [{ sessionId: 'w1', endedAt: 3 }] },
},
rooms: { g1: { id: 'g1', name: 'Launch', named: true, members: ['m1', 'w1'], admin: 'm1', mode: 'admin', notice: '', createdAt: 4 } },
exchanges: [],
exchangeTurns: {},
questions: {},
}
// In-memory stand-in for DSH's schedule service (same shape as test/schedules.test.js).
function scheduleService() {
const tasks = new Map()
let next = 1
return {
tasks,
async create(sessionId, request) {
const id = `schedule-${next++}`
const kind = Object.keys(request).find(key => !['title', 'prompt'].includes(key))
const record = { id, kind, title: request.title, prompt: request.prompt, timing: request[kind], scheduledAt: '2030-01-01T01:00:00.000Z' }
tasks.set(id, { sessionId, record, status: 'active' })
return record
},
async list({ sessionId }) { return [...tasks.values()].filter(task => task.sessionId === sessionId).map(task => task.record) },
async catalog() { return [...tasks.values()].map(task => ({ ...task.record, sessionId: task.sessionId, status: task.status })) },
async delete({ sessionId, id }) {
if (tasks.get(id)?.sessionId !== sessionId) return { id, deleted: false, code: 'schedule_not_found' }
tasks.delete(id)
return { id, deleted: true }
},
}
}
const schedule = scheduleService()
const home = mkdtempSync(join(tmpdir(), 'repro-sched-'))
writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM))
const ctx = fakeContext({ services: { schedule } })
const sent = []
Object.assign(ctx, {
workspaceRegistry: { archiveSession: async () => {} },
sessionController: {
create: async () => ({ sessionId: 'fresh' }),
resolveAgent: async sessionId => ({ agent: { session: { id: sessionId }, followup: message => sent.push({ sessionId, message }) } }),
selectModel: async () => {},
},
sessionTitle: { rename: async () => {} },
sessions: { flush: async () => {} },
})
let route
const register = ctx.connection.fetch.register
ctx.connection.fetch.register = (options) => { route = options; return register(options) }
apply(ctx, { home })
const call = async (endpoint, payload) => (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json()
// 1. A daily task on Writer (w1) and one on the group chat (g1).
const made = await call('schedule-create', { target: 'w1', title: 'Briefing', prompt: 'Sum up the day', when: { kind: 'daily', time: '09:00', timeZone: 'UTC' } })
console.log('create w1 task ok:', made.ok)
const madeRoom = await call('schedule-create', { target: 'g1', title: 'Standup', prompt: 'Where are we?', when: { kind: 'daily', time: '10:00', timeZone: 'UTC' } })
console.log('create g1 task ok:', madeRoom.ok)
const botTask = [...schedule.tasks.values()].find(task => task.sessionId === 'w2')
const roomTask = [...schedule.tasks.values()].find(task => task.sessionId === 'g1')
console.log('service holds:', [...schedule.tasks.values()].map(t => [t.sessionId, t.record.title, t.status]))
// 2. Delete the Bot and the group chat through the API.
console.log('delete-bot ok:', (await call('delete-bot', { id: 'w1' })).ok)
console.log('delete-room ok:', (await call('delete-room', { id: 'g1' })).ok)
// 3. What does the user see, and can they clean up?
const listed = await call('schedules', {})
console.log('schedules view after delete:', JSON.stringify(listed.value))
const tryDeleteBotTask = await call('schedule-delete', { sessionId: 'w2', id: botTask.record.id })
console.log('delete the Bot task via API:', JSON.stringify(tryDeleteBotTask))
const tryDeleteRoomTask = await call('schedule-delete', { sessionId: 'g1', id: roomTask.record.id })
console.log('delete the room task via API:', JSON.stringify(tryDeleteRoomTask))
console.log('service still holds:', [...schedule.tasks.values()].map(t => [t.sessionId, t.record.title, t.status]))
ctx.dispose()
rmSync(home, { recursive: true, force: true })Run from ds-bot/ (the @deepseek-ai peers resolve through the repo's own test hook, against a deepseek-harness checkout at dsh-v0.2.1-alpha.1):
node --import ./test/harness-resolve.mjs repro-schedules-orphan.mjsObserved output (verbatim, at bdc5fda64e2ea1e83a4d146440072a2413025df4 on main):
create w1 task ok: true
create g1 task ok: true
service holds: [ [ 'w2', 'Briefing', 'active' ], [ 'g1', 'Standup', 'active' ] ]
delete-bot ok: true
delete-room ok: true
schedules view after delete: {"available":true,"tasks":[]}
delete the Bot task via API: {"ok":false,"error":{"code":"bot/invalid","message":"No such task","details":{}}}
delete the room task via API: {"ok":false,"error":{"code":"bot/invalid","message":"No such task","details":{}}}
service still holds: [ [ 'w2', 'Briefing', 'active' ], [ 'g1', 'Standup', 'active' ] ]
An independent verification run of the same steps, which additionally traced what archiveSession does (without stopActivity), observed both archives refused and no session archived:
service before delete: [ [ 'w2', 'Briefing', 'active' ], [ 'g1', 'Standup', 'active' ] ]
delete-bot ok: true / delete-room ok: true
archive attempts: [] archive refusals: ["w2","g1"]
schedules view after delete: {"available":true,"tasks":[]}
schedule-delete bot/room task: {"code":"bot/invalid","message":"No such task"}
service after delete: [ [ 'w2', 'Briefing', 'active' ], [ 'g1', 'Standup', 'active' ] ]
Diagnostics
- DS Bot
0.1.2,mainatbdc5fda64e2ea1e83a4d146440072a2413025df4 - DSH
dsh-v0.2.1-alpha.1(the harness version the plugin is tested with, per CONTRIBUTING), DSH Web - Node.js
v22.23.2, macOS 15.6 (arm64) - Exact error text from
schedule-delete:{"ok":false,"error":{"code":"bot/invalid","message":"No such task","details":{}}} - The reproduction is fully scripted (test context + in-memory schedule service): no API key, no real model, no tokens. Found during an assisted code review of
ds-bot/src/host/schedules.jsandroster.js.
How did you install DS Bot?
Git address
Privacy
- I checked that this report has no API keys, tokens or personal data.
Happy to be assigned. The fix direction I verified against the code: stop the deleted Session's scheduled tasks on delete — archiveSession(id, { stopActivity: true }) lets DSH's workspace/session-stop hook delete the task rows, or a small helper in schedules.js (service.list({ sessionId }) + service.delete(...) per record) called from deleteBot/deleteRoom. I did not find an existing issue or PR about scheduled tasks surviving Bot/group deletion (searched open and closed issues for "schedule", "task", "delete").
Activity
BlueX888 commentedon Oct 12, 2026
Reproduced this. Root cause: ds-bot/src/host/roster.js:332 and :391 archive the deleted room's and Bot's Session without stopActivity, so DSH's workspace/session-stop listener that deletes the session's task rows is never triggered.
Minimal reproduction:
Observed output:
Fix is ready on
BlueX888:fix/prep-schedule-tasks-orphaned-on-bot-or-room-delete(commita8b8478d). Pass { stopActivity: true } to both archiveSession calls, so DSH's session-stop listener drops the deleted Session's task rows.Verified with:
node --import ./test/harness-resolve.mjs --test test/schedules.test.jsHappy to open the PR once you confirm the approach or assign this to me.
Fei2411 commentedon Oct 12, 2026
已完成修复