deer-flow/docs/plans/workflows-phase4.md
Xuehao Xu cfe2bec371
feat(workflows): add multi-Gateway coordination and session-authorized failover (#7138)
* feat(workflows): add leased multi-Gateway coordination and fenced dispatch

* test(workflows): validate deployed failover and canonicalize config revisions

* fix(workflows): share namespace across registration and lease fencing

* fix(workflows): recover interrupted cancellation without admission caps

* fix(workflows): retain execution lease through child cleanup

* docs: trim guidance padding to fit merged instruction budget

* fix(workflows): release rejected admission slots and normalize gateway routing

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-23 23:31:43 +08:00

123 lines
8.7 KiB
Markdown

# Phase 4 implementation contract
- Opt-in PostgreSQL workflow store; default SQLite/exclusive flock remains unchanged.
- Shared workflow rows/artifacts/definitions, durable control inbox, database-clock execution leases and epoch fencing.
- Generic host ExecutionFence checked in same RunStore transaction as child admission. A preflight-only lease check is insufficient.
- Lease loss stops local worker; stale owner cannot save, dispatch, approve, capture artifacts or cancel a successor's children.
- Remote browser actions revalidate current permission/config, commit exact-version decisions, and owner consumes durable controls. Duplicates are idempotent.
- Lease expiry exposes recoverable/reconciliation state. Takeover requires fresh authenticated browser capability; no credentials serialized and no autonomous execution under a reconstructed user ID.
- Cluster quotas are transactionally reserved; uncertain child effects retain capacity until reconciled, never automatically redispatched.
- Runtime/config incompatibility blocks takeover; old compatible replicas may continue their own live runs during rolling upgrade.
- Tests: opt-in deployed two-Gateway HTTP/auth/PostgreSQL/Redis/nginx acceptance, real process faults, and complementary PostgreSQL transaction/fencing regressions.
## Validation boundaries
`backend/tests/test_workflow_gateway_acceptance.py` starts two real Gateways with
normal login/CSRF, shared PostgreSQL 17, Redis 7, and nginx round-robin routing.
The deterministic model replaces provider answers, not AgentRuns, admission,
LangGraph/checkpoints, storage or authorization. Test-only barriers pause real
operations before SIGKILL/SIGSTOP; no production fault hook or auth bypass exists.
| Scenario | Deployed assertion |
| --- | --- |
| A launch; B inspection/resume/approval/cancel | Same session, real workflow reads through both proxy upstreams without affinity |
| Duplicate launch and approval | Concurrent HTTP requests to A/B produce one workflow, one decision, one child per step |
| Admission receipt lost | Actual child committed, Gateway killed before workflow receipt; recovery retains uncertain slot and never redispatches |
| Completed child, result not yet persisted | Kill before result write; survivor reads the known completed child without starting another |
| Result persisted, response not delivered | Kill after result commit; replay reuses retained output |
| Approval persisted, downstream not dispatched | Kill after decision commit; survivor preserves approval and dispatches the next step once |
| SIGSTOP old owner and restore after takeover | Actual stale host admission rejected; old workflow/artifact writes cannot change successor state |
| File approval after owner death | Modify original after capture; B reviews/approves and downstream receives original immutable bytes |
| Shared quotas | Per-owner workflow limit and cluster child reservations constrain real HTTP launches/host admissions |
| Revocation/config drift | Revoked cookie is rejected; fresh login still cannot bypass changed configuration; rejected takeover does not advance epoch |
| Rolling runtime change | Separate source trees with different worker IPC limits execute; old owner drains, incompatible survivor rejects takeover |
| Active-child cancellation | Peer request reaches actual child owner; delayed observation remains an explicit reconciliation outcome |
The database-level suite additionally tests stale cancellation and admission while
waiting for a real PostgreSQL row lock. It complements the process faults; it is
not a claim that every transaction boundary has been exercised over HTTP.
The local deployment uses OrbStack containers and native Gateway processes with
shared local storage. This does not certify Kubernetes, arbitrary network partitions,
all published-version upgrade pairs, external business-effect rollback, or model
quality. The alternate runtime is a test source revision, not a historical release.
### Authorization and reconciliation delivery boundary
Lease expiry alone grants no user authority. Takeover is an explicit action using a
fresh valid browser-session capability; current permissions, configuration and exact
runtime compatibility are rechecked. No serialized credentials or reconstructed user
ID substitutes for that capability. Unattended automatic takeover needs a separate
durable delegation/authorization design and is not delivered here.
Unknown effects retain the workflow/step evidence and child reservation. Manual
inspection can establish the business outcome, but the product currently has no
supported API/UI that records an operator attestation, audits it, and atomically
releases that reservation. That closure remains open; do not equate direct SQL,
workflow deletion, or a new request ID with a supported safe retry. Cancellation is
not rollback. Host cancellation may be observed after the extension's bounded wait,
leaving `needs_reconciliation` even when the child later becomes interrupted.
### Defect found by deployed acceptance
Pydantic JSON dumps converted `tool_progress.exempt_tools` from a set into a
process-dependent list order. Identical configuration on A and B could therefore
produce different durable HMACs and reject peer controls. Revision encoding now
retains Python set identity and canonicalizes sets while preserving ordered lists.
A focused regression and Gateways with different `PYTHONHASHSEED` values exercise
this fix. Existing pre-fix fingerprints may fail compatibility checks; drain them
on their original compatible runtime rather than rewriting stored fingerprints.
## Earlier validation (before deployed acceptance)
- Full offline backend suite with PostgreSQL enabled: 34,527 passed, 686 skipped.
- Final extension/recovery/API subset: 144 passed, including 14 cluster cases.
- Host fencing, AgentRuns, run ownership and SQL repository subset: 213 passed.
- Blocking-I/O suite: 262 passed, 1 skipped.
- Backend and extension lint/format, frontend lint/typecheck, browser fixture and
guidance checks passed. PostgreSQL 17 ran on loopback with disposable schemas.
- The final targeted runs include the owner predicate on fenced cancellation and
file-integrity validation for remote approval added during the full-suite run.
## Deployed acceptance follow-up (2026-10-19)
- OrbStack PostgreSQL 17 / Redis 7 / nginx, native Python 3.12 Gateways:
**14 deployed acceptance cases passed** (157 seconds).
- Final strengthened proxy/readiness and active-successor stale-artifact checks:
**1 passed each**, after the full deployed run.
- Full backend offline target with PostgreSQL regressions enabled:
**34,573 passed, 656 skipped** across four shards. The deployed suite is explicitly
opt-in and skipped in that target; its result is reported separately above.
- Blocking-I/O target: **262 passed, 1 skipped**.
- PostgreSQL coordination/fencing and AgentRuns subset: **48 passed**.
- AgentRuns and guidance checks: **38 passed**. Changed Python files passed Ruff
lint/format checks; `git diff --check` passed.
- Acceptance teardown removed its Gateway processes and disposable containers;
pytest temporary directories retain diagnostic logs. No production/user stack
was reused or modified. The standalone PostgreSQL regression server was stopped.
## Review follow-up (2026-10-20)
Expired `cancelling` records now project as recoverable so authenticated cancellation
can reclaim the lease and finish cleanup. Ordinary replay rejects stored cancelling,
even without a cancel inbox entry; cancel claims themselves persist cancelling so a
second crash cannot lose the cancellation intent. Cancel takeover bypasses workflow
admission caps while retaining fencing and current permission/configuration checks.
Uncertain child reservations still require reconciliation before deletion.
Workflow get/list and database-identity reads use read-only transactions without
the global budget advisory lock. Listing joins workflow records and lease visibility
in one query/connection. Writes retain conservative global serialization; pooling
and finer write-lock granularity remain deferred for this bounded mode.
Regressions reproduce expired cancellation with and without saturated limits and
inspection while another connection holds the budget lock. Deployed acceptance adds
SIGKILL during cancellation takeover, then cleanup and deletion through a survivor.
Review validation: 17 PostgreSQL cluster cases passed (the three new regressions
failed before the fix); 15 real deployed acceptance cases passed. Full backend
regression: 34,576 passed, 657 skipped; blocking-I/O: 262 passed, 1 skipped.
Backend format/lint, frontend lint/typecheck, extension browser checks and diff
whitespace checks passed. The acceptance processes/containers and the dedicated
PostgreSQL regression server were cleaned up after validation.