Stage 1 Technical Spec: Event Capture Layer
- Stage 1 Technical Spec: Event Capture Layer
Stage 1 Technical Spec: Event Capture Layer
Depends on: Stage 0 validated (TASK-001 through TASK-004 complete)
Goal: Every agent action becomes a signed Nostr event in a local store. The user sees a timeline. Everything is undoable.
Language: Go (aligns with Paul’s stack and the Nostr ecosystem)
Primary dependency: github.com/nbd-wtf/go-nostr (canonical Go Nostr library by fiatjaf)
Overview
A lightweight daemon (tenex-eventd) runs inside the container alongside the agent. It watches the workspace for changes, captures agent actions, signs them as Nostr events, stores them in local SQLite, and serves a timeline API.
The user never sees the word “Nostr.” They see “timeline” and “undo.”
Architecture
┌──────────────────────────────────────────────────────┐
│ Container │
│ │
│ ┌──────────┐ FS events ┌──────────────────┐ │
│ │ OpenCode │ ──────────────── │ tenex-eventd │ │
│ │ (agent) │ git hooks │ │ │
│ └──────────┘ │ - fs watcher │ │
│ │ │ - git hook recv │ │
│ │ git commits │ - event signer │ │
│ ▼ │ - SQLite store │ │
│ ┌──────────┐ │ - timeline API │ │
│ │ git │ ── post-commit──▶│ │ │
│ │ (.git) │ └──────────────────┘ │
│ └──────────┘ │ │
│ │ HTTP :3001 │
│ ┌─────────▼──────────┐ │
│ │ Timeline UI │ │
│ │ (localhost:3001) │ │
│ └────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ events.db (SQLite) │ │
│ │ - Nostr events in NIP-01 format │ │
│ │ - Same format as relay-publishable events │ │
│ │ - Persisted via workspace volume mount │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Event Schema
All events follow NIP-01 format exactly. Custom semantics are encoded in kind and tags, not in non-standard fields. This means every event in the local store can be published to a relay with zero transformation.
Identity
On first run, tenex-eventd generates a keypair and stores it in the workspace volume:
/workspace/.tenex/
├── identity.json # { nsec: "...", npub: "...", pubkey_hex: "..." }
├── events.db # SQLite event store
└── config.json # Runtime config (checkpoint interval, etc.)
One keypair per workspace. The keypair persists across container restarts (lives in the volume mount). Different workspaces = different identities = different reputation histories.
Event Kinds
Use kind 30078 (application-specific data, parameterized replaceable) for all custom events initially. Differentiate by d tag and custom tags.
| Event | Kind | d tag |
Key tags | Content |
|---|---|---|---|---|
| Session start | 30078 | tenex:session:{id} |
["t", "session-start"], ["agent", "opencode"], ["model", "qwen2.5-coder:7b"] |
JSON: { recipe_hash, workspace_path, agent_version } |
| File change | 30078 | tenex:change:{hash} |
["t", "file-change"], ["path", "src/main.py"], ["action", "create|modify|delete"] |
JSON: { diff_summary, lines_added, lines_removed, reversible: true } |
| Command execution | 30078 | tenex:cmd:{hash} |
["t", "command"], ["cmd", "python main.py"], ["exit_code", "0"] |
JSON: { stdout_summary, stderr_summary, duration_ms } |
| Git checkpoint | 30078 | tenex:checkpoint:{commit} |
["t", "checkpoint"], ["commit", "{sha}"], ["auto", "true|false"] |
JSON: { message, files_changed, insertions, deletions } |
| Error | 30078 | tenex:error:{hash} |
["t", "error"], ["agent", "opencode"] |
JSON: { error_type, message, attempted_action, recoverable: true } |
| Session end | 30078 | tenex:session-end:{id} |
["t", "session-end"], ["e", "{session-start-event-id}"] |
JSON: { duration_s, actions_count, checkpoints_count, errors_count } |
Tag Conventions
ttags for type filtering (standard Nostr hashtag convention)etags for event references (standard NIP-01)pathfor file pathsagentfor agent identifiermodelfor model identifiercommitfor git commit SHArecipefor recipe hash (when recipes exist)
Capture Mechanisms
1. Git post-commit hook
Installed automatically by tenex-eventd on startup:
#!/bin/sh
# .git/hooks/post-commit
curl -s -X POST http://localhost:3001/api/hook/commit \
-H "Content-Type: application/json" \
-d "{\"commit\": \"$(git rev-parse HEAD)\", \"message\": \"$(git log -1 --pretty=%B)\"}"
This is the primary capture mechanism. Every git commit = a checkpoint event. Simple, reliable, works with any agent.
2. Filesystem watcher (inotify)
Watches /workspace for file creates, modifies, and deletes. Debounced (500ms) to batch rapid changes. Generates file-change events for changes that happen between git commits.
Implementation: Go’s fsnotify library.
3. Command execution capture (optional, Phase 1b)
If the agent runs commands through a monitored channel (e.g., if we wrap the shell), capture command + exit code + summary of output. This is lower priority than git hooks and FS watching.
SQLite Schema
CREATE TABLE events (
id TEXT PRIMARY KEY, -- Nostr event ID (hex)
pubkey TEXT NOT NULL, -- Author pubkey (hex)
created_at INTEGER NOT NULL, -- Unix timestamp
kind INTEGER NOT NULL, -- Nostr event kind
tags TEXT NOT NULL, -- JSON array of tag arrays
content TEXT NOT NULL, -- Event content (JSON string)
sig TEXT NOT NULL, -- Schnorr signature (hex)
-- Local metadata (not part of the Nostr event, not published)
local_session_id TEXT, -- Groups events by session
local_reversible BOOLEAN, -- Can this action be undone?
local_reverted BOOLEAN DEFAULT FALSE -- Has this been undone?
);
CREATE INDEX idx_events_kind ON events(kind);
CREATE INDEX idx_events_created ON events(created_at);
CREATE INDEX idx_events_session ON events(local_session_id);
-- For fast tag-based queries
CREATE TABLE event_tags (
event_id TEXT NOT NULL REFERENCES events(id),
tag_name TEXT NOT NULL,
tag_value TEXT NOT NULL
);
CREATE INDEX idx_tags_name_value ON event_tags(tag_name, tag_value);
Timeline API
tenex-eventd serves a minimal HTTP API on localhost:3001:
GET /api/timeline -- All events, newest first, paginated
GET /api/timeline?session={id} -- Events for a specific session
GET /api/timeline?type={type} -- Filter by event type (file-change, checkpoint, etc.)
GET /api/session/current -- Current session info
POST /api/undo/{event_id} -- Revert to the checkpoint before this event
GET /api/stats -- Session stats (action count, error rate, etc.)
POST /api/export -- Export events as Nostr event array (relay-publishable)
-- For future relay sync:
POST /api/publish -- Publish all events to configured relays
Timeline UI
Minimal web UI served at localhost:3001:
- Reverse-chronological timeline of events
- Color-coded by type (green = checkpoint, blue = file change, red = error, gray = command)
- Each event shows: timestamp, type, summary, reversibility indicator
- “Undo to here” button on checkpoint events → runs
git reset --hard {commit} - Session stats bar: duration, actions, errors, checkpoints
- No framework. Plain HTML + vanilla JS. Served by the Go binary. Keep it tiny.
Auto-Checkpointing
tenex-eventd automatically creates git commits at configurable intervals:
- On significant file changes: After 5+ files modified without a manual commit
- On time interval: Every 5 minutes if there are uncommitted changes
- Before risky operations: If the FS watcher detects deletion of multiple files simultaneously
Auto-commits use the message format: tenex: auto-checkpoint ({N} files changed)
Go Module Structure
tenex-eventd/
├── cmd/
│ └── tenex-eventd/
│ └── main.go -- Entry point, starts all subsystems
├── internal/
│ ├── identity/
│ │ └── keys.go -- Keypair generation, loading, storage
│ ├── events/
│ │ ├── signer.go -- Signs events using identity keypair
│ │ ├── store.go -- SQLite event storage
│ │ └── types.go -- Event type constants, builder helpers
│ ├── capture/
│ │ ├── git.go -- Git hook handler + auto-checkpointing
│ │ ├── fs.go -- Filesystem watcher (fsnotify)
│ │ └── session.go -- Session lifecycle management
│ ├── api/
│ │ ├── server.go -- HTTP server setup
│ │ ├── timeline.go -- Timeline API handlers
│ │ └── undo.go -- Undo/revert handlers
│ └── ui/
│ └── embed.go -- Embedded static files for timeline UI
├── go.mod
├── go.sum
└── Makefile
Dependencies
| Library | Purpose | Version |
|---|---|---|
github.com/nbd-wtf/go-nostr |
Nostr event signing, NIP-01 format | latest |
github.com/fsnotify/fsnotify |
Filesystem watching | v1.7+ |
github.com/mattn/go-sqlite3 |
SQLite driver | latest |
embed (stdlib) |
Embed timeline UI static files | Go 1.16+ |
net/http (stdlib) |
API server | stdlib |
Integration with Stage 0
tenex-eventd is added to the Docker image and started by the entrypoint script alongside Ollama and before OpenCode launches. It:
- Generates or loads identity from
/workspace/.tenex/identity.json - Opens or creates
/workspace/.tenex/events.db - Installs git hooks in
/workspace/.git/hooks/ - Starts filesystem watcher on
/workspace - Starts HTTP API on
localhost:3001 - Creates a session-start event
- Signals ready (entrypoint proceeds to launch OpenCode)
Acceptance Criteria
- [ ]
tenex-eventdstarts and generates a keypair on first run - [ ] Git commits in the workspace produce checkpoint events in SQLite
- [ ] File changes between commits produce file-change events
- [ ]
GET /api/timelinereturns events in reverse chronological order - [ ]
POST /api/undo/{id}reverts to the specified checkpoint - [ ] Timeline UI is viewable at
localhost:3001and shows events in real-time - [ ] Events in SQLite are valid NIP-01 Nostr events (verified by signature check)
- [ ]
POST /api/exportproduces a JSON array publishable to any Nostr relay - [ ] Session stats are accurate (action count, error rate, duration)
- [ ] Auto-checkpointing creates commits at the configured interval
- [ ] The daemon adds <50ms latency to the agent workflow (non-blocking FS watching)
- [ ] Total binary size <20MB (Go compiles to a single binary)
What’s NOT in Stage 1
- Relay publishing (Stage 3)
- Multi-agent coordination (Stage 2)
- Recipe packaging (Stage 2)
- Web-of-Trust scoring (Signet)
- Any cloud connectivity requirement
Write a comment