#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include "cJSON.h" #include "cst_agent.h" #include "os_compat.h" #include #include #include #include #include #include #ifdef _WIN32 #include #define OS_ACCESS _access #define OS_F_OK 0 #else #include #define OS_ACCESS access #define OS_F_OK F_OK #endif int cst_agent_store_dir(const char *workspace, char *out, size_t out_sz) { if (!workspace || !out || out_sz < 16) return -1; return os_path_join(out, out_sz, workspace, ".agent/sessions"); } int cst_agent_store_ensure(const char *workspace) { char base[OS_PATH_MAX]; char sess[OS_PATH_MAX]; if (cst_agent_store_dir(workspace, sess, sizeof(sess)) != 0) return -1; if (os_path_join(base, sizeof(base), workspace, ".agent") != 0) return -1; if (os_mkdir(base, 0755) != 0) return -1; if (os_mkdir(sess, 0755) != 0) return -1; /* keep sessions, undo snapshots and exports out of git */ char gi[OS_PATH_MAX]; if (os_path_join(gi, sizeof(gi), base, ".gitignore") == 0 && OS_ACCESS(gi, OS_F_OK) != 0) { FILE *fp = fopen(gi, "wb"); if (fp) { fputs("*\n", fp); fclose(fp); } } return 0; } static void make_id(char *out, size_t n) { time_t now = time(NULL); struct tm tm; os_localtime(&now, &tm); unsigned r = (unsigned)(os_getpid() ^ (unsigned)now); snprintf(out, n, "%04d%02d%02d-%02d%02d%02d-%04x", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, r & 0xffff); } int cst_agent_store_new_id(char *id_out, size_t id_sz) { if (!id_out || id_sz < 24) return -1; make_id(id_out, id_sz); return 0; } int cst_agent_store_path_for_id(const char *workspace, const char *id, char *out, size_t out_sz) { if (!workspace || !id || !out) return -1; /* Accept bare id or full path. */ if (strchr(id, '/') || strchr(id, '\\')) { snprintf(out, out_sz, "%s", id); return 0; } char dir[OS_PATH_MAX]; char leaf[160]; if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0) return -1; snprintf(leaf, sizeof(leaf), "%s.jsonl", id); return os_path_join(out, out_sz, dir, leaf); } int cst_agent_store_set_current(const char *workspace, const char *id) { if (cst_agent_store_ensure(workspace) != 0) return -1; char dir[OS_PATH_MAX]; char path[OS_PATH_MAX]; if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0) return -1; if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0) return -1; FILE *fp = fopen(path, "wb"); if (!fp) return -1; fprintf(fp, "%s\n", id); fclose(fp); return 0; } int cst_agent_store_get_current(const char *workspace, char *id_out, size_t id_sz) { char dir[OS_PATH_MAX]; char path[OS_PATH_MAX]; if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0) return -1; if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0) return -1; FILE *fp = fopen(path, "rb"); if (!fp) return -1; if (!fgets(id_out, (int)id_sz, fp)) { fclose(fp); return -1; } fclose(fp); size_t n = strlen(id_out); while (n > 0 && (id_out[n - 1] == '\n' || id_out[n - 1] == '\r')) id_out[--n] = 0; return id_out[0] ? 0 : -1; } int cst_agent_store_resolve_default(const char *workspace, char *path_out, size_t path_sz, char *id_out, size_t id_sz) { if (cst_agent_store_ensure(workspace) != 0) return -1; char id[128]; if (cst_agent_store_get_current(workspace, id, sizeof(id)) == 0) { char p[OS_PATH_MAX]; if (cst_agent_store_path_for_id(workspace, id, p, sizeof(p)) == 0 && OS_ACCESS(p, OS_F_OK) == 0) { snprintf(path_out, path_sz, "%s", p); if (id_out && id_sz) snprintf(id_out, id_sz, "%s", id); return 0; } } if (cst_agent_store_new_id(id, sizeof(id)) != 0) return -1; if (cst_agent_store_path_for_id(workspace, id, path_out, path_sz) != 0) return -1; cst_agent_store_set_current(workspace, id); if (id_out && id_sz) snprintf(id_out, id_sz, "%s", id); return 0; } int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids) { if (!workspace || !ids || max_ids <= 0) return 0; char dir[OS_PATH_MAX]; if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0) return 0; os_dir_t *d = os_dir_open(dir); if (!d) return 0; typedef struct { char id[128]; time_t mtime; } ent_t; ent_t *ents = calloc((size_t)max_ids * 4, sizeof(ent_t)); int n = 0, cap = max_ids * 4; const os_dirent_t *de; while ((de = os_dir_next(d)) != NULL) { size_t len = strlen(de->name); if (len < 7 || strcmp(de->name + len - 6, ".jsonl") != 0) continue; if (n >= cap) break; snprintf(ents[n].id, sizeof(ents[n].id), "%.*s", (int)(len - 6), de->name); char full[OS_PATH_MAX]; if (os_path_join(full, sizeof(full), dir, de->name) != 0) continue; struct stat st; ents[n].mtime = (stat(full, &st) == 0) ? st.st_mtime : 0; n++; } os_dir_close(d); for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (ents[j].mtime > ents[i].mtime) { ent_t tmp = ents[i]; ents[i] = ents[j]; ents[j] = tmp; } } } int out_n = n < max_ids ? n : max_ids; for (int i = 0; i < out_n; i++) snprintf(ids[i], 128, "%s", ents[i].id); free(ents); return out_n; } static int side_path(const char *workspace, const char *id, const char *ext, char *out, size_t out_sz) { char dir[OS_PATH_MAX], leaf[192]; if (!id || !id[0] || strchr(id, '/') || strchr(id, '\\') || cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0) return -1; snprintf(leaf, sizeof(leaf), "%s%s", id, ext); return os_path_join(out, out_sz, dir, leaf); } int cst_agent_store_title_set(const char *workspace, const char *id, const char *title) { char p[OS_PATH_MAX]; if (side_path(workspace, id, ".title", p, sizeof(p)) != 0) return -1; if (!title || !title[0]) return remove(p) == 0 || errno == ENOENT ? 0 : -1; FILE *fp = fopen(p, "wb"); if (!fp) return -1; for (const char *c = title; *c; c++) fputc(*c == '\n' || *c == '\r' ? ' ' : *c, fp); fputc('\n', fp); return fclose(fp) == 0 ? 0 : -1; } /* Cut to n bytes without splitting a UTF-8 sequence; newlines → spaces. */ static void clip_line(char *dst, size_t n, const char *src) { size_t i = 0; while (src[i] && i + 1 < n) { dst[i] = (src[i] == '\n' || src[i] == '\r' || src[i] == '\t') ? ' ' : src[i]; i++; } while (i > 0 && src[i] && ((unsigned char)src[i] & 0xC0) == 0x80) i--; dst[i] = 0; } int cst_agent_store_info(const char *workspace, const char *id, cst_session_info_t *info) { if (!info) return -1; memset(info, 0, sizeof(*info)); snprintf(info->id, sizeof(info->id), "%s", id ? id : ""); char p[OS_PATH_MAX]; if (side_path(workspace, id, ".jsonl", p, sizeof(p)) != 0) return -1; struct stat st; if (stat(p, &st) != 0) return -1; info->mtime = (long long)st.st_mtime; char tp[OS_PATH_MAX]; if (side_path(workspace, id, ".title", tp, sizeof(tp)) == 0) { FILE *tf = fopen(tp, "rb"); if (tf) { if (fgets(info->title, sizeof(info->title), tf)) { size_t n = strlen(info->title); while (n && (info->title[n - 1] == '\n' || info->title[n - 1] == '\r')) info->title[--n] = 0; } fclose(tf); } } FILE *fp = fopen(p, "rb"); if (!fp) return 0; size_t cap = 1 << 16; char *line = malloc(cap); int have_first = 0; while (line && fgets(line, (int)cap, fp)) { size_t n = strlen(line); if (n == cap - 1 && line[n - 1] != '\n') { /* very long line: skip the rest, it is not the first user line */ int ch; while ((ch = fgetc(fp)) != EOF && ch != '\n') ; info->messages++; continue; } if (n <= 1) continue; info->messages++; if (have_first) continue; cJSON *o = cJSON_Parse(line); const cJSON *r = cJSON_GetObjectItemCaseSensitive(o, "role"); const cJSON *c = cJSON_GetObjectItemCaseSensitive(o, "content"); if (cJSON_IsString(r) && strcmp(r->valuestring, "user") == 0 && cJSON_IsString(c) && c->valuestring[0]) { clip_line(info->first_user, sizeof(info->first_user), c->valuestring); have_first = 1; } cJSON_Delete(o); } free(line); fclose(fp); return 0; } int cst_agent_store_delete(const char *workspace, const char *id) { char p[OS_PATH_MAX]; if (side_path(workspace, id, ".jsonl", p, sizeof(p)) != 0) return -1; if (remove(p) != 0) return -1; if (side_path(workspace, id, ".title", p, sizeof(p)) == 0) remove(p); /* sub-agent transcripts live in / */ if (side_path(workspace, id, "", p, sizeof(p)) == 0) { os_dir_t *d = os_dir_open(p); if (d) { const os_dirent_t *de; while ((de = os_dir_next(d)) != NULL) { char f[OS_PATH_MAX]; if (strncmp(de->name, "sub-", 4) == 0 && os_path_join(f, sizeof(f), p, de->name) == 0) remove(f); } os_dir_close(d); rmdir(p); } } char cur[128]; if (cst_agent_store_get_current(workspace, cur, sizeof(cur)) == 0 && strcmp(cur, id) == 0) { char dir[OS_PATH_MAX]; if (cst_agent_store_dir(workspace, dir, sizeof(dir)) == 0 && os_path_join(p, sizeof(p), dir, "CURRENT") == 0) remove(p); } return 0; }