Files
bantam-cpp/session.cpp
T

216 lines
6.9 KiB
C++
Raw Normal View History

2026-10-14 00:01:03 +03:00
/* session.cpp — session persistence, loading, compaction. */
#include "bantam.hpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
2026-10-14 00:01:03 +03:00
#include <ctime>
#include <dirent.h>
#include <memory>
#include <sys/stat.h>
#include <unistd.h>
static const char *compaction_prompt =
"You are now acting as a compaction engine. Summarize the preceding conversation concisely but completely, preserving all important facts, decisions, code snippets, tool outputs, errors, and current task state so work can seamlessly continue. Output only the summary.";
/* ----------------------------- paths ---------------------------------- */
static std::string home_dir() {
const char *h = std::getenv("HOME");
if (h && *h) return h;
return ".";
}
static std::string sessions_dir() {
std::string d = home_dir() + "/.bantam/sessions";
mkdir_p(d);
return d;
}
void save_session(const Messages &msgs, std::string_view sid,
std::string *out_id, std::string *out_summary) {
std::string id = (!sid.empty()) ? std::string(sid) : project_id();
std::string d = sessions_dir();
std::string path = d + "/" + id + ".json";
Json root = Json::object();
2026-10-14 00:01:03 +03:00
root["id"] = id;
time_t now = time(nullptr);
struct tm tm_buf{};
localtime_r(&now, &tm_buf);
char ts[64];
std::strftime(ts, sizeof ts, "%Y-%m-%d %H:%M:%S", &tm_buf);
root["created"] = ts;
std::string sum = summary_of(msgs);
root["summary"] = sum;
Json arr = Json::array();
2026-10-14 00:01:03 +03:00
for (auto &m : msgs.items) {
Json mo;
serialize_message_json(mo, m);
arr.push_back(std::move(mo));
2026-10-14 00:01:03 +03:00
}
root["messages"] = std::move(arr);
write_file_str(path, root.dump(2));
2026-10-14 00:01:03 +03:00
if (out_id) *out_id = id;
if (out_summary) *out_summary = sum;
}
std::vector<Session> sessions_load() {
std::vector<Session> ss;
std::string d = sessions_dir();
std::vector<std::string> files;
{
DIR *dir = opendir(d.c_str());
if (dir) {
struct dirent *de;
while ((de = readdir(dir))) {
std::string nm = de->d_name;
if (nm == "." || nm == "..") continue;
if (nm.size() < 5 || nm.substr(nm.size() - 5) != ".json") continue;
files.push_back(nm);
}
closedir(dir);
}
}
for (size_t i = 1; i < files.size(); i++) {
for (size_t j = i; j > 0 && files[j - 1] > files[j]; j--) {
std::swap(files[j - 1], files[j]);
}
}
2026-10-14 00:01:03 +03:00
for (auto &nm : files) {
std::string full = d + "/" + nm;
std::string buf;
if (!read_file_str(full, buf)) continue;
Json root_mut;
if (!Json::parse(buf, root_mut) || !root_mut.is_object()) continue;
const Json &root = root_mut;
2026-10-14 00:01:03 +03:00
Session s;
s.id = root["id"].str();
s.created = root["created"].str();
s.summary = root["summary"].str();
const Json &marr = root["messages"];
if (marr.is_array()) {
for (auto &mo : marr.aval) {
Message m;
parse_message_json(mo, m);
s.messages.items.push_back(std::move(m));
2026-10-14 00:01:03 +03:00
}
}
if (s.summary.empty()) s.summary = "(no summary)";
ss.push_back(std::move(s));
}
/* sort by id descending (newest first) */
for (size_t i = 1; i < ss.size(); i++) {
for (size_t j = i; j > 0 && ss[j - 1].id < ss[j].id; j--) {
std::swap(ss[j - 1], ss[j]);
}
}
2026-10-14 00:01:03 +03:00
return ss;
}
std::unique_ptr<Messages> load_session(std::string_view sid, std::string *err) {
auto ss = sessions_load();
for (auto &s : ss) {
if (s.id == sid) {
auto res = std::make_unique<Messages>();
for (auto &m : s.messages.items) res->items.push_back(m);
return res;
}
}
/* prefix match */
std::vector<Session *> pref;
for (auto &s : ss) if (s.id.rfind(sid, 0) == 0) pref.push_back(&s);
if (pref.size() == 1) {
auto res = std::make_unique<Messages>();
for (auto &m : pref[0]->messages.items) res->items.push_back(m);
return res;
}
if (pref.size() > 1) {
std::string names;
for (auto *p : pref) str_appendf(names, "%s, ", p->id.c_str());
if (names.size() > 2) names.resize(names.size() - 2);
if (err) *err = sfmt("ambiguous prefix: %s", names.c_str());
} else {
if (err) *err = sfmt("session not found: %s", std::string(sid).c_str());
}
return nullptr;
}
std::unique_ptr<Session> find_project_session() {
std::string pid = project_id();
auto ss = sessions_load();
for (auto &s : ss) {
if (s.id == pid) {
auto found = std::make_unique<Session>();
*found = std::move(s);
return found;
}
}
return nullptr;
}
void autosave(const Messages &msgs) {
std::string id, sum;
save_session(msgs, project_id(), &id, &sum);
}
/* ----------------------------- summary / compact ---------------------- */
std::string summary_of(const Messages &msgs) {
for (auto &m : msgs.items) {
if (m.role == "user" && !m.content.empty()) {
std::string s;
const char *p = m.content.c_str();
bool prev_space = false;
while (*p) {
if (*p == '\n' || *p == '\t' || *p == '\r') { if (!prev_space) s += ' '; prev_space = true; }
else { s += *p; prev_space = false; }
p++;
}
if (s.size() > 80) {
size_t len = utf8_truncate(s, 80).size();
s.resize(len);
s += "...";
}
return s;
}
}
return "(empty session)";
}
std::unique_ptr<Messages> compact(Cfg &cfg, const Messages &msgs, std::string *summary,
std::string *err, volatile int *interrupted) {
if (msgs.items.empty() || msgs.items[0].role != "system") {
if (err) *err = "session has no system message";
return nullptr;
}
if (msgs.items.size() <= 1) { if (err) *err = "nothing to compact"; return nullptr; }
Messages cMsgs;
for (auto &m : msgs.items) cMsgs.items.push_back(m);
Message userp;
userp.role = "user";
userp.content = compaction_prompt;
cMsgs.items.push_back(std::move(userp));
Cfg cc = cfg; cc.stream = false; cc.force_stream = false; /* force non-streaming for clean summary */
Message m; Usage u;
std::string lerr;
int rc = llm(cc, cMsgs, false, m, u, lerr, interrupted);
if (rc != 0) { if (err) *err = lerr.empty() ? "compaction LLM call failed" : lerr; return nullptr; }
std::string s;
if (!m.content.empty()) s = m.content;
else if (!m.reasoning.empty()) s = m.reasoning;
else { if (err) *err = "LLM returned an empty summary"; return nullptr; }
size_t a = 0;
while (a < s.size() && (s[a] == ' ' || s[a] == '\n' || s[a] == '\r' || s[a] == '\t')) a++;
std::string trim = s.substr(a);
while (!trim.empty() && (trim.back() == ' ' || trim.back() == '\n' || trim.back() == '\r' || trim.back() == '\t'))
trim.pop_back();
std::string final = trim;
auto newMsgs = std::make_unique<Messages>();
Message sys; sys.role = "system";
sys.content = msgs.items[0].content;
newMsgs->items.push_back(std::move(sys));
Message sm; sm.role = "user";
sm.content = sfmt("Summary of the previous conversation:\n%s\n\nPlease continue from here.", final.c_str());
newMsgs->items.push_back(std::move(sm));
if (summary) *summary = newMsgs->items[1].content;
return newMsgs;
}