Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1c4f272
chore: add org-workspace dependency, pytest config, test scaffolding
Mar 11, 2026
f92c726
feat: extract shared config module — single source for settings, trus…
Mar 11, 2026
41e1cf5
feat: add message store — org-workspace backed message CRUD with File…
Mar 11, 2026
d7c98b6
feat: implement agent inbox store with full task lifecycle
Mar 11, 2026
0bccfbb
refactor: extract shared _refresh_node, fix agent inbox review issues
Mar 11, 2026
66cc704
feat: add task governor — trust tiers, per-sender budgets, rate limiting
Mar 11, 2026
c04f8e0
fix: make check_and_record atomic, add queue depth check, fix config …
Mar 11, 2026
72dfd68
feat: consolidate relay to single lib/relay.py — fix auth leak, -h co…
Mar 11, 2026
294e797
refactor: rewire all hooks to use lib modules — remove duplicated cod…
Mar 11, 2026
9bf051d
test: add integration tests — message-to-task flow, governance blocks…
Mar 11, 2026
c7c9e49
docs: update module manifest, agent specs, and CLAUDE.md to match v0.…
Mar 11, 2026
b4fdc34
chore: cleanup — remove legacy files, add contacts template, update R…
Mar 11, 2026
6b8767d
fix: governor concurrency — atomic writes, reload-inside-lock for all…
Mar 11, 2026
4959f37
fix: remove workspace exposure, add ID monotonic counter, validate pr…
Mar 11, 2026
f838631
docs: fix stale references, add migration guide, update module.yaml f…
Mar 11, 2026
11b53dc
fix: address iteration 2 review — stale settings example, governor re…
Mar 11, 2026
b2d234c
docs: add Phase 1 implementation plan
Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 160 additions & 59 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Messaging Module Context

This module adds inter-user messaging to Datacore via shared space inboxes.
This module adds inter-user and human-to-agent messaging to Datacore via org-workspace backed storage.

## Overview

Messages are org-mode entries stored in `[space]/org/inboxes/[username].org`. Users send messages with `/msg`, read with `/my-messages`, and reply with `/reply`.
Messages are org-mode entries managed by `lib/message_store.py` and stored in `[space]/org/messaging/inbox.org`. Users send messages with `/msg`, read with `/my-messages`, and reply with `/reply`. Messages addressed to `@claude` are routed as agent tasks with trust tier enforcement.

## Key Concepts

