# Handle Architecture

## Algebraic Model

### Handles as Typed References

A Handle is a type-erased reference that can be resolved to typed data. The key insight is that the **origin method determines the handle's schema**.

```
Handle = { plugin, version, method, meta }

resolve : Handle → Stream[T]
  where T is determined by (plugin, method)
```

When `cone@1.0.0::chat` produces a handle, it defines what resolving that handle returns. The schema for `cone.resolve_handle` when given a `cone::chat` handle is:

```json
{
  "type": "resolved_message",
  "id": "uuid",
  "role": "user|assistant|system",
  "content": "string",
  "model": "string?"
}
```

### Plugin Categories

**Leaf plugins**: Produce handles, expose `resolve_handle`, no Plexus dependency
- Echo, Bash, Health
- Simple: input → output, may produce handles

**Composite plugins**: Walk data structures containing foreign handles, need Plexus
- Cone, ClaudeCode
- Must resolve handles from *any* plugin to build context

```rust
// Composite plugin pattern
pub struct Cone {
    storage: Arc<ConeStorage>,
    plexus: Weak<Plexus>,  // For resolving foreign handles
}

impl Cone {
    async fn build_context(&self, tree_id: TreeId, head: NodeId) -> Vec<Message> {
        let nodes = self.storage.arbor().context_get_path(&tree_id, &head).await?;
        let plexus = self.plexus.upgrade()?;

        for node in nodes {
            match &node.data {
                NodeType::External { handle } => {
                    // Route ALL handles through Plexus (including our own)
                    let resolved = plexus.resolve_handle(handle).await?;
                    // Convert resolved content to LLM message
                }
                NodeType::Text { content } => {
                    // Inline text, use directly
                }
            }
        }
    }
}
```

**Why Plexus reference in composite plugins:**
- Trees can contain handles from any plugin
- Cone shouldn't know about ClaudeCode, Bash, future plugins
- Plexus is the universal resolver - it routes to the owning plugin

### Resolved Content Type

When `plexus.resolve_handle(handle)` returns, what type is it?

**Option 1: Untyped JSON**
```rust
async fn resolve_handle(&self, handle: &Handle) -> PlexusStream  // Stream<Value>
```
- Composite plugin receives raw JSON
- Must interpret based on handle.plugin/method
- No compile-time safety

**Option 2: Common trait**
```rust
pub trait ResolvedContent: Send + Sync {
    fn as_message(&self) -> Option<&dyn MessageContent>;
    fn as_command_output(&self) -> Option<&dyn CommandOutput>;
}
```
- Plugins return trait objects
- Composite plugins downcast to known types
- Extensible but awkward

**Option 3: Tagged enum at Plexus level**
```rust
pub enum ResolvedContent {
    Message { role: String, content: String, model: Option<String> },
    CommandOutput { stdout: String, stderr: String, exit_code: i32 },
    Unknown(Value),  // Fallback for unknown plugins
}
```
- Plexus defines common shapes
- Plugins map their data to these shapes
- Composite plugins get typed data

**Recommendation: Option 3 with schema**

Plexus defines a set of "content kinds" that handles can resolve to:

```rust
pub enum ContentKind {
    Message,      // Chat message (role, content, metadata)
    Output,       // Command/tool output
    Document,     // Structured document
    Binary,       // Opaque bytes with mime type
}

pub struct ResolvedHandle {
    pub handle: Handle,
    pub kind: ContentKind,
    pub data: Value,  // Schema determined by kind + handle.method
}
```

Each plugin's `resolve_handle` returns `ResolvedHandle` with appropriate kind.
Schema lookup: `(handle.plugin, handle.method) → (kind, data_schema)`

For Synapse:
```
synapse substrate resolve_handle --handle "cone@1.0.0::chat:msg-123:user:bob"

{
  "handle": "cone@1.0.0::chat:msg-123:user:bob",
  "kind": "message",
  "data": {
    "role": "user",
    "content": "Hello world",
    "model": null
  }
}
```

### Client-Side Resolution Model

For external clients (Synapse CLI, web UI), push resolution to client:

```
1. Client: arbor.tree_get(tree_id)
   → Tree { nodes: [Node { data: External { handle } }] }

2. Client caches handles locally

3. When client needs data:
   plexus.resolve_handle(handle)
   → Stream[T] where T schema from handle.plugin.method
```

