Skip to main content

File Watching & IPC

Automatic memory ingestion from files and local tool integration via IPC.

File Watching

The daemon can automatically create memories from files in watched directories. When a file matching your patterns is created or modified, the daemon reads its contents and creates a memory in Trix.

Enabling File Watching

File watching is disabled by default for security. Enable it in your configuration:

# ~/.trix/daemon.yaml
watcher:
  enabled: true
  directories:
    - ~/Documents/notes
    - ~/Projects/work/docs
    - /shared/team-wiki
  patterns:
    - "*.md"
    - "*.txt"
    - "*.org"
  ignore_hours: 24
  debounce_ms: 500

Pattern Matching

File patterns use standard glob syntax:

PatternMatches
*.mdAll markdown files in watched directories
*.txtAll text files
README*Files starting with README
docs/*.mdMarkdown files in docs subdirectory
**/*.mdMarkdown files in any subdirectory (recursive)
*.{md,txt}Markdown or text files

Watcher Behavior

  • Creation - New files matching patterns are ingested immediately
  • Modification - Updated files trigger re-ingestion (creates new memory)
  • Debouncing - Rapid changes are debounced to avoid duplicate ingestion
  • Age Filter - Files older than ignore_hours are skipped on startup

Memory Metadata

Memories created from files include useful metadata:

{
  "id": "mem_abc123",
  "content": "Contents of the file...",
  "metadata": {
    "source": "file",
    "filepath": "/home/user/notes/meeting.md",
    "filename": "meeting.md",
    "directory": "/home/user/notes",
    "size_bytes": 1234,
    "ingested_at": "2024-01-15T10:30:00Z",
    "modified_at": "2024-01-15T10:25:00Z"
  },
  "tags": ["file"]
}

Supported File Types

  • Text files - .txt, .md, .org, .rst
  • Code files - .py, .js, .go, etc.
  • Config files - .yaml, .json, .toml
  • Documents - .pdf (text extraction)

Security Considerations:

  • Only watch directories you explicitly trust
  • Be cautious with broad patterns like *
  • Avoid watching directories with sensitive files (credentials, keys)
  • The daemon runs with your user permissions

Troubleshooting File Watching

# Check watcher status
trixd status

# View watcher events in logs
trixd logs -f | grep watcher

# Run with debug logging to see all events
trixd run -vv

Linux inotify limits: If watching many directories, you may need to increase the inotify limit:

# Temporary (until reboot)
sudo sysctl fs.inotify.max_user_watches=524288

# Permanent
echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf

.trixignore Files

The .trixignore file allows you to exclude files and directories from being watched and ingested by the daemon. It uses the same syntax as .gitignore, making it familiar to developers.

Enabling .trixignore Support

To use .trixignore files, enable them in your daemon configuration:

# ~/.trix/daemon.yaml
watcher:
  enabled: true
  directories:
    - ~/Documents/notes
    - ~/Projects
  patterns:
    - "*.md"
    - "*.txt"

  # Enable .trixignore file discovery
  check_ignore_files: true

  # Optional: customize the ignore file name (default: .trixignore)
  ignore_file_name: ".trixignore"

Creating a .trixignore File

Place a .trixignore file in any watched directory. The rules apply to that directory and all its subdirectories.

# ~/Projects/.trixignore

# Build outputs
build/
dist/
out/
*.o
*.pyc
*.class

# Dependencies
node_modules/
vendor/
venv/
__pycache__/

# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store

# Version control
.git/
.svn/
.hg/

# Temporary files
*.tmp
*.temp
*.bak
*.log

# Secrets and credentials (never ingest these!)
.env
.env.local
*.pem
*.key
credentials.json

# Large binary files
*.zip
*.tar.gz
*.mp4
*.mov

