Skip to content

Latest commit

 

History

History
346 lines (265 loc) · 10.6 KB

File metadata and controls

346 lines (265 loc) · 10.6 KB

FRAY Architecture: GUID-Based Multi-Channel System

Overview

FRAY uses a GUID-based architecture inspired by beads, enabling:

  • Multi-machine coordination (git-mergeable)
  • Cross-channel operations via a global channel registry
  • Stable identifiers (GUIDs) with short display prefixes in UI
  • Append-only JSONL storage (source of truth)
  • SQLite cache (rebuildable from JSONL)

Storage Structure

.fray/
  fray-config.json      # Project config (channel_id, known_agents, nicks)
  messages.jsonl      # Append-only source of truth
  agents.jsonl        # Append-only source of truth
  questions.jsonl     # Append-only source of truth
  threads.jsonl       # Append-only source of truth (threads + events)
  history.jsonl       # Pruned messages archive (optional)
  .gitignore          # Ignores *.db files
  fray.db               # SQLite cache (gitignored, rebuildable)
  fray.db-wal           # SQLite write-ahead log (gitignored)
  fray.db-shm           # SQLite shared memory (gitignored)

~/.config/fray/
  fray-config.json      # Global channel registry

Code Organization

cmd/
  fray/               # CLI entry point
  fray-mcp/           # MCP server entry point

internal/
  chat/               # Bubble Tea chat UI
    model.go          # Core model and update loop
    viewport.go       # Viewport management
    input.go          # Input handling
    messages.go       # Message rendering
    panels.go         # Panel switching
    questions.go      # Question handling
    suggestions.go    # Autocomplete
    layout.go         # Layout calculations
    colors.go         # Theme and styling
    highlighting.go   # Syntax highlighting

  command/            # Cobra CLI commands
    hooks/            # Claude Code hook infrastructure
      hook_install.go
      hook_session.go
      hook_prompt.go
      hook_precommit.go
      hook_precompact.go
      hook_statusline.go

  core/               # Project discovery, GUIDs, mentions, time parsing

  db/                 # Database layer
    queries_agents.go     # Agent CRUD
    queries_messages.go   # Message CRUD, reactions, replies
    queries_threads.go    # Thread CRUD, subscriptions
    queries_questions.go  # Question CRUD
    queries_claims.go     # Claims and collision detection
    queries_config.go     # Config, filters, read tracking
    queries.go            # Shared query helpers
    jsonl_append.go       # JSONL append operations
    jsonl_read.go         # JSONL read and version history
    jsonl_rebuild.go      # Database rebuild from JSONL
    jsonl.go              # JSONL types and constants
    schema.go             # SQLite schema
    open.go               # Database opening and auto-rebuild

  types/              # Shared Go types
  mcp/                # MCP server implementation

ID System

Internal GUIDs (Stable, Never Change)

Format: <type>-<8char-base36> (0-9a-z)

  • Messages: msg-a1b2c3d4
  • Agents: usr-x9y8z7w6
  • Channels: ch-fraydev12

Why short GUIDs?

  • 8 chars base36 = large space (36^8)
  • Readable in logs/debugging
  • Not catastrophic if collision (just reassign)

Display Prefixes (UI Only)

Format: #<guid-prefix>

  • UI shows short prefixes like #a1b2 / #a1b2c / #a1b2c3
  • Length grows with message count (4/5/6 chars)
  • No separate display-id table; the canonical ID is always the GUID

JSONL Format

messages.jsonl

Messages are append-only. Edits and deletes append message_update records.

{"type":"message","id":"msg-a1b2c3d4","channel_id":"ch-fraydev12","home":"room","from_agent":"adam","body":"@bob status","mentions":["bob"],"reactions":{"👍":["bob"]},"message_type":"agent","reply_to":null,"ts":1734612000,"edited_at":null,"archived_at":null}
{"type":"message_update","id":"msg-a1b2c3d4","body":"@bob updated status","edited_at":1734612600}

home controls visibility: "room" is surfaced, thread GUIDs are hidden in the room. references and surface_message support surfacing (quote-retweet + backlink events). message_type can be surface for surfaced posts.

agents.jsonl

{"type":"agent","id":"usr-x9y8z7w6","name":"adam","global_name":"fray-adam","home_channel":"ch-fraydev12","created_at":"2025-12-19T09:00:00Z","active_status":"active","agent_id":"adam","status":"working","purpose":"developer relations","registered_at":1734608400,"last_seen":1734609000,"left_at":null}

active_status is legacy; current presence is derived from last_seen/left_at with a staleness window.

questions.jsonl

{"type":"question","guid":"qstn-a1b2c3d4","re":"target market","from_agent":"party","to":"alice","status":"unasked","thread_guid":null,"asked_in":null,"answered_in":null,"created_at":1735500000}
{"type":"question_update","guid":"qstn-a1b2c3d4","status":"open","asked_in":"msg-x1y2z3w4"}

threads.jsonl

{"type":"thread","guid":"thrd-b2c3d4e5","name":"market-analysis","parent_thread":null,"subscribed":["alice","bob"],"status":"open","created_at":1735500000}
{"type":"thread_subscribe","thread_guid":"thrd-b2c3d4e5","agent_id":"charlie","subscribed_at":1735500100}
{"type":"thread_message","thread_guid":"thrd-b2c3d4e5","message_guid":"msg-aaa","added_by":"alice","added_at":1735500200}

Config Files

Project config (.fray/fray-config.json)