**Benefits:**
- Plugins don't need Plexus reference
- Client controls resolution timing (lazy/eager)
- Client can batch resolutions
- Clear ownership: each plugin resolves only its own handles

### Schema Integration with Synapse

Synapse derives CLI from schema. For handles:

```
synapse cone chat --name mycone --prompt "hello"
  → ChatEvent stream, some events contain handles

synapse substrate resolve_handle --handle "cone@1.0.0::chat:msg-123:user:bob"
  → ResolvedMessage (schema from cone.chat.resolve_schema)
```

Each method can declare a `handle_schema` that describes:
1. What handles it produces (output)
2. What resolved data looks like (resolve output)

```rust
#[hub_method(
    produces_handles = "cone::chat",
    handle_schema = ConeMessageHandle {
        meta: ["msg-{uuid}", "{role}", "{name}"]
    }
)]
async fn chat(...) -> Stream<ChatEvent>
```

### The Algebra

```
Method → Stream[Event]
Event may contain Handle
Handle : { plugin, version, method, meta }

resolve : Handle → Stream[Resolved]
  where Resolved.schema = lookup(handle.plugin, handle.method).resolve_schema

-- Composition
walk_and_resolve : Tree → Vec<Resolved>
walk_and_resolve tree =
  tree.nodes
    |> filter(is_external)
    |> map(|n| n.handle)
    |> map(resolve)
    |> flatten
```

### Plexus Schema Extension

Add to MethodSchema:

```rust
pub struct MethodSchema {
    pub name: String,
    pub description: String,
    pub params: Schema,
    pub returns: Schema,
    // NEW: If this method produces handles
    pub handle_schema: Option<HandleSchema>,
}

pub struct HandleSchema {
    // What the handle's meta fields mean
    pub meta_fields: Vec<MetaFieldSchema>,
    // What resolve_handle returns for this handle type
    pub resolve_schema: Schema,
}

pub struct MetaFieldSchema {
    pub name: String,
    pub index: usize,
    pub prefix: Option<String>,  // e.g., "msg-"
}
```

This way Synapse can show:

```
synapse cone chat --help

cone.chat - Chat with an LLM cone

Parameters:
  --identifier  Cone name or UUID
  --prompt      User message
  --ephemeral   Don't advance head

Returns: Stream[ChatEvent]

Produces handles: cone@1.0.0::chat
  meta[0]: message_id (prefix: msg-)
  meta[1]: role
  meta[2]: name

  Resolve returns: ResolvedMessage
    id: string
    role: string
    content: string
    model: string?
```

## Overview

Handles are external data references stored in Arbor trees. When a node points to data owned by another activation (Cone messages, ClaudeCode sessions, Bash output), it stores a Handle instead of the actual content.

## Current State

### Handle Structure

```rust
pub struct Handle {
    pub plugin: String,      // "cone", "claudecode", "bash"
    pub version: String,     // "1.0.0"
    pub method: String,      // "chat", "execute"
    pub meta: Vec<String>,   // ["msg-{uuid}", "role", "name"]
}
```

**Display format:** `plugin@version::method:meta[0]:meta[1]:...`

Example: `cone@1.0.0::chat:msg-550e8400:user:bob`

### Issues

#### 1. Duplicate Definition (Critical)

Handle is defined identically in two places:
- `src/types.rs:20` - The canonical location
- `src/activations/arbor/types.rs:115` - Duplicate for historical reasons

The Activation trait uses `crate::types::Handle`, but arbor internally uses its own copy.

#### 2. Meta Format Coupling

Creation and resolution are tightly coupled through string format:

**Creation** (`cone/storage.rs:492`):
```rust
Handle::new("cone", "1.0.0", "chat")
    .with_meta(vec![
        format!("msg-{}", message.id),  // Adds "msg-" prefix
        message.role.as_str().to_string(),
        name.to_string(),
    ])
```

**Resolution** (`cone/storage.rs:467`):
```rust
// Expects: "msg-{uuid}:{role}:{name}"
let parts: Vec<&str> = identifier.splitn(3, ':').collect();
let message_id_str = &msg_part[4..]; // Strips "msg-" prefix
```

The resolver must know the exact format created by the creator.

#### 3. Hardcoded Plugin Dispatch

Context building hardcodes plugin names:
```rust
match handle.plugin.as_str() {
    "cone" => { /* resolve cone message */ }
    "bash" => { /* TODO */ }
    _ => { /* unknown - display as reference */ }
}
```

