summaryrefslogtreecommitdiff
path: root/libpanto/src/null_store.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 /libpanto/src/null_store.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 'libpanto/src/null_store.zig')
-rw-r--r--libpanto/src/null_store.zig104
1 files changed, 60 insertions, 44 deletions
diff --git a/libpanto/src/null_store.zig b/libpanto/src/null_store.zig
index d2337ee..1f3ab74 100644
--- a/libpanto/src/null_store.zig
+++ b/libpanto/src/null_store.zig
@@ -2,15 +2,10 @@
//! persistence (and the default backing for an `Agent` constructed without
//! a real store).
//!
-//! Every append is dropped (but its messages are freed, honoring the store
-//! ownership contract — appends consume their messages). `loadConversation`
-//! returns an empty conversation with no dangling prompt. `activeModel` is
-//! null and the session id is the empty string.
-//!
-//! Because freeing the dropped messages needs an allocator, `NullStore`
-//! carries one. Construct it with `init(alloc)` and call `.store()` for the
-//! interface handle. The struct is trivially copyable and holds no other
-//! state, so the handle may borrow it for the agent's lifetime.
+//! Every append is dropped. `load` returns null. `list` returns an empty
+//! slice. `create`/`resolve`/`latest` mint/return empty handles. The struct
+//! holds an allocator (needed to satisfy the `SessionInfo` ownership
+//! contract for the empty handles it mints) and is trivially copyable.
const std = @import("std");
const Allocator = std.mem.Allocator;
@@ -19,9 +14,10 @@ const session_store_mod = @import("session_store.zig");
const conversation_mod = @import("conversation.zig");
const SessionStore = session_store_mod.SessionStore;
-const LoadedSession = session_store_mod.LoadedSession;
-const DiskMessage = session_store_mod.DiskMessage;
-const ActiveModel = session_store_mod.ActiveModel;
+const Session = session_store_mod.Session;
+const SessionInfo = session_store_mod.SessionInfo;
+const PersistentMessage = session_store_mod.PersistentMessage;
+const Conversation = conversation_mod.Conversation;
pub const NullStore = struct {
allocator: Allocator,
@@ -30,38 +26,62 @@ pub const NullStore = struct {
return .{ .allocator = allocator };
}
- fn appendMessagesVT(
- ctx: *anyopaque,
- messages: []DiskMessage,
- _: []const ?[]const u8,
- _: []const ?[]const u8,
- ) anyerror!void {
+ fn emptyInfo(self: *NullStore) SessionInfo {
+ const a = self.allocator;
+ return .{
+ .id = a.dupe(u8, "") catch "",
+ .created = a.dupe(u8, "") catch "",
+ .modified = a.dupe(u8, "") catch "",
+ .message_count = 0,
+ .last_user_message = a.dupe(u8, "") catch "",
+ .api_style = .openai_chat,
+ .base_url = a.dupe(u8, "") catch "",
+ .model = a.dupe(u8, "") catch "",
+ .reasoning = .default,
+ };
+ }
+
+ fn createVT(ctx: *anyopaque) Session {
const self: *NullStore = @ptrCast(@alignCast(ctx));
- // Appends consume their messages; we drop them, so free them.
- for (messages) |m| m.deinit(self.allocator);
+ return .{ .info = self.emptyInfo(), .store = self.store() };
}
- fn loadConversationVT(ctx: *anyopaque, alloc: Allocator) anyerror!LoadedSession {
- _ = ctx;
- return .{
- .conversation = conversation_mod.Conversation.init(alloc),
- .dangling_user = null,
- };
+ fn listVT(ctx: *anyopaque) anyerror![]SessionInfo {
+ const self: *NullStore = @ptrCast(@alignCast(ctx));
+ return self.allocator.alloc(SessionInfo, 0);
}
- fn sessionIdVT(_: *anyopaque) []const u8 {
- return "";
+ fn freeSessionInfosVT(ctx: *anyopaque, infos: []SessionInfo) void {
+ const self: *NullStore = @ptrCast(@alignCast(ctx));
+ for (infos) |i| i.deinit(self.allocator);
+ self.allocator.free(infos);
}
- fn activeModelVT(_: *anyopaque) ?ActiveModel {
+ fn resolveVT(_: *anyopaque, _: []const u8) anyerror!?Session {
return null;
}
+ fn latestVT(_: *anyopaque) anyerror!?Session {
+ return null;
+ }
+
+ fn loadVT(_: *anyopaque, _: []const u8) anyerror!?Conversation {
+ return null;
+ }
+
+ fn appendMessagesVT(_: *anyopaque, _: []const u8, _: []PersistentMessage) anyerror!void {
+ // Drop everything. The PersistentMessages borrow in-memory data
+ // owned by the caller (the conversation); nothing to free here.
+ }
+
const vtable: SessionStore.VTable = .{
+ .create = createVT,
+ .list = listVT,
+ .freeSessionInfos = freeSessionInfosVT,
+ .resolve = resolveVT,
+ .latest = latestVT,
+ .load = loadVT,
.appendMessages = appendMessagesVT,
- .loadConversation = loadConversationVT,
- .sessionId = sessionIdVT,
- .activeModel = activeModelVT,
};
/// Wrap this `NullStore` as a `SessionStore`. The handle borrows
@@ -73,20 +93,16 @@ pub const NullStore = struct {
const testing = std.testing;
-test "NullStore: appends are dropped (and freed) and load returns empty" {
+test "NullStore: appends are dropped and load returns null" {
var ns = NullStore.init(testing.allocator);
const s = ns.store();
- var msg_content = try testing.allocator.alloc(session_store_mod.DiskContentBlock, 1);
- msg_content[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hi") } };
- var messages = [_]DiskMessage{.{ .role = .user, .content = msg_content }};
- // No defer-free: the store consumes (frees) the message.
- try s.appendMessages(&messages, &.{null}, &.{null});
+ try s.appendMessages("sid", &.{});
+ try testing.expect((try s.load("sid")) == null);
+ try testing.expect((try s.resolve("sid")) == null);
+ try testing.expect((try s.latest()) == null);
- var loaded = try s.loadConversation(testing.allocator);
- defer loaded.deinit(testing.allocator);
- try testing.expectEqual(@as(usize, 0), loaded.conversation.messages.items.len);
- try testing.expect(loaded.dangling_user == null);
- try testing.expect(s.activeModel() == null);
- try testing.expectEqualStrings("", s.sessionId());
+ const infos = try s.list();
+ defer s.freeSessionInfos(infos);
+ try testing.expectEqual(@as(usize, 0), infos.len);
}