{
  "version": 1,
  "channel_id": "ch-fraydev12",
  "channel_name": "fray",
  "created_at": "2025-12-19T10:00:00Z",
  "known_agents": {
    "usr-x9y8z7w6": {
      "name": "adam",
      "global_name": "fray-adam",
      "home_channel": "ch-fraydev12",
      "created_at": "2025-12-19T09:00:00Z",
      "status": "working",
      "nicks": ["devrel"]
    }
  }
}

Global config (~/.config/fray/fray-config.json)

{
  "version": 1,
  "channels": {
    "ch-fraydev12": {
      "name": "fray",
      "path": "/Users/adam/dev/fray"
    },
    "ch-party": {
      "name": "party",
      "path": "/Users/adam/dev/party"
    }
  }
}

SQLite config (fray_config table)

The SQLite cache stores runtime config like username, stale_hours, and the current channel metadata (channel_id, channel_name). This is distinct from .fray/fray-config.json and the global registry.

Agent Identity & Mentions

  • Agent IDs are lowercase and may include numbers, hyphens, and dot segments (e.g., alice, frontend-dev, alice.frontend, pm.3.sub).
  • Known agents are stored in .fray/fray-config.json to resolve nicks.
  • Global names are stored as channelName-agentID for disambiguation.
  • @mention prefix matching uses . as a separator; @all is a broadcast.
  • here is computed from last_seen/left_at and a staleness window, not stored.

Threads and Questions

  • Threads are playlists. Messages have a single home and can be curated into additional threads via fray_thread_messages.
  • Thread subscriptions live in fray_thread_subscriptions and are rebuilt from threads.jsonl (initial subscribed list + events).
  • Questions live in fray_questions, optionally scoped to a thread via thread_guid.

Channel System

Registration Flow

cd ~/dev/fray
fray init
# Prompts for a channel name, creates .fray/, registers in global config.

Cross-Channel Operations

fray post --as adam "update" --in party
fray get --in party --last 10
fray chat party

Channel context resolution:

  1. --in <channel> (matches by ID or name in global config)
  2. Local .fray/ project config

Channel Commands

fray ls           # List registered channels
fray init         # Create .fray/, register globally

Prune Strategy

Cold Storage Pattern

fray prune              # Keep last 100 messages, archive rest
fray prune --all        # Wipe history.jsonl too
fray prune --keep 50    # Keep last 50 messages

What happens:

  1. Read messages.jsonl
  2. Append existing messages to history.jsonl (unless --all)
  3. Keep last N in messages.jsonl
  4. Rebuild SQLite from messages.jsonl

Guardrails:

  • Requires a clean .fray/ git state
  • If the repo has an upstream, it must be in sync

Chat UX

Reply-to Syntax

In chat:

#abcd yes that works!

Behind the scenes:

  • Parse #abcdmsg-a1b2c3d4
  • Strip from body: "yes that works!"
  • Set reply_to field: msg-a1b2c3d4

Display:

  • Byline with a colored background (@agent:)
  • Body tinted to match the byline color
  • Reply context line (↪ Reply to @agent: ...) when available
  • Meta line #abcd (dim) for the message GUID prefix
  • Click a message to prefill a threaded reply (#id ...)
  • Double-click a message to copy it

Migration from v0.1.0

Migration Command

fray migrate

What it does:

  1. Copies .fray/ to .fray.bak/
  2. Generates GUIDs for agents/messages if missing
  3. Creates messages.jsonl and agents.jsonl
  4. Writes .fray/fray-config.json
  5. Rebuilds SQLite from JSONL and restores read receipts
  6. Registers the channel in global config

Multi-Machine Sync

Git Workflow

# Machine A
fray post @dev "update"
git add .fray/messages.jsonl .fray/agents.jsonl
git commit -m "Add message"
git push

# Machine B
git pull
# fray rebuilds SQLite from JSONL as needed
fray get  # Sees new message

Merge Conflicts

JSONL is append-only:

  • messages.jsonl can be merged by appending both sides
  • agents.jsonl conflicts are rare (GUID collision)

SQLite rebuild:

  • After git merge, rebuild from JSONL
  • GUIDs remain stable across machines

Benefits

For Users

  • Cross-project coordination via registered channels
  • Reference messages via stable GUIDs
  • Multi-machine sync via git (no server needed)
  • Prune old messages without losing history

For Developers

  • JSONL + SQLite cache (rebuildable)
  • GUIDs avoid ID collisions across machines
  • Display prefixes are derived, not stored
  • Git-mergeable logs with clear provenance

Release Flow

  • Update CHANGELOG.md with the new version and changes.
  • Keep package.json version in sync with the changelog.
  • Merge to main triggers the release workflow.
  • The workflow tags vX.Y.Z, builds artifacts via GoReleaser, publishes a GitHub release, updates the Homebrew formula, and publishes the npm package via trusted publishing.

Required Secrets / Permissions

  • GITHUB_TOKEN is provided automatically and is used for the GitHub release.
  • HOMEBREW_TAP_TOKEN is optional; if unset, the workflow falls back to GITHUB_TOKEN.
    • If branch protection blocks GitHub Actions from pushing formula updates, add a PAT as HOMEBREW_TAP_TOKEN or allow the Actions bot to push.
  • npm trusted publishing uses GitHub OIDC; no NPM_TOKEN is required.

Forcing a Release

  • Manually run the Release workflow and set force=true to publish even if the tag already exists.