integrate: resolve header collisions, keep both plexer-flow drivers in separate namespaces, rename shell pty to ptyproc, link -lz -pthread -lutil

Everything compiles; main.cpp still uses the removed placeholder agent API.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 06:48:34 +01:00
co-authored by Claude Sonnet 5.5
parent 2308e9e3df
commit f44e505b0f
23 changed files with 1401 additions and 1156 deletions
+3 -3
View File
@@ -27,17 +27,17 @@ $(BUILD)/main.o: main.cpp $(VERSION_HPP)
$(CXX) $(CXXFLAGS) -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) -c main.cpp -o $@
$(BIN): $(MAIN_OBJ) $(ALL_OBJS)
$(CXX) $(CXXFLAGS) $(MAIN_OBJ) $(ALL_OBJS) -lsqlite3 -lcurl -o $@
$(CXX) $(CXXFLAGS) $(MAIN_OBJ) $(ALL_OBJS) -lsqlite3 -lcurl -lz -pthread -lutil -o $@
static: $(MAIN_OBJ) $(ALL_OBJS)
$(CXX) $(CXXFLAGS) -pthread -static $(MAIN_OBJ) $(ALL_OBJS) third_party/curl/libcurl.a third_party/sqlite/libsqlite3.a -lssl -lcrypto third_party/jitterentropy/libjitterentropy.a -lzstd -lz -lnghttp2 -lpsl -lbrotlidec -lidn2 -lrtmp -o build/detz-static
$(BUILD)/store_test: tests/store_test.cpp src/store/store.cpp
@mkdir -p $(BUILD)
$(CXX) $(CXXFLAGS) -Isrc/store tests/store_test.cpp src/store/store.cpp -lsqlite3 -lcurl -o $@
$(CXX) $(CXXFLAGS) -Isrc/store tests/store_test.cpp src/store/store.cpp -lsqlite3 -lcurl -lz -pthread -lutil -o $@
$(BUILD)/detz_test: tests/test_main.cpp $(ALL_SRCS) $(VERSION_HPP)
@mkdir -p $(BUILD)
$(CXX) $(CXXFLAGS) -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) tests/test_main.cpp $(ALL_SRCS) -lsqlite3 -lcurl -o $@
$(CXX) $(CXXFLAGS) -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) tests/test_main.cpp $(ALL_SRCS) -lsqlite3 -lcurl -lz -pthread -lutil -o $@
lint:
$(CXX) $(CXXSTD) $(WARN) -Werror -fsyntax-only main.cpp -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) || true
+2 -2
View File
@@ -6,10 +6,10 @@ SRCS += src/mplex/mplex_json.cpp src/mplex/mplex_keys.cpp src/mplex/mplex_layout
SRCS += src/mplex/mplex_options.cpp src/mplex/mplex_pane.cpp src/mplex/mplex_panenumbers.cpp src/mplex/mplex_prompt.cpp
SRCS += src/mplex/mplex_py.cpp src/mplex/mplex_renderer.cpp src/mplex/mplex_server.cpp src/mplex/mplex_style.cpp
SRCS += src/mplex/mplex_toml.cpp src/mplex/mplex_vtscreen.cpp
SRCS += src/plexer/plexer_flow.cpp
SRCS += src/mplex/mplex_plexer_flow.cpp
# Standalone client/server binary and its tests; every output lands in $(BUILD).
MPLEX_LIB_SRCS = $(filter src/mplex/%.cpp src/plexer/plexer_flow.cpp,$(SRCS))
MPLEX_LIB_SRCS = $(filter src/mplex/%.cpp src/mplex/mplex_plexer_flow.cpp,$(SRCS))
MPLEX_INC = -Isrc/mplex -Isrc/plexer
$(BUILD)/mplex_cli: tests/mplex_cli_main.cpp $(MPLEX_LIB_SRCS)
@mkdir -p $(BUILD)
File diff suppressed because it is too large Load Diff
+156
View File
@@ -0,0 +1,156 @@
// uiflow (uiflow.md): the controller side of the plexer protocol (PlexerController, its native
// and tmux backends, the factory), the plexer_show spec validation and the PlexerFlow driver.
// Port of tai.py:9302-9985. Namespace detz::plexer, names are the Python ones.
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "mplex_json.h"
namespace detz::mplex_flow {
using detz::mplex::Json;
constexpr const char* PLEXER_DIR_NAME = "plexer";
constexpr const char* PLEXER_PANE_ENV = "TAI_PLEXER_PANE";
constexpr const char* PLEXER_SOCKET_ENV = "TAI_PLEXER_SOCKET";
constexpr const char* PLEXER_ACTIVE_ENV = "TAI_PLEXER_ACTIVE";
constexpr long PLEXER_RECV_MAX = 1L << 20;
constexpr int PLEXER_FLOW_VERSION = 1;
constexpr int PLEXER_FLOW_SCREENS_MAX = 32;
constexpr int PLEXER_FLOW_FIELDS_MAX = 12;
constexpr int PLEXER_FLOW_OPTIONS_MAX = 12;
constexpr int PLEXER_FLOW_ACTIONS_MAX = 12;
constexpr int PLEXER_FLOW_VISITS_MAX = 64;
constexpr int PLEXER_FLOW_ID_MAX = 64;
constexpr int PLEXER_FLOW_TITLE_MAX = 200;
constexpr int PLEXER_FLOW_TEXT_MAX = 2000;
constexpr int PLEXER_FLOW_LABEL_MAX = 200;
constexpr int PLEXER_FLOW_ANSWER_MAX = 2000;
constexpr int PLEXER_FLOW_TIMEOUT_DEFAULT = 300;
constexpr int PLEXER_FLOW_TIMEOUT_MIN = 5;
constexpr int PLEXER_FLOW_TIMEOUT_MAX = 600;
constexpr double PLEXER_FLOW_POLL = 0.25;
extern const std::vector<std::string> PLEXER_FLOW_TYPES; // choice text info
extern const std::vector<std::string> PLEXER_FLOW_KINDS; // message choice text
constexpr int MPLEX_FLOW_MAX = 16;
constexpr int MPLEX_FLOW_KEEP = 300;
// plexer_dir(home): <home>/plexer, created 0700.
std::string plexer_dir(const std::string& home);
// Raised by a PlexerController when the backend can't be reached or refuses a command.
class PlexerError : public std::runtime_error {
public:
explicit PlexerError(const std::string& text) : std::runtime_error(text) {}
};
// ---- controller side ---------------------------------------------------------------------
class PlexerController {
public:
virtual ~PlexerController() = default;
std::string kind = "abstract";
virtual Json send_keys(const std::string& text, bool enter = false) = 0;
virtual Json read_pane(int lines = 200) = 0;
void type_text(const std::string& text, double delay = 0.03, bool enter = true);
virtual Json status() = 0;
// One blocking question on the user's terminal. screen: {"title","text","kind","options","initial","default"}.
// Returns {"status":"ok","value":...} / {"status":"cancelled"} / {"status":"timeout"}; throws PlexerError.
virtual Json ask(const Json& screen, double timeout) = 0;
};
class NativePlexerController : public PlexerController {
public:
NativePlexerController(const std::string& socket_path_in, const std::optional<std::string>& pane_in = std::nullopt);
std::string socket_path;
std::optional<std::string> pane;
Json call(const Json& payload); // Python _call
Json send_keys(const std::string& text, bool enter = false) override;
Json read_pane(int lines = 200) override;
Json status() override;
Json ask(const Json& screen, double timeout) override;
};
class TmuxPlexerController : public PlexerController {
public:
explicit TmuxPlexerController(const std::string& pane_in);
std::string pane;
std::string tmux(const std::vector<std::string>& args); // Python _tmux
Json send_keys(const std::string& text, bool enter = false) override;
Json read_pane(int lines = 200) override;
Json status() override;
Json ask(const Json& screen, double timeout) override;
std::string ask_result_path();
std::optional<std::string> await_answer(const std::string& path, double budget = 1.0);
void ask_dismiss();
// Returns the answer file content, nullopt when dismissed, or "timeout" via timed_out.
struct RunOutcome {
bool timed_out = false;
std::optional<std::string> content;
};
RunOutcome ask_run(const std::vector<std::string>& argv, const std::string& path, double timeout);
Json ask_choice(const Json& screen, double timeout);
RunOutcome ask_popup(const std::string& title, const std::string& body, const std::string& reader, double timeout);
Json ask_text(const Json& screen, double timeout);
Json ask_message(const Json& screen, double timeout);
};
class PlexerControllerFactory {
public:
// Which env var is set decides the backend; null when neither is.
static std::unique_ptr<PlexerController> detect();
};
// ---- spec validation ------------------------------------------------------------------------
// Each returns the normalized value and an error text ("" when fine), like the Python tuples.
std::pair<std::string, std::string> validate_flow_id(const Json& raw, const std::string& what);
std::pair<std::string, std::string> validate_flow_text(const Json& raw, const std::string& what, size_t limit);
std::pair<Json, std::string> validate_flow_options(const Json& raw, const std::string& what);
std::pair<Json, std::string> validate_flow_condition(const Json& raw, const std::string& what,
const std::vector<std::string>& known);
// Returns (normalized app, "") or (Json null, reason).
std::pair<Json, std::string> validate_flow_spec(const Json& spec);
// ---- the flow driver ----------------------------------------------------------------------------
class PlexerFlow {
public:
explicit PlexerFlow(const Json& app);
Json app;
std::map<std::string, Json> by_id;
std::vector<std::string> order;
Json answers = Json::object();
Json action; // None until an action is taken
int visits = 0;
double deadline = 0.0;
static bool condition_holds(const Json& clauses, const Json& answers);
std::string follow_up(const Json& screen);
std::string screen_text(const Json& screen, const Json* field = nullptr);
const Json* first_askable(const Json& screen);
double remaining() const;
Json run(PlexerController& controller);
// Returns: null optional to continue with follow_up, a screen id, or a final outcome.
struct ScreenResult {
enum class Kind { Continue, Jump, Outcome } kind = Kind::Continue;
std::string target;
Json outcome;
};
ScreenResult run_screen(PlexerController& controller, const Json& screen);
};
// PlexerBackend (the extension point the shell hook execs into).
class PlexerBackend {
public:
virtual ~PlexerBackend() = default;
virtual int run() = 0;
};
} // namespace detz::mplex_flow
File diff suppressed because it is too large Load Diff
+35 -143
View File
@@ -1,156 +1,48 @@
// uiflow (uiflow.md): the controller side of the plexer protocol (PlexerController, its native
// and tmux backends, the factory), the plexer_show spec validation and the PlexerFlow driver.
// Port of tai.py:9302-9985. Namespace detz::plexer, names are the Python ones.
#pragma once
#include <functional>
#include <chrono>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "mplex_json.h"
#include "plexer_controller.h"
#include "store/json.h"
namespace detz::plexer {
using detz::mplex::Json;
constexpr const char* PLEXER_DIR_NAME = "plexer";
constexpr const char* PLEXER_PANE_ENV = "TAI_PLEXER_PANE";
constexpr const char* PLEXER_SOCKET_ENV = "TAI_PLEXER_SOCKET";
constexpr const char* PLEXER_ACTIVE_ENV = "TAI_PLEXER_ACTIVE";
constexpr long PLEXER_RECV_MAX = 1L << 20;
constexpr int PLEXER_FLOW_VERSION = 1;
constexpr int PLEXER_FLOW_SCREENS_MAX = 32;
constexpr int PLEXER_FLOW_FIELDS_MAX = 12;
constexpr int PLEXER_FLOW_OPTIONS_MAX = 12;
constexpr int PLEXER_FLOW_ACTIONS_MAX = 12;
constexpr int PLEXER_FLOW_VISITS_MAX = 64;
constexpr int PLEXER_FLOW_ID_MAX = 64;
constexpr int PLEXER_FLOW_TITLE_MAX = 200;
constexpr int PLEXER_FLOW_TEXT_MAX = 2000;
constexpr int PLEXER_FLOW_LABEL_MAX = 200;
constexpr int PLEXER_FLOW_ANSWER_MAX = 2000;
constexpr int PLEXER_FLOW_TIMEOUT_DEFAULT = 300;
constexpr int PLEXER_FLOW_TIMEOUT_MIN = 5;
constexpr int PLEXER_FLOW_TIMEOUT_MAX = 600;
constexpr double PLEXER_FLOW_POLL = 0.25;
extern const std::vector<std::string> PLEXER_FLOW_TYPES; // choice text info
extern const std::vector<std::string> PLEXER_FLOW_KINDS; // message choice text
constexpr int MPLEX_FLOW_MAX = 16;
constexpr int MPLEX_FLOW_KEEP = 300;
// plexer_dir(home): <home>/plexer, created 0700.
std::string plexer_dir(const std::string& home);
// Raised by a PlexerController when the backend can't be reached or refuses a command.
class PlexerError : public std::runtime_error {
public:
explicit PlexerError(const std::string& text) : std::runtime_error(text) {}
};
// ---- controller side ---------------------------------------------------------------------
class PlexerController {
public:
virtual ~PlexerController() = default;
std::string kind = "abstract";
virtual Json send_keys(const std::string& text, bool enter = false) = 0;
virtual Json read_pane(int lines = 200) = 0;
void type_text(const std::string& text, double delay = 0.03, bool enter = true);
virtual Json status() = 0;
// One blocking question on the user's terminal. screen: {"title","text","kind","options","initial","default"}.
// Returns {"status":"ok","value":...} / {"status":"cancelled"} / {"status":"timeout"}; throws PlexerError.
virtual Json ask(const Json& screen, double timeout) = 0;
};
class NativePlexerController : public PlexerController {
public:
NativePlexerController(const std::string& socket_path_in, const std::optional<std::string>& pane_in = std::nullopt);
std::string socket_path;
std::optional<std::string> pane;
Json call(const Json& payload); // Python _call
Json send_keys(const std::string& text, bool enter = false) override;
Json read_pane(int lines = 200) override;
Json status() override;
Json ask(const Json& screen, double timeout) override;
};
class TmuxPlexerController : public PlexerController {
public:
explicit TmuxPlexerController(const std::string& pane_in);
std::string pane;
std::string tmux(const std::vector<std::string>& args); // Python _tmux
Json send_keys(const std::string& text, bool enter = false) override;
Json read_pane(int lines = 200) override;
Json status() override;
Json ask(const Json& screen, double timeout) override;
std::string ask_result_path();
std::optional<std::string> await_answer(const std::string& path, double budget = 1.0);
void ask_dismiss();
// Returns the answer file content, nullopt when dismissed, or "timeout" via timed_out.
struct RunOutcome {
bool timed_out = false;
std::optional<std::string> content;
};
RunOutcome ask_run(const std::vector<std::string>& argv, const std::string& path, double timeout);
Json ask_choice(const Json& screen, double timeout);
RunOutcome ask_popup(const std::string& title, const std::string& body, const std::string& reader, double timeout);
Json ask_text(const Json& screen, double timeout);
Json ask_message(const Json& screen, double timeout);
};
class PlexerControllerFactory {
public:
// Which env var is set decides the backend; null when neither is.
static std::unique_ptr<PlexerController> detect();
};
// ---- spec validation ------------------------------------------------------------------------
// Each returns the normalized value and an error text ("" when fine), like the Python tuples.
std::pair<std::string, std::string> validate_flow_id(const Json& raw, const std::string& what);
std::pair<std::string, std::string> validate_flow_text(const Json& raw, const std::string& what, size_t limit);
std::pair<Json, std::string> validate_flow_options(const Json& raw, const std::string& what);
std::pair<Json, std::string> validate_flow_condition(const Json& raw, const std::string& what,
const std::vector<std::string>& known);
// Returns (normalized app, "") or (Json null, reason).
std::pair<Json, std::string> validate_flow_spec(const Json& spec);
// ---- the flow driver ----------------------------------------------------------------------------
// The driver of one UI flow: the main agent hands over a whole validated app
// in a single plexer_show call, this decomposes it into PlexerController::ask
// calls, evaluates conditions and branches locally, and reports only the
// final outcome (uiflow.md).
class PlexerFlow {
public:
explicit PlexerFlow(const Json& app);
Json app;
std::map<std::string, Json> by_id;
std::vector<std::string> order;
Json answers = Json::object();
Json action; // None until an action is taken
int visits = 0;
double deadline = 0.0;
static bool condition_holds(const Json& clauses, const Json& answers);
std::string follow_up(const Json& screen);
std::string screen_text(const Json& screen, const Json* field = nullptr);
const Json* first_askable(const Json& screen);
double remaining() const;
using Json = store::Json;
explicit PlexerFlow(Json app);
PlexerFlow(const PlexerFlow&) = delete;
PlexerFlow& operator=(const PlexerFlow&) = delete;
// {"status": done|cancelled|timeout|error, "answers": {...}, "action": ..., "at": ..., "field": ...}
Json run(PlexerController& controller);
// Returns: null optional to continue with follow_up, a screen id, or a final outcome.
struct ScreenResult {
enum class Kind { Continue, Jump, Outcome } kind = Kind::Continue;
std::string target;
Json outcome;
};
ScreenResult run_screen(PlexerController& controller, const Json& screen);
};
// PlexerBackend (the extension point the shell hook execs into).
class PlexerBackend {
public:
virtual ~PlexerBackend() = default;
virtual int run() = 0;
// answers.get(key, MISSING) in accepted for every clause
static bool condition_holds(const Json& clauses, const Json& answers);
private:
struct Outcome {
enum class Kind { Continue, Jump, Final } kind{Kind::Continue};
std::string target;
Json result;
};
std::string follow_up(const Json& screen) const;
std::string screen_text(const Json& screen, int field_index = -1) const;
int first_askable(const Json& screen) const; // index into fields, -1 for none
double remaining() const;
Outcome run_screen(PlexerController& controller, const Json& screen);
Json result_with(const std::string& status, const std::string& at, const std::string& field = "") const;
Json app_;
std::map<std::string, const Json*> by_id_;
std::vector<std::string> order_;
Json answers_ = Json::object();
Json action_;
int visits_{0};
std::chrono::steady_clock::time_point deadline_;
};
} // namespace detz::plexer
+2 -2
View File
@@ -1,7 +1,7 @@
#include "audio.h"
#include "proc.h"
#include "text.h"
#include "utf8.h"
#include "../util/text.h"
#include "../util/utf8.h"
#include <cstdio>
#include <ctime>
#include <unistd.h>
+1 -1
View File
@@ -1,6 +1,6 @@
#include "edge.h"
#include "digest.h"
#include "utf8.h"
#include "../util/utf8.h"
#include <cerrno>
#include <cmath>
#include <cstdint>
+2 -2
View File
@@ -1,9 +1,9 @@
#include "glow.h"
#include "ansi.h"
#include "pypath.h"
#include "text.h"
#include "../util/text.h"
#include "toolargs.h"
#include "utf8.h"
#include "../util/utf8.h"
#include <cstdio>
#include <map>
namespace detz::shell {
+2 -2
View File
@@ -1,7 +1,7 @@
#include "livestream.h"
#include "ansi.h"
#include "text.h"
#include "utf8.h"
#include "../util/text.h"
#include "../util/utf8.h"
#include <algorithm>
#include <cstdio>
namespace detz::shell {
+1 -1
View File
@@ -1,5 +1,5 @@
#include "proc.h"
#include "utf8.h"
#include "../util/utf8.h"
#include "jsonvalue.h"
#include <algorithm>
#include <cerrno>
@@ -1,4 +1,4 @@
#include "pty.h"
#include "ptyproc.h"
#include <cerrno>
#include <cstdlib>
#include <fcntl.h>
+2 -2
View File
@@ -1,6 +1,6 @@
#include "runlive.h"
#include "pty.h"
#include "utf8.h"
#include "ptyproc.h"
#include "../util/utf8.h"
#include <algorithm>
#include <condition_variable>
#include <deque>
+2 -2
View File
@@ -1,8 +1,8 @@
#include "sandbox.h"
#include "ansi.h"
#include "boxdata.h"
#include "text.h"
#include "utf8.h"
#include "../util/text.h"
#include "../util/utf8.h"
#include <cstdio>
#include <cstdlib>
#include <filesystem>
+2 -2
View File
@@ -1,8 +1,8 @@
#include "session.h"
#include "jsonvalue.h"
#include "pypath.h"
#include "text.h"
#include "utf8.h"
#include "../util/text.h"
#include "../util/utf8.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
+1 -1
View File
@@ -1 +1 @@
SHELL_SRCS = src/shell/audio.cpp src/shell/boxdata.cpp src/shell/edge.cpp src/shell/glow.cpp src/shell/lanes.cpp src/shell/livestream.cpp src/shell/proc.cpp src/shell/pty.cpp src/shell/runlive.cpp src/shell/sandbox.cpp src/shell/session.cpp src/shell/shellio.cpp src/shell/spinner.cpp src/shell/targets.cpp
SHELL_SRCS = src/shell/audio.cpp src/shell/boxdata.cpp src/shell/edge.cpp src/shell/glow.cpp src/shell/lanes.cpp src/shell/livestream.cpp src/shell/proc.cpp src/shell/ptyproc.cpp src/shell/runlive.cpp src/shell/sandbox.cpp src/shell/session.cpp src/shell/shellio.cpp src/shell/spinner.cpp src/shell/targets.cpp
+1 -1
View File
@@ -1,6 +1,6 @@
#include "shellio.h"
#include "ansi.h"
#include "utf8.h"
#include "../util/utf8.h"
#include <cstdio>
#include <fcntl.h>
#include <iostream>
+1 -1
View File
@@ -2,7 +2,7 @@
#include "ansi.h"
#include "glow.h"
#include "proc.h"
#include "utf8.h"
#include "../util/utf8.h"
#include <algorithm>
#include <chrono>
#include <cstdio>
+1 -1
View File
@@ -2,7 +2,7 @@
#include "pyglob.h"
#include "pypath.h"
#include "shlex.h"
#include "utf8.h"
#include "../util/utf8.h"
#include <algorithm>
namespace detz::shell {
namespace {
+2 -2
View File
@@ -10,10 +10,10 @@
#include <string>
#include <vector>
#include "plexer_flow.h"
#include "mplex_plexer_flow.h"
using detz::mplex::Json;
using namespace detz::plexer;
using namespace detz::mplex_flow;
namespace {
+1 -1
View File
@@ -1,6 +1,6 @@
// Regression tests for C++-vs-Python semantic traps found by the audit (G2): const-reference
// aliasing of members, killed objects reachable from the argument, buffer self-reference.
// Links all of src/mplex and src/plexer/plexer_flow.cpp. Plain main + asserts.
// Links all of src/mplex and src/mplex/mplex_plexer_flow.cpp. Plain main + asserts.
#include <unistd.h>
#include <cstdio>
+2 -2
View File
@@ -18,7 +18,7 @@
#include "mplex_copymode.h"
#include "mplex_options.h"
#include "mplex_server.h"
#include "plexer_flow.h"
#include "mplex_plexer_flow.h"
using namespace detz::mplex;
@@ -201,7 +201,7 @@ int main(int argc, char** argv) {
"PLEXER_FLOW_TIMEOUT_DEFAULT", "PLEXER_FLOW_TIMEOUT_MAX", "PLEXER_FLOW_POLL", "MPLEX_FLOW_MAX",
"MPLEX_FLOW_KEEP"};
for (const auto& name : caps) check(contains(flow, "`" + name + "`"), "flow cap documented: " + name);
namespace pf = detz::plexer;
namespace pf = detz::mplex_flow;
check(pf::PLEXER_FLOW_VERSION == 1 && pf::PLEXER_FLOW_SCREENS_MAX == 32 && pf::PLEXER_FLOW_FIELDS_MAX == 12 &&
pf::PLEXER_FLOW_OPTIONS_MAX == 12 && pf::PLEXER_FLOW_ACTIONS_MAX == 12 && pf::PLEXER_FLOW_VISITS_MAX == 64 &&
pf::PLEXER_FLOW_ID_MAX == 64 && pf::PLEXER_FLOW_TITLE_MAX == 200 && pf::PLEXER_FLOW_TEXT_MAX == 2000 &&