# Re-include specific files (negation)
!README.md
!docs/*.md
!.vscode/settings.json

Pattern Syntax Reference

The .trixignore file supports full gitignore syntax:

PatternDescriptionExample Matches
*.logWildcard - matches any .log fileapp.log, error.log
build/Directory - matches directories onlybuild/, src/build/
**/node_modules/**Double-star - matches at any depthnode_modules/, a/b/node_modules/pkg/
/config.yamlRoot-anchored - only in root directoryconfig.yaml (not sub/config.yaml)
!important.mdNegation - re-includes previously ignoredInclude important.md even if *.md is ignored
# commentComment - ignored line-
\#fileEscaped - matches literal #file#file
doc?.txtSingle char wildcarddoc1.txt, docA.txt
*.[oa]Character classfile.o, file.a

Config vs .trixignore

You can use both global ignore_patterns in config and per-directory .trixignore files:

FeatureConfig ignore_patterns.trixignore File
ScopeAll watched directoriesDirectory and subdirectories
Location~/.trix/daemon.yamlIn watched directories
Version ControlUsually not committedCan be committed to repo
Use CaseSystem-wide excludesProject-specific excludes
PriorityApplied firstApplied after, can override

Best Practice: Use config ignore_patterns for system-wide excludes like node_modules and .git. Use .trixignore files for project-specific rules that should be shared with your team.

Common .trixignore Examples

Node.js Project

# Node.js .trixignore
node_modules/
dist/
build/
coverage/
.nyc_output/
*.log
npm-debug.log*
.env
.env.local

Python Project

# Python .trixignore
__pycache__/
*.py[cod]
*$py.class
venv/
.venv/
env/
.eggs/
*.egg-info/
dist/
build/
.tox/
.pytest_cache/
.mypy_cache/

Go Project

# Go .trixignore
vendor/
bin/
*.exe
*.dll
*.so
*.dylib
*.test
*.out
go.sum

Documentation Project

# Docs .trixignore
_site/
.jekyll-cache/
.sass-cache/
node_modules/

# Include all markdown except drafts
!*.md
drafts/

Security Best Practices

Always ignore sensitive files:

  • .env - Environment variables with secrets
  • *.pem, *.key - Private keys and certificates
  • credentials.json - API credentials
  • *.sqlite, *.db - Database files
  • config/secrets.* - Secret configuration files

Add these to your global config ignore_patterns for maximum safety.

Debugging Ignore Patterns

To see which files are being ignored, run the daemon with debug logging:

# See ignore pattern matching in real-time
trixd run -vv 2>&1 | grep -E "(ignored|pattern)"

# Check daemon status including ignore stats
trixd status --json | jq '.watcher'

# Check daemon status including ignore stats
trixd status --json | jq '.watcher.ignored_count'

IPC Integration

The daemon exposes a Unix socket for local inter-process communication. This allows other tools on your machine to interact with Trix through the daemon without making direct API calls.

Socket Location

By default, the socket is at ~/.trix/daemon.sock. The Trix CLI automatically uses this socket when the daemon is running for faster operations and offline support.

Protocol

The IPC uses JSON-RPC 2.0 over the Unix socket:

// Request format
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "method_name",
  "params": { ... }
}

// Success response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { ... }
}

// Error response
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32600,
    "message": "Invalid request"
  }
}

Available Methods

MethodParametersDescription
pingnoneCheck if daemon is responsive
statusnoneGet detailed daemon status
sync{"force": bool}Trigger immediate sync with API
search{"query": string, "limit": int}Search cached memories locally
get{"id": string}Get a specific memory from cache
invalidate{"id": string} or {"all": bool}Clear specific cache entries
confignoneGet current configuration

Method Examples

ping

// Request
{"jsonrpc": "2.0", "id": 1, "method": "ping", "params": {}}

// Response
{"jsonrpc": "2.0", "id": 1, "result": {"pong": true, "timestamp": "2024-01-15T10:30:00Z"}}

status

// Request
{"jsonrpc": "2.0", "id": 1, "method": "status", "params": {}}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "status": "running",
    "uptime_seconds": 3600,
    "cache_count": 1234,
    "cache_size_bytes": 52428800,
    "api_connected": true,
    "last_sync": "2024-01-15T10:29:30Z",
    "watchers_active": 2
  }
}

search

// Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "search",
  "params": {
    "query": "user preferences",
    "limit": 5
  }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "memories": [
      {"id": "mem_abc123", "content": "User prefers dark mode...", "score": 0.92},
      {"id": "mem_def456", "content": "Preferred language is Python...", "score": 0.85}
    ],
    "from_cache": true
  }
}

sync

// Request - trigger immediate sync
{"jsonrpc": "2.0", "id": 1, "method": "sync", "params": {"force": true}}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "success": true,
    "memories_synced": 42,
    "duration_ms": 1234
  }
}

Go Client Example

package main

import (
    "encoding/json"
    "fmt"
    "net"
    "os"
)

type Request struct {
    JSONRPC string                 `json:"jsonrpc"`
    ID      int                    `json:"id"`
    Method  string                 `json:"method"`
    Params  map[string]interface{} `json:"params"`
}

type Response struct {
    JSONRPC string          `json:"jsonrpc"`
    ID      int             `json:"id"`
    Result  json.RawMessage `json:"result"`
    Error   *struct {
        Code    int    `json:"code"`
        Message string `json:"message"`
    } `json:"error"`
}

func main() {
    sockPath := os.ExpandEnv("$HOME/.trix/daemon.sock")
    conn, err := net.Dial("unix", sockPath)
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    // Send status request
    req := Request{
        JSONRPC: "2.0",
        ID:      1,
        Method:  "status",
        Params:  map[string]interface{}{},
    }
    json.NewEncoder(conn).Encode(req)

    // Read response
    var resp Response
    json.NewDecoder(conn).Decode(&resp)

    if resp.Error != nil {
        fmt.Printf("Error: %s\n", resp.Error.Message)
        return
    }

    fmt.Printf("Status: %s\n", string(resp.Result))
}

Python Client Example

import socket
import json
import os

class TrixDaemonClient:
    def __init__(self, sock_path=None):
        self.sock_path = sock_path or os.path.expanduser("~/.trix/daemon.sock")
        self._id = 0

    def _request(self, method: str, params: dict = None) -> dict:
        self._id += 1
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock.connect(self.sock_path)

        request = {
            "jsonrpc": "2.0",
            "id": self._id,
            "method": method,
            "params": params or {}
        }
        sock.send(json.dumps(request).encode() + b"\n")

        response = json.loads(sock.recv(65536).decode())
        sock.close()

        if "error" in response:
            raise Exception(response["error"]["message"])

        return response.get("result")

    def ping(self) -> bool:
        result = self._request("ping")
        return result.get("pong", False)

    def status(self) -> dict:
        return self._request("status")

    def search(self, query: str, limit: int = 10) -> list:
        result = self._request("search", {"query": query, "limit": limit})
        return result.get("memories", [])

    def sync(self, force: bool = False) -> dict:
        return self._request("sync", {"force": force})

# Usage
client = TrixDaemonClient()

if client.ping():
    status = client.status()
    print(f"Cache contains {status['cache_count']} memories")

    memories = client.search("user preferences", limit=5)
    for mem in memories:
        print(f"- {mem['id']}: {mem['content'][:50]}...")

Node.js Client Example

const net = require('net');
const os = require('os');
const path = require('path');

class TrixDaemonClient {
  constructor(sockPath) {
    this.sockPath = sockPath || path.join(os.homedir(), '.trix', 'daemon.sock');
    this.id = 0;
  }

  async request(method, params = {}) {
    return new Promise((resolve, reject) => {
      const client = net.createConnection(this.sockPath);

      const request = {
        jsonrpc: '2.0',
        id: ++this.id,
        method,
        params
      };

      client.write(JSON.stringify(request) + '\n');

      let data = '';
      client.on('data', chunk => data += chunk);
      client.on('end', () => {
        const response = JSON.parse(data);
        if (response.error) {
          reject(new Error(response.error.message));
        } else {
          resolve(response.result);
        }
      });
      client.on('error', reject);
    });
  }

  async ping() {
    const result = await this.request('ping');
    return result.pong;
  }

  async status() {
    return this.request('status');
  }

  async search(query, limit = 10) {
    const result = await this.request('search', { query, limit });
    return result.memories;
  }
}

// Usage
const client = new TrixDaemonClient();

(async () => {
  if (await client.ping()) {
    const status = await client.status();
    console.log(`Cache contains ${status.cache_count} memories`);

    const memories = await client.search('user preferences', 5);
    memories.forEach(m => console.log(`- ${m.id}: ${m.content.slice(0, 50)}...`));
  }
})();

CLI Integration

The Trix CLI automatically uses the daemon when available. This provides:

  • Faster queries - Uses local cache instead of API calls
  • Offline support - Works without internet connection
  • Lower latency - Sub-millisecond response for cached data
# These commands use the daemon automatically when running:
trix recall "user preferences"    # Uses local cache
trix memories list                # Uses local cache
trix context "project info"       # Uses local cache

# Force API call (bypass daemon)
trix recall "query" --no-daemon

IPC Error Codes

Standard JSON-RPC 2.0 error codes:

CodeMessageDescription
-32700Parse errorInvalid JSON received
-32600Invalid requestNot a valid JSON-RPC request
-32601Method not foundUnknown method name
-32602Invalid paramsInvalid method parameters
-32603Internal errorInternal daemon error
-32000Not connectedDaemon not connected to API
-32001Cache missRequested item not in cache