Skip to content

feat: a subagent card can open subagent cards of its own, one level down - #690

Merged
omartelo merged 2 commits into
mainfrom
feat/nested-subagent-cards
Oct 9, 2026
Merged

omartelo merged 2 commits into
mainfrom
feat/nested-subagent-cards

Conversation

@omartelo

@omartelo omartelo commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

A Claude Code worker opened as a subagent card can now turn its own general-purpose subagents into cards, one level down. Two commits: a fix the feature depends on, then the feature.

1. fix(relay): a worker reports only once the work it handed off is back

The worker-answer mod answers a worker's errand from the first main-loop Stop whose background_tasks is empty. An errand the worker sent to another session (a card, or a send_to_session that handed back a ticket) is not one of Claude Code's tasks, so the turn that handed it off reported "I handed it off" as the result, and the worker was parked under the session it was waiting on. This already happens today when a worker opens a session through MCP or lich open --subagent.

  • relay.WorkerAnswered ignores a worker that still awaits an outcome (awaitsOutcomeLocked): an errand it sent is open, or a result for it has not been read by a turn of its own (inboxEntry.seen, set on the first busy after the nudge). The outcome reaching its prompt starts the turn that answers. Private errands are excluded, since nothing resumes the worker for them.
  • A worker in that state is not parked (finishedWorkerLocked, finishIfTurnEnded).
  • A worker whose own worker is closed hears stopped through its inbox instead of waiting on silence (SessionClosed). Top-level callers keep today's silent close.

2. feat: nested subagent cards

  • Depth on the row. The subagent column holds the depth (caller's depth + 1, spawn.markSubagent), carried through resume and fork. reopen scanned that column into a bool, which would fail on a depth of 2. Rows written before this change hold 1, which is what they were.
  • Signals split. Every Claude Code session gets LICH_SUBAGENT_DEPTH. LICH_SUBAGENT_CARDS=off now means only "subagents stay native": the setting is off, the card is at maxSubagentDepth (2), or the installed plugin predates 0.19.0, whose mod tells a worker only by off.
  • Briefing. A third relay.SubagentRoute: a card at the limit is told its Agent tool stays inside it, and to fan out with that rather than with new lich sessions, which would nest by another door.
  • Contract and docs: docs/hooks/README.md (sixth variable), mod-answer.md, cli.md, ceilings.md, provider table in both READMEs, CHANGELOG.

Tests changed because the contract changed: the store's subagent mark is now a depth (SessionSubagentDepth), and SpawnBriefing/providerArgs take a SubagentRoute instead of a bool.

Plugin side and release order

The client side is lichdotdev/lich-plugin#58: worker-answer.js tells a worker by LICH_SUBAGENT_DEPTH > 0 and falls back to LICH_SUBAGENT_CARDS=off under an older lich; agent-cards.js drops the subagent/ branch gate when the depth is present and toasts why a worker stays native.

NestedSubagentCardsRelease is 0.19.0, the same contract minor #687 opened for compacting (ceiling already 0.20.0). That plugin PR has to land before 0.19.0 is cut: a 0.19.0 without it would get nested cards switched on by this lich and never answer for the workers that open them.

Gate

  • gofmt -l . clean; go vet ./... clean (with a placeholder frontend/dist, which the embed needs).
  • go test ./... green; -race on relay, terminal, spawn, store and agentplugin.
  • Cross-compile loop for linux, darwin and windows (build + vet).
  • New relay tests run -count=100 without a failure.
  • Frontend untouched, so its gate was not run.
  • Not exercised with real nested sessions yet.

The mod answers a worker's errand from the first main-loop Stop whose
background_tasks is empty. An errand the worker sent to another session
(a subagent card, or a send that handed back a ticket) is not one of
Claude Code's tasks, so the turn that handed it off reported "I handed it
off" as the result and the worker was parked under the session it was
waiting on.

WorkerAnswered now ignores a worker that still awaits an outcome: an
errand it sent is open, or a result for it has not been read by a turn
of its own. The outcome reaching its prompt starts the turn that answers.
A worker in that state is not parked, and one whose own worker is closed
hears the stop through its inbox instead of waiting on silence.
A worker used to spawn with LICH_SUBAGENT_CARDS=off, which kept its own
subagents native and was also how the plugin's worker-answer mod told a
worker. Cards now nest two levels (maxSubagentDepth):

- The sessions row's subagent column holds the depth (spawn: caller's
  depth + 1), carried through resume and fork. reopen read it as a bool,
  which would fail on a depth of 2.
- Every Claude Code session gets LICH_SUBAGENT_DEPTH; LICH_SUBAGENT_CARDS=off
  now means only "subagents stay native": the setting is off, the card is
  at the limit, or the installed plugin predates 0.19.0, whose mod tells a
  worker by the depth.
- The briefing has a third route: a card at the limit is told its Agent
  tool stays inside it and to fan out with that, not with new sessions.

Tests changed because the contract changed: the store's subagent mark is
now a depth (SessionSubagentDepth), SpawnBriefing and providerArgs take a
SubagentRoute instead of a bool, and the terminal's env helper adds the
depth. The plugin ceiling is already 0.20.0 (#687): nesting rides the same
0.19 contract as compacting.
@omartelo
omartelo merged commit 5488364 into main Oct 9, 2026
7 checks passed
@omartelo
omartelo deleted the feat/nested-subagent-cards branch October 9, 2026 02:05
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