/* session.cpp — session persistence, loading, compaction. */ #include "bantam.hpp" #include #include #include #include #include #include #include #include #include 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; } static bool load_session_file(const std::string &full, Session &s) { std::string buf; if (!read_file_str(full, buf)) return false; Json root_mut; if (!Json::parse(buf, root_mut) || !root_mut.is_object()) return false; const Json &root = root_mut; s = Session{}; s.id = root["id"].str(); s.created = root["created"].str(); s.summary = root["summary"].str(); const Json &marr = root["messages"]; if (marr.is_array()) { s.messages.items.reserve(marr.aval.size()); for (auto &mo : marr.aval) { Message m; parse_message_json(mo, m); s.messages.items.push_back(std::move(m)); } } if (s.summary.empty()) s.summary = "(no summary)"; return true; } 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(); 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(); arr.aval.reserve(msgs.items.size()); for (auto &m : msgs.items) { Json mo; serialize_message_json(mo, m); arr.push_back(std::move(mo)); } root["messages"] = std::move(arr); write_file_str(path, root.dump(2)); if (out_id) *out_id = id; if (out_summary) *out_summary = sum; } std::vector sessions_load() { std::vector ss; std::string d = sessions_dir(); std::vector 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(std::move(nm)); } closedir(dir); } } std::sort(files.begin(), files.end()); ss.reserve(files.size()); for (auto &nm : files) { std::string full = d + "/" + nm; Session s; if (load_session_file(full, s)) { ss.push_back(std::move(s)); } } /* sort by id descending (newest first) */ std::sort(ss.begin(), ss.end(), [](const Session &a, const Session &b) { return a.id > b.id; }); return ss; } std::unique_ptr load_session(std::string_view sid, std::string *err) { if (!sid.empty() && sid.find('/') == std::string_view::npos) { std::string exact = sessions_dir() + "/" + std::string(sid) + ".json"; Session s; if (load_session_file(exact, s) && s.id == sid) { return std::make_unique(std::move(s.messages)); } } auto ss = sessions_load(); for (auto &s : ss) { if (s.id == sid) { return std::make_unique(std::move(s.messages)); } } /* prefix match */ std::vector pref; for (auto &s : ss) if (s.id.starts_with(sid)) pref.push_back(&s); if (pref.size() == 1) { return std::make_unique(std::move(pref[0]->messages)); } 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 find_project_session() { std::string pid = project_id(); std::string path = sessions_dir() + "/" + pid + ".json"; auto found = std::make_unique(); if (load_session_file(path, *found) && found->id == pid) { 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 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(); 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; }