# Handle Resolution and Rendering System

## Overview

This document defines the architecture for handle resolution and value rendering in Plexus. The system provides:

1. **Plugin Identity** - UUID-based plugin identification for stable handle routing
2. **Handle Resolution** - Lazy value resolution with provenance tracking
3. **Mustache Templating** - Runtime-configurable rendering via a dedicated plugin
4. **Default Rendering** - Plugin-defined default templates stored in database

## Core Algebra

```
method() → Envelope<Handle | Value>
              ↓
         resolve (if Handle)
              ↓
         Envelope<Value>
              ↓
         render (via mustache plugin)
              ↓
           Text
```

## Plugin Identity System

### Plugin UUID

Every plugin instance has a stable UUID:

```rust
#[hub_activation(
    name = "cone",
    plugin_id = "550e8400-e29b-41d4-a716-446655440000",  // explicit
)]
struct Cone { ... }

#[hub_activation(name = "echo")]  // auto-generated UUID
struct Echo { ... }
```

The `plugin_id`:
- Is stable across restarts
- Uniquely identifies plugin instances
- Enables handle routing without path dependency
- Stored in Plexus registry: `plugin_id → current_path`

### Hub Macro Changes

```rust
// Generated by hub-macro
impl Cone {
    pub const PLUGIN_ID: Uuid = uuid!("550e8400-e29b-41d4-a716-446655440000");

    pub fn plugin_id(&self) -> Uuid {
        Self::PLUGIN_ID
    }
}
```

Auto-generation when not specified:
- Derive from plugin name + version hash (deterministic)
- Or generate once and store in config file

## Handle Structure

```rust
struct Handle {
    /// Stable plugin instance identifier
    plugin_id: Uuid,

    /// Method that created this handle (for schema/type lookup)
    method: String,

    /// Address within the plugin's storage
    meta: Vec<String>,
}

impl Display for Handle {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        // Format: {plugin_id}::{method}:meta[0]:meta[1]:...
        write!(f, "{}::{}", self.plugin_id, self.method)?;
        for m in &self.meta {
            write!(f, ":{}", m)?;
        }
        Ok(())
    }
}
```

## Envelope Pattern

Values and handles carry provenance through the pipeline:

```rust
/// Wrapper that carries rendering provenance
struct Envelope<T> {
    /// Who created this / knows how to render it
    origin: Origin,

    /// The actual data
    data: T,
}

struct Origin {
    /// Plugin that can render this value
    plugin_id: Uuid,

    /// Method context (for schema lookup)
    method: String,
}
```

### Flow

1. **Method returns** - Values wrapped with method's origin
2. **Handle in event** - Handle carries its own origin (plugin_id, method)
3. **Resolution** - Produces `Envelope<Value>` preserving handle's origin
4. **Rendering** - Uses origin to find template, calls mustache plugin

## Mustache Plugin

A dedicated top-level plugin for template rendering:

```rust
#[hub_activation(
    name = "mustache",
    plugin_id = "00000000-0000-0000-0000-000000000001",  // well-known ID
)]
struct Mustache {
    storage: MustacheStorage,  // SQLite for templates
}

#[hub_method]
impl Mustache {
    /// Render a value using a template
    async fn render(
        &self,
        plugin_id: Uuid,     // which plugin's template
        method: String,      // which method's output
        template_name: Option<String>,  // specific template or "default"
        value: Value,
    ) -> String;

    /// Register a template for a plugin/method
    async fn register_template(
        &self,
        plugin_id: Uuid,
        method: String,
        name: String,        // e.g., "default", "compact", "verbose"
        template: String,    // mustache template
    );

    /// List templates for a plugin
    async fn list_templates(
        &self,
        plugin_id: Uuid,
    ) -> Vec<TemplateInfo>;

    /// Get a specific template
    async fn get_template(
        &self,
        plugin_id: Uuid,
        method: String,
        name: String,
    ) -> Option<String>;
}
```

### Template Storage Schema

```sql
CREATE TABLE templates (
    id TEXT PRIMARY KEY,
    plugin_id TEXT NOT NULL,      -- UUID of plugin
    method TEXT NOT NULL,         -- method name
    name TEXT NOT NULL,           -- template name (e.g., "default")
    template TEXT NOT NULL,       -- mustache template content
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL,

    UNIQUE(plugin_id, method, name)
);

CREATE INDEX idx_templates_plugin ON templates(plugin_id);
CREATE INDEX idx_templates_lookup ON templates(plugin_id, method, name);
```

### Default Templates

Plugins register their default templates on initialization:

```rust
impl Cone {
    async fn register_default_templates(&self, mustache: &Mustache) {
        // Chat message template
        mustache.register_template(
            Self::PLUGIN_ID,
            "chat",
            "default",
            "[{{role}}]: {{content}}"
        ).await;

        // Verbose template
        mustache.register_template(
            Self::PLUGIN_ID,
            "chat",
            "verbose",
            "--- {{role}} ({{model}}) ---\n{{content}}\n---"
        ).await;
    }
}

impl Bash {
    async fn register_default_templates(&self, mustache: &Mustache) {
        mustache.register_template(
            Self::PLUGIN_ID,
            "execute",
            "default",
            "```\n$ {{command}}\n{{stdout}}{{#stderr}}\nSTDERR: {{stderr}}{{/stderr}}\n```"
        ).await;
    }
}
```

## Resolution and Rendering Flow

### Client-Side Rendering (External)

External clients (Synapse, UI) control their own rendering:

```
1. Call method → Stream<Event>
2. Extract handles from events
3. For custom rendering:
   - Call plexus.resolve(handle) → Value
   - Apply client's own template
4. For default rendering:
   - Call plexus.render(handle) → Text
   - Or: Call mustache.render(origin, value) directly
```

### Plugin-Side Rendering (Internal)

Plugins like Cone that build LLM context:

```rust
impl Cone {
    async fn build_context(&self, tree_id: TreeId, head: NodeId) -> Vec<Message> {
        let nodes = self.arbor.get_path(tree_id, head).await?;
        let mustache = self.plexus.get_plugin::<Mustache>()?;

        let mut messages = Vec::new();
        for node in nodes {
            let text = match &node.data {
                NodeData::Text { content } => content.clone(),
                NodeData::External { handle } => {
                    // Resolve handle to value
                    let resolved = self.plexus.resolve(handle).await?;

                    // Render using default template
                    mustache.render(
                        resolved.origin.plugin_id,
                        resolved.origin.method,
                        None,  // use "default"
                        resolved.value,
                    ).await?
                }
            };
            messages.push(Message { role: "user", content: text });
        }
        messages
    }
}
```

### Plexus-Level Convenience Methods

```rust
impl Plexus {
    /// Resolve a handle to its value
    async fn resolve(&self, handle: &Handle) -> Result<Envelope<Value>> {
        let plugin_path = self.registry.lookup(handle.plugin_id)?;
        let value = self.call(&plugin_path, "resolve_handle", handle).await?;

        Ok(Envelope {
            origin: Origin {
                plugin_id: handle.plugin_id,
                method: handle.method.clone(),
            },
            data: value,
        })
    }

    /// Resolve and render with default template
    async fn render(&self, handle: &Handle) -> Result<String> {
        let resolved = self.resolve(handle).await?;
        let mustache = self.get_plugin::<Mustache>()?;

        mustache.render(
            resolved.origin.plugin_id,
            resolved.origin.method,
            None,
            resolved.data,
        ).await
    }

    /// Render an already-resolved value
    async fn render_value(&self, envelope: Envelope<Value>) -> Result<String> {
        let mustache = self.get_plugin::<Mustache>()?;

        mustache.render(
            envelope.origin.plugin_id,
            envelope.origin.method,
            None,
            envelope.data,
        ).await
    }
}
```

## Plugin Registry

Maps plugin UUIDs to current paths:

```rust
struct PluginRegistry {
    by_id: HashMap<Uuid, PluginEntry>,
    by_path: HashMap<String, Uuid>,
}

struct PluginEntry {
    id: Uuid,
    path: String,
    plugin_type: String,  // "cone", "bash", etc.
}

impl PluginRegistry {
    fn lookup(&self, id: Uuid) -> Option<&str> {
        self.by_id.get(&id).map(|e| e.path.as_str())
    }

    fn register(&mut self, id: Uuid, path: String, plugin_type: String) {
        self.by_id.insert(id, PluginEntry { id, path: path.clone(), plugin_type });
        self.by_path.insert(path, id);
    }
}
```

## Runtime Configuration

Templates can be modified at runtime:

```bash
# List templates for a plugin
synapse mustache list_templates --plugin-id "550e8400-..."

# Update a template
synapse mustache register_template \
    --plugin-id "550e8400-..." \
    --method "chat" \
    --name "default" \
    --template "[{{role}}] {{content}}"

# Use a specific template
synapse mustache render \
    --plugin-id "550e8400-..." \
    --method "chat" \
    --template-name "verbose" \
    --value '{"role": "user", "content": "hello"}'
```

## Implementation Phases

### Phase 1: Plugin ID System
- Add `plugin_id` to hub-macro attributes
- Generate UUID constant in plugin impl
- Add `plugin_id()` method to Activation trait
- Create PluginRegistry in Plexus
- Update plugin registration to populate registry

### Phase 2: Handle Updates
- Update Handle struct to use plugin_id instead of plugin name
- Update handle parsing/display
- Update resolve routing to use registry lookup
- Add Envelope and Origin types

### Phase 3: Mustache Plugin
- Create mustache activation with SQLite storage
- Implement render, register_template, list_templates, get_template
- Add as core plugin in Plexus builder

### Phase 4: Default Template Registration
- Add register_default_templates to plugins with handles
- Call during plugin initialization
- Update Cone, Bash, ClaudeCode with default templates

### Phase 5: Integration
- Add resolve/render convenience methods to Plexus
- Update Cone context building to use mustache
- Update ClaudeCode context building
- Add CLI commands to Synapse for template management

## Migration

Existing handles use plugin name. Migration path:

1. Registry supports lookup by name OR UUID (transition period)
2. New handles use UUID
3. Old handles resolved via name → UUID lookup
4. Eventually deprecate name-based lookup

## Security Considerations

- Templates are stored per-plugin, isolated by plugin_id
- Only the plugin that owns a template can modify it (or admin)
- Template rendering is sandboxed (mustache is logic-less)
- Handle resolution validates plugin_id exists in registry

---

## Implementation Notes

### What Was Implemented

The implementation covers all five phases:

1. **Plugin ID System** (`hub-macro/src/codegen/activation.rs`, `src/plexus/plexus.rs:291`)
   - Deterministic UUID v5 from `namespace@version` when not explicitly provided
   - `PLUGIN_ID` constant generated in each plugin
   - `PluginRegistry` populated during `register()` and `register_hub()`

2. **Handle/Envelope Types** (`src/types.rs`)
   - `Handle { plugin_id, version, method, meta, plugin_name }`
   - `Origin { plugin_id, method }` for provenance
   - `Envelope<T> { origin, data }` for wrapped values

3. **Mustache Plugin** (`src/activations/mustache/`)
   - SQLite storage with (plugin_id, method, name) key
   - Streaming RPC methods + direct `register_template_direct()` for init

4. **Default Templates** (`src/activations/cone/activation.rs:52`, `src/activations/bash/activation.rs:19`)
   - `register_default_templates(&self, mustache)` pattern

5. **Registry Integration** (`src/plexus/plexus.rs:569`)
   - `registry_snapshot()`, `lookup_plugin()`, `lookup_plugin_by_path()`

---

## Shortcuts and Technical Debt

### 1. Dual Handle Identity (plugin_id + plugin_name)

**Shortcut**: We kept `plugin_name: Option<String>` for backwards compatibility.

```rust
// Current: redundant identity
pub struct Handle {
    pub plugin_id: Uuid,              // The "real" identity
    pub plugin_name: Option<String>,  // Legacy fallback
    ...
}
```

**Issue**: Creates ambiguity about which field is authoritative. Resolution falls back to name-based lookup if UUID not found.

**Fix**: After migration, remove `plugin_name` and use registry exclusively. The database should migrate `handle_plugin` column to store UUIDs.

### 2. Arbor Still Stores Plugin Names

**Shortcut**: Database column `handle_plugin` stores string names, not UUIDs.

```rust
// src/activations/arbor/storage.rs:851
.bind(handle.plugin_name.as_deref().unwrap_or(&handle.plugin_id.to_string()))
```

**Issue**: Reconstructing Handle from DB requires name→UUID conversion.

**Fix**: Add `handle_plugin_id` column to arbor nodes table, migrate existing data.

### 3. Template Registration Not Automated

**Shortcut**: Plugins must explicitly call `register_default_templates()`.

**Issue**: Easy to forget, templates not registered until called.

**Fix**: Add `default_templates()` method to `Activation` trait:

```rust
trait Activation {
    fn default_templates(&self) -> &[(&str, &str, &str)] { &[] }
}
```

Then Plexus auto-registers during `register()`.

### 4. No Envelope in Stream Events

**Shortcut**: Methods return `Stream<Event>`, not `Stream<Envelope<Event>>`.

**Issue**: Provenance is lost unless handles explicitly carry it.

**Fix**: Consider:
```rust
// Option A: Envelope per event
async fn chat(&self, ...) -> impl Stream<Item = Envelope<ConeEvent>>

// Option B: Origin in stream metadata (current approach via wrap_stream)
wrap_stream(stream, "cone.chat", vec!["cone".into()])
```

Current `wrap_stream` passes origin info, but it's metadata not typed.

### 5. Mustache Plugin Not Auto-Registered

**Shortcut**: `build_plexus()` doesn't include Mustache by default.

**Issue**: Template features unavailable unless explicitly added.

**Fix**: Add to default plugins or make optional with feature flag.

### 6. No Schema Integration

**Shortcut**: Templates render JSON values, but schemas not consulted.

**Issue**: Templates might reference fields that don't exist, no validation.

**Possible improvement**: Validate templates against method return schema:
```rust
mustache.register_template_validated(
    plugin_id, method, name, template, schema
)
```

---

## Code Deduplication Patterns

### 1. Template Registration Trait

Current duplication:
```rust
// Cone
pub async fn register_default_templates(&self, mustache: &Mustache) -> Result<(), String> {
    mustache.register_templates(Self::PLUGIN_ID, &[...]).await
}

