* fix(workflows): stop idle agent waits from consuming worker turns
While a script step waited on a child Agent run, the supervisor sent an
empty response frame to the QuickJS worker every 20 ms and each round
trip counted toward the 10,000-turn cap, so any workflow whose Agent
calls kept it waiting for about 209 s failed with "Worker turn limit"
regardless of `timeout_seconds`.
The worker now reports when its job queue has drained, and the
supervisor waits on the pending calls locally while the worker is idle,
still re-checking cancellation and faults every 20 ms, without
exchanging frames. `timeout_seconds` is the only wall-clock bound again;
scripts that spin the job queue still hit the turn cap. The cap is a
module constant so tests can lower it.
Fixes bytedance/deer-flow#7022
* fix(workflows): default a frame without idle to the polling path
Review follow-up: read `idle` with `frame.get("idle", False)` so an
outcome-less frame that lacks the key degrades to the previous poll
behaviour instead of failing the run with a KeyError, and give the
cancel-latency regression test a 3 s budget so store writes and process
reaping on a loaded CI runner cannot flake it.
* fix(workflows): detect worker exit during idle Agent waits
Check worker exit in the existing local poll and clean up pending child runs. Adapt the work-frame budget regression from PR #7025 while retaining precise idle detection.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2.9 KiB
Script workflow extension
Keep workflow business logic in this packaged extension. Import only public
deerflow_extension_api contracts, standard library and declared QuickJS dependency;
never import private app.* or deerflow.* runtime modules.
Generated script runs only in worker.py's isolated, capability-free JS context. Never pass host callables, capability handles, credentials or ambient environment into it. Keep every IPC and stored/public payload bounded. Parent lifecycle must kill and reap the worker and explicitly observe child cancellation. The worker turn cap counts worker frames only; while the worker is idle (its job queue drained) and Agent calls are pending, wait locally with bounded cancellation, fault and worker-exit polls instead of round-tripping empty frames, so wall time stays the only clock bound.
Persist stable child thread, input message, attempt and idempotency identity before admission. Workflow cancel is a supervisor signal; do not cancel an admission task and lose an already-created child. Timeout/unknown outcomes require reconciliation, never automatic retries. Distinguish admission uncertainty from post-admission observation timeout: interrupt/observe known runs and escalate only uncertain cleanup. Keep the workflow deadline handler outside asyncio.timeout and test external cancellation separately. Hash the Agent allowlist in canonical sorted order. Owner scope applies to every public read and mutation. Own persistence through supervisor publication even if the launching caller cancels. Serialize launch admission and shutdown. After artifact IO, validate call limits and step uniqueness together with await-free reservation. Hold the process lock across service life; do not recover live records while another Gateway owns the file.
Anchor results on the visible human input, including canonical __user rewrites; never use hidden system/memory IDs. Retain UI launch identity across refresh failures. Thread latest state is not immutable run output. Check message/run attribution, interrupts and pending human input before copying output. Agent success is never human approval. Live checkpoints retain the worker without replay; restart interrupts all waits. Bind human decisions to source output hash, step/attempt and authenticated session ActionContext, never ToolContext. Persist before waking; fence duplicate resume by checkpoint ID. Config revision is a declarative preflight guard. Store actual host assembly evidence per child run; never expose raw credentials. File checkpoints bind append-only captured bytes and SHA-256; preview and approved consumption read that exact version. Never claim hard currency limits or freeze external state. Do not serialize capability handles.
Tests live in backend/tests/test_workflow_plugin.py. Run the backend offline and blocking-I/O targets plus verify_browser.mjs. The browser fixture is loopback-only and synthetic; it must never become a deployed authentication bypass.