From a1d5f4bf48de951201666e100b4013e636a2c056 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Wed, 14 Oct 2026 15:30:12 +0300 Subject: [PATCH] Optimize startup, HTTP/SSE streaming, and fix macOS libedit bindings --- Makefile | 6 +- bantam.hpp | 41 +++++++- config.cpp | 10 +- editor.cpp | 48 +++++++--- json.cpp | 107 +++++++++++++++------ llm.cpp | 268 ++++++++++++++++++++++++++++++++++++---------------- main.cpp | 27 +++--- session.cpp | 89 +++++++++-------- tools.cpp | 62 ++++++++---- util.cpp | 61 +++++++++--- 10 files changed, 497 insertions(+), 222 deletions(-) diff --git a/Makefile b/Makefile index b2eee20..4fc5bb1 100644 --- a/Makefile +++ b/Makefile @@ -12,9 +12,9 @@ CXX = c++ STRIP = strip -CXXFLAGS = -std=c++20 -s -Wall -Wextra -Wpedantic -O2 -march=native -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -CPPFLAGS = -I/usr/local/include -I. -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_XOPEN_SOURCE=700 -LDFLAGS = -s +CXXFLAGS = -std=c++20 -Wall -Wextra -Wpedantic -O2 -march=native -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE +CPPFLAGS = -I/usr/local/include -I. -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_XOPEN_SOURCE=700 -D_DARWIN_C_SOURCE +LDFLAGS = LDLIBS = -L/usr/local/lib -lcurl -lreadline # Location of the AddressSanitizer/UBSan runtime libraries plus a replacement diff --git a/bantam.hpp b/bantam.hpp index c03cf06..fab7684 100644 --- a/bantam.hpp +++ b/bantam.hpp @@ -138,6 +138,7 @@ struct Cfg { int force_stream = -1; /* -1 = auto, 0/1 = force (compaction) */ int context_window = 262144; int max_tool_res = 65536; + bool context_window_set = false; std::map raw; /* raw key/value map */ }; @@ -180,17 +181,48 @@ inline std::string cstr(std::string_view text, const R& codes) { s += "\033["; for (size_t i = 0; i < codes.size(); i++) { if (i) s += ';'; - s += sfmt("%d", (int)codes[i]); + int v = (int)codes[i]; + if (v <= 0) { + s += '0'; + } else { + char rev[12]; + int rn = 0; + while (v > 0 && rn < 12) { rev[rn++] = (char)('0' + (v % 10)); v /= 10; } + while (rn > 0) s += rev[--rn]; + } } - s += "m"; + s += 'm'; s += text; s += "\033[0m"; return s; } template inline void cprint(std::string_view text, const R& codes) { - std::string w = cstr(text, codes); - std::fputs(w.c_str(), stdout); + if (text.empty()) return; + if (!g_col || codes.empty()) { + std::fwrite(text.data(), 1, text.size(), stdout); + return; + } + char seq[32]; + size_t pos = 0; + seq[pos++] = '\033'; + seq[pos++] = '['; + for (size_t i = 0; i < codes.size() && pos + 12 < sizeof(seq); i++) { + if (i) seq[pos++] = ';'; + int v = (int)codes[i]; + if (v <= 0) { + seq[pos++] = '0'; + } else { + char rev[12]; + int rn = 0; + while (v > 0 && rn < 12) { rev[rn++] = (char)('0' + (v % 10)); v /= 10; } + while (rn > 0) seq[pos++] = rev[--rn]; + } + } + seq[pos++] = 'm'; + std::fwrite(seq, 1, pos, stdout); + std::fwrite(text.data(), 1, text.size(), stdout); + std::fwrite("\033[0m", 1, 4, stdout); } int visible_len(std::string_view s); int rune_width(int cp); @@ -226,6 +258,7 @@ bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage) /* ------------------------------------------------------------------ */ /* llm.cpp — HTTP client (libcurl), headers, backoff, dispatch. */ /* ------------------------------------------------------------------ */ +void http_cleanup(); int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code, std::string &errmsg); int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage, diff --git a/config.cpp b/config.cpp index ba9cb24..391703c 100644 --- a/config.cpp +++ b/config.cpp @@ -87,7 +87,7 @@ static void parse_cfg_file(std::string_view path, Cfg &c, std::set else if (k == "max_al_iterations") c.max_al_iterations = to_int_val(v, c.max_al_iterations); else if (k == "stream") c.stream = (v == "true" || v == "1" || v == "yes"); else if (k == "color") c.color = std::string(v); - else if (k == "context_window") c.context_window = to_int_val(v, c.context_window); + else if (k == "context_window") { c.context_window = to_int_val(v, c.context_window); c.context_window_set = true; } else if (k == "max_tool_res") c.max_tool_res = to_int_val(v, c.max_tool_res); } } @@ -111,7 +111,7 @@ static void apply_env_cfg(Cfg &c, const std::set &file_keys) { if ((e = ev("timeout", "BANTAM_TIMEOUT"))) { c.timeout = to_int_val(e, c.timeout); cfg_raw_set(c, "timeout", e); } if ((e = ev("shell_timeout", "BANTAM_SHELL_TIMEOUT"))) { c.shell_timeout = to_int_val(e, c.shell_timeout); cfg_raw_set(c, "shell_timeout", e); } if ((e = ev("max_al_iterations", "BANTAM_MAX_AL_ITERATIONS"))) { c.max_al_iterations = to_int_val(e, c.max_al_iterations); cfg_raw_set(c, "max_al_iterations", e); } - if ((e = ev("context_window", "BANTAM_CONTEXT_WINDOW"))) { c.context_window = to_int_val(e, c.context_window); cfg_raw_set(c, "context_window", e); } + if ((e = ev("context_window", "BANTAM_CONTEXT_WINDOW"))) { c.context_window = to_int_val(e, c.context_window); c.context_window_set = true; cfg_raw_set(c, "context_window", e); } if ((e = ev("max_tool_res", "BANTAM_MAX_TOOL_RES"))) { c.max_tool_res = to_int_val(e, c.max_tool_res); cfg_raw_set(c, "max_tool_res", e); } if ((e = ev("reasoning_effort", "BANTAM_REASONING_EFFORT"))) cfg_raw_set(c, "reasoning_effort", e); if ((e = ev("bantam_tools_dir", "BANTAM_TOOLS_DIR"))) cfg_raw_set(c, "bantam_tools_dir", e); @@ -211,6 +211,7 @@ static int query_models_context_window(Cfg &cfg) { } int fetch_context_window(Cfg &cfg) { + if (cfg.context_window_set && cfg.context_window > 0) return cfg.context_window; static std::map cache; std::string key = cfg.endpoint + std::string(1, '\0') + cfg.model; auto it = cache.find(key); @@ -218,8 +219,9 @@ int fetch_context_window(Cfg &cfg) { int cw = query_models_context_window(cfg); if (cw > 0) { cache[key] = cw; return cw; } const std::string *v = cfg_raw_get(cfg, "context_window"); - if (v && !v->empty()) return to_int_val(*v, 262144); - return 262144; + int fallback = (v && !v->empty()) ? to_int_val(*v, 262144) : 262144; + cache[key] = fallback; + return fallback; } std::string list_models(Cfg &cfg, int *errout) { diff --git a/editor.cpp b/editor.cpp index 831e93f..09fbb51 100644 --- a/editor.cpp +++ b/editor.cpp @@ -55,13 +55,41 @@ std::string read_plain(std::string_view prompt, bool &ok) { } /* ----------------------------- readline editor ------------------------ */ +static void ensure_readline_init() { + static bool initialized = false; + if (initialized) return; + initialized = true; + rl_initialize(); +#if !defined(__APPLE__) && defined(RL_READLINE_VERSION) && (RL_READLINE_VERSION >= 0x0500) + /* On GNU Readline (Linux, FreeBSD), terminal raw mode clears ICRNL so + * Enter sends '\r' (rl_newline = submit) while Ctrl+J sends '\n' + * (rl_insert = literal newline). On macOS / BSD libedit (EditLine), + * ICRNL remains enabled in termios (translating Enter '\r' into '\n') and + * '\n' is the sole accept-line key (ed-newline), so rebinding '\n' to + * rl_insert prevents submitting any prompt. */ + if (!rl_library_version || std::strncmp(rl_library_version, "EditLine", 8) != 0) { + rl_bind_key('\n', rl_insert); + rl_bind_key('\r', rl_newline); + } +#endif +} + std::string read_line(std::string_view prompt, bool &ok) { if (!is_terminal(STDIN_FILENO)) return read_plain(prompt, ok); - /* Enter (\r) submits the line; Ctrl+J inserts a literal newline so a - * multi-line prompt can be composed before submitting. */ - rl_bind_key('\n', rl_insert); + ensure_readline_init(); char *line = readline(prompt.empty() ? "" : std::string(prompt).c_str()); - if (!line) { ok = false; return ""; } + setup_signal_handler(); + if (!line) { + if (g_interrupted) { + g_interrupted = 0; + std::fputc('\n', stdout); + std::fflush(stdout); + ok = true; + return ""; + } + ok = false; + return ""; + } std::string r(line); free(line); ok = true; @@ -74,19 +102,11 @@ int is_terminal(int fd) { return isatty(fd) != 0; } int term_width() { #ifdef TIOCGWINSZ struct winsize ws{}; - if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col; + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col; + if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col; #endif const char *c = std::getenv("COLUMNS"); if (c && *c) { int n = std::atoi(c); if (n > 0) return n; } - FILE *p = popen("tput cols 2>/dev/null", "r"); - if (p) { - char buf[32]; - if (fgets(buf, sizeof buf, p)) { - int n = std::atoi(buf); - pclose(p); - if (n > 0) return n; - } else pclose(p); - } return 80; } diff --git a/json.cpp b/json.cpp index 33ba0f3..b7a6b74 100644 --- a/json.cpp +++ b/json.cpp @@ -2,6 +2,7 @@ * and non-streaming response parsing. */ #include "bantam.hpp" #include +#include #include #include #include @@ -118,6 +119,19 @@ static void dump_str(std::string &out, std::string_view s) { const unsigned char *p = (const unsigned char *)s.data(); size_t avail = s.size(); while (avail > 0) { + /* Fast path: scan contiguous unescaped printable ASCII. */ + size_t run = 0; + while (run < avail) { + unsigned char b = p[run]; + if (b >= 0x20 && b < 0x80 && b != '"' && b != '\\') run++; + else break; + } + if (run > 0) { + out.append((const char *)p, run); + p += run; + avail -= run; + if (avail == 0) break; + } unsigned char c = p[0]; if (c < 0x80) { p++; @@ -130,10 +144,12 @@ static void dump_str(std::string &out, std::string_view s) { case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; - default: - if (c < 0x20) str_appendf(out, "\\u%04x", (unsigned)c); - else out += (char)c; + default: { + char ubuf[7]; + std::snprintf(ubuf, sizeof ubuf, "\\u%04x", (unsigned)c); + out.append(ubuf, 6); break; + } } } else { size_t pl = 0; @@ -160,12 +176,22 @@ static void dump_rec(std::string &out, const Json &j, int indent, int depth) { case JType::Bool: out += (j.bval ? "true" : "false"); break; - case JType::Int: - str_appendf(out, "%lld", (long long)j.ival); + case JType::Int: { + char ibuf[32]; + auto [ptr, ec] = std::to_chars(ibuf, ibuf + sizeof ibuf, j.ival); + if (ec == std::errc()) out.append(ibuf, (size_t)(ptr - ibuf)); + else str_appendf(out, "%lld", (long long)j.ival); break; + } case JType::Float: - if (!std::isfinite(j.dval)) out += "null"; - else str_appendf(out, "%.15g", j.dval); + if (!std::isfinite(j.dval)) { + out += "null"; + } else { + char fbuf[32]; + int n = std::snprintf(fbuf, sizeof fbuf, "%.15g", j.dval); + if (n > 0 && (size_t)n < sizeof fbuf) out.append(fbuf, (size_t)n); + else str_appendf(out, "%.15g", j.dval); + } break; case JType::Str: dump_str(out, j.sval); @@ -217,6 +243,7 @@ static void dump_rec(std::string &out, const Json &j, int indent, int depth) { std::string Json::dump(int indent) const { std::string out; + out.reserve(256); dump_rec(out, *this, indent, 0); return out; } @@ -278,16 +305,25 @@ struct JParser { if (pos >= src.size() || src[pos] != '"') return false; pos++; out.clear(); - while (pos < src.size()) { - unsigned char c = (unsigned char)src[pos++]; + const char *base = src.data(); + size_t n = src.size(); + while (pos < n) { + /* Fast path: scan contiguous unescaped bytes >= 0x20. */ + size_t run_start = pos; + while (pos < n) { + unsigned char b = (unsigned char)base[pos]; + if (b == '"' || b == '\\' || b < 0x20) break; + pos++; + } + if (pos > run_start) { + out.append(base + run_start, pos - run_start); + } + if (pos >= n) return false; + unsigned char c = (unsigned char)base[pos++]; if (c == '"') return true; if (c < 0x20) return false; - if (c != '\\') { - out += (char)c; - continue; - } - if (pos >= src.size()) return false; - char esc = src[pos++]; + if (pos >= n) return false; + char esc = base[pos++]; switch (esc) { case '"': out += '"'; break; case '\\': out += '\\'; break; @@ -301,7 +337,7 @@ struct JParser { uint32_t cp = 0; if (!parse_hex4(cp)) return false; if (cp >= 0xD800 && cp <= 0xDBFF) { - if (pos + 6 <= src.size() && src[pos] == '\\' && src[pos + 1] == 'u') { + if (pos + 6 <= n && base[pos] == '\\' && base[pos + 1] == 'u') { size_t saved = pos; pos += 2; uint32_t low = 0; @@ -353,20 +389,32 @@ struct JParser { if (pos >= src.size() || src[pos] < '0' || src[pos] > '9') return false; while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') pos++; } - std::string num_str(src.substr(start, pos - start)); + const char *first = src.data() + start; + const char *last = src.data() + pos; if (!is_float) { - errno = 0; - char *end = nullptr; - long long iv = std::strtoll(num_str.c_str(), &end, 10); - if (errno == 0 && end == num_str.c_str() + num_str.size()) { - out = Json((int64_t)iv); + int64_t iv = 0; + auto [ptr, ec] = std::from_chars(first, last, iv); + if (ec == std::errc() && ptr == last) { + out = Json(iv); return true; } } + char stack_num[64]; + std::string heap_num; + const char *cstr = nullptr; + size_t nlen = pos - start; + if (nlen < sizeof stack_num) { + std::memcpy(stack_num, first, nlen); + stack_num[nlen] = '\0'; + cstr = stack_num; + } else { + heap_num.assign(first, nlen); + cstr = heap_num.c_str(); + } errno = 0; char *end = nullptr; - double dv = std::strtod(num_str.c_str(), &end); - if (end != num_str.c_str() + num_str.size() || !std::isfinite(dv)) return false; + double dv = std::strtod(cstr, &end); + if (end != cstr + nlen || !std::isfinite(dv)) return false; out = Json(dv); return true; } @@ -554,6 +602,7 @@ std::string build_request_json(const Cfg &cfg, const Messages &msgs, root["model"] = cfg.model; root["temperature"] = cfg.temperature; Json arr = Json::array(); + arr.aval.reserve(msgs.items.size()); for (auto &m : msgs.items) { Json mo; serialize_message_json(mo, m); @@ -562,10 +611,12 @@ std::string build_request_json(const Cfg &cfg, const Messages &msgs, root["messages"] = std::move(arr); root["stream"] = stream; if (use_tools) { - Json tools; - if (Json::parse(TOOLS_JSON, tools)) { - root["tools"] = std::move(tools); - } + static const Json kTools = []() { + Json t; + Json::parse(TOOLS_JSON, t); + return t; + }(); + root["tools"] = kTools; } if (stream) { Json so = Json::object(); diff --git a/llm.cpp b/llm.cpp index ea2e785..93a8303 100644 --- a/llm.cpp +++ b/llm.cpp @@ -18,15 +18,29 @@ static size_t mem_cb(char *ptr, size_t sz, size_t nm, void *ud) { return total; } -/* ----------------------------- curl RAII ------------------------------ */ -struct CurlHandle { - CURL *c; - CurlHandle() : c(curl_easy_init()) {} - ~CurlHandle() { if (c) curl_easy_cleanup(c); } - CurlHandle(const CurlHandle &) = delete; - CurlHandle &operator=(const CurlHandle &) = delete; - explicit operator bool() const { return c != nullptr; } -}; +static int xferinfo_cb(void *clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) { + volatile int *interrupted = (volatile int *)clientp; + return (interrupted && *interrupted) ? 1 : 0; +} + +/* ----------------------------- curl handle & headers ------------------ */ +static CURL *g_shared_curl = nullptr; + +static CURL *get_shared_curl() { + if (!g_shared_curl) { + g_shared_curl = curl_easy_init(); + } else { + curl_easy_reset(g_shared_curl); + } + return g_shared_curl; +} + +void http_cleanup() { + if (g_shared_curl) { + curl_easy_cleanup(g_shared_curl); + g_shared_curl = nullptr; + } +} struct CurlHeaders { curl_slist *list = nullptr; @@ -53,8 +67,23 @@ static void set_proxy(CURL *c) { } } +static void apply_common_curl_opts(CURL *c, volatile int *interrupted) { + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(c, CURLOPT_XFERINFOFUNCTION, xferinfo_cb); + curl_easy_setopt(c, CURLOPT_XFERINFODATA, (void *)interrupted); + curl_easy_setopt(c, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(c, CURLOPT_TCP_KEEPIDLE, 30L); + curl_easy_setopt(c, CURLOPT_TCP_KEEPINTVL, 15L); + curl_easy_setopt(c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp"); + set_proxy(c); +} + static void apply_llm_headers(CurlHeaders &h, const Cfg &cfg) { h.append("Content-Type: application/json"); + /* Suppress Expect: 100-continue so large POST bodies are sent immediately + * without waiting for a 100 Continue round-trip or hanging behind proxies. */ + h.append("Expect:"); if (!cfg.api_key.empty() && cfg.api_key != "-") { h.append(sfmt("Authorization: Bearer %s", cfg.api_key.c_str())); } @@ -63,21 +92,32 @@ static void apply_llm_headers(CurlHeaders &h, const Cfg &cfg) { /* ----------------------------- http_get ------------------------------- */ int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code, std::string &errmsg) { - CurlHandle curl; - if (!curl) { errmsg = "curl init failed"; return -1; } + CURL *c = get_shared_curl(); + if (!c) { errmsg = "curl init failed"; return -1; } std::string url_s(url); - curl_easy_setopt(curl.c, CURLOPT_URL, url_s.c_str()); - curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, mem_cb); - curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &body); - set_proxy(curl.c); - long t = cfg.timeout > 0 ? cfg.timeout : 30; - curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, t); - curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, t); - curl_easy_setopt(curl.c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp"); - CURLcode res = curl_easy_perform(curl.c); - curl_easy_getinfo(curl.c, CURLINFO_RESPONSE_CODE, &code); + CurlHeaders headers; + if (!cfg.api_key.empty() && cfg.api_key != "-") { + headers.append(sfmt("Authorization: Bearer %s", cfg.api_key.c_str())); + } + apply_common_curl_opts(c, &g_interrupted); + curl_easy_setopt(c, CURLOPT_URL, url_s.c_str()); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1L); + if (headers.list) curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers.list); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, mem_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &body); + long cfg_t = cfg.timeout > 0 ? cfg.timeout : 15; + long conn_t = (cfg_t < 10) ? cfg_t : 10; + long tot_t = (cfg_t < 15) ? cfg_t : 15; + curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, conn_t); + curl_easy_setopt(c, CURLOPT_TIMEOUT, tot_t); + curl_easy_setopt(c, CURLOPT_LOW_SPEED_LIMIT, 1L); + curl_easy_setopt(c, CURLOPT_LOW_SPEED_TIME, tot_t); + CURLcode res = curl_easy_perform(c); + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code); if (res != CURLE_OK) { - errmsg = curl_easy_strerror(res); + errmsg = (res == CURLE_ABORTED_BY_CALLBACK || g_interrupted) + ? "interrupted" + : curl_easy_strerror(res); return -1; } return 0; @@ -113,11 +153,14 @@ static int est_tokens_msg(const Message &m) { /* ----------------------------- streaming parser ----------------------- */ struct StreamCtx { - std::string content, reas, line_buf, live_printed, buf, raw; + std::string content, reas, line_buf, buf, raw; std::vector tbl; Usage last_usage; std::vector tcs; + std::vector tcs_filt_args; + std::vector tcs_filt_pos; std::vector tcs_idx; + size_t live_printed_len = 0; int tool_calls_shown = 0; int tty = 0; int live_idx = -1; @@ -128,6 +171,32 @@ struct StreamCtx { volatile int *interrupted = nullptr; }; +static void advance_filtered_args(StreamCtx *sc, int slot, bool flush_all) { + const std::string &raw_args = sc->tcs[slot].arguments; + size_t pos = sc->tcs_filt_pos[slot]; + if (pos >= raw_args.size()) return; + std::string_view rem = std::string_view(raw_args).substr(pos); + if (!flush_all) { + /* Hold back an incomplete trailing multi-byte UTF-8 sequence until the + * next chunk arrives. */ + size_t back = 0; + while (back < 3 && back < rem.size()) { + unsigned char b = (unsigned char)rem[rem.size() - 1 - back]; + if ((b & 0x80) == 0) break; /* ASCII */ + if ((b & 0xC0) == 0xC0) { /* UTF-8 lead byte */ + size_t need = ((b & 0xE0) == 0xC0) ? 2 : ((b & 0xF0) == 0xE0) ? 3 : 4; + if (back + 1 < need) rem.remove_suffix(back + 1); + break; + } + back++; + } + } + if (!rem.empty()) { + sc->tcs_filt_args[slot] += filter_text(rem); + sc->tcs_filt_pos[slot] = pos + rem.size(); + } +} + static void stream_upsert_tc(StreamCtx *sc, int idx, std::string_view id, std::string_view name, std::string_view args) { int slot = -1; @@ -136,30 +205,54 @@ static void stream_upsert_tc(StreamCtx *sc, int idx, std::string_view id, if (slot < 0) { slot = (int)sc->tcs.size(); sc->tcs.emplace_back(); + sc->tcs_filt_args.emplace_back(); + sc->tcs_filt_pos.push_back(0); sc->tcs_idx.push_back(idx); } if (!id.empty()) sc->tcs[slot].id = std::string(id); if (!name.empty()) sc->tcs[slot].name += std::string(name); - if (!args.empty()) sc->tcs[slot].arguments += std::string(args); + if (!args.empty()) { + sc->tcs[slot].arguments += std::string(args); + advance_filtered_args(sc, slot, false); + } } -static void live_target(StreamCtx *sc, int slot, bool closed, std::string &out) { - ToolCall &t = sc->tcs[slot]; - str_appendf(out, "[tool call: %s", t.name.c_str()); - if (closed || !t.arguments.empty()) { - std::string fa = filter_text(t.arguments); - str_appendf(out, "(%s", fa.c_str()); - if (closed) out += ")]"; +static void print_live_delta(StreamCtx *sc, int slot, bool closed) { + if (closed) advance_filtered_args(sc, slot, true); + const ToolCall &t = sc->tcs[slot]; + const std::string &fa = sc->tcs_filt_args[slot]; + bool has_args = closed || !t.arguments.empty(); + size_t prefix_len = 12 + t.name.size(); /* "[tool call: " + name */ + size_t total_len = prefix_len + (has_args ? (1 + fa.size() + (closed ? 2 : 0)) : 0); + size_t cur = sc->live_printed_len; + if (total_len <= cur) return; + + std::string delta; + delta.reserve(total_len - cur); + auto emit_part = [&](std::string_view part, size_t part_start) { + size_t part_end = part_start + part.size(); + if (part_end <= cur) return; + size_t off = (cur > part_start) ? (cur - part_start) : 0; + delta.append(part.substr(off)); + }; + size_t off = 0; + emit_part("[tool call: ", off); off += 12; + emit_part(t.name, off); off += t.name.size(); + if (has_args) { + emit_part("(", off); off += 1; + emit_part(fa, off); off += fa.size(); + if (closed) { + emit_part(")]", off); off += 2; + } } + cprint(delta, std::array{33}); + sc->live_printed_len = total_len; } + static void finish_live(StreamCtx *sc, bool closed) { if (sc->live_idx < 0) return; - std::string target; - live_target(sc, sc->live_idx, closed, target); - if (target.size() > sc->live_printed.size()) { - cprint(target.substr(sc->live_printed.size()), std::array{33}); - } - sc->live_printed.clear(); + print_live_delta(sc, sc->live_idx, closed); + sc->live_printed_len = 0; sc->live_idx = -1; /* The tool call marker must always terminate its own line so the * following [tool result: ...] header is separated from it by a @@ -172,13 +265,8 @@ static void step_live(StreamCtx *sc) { if (!sc->tty || sc->tcs.empty()) return; int cur = (int)sc->tcs.size() - 1; if (sc->live_idx != -1 && sc->live_idx != cur) finish_live(sc, true); - if (sc->live_idx == -1) { sc->live_idx = cur; sc->live_printed.clear(); } - std::string target; - live_target(sc, cur, false, target); - if (target.size() > sc->live_printed.size()) { - cprint(target.substr(sc->live_printed.size()), std::array{33}); - sc->live_printed += target.substr(sc->live_printed.size()); - } + if (sc->live_idx == -1) { sc->live_idx = cur; sc->live_printed_len = 0; } + print_live_delta(sc, cur, false); sc->tool_calls_shown = 1; } @@ -196,7 +284,7 @@ static void stream_maybe_erase(StreamCtx *sc) { } } -static void stream_handle_data(StreamCtx *sc, const std::string &data) { +static void stream_handle_data(StreamCtx *sc, std::string_view data) { if (data == "[DONE]") { sc->done = 1; return; } Json d_mut; if (!Json::parse(data, d_mut) || !d_mut.is_object()) return; @@ -253,21 +341,23 @@ static void stream_handle_data(StreamCtx *sc, const std::string &data) { if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; } sc->content += ct; sc->line_buf += ct; + size_t lpos = 0; while (true) { - auto nl = sc->line_buf.find('\n'); + auto nl = sc->line_buf.find('\n', lpos); if (nl == std::string::npos) break; - std::string cur = sc->line_buf.substr(0, nl); - sc->line_buf.erase(0, nl + 1); + std::string_view cur_sv = std::string_view(sc->line_buf).substr(lpos, nl - lpos); + lpos = nl + 1; /* Inside a code block every line is rendered verbatim as code; only * outside a code block do table-shaped lines get accumulated for * box-drawing table rendering. */ - if (sc->md.in_code || !is_table_line(cur)) { + if (sc->md.in_code || !is_table_line(cur_sv)) { flush_tbl(sc); - render_md_line_print(cur, sc->md); + render_md_line_print(cur_sv, sc->md); } else { - sc->tbl.push_back(cur); + sc->tbl.emplace_back(cur_sv); } } + if (lpos > 0) sc->line_buf.erase(0, lpos); } const Json &tcs = delta["tool_calls"]; if (tcs.is_array()) { @@ -297,22 +387,26 @@ static int stream_feed(StreamCtx *sc, const char *data, size_t len, volatile int *interrupted) { sc->interrupted = interrupted; sc->buf.append(data, len); + size_t pos = 0; while (true) { - auto nl = sc->buf.find('\n'); + auto nl = sc->buf.find('\n', pos); if (nl == std::string::npos) break; - std::string line = sc->buf.substr(0, nl); + std::string_view line = std::string_view(sc->buf).substr(pos, nl - pos); + pos = nl + 1; while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) - line.pop_back(); - if (line.rfind("data:", 0) == 0) { - std::string_view dd = line; - dd.remove_prefix(5); - while (!dd.empty() && dd.front() == ' ') dd.remove_prefix(1); - stream_handle_data(sc, std::string(dd)); + line.remove_suffix(1); + if (line.starts_with("data:")) { + line.remove_prefix(5); + while (!line.empty() && line.front() == ' ') line.remove_prefix(1); + stream_handle_data(sc, line); } - sc->buf.erase(0, nl + 1); if (sc->done) break; - if (*interrupted) return -1; + if (*interrupted) { + sc->buf.erase(0, pos); + return -1; + } } + if (pos > 0) sc->buf.erase(0, pos); return 0; } @@ -336,9 +430,9 @@ static void stream_result(StreamCtx *sc, Message &out, Usage &usage) { out = Message{}; out.role = "assistant"; out.calls_shown = sc->tool_calls_shown; - if (!sc->content.empty()) out.content = sc->content; - if (!sc->reas.empty()) out.reasoning = sc->reas; - for (auto &t : sc->tcs) out.calls.push_back(t); + if (!sc->content.empty()) out.content = std::move(sc->content); + if (!sc->reas.empty()) out.reasoning = std::move(sc->reas); + for (auto &t : sc->tcs) out.calls.push_back(std::move(t)); usage = sc->last_usage; if (usage.total_tokens == 0 && usage.prompt_tokens > 0) usage.total_tokens = usage.prompt_tokens + usage.completion_tokens; @@ -349,7 +443,11 @@ static size_t stream_write_cb(char *ptr, size_t sz, size_t nm, void *ud) { StreamCtx *sc = (StreamCtx *)ud; size_t total = sz * nm; if (*sc->interrupted) return 0; - sc->raw.append((const char *)ptr, total); + /* Cap raw error buffer at 4096 bytes; http_err_body only uses 4000 bytes. */ + if (sc->raw.size() < 4096) { + size_t rem = 4096 - sc->raw.size(); + sc->raw.append((const char *)ptr, total < rem ? total : rem); + } int r = stream_feed(sc, ptr, total, sc->interrupted); if (r != 0 || *sc->interrupted) return 0; return total; @@ -388,20 +486,23 @@ int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage, int nf = 9; std::string pend = cstr("...requesting...", std::array{1, 2}); - CurlHandle curl; - if (!curl) { errmsg = "curl init failed"; return -1; } - curl_easy_setopt(curl.c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp"); + CURL *c = get_shared_curl(); + if (!c) { errmsg = "curl init failed"; return -1; } CurlHeaders headers; apply_llm_headers(headers, cfg); - curl_easy_setopt(curl.c, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl.c, CURLOPT_POST, 1L); - curl_easy_setopt(curl.c, CURLOPT_POSTFIELDS, payload.c_str()); - curl_easy_setopt(curl.c, CURLOPT_POSTFIELDSIZE, (long)payload.size()); - curl_easy_setopt(curl.c, CURLOPT_HTTPHEADER, headers.list); - set_proxy(curl.c); - curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 30)); - if (!stream) curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 300)); + apply_common_curl_opts(c, interrupted); + curl_easy_setopt(c, CURLOPT_URL, url.c_str()); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, payload.c_str()); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)payload.size()); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers.list); + long t = (long)(cfg.timeout > 0 ? cfg.timeout : 300); + long conn_t = (t < 20) ? t : 20; + curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, conn_t); + curl_easy_setopt(c, CURLOPT_LOW_SPEED_LIMIT, 1L); + curl_easy_setopt(c, CURLOPT_LOW_SPEED_TIME, t); + if (!stream) curl_easy_setopt(c, CURLOPT_TIMEOUT, t); int attempt, rc = -1; for (attempt = 0; attempt <= nf; attempt++) { @@ -416,20 +517,21 @@ int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage, sc.tty = is_terminal(STDOUT_FILENO); sc.interrupted = interrupted; sc.erase_pending = 1; - curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, stream_write_cb); - curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &sc); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, stream_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &sc); } else { - curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, mem_cb); - curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &mbody); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, mem_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &mbody); } - res = curl_easy_perform(curl.c); - if (*interrupted) { + res = curl_easy_perform(c); + if (*interrupted || res == CURLE_ABORTED_BY_CALLBACK) { + *interrupted = 1; if (g_col && (!stream || sc.erase_pending)) printf("\r\033[K"); errmsg = "interrupted"; rc = -1; break; } - curl_easy_getinfo(curl.c, CURLINFO_RESPONSE_CODE, &code); + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code); int transport_err = (res != CURLE_OK); int is4xx = (!transport_err && code >= 400 && code < 500 && code != 408 && code != 429); diff --git a/main.cpp b/main.cpp index d7527db..43f3597 100644 --- a/main.cpp +++ b/main.cpp @@ -180,12 +180,13 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s for (size_t tci = 0; tci < m.calls.size(); tci++) { if (*interrupted) { rc = -1; break; } ToolCall &tc = m.calls[tci]; - std::string astr = filter_text(tc.arguments); + const std::string &astr = tc.arguments; /* already filtered by sanitize_arguments */ if (!m.calls_shown) { cprint(sfmt("[tool call: %s(%s)]", tc.name.c_str(), astr.c_str()), std::array{33}); printf("\n"); } std::string res; int sty = 2; + bool res_already_filtered = false; Json a_mut; if (!Json::parse(astr, a_mut) || !a_mut.is_object()) { res = sfmt("[tool error: invalid JSON args for %s: parse error. Raw: %s]", tc.name.c_str(), astr.c_str()); @@ -196,11 +197,11 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s if (fn == "bash" || fn == "shell_exec") { std::string cmd = get_str(a, "command"); std::string workdir = get_str(a, "workdir"); - std::string fcmd = filter_text(cmd); std::string fwd = filter_text(workdir); int to = cfg.shell_timeout; if (a.contains("timeout")) { int n = get_int(a, "timeout"); if (n > 0) to = (n > 1000) ? n / 1000 : n; } - res = shell_with_workdir(fcmd, fwd, to, interrupted); + res = shell_with_workdir(cmd, fwd, to, interrupted); + res_already_filtered = true; } else if (fn == "read") { std::string p = tool_path_arg(a); std::string fp = filter_text(p); @@ -250,17 +251,17 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s sty = 31; } } - std::string fres = filter_text(res); - res = offload_tool_result(fres, cfg.max_tool_res, tmps); + if (!res_already_filtered) res = filter_text(res); + res = offload_tool_result(res, cfg.max_tool_res, tmps); { std::string hdr = cstr("[tool result: ", std::array{32}); std::string fn2 = cstr(tc.name, std::array{32}); printf("%s%s]\n", hdr.c_str(), fn2.c_str()); - std::string body = cstr(res, std::array{sty}); - printf("%s\n\n", body.c_str()); + cprint(res, std::array{sty}); + printf("\n\n"); } - Message tm; tm.role = "tool"; tm.tool_call_id = tc.id; tm.content = res; - msgs.items.push_back(tm); + Message tm; tm.role = "tool"; tm.tool_call_id = tc.id; tm.content = std::move(res); + msgs.items.push_back(std::move(tm)); if (*interrupted) { rc = -1; break; } } if (rc != 0) break; @@ -374,16 +375,15 @@ static void do_compact(Cfg &cfg, Messages &msgs) { /* ----------------------------- main ----------------------------------- */ struct CurlGlobalGuard { CurlGlobalGuard() { curl_global_init(CURL_GLOBAL_DEFAULT); } - ~CurlGlobalGuard() { curl_global_cleanup(); } + ~CurlGlobalGuard() { http_cleanup(); curl_global_cleanup(); } }; int main(int argc, char **argv) { CurlGlobalGuard curl_guard; setup_signal_handler(); Cfg cfg = get_cfg(config_path()); - cfg.context_window = fetch_context_window(cfg); - std::string sp = build_system_prompt(cfg); g_col = color_enabled(cfg); + std::string sp = build_system_prompt(cfg); Messages msgs; { Message sys; sys.role = "system"; sys.content = sp; @@ -405,6 +405,7 @@ int main(int argc, char **argv) { size_t a = 0; while (a < u.size() && (u[a] == ' ' || u[a] == '\n' || u[a] == '\r' || u[a] == '\t')) a++; u = u.substr(a); if (!u.empty() && u[0] == '!') { run_direct_shell(u.substr(1), cfg.shell_timeout); return 0; } + cfg.context_window = fetch_context_window(cfg); Message um; um.role = "user"; um.content = u; msgs.items.push_back(um); Usage usg; std::string err; @@ -443,6 +444,8 @@ int main(int argc, char **argv) { { cprint("Bantam Agent ready", std::array{1, 32}); printf(" "); cprint(" (Enter submits; Ctrl+J or paste = new line)\n", std::array{2}); + fflush(stdout); + cfg.context_window = fetch_context_window(cfg); std::string info = sfmt("endpoint: %s model: %s temp: %g reasoning: %s context: %d\n", cfg.endpoint.c_str(), cfg.model.c_str(), cfg.temperature, re.c_str(), cfg.context_window); cprint(info, std::array{2}); diff --git a/session.cpp b/session.cpp index a5feef0..9974c1b 100644 --- a/session.cpp +++ b/session.cpp @@ -1,5 +1,6 @@ /* session.cpp — session persistence, loading, compaction. */ #include "bantam.hpp" +#include #include #include #include @@ -24,6 +25,29 @@ static std::string sessions_dir() { return d; } +static bool load_session_file(const std::string &full, Session &s) { + std::string buf; + if (!read_file_str(full, buf)) return false; + Json root_mut; + if (!Json::parse(buf, root_mut) || !root_mut.is_object()) return false; + const Json &root = root_mut; + s = Session{}; + s.id = root["id"].str(); + s.created = root["created"].str(); + s.summary = root["summary"].str(); + const Json &marr = root["messages"]; + if (marr.is_array()) { + s.messages.items.reserve(marr.aval.size()); + for (auto &mo : marr.aval) { + Message m; + parse_message_json(mo, m); + s.messages.items.push_back(std::move(m)); + } + } + if (s.summary.empty()) s.summary = "(no summary)"; + return true; +} + void save_session(const Messages &msgs, std::string_view sid, std::string *out_id, std::string *out_summary) { std::string id = (!sid.empty()) ? std::string(sid) : project_id(); @@ -40,6 +64,7 @@ void save_session(const Messages &msgs, std::string_view sid, std::string sum = summary_of(msgs); root["summary"] = sum; Json arr = Json::array(); + arr.aval.reserve(msgs.items.size()); for (auto &m : msgs.items) { Json mo; serialize_message_json(mo, m); @@ -63,63 +88,46 @@ std::vector sessions_load() { std::string nm = de->d_name; if (nm == "." || nm == "..") continue; if (nm.size() < 5 || nm.substr(nm.size() - 5) != ".json") continue; - files.push_back(nm); + files.push_back(std::move(nm)); } closedir(dir); } } - for (size_t i = 1; i < files.size(); i++) { - for (size_t j = i; j > 0 && files[j - 1] > files[j]; j--) { - std::swap(files[j - 1], files[j]); - } - } + std::sort(files.begin(), files.end()); + ss.reserve(files.size()); for (auto &nm : files) { std::string full = d + "/" + nm; - std::string buf; - if (!read_file_str(full, buf)) continue; - Json root_mut; - if (!Json::parse(buf, root_mut) || !root_mut.is_object()) continue; - const Json &root = root_mut; Session s; - s.id = root["id"].str(); - s.created = root["created"].str(); - s.summary = root["summary"].str(); - const Json &marr = root["messages"]; - if (marr.is_array()) { - for (auto &mo : marr.aval) { - Message m; - parse_message_json(mo, m); - s.messages.items.push_back(std::move(m)); - } + if (load_session_file(full, s)) { + ss.push_back(std::move(s)); } - if (s.summary.empty()) s.summary = "(no summary)"; - ss.push_back(std::move(s)); } /* sort by id descending (newest first) */ - for (size_t i = 1; i < ss.size(); i++) { - for (size_t j = i; j > 0 && ss[j - 1].id < ss[j].id; j--) { - std::swap(ss[j - 1], ss[j]); - } - } + std::sort(ss.begin(), ss.end(), [](const Session &a, const Session &b) { + return a.id > b.id; + }); return ss; } std::unique_ptr load_session(std::string_view sid, std::string *err) { + if (!sid.empty() && sid.find('/') == std::string_view::npos) { + std::string exact = sessions_dir() + "/" + std::string(sid) + ".json"; + Session s; + if (load_session_file(exact, s) && s.id == sid) { + return std::make_unique(std::move(s.messages)); + } + } auto ss = sessions_load(); for (auto &s : ss) { if (s.id == sid) { - auto res = std::make_unique(); - for (auto &m : s.messages.items) res->items.push_back(m); - return res; + return std::make_unique(std::move(s.messages)); } } /* prefix match */ std::vector pref; - for (auto &s : ss) if (s.id.rfind(sid, 0) == 0) pref.push_back(&s); + for (auto &s : ss) if (s.id.starts_with(sid)) pref.push_back(&s); if (pref.size() == 1) { - auto res = std::make_unique(); - for (auto &m : pref[0]->messages.items) res->items.push_back(m); - return res; + return std::make_unique(std::move(pref[0]->messages)); } if (pref.size() > 1) { std::string names; @@ -134,13 +142,10 @@ std::unique_ptr load_session(std::string_view sid, std::string *err) { std::unique_ptr find_project_session() { std::string pid = project_id(); - auto ss = sessions_load(); - for (auto &s : ss) { - if (s.id == pid) { - auto found = std::make_unique(); - *found = std::move(s); - return found; - } + std::string path = sessions_dir() + "/" + pid + ".json"; + auto found = std::make_unique(); + if (load_session_file(path, *found) && found->id == pid) { + return found; } return nullptr; } diff --git a/tools.cpp b/tools.cpp index ffcf10f..66ec99c 100644 --- a/tools.cpp +++ b/tools.cpp @@ -1,5 +1,6 @@ /* tools.cpp — bash/read/write tools and oversized result offloading. */ #include "bantam.hpp" +#include #include #include #include @@ -20,6 +21,8 @@ * be located anywhere on PATH. Never returns a hardcoded bash path. */ static const char *resolve_shell() { static char buf[4096]; + static const char *cached = nullptr; + if (cached) return cached; const char *path = getenv("PATH"); if (path && *path) { std::string p(path); @@ -34,19 +37,22 @@ static const char *resolve_shell() { if (stat(cand.c_str(), &st) == 0 && S_ISREG(st.st_mode) && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0) { snprintf(buf, sizeof buf, "%s", cand.c_str()); - return buf; + cached = buf; + return cached; } if (end == std::string::npos) break; start = end + 1; } } - return "/bin/sh"; + cached = "/bin/sh"; + return cached; } std::string shell_with_workdir(std::string_view cmd, std::string_view workdir, int timeout, volatile int *interrupted) { std::string fcmd = filter_text(cmd); if (timeout <= 0) timeout = 120; + const char *sh = resolve_shell(); int pipefd[2]; if (pipe(pipefd) != 0) return "[tool error: pipe failed]"; @@ -57,13 +63,14 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir, return "[tool error: fork failed]"; } if (pid == 0) { - /* child */ + /* child: create a dedicated process group so timeout/SIGINT kills the + * entire process tree spawned by the shell. */ + setpgid(0, 0); dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[0]); close(pipefd[1]); if (!workdir.empty()) chdir(std::string(workdir).c_str()); - const char *sh = resolve_shell(); const char *shname = (strrchr(sh, '/') ? strrchr(sh, '/') + 1 : sh); execl(sh, shname, "-c", fcmd.c_str(), (char *)nullptr); _exit(127); @@ -81,9 +88,11 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir, if (*interrupted) break; int remain = (int)(deadline - time(nullptr)); if (remain <= 0) { killed = true; break; } - int pr = poll(&pfd, 1, remain * 1000); + int wait_ms = remain * 1000; + if (wait_ms > 200) wait_ms = 200; + int pr = poll(&pfd, 1, wait_ms); if (pr < 0) { if (errno == EINTR) continue; break; } - if (pr == 0) { killed = true; break; } + if (pr == 0) continue; char buf[4096]; ssize_t n = read(pipefd[0], buf, sizeof buf); if (n > 0) out.append(buf, (size_t)n); @@ -92,6 +101,7 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir, else break; } if (*interrupted || killed) { + kill(-pid, SIGKILL); kill(pid, SIGKILL); /* drain remaining output briefly */ struct pollfd dpfd; dpfd.fd = pipefd[0]; dpfd.events = POLLIN; @@ -141,29 +151,41 @@ std::string read_file_or_dir(std::string_view path, int offset, int limit, int * return sfmt("[tool error: read %s: %s]", p.c_str(), strerror(errno)); } if (S_ISDIR(st.st_mode)) { - std::vector raw_names; + struct DirEnt { + std::string name; + int is_dir; /* -1 = unknown, 0 = non-dir, 1 = dir */ + }; + std::vector entries; { DIR *dir = opendir(p.c_str()); if (!dir) { *style = 31; return "[tool error: read (directory listing failed)]"; } struct dirent *de; while ((de = readdir(dir))) { - std::string nm = de->d_name; - if (nm == "." || nm == "..") continue; - raw_names.push_back(std::move(nm)); + if (de->d_name[0] == '.' && + (de->d_name[1] == '\0' || (de->d_name[1] == '.' && de->d_name[2] == '\0'))) + continue; + int isdir = -1; +#if defined(DT_DIR) && defined(DT_UNKNOWN) && defined(DT_LNK) + if (de->d_type != DT_UNKNOWN && de->d_type != DT_LNK) { + isdir = (de->d_type == DT_DIR) ? 1 : 0; + } +#endif + entries.push_back(DirEnt{de->d_name, isdir}); } closedir(dir); } - for (size_t i = 1; i < raw_names.size(); i++) { - for (size_t j = i; j > 0 && raw_names[j - 1] > raw_names[j]; j--) { - std::swap(raw_names[j - 1], raw_names[j]); - } - } + std::sort(entries.begin(), entries.end(), [](const DirEnt &a, const DirEnt &b) { + return a.name < b.name; + }); std::string b; - for (auto &nm : raw_names) { - std::string full = p + "/" + nm; - struct stat ds; - bool isdir = (stat(full.c_str(), &ds) == 0 && S_ISDIR(ds.st_mode)); - b += nm; + for (auto &ent : entries) { + bool isdir = (ent.is_dir == 1); + if (ent.is_dir < 0) { + std::string full = p + "/" + ent.name; + struct stat ds; + isdir = (stat(full.c_str(), &ds) == 0 && S_ISDIR(ds.st_mode)); + } + b += ent.name; if (isdir) b += '/'; b += '\n'; } diff --git a/util.cpp b/util.cpp index da49fa1..6360f47 100644 --- a/util.cpp +++ b/util.cpp @@ -12,11 +12,15 @@ /* ----------------------------- formatting ----------------------------- */ static std::string vfmt(const char *fmt, va_list ap) { + char stack_buf[512]; va_list cp; va_copy(cp, ap); - int need = std::vsnprintf(nullptr, 0, fmt, cp); + int need = std::vsnprintf(stack_buf, sizeof stack_buf, fmt, cp); va_end(cp); if (need < 0) return {}; + if ((size_t)need < sizeof stack_buf) { + return std::string(stack_buf, (size_t)need); + } std::string s; s.resize((size_t)need); std::vsnprintf(s.data(), (size_t)need + 1, fmt, ap); @@ -30,11 +34,22 @@ std::string sfmt(const char *fmt, ...) { return s; } void str_appendf(std::string &s, const char *fmt, ...) { - va_list ap; + char stack_buf[512]; + va_list ap, cp; va_start(ap, fmt); - std::string more = vfmt(fmt, ap); + va_copy(cp, ap); + int need = std::vsnprintf(stack_buf, sizeof stack_buf, fmt, cp); + va_end(cp); + if (need > 0) { + if ((size_t)need < sizeof stack_buf) { + s.append(stack_buf, (size_t)need); + } else { + size_t old_sz = s.size(); + s.resize(old_sz + (size_t)need); + std::vsnprintf(s.data() + old_sz, (size_t)need + 1, fmt, ap); + } + } va_end(ap); - s += more; } /* ----------------------------- UTF-8 ---------------------------------- */ @@ -102,6 +117,19 @@ std::string filter_text(std::string_view s) { const unsigned char *p = (const unsigned char *)s.data(); size_t avail = s.size(); while (avail > 0) { + /* Fast path: scan contiguous printable ASCII plus '\t' and '\n'. */ + size_t run = 0; + while (run < avail) { + unsigned char b = p[run]; + if ((b >= 0x20 && b <= 0x7e) || b == '\n' || b == '\t') run++; + else break; + } + if (run > 0) { + out.append((const char *)p, run); + p += run; + avail -= run; + if (avail == 0) break; + } size_t pl; int cp = utf8_decode(p, avail, &pl); if (cp == 0xFFFD && pl == 1) { p++; avail--; continue; } @@ -183,10 +211,12 @@ int rune_width(int cp) { void mkdir_p(std::string_view dir) { if (dir.empty()) return; std::string path(dir); + struct stat st; + if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) return; for (size_t i = 1; i < path.size(); i++) { if (path[i] == '/') { path[i] = '\0'; - mkdir(path.c_str(), 0755); + if (stat(path.c_str(), &st) != 0) mkdir(path.c_str(), 0755); path[i] = '/'; } } @@ -218,12 +248,15 @@ std::string tool_res_dir(std::string_view sess_id) { return p; } std::string project_id() { - char cwd[4096]; - if (!getcwd(cwd, sizeof cwd)) std::strcpy(cwd, "."); - char *abs = realpath(cwd, nullptr); - std::string src = abs ? abs : cwd; - if (abs) free(abs); - return md5_hex(src); + static const std::string cached_id = []() { + char cwd[4096]; + if (!getcwd(cwd, sizeof cwd)) std::strcpy(cwd, "."); + char *abs = realpath(cwd, nullptr); + std::string src = abs ? abs : cwd; + if (abs) free(abs); + return md5_hex(src); + }(); + return cached_id; } /* ----------------------------- MD5 ------------------------------------ */ @@ -322,7 +355,11 @@ bool read_file_str(std::string_view path, std::string &out) { std::string p(path); FILE *f = std::fopen(p.c_str(), "rb"); if (!f) return false; - char buf[4096]; + struct stat st; + if (fstat(fileno(f), &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0) { + out.reserve((size_t)st.st_size); + } + char buf[16384]; size_t n; while ((n = std::fread(buf, 1, sizeof buf, f)) > 0) { out.append(buf, n);