Skip to content

feat(*): let a dag node be handed to another sub-agent mid-run - #1118

Open
LivXue wants to merge 6 commits into
mainfrom
feat/dag_node_reassignment
Open

LivXue wants to merge 6 commits into
mainfrom
feat/dag_node_reassignment

Conversation

@LivXue

@LivXue LivXue commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

A suspended DAG node can now be handed to another sub-agent and the run keeps
going. Before this, a node whose agent lacked a capability could only be
continued on the same agent, abandoned, or fixed by winding the whole run down
and submitting another graph.

How it works:

  • run_subagent_dag nodes gain two fields. A decision node (routes, plus
    select: one|many) still uses the existing per-node judge to pick a route; a
    guard (when) selects which nodes run at all. Those shipped in F1.
  • This PR adds the fourth adjudication decision, reassign: resolve_dag_node
    accepts decision: "reassign" with an agent (roster enum, never a worker
    label) and a message. The node keeps its id, task, dependencies and place in
    the graph; the next attempt runs on the new agent; the continuation budget is
    shared, not reset.
  • Admission is explicit and fails closed: roster membership, not the current
    agent, local-file capability for every node that moves, a stateful target for
    an originally shared instance, an unoccupied destination handle, and a
    handover that can actually be rendered (a task over the 128000-character
    budget is refused before the move is committed). A confirm: true run asks
    again before moving; with no ask channel it refuses rather than inheriting the
    permissive default.
  • The new agent's first prompt carries a handover block: the instance's visible
    conversation, or the node's own archived attempts when there is no instance.
    It excludes tool calls, tool output and private model context; the main
    agent's message travels as the ordinary follow-up, so the judge still reads
    the original task plus the recorded follow-ups. Each carried answer is
    sub-agent output and crosses the same nonce-tagged untrusted fence every other
    sub-agent output does; a handover carried inside an earlier user row is fenced
    the same way, while the task, the person's and the main agent's words stay
    unfenced. The escaping is a framing guard on top of that fence, not a
    sanitizer.
  • The scheduler moves pending and suspended nodes, leaves completed ones where
    they ran, plants the handover once per destination handle, keeps the global
    concurrency slot free while it waits for the source attempt to settle, and
    gates the file-reference rule again for every node the installer builds. A
    sibling's answer decided in the same batch as a reassign is applied at once
    rather than stranded behind the moved node's next attempt.
  • Records follow the agent that actually ran: one resolver answers "who runs
    this node now" for dag.get, tasks.list, subagent.list and the instance
    history; the registry row moves atomically at attempt start; both the TUI and
    the web task pane show the attempt history. A moved node's instance history is
    read from its per-attempt archives, so one agent's answer is never shown as
    another's own turn.
  • A successful reassigned run captures to a Playbook on the agents that finished
    it while the audit keeps the factual history; the submitted graph is never
    modified.
  • Node judgement stays always on. The exception report names the attempt that
    ended, lists earlier attempts with bounded reasons, and teaches the four
    recovery choices against the remaining budget. On a Worker Table run a
    reassign names a roster agent rather than a label, so the node leaves the
    table and keeps the Charter of the role it was submitted under; CONTEXT.md and
    the DAG skill say so.

This branch is now rebased onto 959cd0a9f (main). The one conflict, in
_dag_row in raven/rpc/methods/tasks.py, was against #1131, which removed
the per-node tool-failure tally. The resolution keeps main's reading
(call_count = _tool_call_count(account)), produces and declares no
tool_failure_count field, and keeps this branch's shared current_assignment
resolver for the node's agent, handle and attempt history. That resolver falls
back to entry["subagent"] or graph["subagent"] exactly as main's inline chain
did, so a node that never moved reads the same as before. The automatic merge
left two expectations this branch adds in ui-web/src/features/tasks/live.test.ts
still listing the removed field; they were dropped. The contract, both
generated clients and both type checks were re-run at the new head.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

