Port ConversationTranscript to React with role-labeled turns
(System/User/Assistant/Tool), collapsible tool calls, search within
conversation, and jump between turns. WorkerDetail now has tabbed
overview/conversation view. Clicking events in ActivityStream selects
the worker and highlights the matching turn in the conversation tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Port BudgetAlertPanel behaviour to CostDashboard: warning at 80% and
critical at 95% of configured daily budget. Adds BudgetBanner (sticky
top-of-page alert when budget >= 80%), burn rate with ETA-to-exhaust,
and top consumers breakdown. Sources data from /api/cost/summary polled
every 15 seconds.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Port TUI ErrorGroupPanel to React — groups errors by signature with
occurrence count, affected workers, time span, severity badges, and
expandable detail cards. Links to similar past errors from fabric.db
error_history via /api/errors/history/similar endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add startPosition option and currentPosition getter to LogTailer so
evicted tailers can be resumed from their last read byte offset
- Rewrite DirectoryTailer with a bounded active set (maxActiveFiles=200):
only the N most-recently-modified files have open watchers; older files
are tracked in a fileInfo Map but not watched
- LRU eviction: when the active set is full and a new file needs activation,
the least-recently-active tailer is stopped (position checkpointed) and
replaced
- Re-activation: the poll loop (default 30 s) detects mtime changes in
inactive files and opens them from their saved position so no bytes are
missed or replayed
- RSS back-pressure: skip new activations when process.memoryUsage().rss
exceeds maxRssBytes (default 400 MB)
- Hot-add new files via fs.watch rename, always reading from position 0
- Add three new tests: 10k-file cap assertion, LRU eviction+reactivation,
and position-checkpoint correctness
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Browser-compatible port of src/tui/utils/fuzzyMatch.ts with fzf-style
scoring and React highlight segments support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add src/serverMetrics.ts (ServerMetrics class for /api/health + /api/metrics)
- Add scripts/fabric-health-check.sh (curl-based liveness probe)
- Wire sd_notify READY=1 on server start and WATCHDOG=1 keepalives in server.ts
so the Type=notify systemd service correctly reports start and keeps the
watchdog alive without an external npm package
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /api/health returns {status, uptime_sec, version, event_count,
ingest_rate_per_sec, ws_clients, tailer_files_watched, dedup_dropped,
process_resident_memory_bytes}; returns HTTP 503 with status='overloaded'
when maxEventCount is exceeded
- /api/metrics exposes the same counters in Prometheus text format;
fabric_status=0 when overloaded
- Add ServerMetrics.eventCount setter so both endpoints sync from store.size
(fixes fabric_event_count in /api/metrics showing 0 when events added directly)
- Wire --max-events CLI option into `fabric web`; pass maxEventCount and
deduplicator to createWebServer so the memory-bomb guard and dedup_dropped
reporting are actually activated
- Track tailerFilesWatched: set after tailer.start() and update on each event
for DirectoryTailer (uses activeFiles.length getter)
- Add import for Node net module used by systemd watchdog notify
- Add tests: overload guard returns 503, within-limit returns 200, Prometheus
reflects fabric_status=0 when overloaded
systemd service already has Restart=on-failure + WatchdogSec=30 (scripts/fabric-web.service);
liveness guard in server.ts calls process.exit(1) after 3 consecutive overload
checks, triggering systemd restart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The existing React CommandPalette was losing fuzzy match label indices
by flattening ScoredEntry[] to CommandSuggestion[] before render, so
HighlightedText always received empty indices (no highlights). Also the
click handler called setTimeout(executeSelected, 0) which executed on
a stale selectedIndex after the state update.
Fix: introduce FilteredEntry type that carries labelIndices through to
the render step; pass correct indices to HighlightedText; replace the
setTimeout click pattern with a direct executeAction(s.action) call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Warn at startup when FABRIC_AUTH_TOKEN is unset so operators know
POST /api/events is open to any local process; surfaced before
"Press Ctrl+C to stop" so it's visible in systemd journal
- Add "Token rotation" section to README with step-by-step procedure:
generate new secret, update secrets.env (0600), restart service,
verify 401 enforcement; notes that NEEDLE workers reload on next task
start when auth_token uses \${FABRIC_AUTH_TOKEN} substitution
The full auth chain is now in place end-to-end:
~/.config/fabric/secrets.env (0600) → EnvironmentFile →
FABRIC_AUTH_TOKEN env var → server auth middleware → 401/403 on
unauthenticated POST; NEEDLE config auth_token: "\${FABRIC_AUTH_TOKEN}"
routes worker events through the same token.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NEEDLE log retention for ~/.needle/logs/. The directory had grown to
103k files / 11GB with no cleanup. Adds:
- fabric prune command: archives old files into dated tar.gz, deletes
expired archives, with configurable age thresholds
- mend.logs_pruned events emitted to fabric-mend.jsonl for FABRIC tailer
- systemd timer (fabric-prune.timer) for daily automatic pruning
- 9 tests covering archive, delete, dry-run, edge cases
Ran initial prune: 103,857 -> 1,006 files, 8.6 GB freed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move auth middleware before OTLP router mount and apply it as app-level
middleware for all POST requests. This protects event ingestion endpoints
(/api/events, /api/events/batch), OTLP endpoints (/v1/logs, /v1/traces,
/v1/metrics), and cost alert acknowledgement. GET endpoints remain open.
Adds comprehensive auth tests covering 401/403/201 responses.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add e2e test proving DirectoryTailer works with real NEEDLE per-worker
JSONL format: copies anonymized fixtures to temp dir, asserts events from
multiple workers, hot-adds a gamma worker mid-test, all under 1.4s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- resolveSource() now returns { kind: 'directory'|'file', path } instead
of appending workers.log to directories
- New resolveFromOptions() handles CLI flag precedence: --source > --file > default
- Default (no flags) now tails ~/.needle/logs/ as a directory via DirectoryTailer
- --source <nonexistent-path> prints clear error and exits
- -f/--file keeps single-file LogTailer behavior for backwards compat
- tui, web, and tail/logs commands all updated
- replay and digest commands unchanged (file-only, no --source)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New DirectoryTailer class that watches a directory for *.jsonl files,
spawning a LogTailer per file and hot-adding new files via fs.watch.
Forwards child events with source file path and deduplicates across
sources using a shared EventDeduplicator.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire shared EventDeduplicator across all ingestion paths (JSONL tailer,
OTLP/gRPC receiver, OTLP/HTTP receiver) so duplicate events from dual
ingestion are silently dropped on (session_id, worker_id, sequence).
Also adds docs/needle-exporter-wiring.md (OTLP configuration guide for
NEEDLE), SpanDag React component, EventFilter.eventType field, and
various test/layout fixes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- getSessionWorkerSummaries now maps SQLite snake_case columns to the
TypeScript camelCase return type, fixing the type mismatch
- Fix getSessionsInRange typo (was getSingsInRange)
- Fix flaky duration test using deterministic timestamps
- Update tests to use camelCase property names
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add OTLP metric ingestion pipeline that persists Sum/Histogram/Gauge
data points to fabric.db with canonical instrument names:
- MetricAccumulator in workerAnalytics.ts accumulates per-worker
running totals for tokens, cost, durations, bead counts
- Schema v2 in historicalStore.ts adds metric_samples and
session_worker_summaries tables with source preference tracking
- flushMetricSamples() in store.ts drains accumulator → SQLite on
every event, upserting session summaries and live session rows
- docs/schema.md documents instrument names and resolution order
Fix source preference ordering (otlp-metric > otlp-span > log-derived)
using CASE-based SQL sort instead of alphabetical ASC which was
inverted. Fix metricsSource detection to not require costUsd > 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix histogram data point value extraction in normalizer — OTLP histogram
points carry sum/count instead of asDouble/asInt, so needle.bead.duration
was silently dropped. Add MetricAccumulator tests and end-to-end tests
validating OTLP metrics flow through to session_worker_summaries in
fabric.db with otlp-metric source preference.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add --source flag to tui/web/tail subcommands for specifying log source
as file or directory (directories get workers.log appended). Add 'logs'
as alias for 'tail' subcommand per plan.md CLI spec. Update README.md
with fabric logs examples and --source usage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When FABRIC ingests events from both JSONL and OTLP sources, the same
logical event can arrive twice. EventDeduplicator keeps the first
arrival and silently drops duplicates with a counter for observability.
Events with sequence < 0 (legacy formats) always pass through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- normalizeOtlpSpanStart: emit {name}.started with span_id/parent_span_id/trace_id
- normalizeOtlpSpanEnd: emit {name}.finished with duration_ms and span attributes
- needleEventToLogEvent: promote span_id, parent_span_id, trace_id, span_name
- dagUtils: add buildSpanDag() using parent_span_id for parent-child linkage
- dagUtils: add findSpansForBead() for bead-to-span lookup
- Add integration test confirming bead lifecycle renders as DAG node with children
- Add namespaced OTLP attribute resolution (needle.worker.id etc.)
- Add OTLP body (AnyValue) extraction for logs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace coarse NeedleWorkerStatus ('idle'|'executing'|'draining'|'starting')
with the real NEEDLE state machine: BOOTING→SELECTING→CLAIMING→WORKING→CLOSING→STOPPED.
- Add NeedleState type, VALID_TRANSITIONS map, needleStateToStatus() helper in types.ts
- Track needleState + lastStateTransition per worker by consuming worker.state_transition events
- Surface all six states in TUI worker cards (WorkerGrid, WorkerDetail) with per-state icons/colors
- Surface all six states in web WorkerGrid.tsx with NEEDLE_STATE_LABELS and NEEDLE_STATE_COLORS
- Add getNeedleStateColor/getNeedleStateIcon to colors.ts
- Rewire stuck detection to fire on state-transition gaps (state_gap pattern in stuckDetection.ts)
- Add sequence-based event ordering via compareEventsBySequence and queryOrdered()
- Legacy event-type fallback preserved for workers not emitting state_transition events
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WorkerDetail now renders statuses in uppercase (ACTIVE/IDLE/ERROR) to
match NeedleState labels. Store only recognizes canonical event types
(bead.completed, not "Task completed") for state transitions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The web frontend SessionReplay component was sorting events by
timestamp, inconsistent with all other consumers (store, TUI replay,
export, analytics, narrative) which use compareEventsBySequence.
Switched to the canonical (worker, sequence) ordering so multi-host
OTLP ingestion produces correct replay order despite clock skew.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mount OTLP/HTTP handlers on the existing Express web server via a second
HTTP listener so OTLP endpoints are reachable at the standard :4318
address without a separate process. Accepts both application/x-protobuf
and application/json content types, routing decoded records through the
same Normalizer pipeline as the gRPC receiver.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add state_gap stuck detection using lastStateTransition — fires when
a worker in an active NeedleState hasn't transitioned within the
configurable threshold (default 5min, critical at 2x).
- Update web WorkerDetail.tsx to display needleState with icons and
colors instead of coarse active/idle/error status.
- Add stuckDetection.test.ts with 16 tests covering gap-based detection,
legacy patterns, and edge cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add full OTLP/gRPC receiver terminating LogsService, TraceService, and
MetricsService Export RPCs. Decoded protobuf records are normalized via
the shared Normalizer pipeline and emitted as LogEvents on the event bus.
- gRPC server via @grpc/grpc-js with protobufjs codec
- Protobuf definitions for all three OTLP collector services
- enrichRecord() merges scope/resource attributes for normalizer
- extractDataPoints() handles gauge, sum, and histogram metrics
- Integration tests with real gRPC client/server round-trip
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Tighten parseNeedleEvent signature to accept string (JSON line) and
preserve all canonical fields (timestamp, event_type, worker_id,
session_id, sequence, bead_id, data)
- Make parseLogLine a thin adapter that calls parseNeedleEvent then
projects to legacy LogEvent via needleEventToLogEvent
- Add comprehensive parseNeedleEvent unit tests covering canonical format,
session_id/sequence/data round-trip, all 47 NeedleEventType values,
schema version validation, and legacy format conversion
- Rewrite parser.real-logs.integration.test.ts to assert NeedleEvent
shape against real ~/.needle/logs/*.jsonl fixtures
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Create src/normalizer.ts with normalize(raw, source): NeedleEvent supporting
all 5 sources: jsonl, otlp-log, otlp-span-start, otlp-span-end, otlp-metric
- Move JSONL parsing logic (canonical, legacy NEEDLE, flat legacy) from
parser.ts into normalizer.ts
- Refactor parser.ts to thin facade delegating to normalizer
- Tailer now imports directly from normalizer (pure source, no parse coupling)
- Add 63 unit tests covering all source paths, edge cases, and cross-source
parity between JSONL and OTLP-log
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add NeedleEvent interface as the canonical internal shape versioned at
schema-version 1. Parser validates wire format and asserts schema version
on incoming events. Legacy LogEvent retained as backward-compatible adapter.
docs/schema.md documents all fields, the (worker_id, sequence) ordering
contract, and the full event taxonomy cross-referenced with NeedleEventType.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Budget panel (B key) to TUI with per-bead/per-worker cost tracking,
EMA-smoothed burn rate, time-series storage, and CostDashboard web component.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add types for fleet analytics dashboard:
- DurationBucket for bead completion histograms
- ModelPerformanceMetrics with duration distribution
- StrandMetrics for strand utilization tracking
- CompletionQualityIndicator for shallow completions
- FleetAnalytics aggregating all metrics
- FleetAnalyticsOptions for query configuration
Add CSS styles for analytics dashboard:
- Stats bar with quick metrics
- Tab navigation for Models/Strands/Quality/Fleet views
- Mini bar charts for duration distributions
- Sparkline charts for worker time series
- Quality cards for shallow completion tracking
- Responsive design for mobile/tablet views
Co-Authored-By: Claude Code (glm-5) <noreply@anthropic.com>
Feed replay events into the store's analytics pipeline via store.add()
in the onEvent callback, enabling worker analytics, error grouping,
cross-reference tracking, collision detection, and semantic narrative
generation during replay sessions. Print analytics summary on replay end.
Co-Authored-By: Claude Code (glm-5-turbo) <noreply@anthropic.com>
The header now displays a live badge showing worker counts by status:
- Green for active workers
- Blue for idle workers
- Red for error workers
Example: "FABRIC - Worker Activity Monitor [2 active | 1 idle]"
Changes:
- getWorkerStats() calculates total/active/idle/error counts
- getHeaderContent() builds colored badge with status breakdown
- updateHeader() refreshes the badge in default view
- Added updateHeader() call in addEvent() for real-time updates
- Fixed setViewMode() to use getHeaderContent() when returning to default view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify WorkerDetail displays correct worker information including status
(active/idle/error), uptime formatting, beads completed count, last
activity details (bead, tool, duration, error), and recent events with
proper color tags and truncation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create vitest E2E test that verifies WorkerGrid component renders
worker entries with correct status colors:
- Green (light-green-fg) for active workers
- Yellow (light-yellow-fg) for idle workers
- Red (light-red-fg) for error workers
The test mocks blessed screen and verifies the content contains
appropriate color tags matching the theme system's status colors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap os.homedir() in try/catch and guard against undefined return value
in ThemeManager.getConfigPath(). In CI environments, os.homedir() may be
undefined, causing path.join() to throw and preventing SessionReplay
from being constructed. When homedir is unavailable, theme persistence
is gracefully skipped and the default dark theme is used.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
persistSession() still used msg.includes('completed'/'finished'/'closed') to
find bead completion events. With NEEDLE format, msg is the event type string
(e.g. 'bead.completed'), so substring matching can misfire on
'bead.agent_completed'. Match on exact event types 'bead.completed' and
'bead.failed' instead — consistent with the fix already applied to
updateWorkerInfo() and trackTaskForHistory() in bd-vqd.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fixed corrupted auth middleware code in server.ts that was causing build failure
- Verified TUI color rendering works correctly (bd-2b3)
- Resolved bd-jec: Found 8 beads in ready queue, worker starvation was due to assignee filtering
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added authToken option to WebServerOptions interface
- Created createAuthMiddleware function for Bearer token auth
- Applied auth middleware to POST /api/events and /api/events/batch
- Updated CLI to read FABRIC_AUTH_TOKEN env var
- Added comprehensive tests for authentication scenarios
- Updated POST /api/events test cases to use auth token
- All tests passing successfully
- Add MAX_BATCH_SIZE constant (100 events limit)
- Implement POST /api/events/batch endpoint that accepts JSON array of events
- Validate array format, empty batches, and batch size limits
- Validate each event has required fields (ts, event)
- Store all valid events via store.add()
- Broadcast all ingested events via WebSocket
- Return 201 with ingested count, total count, and errors array
- Handle partial success (valid events processed, errors reported)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude Worker <noreply@anthropic.com>
Add HTTP POST endpoint to receive NEEDLE telemetry events from the
fabric.sh forwarder. This bridges NEEDLE and FABRIC, enabling real-time
event ingestion via HTTP.
Changes:
- Add parseEventObject() to parser.ts for parsing JSON objects directly
- Add POST /api/events endpoint with JSON body parser (64KB limit)
- Validate required fields (ts, event) before processing
- Store events and broadcast to WebSocket clients in real-time
- Return 201 Created on success, 400 for invalid payloads
Acceptance criteria met:
- NEEDLE events sent via curl POST arrive in FABRIC's event store
- Events are broadcast to WebSocket clients in real-time
- Invalid payloads return appropriate error codes (400)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add scripts/fabric-web.sh for managing FABRIC web server as tmux session
- Supports start/stop/restart/status/logs commands
- Auto-restarts on crash via tmux session management
- Configurable via FABRIC_PORT and FABRIC_LOG_PATH env vars
- Fix TypeScript build error in CommandPalette.test.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>