|
Entropic 2.11.1
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). | |
| bool | ask_streaming () const |
| Whether entropic.ask routes through entropic_run_streaming. | |
| 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.
@dg_internal
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). |
| handle | Engine handle (must outlive the bridge). |
| config | External MCP configuration. |
| project_dir | Project directory (for socket path derivation). @req REQ-BRIDGE-001 |
Definition at line 604 of file external_bridge.cpp.
| entropic::ExternalBridge::~ExternalBridge | ( | ) |
Destructor — stop if running.
@req REQ-BRIDGE-001
Definition at line 619 of file external_bridge.cpp.
|
inline |
Return the currently-active async task_id (if any).
Definition at line 265 of file external_bridge.h.
|
inline |
Whether entropic.ask routes through entropic_run_streaming.
When false, handle_ask uses entropic_run (no on_token callback), which is compatible with MTP and avoids the streaming-path 316 throw (gh#115, v2.9.12). Static-dispatch helper for file-scope functions that hold ExternalBridge*.
Definition at line 117 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 1390 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. @dg_internal |
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. @dg_internal |
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. @req REQ-BRIDGE-001 |
Definition at line 1106 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. @dg_internal (public for unit test access) |
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. @req REQ-BRIDGE-001 |
Definition at line 1292 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.
@dg_internal
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.
@req REQ-BRIDGE-001
Definition at line 1356 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.
@dg_internal
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)
@req REQ-BRIDGE-001
Definition at line 1127 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 498 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 339 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. @req REQ-BRIDGE-001 |
Definition at line 1149 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). @req REQ-BRIDGE-001
Definition at line 1403 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 763 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:
@req REQ-BRIDGE-001
Definition at line 833 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. @dg_internal (public for unit test access) |
@dg_internal
Add a connected fd to the subscriber set.
| fd | Client socket fd. @req REQ-BRIDGE-001 |
Definition at line 1243 of file external_bridge.cpp.
|
inline |
Current subscriber count (for diagnostics / testing).
Definition at line 304 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 255 of file external_bridge.h.
| void entropic::ExternalBridge::unsubscribe | ( | int | fd | ) |
Remove an fd from the subscriber set.
| fd | Client socket fd being closed. @dg_internal (public for unit test access) |
| fd | Client socket fd being closed. @req REQ-BRIDGE-001 |
Definition at line 1254 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. @req REQ-BRIDGE-001 |
Definition at line 1334 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.
@req REQ-BRIDGE-001
Definition at line 1439 of file external_bridge.cpp.
|
mutable |
Async task mutex (public for dispatch_tool access).
Definition at line 225 of file external_bridge.h.