Skip to content

feat(delegation): let a session hand tasks to child sessions - #653

Draft
jiro-by-nori[bot] wants to merge 6 commits into
feat/multi-session-d2-hostfrom
feat/multi-session-e1-delegation
Draft

jiro-by-nori[bot] wants to merge 6 commits into
feat/multi-session-d2-hostfrom
feat/multi-session-e1-delegation

Conversation

@jiro-by-nori

@jiro-by-nori jiro-by-nori Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

🤖 Generated with Nori

Implements E1 delegation behind multi_session, stacked on D2 (#652). /delegate <agent> <task> and the session-bound nori-client MCP delegate tool use one harness-owned admission and lifecycle flow. Same-kind children start immediately; different-kind children wait for Start/Decline. Delegation is limited to two levels below the root.

Children are navigable local and attached sessions. A child permission stays in that child; Alt+Down opens the next waiting session. Notices identify child and parent. Results automatically enter the parent prompt queue, and a child that delegated waits for its descendants and continuation before completing. Completed children release process capacity while retaining history.

Explicit parent close cancels descendants. Parent process failure preserves children and holds results for stable-ID resume; result delivery is acknowledged by the parent's completed prompt. Forks preserve delegation depth and move ownership, approvals, and results to the current conversation.

Design

The Nori-owned tool uses the existing authenticated loopback HTTP MCP service. This gives agents a typed task request without changing ACP or introducing a second server. Its process-bound context supplies parent identity; callers cannot choose another session as parent. Both the remote controller API and TUI call the same manager. Watchers cannot mutate delegation state.

Admission and ownership changes are atomic under the manager lock; process startup, prompt submission, and shutdown run outside it. The session cap includes live children and in-progress launches. Remote lists/events carry hierarchy and delegation state; nori host ls keeps metadata columns fixed and places hierarchy only in TITLE.

Validation

  • Harness: 454 passed, 1 ignored, including 20 delegation lifecycle/concurrency tests and 9 managed-remote tests.
  • TUI: 1,497 passed, 4 ignored, including ten new regression tests for teardown, visible failures, scoped routing, and transcript ordering.
  • PTY: 199 passed, 9 ignored in the full local suite with --no-fail-fast. The known cloud auth-error case passed.
  • CLI, ACP host, config, and protocol: 358 passed during the original E1 validation; full Linux CI passed again on this head.
  • Linux CI, cargo-deny, formatting, and Clippy passed on e93deefdfdaddccfd9bf06ba92c9d9fb6f0498a4.
  • Initial delegation proof and scoped-routing diagram with nested-completion capture are posted on the PR.

The verifier's child-teardown diagnostic finding is fixed: action failures retain their originating session generation, normal shutdown suppresses typed transport closure, and real failures remain visible in their owning transcript. Deterministic tests reproduce the original replay-after-shutdown race and verify genuine visible failures, stale history rejection, and buffered message ordering.

Scope and remaining work

E2 owns polished delegation/result cards and tree presentation. Delegation state lasts for the current manager; host restart persistence is outside E1. Stdio-only custom agents can use user-driven /delegate; an agent-tool fallback for those agents is deferred. Terminal proof uses the mock ACP agent.

Draft only. Do not merge; base remains feat/multi-session-d2-host.

Originating chat: https://demo.norisessions.com/chat/01a1e00a-f59a-773a-8ebd-83da9f17ec64

Share Nori with your team: https://www.npmjs.com/package/nori-skillsets

jiro-by-nori Bot pushed a commit that referenced this pull request Nov 15, 2026
@jiro-by-nori

jiro-by-nori Bot commented Nov 15, 2026

Copy link
Copy Markdown
Contributor Author

E1 terminal proof

Real nori TUI with the repository's mock ACP agents, driven through isolated tmux at 120×40 and rendered with Ghostty Web. Parent and child are separate ACP subprocesses. The fixture's permission marker requests write-report; no external service or production data is used.

The sequence shows /delegate, cross-agent Start/Decline, an attributed waiting notice in the parent, Alt+Down opening the child's permission, and the completed result automatically queued into the parent. The terminal stayed open after child completion. The request is simulated; the manager, MCP transport, permission handling, session navigation, and result delivery are real.

sequenceDiagram
    participant U as User / TUI controller
    participant P as Parent ACP session
    participant M as Nori session manager
    participant C as Child ACP session
    participant W as Remote subscribers
    alt User request
        U->>M: /delegate agent task
    else Agent request
        P->>M: authenticated nori-client MCP delegate
    end
    M->>M: Check flag, process ownership, depth <= 2, capacity
    opt Different agent kind
        M-->>U: Child + parent identity; Start / Decline
        U->>M: Resolve approval
    end
    M->>C: Start child and submit task
    M-->>W: Hierarchy metadata + child envelopes
    C-->>M: Permission request
    M-->>U: Attributed waiting notice
    U->>C: Alt+Down; answer in child
    C-->>M: Prompt completes after descendants/continuations
    M->>M: Retain history; release child process slot
    M->>P: Queue attributed result
    P-->>M: PromptResponse acknowledges delivery
    Note over M,P: Parent crash holds/retries result on stable-ID resume
    Note over M,C: Explicit parent close cancels descendants
Loading

1. User requests delegation

1. User requests delegation

2. Different agent kind requires Start or Decline

2. Different agent kind requires Start or Decline

3. Parent names the waiting child and its parent

3. Parent names the waiting child and its parent

4. Alt+Down opens the child; permission is answered there

4. Alt+Down opens the child; permission is answered there

5. Result returns and the parent continues automatically

5. Result returns and the parent continues automatically

Validation: harness 454 passed / 1 ignored; TUI 1,487 passed / 4 ignored; affected CLI/host/config/protocol 358 passed. Full PTY ran with --no-fail-fast; after four failures were corrected, the three affected targets passed 24/24. Full workspace Clippy, formatting, and local cargo-deny passed. GitHub CI is tracked on this draft PR.

jiro-by-nori Bot pushed a commit that referenced this pull request Nov 15, 2026
@jiro-by-nori

jiro-by-nori Bot commented Nov 15, 2026

Copy link
Copy Markdown
Contributor Author

Independent verification of e9ff623, base feat/multi-session-d2-host. No blockers found; one should-fix UI diagnostic bug. All requested functional scenarios passed. No product code changed, no push to the PR branch, no merge.

Built real nori and mock_acp_agent from this head. Drove separate ACP subprocesses through isolated tmux TUIs at 120×40, including real detached nori host and nori attach. Mock tasks/permissions are synthetic; session management, authenticated MCP delegation, remote transport, navigation, and result delivery are real. Read project docs, this PR/body/proof, and mock-up #236; E2 presentation polish and host-restart session persistence are outside this verification.

Independent suite results

Suite Passed Failed Ignored
Harness 454 0 1
TUI 1,487 0 4
PTY 199 0 9

All three ran with --no-fail-fast, build jobs 2, test threads 2, and MOCK_ACP_AGENT_BIN pointing to this build. NORI_MOCK_INSTRUCTION_FILES and NORI_NO_ANALYTICS were unset suite-wide. The known cloud auth-error test passed on this run. Full test logs and environment.

Pass/fail matrix

Requested scenario Result Observed behavior
1. Local + attached /delegate PASS Same-kind immediate start; different-kind asks; both Start and Decline exercised. Decline creates no child process.
2. Child permission ownership PASS Parent notice names child and parent, with no permission popup. Alt+Down opens child; allow response targets that child.
3. Depth PASS + diagnostic below Grandchild completes and result traverses both ancestors. Third level gives delegation depth limit is 2 levels below the top session.
4. Automatic result + parent draft PASS Delayed child finishes while parent remains selected and DRAFT_STAYS_WHILE_RESULT_ARRIVES remains unsent and intact.
5. Crash cases PASS Parent crash leaves child alive; result held until Ctrl+R resumes same ID. Already-queued result survives SIGKILL. Child crash fails only child; parent answers afterward.
6. Close/cap/history PASS + diagnostic below Explicit /close stops parent, child, grandchild. At cap 2, extra live child refused; completion permits next child; completed history remains viewable.
7. Deferred Alt+Down while typing PASS Delayed list refresh arrives after typing; selection and draft stay in parent. Subsequent prompt RPC targets parent ID.
8. host ls child rows + fresh child PASS Fixed ID/AGENT/STATUS/AGE; parent attribution in TITLE. Long terminal titles ellipsized; pipe retains full text. One Alt+Down opens new child before periodic refresh.
9. D2 + flag-off regression PASS Same-port host restart rotates credentials and gives persistent restart explanation, preserving draft. Current list row retains ID. With multi_session off, normal prompts work, delegation stays unavailable, and no multisession status appears.

For fresh/deferred navigation, a small transparent WebSocket fixture held only session-list requests. Delegation/event/permission traffic continued to the real host. Releasing the list demonstrated both timing cases deterministically. Attached observations · RPC ownership proof · Sanitized local wire sequence.

Finding: should-fix — successful child teardown reports an error in the parent

Normal successful delegation intermittently prints red ACP agent command channel closed into the parent transcript. The child result is correct and the parent remains ready, so this is a misleading error/ownership issue, not lost work.

Repro with this head and the built mock agent:

  1. Enable [features] multi_session = true, select mock-model, and set MOCK_AGENT_MCP_HTTP=1. Keep MOCK_AGENT_ECHO_PROMPT unset.
  2. Send an ordinary root prompt, then /delegate mock-model mock:delegate mock-model DEPTH_TWO_LEAF.
  3. Watch the root during fast child completion: the red error can appear alongside a successful nested result.
  4. Also observed with [tui] max_sessions=2: delegate a permission-blocked child, attempt a second child and hit cap, approve the first, return to parent, then /delegate mock-model CAP_REUSED_AFTER_COMPLETION.

Observed in three independent flows: nested completion, cap reuse, and a child completing while the parent was streaming (captured before the intentional parent kill). Ten fresh nested follow-ups did not repeat it; this is intermittent, not a deterministic frequency claim. Positive captures are below. Lifecycle report.

The wrong-session error route is explicit: HarnessActionFailed is not wrapped with the originating generation, while the app renders it in the current widget. A likely trigger is replayed SessionStarted submitting LoadCustomPrompts against a child handle already closing; the failed action emits the unscoped error. That exact asynchronous action is inferred; the captured false error and unsafely scoped routing are confirmed.

Terminal evidence

Ghostty Web 0.4.0 renders actual tmux capture output. DejaVu Sans Mono and both Noto symbol fonts were installed before capture. The trailing capture delimiter was removed before rendering to preserve the first row; PNGs were inspected against text. No fabricated terminal states. Full capture/text/metadata bundle: proof files.

1. Local and attached delegation: immediate, approve, decline

Local same-kind starts and returns without approval

Local same-kind starts and returns without approval

Local different-kind approval

Local different-kind approval

Local decline returns to parent

Local decline returns to parent

Attached same-kind completion

Attached same-kind completion

Attached cross-kind approval

Attached cross-kind approval

Attached decline

Attached decline

2. Attributed child permission, jump, and child-owned answer

Local parent notice names both sessions; no permission popup

Local parent notice names both sessions; no permission popup

Alt+Down opens child permission

Alt+Down opens child permission

Permission answered in child

Permission answered in child

Attached parent notice

Attached parent notice

Attached child permission answer

Attached child permission answer

3. Grandchild success and third-level refusal; first false-error occurrence

Nested result reaches root; spurious red error is the should-fix finding

Nested result reaches root; spurious red error is the should-fix finding

Clear third-level refusal

Clear third-level refusal

4. Automatic result arrives while parent draft stays intact

Unsent draft preserved while result arrives

Unsent draft preserved while result arrives

5. Parent crash, held result, queued-result crash, and isolated child failure

Parent b8957d30… changed ACP PID 57826 → 62167 after Ctrl+R; child PID 58072 remained alive to answer its permission. Queue case kept parent 7c2bfa11…, changed PID 64211 → 74975, and delivered the child result only to the resumed process. See sanitized wire sequence above.

Parent failed; child still waiting

Parent failed; child still waiting

Held child result reaches same parent after Ctrl+R

Held child result reaches same parent after Ctrl+R

Completed result queued while parent is busy

Completed result queued while parent is busy

Queued result reaches resumed parent

Queued result reaches resumed parent

Child failed; parent responds to ROOT STILL USABLE

Child failed; parent responds to ROOT STILL USABLE

6. Explicit close, cap reuse, and retained history; second false-error occurrence

Before /close, ACP PIDs 80532/80721/80745 were live; afterward all three were absent, not zombies. Process evidence.

Parent close shuts down entire descendant family

Parent close shuts down entire descendant family

Cap slot reused after completion; second spurious error

Cap slot reused after completion; second spurious error

Completed child history remains viewable

Completed child history remains viewable

7. Deferred Alt+Down preserves draft in its parent

List refresh released after typing; parent and draft retained

List refresh released after typing; parent and draft retained

8. Fresh child navigation and fixed host-list columns

One Alt+Down discovers and opens child before periodic refresh

One Alt+Down discovers and opens child before periodic refresh

Real host ls: fixed columns, parent inside TITLE, ellipsized titles

Real host ls: fixed columns, parent inside TITLE, ellipsized titles

9. D2 regressions and single-session flag-off

Current row keeps its eight-character ID

Current row keeps its eight-character ID

Same-port restart gives persistent explanation and keeps draft

Same-port restart gives persistent explanation and keeps draft

Flag off: delegation unavailable

Flag off: delegation unavailable

Flag off: normal prompts still work

Flag off: normal prompts still work

No blocker or separate nit was confirmed. Owned manual TUIs, host, and proxy were stopped after verification. Proof is on the separate nori/pr-proofs branch; PR head remains unchanged.

jiro-by-nori Bot pushed a commit that referenced this pull request Nov 15, 2026
@jiro-by-nori

jiro-by-nori Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Fixed the verifier's child-teardown diagnostic finding on e93deefdfdaddccfd9bf06ba92c9d9fb6f0498a4. PR remains draft on D2; nothing merged.

Action failures retain their originating widget generation. Normal child shutdown suppresses only typed command/response transport closure; real ACP failures still render in their owning session. Completed-child replay also skips maintenance work when no live handle exists. History routing rechecks ownership, including after a session is removed, and preserves in-progress transcript ordering.

flowchart TD
    A[Action from session generation G] --> B{Action failed?}
    B -->|Typed transport closure during normal teardown| C[No diagnostic]
    B -->|Real failure| D[Failure tagged with G]
    D --> E{Owner of G}
    E -->|Visible or parked| F[Owner widget flushes active entry and queues error]
    E -->|Activating candidate| H[Retain candidate failure]
    E -->|Removed or replaced| I[Discard stale failure]
    F --> J{Recheck G when history arrives}
    J -->|Visible| K[Render in owning transcript]
    J -->|Parked| L[Store in owning transcript]
    J -->|Removed or replaced| I
Loading

Deterministic tests first reproduced the exact ACP agent command channel closed error by shutting down a real mock ACP process and replaying its captured startup event. Coverage also includes adoption without a live handle, real visible errors, unexpected process exit, history ordering, and stale queued history. No sleep is used to provoke the race.

The capture below runs the real Nori TUI, session manager, HTTP MCP delegation, and three ACP subprocesses. Agent responses are synthetic. A child delegates to a grandchild, the nested result returns to the root, and the root answers a follow-up. Full terminal history contains neither the false channel-closed diagnostic nor a missing-handle diagnostic.

Nested delegation completes without a false parent error

Terminal text, ANSI, PNG, and metadata

Validation on e93deefdfdaddccfd9bf06ba92c9d9fb6f0498a4: 454 harness, 1,497 TUI, and 199 PTY tests passed; 14 ignored, 0 failed. The full local PTY suite used --no-fail-fast. Ten focused regression tests passed. Linux CI, cargo-deny, nightly formatting, and Clippy are green.

@CSRessel
CSRessel added this pull request to stack #650 November 15, 2026 18:34
@jiro-by-nori jiro-by-nori Bot changed the title feat(delegation): E1 child sessions, approvals, and automatic results feat(delegation): let a session hand tasks to child sessions Nov 15, 2026
A child that crashed during a prompt sent its parent the Debug dump of the
RequestFailure, with escaped newlines and the agent's stderr tail. Keep only
the failure's first line, such as "Agent process exited unexpectedly (exit
code 17)."; the stderr tail stays in the child's own history.

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.

1 participant