perf: faster builds, leaner daemon, smoother install/run (0.9.20)

Reuse libcstcore.a for tests with -MMD incremental deps; idle-backoff the
parliament seat loop; defer curl init and cache settings; add systemd helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 11:41:16 +08:00
co-authored by Cursor
parent 812e3ee265
commit 2f96b050d6
13 changed files with 278 additions and 103 deletions
+2
View File
@@ -1,6 +1,8 @@
*.o *.o
*.d
cst-agent cst-agent
cst-agent.exe cst-agent.exe
libcstcore.a
tests/test_* tests/test_*
!tests/test_*.c !tests/test_*.c
session.jsonl session.jsonl
+4 -8
View File
@@ -400,17 +400,13 @@ if(CST_BUILD_WEBUI)
list(APPEND _CST_CORE_TESTS test_webui) list(APPEND _CST_CORE_TESTS test_webui)
endif() endif()
foreach(t ${_CST_CORE_TESTS}) foreach(t ${_CST_CORE_TESTS})
add_executable(${t} tests/${t}.c ${CST_CORE_SOURCES}) # Link the shared cstcore archive instead of recompiling every core .c.
# The core now contains src/web_md.c, which needs sashidown's header and add_executable(${t} tests/${t}.c)
# archive exactly like the Makefile build does.
target_include_directories(${t} PRIVATE include "${CJSON_DIR}" "${SD_DIR}/include") target_include_directories(${t} PRIVATE include "${CJSON_DIR}" "${SD_DIR}/include")
target_compile_definitions(${t} PRIVATE _GNU_SOURCE) target_compile_definitions(${t} PRIVATE _GNU_SOURCE)
target_link_libraries(${t} PRIVATE target_link_libraries(${t} PRIVATE cstcore)
"${SASHIDOWN_A}" CURL::libcurl Threads::Threads)
add_dependencies(${t} sashidown) add_dependencies(${t} sashidown)
if(NOT WIN32) if(WIN32)
target_link_libraries(${t} PRIVATE m)
else()
target_link_libraries(${t} PRIVATE ws2_32 crypt32 secur32 bcrypt advapi32 user32) target_link_libraries(${t} PRIVATE ws2_32 crypt32 secur32 bcrypt advapi32 user32)
endif() endif()
cst_win_exe(${t}) cst_win_exe(${t})
+47 -31
View File
@@ -134,8 +134,9 @@ OBJ = $(SRC:.c=.o)
# the GUI links the same engine the CLI/TUI/WebUI run. No TUI, no main. # the GUI links the same engine the CLI/TUI/WebUI run. No TUI, no main.
CORE_OBJ = $(CORE_SRC:.c=.o) CORE_OBJ = $(CORE_SRC:.c=.o)
CST_CORE_LIB = libcstcore.a CST_CORE_LIB = libcstcore.a
HDRS = $(wildcard include/*.h) $(wildcard src/tools/*.h) $(CJSON_DIR)/cJSON.h \ # Generated embeds; per-file .d files (from -MMD) track the rest.
.gen/models_builtin.inc .gen/web_assets.inc GEN_INCS = .gen/models_builtin.inc .gen/web_assets.inc
DEPFILES = $(OBJ:.o=.d)
TESTS = \ TESTS = \
tests/test_ctl_frontier \ tests/test_ctl_frontier \
@@ -193,33 +194,29 @@ TEST_LIBS = -L$(SD_DIR) -lsashidown $(CURL_LIBS) $(RPATH_LIBS) $(TEST_SYS_LIBS)
# keeps them, so the small tests link the OS layer too. # keeps them, so the small tests link the OS layer too.
TEST_OS_SRC = src/os_compat.c src/os_win.c src/util.c TEST_OS_SRC = src/os_compat.c src/os_win.c src/util.c
.PHONY: all clean check demo sashidown-lib FORCE cmake-gcc cmake-clang asan install uninstall .PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang asan install \
uninstall install-service
all: sashidown-lib cst-agent $(CST_CORE_LIB) all: sashidown-lib cst-agent $(CST_CORE_LIB)
$(CST_CORE_LIB): $(CORE_OBJ) FORCE $(CST_CORE_LIB): $(CORE_OBJ)
rm -f $@ rm -f $@
$(AR) rcs $@ $(CORE_OBJ) $(AR) rcs $@ $(CORE_OBJ)
cmake-gcc: cmake-gcc:
cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON
cmake --build build-gcc cmake --build build-gcc -j
ctest --test-dir build-gcc --output-on-failure ctest --test-dir build-gcc --output-on-failure -j
cmake-clang: cmake-clang:
cmake -B build-clang -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON cmake -B build-clang -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON
cmake --build build-clang cmake --build build-clang -j
sashidown-lib: $(SD_DIR)/libsashidown.a # Always ask the sub-make; it no-ops when libsashidown.a is current.
sashidown-lib:
# The sub-make decides whether the archive is stale; FORCE only makes sure it
# is asked, and -j builds wait for it before linking.
$(SD_DIR)/libsashidown.a: FORCE
$(MAKE) -C $(SD_DIR) libsashidown.a $(MAKE) -C $(SD_DIR) libsashidown.a
FORCE: cst-agent: $(OBJ) sashidown-lib
cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a
$(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS) $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS)
# Built-in model data as a C byte list (included by model_registry.c). # Built-in model data as a C byte list (included by model_registry.c).
@@ -232,11 +229,15 @@ cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a
@mkdir -p .gen @mkdir -p .gen
@sh scripts/embed_web.sh $@ $(WEB_DIR) @sh scripts/embed_web.sh $@ $(WEB_DIR)
%.o: %.c $(HDRS) # Incremental rebuilds: -MMD tracks real includes instead of every header.
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< COMPILE_C = $(CC) $(CFLAGS) $(CPPFLAGS) -MMD -MP -c -o $@ $<
src/tui.o: src/tui.c $(HDRS) $(SD_DIR)/include/sashidown.h %.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< $(COMPILE_C)
src/model_registry.o: .gen/models_builtin.inc
src/webui.o: .gen/web_assets.inc
src/tui.o: $(SD_DIR)/include/sashidown.h
tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC) tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_frontier.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_frontier.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
@@ -244,7 +245,7 @@ tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h
tests/test_ctl_heredoc: tests/test_ctl_heredoc.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC) tests/test_ctl_heredoc: tests/test_ctl_heredoc.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_heredoc.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_heredoc.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(HDRS) $(TEST_OS_SRC) tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) $(TEST_SYS_LIBS) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC)
@@ -265,32 +266,47 @@ tests/test_pdf_text: tests/test_pdf_text.c src/pdf_text.c include/cst_pdf.h $(TE
tests/test_markdown: tests/test_markdown.c $(SD_DIR)/libsashidown.a tests/test_markdown: tests/test_markdown.c $(SD_DIR)/libsashidown.a
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_markdown.c $(LDFLAGS) -lsashidown -lm $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_markdown.c $(LDFLAGS) -lsashidown -lm
tests/test_%: tests/test_%.c $(CORE_SRC) $(HDRS) # Core tests reuse libcstcore.a — avoid recompiling ~35 .c files per binary.
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CORE_SRC) $(TEST_LIBS) # Do not depend on the phony sashidown-lib target or every test relinks each check.
tests/test_%: tests/test_%.c $(CST_CORE_LIB) $(SD_DIR)/libsashidown.a
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CST_CORE_LIB) $(TEST_LIBS)
$(TESTS): sashidown-lib $(SD_DIR)/libsashidown.a: sashidown-lib
check: $(TESTS) # Build test binaries with make -j; run them one-by-one for readable logs.
check: sashidown-lib $(TESTS)
@set -e; for t in $(TESTS); do echo "== $$t"; ./$$t; done @set -e; for t in $(TESTS); do echo "== $$t"; ./$$t; done
# AddressSanitizer + UBSan build of the agent and all tests. # AddressSanitizer + UBSan rebuild (clears objects so sanitizer flags apply).
# Under a ptrace-based sandbox (some Termux setups) LeakSanitizer aborts with
# "Tracer caught signal 11"; add ASAN_OPTIONS=detect_leaks=0 there.
asan: asan:
$(MAKE) clean $(MAKE) clean
$(MAKE) sashidown-lib $(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" \
$(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" cst-agent check sashidown-lib cst-agent check
demo: cst-agent demo: cst-agent
./cst-agent --demo-ctl --workspace . ./cst-agent --demo-ctl --workspace .
clean: clean:
rm -f $(OBJ) cst-agent $(CST_CORE_LIB) $(TESTS) .gen/models_builtin.inc \ rm -f $(OBJ) $(DEPFILES) cst-agent $(CST_CORE_LIB) $(TESTS) \
.gen/web_assets.inc .gen/models_builtin.inc .gen/web_assets.inc
install: cst-agent install: cst-agent
install -d "$(DESTDIR)$(PREFIX)/bin" install -d "$(DESTDIR)$(PREFIX)/bin"
install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent" install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent"
# Optional systemd --user unit for the parliament seat service.
install-service: install
install -d "$(DESTDIR)$(HOME)/.config/systemd/user"
sed -e 's|@BIN@|$(PREFIX)/bin/cst-agent|g' \
scripts/cst-agent-parliament.service.in \
> "$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service"
@echo "Installed user unit. Enable with:"
@echo " systemctl --user daemon-reload"
@echo " systemctl --user enable --now cst-agent-parliament.service"
@echo "(set CST_REALM_PASSWORD / CST_INSTANCE_ID in the drop-in or environment)"
uninstall: uninstall:
rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent" rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent"
rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service"
-include $(DEPFILES)
+7 -6
View File
@@ -41,8 +41,8 @@ irm https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/install_window
也可以下载 Release 里的安装包 `cst-agent-VERSION-windows-x86_64-setup.exe`(按用户安装、自动加 PATH、 也可以下载 Release 里的安装包 `cst-agent-VERSION-windows-x86_64-setup.exe`(按用户安装、自动加 PATH、
开始菜单有 WebUI 入口),或便携版 zip。详见 [docs/README-windows.md](docs/README-windows.md)。 开始菜单有 WebUI 入口),或便携版 zip。详见 [docs/README-windows.md](docs/README-windows.md)。
一键脚本可以重复运行:已有仓库会 `git pull --ff-only`,代码有更新时干净重编,然后重新安装; 一键脚本可以重复运行:已有仓库会 `git pull --ff-only`,**增量编译**后重新安装(`CST_AGENT_CLEAN=1` 才强制 clean);
同时装一个 `cst-agent-update` 命令(等同重跑脚本)。NixOS 上会自动进 `nix-shell` 编译。 同时装 `cst-agent-update` 与 `cst-agent-service`(议会席位服务辅助)。NixOS 上会自动进 `nix-shell` 编译。
之后再启动会沿用上次的 provider、模型和推理强度(low / medium / high / max),不需要任何参数或环境变量。 之后再启动会沿用上次的 provider、模型和推理强度(low / medium / high / max),不需要任何参数或环境变量。
界面里用 `/provider` 切换(没保存过 key 的会当场询问),`/model`、`/effort` 切换模型和推理强度,`/key` 更换 key。 界面里用 `/provider` 切换(没保存过 key 的会当场询问),`/model`、`/effort` 切换模型和推理强度,`/key` 更换 key。
@@ -90,14 +90,15 @@ scripts/run_matrix.sh -p "native-eos native-inc ctl ctl-heredoc xml" \
```bash ```bash
cd CST-agent cd CST-agent
make # 依赖 pkg-config 找 libcurl / ncursesw(找不到时退回 ncurses) make -j$(nproc) # 增量编译;头文件依赖走 -MMD,测试复用 libcstcore.a
make check # 单元测试 + 截断实验(含真实录制流) make -j$(nproc) check # 并行编测试二进制,再串行跑
make asan # AddressSanitizer + UBSan 构建并跑全部测试 make asan # AddressSanitizer + UBSan 构建并跑全部测试
make CC=tcc AR=ar check # TinyCC:整套构建 + 测试十几秒(CI 的校验就是这条) make CC=tcc AR=ar check # TinyCC:整套构建 + 测试十几秒(CI 的校验就是这条)
make install PREFIX=$HOME/.local # 或 sudo make install(默认 /usr/local) make install PREFIX=$HOME/.local # 或 sudo make install(默认 /usr/local)
make install-service PREFIX=$HOME/.local # 可选:systemd --user 议会服务单元
``` ```
NixOS:`nix-shell`(仓库自带 [`shell.nix`](shell.nix))后再 `make`。 NixOS:`nix-shell --run 'make -j$(nproc) && make install PREFIX=$HOME/.local'`(CLI);Qt 桌面用 `nix-build` / `cst-agent-app`。
macOS:`brew install curl ncurses pkg-config` 后直接 `make`(Makefile 会找 Homebrew 的 keg-only 库)。 macOS:`brew install curl ncurses pkg-config` 后直接 `make`(Makefile 会找 Homebrew 的 keg-only 库)。
各平台细节与验收清单见 [docs/compat.md](docs/compat.md)。 各平台细节与验收清单见 [docs/compat.md](docs/compat.md)。
+16
View File
@@ -35,6 +35,22 @@ cst-agent # TUI: /status 或 /realm
cst-agent --service-stop cst-agent --service-stop
``` ```
### 常驻服务(推荐)
登录后会 fork 后台席位进程;也可用 systemd --user 托管前台循环(崩溃自动拉起):
```bash
# 安装脚本附带的辅助命令
cst-agent-service install-unit
# 在 ~/.config/cst-agent/parliament.env 写入:
# CST_REALM_PASSWORD=…
# CST_INSTANCE_ID=my-laptop
systemctl --user enable --now cst-agent-parliament.service
cst-agent-service status
```
或手动前台:`CST_REALM_PASSWORD=… cst-agent --service-fg`。
密码 Realm 开启后,直接打开 agent 会要求登录(TTY 下可当场输入密码)。 密码 Realm 开启后,直接打开 agent 会要求登录(TTY 下可当场输入密码)。
## 议员协作协议 ## 议员协作协议
+1 -1
View File
@@ -3,7 +3,7 @@
#define CST_VERSION_MAJOR 0 #define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 9 #define CST_VERSION_MINOR 9
#define CST_VERSION_PATCH 19 #define CST_VERSION_PATCH 20
#define CST_VERSION_STR_(x) #x #define CST_VERSION_STR_(x) #x
#define CST_VERSION_STR(x) CST_VERSION_STR_(x) #define CST_VERSION_STR(x) CST_VERSION_STR_(x)
+39 -4
View File
@@ -84,15 +84,20 @@ main() {
local jobs local jobs
jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
echo "==> build" echo "==> build (incremental, -j$jobs)"
if ((changed)); then # Prefer incremental rebuilds. Set CST_AGENT_CLEAN=1 to force a clean tree.
if [[ "${CST_AGENT_CLEAN:-0}" == "1" ]]; then
run_make clean >/dev/null 2>&1 || true run_make clean >/dev/null 2>&1 || true
fi fi
# `changed` is only used so re-exec after pull still builds; keep for clarity.
: "${changed:=0}"
if ! run_make -j"$jobs"; then if ! run_make -j"$jobs"; then
deps_hint deps_hint
return 1 return 1
fi fi
run_make check >/dev/null 2>&1 || echo "warning: some tests failed (run 'make check')" if [[ "${CST_AGENT_SKIP_CHECK:-0}" != "1" ]]; then
run_make -j"$jobs" check >/dev/null 2>&1 || echo "warning: some tests failed (run 'make check')"
fi
echo "==> install to $prefix/bin" echo "==> install to $prefix/bin"
mkdir -p "$prefix/bin" mkdir -p "$prefix/bin"
@@ -103,6 +108,35 @@ export CST_AGENT_PREFIX="\${CST_AGENT_PREFIX:-$prefix}"
exec bash "$dir/scripts/bootstrap_linux.sh" "$dir" "\$@" exec bash "$dir/scripts/bootstrap_linux.sh" "$dir" "\$@"
EOF EOF
chmod 0755 "$prefix/bin/cst-agent-update" chmod 0755 "$prefix/bin/cst-agent-update"
cat >"$prefix/bin/cst-agent-service" <<EOF
#!$(command -v bash)
# Manage the parliament seat service (foreground under systemd, or --service).
set -euo pipefail
BIN="$prefix/bin/cst-agent"
cmd="\${1:-status}"
case "\$cmd" in
status)
"\$BIN" --version
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active cst-agent-parliament.service >/dev/null 2>&1; then
systemctl --user status cst-agent-parliament.service --no-pager
else
"\$BIN" --service-stop >/dev/null 2>&1 || true
echo "parliament service: not managed by systemd --user (use --login / --service)"
fi
;;
install-unit)
make -C "$dir" install-service PREFIX="$prefix"
;;
start) systemctl --user start cst-agent-parliament.service ;;
stop) systemctl --user stop cst-agent-parliament.service; "\$BIN" --service-stop || true ;;
restart) systemctl --user restart cst-agent-parliament.service ;;
*)
echo "usage: cst-agent-service {status|install-unit|start|stop|restart}" >&2
exit 2
;;
esac
EOF
chmod 0755 "$prefix/bin/cst-agent-service"
echo echo
echo "OK: $("$prefix/bin/cst-agent" --version)" echo "OK: $("$prefix/bin/cst-agent" --version)"
@@ -110,7 +144,8 @@ EOF
*":$prefix/bin:"*) ;; *":$prefix/bin:"*) ;;
*) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;;
esac esac
echo "Run 'cst-agent' inside a project directory; upgrade later with 'cst-agent-update'." echo "Run 'cst-agent' inside a project directory; upgrade with 'cst-agent-update'."
echo "Parliament seat service helper: 'cst-agent-service status|install-unit'."
echo "The first start asks for the provider and its API key and saves them." echo "The first start asks for the provider and its API key and saves them."
} }
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=CST-Agent parliament seat service
Documentation=file://%h/CST-agent/docs/parliament.md
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# Prefer --service-fg under systemd so the unit tracks the real process.
ExecStart=@BIN@ --service-fg --peer-port ${CST_PEER_PORT:-9100}
Restart=on-failure
RestartSec=3
Environment=CST_INSTANCE_ID=%H
# Put the Realm password in a drop-in, e.g.:
# ~/.config/systemd/user/cst-agent-parliament.service.d/override.conf
# [Service]
# Environment=CST_REALM_PASSWORD=…
EnvironmentFile=-%h/.config/cst-agent/parliament.env
[Install]
WantedBy=default.target
+27 -4
View File
@@ -139,6 +139,26 @@ int cst_discover_local_put(const char *realm_fp, const char *instance_id,
arr = cJSON_CreateArray(); arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "instances", arr); cJSON_AddItemToObject(root, "instances", arr);
} }
time_t now = time(NULL);
const cJSON *it;
cJSON_ArrayForEach(it, arr) {
const cJSON *iid = cJSON_GetObjectItemCaseSensitive(it, "iid");
const cJSON *rid = cJSON_GetObjectItemCaseSensitive(it, "rid");
const cJSON *port = cJSON_GetObjectItemCaseSensitive(it, "port");
const cJSON *updated = cJSON_GetObjectItemCaseSensitive(it, "updated");
if (!cJSON_IsString(iid) || strcmp(iid->valuestring, instance_id) != 0)
continue;
if (cJSON_IsString(rid) && !strcmp(rid->valuestring, realm_fp) &&
cJSON_IsNumber(port) && port->valueint == peer_port &&
cJSON_IsNumber(updated) &&
now - (time_t)updated->valuedouble < 60) {
/* Same seat advertisement still fresh — skip disk rewrite. */
cJSON_Delete(root);
pthread_mutex_unlock(&local_mu);
return 0;
}
break;
}
local_remove_iid(arr, instance_id); local_remove_iid(arr, instance_id);
cJSON *row = cJSON_CreateObject(); cJSON *row = cJSON_CreateObject();
cJSON_AddStringToObject(row, "rid", realm_fp); cJSON_AddStringToObject(row, "rid", realm_fp);
@@ -146,7 +166,7 @@ int cst_discover_local_put(const char *realm_fp, const char *instance_id,
cJSON_AddStringToObject(row, "name", name ? name : ""); cJSON_AddStringToObject(row, "name", name ? name : "");
cJSON_AddNumberToObject(row, "port", peer_port); cJSON_AddNumberToObject(row, "port", peer_port);
cJSON_AddNumberToObject(row, "pid", (double)os_getpid()); cJSON_AddNumberToObject(row, "pid", (double)os_getpid());
cJSON_AddNumberToObject(row, "updated", (double)time(NULL)); cJSON_AddNumberToObject(row, "updated", (double)now);
cJSON_AddItemToArray(arr, row); cJSON_AddItemToArray(arr, row);
int rc = save_local(root); int rc = save_local(root);
cJSON_Delete(root); cJSON_Delete(root);
@@ -283,12 +303,15 @@ static void *discover_main(void *arg) {
cst_discover_t *d = arg; cst_discover_t *d = arg;
int ticks = 0; int ticks = 0;
while (!d->stop) { while (!d->stop) {
if ((ticks++ % 10) == 0) { /* ~100ms recv timeout: beacon every ~5s, rewrite local registry ~60s. */
if ((ticks % 50) == 0) {
send_beacon(d); send_beacon(d);
(void)cst_discover_local_put(d->realm_fp, d->instance_id, d->name, if ((ticks / 50) % 12 == 0)
d->peer_port); (void)cst_discover_local_put(d->realm_fp, d->instance_id, d->name,
d->peer_port);
ingest_local(d); ingest_local(d);
} }
ticks++;
if (d->sock == DISC_BAD) { if (d->sock == DISC_BAD) {
struct timespec ts = {0, 100 * 1000 * 1000}; struct timespec ts = {0, 100 * 1000 * 1000};
nanosleep(&ts, NULL); nanosleep(&ts, NULL);
+6 -1
View File
@@ -20,7 +20,12 @@ void cst_extensions_wait(FILE *log) {
cst_mcp_counts(&ready, &total); cst_mcp_counts(&ready, &total);
if (total == 0) if (total == 0)
return; return;
cst_mcp_wait_ready(30000); /* Default 8s (was 30s); override with CST_MCP_WAIT_MS. */
const char *e = getenv("CST_MCP_WAIT_MS");
int ms = e && atoi(e) >= 0 ? atoi(e) : 8000;
if (ms > 60000)
ms = 60000;
cst_mcp_wait_ready(ms);
cst_mcp_info_t info[CST_MCP_MAX]; cst_mcp_info_t info[CST_MCP_MAX];
int n = cst_mcp_info(info, CST_MCP_MAX); int n = cst_mcp_info(info, CST_MCP_MAX);
for (int i = 0; log && i < n; i++) { for (int i = 0; log && i < n; i++) {
+54 -36
View File
@@ -22,6 +22,21 @@
#include <unistd.h> #include <unistd.h>
#endif #endif
/* Defer libcurl init: --version / --login / parliament CLI need none of it. */
static int g_curl_ready;
static void cst_curl_ensure(void) {
if (!g_curl_ready) {
curl_global_init(CURL_GLOBAL_DEFAULT);
g_curl_ready = 1;
}
}
static void cst_curl_cleanup(void) {
if (g_curl_ready) {
curl_global_cleanup();
g_curl_ready = 0;
}
}
static void usage(const char *argv0) { static void usage(const char *argv0) {
fprintf(stderr, fprintf(stderr,
"Usage: %s [options] (works in the current directory)\n" "Usage: %s [options] (works in the current directory)\n"
@@ -311,7 +326,6 @@ int main(int argc, char **argv) {
"1903 or later is required); non-ASCII text may break\n"); "1903 or later is required); non-ASCII text may break\n");
os_set_prog_path(argv[0]); os_set_prog_path(argv[0]);
os_temp_dir_export(); os_temp_dir_export();
curl_global_init(CURL_GLOBAL_DEFAULT);
cst_agent_config_t cfg = {0}; cst_agent_config_t cfg = {0};
char cwd[4096]; char cwd[4096];
@@ -532,7 +546,7 @@ int main(int argc, char **argv) {
case 'r': case 'r':
if (cst_protocol_parse(optarg, &cfg.protocol) != 0) { if (cst_protocol_parse(optarg, &cfg.protocol) != 0) {
fprintf(stderr, "unknown protocol: %s\n", optarg); fprintf(stderr, "unknown protocol: %s\n", optarg);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
break; break;
@@ -545,7 +559,7 @@ int main(int argc, char **argv) {
if (cst_tool_profile_parse(optarg, &cfg.tool_profile) != 0) { if (cst_tool_profile_parse(optarg, &cfg.tool_profile) != 0) {
fprintf(stderr, "unknown tool profile: %s (default | minimal)\n", fprintf(stderr, "unknown tool profile: %s (default | minimal)\n",
optarg); optarg);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
break; break;
@@ -580,14 +594,14 @@ int main(int argc, char **argv) {
case 'A': case 'A':
if (cst_approval_parse(optarg, &approval) != 0) { if (cst_approval_parse(optarg, &approval) != 0) {
fprintf(stderr, "bad --approval %s (ask | auto | yolo)\n", optarg); fprintf(stderr, "bad --approval %s (ask | auto | yolo)\n", optarg);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
break; break;
case 'C': case 'C':
if (parse_confirm(optarg, &cfg) != 0) { if (parse_confirm(optarg, &cfg) != 0) {
fprintf(stderr, "bad --confirm %s (shell,write,net | none)\n", optarg); fprintf(stderr, "bad --confirm %s (shell,write,net | none)\n", optarg);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
break; break;
@@ -595,7 +609,7 @@ int main(int argc, char **argv) {
if (!cst_effort_valid(optarg)) { if (!cst_effort_valid(optarg)) {
fprintf(stderr, "bad --effort %s (low | medium | high | max)\n", fprintf(stderr, "bad --effort %s (low | medium | high | max)\n",
optarg); optarg);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg); snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg);
@@ -603,21 +617,22 @@ int main(int argc, char **argv) {
case 'M': case 'M':
if (!cst_mode_valid(optarg)) { if (!cst_mode_valid(optarg)) {
fprintf(stderr, "bad --mode %s (coding | general)\n", optarg); fprintf(stderr, "bad --mode %s (coding | general)\n", optarg);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
snprintf(cfg.mode, sizeof(cfg.mode), "%s", optarg); snprintf(cfg.mode, sizeof(cfg.mode), "%s", optarg);
break; break;
case 'h': case 'h':
usage(argv[0]); usage(argv[0]);
curl_global_cleanup(); cst_curl_cleanup();
return 0; return 0;
case 0x100: { case 0x100: {
const char *only = optarg; const char *only = optarg;
if (!only && optind < argc && argv[optind][0] != '-') if (!only && optind < argc && argv[optind][0] != '-')
only = argv[optind++]; only = argv[optind++];
cst_curl_ensure();
int rc = list_models(only); int rc = list_models(only);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
case 0x101: case 0x101:
@@ -626,7 +641,7 @@ int main(int argc, char **argv) {
case 0x102: case 0x102:
if (parse_port(optarg, &web.port) != 0) { if (parse_port(optarg, &web.port) != 0) {
fprintf(stderr, "--web-port %s is not a port (0-65535)\n", optarg); fprintf(stderr, "--web-port %s is not a port (0-65535)\n", optarg);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
web_mode = 1; web_mode = 1;
@@ -652,30 +667,30 @@ int main(int argc, char **argv) {
break; break;
case 'V': case 'V':
printf("cst-agent %s\n", CST_VERSION); printf("cst-agent %s\n", CST_VERSION);
curl_global_cleanup(); cst_curl_cleanup();
return 0; return 0;
default: default:
usage(argv[0]); usage(argv[0]);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
} }
if (do_migrate) { if (do_migrate) {
int rc = migrate_store(cfg.workspace_root); int rc = migrate_store(cfg.workspace_root);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_realm_new) { if (do_realm_new) {
int rc = cst_parliament_cli_realm_new(); int rc = cst_parliament_cli_realm_new();
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_realm_pw_init) { if (do_realm_pw_init) {
const char *pw = getenv("CST_REALM_PASSWORD"); const char *pw = getenv("CST_REALM_PASSWORD");
int rc = cst_parliament_cli_realm_password_init(pw); int rc = cst_parliament_cli_realm_password_init(pw);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_login) { if (do_login) {
@@ -687,17 +702,17 @@ int main(int argc, char **argv) {
iid = iidbuf; iid = iidbuf;
} }
int rc = cst_parliament_cli_login(pw, iid, realm_label, discover_peer_port); int rc = cst_parliament_cli_login(pw, iid, realm_label, discover_peer_port);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_new_project) { if (do_new_project) {
int rc = cst_parliament_cli_new_project(new_project_id); int rc = cst_parliament_cli_new_project(new_project_id);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_service_stop) { if (do_service_stop) {
int rc = cst_parliament_cli_service_stop(); int rc = cst_parliament_cli_service_stop();
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_service || do_service_fg) { if (do_service || do_service_fg) {
@@ -710,12 +725,12 @@ int main(int argc, char **argv) {
} }
int rc = cst_parliament_cli_service(pw, iid, discover_peer_port, int rc = cst_parliament_cli_service(pw, iid, discover_peer_port,
do_service_fg); do_service_fg);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_realm_enroll) { if (do_realm_enroll) {
int rc = cst_parliament_cli_realm_enroll(realm_enroll_id, realm_label); int rc = cst_parliament_cli_realm_enroll(realm_enroll_id, realm_label);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_peer_listen) { if (do_peer_listen) {
@@ -723,7 +738,7 @@ int main(int argc, char **argv) {
if (!iid) if (!iid)
iid = getenv("CST_INSTANCE_ID"); iid = getenv("CST_INSTANCE_ID");
int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid); int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_peer_dial) { if (do_peer_dial) {
@@ -743,13 +758,13 @@ int main(int argc, char **argv) {
} }
} }
int rc = cst_parliament_cli_peer_dial(host, port, iid); int rc = cst_parliament_cli_peer_dial(host, port, iid);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
if (do_discover) { if (do_discover) {
const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID"); const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID");
int rc = cst_parliament_cli_discover(iid, discover_peer_port, discover_wait_ms); int rc = cst_parliament_cli_discover(iid, discover_peer_port, discover_wait_ms);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
@@ -778,7 +793,7 @@ int main(int argc, char **argv) {
fprintf(stderr, "%s\n", err[0] ? err : "login required"); fprintf(stderr, "%s\n", err[0] ? err : "login required");
if (pw == pwbuf) if (pw == pwbuf)
memset(pwbuf, 0, sizeof(pwbuf)); memset(pwbuf, 0, sizeof(pwbuf));
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
free(hand); free(hand);
@@ -792,7 +807,7 @@ int main(int argc, char **argv) {
fprintf(stderr, fprintf(stderr,
"password Realm active: run --login (or set " "password Realm active: run --login (or set "
"CST_REALM_PASSWORD) before opening the agent\n"); "CST_REALM_PASSWORD) before opening the agent\n");
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
} }
@@ -800,10 +815,13 @@ int main(int argc, char **argv) {
if (do_list) { if (do_list) {
int rc = list_sessions(cfg.workspace_root); int rc = list_sessions(cfg.workspace_root);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
/* Agent / TUI / WebUI / headless need libcurl; parliament CLI above does not. */
cst_curl_ensure();
/* Explicit option > environment > last used (settings.json) > preset. */ /* Explicit option > environment > last used (settings.json) > preset. */
cst_settings_t saved; cst_settings_t saved;
cst_settings_load(&saved); cst_settings_load(&saved);
@@ -815,7 +833,7 @@ int main(int argc, char **argv) {
prov = cst_provider_find(provider_name); prov = cst_provider_find(provider_name);
if (!prov) { if (!prov) {
fprintf(stderr, "unknown provider: %s\n", provider_name); fprintf(stderr, "unknown provider: %s\n", provider_name);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
} }
@@ -894,7 +912,7 @@ int main(int argc, char **argv) {
if (rc != 0 || cst_agent_store_path_for_id(cfg.workspace_root, id, if (rc != 0 || cst_agent_store_path_for_id(cfg.workspace_root, id,
cfg.session_path, cfg.session_path,
sizeof(cfg.session_path)) != 0) { sizeof(cfg.session_path)) != 0) {
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
cst_agent_store_set_current(cfg.workspace_root, id); cst_agent_store_set_current(cfg.workspace_root, id);
@@ -906,7 +924,7 @@ int main(int argc, char **argv) {
sizeof(id)) != 0) { sizeof(id)) != 0) {
fprintf(stderr, "failed to init .agent/sessions under %s\n", fprintf(stderr, "failed to init .agent/sessions under %s\n",
cfg.workspace_root); cfg.workspace_root);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
fprintf(stderr, "session %s\n", id); fprintf(stderr, "session %s\n", id);
@@ -914,7 +932,7 @@ int main(int argc, char **argv) {
if (do_demo) { if (do_demo) {
int rc = demo_ctl_impl(cfg.workspace_root); int rc = demo_ctl_impl(cfg.workspace_root);
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
@@ -924,7 +942,7 @@ int main(int argc, char **argv) {
if (cst_creds_prompt_console(cfg.provider, cfg.api_key, if (cst_creds_prompt_console(cfg.provider, cfg.api_key,
sizeof(cfg.api_key)) != 0) { sizeof(cfg.api_key)) != 0) {
fprintf(stderr, "no API key entered\n"); fprintf(stderr, "no API key entered\n");
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
char dir[1024] = "the config directory"; char dir[1024] = "the config directory";
@@ -936,7 +954,7 @@ int main(int argc, char **argv) {
dir); dir);
/* --ask-key alone: save and exit */ /* --ask-key alone: save and exit */
if (force_ask_key && !cfg.headless && !prompt) { if (force_ask_key && !cfg.headless && !prompt) {
curl_global_cleanup(); cst_curl_cleanup();
return 0; return 0;
} }
} }
@@ -945,7 +963,7 @@ int main(int argc, char **argv) {
"no API key for %s: start cst-agent once to enter it, or run " "no API key for %s: start cst-agent once to enter it, or run "
"cst-agent --ask-key --provider %s\n", "cst-agent --ask-key --provider %s\n",
cfg.provider, cfg.provider); cfg.provider, cfg.provider);
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
/* Router presets have no fixed model: take the first one the API lists /* Router presets have no fixed model: take the first one the API lists
@@ -968,7 +986,7 @@ int main(int argc, char **argv) {
} else { } else {
fprintf(stderr, "no model for %s: pass --model (%s)\n", cfg.provider, fprintf(stderr, "no model for %s: pass --model (%s)\n", cfg.provider,
err[0] ? err : "the API listed none"); err[0] ? err : "the API listed none");
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
} }
@@ -989,7 +1007,7 @@ int main(int argc, char **argv) {
cst_agent_t *agent = cst_agent_create(&cfg); cst_agent_t *agent = cst_agent_create(&cfg);
if (!agent) { if (!agent) {
fprintf(stderr, "failed to create agent (session path?)\n"); fprintf(stderr, "failed to create agent (session path?)\n");
curl_global_cleanup(); cst_curl_cleanup();
return 1; return 1;
} }
@@ -1021,6 +1039,6 @@ int main(int argc, char **argv) {
cst_agent_destroy(agent); cst_agent_destroy(agent);
cst_extensions_unload(); cst_extensions_unload();
curl_global_cleanup(); cst_curl_cleanup();
return rc; return rc;
} }
+22 -12
View File
@@ -272,33 +272,43 @@ int cst_parliament_daemon_run(const char *password, const char *instance_id,
signal(SIGHUP, on_sig); signal(SIGHUP, on_sig);
#endif #endif
g_stop = 0; g_stop = 0;
int tick = 0; int tick = 0, idle = 0;
while (!g_stop) { while (!g_stop) {
cst_peer_session_t sess; cst_peer_session_t sess;
char herr[128]; char herr[128];
if (cst_peer_accept((intptr_t)ls, &cert, sk, realm.pk, 500, &sess, herr, /* Accept wait backs off when idle so a quiet seat barely wakes. */
sizeof(herr)) == 0) { int accept_ms = idle > 20 ? 2000 : idle > 5 ? 1000 : 400;
if (cst_peer_accept((intptr_t)ls, &cert, sk, realm.pk, accept_ms, &sess,
herr, sizeof(herr)) == 0) {
sync_on_session(&sess); sync_on_session(&sess);
cst_peer_close(&sess); cst_peer_close(&sess);
idle = 0;
} else {
idle++;
} }
int push = cst_parliament_sync_pending(); int push = cst_parliament_sync_pending();
tick++; tick++;
if ((push || tick % 10 == 0) && disc) { /* Dial peers on push, or periodically (slower while idle). */
int period = idle > 20 ? 45 : idle > 5 ? 20 : 8;
if ((push || tick % period == 0) && disc) {
cst_peer_info_t peers[16]; cst_peer_info_t peers[16];
int n = cst_discover_peers(disc, peers, 16); int n = cst_discover_peers(disc, peers, 16);
for (int i = 0; i < n; i++) { if (n > 0 || push) {
if (peers[i].port <= 0) for (int i = 0; i < n; i++) {
continue; if (peers[i].port <= 0)
if (cst_peer_dial(peers[i].host, peers[i].port, &cert, sk, realm.pk, continue;
3000, &sess, herr, sizeof(herr)) == 0) { if (cst_peer_dial(peers[i].host, peers[i].port, &cert, sk, realm.pk,
sync_on_session(&sess); 3000, &sess, herr, sizeof(herr)) == 0) {
cst_peer_close(&sess); sync_on_session(&sess);
cst_peer_close(&sess);
idle = 0;
}
} }
} }
if (push) if (push)
cst_parliament_sync_ack(); cst_parliament_sync_ack();
} }
sleep_ms(200); /* No extra sleep: accept_ms already paced the loop. */
} }
if (disc) if (disc)
cst_discover_stop(disc); cst_discover_stop(disc);
+32
View File
@@ -7,6 +7,8 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include <time.h>
static int settings_path(char *out, size_t n) { static int settings_path(char *out, size_t n) {
char dir[OS_PATH_MAX]; char dir[OS_PATH_MAX];
@@ -20,11 +22,32 @@ static void copy_str(char *dst, size_t n, const cJSON *v) {
snprintf(dst, n, "%s", v->valuestring); snprintf(dst, n, "%s", v->valuestring);
} }
static struct {
int valid;
time_t mtime;
off_t size;
cst_settings_t s;
} g_settings_cache;
static void settings_cache_invalidate(void) {
g_settings_cache.valid = 0;
g_settings_cache.mtime = 0;
g_settings_cache.size = 0;
}
int cst_settings_load(cst_settings_t *s) { int cst_settings_load(cst_settings_t *s) {
memset(s, 0, sizeof(*s)); memset(s, 0, sizeof(*s));
char path[OS_PATH_MAX]; char path[OS_PATH_MAX];
if (settings_path(path, sizeof(path)) != 0) if (settings_path(path, sizeof(path)) != 0)
return -1; return -1;
struct stat st;
int have_st = stat(path, &st) == 0;
if (have_st && g_settings_cache.valid &&
g_settings_cache.mtime == st.st_mtime &&
g_settings_cache.size == st.st_size) {
*s = g_settings_cache.s;
return 0;
}
FILE *fp = fopen(path, "rb"); FILE *fp = fopen(path, "rb");
if (!fp) if (!fp)
return -1; return -1;
@@ -94,10 +117,19 @@ int cst_settings_load(cst_settings_t *s) {
s->nparams++; s->nparams++;
} }
cJSON_Delete(root); cJSON_Delete(root);
if (!have_st)
have_st = stat(path, &st) == 0;
if (have_st) {
g_settings_cache.s = *s;
g_settings_cache.mtime = st.st_mtime;
g_settings_cache.size = st.st_size;
g_settings_cache.valid = 1;
}
return 0; return 0;
} }
int cst_settings_save(const cst_settings_t *s) { int cst_settings_save(const cst_settings_t *s) {
settings_cache_invalidate();
char dir[OS_PATH_MAX], path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8]; char dir[OS_PATH_MAX], path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8];
if (os_config_dir(dir, sizeof(dir)) != 0 || os_mkdir_p(dir, 0700) != 0 || if (os_config_dir(dir, sizeof(dir)) != 0 || os_mkdir_p(dir, 0700) != 0 ||
settings_path(path, sizeof(path)) != 0) settings_path(path, sizeof(path)) != 0)