Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1120.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Starting FlyBot on DSH no longer stops before the Main Bot is ready: the task drive registers its handle with Cordis `provide` instead of a property write the host rejects ([#1120](https://github.com/ThinkFlowLab/FlyBot/issues/1120)).
17 changes: 16 additions & 1 deletion core/src/host/task-drive.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,25 @@ export function install(rt) {
if (event.data?.source?.kind === 'user') clearMiss(session?.id)
})

ctx.taskDrive = {
// A live Cordis context refuses `ctx.taskDrive = …` once the plugin fiber is
// active (`cannot set property "taskDrive" without provide`). Same order as
// provideFlyBot: provide, then reflect.provide, then set, then a plain write.
const api = {
get now() { return clock.now },
set now(fn) { clock.now = fn },
settled: () => tail,
consider: () => consider(),
}
const provide = typeof ctx.provide === 'function' ? ctx.provide.bind(ctx) : typeof ctx.reflect?.provide === 'function' ? ctx.reflect.provide.bind(ctx.reflect) : undefined
if (provide) {
provide('taskDrive', api)
return
}
if (typeof ctx.set === 'function') {
try {
ctx.set('taskDrive', api)
return
} catch { /* the key is refused: fall through to the plain property */ }
}
ctx.taskDrive = api
}
46 changes: 46 additions & 0 deletions core/test/task-drive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,52 @@ test('a board card that is not a conversation task does not wake', async (t) =>
assert.equal(app.calls('s').length, 0)
})

test('a Cordis context provides taskDrive instead of assigning it', async (t) => {
// Live dsh applies this plugin on a Cordis 4 context whose fiber is already
// active, and that proxy throws `cannot set property "taskDrive" without
// provide` on a bare write (#1120). The fake context used above allows the
// write, so this case is the host that actually starts.
const { Context } = await import('@deepseek-ai/cordis')
const home = mkdtempSync(join(tmpdir(), 'ds-bot-drive-cordis-'))
writeFileSync(join(home, 'state.json'), JSON.stringify({ version: 2, revision: 1, mainBotIds: [], members: {}, bots: {}, rooms: {}, grants: {}, agents: {}, exchanges: [], exchangeTurns: {}, questions: {}, prefs: {} }))
t.after(() => rmSync(home, { recursive: true, force: true, maxRetries: 5 }))
const fake = fakeContext()
Object.assign(fake, {
llm: { listProviders: () => [], listModels: async () => [], resolveModelInfo: async () => ({}), stream: async function* () {} },
workspaceRegistry: { get: () => undefined, resolveByPath: async () => undefined, create: async path => ({ id: 'w1', path }), archiveSession: async () => {} },
sessionController: {
create: async () => ({ sessionId: 'made-1' }),
resolveAgent: async id => ({ agent: { id, session: { id } } }),
selectModel: async () => {},
page: async () => ({ records: [], hasMore: false }),
},
sessionTitle: { rename: async () => {} },
sessions: { flush: async () => {} },
})
const root = new Context()
for (const name of ['tools', 'systemPrompt', 'connection', 'llm', 'workspaceRegistry', 'sessionController', 'sessions', 'sessionTitle']) {
root.provide(name, fake[name])
}
let pluginCtx
const fiber = root.plugin({
name: 'ds-bot',
apply(ctx) {
pluginCtx = ctx
createRuntime(ctx, { home })
},
})
t.after(() => fiber.dispose())
await fiber
const drive = pluginCtx.taskDrive
assert.equal(typeof drive.settled, 'function')
assert.equal(typeof drive.consider, 'function')
let now = 42
drive.now = () => now
assert.equal(drive.now(), 42)
await drive.settled()
await drive.consider()
})

test('asking during the wake does not schedule another wake', async (t) => {
const app = await start(t)
const body = await app.track('s', { tasks: [{ title: 'Draft the note', column: 'in-progress', progress: 10 }] })
Expand Down
2 changes: 2 additions & 0 deletions docs/subsystems/core-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ A group job is one overall task plus a subtask per Bot (#1099). The overall task

Unfinished conversation tasks wake the owning Bot. `src/host/task-drive.js` runs after a board write and after a turn ends. For each owner, the next task is the one `compareForWork` would name: `open`, `claimed`, or `working`, not `waitingFor: 'input'`, and not an overall group card (`kind: 'group'`). That card is the plan; the subtasks are the work. One drive pass wakes every such owner, so a group assignment starts each member's own subtask in that same pass. One Bot still receives only its own next task. A task with no group wakes the Bot's own chat. A subtask wakes that Bot's Session in the group, opening the Session the first time (`memberSession`) so the driver does not wait for someone to speak there. The wake is a follow-up whose header is `[Continue task]` (`delivery.js`, role `task`). It names the task id, priority, and title. It is not a group post and not a room turn, so the reply is not posted back and does not start a round.

Tests reach the driver through `ctx.taskDrive` (`now`, `settled`, `consider`). That handle is a Cordis service registered with `provide`, the same path as `ctx.flybot`. A live context refuses a bare property write, which stopped startup before any Bot loaded (#1120).

The driver skips a Session that already has a turn, and a second wake while the first is still queued or in progress. A queued wake that has not started is replaced when a different task becomes that Bot's next one. A live group round defers member wakes until the round's turn ends; the parallel wake is the assignment pass, not an extra turn beside a round. A wake that ends with the same board (status, progress, priority, note, title, and who is waiting — not `updatedAt`, which a no-op `track_tasks` call still bumps) waits 30s, then 2min, 10min, and 30min before trying that Session again. A change on the board, or a person speaking in that Session, clears the wait. A task that is waiting on an answer does not wake: the turn that called `ask` asks once, and the driver does not loop. The idle rule still forbids inventing work; unfinished work the user or a group already handed the Bot counts as that task. Tests: `core/test/task-drive.test.js`.

### Work view step (#546)
Expand Down
17 changes: 16 additions & 1 deletion patches/ds-bot/files/src/host/task-drive.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,25 @@ export function install(rt) {
if (event.data?.source?.kind === 'user') clearMiss(session?.id)
})

ctx.taskDrive = {
// A live Cordis context refuses `ctx.taskDrive = …` once the plugin fiber is
// active (`cannot set property "taskDrive" without provide`). Same order as
// provideFlyBot: provide, then reflect.provide, then set, then a plain write.
const api = {
get now() { return clock.now },
set now(fn) { clock.now = fn },
settled: () => tail,
consider: () => consider(),
}
const provide = typeof ctx.provide === 'function' ? ctx.provide.bind(ctx) : typeof ctx.reflect?.provide === 'function' ? ctx.reflect.provide.bind(ctx.reflect) : undefined
if (provide) {
provide('taskDrive', api)
return
}
if (typeof ctx.set === 'function') {
try {
ctx.set('taskDrive', api)
return
} catch { /* the key is refused: fall through to the plain property */ }
}
ctx.taskDrive = api
}
46 changes: 46 additions & 0 deletions patches/ds-bot/files/test/task-drive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,52 @@ test('a board card that is not a conversation task does not wake', async (t) =>
assert.equal(app.calls('s').length, 0)
})

test('a Cordis context provides taskDrive instead of assigning it', async (t) => {
// Live dsh applies this plugin on a Cordis 4 context whose fiber is already
// active, and that proxy throws `cannot set property "taskDrive" without
// provide` on a bare write (#1120). The fake context used above allows the
// write, so this case is the host that actually starts.
const { Context } = await import('@deepseek-ai/cordis')
const home = mkdtempSync(join(tmpdir(), 'ds-bot-drive-cordis-'))
writeFileSync(join(home, 'state.json'), JSON.stringify({ version: 2, revision: 1, mainBotIds: [], members: {}, bots: {}, rooms: {}, grants: {}, agents: {}, exchanges: [], exchangeTurns: {}, questions: {}, prefs: {} }))
t.after(() => rmSync(home, { recursive: true, force: true, maxRetries: 5 }))
const fake = fakeContext()
Object.assign(fake, {
llm: { listProviders: () => [], listModels: async () => [], resolveModelInfo: async () => ({}), stream: async function* () {} },
workspaceRegistry: { get: () => undefined, resolveByPath: async () => undefined, create: async path => ({ id: 'w1', path }), archiveSession: async () => {} },
sessionController: {
create: async () => ({ sessionId: 'made-1' }),
resolveAgent: async id => ({ agent: { id, session: { id } } }),
selectModel: async () => {},
page: async () => ({ records: [], hasMore: false }),
},
sessionTitle: { rename: async () => {} },
sessions: { flush: async () => {} },
})
const root = new Context()
for (const name of ['tools', 'systemPrompt', 'connection', 'llm', 'workspaceRegistry', 'sessionController', 'sessions', 'sessionTitle']) {
root.provide(name, fake[name])
}
let pluginCtx
const fiber = root.plugin({
name: 'ds-bot',
apply(ctx) {
pluginCtx = ctx
createRuntime(ctx, { home })
},
})
t.after(() => fiber.dispose())
await fiber
const drive = pluginCtx.taskDrive
assert.equal(typeof drive.settled, 'function')
assert.equal(typeof drive.consider, 'function')
let now = 42
drive.now = () => now
assert.equal(drive.now(), 42)
await drive.settled()
await drive.consider()
})

test('asking during the wake does not schedule another wake', async (t) => {
const app = await start(t)
const body = await app.track('s', { tasks: [{ title: 'Draft the note', column: 'in-progress', progress: 10 }] })
Expand Down
Loading