// Bash (identical pattern)
pub async fn register_default_templates(&self, mustache: &Mustache) -> Result<(), String> {
    mustache.register_templates(Self::PLUGIN_ID, &[...]).await
}
```

**Pattern**: Extract to trait or macro:

```rust
pub trait HasTemplates: Activation {
    fn templates() -> &'static [(&'static str, &'static str, &'static str)];

    async fn register_templates(&self, mustache: &Mustache) -> Result<(), String> {
        mustache.register_templates(Self::PLUGIN_ID, Self::templates()).await
    }
}

// Usage
impl HasTemplates for Cone {
    fn templates() -> &'static [(&'static str, &'static str, &'static str)] {
        &[
            ("chat", "default", "[{{role}}] {{content}}"),
            ("chat", "markdown", "**{{role}}**\n\n{{content}}"),
        ]
    }
}
```

### 2. Handle Construction

Current duplication in storage modules:
```rust
// cone/storage.rs
Handle::from_name("cone", "1.0.0", "chat").with_meta(vec![...])

// claudecode/storage.rs
Handle::from_name("claudecode", "1.0.0", "chat").with_meta(vec![...])
```

**Pattern**: Associated function on plugin:

```rust
impl Cone {
    pub fn handle(method: &str, meta: Vec<String>) -> Handle {
        Handle::new(Self::PLUGIN_ID, Self::VERSION, method).with_meta(meta)
    }
}

// Usage
Cone::handle("chat", vec![msg_id, role, name])
```

### 3. Plugin Resolution Fallback

Current pattern repeated:
```rust
let plugin = handle.plugin_name.as_deref().unwrap_or("unknown");
match plugin { ... }
```

**Pattern**: Method on Handle:

```rust
impl Handle {
    pub fn plugin_name_or_id(&self) -> String {
        self.plugin_name.clone().unwrap_or_else(|| self.plugin_id.to_string())
    }
}
```

### 4. Registry Lookup + Activation Fetch

Current pattern:
```rust
let registry = self.inner.registry.read().unwrap();
let plugin_path = registry.lookup(handle.plugin_id)
    .map(|s| s.to_string())
    .or_else(|| handle.plugin_name.clone());
drop(registry);

let path = plugin_path.ok_or_else(|| ...)?;
let activation = self.inner.activations.get(&path).ok_or_else(|| ...)?;
```

**Pattern**: Single method:

```rust
impl Plexus {
    fn get_activation_for_handle(&self, handle: &Handle) -> Result<&dyn ActivationObject, PlexusError> {
        // encapsulates registry lookup + fallback + activation fetch
    }
}
```

---

## Missing Pieces

### 1. Plexus-Level render() Method

The arch doc describes `Plexus::render(handle)` but it's not implemented. We have:
- `do_resolve_handle()` - resolves handle to stream
- `lookup_plugin()` - finds plugin path
- But no direct resolve→render convenience method

### 2. Context Building Integration

`Cone::build_context()` from the arch doc not implemented. Current code in `resolve_context_to_messages()` does inline resolution without mustache rendering.

### 3. Template Inheritance / Fallbacks

No fallback chain:
```
plugin/method/custom → plugin/method/default → plugin/*/default → global/default
```

### 4. Template Validation

Templates not validated against schemas. Invalid templates fail at render time.

### 5. Batch Resolution

No batch resolve for multiple handles:
```rust
async fn resolve_batch(&self, handles: &[Handle]) -> Vec<Result<Envelope<Value>>>
```

---

## Suggested Improvements Priority

| Priority | Item | Effort | Impact |
|----------|------|--------|--------|
| High | Auto-register templates in `register()` | Low | Reduces boilerplate |
| High | Add `Plexus::render(handle)` | Low | Completes the API |
| Medium | Database UUID migration | Medium | Removes dual identity |
| Medium | HasTemplates trait | Low | Deduplication |
| Medium | `Handle::for_method()` pattern | Low | Cleaner handle creation |
| Low | Template validation | Medium | Better error messages |
| Low | Batch resolution | Medium | Performance for context |

---

## File Reference

Key implementation files:

| File | Purpose |
|------|---------|
| `hub-macro/src/codegen/activation.rs` | PLUGIN_ID generation |
| `src/types.rs` | Handle, Origin, Envelope |
| `src/plexus/plexus.rs:291` | PluginRegistry, PluginEntry |
| `src/plexus/plexus.rs:502` | do_resolve_handle with registry |
| `src/activations/mustache/` | Template storage and rendering |
| `src/activations/arbor/types.rs` | Re-exports Handle from crate::types |
| `src/activations/cone/activation.rs:52` | register_default_templates example |
