Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
entropic::ExternalBridge Class Reference

Unix socket MCP bridge for external client access. More...

#include <entropic/mcp/external_bridge.h>

Classes

struct  AsyncTask
 Async task state for background entropic.ask runs. More...
 

Public Member Functions

 ExternalBridge (entropic_handle_t handle, const ExternalMCPConfig &config, const std::filesystem::path &project_dir)
 Construct with engine handle and config.
 
 ~ExternalBridge ()
 Destructor — stop if running.
 
bool start ()
 Start the background accept loop.
 
void stop ()
 Stop the accept loop and close the socket.
 
const std::filesystem::path & socket_path () const
 Get the socket path (for logging/diagnostics).
 
nlohmann::json handle_ask_status (const nlohmann::json &args)
 Handle entropic.ask_status — check async task state.
 
void attach_phase_observer (const std::string &task_id)
 Run an async entropic.ask in a background thread.
 
void detach_phase_observer ()
 Clear the phase observer installed by attach_phase_observer.
 
void run_async_ask (const std::string &prompt, const std::string &task_id, int client_fd)
 Run an async entropic.ask in a detached background thread.
 
void write_sentinel (const std::string &task_id, const std::string &status)
 Write the sentinel file for an async task completion.
 
std::filesystem::path async_sentinel_dir () const
 Sentinel directory (lazy: returns empty path until the engine's log_dir is configured).
 
void set_async_sentinel_root (const std::filesystem::path &root)
 Override the async sentinel root directory.
 
void cleanup_expired_tasks ()
 Remove tasks older than TTL from the registry.
 
std::unordered_map< std::string, AsyncTask > & tasks_for_cancel ()
 Mutable accessor to the task registry.
 
const std::string & active_task_id_for_observer () const
 Return the currently-active async task_id (if any).
 
void subscribe (int fd)
 Add a connected fd to the subscriber set.
 
void unsubscribe (int fd)
 Remove an fd from the subscriber set.
 
void broadcast_notification (const nlohmann::json &notif)
 Write a JSON-RPC notification to every subscribed fd.
 
size_t subscriber_count () const
 Current subscriber count (for diagnostics / testing).
 
void update_task_phase (const std::string &task_id, const std::string &status, const std::string &phase)
 Update status/phase for a tracked task atomically.
 
bool observer_call_is_stale () const
 Test whether an in-flight observer callback is stale.
 

Public Attributes

std::mutex tasks_mutex_
 Async task mutex (public for dispatch_tool access).
 

Detailed Description

Unix socket MCP bridge for external client access.

Listens on a unix domain socket and dispatches JSON-RPC requests to the engine handle it was constructed with. Lifetime is tied to the engine handle — created in configure_common, destroyed before engine teardown.

Issue #12 (v2.1.4): also writes per-task sentinel files at <log_dir>/async/<task_id>.{done,failed,cancelled} so external monitors can use inotify rather than parsing log output.

Definition at line 68 of file external_bridge.h.

Constructor & Destructor Documentation

◆ ExternalBridge()

entropic::ExternalBridge::ExternalBridge ( entropic_handle_t  handle,
const ExternalMCPConfig config,
const std::filesystem::path &  project_dir 
)

Construct with engine handle and config.

Parameters
handleEngine handle (must outlive the bridge).
configExternal MCP configuration.
project_dirProject directory (for socket path derivation).
Version
2.0.8

Definition at line 564 of file external_bridge.cpp.

◆ ~ExternalBridge()

entropic::ExternalBridge::~ExternalBridge ( )

Destructor — stop if running.

Version
2.0.8

Definition at line 578 of file external_bridge.cpp.

Member Function Documentation

◆ active_task_id_for_observer()

const std::string & entropic::ExternalBridge::active_task_id_for_observer ( ) const
inline

Return the currently-active async task_id (if any).

Returns
Task id or empty string. Caller MUST hold tasks_mutex_. @utility
Version
2.0.6-rc16.2

Definition at line 251 of file external_bridge.h.

◆ async_sentinel_dir()

std::filesystem::path entropic::ExternalBridge::async_sentinel_dir ( ) const

Sentinel directory (lazy: returns empty path until the engine's log_dir is configured).

Resolve the async sentinel directory.

Issue #12 (v2.1.4). Lives at <log_dir>/async/ by default (or <override>/async/ if set_async_sentinel_root was called). The bridge does not own log_dir lifetime — the path is recomputed each call so working-dir swaps via configure_dir take effect on the next async task.

Returns
Absolute sentinel directory path. Empty if neither log_dir nor an override is configured. @utility
Version
2.1.4

Override (set via set_async_sentinel_root) takes precedence; falls back to <handle_->config.log_dir>/async. Returns an empty path if neither source is configured. Issue #12 (v2.1.4).

Definition at line 1347 of file external_bridge.cpp.

◆ attach_phase_observer()

void entropic::ExternalBridge::attach_phase_observer ( const std::string &  task_id)

Run an async entropic.ask in a background thread.

Install phase observer scoped to one task.

Parameters
promptUser prompt.
task_idAssigned task ID.
client_fdSocket fd for completion notification.

Install VERIFYING-state observer scoped to one task.

Registers a state-change observer on the handle and records task_id as the active task. The observer maps VERIFYING transitions onto phase="validating" (first) or "revising" (subsequent). Pair with detach_phase_observer(). (P1-5 follow-up, 2.0.6-rc16.2)

Parameters
task_idTask whose phase will be updated.

Increments observer_gen_ under tasks_mutex_ and captures it as attached_gen_ so phase_observer_cb can detect stale post-detach fires via a simple generation comparison. (E5+E6, 2.1.0)

Parameters
task_idTask whose phase transitions the observer tracks.

Definition at line 1065 of file external_bridge.cpp.

◆ broadcast_notification()

void entropic::ExternalBridge::broadcast_notification ( const nlohmann::json &  notif)

Write a JSON-RPC notification to every subscribed fd.

Broadcast a JSON-RPC notification to every subscriber.

Writes are serialized under subscribers_mutex_ so concurrent broadcasts do not interleave on the same fd. Fds that fail to write are removed from the set.

Parameters
notifJSON-RPC notification object.

Snapshots the subscriber set under the lock, releases the lock before writing, then re-acquires to remove dead fds. This prevents one blocked or slow client from stalling all other broadcasts.

Issue #4 (v2.1.2, part C): write is non-blocking via send(MSG_DONTWAIT). A subscriber whose recv buffer is full (slow / non-draining consumer) returns EAGAIN/EWOULDBLOCK and is dropped on the same path as EBADF/EPIPE. Pre-2.1.2 the broadcast used blocking write(), which let one stalled consumer wedge the async-task thread that emitted the notification (the field-observed deadlock against entropic-explorer ↔ Claude Code; see issue #4 reproduction).

A partial write is also treated as a drop. The dropped subscriber loses the in-flight notification — acceptable because (a) post-#4 notifications are tiny progress signals (consumers fetch state via ask_status, not from the notification body), and (b) a subscriber that can't accept ~200 bytes of buffered notification is unhealthy anyway. The longer-term replacement is a per-subscriber outbound queue + writer thread (see proposal .claude/proposals/BACKLOG/P2-20260429-001-async-bridge-io-architecture.md).

Subscribers added between snapshot and dead-fd cleanup miss this broadcast — correct, as they subscribed after the message was initiated.

Parameters
notifJSON-RPC notification object.

Definition at line 1251 of file external_bridge.cpp.

◆ cleanup_expired_tasks()

void entropic::ExternalBridge::cleanup_expired_tasks ( )

Remove tasks older than TTL from the registry.

Remove tasks older than 15 minutes from the registry, AND delete their sentinel files if present.

Issue #12 (v2.1.4): sentinel files would otherwise accumulate in <log_dir>/async/ indefinitely. Cleanup uses the same TTL as the in-memory registry so an external monitor that consumed the sentinel within 15 minutes still sees a consistent picture.

Definition at line 1315 of file external_bridge.cpp.

◆ detach_phase_observer()

void entropic::ExternalBridge::detach_phase_observer ( )

Clear the phase observer installed by attach_phase_observer.

Clear phase observer and the active-task pointer.

Increments observer_gen_ under the lock before clearing active_task_id_ — any in-flight phase_observer_cb will see a generation mismatch and return immediately without accessing stale state. entropic_set_state_observer(nullptr) is called after the lock is released. (E5+E6, 2.1.0)

Definition at line 1086 of file external_bridge.cpp.

◆ handle_ask_status()

json entropic::ExternalBridge::handle_ask_status ( const nlohmann::json &  args)

Handle entropic.ask_status — check async task state.

Parameters
argsTool arguments (JSON with task_id).
Returns
MCP tool result JSON.

Returns coarse status + granular phase so pollers see progression through queued → running → running:<tier> → done|failed|cancelled.

Parameters
argsTool arguments (must contain "task_id").
Returns
MCP tool result JSON with status/phase/result/error.

Definition at line 460 of file external_bridge.cpp.

◆ observer_call_is_stale()

bool entropic::ExternalBridge::observer_call_is_stale ( ) const
inline

Test whether an in-flight observer callback is stale.

Called from phase_observer_cb (free function) under tasks_mutex_. Returns true if observer_gen_ has been bumped since this callback's attach point — indicating detach_phase_observer (or a re-attach for a different task) has run and the firing callback should exit without touching state. (E5+E6, 2.1.0)

Returns
true if the firing observer call should be discarded. @utility
Version
2.1.0

Definition at line 325 of file external_bridge.h.

◆ run_async_ask()

void entropic::ExternalBridge::run_async_ask ( const std::string &  prompt,
const std::string &  task_id,
int  client_fd 
)

Run an async entropic.ask in a detached background thread.

Creates a task registry entry, runs the engine, stores the result, writes the per-task sentinel file (#12, v2.1.4), and broadcasts notifications/progress to subscribed clients.

Parameters
promptUser prompt.
task_idAssigned task ID.
client_fdSocket fd for completion notification.

Definition at line 1108 of file external_bridge.cpp.

◆ set_async_sentinel_root()

void entropic::ExternalBridge::set_async_sentinel_root ( const std::filesystem::path &  root)

Override the async sentinel root directory.

Issue #12 (v2.1.4): primarily a testability hook so unit tests can exercise the sentinel write path without setting up a full engine handle. Production code receives the path from the engine handle's log_dir; passing an empty path here clears the override and restores log_dir resolution.

Parameters
rootNew sentinel root (the bridge appends /async). @utility
Version
2.1.4

Issue #12 (v2.1.4).

Definition at line 1360 of file external_bridge.cpp.

◆ socket_path()

const std::filesystem::path & entropic::ExternalBridge::socket_path ( ) const
inline

Get the socket path (for logging/diagnostics).

Returns
Socket path. @utility
Version
2.0.8

Definition at line 103 of file external_bridge.h.

◆ start()

bool entropic::ExternalBridge::start ( )

Start the background accept loop.

Returns
true if the socket was created and listening.
Version
2.0.8
Returns
true if the socket was created and listening.

Definition at line 722 of file external_bridge.cpp.

◆ stop()

void entropic::ExternalBridge::stop ( )

Stop the accept loop and close the socket.

Stop the accept loop, drain client threads, and close the socket.

Version
2.0.8

Issue #4 (v2.1.2, part D): pre-2.1.2 stop() only joined the accept thread because serve_client ran inline. Now each connected client has its own thread blocking in read(); we wake them via shutdown(SHUT_RDWR) on the client fd (which causes the pending read to return EOF) and then join. Order matters:

  1. running_ = false (accept_loop will exit on next poll cycle)
  2. close(listen_fd_) (stops new connections; in-flight clients are unaffected)
  3. join accept_thread_ (no more clients spawn after this)
  4. shutdown each client fd to wake blocking read()s
  5. join each client thread
  6. close client fds (the threads' RAII guards do this too, but we run it again as a defensive measure on shutdown errors)

Definition at line 792 of file external_bridge.cpp.

◆ subscribe()

void entropic::ExternalBridge::subscribe ( int  fd)

Add a connected fd to the subscriber set.

Remove tasks older than 15 minutes from the registry.

Parameters
fdConnected client socket fd.

Add a connected fd to the subscriber set.

Parameters
fdClient socket fd.

Definition at line 1202 of file external_bridge.cpp.

◆ subscriber_count()

size_t entropic::ExternalBridge::subscriber_count ( ) const
inline

Current subscriber count (for diagnostics / testing).

Returns
Number of connected clients currently subscribed. @utility
Version
2.0.6-rc16

Definition at line 290 of file external_bridge.h.

◆ tasks_for_cancel()

std::unordered_map< std::string, AsyncTask > & entropic::ExternalBridge::tasks_for_cancel ( )
inline

Mutable accessor to the task registry.

Caller MUST hold tasks_mutex_. Used by the cancel-on-clear path to flip task statuses atomically with the mutex held. Contract verified against all call sites for v2.1.0 (E8). (P1-8, 2.0.6-rc16)

Returns
Reference to task registry. @utility
Version
2.1.0

Definition at line 241 of file external_bridge.h.

◆ unsubscribe()

void entropic::ExternalBridge::unsubscribe ( int  fd)

Remove an fd from the subscriber set.

Parameters
fdClient socket fd being closed.

Definition at line 1213 of file external_bridge.cpp.

◆ update_task_phase()

void entropic::ExternalBridge::update_task_phase ( const std::string &  task_id,
const std::string &  status,
const std::string &  phase 
)

Update status/phase for a tracked task atomically.

Update the status/phase of a tracked task.

Used by the async run thread to advance through queued → running → running:<tier> → done|failed|cancelled. Safe to call with an unknown task_id (no-op).

Parameters
task_idTask identifier.
statusNew coarse status string.
phaseNew granular phase string. @utility
Version
2.0.6-rc16
Parameters
task_idTask identifier.
statusNew coarse status string.
phaseNew granular phase string.

Definition at line 1293 of file external_bridge.cpp.

◆ write_sentinel()

void entropic::ExternalBridge::write_sentinel ( const std::string &  task_id,
const std::string &  status 
)

Write the sentinel file for an async task completion.

Issue #12 (v2.1.4): persistent on-disk completion signal so external monitors (CLI scripts, watchdogs) can inotifywait on the async sentinel directory without parsing log output or subscribing to MCP notifications. Sentinel filename encodes the terminal status: <task_id>.done, .failed, .cancelled.

Caller MUST hold tasks_mutex_. The sentinel write happens under-lock together with the status update so that any external monitor reacting to the sentinel sees a consistent registry state on a follow-up entropic.ask_status query.

Parameters
task_idTask identifier.
statusTerminal status string (done | error | cancelled). error maps to .failed. @utility
Version
2.1.4

Issue #12 (v2.1.4). Caller MUST hold tasks_mutex_ — this is invoked inside run_async_ask's terminal critical section so external monitors observe a consistent sentinel + registry pair.

Failure to create the parent directory or open the file is logged at WARN and swallowed; the MCP notification path remains the primary signal and a missing sentinel is non-fatal for callers that don't depend on it.

Definition at line 1396 of file external_bridge.cpp.

Member Data Documentation

◆ tasks_mutex_

std::mutex entropic::ExternalBridge::tasks_mutex_
mutable

Async task mutex (public for dispatch_tool access).

Definition at line 211 of file external_bridge.h.


The documentation for this class was generated from the following files: