mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(daemon): treat media generate handoff as success (#1715)
This commit is contained in:
parent
cd3acda6f6
commit
3d0e708720
3 changed files with 54 additions and 18 deletions
|
|
@ -399,7 +399,9 @@ async function runMediaGenerate(rawArgs) {
|
|||
process.exit(4);
|
||||
}
|
||||
console.error(`task ${taskId} queued (${accepted.status || 'queued'})`);
|
||||
await pollUntilDoneOrBudget(daemonUrl, taskId, 0);
|
||||
await pollUntilDoneOrBudget(daemonUrl, taskId, 0, {
|
||||
stillRunningExitCode: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function runMediaWait(rawArgs) {
|
||||
|
|
@ -428,9 +430,13 @@ async function runMediaWait(rawArgs) {
|
|||
await pollUntilDoneOrBudget(daemonUrl, taskId, since);
|
||||
}
|
||||
|
||||
async function pollUntilDoneOrBudget(daemonUrl, taskId, sinceStart) {
|
||||
async function pollUntilDoneOrBudget(daemonUrl, taskId, sinceStart, options = {}) {
|
||||
const totalBudgetMs = 25_000;
|
||||
const perCallTimeoutMs = 4_000;
|
||||
const stillRunningExitCode =
|
||||
typeof options.stillRunningExitCode === 'number'
|
||||
? options.stillRunningExitCode
|
||||
: 2;
|
||||
const startedAt = Date.now();
|
||||
const url = `${daemonUrl.replace(/\/$/, '')}/api/media/tasks/${encodeURIComponent(taskId)}/wait`;
|
||||
|
||||
|
|
@ -520,12 +526,16 @@ async function pollUntilDoneOrBudget(daemonUrl, taskId, sinceStart) {
|
|||
elapsed: Math.round((Date.now() - startedAt) / 1000),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(handoff) + '\n');
|
||||
const stillRunningHint =
|
||||
stillRunningExitCode === 0
|
||||
? 'This is a successful queued/running handoff, not a failure.'
|
||||
: `exit code ${stillRunningExitCode} = still running.`;
|
||||
process.stderr.write(
|
||||
`task ${taskId} still running after ${handoff.elapsed}s. ` +
|
||||
`Run \`"$OD_NODE_BIN" "$OD_BIN" media wait ${taskId} --since ${since}\` to continue in an agent runtime ` +
|
||||
`(exit code 2 = still running).\n`,
|
||||
`(${stillRunningHint}).\n`,
|
||||
);
|
||||
process.exit(2);
|
||||
process.exit(stillRunningExitCode);
|
||||
}
|
||||
|
||||
function surfaceFetchError(err, daemonUrl) {
|
||||
|
|
|
|||
|
|
@ -184,11 +184,13 @@ the same turn.
|
|||
### Long-running renders (Volcengine i2v, hyperframes-html): generate → wait loop
|
||||
|
||||
\`media generate\` no longer blocks for the full render. It dispatches
|
||||
the task daemon-side and returns within ~1s with a \`{taskId}\`. You then
|
||||
the task daemon-side and either returns the finished \`{"file":{...}}\`
|
||||
or returns a successful queued/running handoff with \`{taskId}\`. You then
|
||||
drive the render to completion by calling \`media wait <taskId>\` through \`OD_NODE_BIN\` + \`OD_BIN\` in
|
||||
a loop — each call long-polls the daemon for up to 25s, well below your
|
||||
shell tool's default 30s timeout. The wait subcommand exits with a
|
||||
distinct code per outcome:
|
||||
shell tool's default 30s timeout. \`media generate\` treats the handoff as
|
||||
exit \`0\` so the first dispatch does not look like a failed shell call.
|
||||
The wait subcommand exits with a distinct code per outcome:
|
||||
|
||||
- \`exit 0\` — terminal **done**. Final stdout line is \`{"file":{...}}\`.
|
||||
- \`exit 5\` — terminal **failed**. Stderr carries the upstream error.
|
||||
|
|
@ -203,18 +205,22 @@ The pattern in your shell tool:
|
|||
\`\`\`bash
|
||||
out=$("$OD_NODE_BIN" "$OD_BIN" media generate --surface video --model … --image …)
|
||||
ec=$?
|
||||
if [ "$ec" -ne 0 ] && [ "$ec" -ne 2 ]; then
|
||||
if [ "$ec" -ne 0 ]; then
|
||||
echo "$out" >&2; exit "$ec"
|
||||
fi
|
||||
task_id=$(printf '%s\\n' "$out" | tail -1 | jq -r '.taskId // empty')
|
||||
since=$(printf '%s\\n' "$out" | tail -1 | jq -r '.nextSince // 0')
|
||||
while [ "$ec" -eq 2 ] && [ -n "$task_id" ]; do
|
||||
while [ -n "$task_id" ]; do
|
||||
out=$("$OD_NODE_BIN" "$OD_BIN" media wait "$task_id" --since "$since")
|
||||
ec=$?
|
||||
since=$(printf '%s\\n' "$out" | tail -1 | jq -r '.nextSince // '"$since")
|
||||
if [ "$ec" -eq 0 ]; then
|
||||
task_id=""
|
||||
elif [ "$ec" -ne 2 ]; then
|
||||
echo "$out" >&2; exit "$ec"
|
||||
fi
|
||||
done
|
||||
# At this point ec is 0 (done) or 5 (failed). Final result on the last
|
||||
# stdout line of \`out\`.
|
||||
# At this point ec is 0 (done). Final result on the last stdout line of \`out\`.
|
||||
\`\`\`
|
||||
|
||||
Each \`generate\` and \`wait\` call lasts at most ~25s, so the agent
|
||||
|
|
@ -325,13 +331,16 @@ do **not** narrate a stub as if it were the final result.
|
|||
models without a real renderer, and the CLI prints the daemon's
|
||||
error message. Set \`OD_MEDIA_ALLOW_STUBS=1\` to write a labelled
|
||||
placeholder instead.
|
||||
2. **Exit code.** \`"$OD_NODE_BIN" "$OD_BIN" media generate\` and \`"$OD_NODE_BIN" "$OD_BIN" media wait\` exit:
|
||||
\`0\` on real success, \`2\` when the task is **still running** and
|
||||
needs another \`wait\` call (see "Long-running renders" above), \`5\`
|
||||
when the daemon accepted the request but the provider call failed
|
||||
(key missing / 4xx / network blip), and \`1–4\` for client / daemon
|
||||
errors. Always check \`$?\` before describing the output. \`2\` is
|
||||
not a failure — it just means "keep polling".
|
||||
2. **Exit code.** \`"$OD_NODE_BIN" "$OD_BIN" media generate\` exits \`0\` for
|
||||
both immediate completion and successful queued/running handoff; inspect
|
||||
the final stdout JSON for either \`file\` or \`taskId\`. \`"$OD_NODE_BIN"
|
||||
"$OD_BIN" media wait\` exits \`0\` on terminal **done**, \`2\` when the
|
||||
task is still **running** and needs another \`wait\` call (see
|
||||
"Long-running renders" above), \`5\` when the daemon accepted the request
|
||||
but the provider call failed (key missing / 4xx / network blip), and
|
||||
\`1–4\` for client / daemon errors. Always check \`$?\` before describing
|
||||
the output. \`2\` from \`media wait\` is not a failure — it just means
|
||||
"keep polling".
|
||||
3. **stderr WARN lines.** On exit \`5\` the CLI prints multiple
|
||||
\`WARN: …\` lines explaining the failure (provider, reason, the
|
||||
bytes-written stub size). Quote the reason in your reply.
|
||||
|
|
|
|||
|
|
@ -328,6 +328,23 @@ describe('composeSystemPrompt — metadata.promptTemplate', () => {
|
|||
expect(out).not.toContain('fishaudio, …) are still stubs');
|
||||
});
|
||||
|
||||
it('documents media generate handoffs as successful queued results', () => {
|
||||
const out = composeSystemPrompt({
|
||||
metadata: {
|
||||
kind: 'video',
|
||||
videoModel: 'seedance-2.0',
|
||||
videoAspect: '16:9',
|
||||
videoLength: 5,
|
||||
},
|
||||
});
|
||||
|
||||
expect(out).toContain('`media generate` treats the handoff as');
|
||||
expect(out).toContain('exit `0` so the first dispatch does not look like a failed shell call');
|
||||
expect(out).toContain('`"$OD_NODE_BIN" "$OD_BIN" media generate` exits `0`');
|
||||
expect(out).toContain('either `file` or `taskId`');
|
||||
expect(out).toContain('`2` from `media wait` is not a failure');
|
||||
});
|
||||
|
||||
it('surfaces ElevenLabs voice options for project discovery when no voice was preselected', () => {
|
||||
const voiceOptions = Array.from({ length: 50 }, (_, index) => {
|
||||
const ordinal = index + 1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue