|
Entropic 2.3.8
Local-first agentic inference engine
|
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 ¬if) |
| 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). | |
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.
| entropic::ExternalBridge::ExternalBridge | ( | entropic_handle_t | handle, |
| const ExternalMCPConfig & | config, | ||
| const std::filesystem::path & | project_dir | ||
| ) |
Construct with engine handle and config.
| handle | Engine handle (must outlive the bridge). |
| config | External MCP configuration. |
| project_dir | Project directory (for socket path derivation). |
Definition at line 564 of file external_bridge.cpp.
| entropic::ExternalBridge::~ExternalBridge | ( | ) |
|
inline |
Return the currently-active async task_id (if any).
Definition at line 251 of file external_bridge.h.
| 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.
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.
| 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.
| prompt | User prompt. |
| task_id | Assigned task ID. |
| client_fd | Socket 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)
| task_id | Task 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)
| task_id | Task whose phase transitions the observer tracks. |
Definition at line 1065 of file external_bridge.cpp.
| 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.
| notif | JSON-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.
| notif | JSON-RPC notification object. |
Definition at line 1251 of file external_bridge.cpp.
| 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.
| 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.
| json entropic::ExternalBridge::handle_ask_status | ( | const nlohmann::json & | args | ) |
Handle entropic.ask_status — check async task state.
| args | Tool arguments (JSON with task_id). |
Returns coarse status + granular phase so pollers see progression through queued → running → running:<tier> → done|failed|cancelled.
| args | Tool arguments (must contain "task_id"). |
Definition at line 460 of file external_bridge.cpp.
|
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)
Definition at line 325 of file external_bridge.h.
| 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.
| prompt | User prompt. |
| task_id | Assigned task ID. |
| client_fd | Socket fd for completion notification. |
Definition at line 1108 of file external_bridge.cpp.
| 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.
| root | New sentinel root (the bridge appends /async). @utility |
Issue #12 (v2.1.4).
Definition at line 1360 of file external_bridge.cpp.
|
inline |
Get the socket path (for logging/diagnostics).
Definition at line 103 of file external_bridge.h.
| bool entropic::ExternalBridge::start | ( | ) |
Start the background accept loop.
Definition at line 722 of file external_bridge.cpp.
| void entropic::ExternalBridge::stop | ( | ) |
Stop the accept loop and close the socket.
Stop the accept loop, drain client threads, and close the socket.
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:
Definition at line 792 of file external_bridge.cpp.
| void entropic::ExternalBridge::subscribe | ( | int | fd | ) |
Add a connected fd to the subscriber set.
Remove tasks older than 15 minutes from the registry.
| fd | Connected client socket fd. |
Add a connected fd to the subscriber set.
| fd | Client socket fd. |
Definition at line 1202 of file external_bridge.cpp.
|
inline |
Current subscriber count (for diagnostics / testing).
Definition at line 290 of file external_bridge.h.
|
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)
Definition at line 241 of file external_bridge.h.
| void entropic::ExternalBridge::unsubscribe | ( | int | fd | ) |
Remove an fd from the subscriber set.
| fd | Client socket fd being closed. |
Definition at line 1213 of file external_bridge.cpp.
| 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).
| task_id | Task identifier. |
| status | New coarse status string. |
| phase | New granular phase string. @utility |
| task_id | Task identifier. |
| status | New coarse status string. |
| phase | New granular phase string. |
Definition at line 1293 of file external_bridge.cpp.
| 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.
| task_id | Task identifier. |
| status | Terminal status string (done | error | cancelled). error maps to .failed. @utility |
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.
|
mutable |
Async task mutex (public for dispatch_tool access).
Definition at line 211 of file external_bridge.h.