Dispatch: a poke when the owner has work waiting
An agent session that went idle doesn't hear that the owner moved work to
ready. The dispatcher closes that gap (board #161): a Claude session the
owner runs on /loop asks the board, each tick, which sessions to poke,
sends each one short message, and records it.
It is off by default and switched on per project, on the board, by a
person signed in: project settings › Dispatch names the session to poke,
how often (dispatchEvery, minutes, default 60) and how many times a day
(dispatchCap, default 4). Each poke starts a paid turn, so those are cost
controls. A token can turn dispatch off, never on.
A poke is due when all of these hold:
- a ready task is waiting that a person moved to ready (an agent's own move doesn't count, so a poke never approves on the owner's behalf), that isn't a question, and that nobody is on;
- it was moved there since the last poke (no nagging about the same work);
- the last poke is older than the interval, and today's are under the cap.
planDispatch(store) says, per project with dispatch on, whether a poke is
due and why not when it isn't; it reads only. recordPoke(store, project)
writes the poke as a comment on the task (dispatch: poked …), so every
poke is on the board: who was poked, about what, when.
import { MemoryLog, VirtaStore, planDispatch, recordPoke } from 'tosijs-virta'
const store = new VirtaStore({ log: new MemoryLog(), identity: 'owner' })
await store.load()
await store.setProject('demo', { enrolled: 'today', dispatch: 'demo-session' })
await store.create({ title: 'port the tests', tags: ['project:demo', 'status:ready'] })
const [before] = planDispatch(store)
await recordPoke(store, 'demo')
const [after] = planDispatch(store)
preview.textContent = [before.due, before.text, after.due, after.why].join('\n')
test('a poke is due once, then not until something new is ready', () => {
const [due, text, again, why] = preview.textContent.split('\n')
expect(due).toBe('true')
expect(text).toContain('#1 "port the tests"')
expect(again).toBe('false')
expect(why).toBe('nothing new moved to ready since the last poke')
})