Run on the rebased branch head 0d0f1b74b (origin/main = 959cd0a9f):

  • pytest -q -n 0 over the 16 test files this PR changes -> 2226 passed,
    exit 0. That set includes both tests that pin the two sides of the rebase:
    test_a_reassigned_node_reads_back_its_current_agent_and_attempt_history
    (the current_assignment side) and
    test_tool_call_count_reads_only_the_listed_calls (the _tool_call_count
    side); both pass.

  • pytest -q -n 0 over a wider DAG subset (15 files: adjudication, control
    tools, core, guard, handover, live, MCP scope, runner, skills, verdict,
    instance log, instance registry reload, unified playbook, playbook e2e and
    playbook delegate) -> 1553 passed, exit 0.

  • vitest run in ui-web on live.test.ts, TasksPage.test.tsx,
    nodes.test.ts, graph.test.ts and scripts/gates/fixture-shape.test.mjs
    -> 265 passed, exit 0.

  • vitest run in ui-tui on dagCommand, dagPanel, dagRun, dagStatus and
    liveAgentsStore -> 343 passed, exit 0.

  • tsc --noEmit in ui-web and in ui-tui -> exit 0.

  • npm run gen:check (ui-web) -> generated.ts matches the contract (248 methods), exit 0. npm run lint:rpc (ui-tui) -> generated.ts in sync (sha256: 5bdee73ccfb5), exit 0. gen-i18n.mjs --check -> catalogue up to
    date, exit 0.

  • ruff check and ruff format --check on the 34 changed Python files ->
    exit 0. ty check raven -> exit 0. lint-imports -> 10 contracts kept,
    0 broken.

  • check_commit_messages.py, check_source_language.py and
    check_large_files.py over origin/main..HEAD, plus commitlint ->
    exit 0.

  • Mutation checks at the authored head d62e293ab (the rebase changes none of
    the sources they target): nine source mutations (no answer fence, no nested
    fence, all-attempts completeness, sibling answers left on the desk, no
    oversized-task door, anchor-only group retarget, replace-instead-of-union,
    dropped partial MCP notice, flat files for a moved node) each turned its
    test red; sources restored.

  • The coverage gate and the full suite were not re-run on this rebase round:
    both need the whole suite, and this rebase adds no source line of its own
    beyond resolving _dag_row. CI runs both on the pushed head.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Security: the handover carries only what the source agent said, never tool calls
or private context, and every carried answer is fenced as untrusted sub-agent
output. Admission checks run before the confirmation question and are re-read in
one synchronous segment at commit time, so a change during the ask refuses the
move instead of applying it.

Compatibility: a run with no reassign is unchanged. A base-vs-head A/B run
produced byte-identical summary, files, terminal outputs, manifest, events,
graph, prompts and artifacts after normalizing paths and the per-run untrusted
tag. The only deliberate no-reassign text change is the exception report's new
attempt lines; a handover can only appear on a moved node. New wire fields are
optional; TaskCounts is untouched.

The rebase is backward compatible in the same way: tool_call_count keeps the
meaning and the null rule main gave it in #1131, and the added attempt_agents
and instance_auto fields remain optional and present only for a node that
moved.

Known limits, recorded rather than fixed: a multi-hop middle agent whose
attempt ran on a minted handle cannot be found by the instance history (the
archive is addressed by node and attempt, and nothing records which handle that
attempt owned); a node whose handle was dropped and then moved again captures
the handle that actually finished; handover escaping does not cover vertical
tab, form feed or the Unicode line separators.

Rollback: revert the squashed commit. No migration, no config change, no
stored-format change that older builds cannot read.

Related Issues

N/A

@LivXue
LivXue requested review from 0xKT and gloryfromca October 27, 2026 02:14

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the TUI must recover a reassigned node's current agent and handle from completion data.

I found one concrete correctness issue in the TUI live-agent store. I otherwise covered the repository rules and canonical terms, the complete diff, relevant runtime/RPC/UI callers, both commits' history, backward-compatible absence semantics for unreassigned runs, test-strength changes, and the reassignment architecture across admission, staging, handover, persistence, reload, and client event ordering.

Verification: uv run pytest tests/test_subagent_dag_handover.py tests/test_subagent_dag_runner.py tests/test_subagent_dag_control_tools.py tests/test_subagent_instance_registry_reload.py tests/test_rpc_schema_match.py tests/test_unified_playbook.py -x -q (1226 passed); the remaining changed Python test modules (977 passed); changed TUI tests (339 passed) plus TUI type-check; changed web tests (265 passed) plus web type-check. The first Python attempt failed during fixture setup because the fresh environment lacked the dev extra; after uv sync --extra dev, both Python runs passed.

Comment thread ui-tui/src/app/liveAgentsStore.ts Resolved
@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility review — PR #1118 @ 2bad12f004bef16952614ac3325c9ac2f576b4d3

Scope: full base 4718421324 → this head (63 files, +14,423). Read the new raven/agent/subagent/dag_handover.py in full, the reassignment/path-gate code in dag_tool.py (_check_reassign_paths, prepare_reassign, commit_reassign), the store I/O (dag_store.read_text/write_text, the JSONL log read in dag_handover._instance_log_available), the RPC/tasks/instances changes, and the changed Web/TUI surfaces; DAG/instance suites run at head and at base on native Windows.

One Windows finding (medium, test-level; root cause inherited).

7 new tests fail on Windows because the inherited DAG ref-path containment refuses every workdir reference on NTFS

Files: the seven tests in tests/test_subagent_dag_control_tools.py (test_a_path_using_node_may_move_to_an_agent_that_reads_local_files, the four ..._unreadable_path_is_refused_up_front, test_a_terminal_siblings_paths_do_not_gate_the_move, test_the_move_installer_gates_every_node_it_is_about_to_build); root cause raven/agent/subagent/prompt_paths.py — resolve_reference (:110-128) and within (:135-145).

Affected scenario: any DAG node whose prompt template carries {{ ref:... }} or {{ ref_path:... }} for a file in the session workdir, on a Windows host.

Mechanism: resolve_reference normalises with posixpath while os.path.realpath on Windows returns backslashes; within() then tests child.startswith(root.rstrip("/") + "/"), which can never match D:\...\notes.md. check_confined therefore rejects a plain notes.md inside the workdir as "outside the session workdir and its sub-agent history", tool.execute returns an invalid-DAG error, no run starts, and the new tests' _wait_until times out (10 s).

Evidence (native Windows 11, CPython 3.12.13): a direct probe of the same tool setup returns

Error: invalid DAG — ref path 'notes.md' (-> 'D:\tmp\dag1118-.../notes.md') is outside the
session workdir and its sub-agent history. No sub-agent was run. ...

Same nine test files, head vs base:

base 4718421324: 16 failed, 1234 passed, 19 skipped
head 2bad12f0:   23 failed, 1404 passed, 19 skipped
only in head:    7 failures, all tests/test_subagent_dag_control_tools.py (TimeoutError at _wait_until)

The new tests/test_subagent_dag_handover.py passes on Windows (45 passed), as do the other new/changed suites; the 7 additions are exactly the path-gate tests.

