feat: /realm in TUI, daemon instance takeover (0.9.16)

Pidfile records instance+port; login restarts the service when the seat
changes. TUI /realm status|login|stop; prompts discourage filler chatter.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 11:00:34 +08:00
co-authored by Cursor
parent e610d70c30
commit a3e115539e
8 changed files with 202 additions and 24 deletions
+4 -3
View File
@@ -28,9 +28,10 @@ cst-agent --login --realm-label "thinkpad"
查看状态: 查看状态:
```bash ```bash
cst-agent # 进入 TUI 后 /status cst-agent # TUI: /status 或 /realm
# 或 CLI # /realm login # TUI 内登录并启服务
cst-agent --service-stop # 停服务 # /realm stop # 停服务
cst-agent --service-stop
``` ```
密码 Realm 开启后,直接打开 agent 会要求登录(TTY 下可当场输入密码)。 密码 Realm 开启后,直接打开 agent 会要求登录(TTY 下可当场输入密码)。
+4 -1
View File
@@ -63,8 +63,11 @@ int cst_parliament_apply_sync(unsigned long revision, const char *parliament_id,
/* Background parliament service: discover + peer listen + context sync. */ /* Background parliament service: discover + peer listen + context sync. */
int cst_parliament_daemon_pidfile(char *out, size_t n); int cst_parliament_daemon_pidfile(char *out, size_t n);
int cst_parliament_daemon_running(void); int cst_parliament_daemon_running(void);
/* Read pidfile metadata. Returns 0 when a live service is recorded. */
int cst_parliament_daemon_info(int *pid_out, char *instance_out, size_t in_sz,
int *port_out);
/* Start in background (Unix fork). password unlocks Realm in the child. /* Start in background (Unix fork). password unlocks Realm in the child.
* Returns 0 and writes pid. If already running, returns 0. */ * Returns 0 and writes pid. Restarts if a different instance/port is running. */
int cst_parliament_daemon_start(const char *password, const char *instance_id, int cst_parliament_daemon_start(const char *password, const char *instance_id,
int peer_port, char *err, size_t err_sz); int peer_port, char *err, size_t err_sz);
int cst_parliament_daemon_stop(void); int cst_parliament_daemon_stop(void);
+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 15 #define CST_VERSION_PATCH 16
#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)
+4 -2
View File
@@ -292,7 +292,8 @@ static const char DEF_INTRO[] =
"- Verify (build, tests, or running it) before saying it works; if you " "- Verify (build, tests, or running it) before saying it works; if you "
"could not verify, say so.\n" "could not verify, say so.\n"
"- Keep going until the task is done or truly blocked. Prefer acting with " "- Keep going until the task is done or truly blocked. Prefer acting with "
"tools over describing the next step; do not stop at a plan unless asked.\n\n"; "tools over describing the next step; do not stop at a plan unless asked. "
"Skip filler like \"I'll start by looking\" — call a tool immediately.\n\n";
/* Same role, framed for /mode general: the workspace may hold documents, /* Same role, framed for /mode general: the workspace may hold documents,
* data or anything else, not just code. */ * data or anything else, not just code. */
@@ -312,7 +313,8 @@ static const char DEF_INTRO_GENERAL[] =
"- Check your work when you can (re-read it, run it, cross-check the " "- Check your work when you can (re-read it, run it, cross-check the "
"source); if you could not verify something, say so.\n" "source); if you could not verify something, say so.\n"
"- Keep going until the task is done or truly blocked. Prefer acting with " "- Keep going until the task is done or truly blocked. Prefer acting with "
"tools over describing the next step; do not stop at a plan unless asked.\n\n"; "tools over describing the next step; do not stop at a plan unless asked. "
"Skip filler like \"I'll start by looking\" — call a tool immediately.\n\n";
static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) { static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
cst_buf_puts(b, "\nTool use:\n"); cst_buf_puts(b, "\nTool use:\n");
+9 -2
View File
@@ -449,7 +449,10 @@ char *cst_parliament_status_text(void) {
cst_parliament_load(&m); cst_parliament_load(&m);
char sid[64] = {0}; char sid[64] = {0};
int sess = cst_parliament_session_ok(sid, sizeof(sid)); int sess = cst_parliament_session_ok(sid, sizeof(sid));
int svc = cst_parliament_daemon_running(); int svc_pid = 0, svc_port = 0;
char svc_id[64] = {0};
int svc = cst_parliament_daemon_info(&svc_pid, svc_id, sizeof(svc_id),
&svc_port) == 0;
cst_buf_printf(&b, "realm %s\n", pw ? "password" : "file-keys/legacy"); cst_buf_printf(&b, "realm %s\n", pw ? "password" : "file-keys/legacy");
cst_buf_printf(&b, "parliament %s project=%s rev=%lu\n", cst_buf_printf(&b, "parliament %s project=%s rev=%lu\n",
m.status[0] ? m.status : "idle", m.status[0] ? m.status : "idle",
@@ -460,6 +463,10 @@ char *cst_parliament_status_text(void) {
m.chair_device[0] ? " @ " : "", m.chair_device); m.chair_device[0] ? " @ " : "", m.chair_device);
cst_buf_printf(&b, "login %s%s%s\n", sess ? "ok" : "required", cst_buf_printf(&b, "login %s%s%s\n", sess ? "ok" : "required",
sess && sid[0] ? " as " : "", sess ? sid : ""); sess && sid[0] ? " as " : "", sess ? sid : "");
cst_buf_printf(&b, "service %s\n", svc ? "running" : "stopped"); if (svc)
cst_buf_printf(&b, "service running pid=%d id=%s port=%d\n", svc_pid,
svc_id[0] ? svc_id : "-", svc_port);
else
cst_buf_puts(&b, "service stopped\n");
return cst_buf_steal(&b); return cst_buf_steal(&b);
} }
+98 -15
View File
@@ -41,21 +41,53 @@ int cst_parliament_daemon_pidfile(char *out, size_t n) {
return os_path_join(out, n, dir, "parliament.pid"); return os_path_join(out, n, dir, "parliament.pid");
} }
static int read_pid(void) { typedef struct {
int pid;
char instance[64];
int port;
} daemon_meta_t;
static int read_meta(daemon_meta_t *m) {
memset(m, 0, sizeof(*m));
m->pid = -1;
char path[OS_PATH_MAX]; char path[OS_PATH_MAX];
if (cst_parliament_daemon_pidfile(path, sizeof(path)) != 0) if (cst_parliament_daemon_pidfile(path, sizeof(path)) != 0)
return -1; return -1;
FILE *f = fopen(path, "rb"); FILE *f = fopen(path, "rb");
if (!f) if (!f)
return -1; return -1;
int pid = -1; char line[256];
if (fscanf(f, "%d", &pid) != 1) while (fgets(line, sizeof(line), f)) {
pid = -1; if (!strncmp(line, "pid=", 4))
m->pid = atoi(line + 4);
else if (!strncmp(line, "instance=", 9)) {
const char *p = line + 9;
size_t i = 0;
while (p[i] && p[i] != '\n' && p[i] != '\r' &&
i + 1 < sizeof(m->instance)) {
m->instance[i] = p[i];
i++;
}
m->instance[i] = 0;
} else if (!strncmp(line, "port=", 5))
m->port = atoi(line + 5);
else if (m->pid <= 0) {
/* Legacy single-line pidfile. */
int pid = atoi(line);
if (pid > 0)
m->pid = pid;
}
}
fclose(f); fclose(f);
return pid; return m->pid > 0 ? 0 : -1;
} }
static int write_pid(int pid) { static int read_pid(void) {
daemon_meta_t m;
return read_meta(&m) == 0 ? m.pid : -1;
}
static int write_pid(int pid, const char *instance_id, int peer_port) {
char path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8]; char path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8];
if (cst_parliament_daemon_pidfile(path, sizeof(path)) != 0) if (cst_parliament_daemon_pidfile(path, sizeof(path)) != 0)
return -1; return -1;
@@ -64,7 +96,9 @@ static int write_pid(int pid) {
FILE *f = fopen(tmp, "wb"); FILE *f = fopen(tmp, "wb");
if (!f) if (!f)
return -1; return -1;
int ok = fprintf(f, "%d\n", pid) > 0; int ok = fprintf(f, "pid=%d\ninstance=%s\nport=%d\n", pid,
instance_id ? instance_id : "",
peer_port > 0 ? peer_port : 0) > 0;
if (fclose(f) != 0) if (fclose(f) != 0)
ok = 0; ok = 0;
if (!ok) { if (!ok) {
@@ -80,8 +114,7 @@ static void clear_pid(void) {
remove(path); remove(path);
} }
int cst_parliament_daemon_running(void) { static int pid_alive(int pid) {
int pid = read_pid();
if (pid <= 0) if (pid <= 0)
return 0; return 0;
#ifdef _WIN32 #ifdef _WIN32
@@ -95,6 +128,34 @@ int cst_parliament_daemon_running(void) {
#endif #endif
} }
int cst_parliament_daemon_running(void) {
daemon_meta_t m;
if (read_meta(&m) != 0)
return 0;
if (!pid_alive(m.pid)) {
clear_pid();
return 0;
}
return 1;
}
int cst_parliament_daemon_info(int *pid_out, char *instance_out, size_t in_sz,
int *port_out) {
daemon_meta_t m;
if (read_meta(&m) != 0 || !pid_alive(m.pid)) {
if (read_meta(&m) == 0 && !pid_alive(m.pid))
clear_pid();
return -1;
}
if (pid_out)
*pid_out = m.pid;
if (instance_out && in_sz)
snprintf(instance_out, in_sz, "%s", m.instance);
if (port_out)
*port_out = m.port;
return 0;
}
static void sleep_ms(int ms) { static void sleep_ms(int ms) {
#ifdef _WIN32 #ifdef _WIN32
Sleep((DWORD)ms); Sleep((DWORD)ms);
@@ -194,7 +255,8 @@ int cst_parliament_daemon_run(const char *password, const char *instance_id,
#else #else
(int)getpid() (int)getpid()
#endif #endif
); ,
instance_id, bound);
fprintf(stderr, "parliament_service pid=%d port=%d id=%s realm=%s\n", fprintf(stderr, "parliament_service pid=%d port=%d id=%s realm=%s\n",
#ifdef _WIN32 #ifdef _WIN32
(int)GetCurrentProcessId(), (int)GetCurrentProcessId(),
@@ -273,8 +335,25 @@ int cst_parliament_daemon_start(const char *password, const char *instance_id,
int peer_port, char *err, size_t err_sz) { int peer_port, char *err, size_t err_sz) {
if (err && err_sz) if (err && err_sz)
err[0] = 0; err[0] = 0;
if (cst_parliament_daemon_running()) if (!instance_id || !instance_id[0]) {
return 0; if (err && err_sz)
snprintf(err, err_sz, "instance id required");
return -1;
}
int want_port = peer_port > 0 ? peer_port : 9100;
{
int cur_pid = 0, cur_port = 0;
char cur_id[64] = {0};
if (cst_parliament_daemon_info(&cur_pid, cur_id, sizeof(cur_id),
&cur_port) == 0) {
int same = (!cur_id[0] || strcmp(cur_id, instance_id) == 0) &&
(cur_port <= 0 || cur_port == want_port);
if (same)
return 0;
/* Different seat or port: take over. */
(void)cst_parliament_daemon_stop();
}
}
#ifdef _WIN32 #ifdef _WIN32
/* Windows: run in-process foreground loop under a detached console is /* Windows: run in-process foreground loop under a detached console is
* deferred; callers should use --service in a service manager. */ * deferred; callers should use --service in a service manager. */
@@ -294,9 +373,13 @@ int cst_parliament_daemon_start(const char *password, const char *instance_id,
return -1; return -1;
} }
if (pid > 0) { if (pid > 0) {
/* Parent: wait briefly for pidfile. */ /* Parent: wait briefly for matching pidfile. */
for (int i = 0; i < 50; i++) { for (int i = 0; i < 50; i++) {
if (cst_parliament_daemon_running()) int cur_pid = 0, cur_port = 0;
char cur_id[64] = {0};
if (cst_parliament_daemon_info(&cur_pid, cur_id, sizeof(cur_id),
&cur_port) == 0 &&
(!cur_id[0] || strcmp(cur_id, instance_id) == 0))
return 0; return 0;
sleep_ms(50); sleep_ms(50);
} }
@@ -314,7 +397,7 @@ int cst_parliament_daemon_start(const char *password, const char *instance_id,
close(fd); close(fd);
} }
char derr[256]; char derr[256];
(void)cst_parliament_daemon_run(password, instance_id, peer_port, derr, (void)cst_parliament_daemon_run(password, instance_id, want_port, derr,
sizeof(derr)); sizeof(derr));
_exit(0); _exit(0);
#endif #endif
+80
View File
@@ -12,6 +12,7 @@
#include "cst_models.h" #include "cst_models.h"
#include "cst_parliament.h" #include "cst_parliament.h"
#include "cst_plugins.h" #include "cst_plugins.h"
#include "cst_realm.h"
#include "cst_provider_switch.h" #include "cst_provider_switch.h"
#include "cst_skills.h" #include "cst_skills.h"
#include "cst_subagent.h" #include "cst_subagent.h"
@@ -4624,6 +4625,83 @@ static void cmd_max_steps(tui_t *t, char *arg, int *quit) {
push_system_line(t, "(max steps per turn: %d)", cfg->max_steps); push_system_line(t, "(max steps per turn: %d)", cfg->max_steps);
} }
static void cmd_realm(tui_t *t, char *arg, int *quit) {
(void)quit;
const char *sub = arg && arg[0] ? arg : "status";
if (!strcmp(sub, "status") || !strcmp(sub, "show")) {
char *pst = cst_parliament_status_text();
if (pst && pst[0]) {
push_system_line(t, "Realm / Parliament:");
char *p = pst;
while (*p) {
char *nl = strchr(p, '\n');
if (nl)
*nl = 0;
if (p[0])
push_system_line(t, " %s", p);
if (!nl)
break;
p = nl + 1;
}
} else {
push_system_line(t, "(no realm status)");
}
free(pst);
return;
}
if (!strcmp(sub, "stop")) {
if (cst_parliament_daemon_stop() != 0)
push_system_line(t, "Error: could not stop parliament service");
else
push_system_line(t, "(parliament service stopped)");
return;
}
if (!strncmp(sub, "login", 5)) {
char dir[OS_PATH_MAX];
if (cst_realm_dir(dir, sizeof(dir)) != 0 ||
!cst_realm_is_password_mode(dir)) {
push_system_line(t, "No password Realm yet. Outside TUI run:");
push_system_line(t, " cst-agent --realm-password-init");
return;
}
char pw[256];
if (secret_input_run(t, "Realm password",
"Unlocks this device as chair; starts the service",
pw, sizeof(pw)) != 0) {
push_system_line(t, "(login cancelled)");
return;
}
const char *iid = getenv("CST_INSTANCE_ID");
if (!iid || !iid[0])
iid = "chair-local";
char err[256];
char *hand = cst_parliament_login(pw, iid, NULL, err, sizeof(err));
if (!hand) {
memset(pw, 0, sizeof(pw));
push_system_line(t, "Login failed: %s", err[0] ? err : "unknown");
return;
}
/* Show a short summary, not the full handover blob. */
push_system_line(t, "(logged in as chair %s)", iid);
if (strstr(hand, "handover_from=") &&
!strstr(hand, "handover_from=\n") &&
!strstr(hand, "handover_from=\r"))
push_system_line(t, "(took chair from previous device)");
free(hand);
{
char serr[256];
if (cst_parliament_daemon_start(pw, iid, 9100, serr, sizeof(serr)) != 0)
push_system_line(t, "warning: service: %s",
serr[0] ? serr : "start failed");
else if (cst_parliament_daemon_running())
push_system_line(t, "(parliament service running)");
}
memset(pw, 0, sizeof(pw));
return;
}
push_system_line(t, "Usage: /realm [status|login|stop]");
}
static void cmd_status(tui_t *t, char *arg, int *quit) { static void cmd_status(tui_t *t, char *arg, int *quit) {
(void)arg; (void)arg;
(void)quit; (void)quit;
@@ -5605,6 +5683,8 @@ static const slash_cmd_t SLASH[] = {
cmd_mouse}, cmd_mouse},
{"max-steps", "", "[N]", "tool steps allowed per turn", 1, cmd_max_steps}, {"max-steps", "", "[N]", "tool steps allowed per turn", 1, cmd_max_steps},
{"status", "", "", "session, model, context and settings", 1, cmd_status}, {"status", "", "", "session, model, context and settings", 1, cmd_status},
{"realm", "parliament login", "[status|login|stop]",
"Realm / parliament chair status, login, or stop service", 1, cmd_realm},
{"cost", "usage", "", "tokens, cache hit rate and estimated cost", 1, {"cost", "usage", "", "tokens, cache hit rate and estimated cost", 1,
cmd_cost}, cmd_cost},
{"todo", "tasks", "", "show the agent's task list", 1, cmd_todo}, {"todo", "tasks", "", "show the agent's task list", 1, cmd_todo},
+2
View File
@@ -72,8 +72,10 @@ int main(void) {
char *st = cst_parliament_status_text(); char *st = cst_parliament_status_text();
assert(st && strstr(st, "parliament")); assert(st && strstr(st, "parliament"));
assert(strstr(st, "service"));
free(st); free(st);
assert(cst_parliament_is_active()); assert(cst_parliament_is_active());
assert(cst_parliament_daemon_info(NULL, NULL, 0, NULL) != 0);
test_rmrf(dir); test_rmrf(dir);
puts("test_parliament: OK"); puts("test_parliament: OK");