Skip to content

Latest commit

 

History

History
267 lines (181 loc) · 11.8 KB

File metadata and controls

267 lines (181 loc) · 11.8 KB

nbs-chat: File-Based Chat

File-based messaging for any combination of participants — AI instances, humans, or both. No privilege model, no routing hierarchy. Anyone with the file path can read, write, and poll.

The Problem

NBS teams communicate through task files: write once, read once. No back-and-forth. When participants need to share findings, coordinate approaches, or react to each other's work, there is no mechanism. Everything routes through a single supervisor, which doesn't scale.

Use Cases

Worker-to-worker: Two AI workers share findings without supervisor relay.

Peer-to-peer: Equal participants coordinate on a shared problem.

Human-in-the-loop: A human joins via nbs-chat-terminal, observing AI workers and injecting guidance in real time.

Supervisor broadcast: Supervisor posts instructions that multiple workers poll for.

Mixed: Any combination. The protocol doesn't distinguish participant types.

How It Works

A chat file is a plain text file with a header and base64-encoded messages. Any participant can read or write using nbs-chat commands. File locking (flock) ensures atomic operations. The lock is held only during each command invocation — no participant can hold it across tool calls. This is the fundamental design constraint.

=== nbs-chat ===
last-writer: test-runner
last-write: 2026-02-12 14:23:45
file-length: 847
participants: parser-worker, test-runner, human
---
cGFyc2VyLXdvcmtlcjogRm91bmQgMyBmYWlsaW5nIHRlc3Rz
dGVzdC1ydW5uZXI6IENvbmZpcm1lZCAtIHRlc3RfcGFyc2VfaW50IGZhaWxz
YWxleDogQm90aCBvZiB5b3UgZm9jdXMgb24gcGFyc2VfaW50IGZpcnN0

Each line below --- is one message, base64-encoded. Decodes to handle: message text. Base64 prevents message content from breaking the file structure.

The header tracks the last writer, timestamp, file size (integrity check), and participant list. All updated atomically on every send.

Commands

Command Purpose
nbs-chat create <file> Create empty chat file with header
nbs-chat send <file> <handle> <message> Append message (atomic)
nbs-chat read <file> Read all messages (decoded)
nbs-chat read <file> --last=N Read last N messages
nbs-chat read <file> --since=<handle> Read messages after handle's last post
nbs-chat read <file> --unread=<handle> Read unread messages; advances cursor automatically
nbs-chat read <file> --after=<time> Read messages after a time
nbs-chat read <file> --before=<time> Read messages before a time
nbs-chat search <file> <pattern> [--handle=<name>] Search message history by substring
nbs-chat search <file> <pattern> --after=<time> Search within a time range
nbs-chat export <file> [options] Export messages with ANSI colour rendering
nbs-chat delete <file> --after=<time> Delete messages at or after time (atomic, locked)
nbs-chat delete <file> --after=<time> --dry-run Show what would be deleted
nbs-chat poll <file> <handle> --timeout=N Block until new message from someone else
nbs-chat participants <file> List participants and message counts
nbs-chat count <file> Authoritative message count (separator-based, not line count)
nbs-chat cursor-set <file> <handle> <value> Lock-safe cursor write (replaces sed -i on cursor files)
nbs-chat help Usage reference

All read, search, and delete options compose: --after=2h --last=10 --unread=claude shows the last 10 unread-by-claude messages from the past 2 hours.

Time Formats

The --after and --before options accept three formats:

Format Example Meaning
Relative 30s, 5m, 2h, 1d Seconds/minutes/hours/days ago
Epoch 1771834287 Unix timestamp (≥10 digits)
ISO 8601 2026-02-23T00:11:27 Local time

Quick Start

Workers coordinating

# Either worker (or supervisor, or human) creates the channel
nbs-chat create .nbs/chat/debug.chat

# Worker A reports a finding
nbs-chat send .nbs/chat/debug.chat parser-worker "parse_int fails on negative inputs"

# Worker B reads and responds
nbs-chat read .nbs/chat/debug.chat
nbs-chat send .nbs/chat/debug.chat test-runner "Confirmed - fails on -42, root cause is isdigit()"

Human joining

# Human opens an interactive terminal view
nbs-chat-terminal .nbs/chat/debug.chat <your-handle>

The terminal shows a scrolling message view and accepts typed input. See nbs-chat-terminal below.

Poll

poll blocks until a message from someone other than the polling handle appears:

# Wait for a response (up to 60 seconds)
nbs-chat poll .nbs/chat/debug.chat parser-worker --timeout=60

Internally, poll checks once per second under lock. No inotify, no daemons — just a sleep loop. Simple and portable.

Export

export renders chat messages with the same ANSI colour scheme used by nbs-chat-terminal, for post-hoc review of conversations. Output goes to stdout — pipe to a file and view with less -R or vim with AnsiEsc.

# Export last 50 messages
nbs-chat export .nbs/chat/live.chat --last=50 > session.txt
less -R session.txt

# Export only supervisor and theologian messages
nbs-chat export .nbs/chat/live.chat --handle=supervisor,theologian > highlights.txt

# Export messages mentioning "ceiling" from the last 6 hours
nbs-chat export .nbs/chat/live.chat --after=6h --grep=ceiling

# Export a specific message range (0-based indices)
nbs-chat export .nbs/chat/live.chat --from=100 --to=150

Export Options

Option Purpose
--last=N Show only the last N messages
--from=N Start from message N (0-based index)
--to=N End at message N (exclusive)
--handle=h1,h2,... Only messages from these handles (comma-separated)
--after=<time> Messages after time
--before=<time> Messages before time
--grep=<pattern> Only messages matching pattern (case-insensitive)

Options compose: --last=100 --handle=supervisor --grep=phase shows the last 100 messages, filtered to supervisor only, further filtered to those containing "phase".

The rendering is shared with nbs-chat-terminal — both use the same colour palette and formatting code (render.c). Each handle gets a consistent colour (blue, orange, green, pink, yellow, cyan, red, lavender — cycling for more than 8 handles).

Delete

delete removes messages from a time point onwards, atomically and under lock:

# Preview what would be deleted
nbs-chat delete .nbs/chat/live.chat --after=2h --dry-run

# Delete messages from the last 2 hours
nbs-chat delete .nbs/chat/live.chat --after=2h

# Delete from a specific epoch
nbs-chat delete .nbs/chat/live.chat --after=1771834287

The operation is truncation: all messages at or after the given time are removed. The file is rewritten atomically (lock → read → filter → temp write → rename → unlock). Header fields (last-writer, last-write, file-length, participants) are recomputed from the remaining messages.

Terminal Client

nbs-chat-terminal is a separate interactive binary for human participation:

nbs-chat-terminal <file> <handle>

It displays decoded messages in a scrolling view, polls for new ones, and accepts typed input. This lets a human observe and participate in an ongoing AI conversation — or start one.

File Convention

.nbs/
├── chat/
│   ├── coordination.chat    # General channel
│   ├── parser-debug.chat    # Topic-specific
│   └── results.chat         # Results aggregation
└── workers/

Convention: .nbs/chat/<name>.chat. The tool accepts any path.

Any participant can create the chat file. Typically the process that spawns others creates it and passes the path.

Exit Codes

Code Meaning
0 Success
1 General error
2 Chat file not found (or already exists for create)
3 Timeout (poll)
4 Invalid arguments

Design Decisions

Why base64? Messages might contain colons, newlines, quotes, or any other character that could break a delimiter-based format. Base64 makes every message exactly one line of safe ASCII.

Why full rewrite on send? Every send rewrites the entire file (header + all messages) under lock. This keeps the header consistent. Append-only would be faster but the header would drift.

Why file-length in the header? Integrity check. If wc -c doesn't match the header, something went wrong. The test suite verifies this holds even under 50 concurrent writers.

Why not a database? No external dependencies. The file format is human-readable (header is plain text, messages decode with base64 -d). For coordination at the scale of a handful of participants, a file is sufficient.

Why no privilege model? The file is the authority. If you can read the file, you can participate. Access control is filesystem permissions, not application logic.

Single-User Constraint

All agents must run as the same OS user. This is a hard architectural constraint.

What depends on it:

  • flock on the chat file uses 0600 permissions (owner-only). A different user cannot acquire the lock.
  • Cursor files (used by --since and --unread) are stored per-user in .cursors companion files. A different user gets independent cursors that do not track the shared conversation.
  • The bus event directory (.nbs/events/) uses the same permission model.

Failure modes if violated:

  • flock acquisition silently fails — concurrent writes may corrupt the chat file.
  • Cursor tracking silently diverges — agents see repeated or missing messages.
  • No error is reported. The system appears to work but produces incorrect results.

Remote agents (nbs-chat-remote): The remote proxy executes commands via SSH as the SSH user on the remote machine. If the SSH user differs from the user who owns the chat files, all of the above failures apply. Ensure NBS_CHAT_HOST connects as the same user that created the chat file.

Falsifier: Run two agents as different OS users writing to the same chat file. Verify that flock fails to serialise writes and that --since cursors diverge.

Location

bin/nbs-chat            # Non-interactive commands (C binary)
bin/nbs-chat-terminal   # Interactive terminal client (C binary)
bin/nbs-chat-remote     # SSH proxy for remote chat access (C binary)

Source code in src/nbs-chat/. Build with make in that directory.

Installed to ~/.nbs/bin/ by bin/install.sh.

Remote Access

nbs-chat-remote proxies nbs-chat commands over SSH, allowing a local Claude instance to read and write chat files on a remote machine. It forwards the same command syntax (send, read, poll, etc.) to a remote nbs-chat binary via SSH.

Configuration via environment variables:

  • NBS_CHAT_HOST — remote hostname
  • NBS_CHAT_PORT — SSH port
  • NBS_CHAT_KEY — path to SSH private key
  • NBS_CHAT_OPTS — comma-separated SSH -o options (max 4)

Bus Integration

Chat is for conversation. The coordination bus is for notification. The two systems complement each other.

Every nbs-chat send publishes a chat-message event to the bus (if .nbs/events/ exists). Messages containing @mentions additionally publish a chat-mention event at higher priority. This means all agents can overhear all conversations and react to relevant information — even when not directly addressed.

Messages from nbs-chat-terminal (human input) generate human-input bus events, ensuring that human messages receive priority attention.

See nbs-bus for the full bus reference and Bus Recovery for startup/restart protocol.

See Also

  • nbs-bus — Event-driven coordination bus
  • NBS Teams — Supervisor/worker pattern overview
  • nbs-workers — Worker lifecycle management