---
id: IR-19
title: "substrate: cone cones 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

Cone (`src/activations/cone/activation.rs`) exposes cones as flat methods operating on a cone ID, mirroring the ClaudeCode-sessions pattern. Each cone is conceptually a nested namespace — `cone.cone abc123.query("…")` reads better than `cone.query(cone_id="abc123", …)`.

The new `#[plexus_macros::child]` macro enables declaring a `cone(id: &str) -> Option<ConeActivation>` dynamic child gate with `list = "cone_ids"` for tab-completion. Under IR-9's typed-handle codegen, generated clients produce `client.cone.cone.get("abc123").query("…")`. Parallel to IR-18 (ClaudeCode sessions).

This is a **DX improvement**, not a correction. Additive — flat methods stay.

## Why deferred until IR-12 lands

Same reason as IR-18: typed-handle codegen in hub-codegen (IR-9, shipped) only activates when synapse's IR emits `mdRole` (IR-12, Pending). Before IR-12, this ticket lands only the schema-level role tagging; downstream clients don't see the child gate until synapse catches up.

## Upstream context

- IR-9 (`hub-codegen b4fea08`): typed-handle codegen for DynamicChild.
- IR-12 (Pending): synapse emits mdRole.
- CHILD-3/4/8: `#[child]` attribute + list/search opt-in + hub-mode inference.
- **IR-18** (parallel): same pattern for ClaudeCode sessions. Can land independently.

## Required behavior

Add a `ConeActivation` nested client type and dynamic child gate on Cone:

```rust
pub struct ConeActivation {
    cone_id: ConeId,
    storage: Arc<ConeStorage>,
    // ... per-cone-scoped fields
}

#[plexus_macros::activation(namespace = "cone")]
impl ConeActivation {
    /// Query within this cone.
    #[plexus_macros::method]
    async fn query(&self, prompt: String) -> QueryResponse { /* ... */ }

    // ... other per-cone methods
}

// Cone's impl block gains:
#[plexus_macros::child(list = "cone_ids")]
async fn cone(&self, id: &str) -> Option<ConeActivation> {
    let cone_id = ConeId::from(id);
    if self.storage.cone_exists(&cone_id).await? {
        Some(ConeActivation::new(cone_id, self.storage.clone()))
    } else {
        None
    }
}

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

Flat methods on Cone continue to work unchanged.

## Risks

Same as IR-18's risk table. Notable additions:

| Risk | Mitigation |
|---|---|
| The name `cone.cone` is awkward (double noun). | Accept — it's consistent with the CLI's `<activation>.<child>` syntax. Alternative: rename the child method to `of` (`cone.of("abc123").query(…)`) — decide in implementation; the ticket accepts either name. |
| If IR-18 lands first, this ticket benefits from the factored-out helpers pattern (storage-clone, list streaming). | Cross-reference IR-18's implementation when this starts. |

## What must NOT change

- Flat methods on Cone (`query`, `list_cones`, etc.) — unchanged.
- Cone storage format.
- `ConeId` newtype semantics.

## Acceptance criteria

Parallel to IR-18's criteria, substituting "cone" for "session" throughout. Specifically:

1. `cargo build -p plexus-substrate` passes.
2. `cargo test -p plexus-substrate` passes.
3. `plugin_schema()` on Cone contains a method named `cone` (or `of` if that's the chosen name) with `role: MethodRole::DynamicChild { list_method: Some("cone_ids"), search_method: None }`.
4. `ChildRouter::get_child(cone_instance, "<valid-cone-id>")` returns `Some(ConeActivation)`; `get_child("<invalid-id>")` returns `None`.
5. Integration test demonstrates `cone.cone.<cone-id>.query(…)` resolves end-to-end.
6. `ChildCapabilities::LIST` set on the generated `ChildRouter` impl.

## Completion

PR against plexus-substrate. CI green. Transcript in PR description showing both flat and nested paths work. Status flipped to Complete in the same commit.
