v0.9.5: a session owns a directory, and the store hands out uuids

A session's transcript lived beside its attachments, both named after an id
that was a timestamp. Two things fell out of that: on a workspace two
machines share, a timestamp is not an id (the same second on both is enough
to hand out the same name), and "delete this conversation" had to chase the
jsonl, the title and a directory in a shared attachments/ tree, guessing what
belonged to whom. A phone's app data has nobody to tidy it by hand.

Now one session is one directory:

  <workspace>/.agent/sessions/
    <uuid>/session.jsonl     the transcript
    <uuid>/title             the window title, one line
    <uuid>/attachments/      images and delivered files (img-…, art-…)
    <uuid>/sub-1.jsonl       an agent role's own transcript
    CURRENT                  the id the next run opens

- src/agent_store.c: new_id() makes a uuid v4 (16 bytes from /dev/urandom,
  clock+pid only as a fallback), dir_for_id/flat_for_id/nested_for_id/
  shape_of_id decide which shape an id has, so every reader takes either.
  An id is a directory name, so it is checked as one (no "/", no "..",
  nothing starting with ".", not CURRENT or attachments). path_for_id() keeps
  the shape a session already has and makes the directory for a new id, so
  nothing had to change in the callers. store_list() lists the directories
  that hold a transcript plus the flat files, newest first. delete() is one
  rm_tree of the session's directory, plus the old shared attachments/<id>
  and CURRENT when it pointed there.
- cst_agent_store_migrate() (--migrate-store, or plain cst-agent for a
  session): moves a flat store in — <id>.jsonl to <id>/session.jsonl,
  <id>.title in, the shared attachments/<id>/ to <id>/attachments/. rename
  first, copy when the filesystem says no, and counts what it moved, so a
  second run is a no-op that says "nothing to move". On the real workspace:
  6 sessions moved, the shared directory gone, --list-sessions reads them
  back unchanged, a second run moved 0.
- cst_agent_store_id_of_path(): the id of a session path. This is the bug the
  whole change had been hiding: cst_agent_session_id() took the id from the
  file name, and the new file name is session.jsonl -- every session would
  have called itself "session" (the sidebar, CURRENT, undo, an agent role's
  directory, and the metrics file, which now also goes through the helper).
- src/attach.c: a session that owns its directory keeps its payloads inside
  it (<id>/attachments/); only the flat layout needs the shared directory and
  the id stem. Attachments behave exactly as before otherwise.
- tests/test_store.c: uuid shape and uniqueness, a small buffer refused, a
  new session's path and directory, write/list/info/title read back, the
  payload landing in <id>/attachments/, delete taking the directory with it;
  then an old-layout store: readable before the move, moved byte for byte,
  its payload readable through the new path, listed once, a second migration
  a no-op, deletion covering the migrated payload. Also in the Makefile and
  CMakeLists test lists.
- app/src/ui/UiTest.cpp: the image directory check wanted "/attachments/",
  which the new layout has no trailing slash for.

Desktop: build clean, make check 35/35, --uitest all green, --selftest and
cst-app-selftest OK, and --selftest opens a session of the migrated workspace.

