---
id: IR-18
title: "substrate: claudecode sessions as dynamic child gate (namespace redesign)"
status: Complete
type: implementation
blocked_by: [IR-3, IR-4, IR-9, IR-12, IR-21]
unlocks: []
severity: Low
target_repo: plexus-substrate
---

## Problem

ClaudeCode (`src/activations/claudecode/activation.rs`) exposes sessions as flat methods operating on a session ID: `chat(session_id, prompt)`, `resume_session(session_id)`, `list_sessions()`, etc. Conceptually, each session IS a nested namespace — `claudecode.session abc123.chat("…")` reads better than `claudecode.chat(session_id="abc123", prompt="…")`.

The new `#[plexus_macros::child]` macro lets a `session(id: &str) -> Option<SessionActivation>` method declare the dynamic child gate with opt-in `list = "session_ids"` for tab-completion. Under IR-9's typed-handle codegen, generated TypeScript / Rust clients would produce `client.claudecode.session.get("abc123").chat("…")` — fully typed, compile-time capability enforcement.

This is a **DX improvement**, not a correction. The flat API works; this ticket adds a typed child gate on top without removing the flat methods (compat).

## Why deferred until IR-12 lands

IR-9's typed-handle codegen emits `DynamicChild<T>` intersections only when the IR carries `MethodRole::DynamicChild { .. }`. Synapse (the Haskell IR builder that feeds hub-codegen) does not yet emit `mdRole` — that's IR-12. Without IR-12, this ticket's work lands the role-tagged `#[child]` method in substrate's schema, but downstream client codegen never sees it.

This ticket's value is maximized once both IR-9 (shipped) and IR-12 (pending) are live. Before then, the migration is structural-only (compile-time Rust API change, no downstream client change).

## Upstream context

- IR-9 (`hub-codegen b4fea08`): typed-handle codegen for DynamicChild.
- IR-12 (Pending): synapse emits mdRole so IR-9's codegen actually fires.
- CHILD-3/4/8: `#[child]` attribute + list/search opt-in + hub-mode inference.

## Required behavior

Add a `SessionActivation` nested client type and a dynamic child gate on ClaudeCode:

```rust
pub struct SessionActivation {
    session_id: SessionId,
    storage: Arc<ClaudeCodeStorage>,
    // ... other fields needed to serve the per-session methods
}

#[plexus_macros::activation(namespace = "session")]
impl SessionActivation {
    /// Send a prompt to this session.
    #[plexus_macros::method]
    async fn chat(&self, prompt: String) -> ChatResponse { /* ... */ }

    /// Resume the session from its last checkpoint.
    #[plexus_macros::method]
    async fn resume(&self) -> ResumeStatus { /* ... */ }

    // ... other per-session methods
}

// ClaudeCode's impl block gains:
#[plexus_macros::child(list = "session_ids")]
async fn session(&self, id: &str) -> Option<SessionActivation> {
    let session_id = SessionId::from(id);
    if self.storage.session_exists(&session_id).await? {
        Some(SessionActivation::new(session_id, self.storage.clone()))
    } else {
        None
    }
}

async fn session_ids(&self) -> impl Stream<Item = String> + Send + '_ {
    futures::stream::iter(self.storage.list_session_ids().await.into_iter().map(|id| id.to_string()))
}
```

**Backward compat:** existing flat methods (`chat`, `resume_session`, `list_sessions`, etc.) on ClaudeCode stay exactly as they are. The new `session` child gate is ADDITIVE. Eventually (next major), flat methods can be `#[deprecated]` in favor of the child-gate path — out of scope here.

## Risks

| Risk | Mitigation |
|---|---|
| Duplicating per-session method bodies in `SessionActivation`. | The per-session methods call into the same underlying `ClaudeCodeStorage` / LLM transport as the flat methods — factor out helper functions first, call from both the flat method and the SessionActivation method. |
| Adds ~200 lines of code for a DX improvement that doesn't help until IR-12. | Accept — the typed-handle codegen that consumes this is already shipped; the gap is synapse. Once IR-12 lands, both child gates (this + the cone ticket IR-19) activate simultaneously. |
| Session lifecycle (creation, deletion) is managed by top-level methods, not per-session. | That's fine — `session(id) -> Option<SessionActivation>` is a LOOKUP, not a creator. Creation stays flat. |
| SessionActivation fields (storage, LLM handles) are cloneable. | Verify before starting. `Arc<Storage>` is Clone; other deps may require refactoring. |

## What must NOT change

- Flat methods on ClaudeCode (`chat`, `resume_session`, `list_sessions`, etc.) — unchanged in signature, body, wire behavior.
- Session storage format / wire format for session data.
- `SessionId` newtype semantics.

## Acceptance criteria

1. `cargo build -p plexus-substrate` passes.
2. `cargo test -p plexus-substrate` passes — all existing ClaudeCode tests pass without modification.
3. A new test verifies that `plugin_schema()` on ClaudeCode contains a method named `session` with `role: MethodRole::DynamicChild { list_method: Some("session_ids"), search_method: None }`.
4. A new test verifies that `ChildRouter::get_child(claudecode_instance, "<valid-session-id>")` returns `Some(SessionActivation)` and that `get_child("<invalid-id>")` returns `None`.
5. A runtime integration test (connect substrate via synapse-or-direct) demonstrates nested routing `claudecode.session.<session-id>.chat(…)` works end-to-end.
6. `ChildCapabilities` on the ChildRouter impl for ClaudeCode includes `LIST` (from the `list = "session_ids"` opt-in).

## Completion

PR against plexus-substrate. CI green. Include in PR description a short synapse transcript showing both paths work:
- Old: `synapse claudecode chat session_id=abc prompt="hi"`
- New: `synapse claudecode session abc chat prompt="hi"`

Status flipped to Complete in the same commit.
