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
8 changes: 7 additions & 1 deletion plugins/workflow/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export function createWorkflowService({ dir, persist = true, now = () => Date.no
const node = nodeById(item, nodeId)
const botId = run.ephemeral[nodeId]
if (!botId || !node) continue
await dispatch({
const answer = await dispatch({
botId,
nodeId,
name: node.name,
Expand All @@ -214,6 +214,12 @@ export function createWorkflowService({ dir, persist = true, now = () => Date.no
brief: run.task?.title ?? '',
taskId: run.task?.id ?? '',
})
// A refused step is not handed over: leave it unmarked so the next settle
// tries again, and say why in one line — otherwise the run stalls silently.
if (answer?.ok === false) {
warn(`flybot-workflow: step ${nodeId} was not handed over: ${answer.code ?? 'dispatch refused'}`)
continue
}
run.handed = { ...run.handed, [nodeId]: true }
wrote = true
}
Expand Down
36 changes: 36 additions & 0 deletions plugins/workflow/test/ephemeral.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,39 @@ test('apply wires spawn to a hidden ephemeral create and removes those Bots when
assert.equal(done.workflows.find(item => item.id === made.id).run.status, 'done')
assert.deepEqual(removed, ['bot-1'])
})

test('#1389: a step the host refuses is not marked as handed, and a later settle sends it again', async () => {
let answer = { ok: false, code: 'no-sender' }
const calls = []
const warnings = []
const { svc } = harness({
service: {
dispatch: async (step) => { calls.push(step); return answer },
warn: (...args) => warnings.push(args.join(' ')),
},
})
const made = svc.define({
name: 'Pair',
nodes: [
{ id: 'plan', name: 'Planner', role: 'plan', prompt: 'Write the plan.' },
{ id: 'code', name: 'Coder', role: 'code', prompt: 'Implement it.' },
],
edges: [],
}).workflows.find(item => item.name === 'Pair')
const started = await svc.runNamed({ workflow: 'Pair', brief: 'Ship the note' })
assert.equal(started.ok, true)
const run = started.workflows.find(item => item.id === made.id).run
assert.equal(run.status, 'running')
assert.deepEqual(calls.map(step => step.nodeId).sort(), ['code', 'plan'])

// A refused step is not welded shut: the next settle sends it again, to the same Bot.
answer = { ok: true }
const moved = await svc.advance({ workflowId: made.id, nodeId: 'plan', outcome: 'done' })
assert.equal(moved.ok, true)
const after = moved.workflows.find(item => item.id === made.id).run
assert.equal(after.nodeStatus.code, 'running')
const codeCalls = calls.filter(step => step.nodeId === 'code')
assert.equal(codeCalls.length, 2)
assert.equal(codeCalls[0].botId, codeCalls[1].botId)
assert.match(warnings.join('\n'), /no-sender/)
})
Loading