No generic resolution mechanism.

#### 4. Database Storage

Handles stored as separate columns in `nodes` table:
```sql
handle_plugin TEXT,
handle_version TEXT,
handle_method TEXT,
handle_meta TEXT,  -- JSON array
```

Reconstructed on read - no schema validation.

## Proposed Improvements

### Option A: Typed Handle Variants

Replace string-based meta with typed variants:

```rust
pub enum HandleData {
    Message {
        message_id: Uuid,
        role: MessageRole,
        name: String,
    },
    Command {
        command_id: Uuid,
        exit_code: Option<i32>,
    },
    // Future variants...
}

pub struct Handle {
    pub plugin: String,
    pub version: String,
    pub method: String,
    pub data: HandleData,
}
```

**Pros:**
- Type safety at compile time
- Clear contract between creator and resolver
- Self-documenting

**Cons:**
- Breaking change to database schema
- Every new plugin type needs enum variant
- Centralized knowledge of all handle types

### Option B: Schema-Validated Meta

Keep `meta: Vec<String>` but add schema:

```rust
pub struct HandleSchema {
    pub plugin: &'static str,
    pub method: &'static str,
    pub meta_fields: &'static [MetaField],
}

pub struct MetaField {
    pub name: &'static str,
    pub required: bool,
    pub prefix: Option<&'static str>,  // e.g., "msg-"
}

// Cone declares its schema
const CONE_CHAT_SCHEMA: HandleSchema = HandleSchema {
    plugin: "cone",
    method: "chat",
    meta_fields: &[
        MetaField { name: "message_id", required: true, prefix: Some("msg-") },
        MetaField { name: "role", required: true, prefix: None },
        MetaField { name: "name", required: false, prefix: None },
    ],
};
```

**Pros:**
- Backward compatible
- Each plugin owns its schema
- Validation without centralization

**Cons:**
- Runtime validation instead of compile-time
- Schema must be registered somewhere

### Option C: Simplify - Remove Prefixes

Simplest fix: remove the `msg-` prefix complexity.

**Current:**
```
meta: ["msg-550e8400", "user", "bob"]
```

**Proposed:**
```
meta: ["550e8400", "user", "bob"]
```

Resolution just parses `meta[0]` as UUID directly.

**Pros:**
- Minimal change
- No format coupling
- Each plugin interprets its own meta

**Cons:**
- Loses human-readable type hint in handle string
- Migration needed for existing data

### Option D: Unified Resolution Through Plexus

Instead of each consumer hardcoding plugin dispatch, use Plexus:

```rust
// Current (in cone/activation.rs)
match handle.plugin.as_str() {
    "cone" => storage.resolve_message_handle(...),
    "bash" => { /* inline logic */ },
    _ => { /* fallback */ }
}

// Proposed
let resolved = plexus.resolve_handle(&handle).await?;
// Returns standardized ResolvedContent enum
```

**Pros:**
- Single resolution path
- Plugins register their own resolvers
- Consumers don't need plugin knowledge

**Cons:**
- Requires Plexus reference in context builders
- Standardized return type needed

## Recommendation

**Phase 1 (Immediate):**
1. Remove duplicate Handle from `arbor/types.rs`
2. Re-export from `crate::types::Handle` everywhere
3. Remove `msg-` prefix from new handles (migrate existing)

**Phase 2 (Short-term):**
1. Implement Option D - route all resolution through Plexus
2. Add `ResolvedContent` enum for standardized returns
3. Remove hardcoded plugin dispatch from consumers

**Phase 3 (Future):**
1. Consider Option B schemas if validation becomes important
2. Add handle versioning for backward compatibility

## Files to Change

### Phase 1
- `src/activations/arbor/types.rs` - Remove Handle, re-export from types
- `src/activations/arbor/mod.rs` - Update re-exports
- `src/activations/cone/storage.rs` - Remove "msg-" prefix
- `src/activations/claudecode/storage.rs` - Remove "msg-" prefix
- Migration script for existing database handles

### Phase 2
- `src/plexus/plexus.rs` - Add `resolve_handle` to Plexus API
- `src/plexus/types.rs` - Add `ResolvedContent` enum
- `src/activations/cone/activation.rs` - Use Plexus for resolution
- `src/activations/claudecode/activation.rs` - Use Plexus for resolution
