Stage 1 Technical Spec: Event Capture Layer

Go daemon (tenex-eventd) that captures agent actions as signed Nostr events in local SQLite. Timeline API, auto-checkpointing, undo. Complete module structure, event schema, and acceptance criteria.

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

  • t tags for type filtering (standard Nostr hashtag convention)
  • e tags for event references (standard NIP-01)
  • path for file paths
  • agent for agent identifier
  • model for model identifier
  • commit for git commit SHA
  • recipe for 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:

  1. Generates or loads identity from /workspace/.tenex/identity.json
  2. Opens or creates /workspace/.tenex/events.db
  3. Installs git hooks in /workspace/.git/hooks/
  4. Starts filesystem watcher on /workspace
  5. Starts HTTP API on localhost:3001
  6. Creates a session-start event
  7. Signals ready (entrypoint proceeds to launch OpenCode)

Acceptance Criteria

  • [ ] tenex-eventd starts 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/timeline returns events in reverse chronological order
  • [ ] POST /api/undo/{id} reverts to the specified checkpoint
  • [ ] Timeline UI is viewable at localhost:3001 and shows events in real-time
  • [ ] Events in SQLite are valid NIP-01 Nostr events (verified by signature check)
  • [ ] POST /api/export produces 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
No comments yet.