Expand All @@ -18,37 +18,98 @@ identity:
handles: ["@gregor", "@gz"] # Aliases for receiving messages
```

### Inbox Location
### Storage Layout

```
[space]/org/inboxes/
├── USERS.yaml # Registry of all users and handles
├── gregor.org # Gregor's inbox
├── crt.org # Črt's inbox
└── claude.org # AI task inbox (special)
[space]/org/messaging/
├── inbox.org # Universal inbox (all incoming messages and file deliveries)
├── outbox.org # Sent message log
└── agents/
├── gregor-claude.org # Gregor's Claude agent task inbox
└── crt-claude.org # Črt's Claude agent task inbox
```

Agent inbox filenames are `{username}-claude.org` — one per user. There is no generic `claude.org`.

### Message Format

Messages in `inbox.org` use TODO state with `:message:` and `:unread:` tags:

```org
* MESSAGE [2025-12-11 Thu 13:00] :unread:
* TODO [2025-12-11 Thu 13:00] :unread:message:
:PROPERTIES:
:ID: msg-{timestamp}-{sender}
:FROM: sender_name
:TO: recipient_name
:PRIORITY: normal|high|low
:THREAD: nil|parent_message_id
:ID: msg-20251211-130000-a1b2c3d4
:FROM: sender_name
:TO: recipient_name
:REPLY_TO: nil
:THREAD: nil
:END:
Message content here.
```

File deliveries use the `:file_delivery:` tag instead of `:message:`.

### State Machine

Messages and tasks go through org-workspace states:

**Messages** (`inbox.org`):
- `TODO` (unread) → `DONE` (read) → `ARCHIVED`

**Agent tasks** (`agents/{username}-claude.org`):
- `WAITING` → `QUEUED` → `WORKING` → `DONE` → `ARCHIVED`
- Terminal states: `CANCELLED`, `ARCHIVED`

### Tags

- `:unread:` - Not yet viewed
- `:read:` - Viewed by recipient
- `:replied:` - Recipient has replied
- `:from-ai:` - Response from Claude
- `:AI:` - Task for AI processing (claude.org only)
- `:unread:` + `TODO` state — not yet viewed
- `:message:` — standard text message
- `:file_delivery:` — file delivered via Fairdrop/Swarm
- `:AI:` — task for agent processing
- `:AI:research:`, `:AI:content:`, `:AI:data:`, `:AI:pm:` — routed subtypes

## lib/ Modules

All message operations go through the lib modules. No direct file manipulation.

| Module | Purpose |
|--------|---------|
| `lib/config.py` | Settings, identity, trust tier resolution, compute config |
| `lib/message_store.py` | CRUD for `inbox.org` — create, find, mark_read, archive |
| `lib/agent_inbox.py` | Task lifecycle for `agents/{username}-claude.org` |
| `lib/governor.py` | Trust tier enforcement, token budgets, rate limits |
| `lib/relay.py` | WebSocket relay client for cross-instance messaging |

### MessageStore

```python
store = MessageStore(space_root)
msg = store.create_message(from_actor="gregor", to_actor="crt", content="Hello")
unread = store.find_unread() # reloads from disk
store.mark_read(unread[0]) # TODO -> DONE
store.archive(unread[0]) # DONE -> ARCHIVED
```

### AgentInbox

```python
inbox = AgentInbox(space_root, "gregor-claude")
task = inbox.create_task(from_actor="gregor", content="Research X", trust_tier="owner", tags=["AI", "research"])
queued = inbox.find_by_state("QUEUED")
inbox.claim(queued[0]) # QUEUED -> WORKING
inbox.complete(queued[0], tokens_used=1200) # WORKING -> DONE
```

### TaskGovernor

```python
gov = TaskGovernor(state_dir=Path(".datacore/state/messaging"))
result = gov.check_and_record("gregor", estimated_tokens=5000, effort=3)
if result.allowed:
# create task
gov.record_usage("gregor", tokens=1150)
gov.record_task_completion("gregor")
```

## Commands

Expand All @@ -57,9 +118,9 @@ Message content here.
Send a message to another user.

**Resolution order for recipient:**
1. Check USERS.yaml for handle → username mapping
1. Check `contacts.yaml` for handle → username mapping
2. If not found, treat handle as username
3. If user doesn't exist, create inbox and add to USERS.yaml
3. If user doesn't exist in contacts, create inbox entry and add to `contacts.yaml`

**Space selection:**
1. Explicit: `--space datafund`
Expand All @@ -71,68 +132,108 @@ Send a message to another user.
Display inbox for current user.

**Steps:**
1. Read `identity.name` from settings
2. Find all `*/org/inboxes/{name}.org` files
3. Parse org entries, filter by tags
4. Display grouped by space, sorted by time
1. Read `identity.name` from settings via `lib/config.get_username()`
2. Open `[space]/org/messaging/inbox.org` via `MessageStore`
3. Call `MessageStore.find_unread()` — returns org-workspace NodeViews
4. Display grouped by sender, sorted by timestamp

### /reply

Reply to a message, creating a thread.

**Steps:**
1. Find original message by ID (or "last")
2. Create new message with `THREAD` property set to original ID
3. Append to sender's inbox (reverse direction)
4. Add `:replied:` tag to original message
1. Find original message by ID (or "last") via `MessageStore.find_by_id()`
2. Create reply via `MessageStore.create_message(reply_to=original_id)`
3. `REPLY_TO` and `THREAD` properties are set automatically by `message_store.py`
4. Mark original as read via `MessageStore.mark_read()`

## Claude Integration

Messages to `@claude` are special:
1. Stored in `[space]/org/inboxes/claude.org`
2. Tagged with `:AI:` for ai-task-executor
3. Agent processes and sends reply to sender's inbox
4. Reply tagged `:from-ai:`
Messages to `@claude` follow the agent task pathway:
1. Message arrives in `org/messaging/inbox.org` with `:AI:` tag
2. `inbox-watcher.py` hook routes it to the `AgentInbox` for `{username}-claude`
3. Trust tier checked by `TaskGovernor` — auto-accept for owner/team, WAITING for others
4. Task entered as QUEUED or WAITING in `org/messaging/agents/{username}-claude.org`
5. Agent claims task (QUEUED → WORKING), executes, completes
6. Reply sent back to sender via `MessageStore.create_message()`

## File Operations
See `agents/message-task-intake.md` for full agent specification.

### Creating a message

```python
# Append to recipient's inbox
with open(f"{space}/org/inboxes/{recipient}.org", "a") as f:
f.write(message_org_format)
```

### Marking as read

Replace `:unread:` tag with `:read:` in the heading line.

### User registry
## Contacts Registry

```yaml
# USERS.yaml
users:
username:
handles: ["@handle1", "@handle2"]
added: YYYY-MM-DD
# contacts.yaml
contacts:
gregor:
handles: ["@gregor", "@gz"]
relay: "wss://relay.example.com/ws"
added: "2025-12-11"
crt:
handles: ["@crt"]
relay: ""
added: "2025-12-11"
Comment on lines +162 to +174
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documented contacts.yaml structure here uses a top-level contacts: mapping, but the new template/migration guide uses a top-level actors: list with id, name, and trust_tier. This mismatch will confuse users and implementers; update the example/schema to match templates/contacts.yaml and UPGRADING.md.