Scope of the defect: the containment bug is inherited from the base (its ref-path tests are in the base 16 failures and PR #1066, "resolve DAG file references by the host's path rules", is the open fix). This PR does not introduce it, but it adds the first tests that depend on a path-using node running, and they cannot pass on Windows until #1066 lands.

Remedy direction: make check_confined/within use host path semantics (os.path.commonpath on normcased paths, or Path.resolve().relative_to()), as #1066 proposes; until then, mark the seven new tests skipif(sys.platform == "win32", reason="#1066 ...") so the Windows signal is an honest skip rather than seven timeouts.

Checked, no separate Windows defect in the new code: _check_reassign_paths delegates to check_path_placeholders (placeholder-kind logic only, no path comparison); dag_handover writes/reads UTF-8 through the store, tolerates CRLF (line strip(); role-label regex uses (?<=\r)/(?<=\n)), and its text fitting is pure string arithmetic; no new subprocess, signal, encoding, or filesystem primitive is introduced.

Verification commands and results (isolated worktrees D:\tmp\rv-1118 and D:\tmp\rv-1118-base, scratch HOME/USERPROFILE, PYTHONUTF8=1):

pytest tests/test_subagent_dag_runner.py tests/test_subagent_dag_control_tools.py \
       tests/test_subagent_dag_adjudication.py tests/test_subagent_dag_core.py \
       tests/test_subagent_dag_verdict.py tests/test_subagent_instance_log.py \
       tests/test_subagent_instance_registry_reload.py tests/test_rpc_instances.py \
       tests/test_rpc_tasks.py -q
→ head: 23 failed, 1404 passed, 19 skipped
→ base: 16 failed, 1234 passed, 19 skipped

pytest tests/test_subagent_dag_handover.py -q
→ 45 passed

Limitations: the Windows ui-web/ui-tui surfaces were not exercised (generated clients only); the full suite was not run; the 16 base failures are the known Windows DAG baseline (POSIX-root fixtures, process-group reaping) and were not re-adjudicated here.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the complete delta from 2bad12f004be to this revision. The completion and list-reconciliation paths now recover a reassigned node's agent and handle with correct null-versus-absent semantics, and focused regressions cover both paths. I also checked the surrounding row merge behavior, backward compatibility for entries without reassignment fields, the unchanged-status reconciliation case, test changes, and the prior finding's concrete reproduction.

Verification: npm test --prefix ui-tui -- --run src/__tests__/liveAgentsStore.test.ts (43 passed) and npm run type-check --prefix ui-tui (passed). The blocker thread is resolved.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1118 @ f2e6df8b704153f15e35492a9de5d133614847f9

Scope: delta from the previously reviewed head 2bad12f0 — ui-tui/src/app/liveAgentsStore.ts and its test only (+103/−11, TUI identity propagation from manifest/list frames); the DAG/path code and tests/test_subagent_dag_control_tools.py are unchanged, so the finding below is re-verified rather than newly raised.

Result: the Windows finding from the 2bad12f0 review is unchanged; the delta itself is platform-neutral.

  • The delta is browser/terminal state logic — spreading agent/instance identity from dag.run_completed manifest entries and list snapshots into rows. It touches no paths, filesystem, subprocess, encoding, or platform API; the /tmp/run-1 strings in the test fixtures are opaque JSON values.
  • Re-ran the affected file at this exact head on native Windows:
pytest tests/test_subagent_dag_control_tools.py -q
→ 7 failed, 127 passed in 45.03s

The same seven path-gate tests fail, at the same place: tool.execute refuses a valid workdir reference on Windows because the inherited raven/agent/subagent/prompt_paths.py:within() compares os.path.realpath output (backslashes) against a posixpath root, so _wait_until times out before the reassignment gate is exercised. Base 4718421324 fails the same reference tests (16 failures in the nine-file set; head had 23, the seven additions being exactly these).

Cross-reference: PR #1066 at e396c4e6 fixes this — its check_confined/within now use host path rules, and I verified on this host that its un-skipped reference tests pass and that a notes.md inside the workdir is accepted. Once #1118 is based on #1066 (or main carries it), these seven tests should run on Windows; until then they are red here.

Limitations: the TUI delta cannot be unit-run in this checkout (ui-tui test harness unavailable via borrowed node_modules), so it is code-read only; no full-suite run.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the handover puts the source sub-agent's answers into the destination agent's first prompt unfenced, framed as that agent's own earlier conversation.

render_handover builds answers = {i: escape_handover_text(turn.assistant)} (raven/agent/subagent/dag_handover.py:254) and emits them as bare Assistant: ... lines (:216-217). The block is wrapped only in a raven-minted <handover> tag and a trailer telling the destination "you are now picking up where it left off" (:286-295). Those answers are sub-agent-authored by construction: instance-log assistant content (:81-84) or the node's attempt output files (:124-127). Neither dag_handover.py nor dag_runner.py calls wrap_untrusted. escape_handover_text (:41-50) only stops the text from impersonating a role label or a <handover> tag, and the PR's own CONTEXT.md entry says as much: "that escape is a framing guard, not a sanitizer for instructions inside the text".

The repo's rule for this channel already exists:

  • CONTEXT.md, Memory record: sub-agent text "must run ... through wrap_untrusted (raven/security/trust.py) before it reaches another sub-agent's prompt, the same as any other sub-agent-controlled content."
  • CONTEXT.md, The fence rule: "Another node's output ... is wrapped with source="subagent" ... a node's output is sub-agent-authored by construction" (prompt_render.py:39 does this for {{ X.output }}).
  • raven/agent/subagent/direct_chat.py:290-298: an unwrapped prepend is safe "only because every byte here is raven-minted, never sub-agent output ... A future field that echoes a sub-agent's own reply text ... must not be added here."
  • raven/agent/subagent/manager.py:2197-2202 fences a sub-agent's result before it reaches the main agent, because it "may have fetched web pages / read files".

Why it matters here: a source agent that was steered by something it read can write instructions that land in the destination's user message with full trust, presented as the destination's own reasoning. The bundled skill tells the main agent to reassign exactly when "the sub-agent lacks what the task needs -- a tool, access to the files" (subagent-dag-orchestration/SKILL.md:177). So the destination is often the more capable agent.

A fix that keeps the design: fence the carried sub-agent answers, and any nested earlier handover carried inside a user row, with wrap_untrusted(..., source="subagent"). Leave the node's task, the person's and the main agent's words, and the raven-minted trailer unfenced. Fencing the whole block would turn the anchor node's task into data. A test in tests/test_subagent_dag_handover.py asserting that a carried answer appears inside the untrusted fence pins it.

Read only; nothing executed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenced, in d62e293 (rebased onto main 26352da).

Exactly the shape you asked for:

  • every carried sub-agent answer is wrapped with wrap_untrusted(..., source="subagent"), so the destination reads it as data from another sub-agent, not as its own earlier reasoning;
  • a nested earlier handover travelling inside a user row is fenced too: the move is transitive, and that inner body is sub-agent text as well;
  • the node's task, the person's words, the main agent's message and the raven-minted trailer stay unfenced, so the anchor task is not turned into data;
  • the existing marker escaping stays, as the framing guard it is, on top of the fence rather than instead of it;
  • the budget reserves the fence length before shortening, so a fitted handover stays inside the limit.

Tests, written first and red against the unfenced implementation: one asserts a carried answer appears inside the untrusted fence, one asserts the task and the main agent's message do not.

Verification at this head: the ten F4 suites 1954 passed across three groups; ruff check and format clean on 34 changed files; the three generator checks exit 0 with no diff; tests/test_agent_loop_token_budget.py 6 passed.

Leaving the thread open for you to verify and resolve.

@0xKT

0xKT commented Oct 27, 2026

Copy link
Copy Markdown
Member

Not a blocker -- other findings from the same pass, separate from the blocking thread. Each was read at this head; none was executed.

Severity of this one finding, not a verdict on the pull request. Any blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.

  1. A sibling's already-decided continue/abandon can be stranded behind a reassign (raven/agent/subagent/dag_runner.py:1955-1970). _await_adjudications now breaks as soon as a decided answer is a REASSIGN and takes only that node. An answer for another suspended node decided in the same wait stays on the desk with the node shown as exception. It is applied only on a later pass with an empty ready set, which is usually after the reassigned node's whole next attempt. Meanwhile prepare_reassign refuses that node as no longer waiting. Nothing is lost or run twice, but the base took every decided answer before returning. Taking all decided answers before the break, and dispatching them with the moved node, keeps the immediate hand-back.

  2. On a Worker Table run, reassign can move a chartered role's node off the table (raven/agent/subagent/dag_control_tools.py:196-208). The new agent enum is registry.names(), while run_subagent_dag/replan and spawn offer table.labels() (dag_tool.py:1539). The moved node keeps its Charter text, but the structured charter payload is enforced only by Raven transports (raven_loop, a Raven ACP receiver), not by cli_agent or openai_api. So a role whose Charter took a tool away may get it back after a move. A test pins an off-table move as intended. If that is the design, CONTEXT.md's Worker Table and Charter entries should name the exception. If it is not, the enum should follow the table.

  3. A node whose rendered task exceeds ~128000 chars can never run after a move (raven/agent/subagent/dag_handover.py:244, dag_runner.py:2407). render_handover keeps the task whole and raises HandoverError when it does not fit, but it first runs at dispatch, after the move is committed. Every continue, or a further reassign, rebuilds the same task and fails again, spending the budget. A {{ ref:... }} of a large file is enough. Measuring at prepare_reassign would refuse it at the door.

  4. Admission for a move is a second copy of submission admission. _target_backend rebuilds _preflight's per-node backend and capability checks, and a reassign resolves the target's MCP grant outside the run's MCP scope. Each change to one now has to reach the other.

  5. Smaller:

    • A second hop from a named instance never uses the instance log.
    • The instance-history fallback shows the source agent's answer as the destination instance's own turn.
    • Several readers decide "who runs a reassigned node now" with different predicates.
    • CONTEXT.md's exception entry still lists three decisions.
    • SKILL.md calls the act a "hand-off" next to the existing Handoff term.
    • Two of the 2bad12f fixes survive mutation: dropping the partial-MCP-grant notice, and replacing instead of unioning group membership. Each needs a test that fails without the fix.

@0xKT

0xKT commented Oct 27, 2026

Copy link
Copy Markdown
Member

Merge conflict: @LivXue this PR no longer merges cleanly into main.

Measured just now with git merge-tree --write-tree of main 26352da0 and this head f2e6df8b. Main moved with #1064 (finish conditional branches in playbooks, the tasks board and the docs), which changed docs-site/docs/orchestration.md and its zh twin. Conflicting files: docs-site/docs/orchestration.md, docs-site/docs/orchestration.zh.md.

After a rebase onto current main I re-check what moved before the grade comes back.

@LivXue
LivXue force-pushed the feat/dag_node_reassignment branch from f2e6df8 to d62e293 Compare October 27, 2026 07:47

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the rebased revision by range-diffing it against the previously reviewed head, then reading the two substantive follow-up commits and their callers. The old commits replay cleanly; the new handover path fences carried sub-agent answers and nested handovers while leaving the task and main-agent instruction trusted, and its budget accounts for the fence. I also checked the adjudication, moved-group, backend/MCP, admission-refusal, multi-hop history, assignment-reader, RPC, and TUI identity changes.

Covered the repository rules (AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and the relevant Runtime/TUI context), the diff and callers, commit/range history, backward-compatible reader fallbacks, test changes for weakening, and the documented Worker Table/Charter/trust-boundary constraints. I found no new ordinary-use failure. My earlier TUI thread remains resolved; I did not alter another reviewer's open thread.

Verification: 904 focused Python tests passed; the TUI store suite passed 43 tests and TUI type-check passed; source-language and large-file checks passed via their underlying scripts (make is unavailable here); git diff --check passed; and git merge-tree --write-tree HEAD github/main completed cleanly.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Not a blocker items and the merge conflict -- all addressed in 0b65b7592 and d62e293ab.

  1. A sibling's decided continue or abandon is now applied in the same batch as a
    reassign, before _await_adjudications returns, instead of waiting for the
    moved node's next attempt. An unanswered sibling's waiter stays open, and a
    reassign that reaches the runner without a prepared move still fails the node
    by name. Two regression cases (continue and abandon) fail without the
    change.
  2. Worker Table: I kept the roster-name design and named the exception where you
    asked. CONTEXT.md's Worker Table and Charter entries and the DAG skill's
    reassign section now say a reassign takes the node out of the table and keeps
    the Charter of the role it was submitted under; the structured charter fields
    are enforced by the Raven transports only, as they already are for the role
    itself.
  3. Oversized task: prepare_reassign now dry-runs the handover with the
    install's own budget and refuses at the door ("no decision was recorded")
    when it cannot be rendered. A 128001-character task case pins it, and the
    mutation that removes the door turns it red.
  4. Admission is one builder now: _node_backend is what both _preflight and a
    move use, so a check added to one reaches the other. That also added the
    missing enabled check to the move path. The run's MCP scope is frozen on
    the admission record and re-entered for the dry run, so a playbook's own
    servers resolve instead of reporting "not connected"; a partial-grant case
    now pins the notice a move must keep.
  5. Smaller items, all landed:
    • a second hop from a named instance reads the log for the attempts that ran
      on it (_attempts_on), instead of demanding the attempts that ran
      elsewhere;
    • a moved node's instance history comes from its per-attempt archives, so the
      flat pair still holding the source's answer is never shown as the
      destination's own turn;
    • "who runs this node now" has one resolver (dag_reader.current_assignment)
      used by dag.get, tasks.list, subagent.list and dag_exchanges, and
      the instance card compares agents canonically;
    • CONTEXT.md's exception entry names four decisions;
    • the skill says "reassignment", not "hand-off";
    • the two mutation survivors have tests: the partial MCP-grant notice on a
      move, and union-instead-of-replace for a group pair.
  6. On my own pass I found _install_move retargeted only the answered node's
    original group, so two graph-named groups merged onto one pair kept a stale
    mapping after a further hop and a capture replayed them at a pair they had
    left. _record_move_groups now retargets every group the moved set belongs
    to and still unions the destination pair.

Merge conflict: rebased onto 26352da09. The conflicts were exactly the two
docs-site/docs/orchestration files you named; the branch side already carried
#1064's wording, so the resolution kept it. #1066 is in main now, so the seven
Windows tests should run after the rebase; that needs a Windows re-run, which I
cannot do here.

Verification at the rebased head: the changed suites (handover, runner,
control-tools, adjudication, core, verdict, RPC instances/tasks/subagent calls,
schema, instance registry/log, playbook delegate, unified playbook, agent role)
give 2195 passed; ruff check/format, ty check raven, lint-imports
(10/10), the source-language and large-file gates all pass; nine source
mutations each turn their test red. The full suite's residue is this machine's
node/remote baseline: the same 14 files fail identically at the unmodified head
(103 failed / 793 passed / 76 skipped both ways), and no failing file is one
this diff touches.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1118 @ d62e293ab52bce6469f9361e4b3ea436bc352c78

Scope: the head moved from the previously reviewed f2e6df8b. The branch now carries the host-path-rules fix in raven/agent/subagent/prompt_paths.py (+48/−12, the #1066 change: resolve_reference uses os.path.join/normpath, within uses os.path.normpath + os.sep, _check_shape judges both POSIX and Windows rules), plus 41 lines of test changes in tests/test_subagent_dag_control_tools.py; the rest of the delta is main's merges.

Result: the Windows finding I reported at 2bad12f0/f2e6df8b is resolved.

The seven path-gate tests that timed out on Windows (valid workdir references refused before dispatch) now pass, because the branch's confinement guard resolves and compares paths with the host's rules:

pytest tests/test_subagent_dag_control_tools.py tests/test_subagent_dag_handover.py -q
→ 188 passed in 10.73s

That covers test_a_path_using_node_may_move_to_an_agent_that_reads_local_files, the four ..._unreadable_path_is_refused_up_front cases, test_a_terminal_siblings_paths_do_not_gate_the_move and test_the_move_installer_gates_every_node_it_is_about_to_build, plus the 45 handover tests.

No new Windows-specific code appears in the delta beyond the confinement fix; the TUI changes in the raw diff are main's merges.

Limitations: no live gateway or real Telegram/ACP process was run; the TUI delta was not unit-run in this checkout.

@0xKT

0xKT commented Oct 28, 2026

Copy link
Copy Markdown
Member

Merge conflict: @LivXue, this PR no longer merges onto the current main tip 959cd0a9 (the #1092 merge). Measured with git merge-tree --write-tree 959cd0a9 d62e293a -> exit 1, one conflict:

  • raven/rpc/methods/tasks.py

The judge's verdict on d62e293a is clean, so a rebase onto main is what stands between this and the acceptance queue.

@gloryfromca

Copy link
Copy Markdown
Member

Blocking: rebase onto current main and resolve the conflict in raven/rpc/methods/tasks.py.

I independently confirmed the new integration state: github/main is 959cd0a9fd55, and git merge-tree --write-tree github/main HEAD exits 1 on that file. The unchanged branch itself still passes uv run pytest tests/test_rpc_tasks.py -q (81 passed), but the merged result cannot be tested until the conflict is resolved. This updates my prior clean stance; it does not add a separate code finding.

A suspended DAG node can now be reassigned: the main agent picks another
sub-agent from the roster, the next attempt runs on it, and the run continues
instead of being wound down and re-submitted. The node keeps its id, task,
dependencies and place in the graph; every attempt is preserved and the
continuation budget is shared, not reset.

The new agent's first prompt carries a handover block: the instance's visible
conversation, or the node's own archived attempts when there is no instance.
It excludes tool calls, tool output and private model context, and the message
the main agent writes travels as the ordinary follow-up, so the judge still
reads the original task plus recorded follow-ups.

Admission is explicit: roster membership, not the current agent, local-file
capability for every node that moves, a stateful target for an originally
shared instance, and an unoccupied destination handle. A confirm run asks again
before the move. The scheduler moves pending and suspended nodes, leaves
completed ones where they ran, plants the handover once per destination, and
keeps the global concurrency slot free while it waits for the source attempt to
settle.

Records follow the agent that actually ran: the registry row moves atomically at
attempt start, readers agree before and after finalization and after a restart,
and both UIs show the attempt history. A successful reassigned run captures to a
Playbook on the agents that finished it while the audit keeps the factual
history. The submitted graph is never modified.

Node judgement stays always on; the exception report names the attempt that
ended, lists earlier attempts with bounded reasons, and teaches the four
recovery choices against the remaining budget.
A partial MCP grant on a reassigned node was dropped: _target_backend built
the same dispatch backend as _preflight but discarded the grant's note, so a
server that could not be resolved cost the node its tools silently. It now
returns the note, the execution helper adds it to the plan's notices, and the
installer logs it instead of swallowing it.

Moving a group onto a pair that already stands for another group replaced its
membership instead of adding to it. A graph may name one handle on two agents,
so the target pair can already belong to a group whose members must keep
following it through later hops; the write is now a union.
Identity was folded only on dag.node_updated, so a client that missed the
moved attempt's frame and received only the run manifest kept the old agent
and handle for good. The completion fold now applies the same presence
semantics the update branch uses: subagent only when truthy, instance only
when the key exists, so a present null clears the handle.

The list snapshot had the same gap: it settled a row's status but never
repaired its identity. Reconcile now repairs an existing row from the
authoritative item on every poll, independently of the status transition.
The carried turns are sub-agent output by construction, and the handover
emitted their answers as bare Assistant: lines inside a raven-minted tag.
A steered source agent could therefore write instructions that read as the
destination's own earlier reasoning.

Fence each carried answer with wrap_untrusted(source="subagent"), and fence
the body of a handover nested inside a carried user row, which is sub-agent
text for the same reason. The task, the person's and the main agent's words,
and the raven-minted trailer stay outside the fence; the existing marker
escaping stays as the framing guard on top of it.

The budget reserves the fence's length before shortening the newest answer,
and raven's own "no answer" placeholders are left unfenced.
The handover fence is the blocking item; the remaining review findings on the
reassignment flow are closed here:

- apply the other answers that land in the same adjudication batch as a
  reassignment (a sibling's continue or abandon no longer waits for the moved
  node's next attempt), through one helper shared with the ordinary path.
- retarget every graph-named group a move actually takes, and union the
  destination pair, so a group the move joins keeps following it through
  later hops.
- build the destination backend through the same helper submission uses, and
  enter the run's MCP scope for the admission dry run, so both admission
  paths agree and a playbook's own servers resolve.
- refuse at admission a move whose handover could never render, instead of
  committing it and failing every later attempt.
- read a moved node's exchanges from its per-attempt archives, and read a
  second hop's instance log for the attempts that ran on it, so one agent's
  answer is never shown as another's turn.
- resolve the current agent, handle and attempt history through one helper
  for every reader, and compare instance-card agent names canonically.
- document the Worker Table exception and the fence in CONTEXT.md and the
  DAG skill.

Tests cover each finding, including the handover fence and its interaction
with a second hop's archived prompt.
The rebase onto main brought #1131's removal of tool_failure_count into the
contract and the node fixture builder, but two expectations this branch adds
still listed the field, which TaskNode no longer carries.
@LivXue
LivXue force-pushed the feat/dag_node_reassignment branch from d62e293 to 0d0f1b7 Compare October 28, 2026 08:40

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the rebase by range-diffing it against d62e293ab52b and inspecting the resolved raven/rpc/methods/tasks.py callers and the added web-test adjustment. The feature commits otherwise replay unchanged. The resolution keeps the reassignment identity/history reader while adopting current main's removal of tool_failure_count; the follow-up commit removes that obsolete field only from the two branch-added expectations. No tests were weakened beyond matching the new schema.

The prior integration blocker is cleared: github/main is the merge base, git merge-tree --write-tree github/main HEAD succeeds, and git diff --check is clean. Verification passed with 904 focused Python tests, 61 affected web live-state tests, and the web TypeScript type-check. I also checked the applicable repository/context rules, caller compatibility, and my thread sweep; my earlier TUI thread remains resolved, and I did not alter another reviewer's thread.

@LivXue

LivXue commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1118 @ 0d0f1b74b04f30e352ef563da022d70171182818

Scope: the branch was force-updated and rebased onto current main; the PR's own diff at this head is 959cd0a9..0d0f1b74 (63 files, +15282/−328). git range-diff 06509f8e..d62e293a 959cd0a9..0d0f1b74 shows the five earlier commits carried forward with two adaptations to main's removal of the tool-failure counts (_tool_call_count, tool_failure_count dropped) and one new commit that drops the removed field from the node expectations. The feature content — handing a DAG node to another sub-agent mid-run, with attempt history, handover fencing, and the tasks/DAG surfaces — is the same set reviewed at the previous head. Re-ran the affected suites on Windows.

Result: no Windows-specific defect introduced by this head. The 5 failures it shows are the known Windows process/path baseline cases and are identical at the base (details below); the new handover state is carried in run records and instance records, not in new subprocess or filesystem primitives.

Verification on native Windows 11, detached worktree D:\tmp\rv-1118 at the reviewed head, offline venv (uv sync --frozen --offline --all-extras --no-extra segmenter --dev), scratch HOME/USERPROFILE, PYTHONUTF8=1, PYTHONPATH pinned:

  • uv run --no-sync pytest tests/test_subagent_dag_runner.py tests/test_subagent_dag_handover.py tests/test_subagent_dag_control_tools.py tests/test_subagent_dag_core.py tests/test_subagent_dag_adjudication.py tests/test_subagent_dag_verdict.py tests/test_subagent_instance_log.py tests/test_subagent_instance_registry_reload.py tests/test_rpc_tasks.py tests/test_rpc_instances.py tests/test_rpc_subagent_calls.py -q → 5 failed, 1555 passed, 3 skipped
  • the ten files that exist at base 959cd0a9 (test_subagent_dag_handover.py is new here) → 5 failed, 1316 passed, 3 skipped
  • failing-ID sets are identical in both directions: test_rpc_instances.py::test_a_log_rows_clock_survives_the_read, the two test_subagent_dag_core.py cases, and the two TestBackgroundRun reaping cases that are the known Windows process-group failures. None is introduced by this PR.
  • ui-web: vitest run src/features/dag/graph.test.ts src/features/dag/nodes.test.ts src/features/tasks/live.test.ts src/features/tasks/TasksPage.test.tsx → 4 passed files, 260 passed tests; the fixture-shape gate → 5 passed
  • ui-tui: vitest run src/__tests__/dagCommand.test.ts src/__tests__/dagPanel.test.tsx src/__tests__/dagRun.test.ts src/__tests__/dagStatus.test.ts src/__tests__/liveAgentsStore.test.ts → 5 passed files, 343 passed tests

Coverage gaps: the full suite was not rerun; multi-process DAG execution was exercised through the test harness, not a long live run; the inherited Windows reaping/log-clock failures above are not attributed to this PR.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants