Skip to main content

Message Types

hcom supports two message scopes:

Broadcast

Sent to all active agents (no @mentions)

Mentions

Sent to specific agents via @mention or explicit targets

Message Structure

Messages are stored as JSON events in the database:
{
  "from": "luna",
  "text": "hey @nova can you help?",
  "scope": "mentions",
  "mentions": ["nova"],
  "sender_kind": "instance",
  "delivered_to": ["nova"],
  "intent": "request",
  "thread": "pr-123",
  "reply_to": "42",
  "bundle_id": "bundle:abc123"
}

Message Intents

Intents provide semantic context:
Expect a response from the recipient.
hcom send @nova --intent request -- can you review this?
Creates request-watch subscription to notify sender when recipient goes idle.

Scope Computation

Target Matching

Target matching uses 3-tier priority:
  1. Full name prefix match (with tag): api-luna matches api-luna
  2. Tag prefix match: api- matches all with tag api
  3. Base name prefix match: luna matches api-luna
// Example instances
// - luna (no tag)
// - api-luna (tag: api)
// - api-nova (tag: api)

@luna       → ["luna", "api-luna"]  // Matches both via base name
@api-luna   → ["api-luna"]           // Exact full name
@api-       → ["api-luna", "api-nova"] // Tag prefix

Underscore Blocking

Underscore prevents accidental prefix matches:
// Instances: luna, luna_reviewer_1

@luna → ["luna"]  // Does NOT match luna_reviewer_1

Cross-Device Matching

Remote agents have device suffix:
// Local: luna
// Remote: luna:BOXE (device ID: BOXE)

@luna      → ["luna", "luna:BOXE"]  // Matches both
@luna:BOXE → ["luna:BOXE"]          // Remote only

Message Delivery

Delivery Flow

1

Send command

User runs hcom send @nova -- hello
2

Scope computation

Parse @mentions and resolve to instance names
3

Event insertion

Insert message event into events table
4

Recipient wake

Notify TCP endpoints for each delivered_to instance
5

Hook polling

Recipient’s hook polls for events > last_event_id
6

Filtering

Hook filters messages by scope and mentions
7

Injection

Hook injects formatted message into agent context
8

Cursor update

Agent updates last_event_id after processing

Delivery Modes

PTY agents have delivery threads that listen on notify ports:
// Register notify endpoint
db.register_notify_port("luna", 50123);

// Delivery thread polls and injects
loop {
    let messages = db.get_unread_messages("luna");
    if !messages.is_empty() {
        inject_into_pty("luna", messages);
    }
    
    // Wait for TCP wake or timeout
    tcp_listener.accept().await;
}
Latency: < 100ms

Filtering

Messages are filtered during delivery:
pub fn should_deliver_message(
    event_data: &Value,
    receiver_name: &str,
    sender_name: &str,
) -> Result<bool, String> {
    // Skip own messages
    if receiver_name == sender_name {
        return Ok(false);
    }
    
    let scope = event_data.get("scope").and_then(|v| v.as_str())?;
    
    match scope {
        "broadcast" => Ok(true),
        "mentions" => {
            let mentions = event_data.get("mentions").as_array()?;
            
            // Cross-device: strip device suffix for matching
            let receiver_base = receiver_name.split(':').next();
            Ok(mentions.iter().any(|m| {
                receiver_base == m.split(':').next()
            }))
        }
        _ => Ok(false),
    }
}

Message Formatting

Hook Injection Format

