Files
CST-agent/src/metrics.c
T
WHC 08008a5a4c 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.
2026-10-11 17:39:00 +08:00

81 lines
2.0 KiB
C

#include "cst_metrics.h"
#include "cJSON.h"
#include "cst_agent.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
struct cst_metrics {
FILE *fp;
char path[OS_PATH_MAX];
};
cst_metrics_t *cst_metrics_open(const char *workspace, const char *session_path) {
const char *off = getenv("CST_METRICS");
if (off && strcmp(off, "0") == 0)
return NULL;
cst_metrics_t *m = calloc(1, sizeof(*m));
if (!m)
return NULL;
const char *override = getenv("CST_METRICS_PATH");
if (override && override[0]) {
snprintf(m->path, sizeof(m->path), "%s", override);
} else {
if (!workspace)
workspace = ".";
char dir[OS_PATH_MAX], agent[OS_PATH_MAX];
os_path_join(agent, sizeof(agent), workspace, ".agent");
os_mkdir(agent, 0755);
os_path_join(dir, sizeof(dir), agent, "metrics");
os_mkdir(dir, 0755);
/* 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];
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);
}
m->fp = fopen(m->path, "ab");
if (!m->fp) {
free(m);
return NULL;
}
return m;
}
void cst_metrics_close(cst_metrics_t *m) {
if (!m)
return;
if (m->fp)
fclose(m->fp);
free(m);
}
void cst_metrics_emit(cst_metrics_t *m, void *obj) {
cJSON *o = obj;
if (!o)
return;
if (m && m->fp) {
if (!cJSON_GetObjectItemCaseSensitive(o, "ts"))
cJSON_AddNumberToObject(o, "ts", (double)time(NULL));
char *txt = cJSON_PrintUnformatted(o);
if (txt) {
fputs(txt, m->fp);
fputc('\n', m->fp);
fflush(m->fp);
free(txt);
}
}
cJSON_Delete(o);
}
const char *cst_metrics_path(const cst_metrics_t *m) {
return m ? m->path : NULL;
}