Copilot uses AI. Check for mistakes.
```

Previously `USERS.yaml` — renamed to `contacts.yaml` in v0.2.0.

## Settings Schema

```yaml
identity:
name: string # Required
handles: [string] # Optional, defaults to ["@{name}"]
name: string # Required
handles: [string] # Optional, defaults to ["@{name}"]

messaging:
default_space: string # Optional, defaults to first team space
show_in_today: bool # Optional, defaults to true
auto_mark_read: bool # Optional, defaults to false
default_space: string # Optional, defaults to "0-personal"
show_in_today: bool # Optional, defaults to true
auto_mark_read: bool # Optional, defaults to false

relay:
url: string # WebSocket relay URL
secret: string # Relay auth secret

trust_tiers: # Per-tier config overrides
owner:
priority_boost: 2.0
daily_token_limit: 0 # 0 = unlimited
max_task_effort: 0 # 0 = unlimited
auto_accept: true
team:
priority_boost: 1.5
daily_token_limit: 100000
max_task_effort: 8
auto_accept: true
trusted:
priority_boost: 1.0
daily_token_limit: 50000
max_task_effort: 5
auto_accept: false
unknown:
priority_boost: 0.5
daily_token_limit: 10000
max_task_effort: 3
auto_accept: false

trust_overrides: # Per-actor tier assignments
crt: team
external-partner: trusted

compute:
daily_budget_tokens: 500000
per_sender_daily_max: 100000
per_task_max_tokens: 50000
per_task_timeout_minutes: 30
max_queue_depth: 20
rate_limits:
tasks_per_hour: 5

inbox_feeds: [] # External relay WebSocket endpoints to poll
```

## Error Handling

- **Unknown recipient**: Create inbox, add to USERS.yaml, warn user
- **Unknown recipient**: Create inbox entry, add to `contacts.yaml`, warn user
- **No identity configured**: Prompt to add `identity.name` to settings
- **Space not found**: List available spaces, ask user to specify
- **Governance rejected**: Reply to sender with rejection reason and current budget status
- **Relay unreachable**: Queue messages locally, retry on next connection
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
web: python lib/datacore-msg-relay.py
web: python -m lib.relay --host
Loading