Messages injected into agent context:
let formatted = format_messages_json(&messages, "nova", ...);
// Output:
// <hcom>[request:pr-123 #42] luna → nova: can you review this?</hcom>
Format components:
  • Envelope: [intent:thread #id] or [new message #id]
  • Sender: Display name (with tag if set)
  • Recipient: Display name + others count
  • Text: Message content
  • Hints: Config hints appended

Multiple Messages

Batch formatting:
// Single message: verbose
[request #42] lunanova (+2 others): first message

// Multiple messages: compact
[3 new messages] | [request #42] lunanova (+1): msg1 | [inform #43] kiranova: msg2 | [ack #44] milanova: msg3

Remote Messages

Remote events include device ID:
// Local event ID: #42
// Remote event ID: #42:BOXE (device: BOXE)

[request #42:BOXE] luna:BOXEnova: hello from remote

Read Receipts

Track message delivery and read status:

Local Instances

Read via deliver events:
// After message sent, deliver events logged
let delivered = db.query(
    "SELECT instance FROM events 
     WHERE type='status' AND status_context='deliver:luna' 
     AND id > ?",
    [message_id]
)?;

// If deliver event exists after message → read

Remote Instances

Read via timestamp comparison:
// Message timestamp: 2024-01-01T12:00:00Z
// Remote instance last_msg_ts: 2024-01-01T12:05:00Z

if remote_msg_ts >= message_timestamp {
    // Message read by remote instance
}

Read Receipt Display

hcom list -v

# Output:
Name   Status     Unread  Read receipts
luna   listening  0       #42(2m): "hello" read by nova, kira [2/3]

Request-Watch Subscriptions

Automatic response tracking for request intent:
// When sending request
if intent == "request" {
    for recipient in delivered_to {
        let (sub_key, sub_value) = build_request_watch_sub(
            sender,
            message_id,
            recipient,
            last_event_id,
            now,
        );
        
        // Subscribe to:
        // - status=listening (recipient idle)
        // - life_action=stopped (recipient ended)
        db.kv_set(&sub_key, Some(&sub_value));
    }
}
When recipient goes idle or stops:
// Subscription fires → send notification
let notification = format!(
    "@{} {} is now {} (request #{})",
    sender, recipient, status, request_id
);

db.log_event("message", "sys_[hcom-events]", &notification);

Threading

Group related messages:
# Start thread
hcom send @nova --thread pr-123 -- can you review?

# Reply in thread
hcom send @luna --thread pr-123 --reply-to 42 -- looks good

# Query thread
hcom events --thread pr-123
Threads are purely organizational (no delivery impact).

Bundles

Attach structured context to messages:
# Create bundle
hcom bundle create "Review PR-123" \
  --description "Frontend changes" \
  --files src/app.tsx,tests/app.test.tsx \
  --transcript 10-15:normal \
  --events 100-120

# Attach to message
hcom send @nova --bundle bundle:abc123 -- please review
Bundles include:
  • Files (metadata + content)
  • Transcript ranges (with detail levels)
  • Event ranges (filtered log)
  • Chaining (extends previous bundle)

Validation

Message Validation

// Size check
if message.len() > 100_000 {
    return Err("Message too large");
}

// Control character check
for ch in message.chars() {
    if matches!(ch, '\x00'..='\x08' | '\x0B'..='\x0C' | '\x0E'..='\x1F') {
        return Err("Control characters not allowed");
    }
}

Scope Validation

Strict failure on unknown targets:
let unmatched: Vec<String> = targets
    .iter()
    .filter(|t| !match_target(t, instances).is_empty())
    .collect();

if !unmatched.is_empty() {
    return Err(format!(
        "@mentions to non-existent agents: {}",
        unmatched.join(", ")
    ));
}

System Mention Protection

// Prevent @[hcom-events] mentions
if message.contains("@[hcom-") {
    return Err(
        "System notifications cannot be mentioned"
    );
}

Performance

Message Delivery Latency

  • PTY agents: < 100ms (TCP notify + injection)
  • Polling agents: 30s average (Claude hook interval)
  • Cross-device: < 500ms (MQTT publish + sync)

Throughput

  • Events/sec: 1000+ (SQLite WAL mode)
  • Concurrent agents: 50+ (tested)
  • Message queue: Unlimited (event log is append-only)

Optimization

  • Indexes: (type, instance), (timestamp), (id)
  • FTS5: Full-text search on message content
  • Views: events_v for JSON extraction caching
  • Notify batching: Single TCP wake per batch

Build docs developers (and LLMs) love