summaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-07 11:35:49 -0600
committert <t@tjp.lol>2026-06-07 11:35:49 -0600
commitb14859b9726185ab873356390068e887b7f486d3 (patch)
tree6530d8ee62e7639b50c99a67d23f89e25ef572dc /src/main.zig
parentd36a51358efbc48de3d5b732727455efc60acae1 (diff)
R2: redesign session store with wire-format identity
Replace the single-session SessionManager seam with a directory-backed catalog. session_manager.zig -> file_system_jsonl_store.zig; the old machinery becomes internal SessionFile, and a new FileSystemJSONLStore implements the redesigned SessionStore vtable (create/list/resolve/latest/ load/appendMessages) minting Session/SessionInfo handles. Session logs now record wire-format provider identity ({api_style, base_url, model, reasoning}) instead of a single provider string or CLI aliases; no api_key material is ever stored. Disk* content types renamed Stored*; the rich audit-oriented write record is PersistentMessage (in-memory Message + WireIdentity + provenance). Agent.init now takes a Session; persist_provider/persist_model display strings deleted (banner stays alias-based, resume picks default model). Message.metadata round-trips. Dangling-prompt recovery dropped. CLI migrated to the catalog store for run/resume/list. Clean break, no version bump (old logs wiped).
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig102
1 files changed, 40 insertions, 62 deletions
diff --git a/src/main.zig b/src/main.zig
index 46b6320..247817a 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -329,48 +329,51 @@ pub fn main(init: std.process.Init) !void {
};
const banner_provider_initial: []const u8 = model_ref.provider;
+ // The session store: a directory-backed JSONL catalog rooted at the
+ // per-cwd `session_dir` (the CLI owns the cwd→dir grouping; the store
+ // is cwd-agnostic). Must outlive the agent, which appends through a
+ // `Session` handle minted/resolved below.
+ var session_store_impl = try panto.session_manager.FileSystemJSONLStore.init(alloc, io, session_dir, cwd);
+ defer session_store_impl.deinit();
+ const session_store = session_store_impl.store();
+
// Create or resume the session. Resume failures (missing/ambiguous id)
// are user errors — print a tidy message and exit 1 rather than
// printing a Zig stack trace.
- var session_mgr = openSession(
- alloc,
- io,
- session_dir,
- cwd,
+ var session = openSession(
+ session_store,
cli_flags,
+ session_dir,
stdout,
&stdout_file,
) catch |err| switch (err) {
error.SessionNotFound, error.AmbiguousSessionId => std.process.exit(1),
else => return err,
};
- defer session_mgr.deinit();
+ // `session.info` is adopted by the agent below (`Agent.init` can't fail)
+ // and freed in the agent's `deinit`; no separate cleanup here.
- const is_resume = session_mgr.getEntries().len > 0;
+ const is_resume = cli_flags.resume_kind != .none and session.info.message_count > 0;
- // On resume, reconstruct the conversation from the store. The dangling
- // user prompt (a trailing user entry with no assistant reply) is split
- // out and currently ignored (a future TUI will prefill it for editing).
- // On a fresh session, the agent starts with an empty conversation.
+ // On resume, reconstruct the conversation from the store. (The
+ // dangling-prompt recovery feature was dropped in R2.) On a fresh
+ // session, the agent starts with an empty conversation.
var adopted_conversation: ?panto.conversation.Conversation = null;
if (is_resume) {
- var loaded = try session_mgr.loadConversation(alloc);
- // We don't use the dangling prompt yet; free it.
- if (loaded.dangling_user) |d| alloc.free(d);
- loaded.dangling_user = null;
- adopted_conversation = loaded.conversation;
+ adopted_conversation = try session.load();
+ const sid = session.info.id;
try stdout.print(
- "resumed session {s} ({d} entries)\n",
- .{ session_mgr.getSessionId()[0..@min(8, session_mgr.getSessionId().len)], session_mgr.getEntries().len },
+ "resumed session {s} ({d} messages)\n",
+ .{ sid[0..@min(8, sid.len)], session.info.message_count },
);
}
// Assemble the active configuration snapshot and build the agent now,
// before luarocks bootstrap and extension loading. The agent adopts the
- // store (for free persistence) and the resumed conversation (or starts
- // fresh). The agent owns its own tool registry (post-R1); extensions
- // register their tool source onto it *after* the agent is built but
- // before the first turn, so in-place registration is visible.
+ // session handle (for free persistence) and the resumed conversation
+ // (or starts fresh). The agent owns its own tool registry (post-R1);
+ // extensions register their tool source onto it *after* the agent is
+ // built but before the first turn, so in-place registration is visible.
// System-prompt seeding/reconciliation below runs *through* the agent
// so those entries persist.
const active_config: panto.config.Config = .{
@@ -381,12 +384,10 @@ pub fn main(init: std.process.Init) !void {
alloc,
io,
&active_config,
- session_mgr.store(),
+ session,
adopted_conversation,
);
defer agent.deinit();
- agent.persist_provider = banner_provider_initial;
- agent.persist_model = banner_model_initial;
// Spin up the long-lived Lua runtime. All Lua extensions load into
// one `lua_State`; module-global state survives across calls. The
@@ -642,54 +643,31 @@ fn parseAgentFlags(alloc: std.mem.Allocator, args: std.process.Args) !AgentFlags
// Session bootstrap
// -----------------------------------------------------------------------------
+/// Mint or resolve the `Session` to drive, against the catalog `store`.
+/// Fresh sessions are created on demand; resume resolves by id or picks the
+/// most recent. The returned `Session` owns its `info` (freed via the
+/// agent's `deinit`, which adopts it).
fn openSession(
- alloc: std.mem.Allocator,
- io: std.Io,
- session_dir: []const u8,
- cwd: []const u8,
+ store: panto.session_store.SessionStore,
flags: AgentFlags,
+ session_dir: []const u8,
stdout: *std.Io.Writer,
stdout_file: *std.Io.File.Writer,
-) !panto.session_manager.SessionManager {
+) !panto.session_store.Session {
switch (flags.resume_kind) {
- .none => return try panto.session_manager.SessionManager.init(
- alloc,
- io,
- session_dir,
- cwd,
- ),
+ .none => return store.create(),
.most_recent => {
- const path_opt = panto.session_manager.findMostRecentSession(alloc, io, session_dir) catch null;
- if (path_opt) |path| {
- defer alloc.free(path);
- return try panto.session_manager.SessionManager.open(alloc, io, path);
- }
+ if (try store.latest()) |sess| return sess;
try stdout.print("no sessions to resume; starting fresh.\n", .{});
try stdout_file.flush();
- return try panto.session_manager.SessionManager.init(
- alloc,
- io,
- session_dir,
- cwd,
- );
+ return store.create();
},
.by_id => {
const id = flags.resume_id.?;
- const path = panto.session_manager.resolveSessionId(alloc, io, session_dir, id) catch |err| switch (err) {
- error.SessionNotFound => {
- try stdout.print("error: no session matching '{s}' in {s}\n", .{ id, session_dir });
- try stdout_file.flush();
- return err;
- },
- error.AmbiguousSessionId => {
- try stdout.print("error: session id '{s}' is ambiguous\n", .{id});
- try stdout_file.flush();
- return err;
- },
- else => return err,
- };
- defer alloc.free(path);
- return try panto.session_manager.SessionManager.open(alloc, io, path);
+ if (try store.resolve(id)) |sess| return sess;
+ try stdout.print("error: no session matching '{s}' in {s}\n", .{ id, session_dir });
+ try stdout_file.flush();
+ return error.SessionNotFound;
},
}
}