Version 0.9.5.
This commit is contained in:
WHC
2026-10-11 17:39:00 +08:00
parent 7299f39245
commit 08008a5a4c
14 changed files with 781 additions and 93 deletions
+2 -10
View File
@@ -2047,16 +2047,8 @@ void cst_agent_session_id(const cst_agent_t *a, char *out, size_t out_sz) {
out[0] = 0;
if (!a)
return;
const char *p = a->cfg.session_path;
const char *sl = strrchr(p, '/');
const char *base = sl ? sl + 1 : p;
size_t n = strlen(base);
if (n >= out_sz)
n = out_sz - 1;
memcpy(out, base, n);
out[n] = 0;
if (n > 6 && strcmp(out + n - 6, ".jsonl") == 0)
out[n - 6] = 0;
if (cst_agent_store_id_of_path(a->cfg.session_path, out, out_sz) != 0)
out[0] = 0;
}
const char *cst_agent_session_path(const cst_agent_t *a) {
+401 -57
View File
@@ -23,6 +23,29 @@
#define OS_F_OK F_OK
#endif
/* --- the store's shape ----------------------------------------------------
* A session owns a directory: everything the session keeps is inside it, so
* "delete this conversation" is one rmdir and nothing has to be found by
* guesswork (a phone's app data directory has no user to tidy it by hand).
*
* <workspace>/.agent/sessions/
* <uuid>/session.jsonl the transcript
* <uuid>/title the window title, one line
* <uuid>/attachments/ what the messages carry: images, delivered
* files (<id>.png, art-<id>.md, ...)
* <uuid>/sub-1.jsonl a sub-agent's own transcript
* CURRENT the id the next run opens
*
* Sessions written before this layout are flat, with one shared attachment
* directory, and keep working: every reader here takes either shape and
* cst_agent_store_migrate() (--migrate-store) moves them in.
*
* <uuid>.jsonl
* <uuid>.title
* attachments/<uuid>/... (shared by every session)
* <uuid>/sub-1.jsonl (already per session)
*/
int cst_agent_store_dir(const char *workspace, char *out, size_t out_sz) {
if (!workspace || !out || out_sz < 16)
return -1;
@@ -53,24 +76,140 @@ int cst_agent_store_ensure(const char *workspace) {
return 0;
}
static void make_id(char *out, size_t n) {
time_t now = time(NULL);
struct tm tm;
os_localtime(&now, &tm);
static unsigned seq;
unsigned r = (unsigned)(os_getpid() ^ (unsigned)now) + 0x9e37u * seq++;
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);
/* An id is a directory name: anything that could leave the store, and the two
* names the store itself uses, is not one. */
static int id_ok(const char *id) {
if (!id || !id[0] || id[0] == '.')
return 0;
for (const char *p = id; *p; p++) {
char c = *p;
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.'))
return 0;
}
if (strstr(id, ".."))
return 0;
return strcmp(id, "CURRENT") != 0 && strcmp(id, "attachments") != 0;
}
/* 16 random bytes, whatever the platform offers; the clock and the pid are
* the fallback (a collision only costs a reopen, not data). */
static void random_bytes(unsigned char *out, size_t n) {
FILE *f = fopen("/dev/urandom", "rb");
if (f) {
size_t got = fread(out, 1, n, f);
fclose(f);
if (got == n)
return;
}
static unsigned seq;
unsigned long long x = (unsigned long long)time(NULL) ^
((unsigned long long)os_getpid() << 20) ^
((unsigned long long)++seq << 8);
for (size_t i = 0; i < n; i++) {
x = x * 6364136223846793005ULL + 1442695040888963407ULL;
out[i] = (unsigned char)(x >> 33);
}
}
/* A uuid v4: unique across machines and time, which a timestamp is not (two
* phones, one synced workspace, and the ids have to stay apart). */
int cst_agent_store_new_id(char *id_out, size_t id_sz) {
if (!id_out || id_sz < 24)
if (!id_out || id_sz < 37)
return -1;
make_id(id_out, id_sz);
unsigned char b[16];
random_bytes(b, sizeof(b));
b[6] = (unsigned char)((b[6] & 0x0f) | 0x40); /* version 4 */
b[8] = (unsigned char)((b[8] & 0x3f) | 0x80); /* variant 1 */
snprintf(id_out, id_sz,
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-"
"%02x%02x%02x%02x%02x%02x",
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10],
b[11], b[12], b[13], b[14], b[15]);
return 0;
}
/* <sessions>/<id>, the session's own directory (not made here). */
static int dir_for_id(const char *workspace, const char *id, char *out,
size_t out_sz) {
if (!workspace || !id_ok(id))
return -1;
char dir[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
return -1;
return os_path_join(out, out_sz, dir, id);
}
/* <sessions>/<id>.jsonl -- the shape before per-session directories. */
static int flat_for_id(const char *workspace, const char *id, char *out,
size_t out_sz) {
if (!workspace || !id_ok(id))
return -1;
char dir[OS_PATH_MAX], leaf[192];
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);
}
/* The transcript of an id in the per-session layout. */
static int nested_for_id(const char *workspace, const char *id, char *out,
size_t out_sz) {
char d[OS_PATH_MAX];
if (dir_for_id(workspace, id, d, sizeof(d)) != 0)
return -1;
return os_path_join(out, out_sz, d, "session.jsonl");
}
int cst_agent_store_id_of_path(const char *session_path, char *out,
size_t out_sz) {
if (!session_path || !session_path[0] || !out || out_sz < 2)
return -1;
const char *base = session_path;
for (const char *p = session_path; *p; p++)
if (*p == '/' || *p == '\\')
base = p + 1;
size_t n = strlen(base);
if (n > 6 && strcmp(base + n - 6, ".jsonl") == 0) {
size_t stem = n - 6;
if (stem == 7 && strncmp(base, "session", 7) == 0) {
/* <id>/session.jsonl: the id is the directory the file sits in. */
if (base == session_path)
return -1; /* a bare "session.jsonl" has no session to name */
const char *end = base - 1; /* the separator before the leaf */
const char *start = end;
while (start > session_path && start[-1] != '/' && start[-1] != '\\')
start--;
size_t dn = (size_t)(end - start);
if (!dn || dn >= out_sz)
return -1;
memcpy(out, start, dn);
out[dn] = 0;
return 0;
}
n = stem;
}
if (!n || n >= out_sz)
return -1;
memcpy(out, base, n);
out[n] = 0;
return 0;
}
/* Which shape this id has. 1 = its own directory, 0 = flat, -1 = neither
* (and not a usable id). */
static int shape_of_id(const char *workspace, const char *id) {
char p[OS_PATH_MAX];
os_stat_t st;
if (nested_for_id(workspace, id, p, sizeof(p)) == 0 &&
os_lstat(p, &st) == 0 && !st.is_dir)
return 1;
if (flat_for_id(workspace, id, p, sizeof(p)) == 0 && os_lstat(p, &st) == 0 &&
!st.is_dir)
return 0;
return -1;
}
int cst_agent_store_path_for_id(const char *workspace, const char *id,
char *out, size_t out_sz) {
if (!workspace || !id || !out)
@@ -80,12 +219,19 @@ int cst_agent_store_path_for_id(const char *workspace, const char *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)
int shape = shape_of_id(workspace, id);
if (shape == 0)
return flat_for_id(workspace, id, out, out_sz);
if (nested_for_id(workspace, id, out, out_sz) != 0)
return -1;
snprintf(leaf, sizeof(leaf), "%s.jsonl", id);
return os_path_join(out, out_sz, dir, leaf);
if (shape < 0) {
/* A new session: its directory is made here, so whoever opens the
* transcript next has a place to put it (and its attachments). */
char d[OS_PATH_MAX];
if (dir_for_id(workspace, id, d, sizeof(d)) == 0)
os_mkdir_p(d, 0700);
}
return 0;
}
int cst_agent_store_set_current(const char *workspace, const char *id) {
@@ -164,24 +310,42 @@ int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids) {
return 0;
typedef struct {
char id[128];
time_t mtime;
double 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) {
if (n >= cap)
break;
if (de->is_dir) {
/* a per-session directory: only one that holds a transcript counts */
if (!id_ok(de->name))
continue;
char p[OS_PATH_MAX];
if (nested_for_id(workspace, de->name, p, sizeof(p)) != 0)
continue;
os_stat_t st;
if (os_lstat(p, &st) != 0 || st.is_dir)
continue;
snprintf(ents[n].id, sizeof(ents[n].id), "%s", de->name);
ents[n].mtime = st.mtime;
n++;
continue;
}
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 id[128];
snprintf(id, sizeof(id), "%.*s", (int)(len - 6), de->name);
if (!id_ok(id))
continue;
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;
os_stat_t st;
snprintf(ents[n].id, sizeof(ents[n].id), "%s", id);
ents[n].mtime = os_lstat(full, &st) == 0 ? st.mtime : 0;
n++;
}
os_dir_close(d);
@@ -201,20 +365,28 @@ int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids) {
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)
/* The title of a session: inside its directory in the new layout, a <id>.title
* beside the transcript in the old one. */
static int title_for_id(const char *workspace, const char *id, char *out,
size_t out_sz) {
if (shape_of_id(workspace, id) == 0) {
char dir[OS_PATH_MAX];
char leaf[192];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
return -1;
snprintf(leaf, sizeof(leaf), "%s.title", id);
return os_path_join(out, out_sz, dir, leaf);
}
char d[OS_PATH_MAX];
if (dir_for_id(workspace, id, d, sizeof(d)) != 0)
return -1;
snprintf(leaf, sizeof(leaf), "%s%s", id, ext);
return os_path_join(out, out_sz, dir, leaf);
return os_path_join(out, out_sz, d, "title");
}
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)
if (title_for_id(workspace, id, p, sizeof(p)) != 0)
return -1;
if (!title || !title[0])
return remove(p) == 0 || errno == ENOENT ? 0 : -1;
@@ -247,14 +419,18 @@ int cst_agent_store_info(const char *workspace, const char *id,
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)
if (shape_of_id(workspace, id) == 0) {
if (flat_for_id(workspace, id, p, sizeof(p)) != 0)
return -1;
} else if (nested_for_id(workspace, id, p, sizeof(p)) != 0) {
return -1;
struct stat st;
if (stat(p, &st) != 0)
}
os_stat_t st;
if (os_lstat(p, &st) != 0)
return -1;
info->mtime = (long long)st.st_mtime;
info->mtime = (long long)st.mtime;
char tp[OS_PATH_MAX];
if (side_path(workspace, id, ".title", tp, sizeof(tp)) == 0) {
if (title_for_id(workspace, id, tp, sizeof(tp)) == 0) {
FILE *tf = fopen(tp, "rb");
if (tf) {
if (fgets(info->title, sizeof(info->title), tf)) {
@@ -304,36 +480,204 @@ int cst_agent_store_info(const char *workspace, const char *id,
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)
/* Remove a tree (a session's directory: its transcript, its attachments and
* its sub-agent files). Links are removed, never followed. */
static int rm_tree(const char *path) {
os_stat_t st;
if (os_lstat(path, &st) != 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 <id>/ */
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);
if (!st.is_dir)
return remove(path) == 0 ? 0 : -1;
os_dir_t *d = os_dir_open(path);
if (d) {
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL) {
char f[OS_PATH_MAX];
if (os_path_join(f, sizeof(f), path, de->name) == 0)
rm_tree(f);
}
os_dir_close(d);
}
return rmdir(path) == 0 ? 0 : -1;
}
int cst_agent_store_delete(const char *workspace, const char *id) {
if (!id_ok(id))
return -1;
char p[OS_PATH_MAX];
int found = 0;
/* both shapes, whichever this session has (or had) */
if (flat_for_id(workspace, id, p, sizeof(p)) == 0 && remove(p) == 0)
found = 1;
if (nested_for_id(workspace, id, p, sizeof(p)) == 0 && remove(p) == 0)
found = 1;
if (!found)
return -1;
if (title_for_id(workspace, id, p, sizeof(p)) == 0)
remove(p);
/* the session's own directory: attachments, sub-agent transcripts */
if (dir_for_id(workspace, id, p, sizeof(p)) == 0)
rm_tree(p);
/* and the shared attachment directory of the old layout */
char dir[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) == 0 &&
os_path_join(p, sizeof(p), dir, "attachments") == 0) {
char one[OS_PATH_MAX];
if (os_path_join(one, sizeof(one), p, id) == 0)
rm_tree(one);
}
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;
}
/* Move a directory: rename if the OS likes it, otherwise copy file by file
* (a synced workspace can be odd about renames). */
static int move_file(const char *from, const char *to) {
if (os_rename_replace(from, to) == 0)
return 0;
FILE *in = fopen(from, "rb");
if (!in)
return -1;
FILE *out = fopen(to, "wb");
if (!out) {
fclose(in);
return -1;
}
char buf[16384];
size_t n;
int rc = 0;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
if (fwrite(buf, 1, n, out) != n) {
rc = -1;
break;
}
}
fclose(in);
if (fclose(out) != 0)
rc = -1;
if (rc == 0)
remove(from);
return rc;
}
static int move_tree(const char *from, const char *to) {
os_stat_t st;
if (os_lstat(from, &st) != 0)
return -1;
if (!st.is_dir)
return move_file(from, to);
if (os_mkdir_p(to, 0700) != 0)
return -1;
os_dir_t *d = os_dir_open(from);
if (!d)
return -1;
int rc = 0;
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL) {
char f[OS_PATH_MAX], t[OS_PATH_MAX];
if (os_path_join(f, sizeof(f), from, de->name) != 0 ||
os_path_join(t, sizeof(t), to, de->name) != 0 || move_tree(f, t) != 0)
rc = -1;
}
os_dir_close(d);
if (rc == 0)
rmdir(from);
return rc;
}
int cst_agent_store_migrate(const char *workspace, int *moved_out,
int *failed_out, char *err, size_t err_sz) {
char dummy[8];
if (!err || !err_sz) {
err = dummy;
err_sz = sizeof(dummy);
}
err[0] = 0;
if (moved_out)
*moved_out = 0;
if (failed_out)
*failed_out = 0;
if (!workspace || !workspace[0]) {
snprintf(err, err_sz, "no workspace");
return -1;
}
if (cst_agent_store_ensure(workspace) != 0) {
snprintf(err, err_sz, "cannot use the store under %s", workspace);
return -1;
}
char dir[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0) {
snprintf(err, err_sz, "no sessions directory");
return -1;
}
os_dir_t *d = os_dir_open(dir);
if (!d) {
snprintf(err, err_sz, "cannot read %s", dir);
return -1;
}
int moved = 0, failed = 0;
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL) {
if (de->is_dir)
continue;
size_t len = strlen(de->name);
if (len < 7 || strcmp(de->name + len - 6, ".jsonl") != 0)
continue;
char id[128];
snprintf(id, sizeof(id), "%.*s", (int)(len - 6), de->name);
if (!id_ok(id))
continue;
char nested_dir[OS_PATH_MAX], nested[OS_PATH_MAX], flat[OS_PATH_MAX];
if (dir_for_id(workspace, id, nested_dir, sizeof(nested_dir)) != 0 ||
nested_for_id(workspace, id, nested, sizeof(nested)) != 0 ||
flat_for_id(workspace, id, flat, sizeof(flat)) != 0) {
failed++;
continue;
}
/* already moved (a second run, or a session that never was flat) */
os_stat_t st;
if (os_lstat(nested, &st) == 0)
continue;
if (os_mkdir_p(nested_dir, 0700) != 0 || move_file(flat, nested) != 0) {
failed++;
continue;
}
/* the title and the attachments that belong to this session */
char sdir[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, sdir, sizeof(sdir)) == 0) {
char t_from[OS_PATH_MAX], t_to[OS_PATH_MAX];
char leaf[192];
snprintf(leaf, sizeof(leaf), "%s.title", id);
if (os_path_join(t_from, sizeof(t_from), sdir, leaf) == 0 &&
os_path_join(t_to, sizeof(t_to), nested_dir, "title") == 0)
move_file(t_from, t_to); /* no title is not a failure */
}
char att_dir[OS_PATH_MAX], att_one[OS_PATH_MAX];
char a_from[OS_PATH_MAX], a_to[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, att_dir, sizeof(att_dir)) == 0 &&
os_path_join(att_one, sizeof(att_one), att_dir, "attachments") == 0 &&
os_path_join(a_from, sizeof(a_from), att_one, id) == 0 &&
os_path_join(a_to, sizeof(a_to), nested_dir, "attachments") == 0) {
os_stat_t ast;
if (os_lstat(a_from, &ast) == 0 && ast.is_dir)
move_tree(a_from, a_to);
}
moved++;
}
os_dir_close(d);
/* the shared directory itself is empty now (and has no other purpose) */
char att[OS_PATH_MAX];
if (os_path_join(att, sizeof(att), dir, "attachments") == 0)
rmdir(att); /* fails while another session still uses it: fine */
if (moved_out)
*moved_out = moved;
if (failed_out)
*failed_out = failed;
return 0;
}
+13 -3
View File
@@ -260,8 +260,13 @@ int cst_attach_dimensions(const void *data, size_t n, int *w, int *h) {
/* --- storage ------------------------------------------------------------- */
/* <session dir>/attachments/<session stem> — beside the JSONL so moving the
* workspace keeps the two together. */
/* Where a session's payloads live, beside its JSONL so moving the workspace
* keeps the two together:
* <uuid>/session.jsonl -> <uuid>/attachments/ (the store's shape
* since v0.9.5)
* <id>.jsonl -> <dir>/attachments/<id>/ (older sessions; the
* migration moves them)
* The directory is made here: every store() writes into it next. */
int cst_attach_dir(const char *session_path, char *out, size_t out_sz) {
if (!session_path || !session_path[0])
return -1;
@@ -289,8 +294,13 @@ int cst_attach_dir(const char *session_path, char *out, size_t out_sz) {
char *dot = strstr(leaf, ".jsonl");
if (dot)
*dot = 0;
/* A session that owns its directory keeps its payloads directly inside it;
* only the flat layout needs the shared directory and its stem. */
char rel[320];
snprintf(rel, sizeof(rel), "attachments/%s", leaf[0] ? leaf : "session");
if (strcmp(leaf, "session") == 0)
snprintf(rel, sizeof(rel), "attachments");
else
snprintf(rel, sizeof(rel), "attachments/%s", leaf[0] ? leaf : "session");
char full[OS_PATH_MAX];
if (os_path_join(full, sizeof(full), dir, rel) != 0)
return -1;
+32 -1
View File
@@ -33,6 +33,8 @@ static void usage(const char *argv0) {
" -N, --new start a new session\n"
" -R, --resume ID open a session by id or unique prefix\n"
" -L, --list-sessions list sessions and exit\n"
" --migrate-store move sessions written by an older version\n"
" into one directory per session, and exit\n"
" --list-models [P] what is known about each provider's models\n"
" (built-in, <config>/models.json, GET /models)\n"
" -A, --approval MODE ask | auto (default: edits run, shell in\n"
@@ -175,6 +177,25 @@ static int list_sessions(const char *ws) {
return 0;
}
/* Move a store written by an older version into one directory per session
* (--migrate-store). Idempotent: what is already a directory is left alone. */
static int migrate_store(const char *ws) {
int moved = 0, failed = 0;
char err[256] = "";
if (cst_agent_store_migrate(ws, &moved, &failed, err, sizeof(err)) != 0) {
fprintf(stderr, "cannot migrate %s/.agent/sessions: %s\n", ws,
err[0] ? err : "unknown error");
return 1;
}
printf("migrated %d session(s) into %s/.agent/sessions/<id>/\n", moved, ws);
if (failed)
printf("%d session(s) could not be moved; their files are untouched\n",
failed);
if (moved == 0)
printf("nothing to move (already one directory per session)\n");
return failed ? 1 : 0;
}
/* Exact id or unique prefix → id. */
static int resolve_session_id(const char *ws, const char *want, char *out,
size_t out_sz) {
@@ -338,7 +359,7 @@ int main(int argc, char **argv) {
int force_ask_key = 0;
int session_explicit = 0;
const char *prompt = NULL;
int new_session = 0, do_list = 0;
int new_session = 0, do_list = 0, do_migrate = 0;
const char *resume_id = NULL;
/* WebUI (--web): a second front end on the same engine. */
int web_mode = 0;
@@ -372,6 +393,7 @@ int main(int argc, char **argv) {
{"continue", no_argument, 0, 'c'},
{"resume", required_argument, 0, 'R'},
{"list-sessions", no_argument, 0, 'L'},
{"migrate-store", no_argument, 0, 0x107},
{"yolo", no_argument, 0, 'y'},
{"approval", required_argument, 0, 'A'},
{"confirm", required_argument, 0, 'C'},
@@ -528,6 +550,9 @@ int main(int argc, char **argv) {
web.open_browser = 1;
web_mode = 1;
break;
case 0x107:
do_migrate = 1;
break;
case 'V':
printf("cst-agent %s\n", CST_VERSION);
curl_global_cleanup();
@@ -539,6 +564,12 @@ int main(int argc, char **argv) {
}
}
if (do_migrate) {
int rc = migrate_store(cfg.workspace_root);
curl_global_cleanup();
return rc;
}
if (do_list) {
int rc = list_sessions(cfg.workspace_root);
curl_global_cleanup();
+5 -11
View File
@@ -1,6 +1,7 @@
#include "cst_metrics.h"
#include "cJSON.h"
#include "cst_agent.h"
#include "os_compat.h"
#include <stdio.h>
@@ -31,18 +32,11 @@ cst_metrics_t *cst_metrics_open(const char *workspace, const char *session_path)
os_mkdir(agent, 0755);
os_path_join(dir, sizeof(dir), agent, "metrics");
os_mkdir(dir, 0755);
const char *base = session_path ? session_path : "session.jsonl";
const char *s1 = strrchr(base, '/');
const char *s2 = strrchr(base, '\\');
if (s2 && (!s1 || s2 > s1))
s1 = s2;
if (s1)
base = s1 + 1;
/* One file per session: the id of the transcript's path (<id>.jsonl, or
* <id>/session.jsonl in the store's own layout). */
char leaf[256];
snprintf(leaf, sizeof(leaf), "%s", base);
size_t n = strlen(leaf);
if (n > 6 && strcmp(leaf + n - 6, ".jsonl") == 0)
leaf[n - 6] = 0;
if (cst_agent_store_id_of_path(session_path, leaf, sizeof(leaf)) != 0)
snprintf(leaf, sizeof(leaf), "session");
char file[300];
snprintf(file, sizeof(file), "%s.jsonl", leaf);
os_path_join(m->path, sizeof(m->path), dir, file);