From b14859b9726185ab873356390068e887b7f486d3 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 7 Jun 2026 11:35:49 -0600 Subject: 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). --- src/system_prompt.zig | 113 +++++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 51 deletions(-) (limited to 'src/system_prompt.zig') diff --git a/src/system_prompt.zig b/src/system_prompt.zig index 6104034..d8c06bb 100644 --- a/src/system_prompt.zig +++ b/src/system_prompt.zig @@ -462,29 +462,28 @@ const TmpLayers = struct { } }; -fn openTmpSession(arena: Allocator, root: []const u8) !panto.session_manager.SessionManager { +fn openTmpStore(arena: Allocator, root: []const u8) !panto.session_manager.FileSystemJSONLStore { const sessions = try std.fs.path.join(arena, &.{ root, "sessions" }); - return panto.session_manager.SessionManager.init(testing.allocator, testing.io, sessions, "/cwd"); + return panto.session_manager.FileSystemJSONLStore.init(testing.allocator, testing.io, sessions, "/cwd"); } /// Minimal agent harness for system-prompt tests: a throwaway provider -/// config wrapping a session store and (optionally) an adopted -/// conversation. Post-R1 the agent owns its own (empty) tool registry, so -/// the harness only holds the config to keep the agent's borrowed pointer -/// valid for its lifetime. +/// config plus a session (minted from / resolved against the catalog +/// store). Post-R1 the agent owns its own (empty) tool registry, so the +/// harness only holds the config to keep the agent's borrowed pointer valid. const SPAgentHarness = struct { config: panto.config.Config, agent: panto.agent.Agent, fn init( self: *SPAgentHarness, - store: panto.session_store.SessionStore, + session: panto.session_store.Session, adopted: ?panto.conversation.Conversation, ) void { self.config = .{ .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } }, }; - self.agent = panto.agent.Agent.init(testing.allocator, testing.io, &self.config, store, adopted); + self.agent = panto.agent.Agent.init(testing.allocator, testing.io, &self.config, session, adopted); } fn deinit(self: *SPAgentHarness) void { @@ -492,17 +491,19 @@ const SPAgentHarness = struct { } }; -/// Force the session to flush to disk by appending an assistant entry. -/// (System/user entries buffer in memory until the first assistant entry; -/// a realistic resume only sees what a completed turn persisted.) -fn forceFlush(mgr: *panto.session_manager.SessionManager) !void { - const blocks = try mgr.allocator.alloc(panto.session_manager.DiskContentBlock, 1); - blocks[0] = .{ .text = .{ .text = try mgr.allocator.dupe(u8, "ok") } }; - _ = try mgr.appendMessage( - .{ .role = .assistant, .content = blocks }, - "prov", - "model", - ); +/// Force the session to flush to disk by appending an assistant message +/// through the agent's session. (System/user messages buffer in memory +/// until the first assistant message; a realistic resume only sees what a +/// completed turn persisted.) +fn forceFlush(agent: *panto.agent.Agent) !void { + var conv = panto.conversation.Conversation.init(testing.allocator); + defer conv.deinit(); + try conv.addAssistantMessage(&.{}); + const id: panto.session_store.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" }; + var batch = [_]panto.session_store.PersistentMessage{ + .{ .message = conv.messages.items[0], .identity = id }, + }; + try agent.session.append(&batch); } test "seedFresh then no-config-change resume is a no-op" { @@ -518,29 +519,39 @@ test "seedFresh then no-config-change resume is a no-op" { const project_dir = try layers.projectDir(arena); // Fresh session: seed + persist (through the agent). - var mgr = try openTmpSession(arena, layers.root); + var store_impl = try openTmpStore(arena, layers.root); + defer store_impl.deinit(); + const store = store_impl.store(); + var session_id_buf: [64]u8 = undefined; + var session_id_len: usize = 0; { var h: SPAgentHarness = undefined; - h.init(mgr.store(), null); + h.init(store.create(), null); defer h.deinit(); try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent); + try forceFlush(&h.agent); // persist seed (+ assistant) to disk + @memcpy(session_id_buf[0..h.agent.session.info.id.len], h.agent.session.info.id); + session_id_len = h.agent.session.info.id.len; } - try forceFlush(&mgr); // persist seed + append (+ assistant) to disk - const entries_on_disk = mgr.getEntries().len; - try testing.expectEqual(@as(usize, 3), entries_on_disk); // seed + append + assistant - const session_file = try arena.dupe(u8, mgr.getSessionFile()); - mgr.deinit(); - - // Resume: reopen the file, adopt the rebuilt conversation + reconcile - // against unchanged config → no new entries. - var mgr2 = try panto.session_manager.SessionManager.open(alloc, testing.io, session_file); - defer mgr2.deinit(); - const conv2 = try mgr2.rebuildConversation(); + const session_id = session_id_buf[0..session_id_len]; + + // The persisted conversation reflects the seeded system prompt. + var conv_disk = (try store.load(session_id)).?; + const eff_disk = try panto.conversation.effectiveSystemBlocks(arena, conv_disk.messages.items); + const seeded_count = eff_disk.items.len; + conv_disk.deinit(); + try testing.expect(seeded_count > 0); + + // Resume: adopt the rebuilt conversation + reconcile against unchanged + // config → the effective system prompt is unchanged. + var sess2 = (try store.resolve(session_id)).?; + const conv2 = try sess2.load(); var h2: SPAgentHarness = undefined; - h2.init(mgr2.store(), conv2); + h2.init(sess2, conv2); defer h2.deinit(); try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); - try testing.expectEqual(entries_on_disk, mgr2.getEntries().len); + const eff_after = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + try testing.expectEqual(seeded_count, eff_after.items.len); } test "resume after config change appends replace + append sequence" { @@ -554,44 +565,44 @@ test "resume after config change appends replace + append sequence" { try layers.writeProject("SYSTEM.md", "old seed"); const project_dir = try layers.projectDir(arena); - var mgr = try openTmpSession(arena, layers.root); + var store_impl = try openTmpStore(arena, layers.root); + defer store_impl.deinit(); + const store = store_impl.store(); + var session_id_buf: [64]u8 = undefined; + var session_id_len: usize = 0; { var h: SPAgentHarness = undefined; - h.init(mgr.store(), null); + h.init(store.create(), null); defer h.deinit(); try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent); + try forceFlush(&h.agent); // persist old seed (+ assistant) to disk + @memcpy(session_id_buf[0..h.agent.session.info.id.len], h.agent.session.info.id); + session_id_len = h.agent.session.info.id.len; } - try forceFlush(&mgr); // persist old seed (+ assistant) to disk - try testing.expectEqual(@as(usize, 2), mgr.getEntries().len); // seed + assistant - const session_file = try arena.dupe(u8, mgr.getSessionFile()); - mgr.deinit(); + const session_id = session_id_buf[0..session_id_len]; // Change the config: new seed + a new append. try layers.writeProject("SYSTEM.md", "new seed"); try layers.writeProject("APPEND_SYSTEM.md", "new append"); - var mgr2 = try panto.session_manager.SessionManager.open(alloc, testing.io, session_file); - defer mgr2.deinit(); - const conv2 = try mgr2.rebuildConversation(); + var sess2 = (try store.resolve(session_id)).?; + const conv2 = try sess2.load(); var h2: SPAgentHarness = undefined; - h2.init(mgr2.store(), conv2); + h2.init(sess2, conv2); defer h2.deinit(); try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); - // old seed + assistant + replace(new seed) + append(new append) = 4. - try testing.expectEqual(@as(usize, 4), mgr2.getEntries().len); - // The effective prompt now reflects only the new config blocks. const eff = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); try testing.expectEqual(@as(usize, 2), eff.items.len); try testing.expectEqualStrings("new seed", eff.items[0]); try testing.expectEqualStrings("new append", eff.items[1]); - // A second no-op resume must not append again (anchors to the new - // `replace` window, not the stale original seed). - const after_first = mgr2.getEntries().len; + // A second no-op resume must not change the effective prompt (anchors + // to the new `replace` window, not the stale original seed). try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); - try testing.expectEqual(after_first, mgr2.getEntries().len); + const eff2 = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + try testing.expectEqual(@as(usize, 2), eff2.items.len); } test "Resolved.blocks - seed leads appends" { -- cgit v1.3