Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253const std = @import("std");const debug_trace = @import("../shared/debug_trace.zig");const mem_utils = @import("../shared/mem_utils.zig");const tool_result_errors = @import("../tooling/tool_result_errors.zig");const types = @import("../shared/types.zig");const session = @import("session.zig");
const Allocator = std.mem.Allocator;const legacy_schema_v1: i64 = 1;const legacy_schema_v2: i64 = 2;
pub const LegacySchemaVersion = enum(u8) { v1 = 1, v2 = 2,};
const TestStoredSession = struct { id: []u8, workspace_root: ?[]u8 = null, created_at_ms: i64, updated_at_ms: i64, conversation_language: session.ConversationLanguage, history: []session.HistoryTurn, total_input_tokens: u64 = 0, total_output_tokens: u64 = 0, total_web_search_requests: u64 = 0,
fn deinit(self: *TestStoredSession, alloc: Allocator) void { if (self.id.len > 0) alloc.free(self.id); if (self.workspace_root) |workspace_root| alloc.free(workspace_root); session.freeHistoryTurnSlice(alloc, self.history); self.* = undefined; }};
/// Renders one session record as JSON; caller owns the returned slice and frees with allocator.free.pub const SessionTokenUsage = struct { input: u64 = 0, output: u64 = 0, web_search_requests: u64 = 0,};
pub noinline fn renderSessionJson( alloc: Allocator, active_id: []const u8, created_at_ms: i64, updated_at_ms: i64, language: session.ConversationLanguage, workspace_root: []const u8, history: []const session.HistoryTurn, token_usage: SessionTokenUsage,) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit();
try out.writer.writeAll("{\"schema_version\":1,\"id\":"); try std.json.Stringify.value(active_id, .{}, &out.writer); try out.writer.print(",\"created_at_ms\":{d},\"updated_at_ms\":{d}", .{ created_at_ms, updated_at_ms }); try out.writer.writeAll(",\"workspace_root\":"); try std.json.Stringify.value(workspace_root, .{}, &out.writer); try out.writer.writeAll(",\"conversation_language\":"); try std.json.Stringify.value(language.view(), .{}, &out.writer); if (token_usage.input > 0 or token_usage.output > 0) { try out.writer.print(",\"total_input_tokens\":{d},\"total_output_tokens\":{d}", .{ token_usage.input, token_usage.output }); } if (token_usage.web_search_requests > 0) { try out.writer.print(",\"total_web_search_requests\":{d}", .{token_usage.web_search_requests}); } try out.writer.print(",\"history_len\":{d},\"history\":[", .{history.len});
for (history, 0..) |turn, i| { if (i > 0) try out.writer.writeByte(','); try writeHistoryTurnJson(&out.writer, turn); }
try out.writer.writeAll("]}"); return try out.toOwnedSlice();}
fn writeHistoryTurnJson(writer: *std.Io.Writer, turn: session.HistoryTurn) !void { switch (turn) { .compacted_summary => |entry| { try writer.writeAll("{\"kind\":\"compacted_summary\",\"summary\":"); try std.json.Stringify.value(entry.summary, .{}, writer); try writer.print(",\"removed_turn_count\":{d},\"compaction_count\":{d}", .{ entry.removed_turn_count, entry.compaction_count }); try writer.writeByte('}'); }, .assistant => |entry| { try writer.writeAll("{\"kind\":\"assistant\",\"user\":"); try writeUserTurnJson(writer, entry.user); try writer.writeAll(",\"assistant\":"); try std.json.Stringify.value(entry.assistant, .{}, writer); if (!entry.execution.isEmpty()) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeByte('}'); }, .interrupted => |entry| { try writer.writeAll("{\"kind\":\"interrupted\",\"user\":"); try writeUserTurnJson(writer, entry.user); try writer.writeAll(",\"assistant\":"); if (entry.assistant) |assistant| { try std.json.Stringify.value(assistant, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_call\":"); if (entry.tool_call) |tool_call| { try writeToolCallJson(writer, tool_call); } else { try writer.writeAll("null"); } try writer.writeAll(",\"completed_tool_names\":"); try writeStringArrayJson(writer, entry.completed_tool_names); try writer.writeAll(",\"terminal_reason\":"); try std.json.Stringify.value(@tagName(entry.terminal_reason), .{}, writer); if (entry.cancellation_origin == .compaction) { try writer.writeAll(",\"cancellation_origin\":\"compaction\""); } if (!entry.execution.isEmpty()) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeByte('}'); }, }}
fn writeToolCallJson(writer: *std.Io.Writer, tool_call: session.ToolCall) !void { try writer.writeAll("{\"id\":"); try std.json.Stringify.value(tool_call.id, .{}, writer); try writer.writeAll(",\"name\":"); try std.json.Stringify.value(tool_call.name, .{}, writer); try writer.writeAll(",\"arguments_json\":"); try std.json.Stringify.value(tool_call.arguments_json, .{}, writer); try writer.writeAll(",\"provider_result\":"); if (tool_call.provider_result) |provider_result| { try std.json.Stringify.value(provider_result, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeByte('}');}
pub fn writeExecutionMemoryJson(writer: *std.Io.Writer, execution: session.ExecutionMemory) !void { try writer.writeAll("{\"schema_version\":3,\"tool_steps\":["); for (execution.tool_steps, 0..) |step, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"assistant\":"); if (step.assistant) |assistant| { try std.json.Stringify.value(assistant, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_calls\":["); for (step.tool_calls, 0..) |tool_call, call_index| { if (call_index > 0) try writer.writeByte(','); try writeToolCallJson(writer, tool_call); } try writer.writeAll("],\"tool_results\":["); for (step.tool_results, 0..) |result, result_index| { if (result_index > 0) try writer.writeByte(','); try writePersistedToolResultJson(writer, result); } try writer.writeAll("]}"); } try writer.writeAll("],\"files\":["); for (execution.files, 0..) |file, i| { if (i > 0) try writer.writeByte(','); try writeFileEvidenceJson(writer, file); } try writer.writeAll("],\"steering\":["); for (execution.steering, 0..) |steering, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"text\":"); try std.json.Stringify.value(steering.text, .{}, writer); try writer.writeAll(",\"assistant_prefix\":"); if (steering.assistant_prefix) |prefix| { try std.json.Stringify.value(prefix, .{}, writer); } else { try writer.writeAll("null"); } try writer.print(",\"after_tool_step_count\":{d}}}", .{steering.after_tool_step_count}); } try writer.writeAll("]}");}
fn writePersistedToolResultJson(writer: *std.Io.Writer, result: session.PersistedToolResult) !void { try writer.writeAll("{\"tool_call_id\":"); try std.json.Stringify.value(result.tool_call_id, .{}, writer); try writer.writeAll(",\"tool_name\":"); try std.json.Stringify.value(result.tool_name, .{}, writer); try writer.writeAll(",\"status\":"); try std.json.Stringify.value(@tagName(result.status), .{}, writer); try writer.writeAll(",\"output\":"); try std.json.Stringify.value(result.output, .{}, writer); if (result.output_handle) |handle| { try writer.writeAll(",\"output_handle\":"); try std.json.Stringify.value(handle, .{}, writer); } if (result.preview) |preview| { try writer.writeAll(",\"preview\":"); try std.json.Stringify.value(preview, .{}, writer); } try writer.print(",\"output_bytes\":{d},\"stored_output_bytes\":{d},\"truncated\":{s},\"provider_native\":{s},\"created_at_ms\":{d}", .{ result.output_bytes, result.stored_output_bytes, if (result.truncated) "true" else "false", if (result.provider_native) "true" else "false", result.created_at_ms, }); try writer.writeAll(",\"permission_feedback\":["); for (result.permission_feedback, 0..) |feedback, i| { if (i > 0) try writer.writeByte(','); try std.json.Stringify.value(feedback, .{}, writer); } try writer.writeByte(']'); if (result.committed_file_presentation) |presentation| { try writer.writeAll(",\"committed_file_presentation\":"); try writeCommittedFilePresentationJson(writer, presentation); } try writer.writeByte('}');}
fn writeCommittedFilePresentationJson( writer: *std.Io.Writer, presentation: types.CommittedFilePresentation,) !void { try writer.writeAll("{\"path\":"); try std.json.Stringify.value(presentation.path, .{}, writer); try writer.writeAll(",\"kind\":"); try std.json.Stringify.value(@tagName(presentation.kind), .{}, writer); try writer.writeAll(",\"lines\":["); for (presentation.lines, 0..) |line, index| { if (index > 0) try writer.writeByte(','); try writer.writeAll("{\"kind\":"); try std.json.Stringify.value(@tagName(line.kind), .{}, writer); try writer.writeAll(",\"old_line\":"); try writeOptionalU32Json(writer, line.old_line); try writer.writeAll(",\"new_line\":"); try writeOptionalU32Json(writer, line.new_line); try writer.writeAll(",\"text\":"); try std.json.Stringify.value(line.text, .{}, writer); try writer.writeByte('}'); } try writer.print("],\"additions\":{d},\"deletions\":{d},\"truncated\":{s},\"previous_content\":", .{ presentation.additions, presentation.deletions, if (presentation.truncated) "true" else "false", }); try writeOptionalStringJson(writer, presentation.previous_content); try writer.writeAll(",\"after_content\":"); try writeOptionalStringJson(writer, presentation.after_content); try writer.writeAll(",\"lifecycle_id\":"); if (presentation.lifecycle_id) |id| { try writer.print("{{\"turn_id\":{d},\"call_id\":", .{id.turn_id}); try std.json.Stringify.value(id.call_id, .{}, writer); try writer.writeByte('}'); } else { try writer.writeAll("null"); } try writer.writeByte('}');}
fn writeOptionalU32Json(writer: *std.Io.Writer, value: ?u32) !void { if (value) |number| { try writer.print("{d}", .{number}); } else { try writer.writeAll("null"); }}
fn writeOptionalStringJson(writer: *std.Io.Writer, value: ?[]const u8) !void { if (value) |text| { try std.json.Stringify.value(text, .{}, writer); } else { try writer.writeAll("null"); }}
fn writeFileEvidenceJson(writer: *std.Io.Writer, file: session.FileEvidence) !void { try writer.writeAll("{\"path\":"); try std.json.Stringify.value(file.path, .{}, writer); try writer.writeAll(",\"new_path\":"); if (file.new_path) |new_path| { try std.json.Stringify.value(new_path, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_call_id\":"); try std.json.Stringify.value(file.tool_call_id, .{}, writer); try writer.writeAll(",\"tool_name\":"); try std.json.Stringify.value(file.tool_name, .{}, writer); try writer.writeAll(",\"action\":"); try std.json.Stringify.value(@tagName(file.action), .{}, writer); try writer.writeAll(",\"status\":"); try std.json.Stringify.value(@tagName(file.status), .{}, writer); try writer.print(",\"model_view_covers_full_file\":{s},\"stale\":{s}", .{ if (file.model_view_covers_full_file) "true" else "false", if (file.stale) "true" else "false", }); try writer.writeByte('}');}
fn writeUserTurnJson(writer: *std.Io.Writer, user: session.UserTurn) !void { try writer.writeAll("{\"text\":"); try std.json.Stringify.value(user.text, .{}, writer); try writer.writeAll(",\"images\":["); for (user.images, 0..) |image, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"path\":"); try std.json.Stringify.value(image.path, .{}, writer); try writer.writeAll(",\"media_type\":"); try std.json.Stringify.value(image.media_type, .{}, writer); try writer.writeAll(",\"snapshot_path\":"); try writeImageSnapshotLocatorJson(writer, image.snapshot_path); try writer.writeAll(",\"snapshot_sha256\":"); try std.json.Stringify.value(image.snapshot_sha256, .{}, writer); try writer.writeByte('}'); } try writer.writeAll("]}");}
fn writeImageSnapshotLocatorJson(writer: *std.Io.Writer, value: ?[]const u8) !void { const path = value orelse { try writer.writeAll("null"); return; }; var locator_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; const locator = try session.projectSnapshotLocator(&locator_buffer, path); try std.json.Stringify.value(locator, .{}, writer);}
test "ordinary session JSON presentation omits per-turn work provenance" { const alloc = std.testing.allocator; const history = [_]session.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("canonical prompt"), .work_id = @constCast("work-private"), }, .assistant = @constCast("canonical reply"), } }}; const json = try renderSessionJson( alloc, "presentation", 1, 2, session.ConversationLanguage.literal("en"), "/tmp/workspace", &history, .{}, ); defer alloc.free(json); try std.testing.expect(std.mem.find(u8, json, "work-private") == null); try std.testing.expect(std.mem.find(u8, json, "work_id") == null); try std.testing.expect(std.mem.find(u8, json, "canonical prompt") != null);}
fn writeStringArrayJson(writer: *std.Io.Writer, items: anytype) !void { try writer.writeByte('['); for (items, 0..) |item, i| { if (i > 0) try writer.writeByte(','); try std.json.Stringify.value(item, .{}, writer); } try writer.writeByte(']');}
/// Parses a session summary; caller owns returned fields and frees with SessionSummary.deinit.pub fn parseSessionSummary(comptime SessionSummary: type, alloc: Allocator, json_text: []const u8) !SessionSummary { var source = std.Io.Reader.fixed(json_text); return parseLegacySummaryStreaming(SessionSummary, alloc, &source) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.UnsupportedSessionSchema => return error.UnsupportedSessionSchema, else => return error.InvalidSessionFormat, };}
pub fn parseLegacySummaryStreaming( comptime LegacySummary: type, alloc: Allocator, source: *std.Io.Reader,) !LegacySummary { return parseLegacySummaryStreamingImpl(LegacySummary, alloc, source) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => return error.ReadFailed, error.UnsupportedSessionSchema => return error.UnsupportedSessionSchema, else => return error.InvalidSessionFormat, };}
fn parseLegacySummaryStreamingImpl( comptime LegacySummary: type, alloc: Allocator, source: *std.Io.Reader,) !LegacySummary { var reader = std.json.Reader.init(alloc, source); defer reader.deinit(); try expectToken(try reader.next(), .object_begin);
var schema_version: ?i64 = null; var id: ?[]u8 = null; errdefer if (id) |owned| mem_utils.free(alloc, owned); var workspace_root: ?[]u8 = null; errdefer if (workspace_root) |owned| mem_utils.free(alloc, owned); var workspace_root_seen = false; var created_at_ms: ?i64 = null; var updated_at_ms: ?i64 = null; var conversation_language: ?session.ConversationLanguage = null; var history_len: ?usize = null;
while (true) { const token = try reader.nextAllocMax(alloc, .alloc_if_needed, 64); defer freeToken(alloc, token); if (token == .object_end) break; const key = switch (token) { .string => |value| value, .allocated_string => |value| value, else => return error.InvalidSessionFormat, };
if (std.mem.eql(u8, key, "history")) { if (summaryComplete( schema_version, id, created_at_ms, updated_at_ms, conversation_language, history_len, )) { try requireLegacySchema(schema_version.?); var result: LegacySummary = .{ .id = id.?, .workspace_root = workspace_root, .created_at_ms = created_at_ms.?, .updated_at_ms = updated_at_ms.?, .conversation_language = conversation_language.?, .history_len = history_len.?, }; if (@hasField(LegacySummary, "schema_version")) { @field(result, "schema_version") = try legacySchemaVersion(schema_version.?); } return result; } try reader.skipValue(); continue; } if (std.mem.eql(u8, key, "schema_version")) { if (schema_version != null) return error.InvalidSessionFormat; schema_version = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "id")) { if (id != null) return error.InvalidSessionFormat; id = try readStringOwned(&reader, alloc, 255); } else if (std.mem.eql(u8, key, "workspace_root")) { if (workspace_root_seen) return error.InvalidSessionFormat; workspace_root_seen = true; workspace_root = try readOptionalStringOwned( &reader, alloc, std.Io.Dir.max_path_bytes, ); } else if (std.mem.eql(u8, key, "created_at_ms")) { if (created_at_ms != null) return error.InvalidSessionFormat; created_at_ms = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "updated_at_ms")) { if (updated_at_ms != null) return error.InvalidSessionFormat; updated_at_ms = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "conversation_language")) { if (conversation_language != null) return error.InvalidSessionFormat; const raw = try readStringOwned( &reader, alloc, session.ConversationLanguage.max_len, ); defer alloc.free(raw); conversation_language = try parseConversationLanguage(raw); } else if (std.mem.eql(u8, key, "history_len")) { if (history_len != null) return error.InvalidSessionFormat; const value = try readU64Token(&reader, alloc); if (value > std.math.maxInt(usize)) return error.InvalidSessionFormat; history_len = @intCast(value); } else { try reader.skipValue(); } }
if (!summaryComplete( schema_version, id, created_at_ms, updated_at_ms, conversation_language, history_len, )) return error.InvalidSessionFormat; try requireLegacySchema(schema_version.?); try expectToken(try reader.next(), .end_of_document); var result: LegacySummary = .{ .id = id.?, .workspace_root = workspace_root, .created_at_ms = created_at_ms.?, .updated_at_ms = updated_at_ms.?, .conversation_language = conversation_language.?, .history_len = history_len.?, }; if (@hasField(LegacySummary, "schema_version")) { @field(result, "schema_version") = try legacySchemaVersion(schema_version.?); } return result;}
/// Parses a full stored session; caller owns returned fields and frees with StoredSession.deinit.pub noinline fn parseStoredSession(comptime StoredSession: type, alloc: Allocator, json_text: []const u8) !StoredSession { return parseLegacyExact(StoredSession, alloc, json_text);}
pub fn parseLegacyExact( comptime LegacySession: type, alloc: Allocator, json_text: []const u8,) !LegacySession { var parsed = std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.InvalidSessionFormat, }; defer parsed.deinit();
const root = try requireObject(parsed.value); try requireSchemaVersion(root);
const history_value = root.get("history") orelse return error.InvalidSessionFormat; if (history_value != .array) return error.InvalidSessionFormat;
const history: []session.HistoryTurn = if (history_value.array.items.len > 0) try alloc.alloc(session.HistoryTurn, history_value.array.items.len) else &.{}; errdefer if (history.len > 0) mem_utils.free(alloc, history);
var parsed_count: usize = 0; errdefer { var i: usize = 0; while (i < parsed_count) : (i += 1) session.freeHistoryTurn(alloc, history[i]); } for (history_value.array.items, 0..) |entry, i| { history[i] = try parseLegacyHistoryTurn(alloc, entry); parsed_count += 1; }
if (try requireUsize(root, "history_len") != history.len) return error.InvalidSessionFormat; const id = try alloc.dupe(u8, try requireString(root, "id")); errdefer mem_utils.free(alloc, id); const workspace_root = try optionalStringDup(alloc, root.get("workspace_root")); errdefer if (workspace_root) |wr| mem_utils.free(alloc, wr); const language = try parseConversationLanguage(try requireString(root, "conversation_language"));
const total_input_tokens: u64 = if (root.get("total_input_tokens")) |v| blk: { if (v == .integer and v.integer >= 0) break :blk @intCast(v.integer); break :blk 0; } else 0; const total_output_tokens: u64 = if (root.get("total_output_tokens")) |v| blk: { if (v == .integer and v.integer >= 0) break :blk @intCast(v.integer); break :blk 0; } else 0; const total_web_search_requests: u64 = if (root.get("total_web_search_requests")) |v| blk: { if (v == .integer and v.integer >= 0) break :blk @intCast(v.integer); break :blk 0; } else 0;
return .{ .id = id, .workspace_root = workspace_root, .created_at_ms = try requireI64(root, "created_at_ms"), .updated_at_ms = try requireI64(root, "updated_at_ms"), .conversation_language = language, .history = history, .total_input_tokens = total_input_tokens, .total_output_tokens = total_output_tokens, .total_web_search_requests = total_web_search_requests, };}
pub fn parseLegacySchemaVersion( alloc: Allocator, json_text: []const u8,) !LegacySchemaVersion { var parsed = std.json.parseFromSlice( std.json.Value, alloc, json_text, .{}, ) catch return error.InvalidSessionFormat; defer parsed.deinit(); const root = try requireObject(parsed.value); return legacySchemaVersion(try requireI64(root, "schema_version"));}
fn formatLegacyBackgroundAssistant( alloc: Allocator, assistant: ?[]const u8, log_path: []const u8, url: ?[]const u8,) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); if (assistant) |text| { if (text.len != 0) { try out.writer.writeAll(text); if (!std.mem.endsWith(u8, text, "\n")) try out.writer.writeByte('\n'); } } try out.writer.writeAll( "[Historical command record: fx no longer owns or controls this process", ); if (log_path.len != 0) try out.writer.print("; former log={s}", .{log_path}); if (url) |value| try out.writer.print("; recorded url={s}", .{value}); try out.writer.writeByte(']'); return out.toOwnedSlice();}
fn parseLegacyHistoryTurn(alloc: Allocator, value: std.json.Value) !session.HistoryTurn { const object = try requireObject(value); const kind = try requireString(object, "kind");
if (std.mem.eql(u8, kind, "compacted_summary")) { const root_user_messages: [][]u8 = if (object.get("root_user_messages")) |messages_value| blk: { if (messages_value != .array) return error.InvalidSessionFormat; const messages = try alloc.alloc([]u8, messages_value.array.items.len); errdefer mem_utils.free(alloc, messages); var copied: usize = 0; errdefer for (messages[0..copied]) |message| mem_utils.free(alloc, message); for (messages_value.array.items, 0..) |item, index| { if (item != .string) return error.InvalidSessionFormat; messages[index] = try alloc.dupe(u8, item.string); copied += 1; } break :blk messages; } else &.{}; errdefer session.freeCompletedToolNames(alloc, root_user_messages); const permission_feedback = try parseOptionalStringArray( alloc, object.get("permission_feedback"), ); errdefer types.freePermissionFeedback(alloc, permission_feedback); const removed_turn_count = try requireUsize(object, "removed_turn_count"); return .{ .compacted_summary = .{ .summary = try alloc.dupe(u8, try requireString(object, "summary")), .removed_turn_count = removed_turn_count, .compaction_count = try requireUsize(object, "compaction_count"), .root_user_messages = root_user_messages, .root_user_messages_complete = if (object.get("root_user_messages_complete") != null) try requireBool(object, "root_user_messages_complete") else object.get("root_user_messages") != null or removed_turn_count == 0, .permission_feedback = permission_feedback, .permission_feedback_complete = if (object.get("permission_feedback_complete") != null) try requireBool(object, "permission_feedback_complete") else removed_turn_count == 0, } }; } if (std.mem.eql(u8, kind, "assistant")) { const user = try parseUserTurn(alloc, object.get("user") orelse return error.InvalidSessionFormat); errdefer session.freeUserTurn(alloc, user); const assistant = try alloc.dupe(u8, try requireString(object, "assistant")); errdefer mem_utils.free(alloc, assistant); const execution = try parseOptionalExecutionMemory(alloc, object.get("execution")); errdefer session.freeExecutionMemory(alloc, execution); return .{ .assistant = .{ .user = user, .assistant = assistant, .execution = execution, } }; } if (std.mem.eql(u8, kind, "background_command")) { const user = try parseUserTurn(alloc, object.get("user") orelse return error.InvalidSessionFormat); errdefer session.freeUserTurn(alloc, user); const legacy_assistant = try optionalStringDup(alloc, object.get("assistant")); defer if (legacy_assistant) |text| alloc.free(text); const execution = try parseOptionalExecutionMemory(alloc, object.get("execution")); errdefer session.freeExecutionMemory(alloc, execution); const log_path = try alloc.dupe(u8, try requireString(object, "log_path")); defer alloc.free(log_path); const url = try optionalStringDup(alloc, object.get("url")); defer if (url) |value_copy| alloc.free(value_copy); _ = try requireBool(object, "expect_url"); _ = try parseOptionalBackgroundRecordId( object.get("background_record_id"), ); const assistant = try formatLegacyBackgroundAssistant( alloc, legacy_assistant, log_path, url, ); return .{ .assistant = .{ .user = user, .assistant = assistant, .execution = execution, } }; } if (std.mem.eql(u8, kind, "interrupted")) { const user = try parseUserTurn(alloc, object.get("user") orelse return error.InvalidSessionFormat); errdefer session.freeUserTurn(alloc, user); const assistant = try optionalStringDup(alloc, object.get("assistant")); errdefer if (assistant) |text| mem_utils.free(alloc, text); const tool_call = try parseOptionalToolCall(alloc, object.get("tool_call")); errdefer if (tool_call) |call| session.freeToolCall(alloc, call); const completed_tool_names = try parseOptionalStringArray(alloc, object.get("completed_tool_names")); errdefer session.freeCompletedToolNames(alloc, completed_tool_names); const terminal_reason = try parseLegacyInterruptedTerminalReason(object.get("terminal_reason")); const cancellation_origin: types.CancellationOrigin = if (object.get("cancellation_origin")) |origin| blk: { if (origin != .string) return error.InvalidSessionFormat; break :blk std.meta.stringToEnum(types.CancellationOrigin, origin.string) orelse return error.InvalidSessionFormat; } else .turn; const execution = try parseOptionalExecutionMemory(alloc, object.get("execution")); return .{ .interrupted = .{ .user = user, .assistant = assistant, .tool_call = tool_call, .completed_tool_names = completed_tool_names, .execution = execution, .terminal_reason = terminal_reason, .cancellation_origin = cancellation_origin, } }; }
return error.InvalidSessionFormat;}
fn parseLegacyInterruptedTerminalReason( value: ?std.json.Value,) !session.InterruptedTerminalReason { const raw = value orelse return .cancelled; if (raw != .string) return error.InvalidSessionFormat; if (std.mem.eql(u8, raw.string, "cancelled")) return .cancelled; if (std.mem.eql(u8, raw.string, "failed")) return .failed; return error.InvalidSessionFormat;}
fn parseOptionalToolCall(alloc: Allocator, maybe_value: ?std.json.Value) !?session.ToolCall { const value = maybe_value orelse return null; var call: ?session.ToolCall = switch (value) { .null => null, .object => try parseToolCall(alloc, value), else => return error.InvalidSessionFormat, }; errdefer if (call) |owned| session.freeToolCall(alloc, owned); if (call) |*parsed| { try session.repairPersistedInterruptedToolArguments(alloc, parsed, .legacy); } return call;}
fn parseToolCall(alloc: Allocator, value: std.json.Value) !session.ToolCall { const object = try requireObject(value); const id = try alloc.dupe(u8, try requireString(object, "id")); errdefer mem_utils.free(alloc, id); const name = try alloc.dupe(u8, try requireString(object, "name")); errdefer mem_utils.free(alloc, name); const raw_arguments_json = try requireString(object, "arguments_json"); const argument_integrity = try types.ToolArgumentIntegrity.classifyFunctionInput(alloc, raw_arguments_json); const arguments_json = try alloc.dupe(u8, if (argument_integrity == .malformed_json) "{}" else raw_arguments_json); errdefer mem_utils.free(alloc, arguments_json); const provider_result = try optionalStringDup(alloc, object.get("provider_result")); errdefer if (provider_result) |result| mem_utils.free(alloc, result); return .{ .id = id, .name = name, .arguments_json = arguments_json, .argument_integrity = argument_integrity, .provider_result = provider_result, };}
fn parseOptionalExecutionMemory(alloc: Allocator, maybe_value: ?std.json.Value) !session.ExecutionMemory { const value = maybe_value orelse return .{}; if (value == .null) return .{}; const object = try requireObject(value); const schema_version: i64 = if (object.get("schema_version")) |version| blk: { if (version != .integer or version.integer < 1 or version.integer > 3) { return error.InvalidSessionFormat; } break :blk version.integer; } else 1; const tool_steps = try parseToolExecutionSteps( alloc, object.get("tool_steps"), schema_version, ); errdefer session.freeExecutionMemory(alloc, .{ .tool_steps = tool_steps }); const steering = try parseOptionalSteering( alloc, object.get("steering"), schema_version, tool_steps.len, ); errdefer types.freePersistedSteering(alloc, steering); const files = try parseFileEvidenceSlice(alloc, object.get("files")); return .{ .tool_steps = tool_steps, .files = files, .steering = steering };}
fn parseOptionalSteering( alloc: Allocator, maybe_value: ?std.json.Value, schema_version: i64, tool_step_count: usize,) ![]types.PersistedSteering { const value = maybe_value orelse return &.{}; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{}; const steering = try alloc.alloc(types.PersistedSteering, value.array.items.len); errdefer mem_utils.free(alloc, steering); var parsed_count: usize = 0; errdefer for (steering[0..parsed_count]) |item| { alloc.free(item.text); if (item.assistant_prefix) |prefix| alloc.free(prefix); }; var prior_tool_step_count: usize = 0; for (value.array.items, 0..) |item, index| { const parsed = if (schema_version <= 2) types.PersistedSteering{ .text = try alloc.dupe(u8, if (item == .string) item.string else return error.InvalidSessionFormat), .after_tool_step_count = tool_step_count, } else blk: { const object = try requireObject(item); const after_tool_step_count = try requireUsize(object, "after_tool_step_count"); if (after_tool_step_count > tool_step_count or (index > 0 and after_tool_step_count < prior_tool_step_count)) { return error.InvalidSessionFormat; } const text = try alloc.dupe(u8, try requireString(object, "text")); const assistant_prefix = optionalStringDup( alloc, object.get("assistant_prefix"), ) catch |err| { alloc.free(text); return err; }; break :blk types.PersistedSteering{ .text = text, .assistant_prefix = assistant_prefix, .after_tool_step_count = after_tool_step_count, }; }; steering[index] = parsed; parsed_count += 1; prior_tool_step_count = parsed.after_tool_step_count; } return steering;}
fn parseToolExecutionSteps( alloc: Allocator, maybe_value: ?std.json.Value, schema_version: i64,) ![]session.ToolExecutionStep { const value = maybe_value orelse return &.{}; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{};
const steps = try alloc.alloc(session.ToolExecutionStep, value.array.items.len); errdefer mem_utils.free(alloc, steps);
var parsed_count: usize = 0; errdefer { var i: usize = 0; while (i < parsed_count) : (i += 1) freeParsedToolExecutionStep(alloc, steps[i]); }
for (value.array.items, 0..) |item, i| { const object = try requireObject(item); const assistant = try optionalStringDup(alloc, object.get("assistant")); errdefer if (assistant) |text| mem_utils.free(alloc, text); const tool_calls = try parseToolCallArray(alloc, object.get("tool_calls")); errdefer session.freeToolCallSlice(alloc, tool_calls); const tool_results = try parsePersistedToolResultArray( alloc, object.get("tool_results"), schema_version, ); errdefer session.freePersistedToolResults(alloc, tool_results); try session.repairPersistedToolArguments(alloc, tool_calls, tool_results, .legacy); steps[i] = .{ .assistant = assistant, .tool_calls = tool_calls, .tool_results = tool_results, }; parsed_count += 1; } return steps;}
fn parseToolCallArray(alloc: Allocator, maybe_value: ?std.json.Value) ![]session.ToolCall { const value = maybe_value orelse return &.{}; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{};
const calls = try alloc.alloc(session.ToolCall, value.array.items.len); errdefer mem_utils.free(alloc, calls);
var parsed_count: usize = 0; errdefer { var i: usize = 0; while (i < parsed_count) : (i += 1) session.freeToolCall(alloc, calls[i]); }
for (value.array.items, 0..) |item, i| { calls[i] = try parseToolCall(alloc, item); parsed_count += 1; } return calls;}
fn parsePersistedToolResultArray( alloc: Allocator, maybe_value: ?std.json.Value, schema_version: i64,) ![]session.PersistedToolResult { const value = maybe_value orelse return &.{}; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{};
const results = try alloc.alloc(session.PersistedToolResult, value.array.items.len); errdefer mem_utils.free(alloc, results);
var parsed_count: usize = 0; errdefer { var i: usize = 0; while (i < parsed_count) : (i += 1) freeParsedPersistedToolResult(alloc, results[i]); }
for (value.array.items, 0..) |item, i| { results[i] = try parsePersistedToolResult(alloc, item, schema_version); parsed_count += 1; } return results;}
fn parsePersistedToolResult( alloc: Allocator, value: std.json.Value, schema_version: i64,) !session.PersistedToolResult { const object = try requireObject(value); const tool_call_id = try alloc.dupe(u8, try requireString(object, "tool_call_id")); errdefer mem_utils.free(alloc, tool_call_id); const tool_name = try alloc.dupe(u8, try requireString(object, "tool_name")); errdefer mem_utils.free(alloc, tool_name); const output = try alloc.dupe(u8, try requireString(object, "output")); errdefer mem_utils.free(alloc, output); const output_handle = try optionalStringDup(alloc, object.get("output_handle")); errdefer if (output_handle) |handle| mem_utils.free(alloc, handle); const preview = try optionalStringDup(alloc, object.get("preview")); errdefer if (preview) |text| mem_utils.free(alloc, text); const permission_feedback: [][]u8 = if (schema_version == 1) &.{} else blk: { const feedback_value = object.get("permission_feedback") orelse return error.InvalidSessionFormat; if (feedback_value != .array) return error.InvalidSessionFormat; break :blk try parseOptionalStringArray(alloc, feedback_value); }; errdefer types.freePermissionFeedback(alloc, permission_feedback); const committed_file_presentation = if (object.get("committed_file_presentation")) |presentation_value| try parseCommittedFilePresentation(alloc, presentation_value) else null; errdefer if (committed_file_presentation) |presentation| { types.freeCommittedFilePresentation(alloc, presentation); }; return .{ .tool_call_id = tool_call_id, .tool_name = tool_name, .status = try parsePersistedToolStatus(try requireString(object, "status")), .output = output, .output_handle = output_handle, .preview = preview, .output_bytes = try requireUsize(object, "output_bytes"), .stored_output_bytes = try requireUsize(object, "stored_output_bytes"), .truncated = try requireBool(object, "truncated"), .provider_native = try requireBool(object, "provider_native"), .created_at_ms = try requireI64(object, "created_at_ms"), .permission_feedback = permission_feedback, .committed_file_presentation = committed_file_presentation, };}
fn parseCommittedFilePresentation( alloc: Allocator, value: std.json.Value,) !types.CommittedFilePresentation { const object = try requireObject(value); const path = try alloc.dupe(u8, try requireString(object, "path")); errdefer mem_utils.free(alloc, path); const lines = try parseCommittedFilePresentationLines(alloc, object.get("lines")); errdefer { for (lines) |line| alloc.free(@constCast(line.text)); if (lines.len > 0) alloc.free(@constCast(lines)); } const previous_content = try optionalStringDup(alloc, object.get("previous_content")); errdefer if (previous_content) |content| mem_utils.free(alloc, content); const after_content = try optionalStringDup(alloc, object.get("after_content")); errdefer if (after_content) |content| mem_utils.free(alloc, content); const lifecycle_id = try parseOptionalLifecycleId(alloc, object.get("lifecycle_id")); errdefer if (lifecycle_id) |id| mem_utils.free(alloc, @constCast(id.call_id)); return .{ .path = path, .kind = try parseCommittedFilePresentationKind(try requireString(object, "kind")), .lines = lines, .additions = try requireUsize(object, "additions"), .deletions = try requireUsize(object, "deletions"), .truncated = try requireBool(object, "truncated"), .previous_content = previous_content, .after_content = after_content, .lifecycle_id = lifecycle_id, };}
fn parseCommittedFilePresentationLines( alloc: Allocator, maybe_value: ?std.json.Value,) ![]types.CommittedFilePresentationLine { const value = maybe_value orelse return error.InvalidSessionFormat; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{}; const lines = try alloc.alloc(types.CommittedFilePresentationLine, value.array.items.len); errdefer mem_utils.free(alloc, lines); var parsed_count: usize = 0; errdefer for (lines[0..parsed_count]) |line| mem_utils.free(alloc, @constCast(line.text)); for (value.array.items, 0..) |item, index| { const object = try requireObject(item); lines[index] = .{ .kind = try parseCommittedFilePresentationLineKind(try requireString(object, "kind")), .old_line = try optionalU32(object.get("old_line")), .new_line = try optionalU32(object.get("new_line")), .text = try alloc.dupe(u8, try requireString(object, "text")), }; parsed_count += 1; } return lines;}
fn parseOptionalLifecycleId( alloc: Allocator, maybe_value: ?std.json.Value,) !?types.ToolLifecycleId { const value = maybe_value orelse return null; if (value == .null) return null; const object = try requireObject(value); const call_id = try alloc.dupe(u8, try requireString(object, "call_id")); errdefer mem_utils.free(alloc, call_id); return .{ .turn_id = @intCast(try requireUsize(object, "turn_id")), .call_id = call_id, };}
test "session JSON frees lifecycle call id when turn id is invalid" { const alloc = std.testing.allocator; var parsed = try std.json.parseFromSlice( std.json.Value, alloc, "{\"call_id\":\"call_lifecycle\",\"turn_id\":-1}", .{}, ); defer parsed.deinit();
try std.testing.expectError( error.InvalidSessionFormat, parseOptionalLifecycleId(alloc, parsed.value), );}
fn optionalU32(maybe_value: ?std.json.Value) !?u32 { const value = maybe_value orelse return error.InvalidSessionFormat; if (value == .null) return null; if (value != .integer or value.integer < 0) return error.InvalidSessionFormat; return std.math.cast(u32, value.integer) orelse error.InvalidSessionFormat;}
fn parseCommittedFilePresentationKind( value: []const u8,) !types.CommittedFilePresentationKind { if (std.mem.eql(u8, value, "added")) return .added; if (std.mem.eql(u8, value, "edited")) return .edited; return error.InvalidSessionFormat;}
fn parseCommittedFilePresentationLineKind( value: []const u8,) !types.CommittedFilePresentationLineKind { if (std.mem.eql(u8, value, "context")) return .context; if (std.mem.eql(u8, value, "addition")) return .addition; if (std.mem.eql(u8, value, "deletion")) return .deletion; if (std.mem.eql(u8, value, "elision")) return .elision; if (std.mem.eql(u8, value, "notice")) return .notice; return error.InvalidSessionFormat;}
fn parseFileEvidenceSlice(alloc: Allocator, maybe_value: ?std.json.Value) ![]session.FileEvidence { const value = maybe_value orelse return &.{}; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{};
const files = try alloc.alloc(session.FileEvidence, value.array.items.len); errdefer mem_utils.free(alloc, files);
var parsed_count: usize = 0; errdefer { var i: usize = 0; while (i < parsed_count) : (i += 1) freeParsedFileEvidence(alloc, files[i]); }
for (value.array.items, 0..) |item, i| { files[i] = try parseFileEvidence(alloc, item); parsed_count += 1; } return files;}
fn parseFileEvidence(alloc: Allocator, value: std.json.Value) !session.FileEvidence { const object = try requireObject(value); const path = try alloc.dupe(u8, try requireString(object, "path")); errdefer mem_utils.free(alloc, path); const new_path = try optionalStringDup(alloc, object.get("new_path")); errdefer if (new_path) |path_copy| mem_utils.free(alloc, path_copy); const tool_call_id = try alloc.dupe(u8, try requireString(object, "tool_call_id")); errdefer mem_utils.free(alloc, tool_call_id); const tool_name = try alloc.dupe(u8, try requireString(object, "tool_name")); errdefer mem_utils.free(alloc, tool_name); return .{ .path = path, .new_path = new_path, .tool_call_id = tool_call_id, .tool_name = tool_name, .action = try parseFileEvidenceAction(try requireString(object, "action")), .status = try parsePersistedToolStatus(try requireString(object, "status")), .model_view_covers_full_file = try requireBool(object, "model_view_covers_full_file"), .stale = try requireBool(object, "stale"), };}
fn parsePersistedToolStatus(value: []const u8) !session.PersistedToolStatus { if (std.mem.eql(u8, value, "success")) return .success; if (std.mem.eql(u8, value, "failure")) return .failure; return error.InvalidSessionFormat;}
fn parseFileEvidenceAction(value: []const u8) !session.FileEvidenceAction { if (std.mem.eql(u8, value, "read")) return .read; if (std.mem.eql(u8, value, "write")) return .write; if (std.mem.eql(u8, value, "edit")) return .edit; if (std.mem.eql(u8, value, "delete")) return .delete; if (std.mem.eql(u8, value, "rename")) return .rename; if (std.mem.eql(u8, value, "copy")) return .copy; if (std.mem.eql(u8, value, "search")) return .search; if (std.mem.eql(u8, value, "list")) return .list; if (std.mem.eql(u8, value, "unknown")) return .unknown; return error.InvalidSessionFormat;}
fn freeParsedToolExecutionStep(alloc: Allocator, step: session.ToolExecutionStep) void { if (step.assistant) |assistant| alloc.free(assistant); session.freeToolCallSlice(alloc, step.tool_calls); session.freePersistedToolResults(alloc, step.tool_results);}
fn freeParsedPersistedToolResult(alloc: Allocator, result: session.PersistedToolResult) void { alloc.free(result.tool_call_id); alloc.free(result.tool_name); alloc.free(result.output); if (result.output_handle) |handle| alloc.free(handle); if (result.preview) |preview| alloc.free(preview); types.freePermissionFeedback(alloc, result.permission_feedback); if (result.committed_file_presentation) |presentation| { types.freeCommittedFilePresentation(alloc, presentation); }}
fn freeParsedFileEvidence(alloc: Allocator, file: session.FileEvidence) void { alloc.free(file.path); if (file.new_path) |new_path| alloc.free(new_path); alloc.free(file.tool_call_id); alloc.free(file.tool_name);}
fn parseUserTurn(alloc: Allocator, value: std.json.Value) !session.UserTurn { const object = try requireObject(value); const text = try alloc.dupe(u8, try requireString(object, "text")); errdefer mem_utils.free(alloc, text); const images = try validateImagesArray(alloc, object.get("images")); errdefer session.freeImageAttachmentSlice(alloc, images); return .{ .text = text, .images = images };}
fn parseConversationLanguage(raw: []const u8) !session.ConversationLanguage { return session.ConversationLanguage.fromSlice(raw) catch error.InvalidSessionFormat;}
/// Parses only the workspace_root field; caller owns a returned string and frees with allocator.free.pub fn parseWorkspaceRoot(alloc: Allocator, json_text: []const u8) !?[]u8 { var parsed = std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}) catch return error.InvalidSessionFormat; defer parsed.deinit(); const root = try requireObject(parsed.value); return try optionalStringDup(alloc, root.get("workspace_root"));}
fn validateImagesArray(alloc: Allocator, maybe_value: ?std.json.Value) ![]session.ImageAttachment { const value = maybe_value orelse return &.{}; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{};
const images = try alloc.alloc(session.ImageAttachment, value.array.items.len); errdefer mem_utils.free(alloc, images);
var parsed_count: usize = 0; errdefer { var i: usize = 0; while (i < parsed_count) : (i += 1) { types.freeImageAttachment(alloc, images[i]); } }
for (value.array.items, 0..) |entry, i| { const object = try requireObject(entry); const snapshot_path = try optionalStringDup(alloc, object.get("snapshot_path")); var owns_snapshot_path = true; errdefer if (owns_snapshot_path) { if (snapshot_path) |path| alloc.free(path); }; const snapshot_sha256 = try optionalStringDup(alloc, object.get("snapshot_sha256")); var owns_snapshot_sha256 = true; errdefer if (owns_snapshot_sha256) { if (snapshot_sha256) |sha256| alloc.free(sha256); }; if ((snapshot_path == null) != (snapshot_sha256 == null)) return error.InvalidSessionFormat; images[i] = try dupeParsedImageAttachment( alloc, if (object.get("id") != null) try requireUsize(object, "id") else 0, try requireString(object, "path"), try requireString(object, "media_type"), snapshot_path, snapshot_sha256, ); owns_snapshot_path = false; owns_snapshot_sha256 = false; parsed_count += 1; } return images;}
fn dupeParsedImageAttachment( alloc: Allocator, id: usize, path_src: []const u8, media_type_src: []const u8, snapshot_path_src: ?[]u8, snapshot_sha256_src: ?[]u8,) !session.ImageAttachment { const path = try alloc.dupe(u8, path_src); errdefer mem_utils.free(alloc, path); const media_type = try alloc.dupe(u8, media_type_src); errdefer mem_utils.free(alloc, media_type); return .{ .id = id, .path = path, .media_type = media_type, .snapshot_path = snapshot_path_src, .snapshot_sha256 = snapshot_sha256_src, };}
fn requireSchemaVersion(object: std.json.ObjectMap) !void { try requireLegacySchema(try requireI64(object, "schema_version"));}
fn requireLegacySchema(version: i64) !void { _ = try legacySchemaVersion(version);}
fn legacySchemaVersion(version: i64) !LegacySchemaVersion { return switch (version) { legacy_schema_v1 => .v1, legacy_schema_v2 => .v2, else => error.UnsupportedSessionSchema, };}
fn summaryComplete( schema_version: ?i64, id: ?[]u8, created_at_ms: ?i64, updated_at_ms: ?i64, conversation_language: ?session.ConversationLanguage, history_len: ?usize,) bool { return schema_version != null and id != null and created_at_ms != null and updated_at_ms != null and conversation_language != null and history_len != null;}
fn readStringOwned( reader: *std.json.Reader, alloc: Allocator, max_len: usize,) ![]u8 { const token = try reader.nextAllocMax(alloc, .alloc_always, max_len); return switch (token) { .allocated_string => |value| value, .string => |value| try alloc.dupe(u8, value), else => { freeToken(alloc, token); return error.InvalidSessionFormat; }, };}
fn readOptionalStringOwned( reader: *std.json.Reader, alloc: Allocator, max_len: usize,) !?[]u8 { const token = try reader.nextAllocMax(alloc, .alloc_always, max_len); return switch (token) { .null => null, .allocated_string => |value| value, .string => |value| try alloc.dupe(u8, value), else => { freeToken(alloc, token); return error.InvalidSessionFormat; }, };}
fn readI64Token(reader: *std.json.Reader, alloc: Allocator) !i64 { const token = try reader.nextAllocMax(alloc, .alloc_if_needed, 32); defer freeToken(alloc, token); const raw = switch (token) { .number => |value| value, .allocated_number => |value| value, else => return error.InvalidSessionFormat, }; return std.fmt.parseInt(i64, raw, 10) catch error.InvalidSessionFormat;}
fn readU64Token(reader: *std.json.Reader, alloc: Allocator) !u64 { const token = try reader.nextAllocMax(alloc, .alloc_if_needed, 32); defer freeToken(alloc, token); const raw = switch (token) { .number => |value| value, .allocated_number => |value| value, else => return error.InvalidSessionFormat, }; return std.fmt.parseUnsigned(u64, raw, 10) catch error.InvalidSessionFormat;}
fn expectToken(actual: std.json.Token, comptime expected: std.meta.Tag(std.json.Token)) !void { if (std.meta.activeTag(actual) != expected) return error.InvalidSessionFormat;}
fn freeToken(alloc: Allocator, token: std.json.Token) void { switch (token) { .allocated_number => |value| alloc.free(value), .allocated_string => |value| alloc.free(value), else => {}, }}
fn parseOptionalBackgroundRecordId( maybe_value: ?std.json.Value,) !?[16]u8 { const value = maybe_value orelse return null; if (value == .null) return null; if (value != .string or value.string.len != 32) return error.InvalidSessionFormat; var id: [16]u8 = undefined; _ = std.fmt.hexToBytes(&id, value.string) catch return error.InvalidSessionFormat; const canonical = std.fmt.bytesToHex(id, .lower); if (!std.mem.eql(u8, &canonical, value.string)) return error.InvalidSessionFormat; return id;}
noinline fn requireObject(value: std.json.Value) !std.json.ObjectMap { if (value != .object) return error.InvalidSessionFormat; return value.object;}
noinline fn requireString(object: std.json.ObjectMap, key: []const u8) ![]const u8 { const value = object.get(key) orelse return error.InvalidSessionFormat; if (value != .string) return error.InvalidSessionFormat; return value.string;}
fn requireBool(object: std.json.ObjectMap, key: []const u8) !bool { const value = object.get(key) orelse return error.InvalidSessionFormat; if (value != .bool) return error.InvalidSessionFormat; return value.bool;}
noinline fn requireI64(object: std.json.ObjectMap, key: []const u8) !i64 { const value = object.get(key) orelse return error.InvalidSessionFormat; if (value != .integer) return error.InvalidSessionFormat; if (value.integer < std.math.minInt(i64) or value.integer > std.math.maxInt(i64)) { return error.InvalidSessionFormat; } return @intCast(value.integer);}
noinline fn requireUsize(object: std.json.ObjectMap, key: []const u8) !usize { const value = try requireI64(object, key); if (value < 0) return error.InvalidSessionFormat; return @intCast(value);}
noinline fn optionalStringDup(alloc: Allocator, maybe_value: ?std.json.Value) !?[]u8 { const value = maybe_value orelse return null; return switch (value) { .null => null, .string => |text| try alloc.dupe(u8, text), else => error.InvalidSessionFormat, };}
fn parseOptionalStringArray(alloc: Allocator, maybe_value: ?std.json.Value) ![][]u8 { const value = maybe_value orelse return &.{}; if (value == .null) return &.{}; if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{};
const items = try alloc.alloc([]u8, value.array.items.len); errdefer mem_utils.free(alloc, items);
var parsed_count: usize = 0; errdefer { var i: usize = 0; while (i < parsed_count) : (i += 1) alloc.free(items[i]); }
for (value.array.items, 0..) |entry, i| { if (entry != .string) return error.InvalidSessionFormat; items[i] = try alloc.dupe(u8, entry.string); parsed_count += 1; } return items;}
test "validateImagesArray frees path when media_type allocation fails" { const alloc = std.testing.allocator; var parsed = try std.json.parseFromSlice(std.json.Value, alloc, "{\"images\":[{\"path\":\"/tmp/a.png\",\"media_type\":\"image/png\"}]}", .{}); defer parsed.deinit(); var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 2 }); try std.testing.expectError(error.OutOfMemory, validateImagesArray(failing.allocator(), parsed.value.object.get("images")));}
test "parseSessionSummary rejects integer above i64 range" { const session_store = @import("session_store.zig"); const alloc = std.testing.allocator; const json_text = "{\"schema_version\":1,\"id\":\"overflow\",\"created_at_ms\":9223372036854775808,\"updated_at_ms\":1," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":0,\"history\":[]}";
try std.testing.expectError(error.InvalidSessionFormat, parseSessionSummary(session_store.SessionSummary, alloc, json_text));}
test "parseSessionSummary rejects integer below i64 range" { const session_store = @import("session_store.zig"); const alloc = std.testing.allocator; const json_text = "{\"schema_version\":1,\"id\":\"underflow\",\"created_at_ms\":1,\"updated_at_ms\":-9223372036854775809," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":0,\"history\":[]}";
try std.testing.expectError(error.InvalidSessionFormat, parseSessionSummary(session_store.SessionSummary, alloc, json_text));}
test "parseWorkspaceRoot probes workspace without requiring supported schema" { const alloc = std.testing.allocator; const workspace = try parseWorkspaceRoot(alloc, "{\"schema_version\":99,\"workspace_root\":\"/tmp/workspace\"}") orelse return error.TestExpectedEqual; defer alloc.free(workspace);
try std.testing.expectEqualStrings("/tmp/workspace", workspace); try std.testing.expect(try parseWorkspaceRoot(alloc, "{\"schema_version\":99}") == null); try std.testing.expect(try parseWorkspaceRoot(alloc, "{\"schema_version\":99,\"workspace_root\":null}") == null);}
test "parseWorkspaceRoot rejects non-object and non-string workspace root" { try std.testing.expectError(error.InvalidSessionFormat, parseWorkspaceRoot(std.testing.allocator, "[]")); try std.testing.expectError(error.InvalidSessionFormat, parseWorkspaceRoot(std.testing.allocator, "{\"workspace_root\":123}"));}
test "legacy session migration keeps missing compacted authority incomplete" { const alloc = std.testing.allocator; var parsed = try std.json.parseFromSlice( std.json.Value, alloc, "{\"kind\":\"compacted_summary\",\"summary\":\"legacy\",\"removed_turn_count\":2,\"compaction_count\":1}", .{}, ); defer parsed.deinit(); const migrated = try parseLegacyHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, migrated); try std.testing.expect(!migrated.compacted_summary.root_user_messages_complete); try std.testing.expect(!migrated.compacted_summary.permission_feedback_complete);
const history = [_]session.HistoryTurn{migrated}; const json = try renderSessionJson( alloc, "legacy-incomplete", 1, 2, session.ConversationLanguage.literal("und-Latn"), "/tmp/workspace", &history, .{}, ); defer alloc.free(json); var reloaded = try parseStoredSession(TestStoredSession, alloc, json); defer reloaded.deinit(alloc); try std.testing.expect(!reloaded.history[0].compacted_summary.root_user_messages_complete); try std.testing.expect(!reloaded.history[0].compacted_summary.permission_feedback_complete);}
test "session JSON round-trips assistant execution memory" { const alloc = std.testing.allocator;
var feedback = [_][]u8{@constCast("read it after writing")}; var calls = [_]session.ToolCall{.{ .id = "call_read", .name = "read_file", .arguments_json = "{\"path\":\"src/main.zig\"}", }}; var results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast("call_read"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("<path>src/main.zig</path>\nconst std = @import(\"std\");"), .output_bytes = 54, .stored_output_bytes = 54, .truncated = false, .provider_native = false, .created_at_ms = 1234, .permission_feedback = feedback[0..], }}; var steps = [_]session.ToolExecutionStep{.{ .assistant = @constCast("I'll inspect it."), .tool_calls = calls[0..], .tool_results = results[0..], }}; var steering = [_]types.PersistedSteering{.{ .text = @constCast("focus on rendering"), .assistant_prefix = @constCast("partial assistant"), .after_tool_step_count = 1, }}; var files = [_]session.FileEvidence{.{ .path = @constCast("src/main.zig"), .tool_call_id = @constCast("call_read"), .tool_name = @constCast("read_file"), .action = .read, .status = .success, .model_view_covers_full_file = true, .stale = false, }}; const history = [_]session.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("what is main") }, .assistant = @constCast("main wires the app"), .execution = .{ .tool_steps = steps[0..], .files = files[0..], .steering = steering[0..] }, } }};
const json = try renderSessionJson(alloc, "exec-json", 1, 2, session.ConversationLanguage.literal("en"), "/tmp/workspace", &history, .{}); defer alloc.free(json); try std.testing.expect(std.mem.find(u8, json, "\"execution\"") != null); try std.testing.expect(std.mem.find(u8, json, "\"tool_steps\"") != null); try std.testing.expect(std.mem.find(u8, json, "\"files\"") != null); try std.testing.expect(std.mem.find(u8, json, "\"execution\":{\"schema_version\":3") != null); try std.testing.expect(std.mem.find(u8, json, "\"permission_feedback\"") != null); try std.testing.expect(std.mem.find(u8, json, "\"text\":\"focus on rendering\"") != null); try std.testing.expect(std.mem.find(u8, json, "\"assistant_prefix\":\"partial assistant\"") != null); try std.testing.expect(std.mem.find(u8, json, "\"after_tool_step_count\":1") != null);
var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); const execution = loaded.history[0].assistant.execution; try std.testing.expectEqual(@as(usize, 1), execution.tool_steps.len); try std.testing.expectEqualStrings("I'll inspect it.", execution.tool_steps[0].assistant.?); try std.testing.expectEqualStrings("call_read", execution.tool_steps[0].tool_calls[0].id); try std.testing.expectEqualStrings("read_file", execution.tool_steps[0].tool_results[0].tool_name); try std.testing.expectEqual(@as(usize, 54), execution.tool_steps[0].tool_results[0].stored_output_bytes); try std.testing.expectEqual( @as(usize, 1), execution.tool_steps[0].tool_results[0].permission_feedback.len, ); try std.testing.expectEqualStrings( "read it after writing", execution.tool_steps[0].tool_results[0].permission_feedback[0], ); try std.testing.expectEqual(@as(usize, 1), execution.files.len); try std.testing.expectEqual(.read, execution.files[0].action); try std.testing.expect(execution.files[0].model_view_covers_full_file); try std.testing.expectEqual(@as(usize, 1), execution.steering.len); try std.testing.expectEqualStrings("focus on rendering", execution.steering[0].text); try std.testing.expectEqualStrings("partial assistant", execution.steering[0].assistant_prefix.?); try std.testing.expectEqual(@as(usize, 1), execution.steering[0].after_tool_step_count);}
fn checkV3SteeringAllocationFailures(alloc: Allocator) !void { const json = "[{\"text\":\"first\",\"assistant_prefix\":\"prefix one\",\"after_tool_step_count\":0}," ++ "{\"text\":\"second\",\"assistant_prefix\":\"prefix two\",\"after_tool_step_count\":1}]"; var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); const steering = try parseOptionalSteering(alloc, parsed.value, 3, 1); defer types.freePersistedSteering(alloc, steering); try std.testing.expectEqual(@as(usize, 2), steering.len);}
test "session JSON steering cleans up every allocation failure" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkV3SteeringAllocationFailures, .{}, );}
const legacy_execution_memory_fixture = "{\"schema_version\":1,\"id\":\"execution-ownership\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"assistant\",\"user\":{\"text\":\"inspect\",\"images\":[]},\"assistant\":\"done\"," ++ "\"execution\":{\"schema_version\":2,\"tool_steps\":[" ++ "{\"assistant\":\"checking\",\"tool_calls\":[{\"id\":\"call_write\",\"name\":\"write_file\",\"arguments_json\":\"{}\",\"provider_result\":null}],\"tool_results\":[{\"tool_call_id\":\"call_write\",\"tool_name\":\"write_file\",\"status\":\"success\",\"output\":\"wrote\",\"output_handle\":null,\"preview\":null,\"output_bytes\":5,\"stored_output_bytes\":5,\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1,\"permission_feedback\":[\"read it\"]}]}],\"files\":[" ++ "{\"path\":\"src/main.zig\",\"new_path\":null,\"tool_call_id\":\"call_read\",\"tool_name\":\"read_file\"," ++ "\"action\":\"read\",\"status\":\"success\",\"model_view_covers_full_file\":true,\"stale\":false}]," ++ "\"steering\":[\"legacy steer\"]}}]}";
fn checkLegacyExecutionMemoryAllocationFailures(alloc: Allocator) !void { var loaded = try parseStoredSession( TestStoredSession, alloc, legacy_execution_memory_fixture, ); defer loaded.deinit(alloc); try std.testing.expectEqual( @as(usize, 1), loaded.history[0].assistant.execution.tool_steps.len, ); try std.testing.expectEqual( @as(usize, 1), loaded.history[0].assistant.execution.files.len, ); try std.testing.expectEqualStrings( "legacy steer", loaded.history[0].assistant.execution.steering[0].text, ); try std.testing.expectEqual( @as(usize, 1), loaded.history[0].assistant.execution.steering[0].after_tool_step_count, ); try std.testing.expectEqual( @as(usize, 1), loaded.history[0].assistant.execution.tool_steps[0].tool_results[0].permission_feedback.len, );}
test "legacy execution memory frees parsed tool steps when files are malformed" { const json = "{\"schema_version\":1,\"id\":\"execution-malformed\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"assistant\",\"user\":{\"text\":\"inspect\",\"images\":[]},\"assistant\":\"done\"," ++ "\"execution\":{\"schema_version\":1,\"tool_steps\":[" ++ "{\"assistant\":\"checking\",\"tool_calls\":[],\"tool_results\":[]}],\"files\":{}}}]}";
try std.testing.expectError( error.InvalidSessionFormat, parseStoredSession(TestStoredSession, std.testing.allocator, json), );}
test "legacy execution memory cleans up every allocation failure" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkLegacyExecutionMemoryAllocationFailures, .{}, );}
test "session JSON persists malformed argument recovery as a safe failed pair" { const alloc = std.testing.allocator; const failure_output = "{\"error\":{\"type\":\"tool_execution_failed\",\"tool_name\":\"read_file\"," ++ "\"message\":\"Tool arguments were not valid JSON.\",\"suggestion\":" ++ "\"Reissue the tool call with complete valid JSON arguments matching the tool schema.\"}}";
var calls = [_]session.ToolCall{.{ .id = "call_bad", .name = "read_file", .arguments_json = "{}", .argument_integrity = .malformed_json, }}; var results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast("call_bad"), .tool_name = @constCast("read_file"), .status = .failure, .output = @constCast(failure_output), .output_bytes = failure_output.len, .stored_output_bytes = failure_output.len, .truncated = false, .provider_native = false, .created_at_ms = 1234, }}; var steps = [_]session.ToolExecutionStep{.{ .assistant = @constCast("I'll inspect it."), .tool_calls = calls[0..], .tool_results = results[0..], }}; const history = [_]session.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("inspect it") }, .assistant = @constCast("Recovered."), .execution = .{ .tool_steps = steps[0..] }, } }};
const json = try renderSessionJson( alloc, "malformed-recovery", 1, 2, session.ConversationLanguage.literal("en"), "/tmp/workspace", &history, .{}, ); defer alloc.free(json); try std.testing.expect(std.mem.find(u8, json, "\"arguments_json\":\"{}\"") != null); try std.testing.expect(std.mem.find(u8, json, "malformed_json") == null);
var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); const execution = loaded.history[0].assistant.execution; try std.testing.expectEqual(@as(usize, 1), execution.tool_steps.len); try std.testing.expectEqualStrings("{}", execution.tool_steps[0].tool_calls[0].arguments_json); try std.testing.expectEqual(.valid, execution.tool_steps[0].tool_calls[0].argument_integrity); try std.testing.expectEqual(session.PersistedToolStatus.failure, execution.tool_steps[0].tool_results[0].status); try std.testing.expectEqualStrings(failure_output, execution.tool_steps[0].tool_results[0].output);}
test "non-object legacy function inputs are repaired without changing recorded outcomes" { const alloc = std.testing.allocator; for ([_]session.PersistedToolStatus{ .success, .failure }) |status| { for (0..3) |native_kind| { var calls = [_]session.ToolCall{.{ .id = "call", .name = "read_file", .arguments_json = "[]" }}; calls[0].provider_result = if (native_kind == 1) "provider result" else null; var results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast("call"), .tool_name = @constCast("read_file"), .status = status, .output = @constCast("recorded"), .output_bytes = 8, .stored_output_bytes = 8, .truncated = false, .provider_native = native_kind == 2, .created_at_ms = 1, }}; var steps = [_]session.ToolExecutionStep{.{ .tool_calls = &calls, .tool_results = &results }}; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeExecutionMemoryJson(&encoded.writer, .{ .tool_steps = &steps }); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); const decoded = try parseOptionalExecutionMemory(alloc, parsed.value); defer session.freeExecutionMemory(alloc, decoded); const step = decoded.tool_steps[0]; try std.testing.expectEqualStrings(if (native_kind == 0) "{}" else "[]", step.tool_calls[0].arguments_json); try std.testing.expectEqual(if (native_kind == 0) types.ToolExecutionProvenance.fx_local else .provider_executed, step.tool_calls[0].provenance); try std.testing.expectEqual(types.ToolArgumentIntegrity.valid, step.tool_calls[0].argument_integrity); try std.testing.expectEqual(status, step.tool_results[0].status); try std.testing.expectEqualStrings("recorded", step.tool_results[0].output); try std.testing.expectEqual(@as(usize, 8), step.tool_results[0].stored_output_bytes); } }}
fn checkNonObjectLegacyInterruptedAllocationFailures(alloc: Allocator, value: std.json.Value) !void { const call = (try parseOptionalToolCall(alloc, value)).?; defer session.freeToolCall(alloc, call); try std.testing.expectEqualStrings("{}", call.arguments_json);}
test "non-object legacy interrupted repair cleans every allocation failure" { const alloc = std.testing.allocator; var parsed = try std.json.parseFromSlice(std.json.Value, alloc, \\{"id":"pending","name":"read_file","arguments_json":"[]","provider_result":null} , .{}); defer parsed.deinit(); try std.testing.checkAllAllocationFailures(alloc, checkNonObjectLegacyInterruptedAllocationFailures, .{parsed.value});}
test "legacy cancellation provenance reads old sessions and roundtrips optional origin" { const Case = struct { fn run(alloc: Allocator, origin_field: []const u8, expected: types.CancellationOrigin) !void { const json = try std.fmt.allocPrint( alloc, "{{\"schema_version\":1,\"id\":\"old\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{{\"kind\":\"interrupted\",\"user\":{{\"text\":\"request\",\"images\":[]}},\"assistant\":\"partial\"{s}}}]}}", .{origin_field}, ); defer alloc.free(json); var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); try std.testing.expectEqual(expected, loaded.history[0].interrupted.cancellation_origin); try std.testing.expectEqual(types.InterruptedTerminalReason.cancelled, loaded.history[0].interrupted.terminal_reason); // This in-memory writer reports injected allocation failure as WriteFailed. const encoded = renderSessionJson(alloc, loaded.id, loaded.created_at_ms, loaded.updated_at_ms, loaded.conversation_language, loaded.workspace_root.?, loaded.history, .{}) catch |err| return if (err == error.WriteFailed) error.OutOfMemory else err; defer alloc.free(encoded); try std.testing.expectEqual(expected == .compaction, std.mem.find(u8, encoded, "\"cancellation_origin\"") != null); var reloaded = try parseStoredSession(TestStoredSession, alloc, encoded); defer reloaded.deinit(alloc); try std.testing.expectEqual(expected, reloaded.history[0].interrupted.cancellation_origin); try std.testing.expectEqualStrings("partial", reloaded.history[0].interrupted.assistant.?); } }; try std.testing.checkAllAllocationFailures(std.testing.allocator, Case.run, .{ "", types.CancellationOrigin.turn }); try std.testing.checkAllAllocationFailures(std.testing.allocator, Case.run, .{ ",\"cancellation_origin\":\"turn\"", types.CancellationOrigin.turn }); try std.testing.checkAllAllocationFailures(std.testing.allocator, Case.run, .{ ",\"cancellation_origin\":\"compaction\"", types.CancellationOrigin.compaction });}
test "legacy cancellation provenance rejects invalid values after owned fields" { const alloc = std.testing.allocator; for ([_][]const u8{ "\"unknown\"", "null", "1", "false", "[]", "{}" }) |origin| { const json = try std.fmt.allocPrint( alloc, "{{\"kind\":\"interrupted\",\"user\":{{\"text\":\"request\",\"images\":[]}},\"assistant\":\"partial\",\"completed_tool_names\":[\"read_file\"],\"cancellation_origin\":{s}}}", .{origin}, ); defer alloc.free(json); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, json, .{}); defer parsed.deinit(); try std.testing.expectError(error.InvalidSessionFormat, parseLegacyHistoryTurn(alloc, parsed.value)); }}
test "old assistant session JSON without execution parses as empty memory" { const alloc = std.testing.allocator; const json = "{\"schema_version\":1,\"id\":\"old\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"assistant\",\"user\":{\"text\":\"hello\",\"images\":[]},\"assistant\":\"world\"}" ++ "]}";
var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), loaded.history.len); try std.testing.expectEqual(@as(usize, 0), loaded.history[0].assistant.execution.tool_steps.len); try std.testing.expectEqual(@as(usize, 0), loaded.history[0].assistant.execution.files.len);}
test "legacy session JSON repairs duplicate-key tool arguments before projection" { const io_mod = @import("../shared/io.zig"); const alloc = std.testing.allocator;
var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "legacy-repair-trace.log" }); defer alloc.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "session");
const json = "{\"schema_version\":1,\"id\":\"legacy-malformed\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"assistant\",\"user\":{\"text\":\"inspect\",\"images\":[]},\"assistant\":\"failed\"," ++ "\"execution\":{\"schema_version\":1,\"tool_steps\":[{\"assistant\":null,\"tool_calls\":[" ++ "{\"id\":\"call_bad\",\"name\":\"read_file\",\"arguments_json\":\"{\\\"depth\\\":1,\\\"depth\\\":2}\",\"provider_result\":null}" ++ "],\"tool_results\":[{\"tool_call_id\":\"call_bad\",\"tool_name\":\"read_file\",\"status\":\"success\"," ++ "\"output\":\"stale success\",\"output_handle\":null,\"preview\":null,\"output_bytes\":13,\"stored_output_bytes\":13," ++ "\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1}]}],\"files\":[]}}]}";
var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); const step = loaded.history[0].assistant.execution.tool_steps[0]; try std.testing.expectEqualStrings("{}", step.tool_calls[0].arguments_json); try std.testing.expectEqual(.valid, step.tool_calls[0].argument_integrity); try std.testing.expectEqual(session.PersistedToolStatus.failure, step.tool_results[0].status); try std.testing.expect(tool_result_errors.isToolExecutionFailedOutput(step.tool_results[0].output));
const rendered = try renderSessionJson( alloc, loaded.id, loaded.created_at_ms, loaded.updated_at_ms, loaded.conversation_language, loaded.workspace_root.?, loaded.history, .{}, ); defer alloc.free(rendered); try std.testing.expect(std.mem.find(u8, rendered, "\"depth\":1") == null);
var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var messages: std.ArrayList(types.ChatMessage) = .empty; defer messages.deinit(arena); try session.appendHistoryChatMessages(arena, &messages, loaded.history); try std.testing.expectEqual(@as(usize, 4), messages.items.len); try std.testing.expectEqualStrings("{}", messages.items[1].tool_calls[0].arguments_json); try std.testing.expect(std.mem.find(u8, messages.items[1].tool_calls[0].arguments_json, "\"depth\":1") == null); try std.testing.expect(tool_result_errors.isToolExecutionFailedOutput(messages.items[2].content.?)); try std.testing.expect(std.mem.find(u8, messages.items[2].content.?, "\"depth\":1") == null);
debug_trace.shutdown(); var trace_file = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), trace_path, .{}); defer trace_file.close(io_mod.getIo()); const trace = try io_mod.readFileToEnd(alloc, &trace_file, 8192); defer alloc.free(trace); try std.testing.expect(std.mem.find(u8, trace, "event=persisted_tool_arguments_repaired") != null); try std.testing.expect(std.mem.find(u8, trace, "source=legacy") != null); try std.testing.expect(std.mem.find(u8, trace, "call_id=call_bad") != null); try std.testing.expect(std.mem.find(u8, trace, "\"depth\":1") == null);}
test "legacy interrupted session sanitizes duplicate-key tool arguments" { const json = "{\"schema_version\":1,\"id\":\"legacy-interrupted\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"interrupted\",\"user\":{\"text\":\"inspect\",\"images\":[]},\"assistant\":\"working\"," ++ "\"tool_call\":{\"id\":\"call_bad\",\"name\":\"glob_files\",\"arguments_json\":\"{\\\"depth\\\":1,\\\"depth\\\":2}\",\"provider_result\":null}," ++ "\"completed_tool_names\":[]}]}";
var loaded = try parseStoredSession(TestStoredSession, std.testing.allocator, json); defer loaded.deinit(std.testing.allocator); const call = loaded.history[0].interrupted.tool_call.?; try std.testing.expectEqualStrings("{}", call.arguments_json); try std.testing.expectEqual(.valid, call.argument_integrity);}
test "legacy session JSON rejects ambiguous malformed tool result pairings" { const read_result = "{\"tool_call_id\":\"call_bad\",\"tool_name\":\"read_file\",\"status\":\"success\"," ++ "\"output\":\"stale\",\"output_handle\":null,\"preview\":null,\"output_bytes\":5,\"stored_output_bytes\":5," ++ "\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1}"; const list_result = "{\"tool_call_id\":\"call_bad\",\"tool_name\":\"glob_files\",\"status\":\"success\"," ++ "\"output\":\"stale\",\"output_handle\":null,\"preview\":null,\"output_bytes\":5,\"stored_output_bytes\":5," ++ "\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1}"; const cases = [_]struct { calls: []const u8, results: []const u8, }{ .{ .calls = "{\"id\":\"call_bad\",\"name\":\"read_file\",\"arguments_json\":\"{]\",\"provider_result\":null}," ++ "{\"id\":\"call_bad\",\"name\":\"read_file\",\"arguments_json\":\"{]\",\"provider_result\":null}", .results = read_result, }, .{ .calls = "{\"id\":\"call_bad\",\"name\":\"read_file\",\"arguments_json\":\"{]\",\"provider_result\":null}", .results = "", }, .{ .calls = "{\"id\":\"call_bad\",\"name\":\"read_file\",\"arguments_json\":\"{]\",\"provider_result\":null}", .results = read_result ++ "," ++ read_result, }, .{ .calls = "{\"id\":\"call_bad\",\"name\":\"read_file\",\"arguments_json\":\"{]\",\"provider_result\":null}", .results = list_result, }, };
for (cases) |case| { const json = try legacySessionWithToolStep(std.testing.allocator, case.calls, case.results); defer std.testing.allocator.free(json); try std.testing.expectError( error.InvalidSessionFormat, parseStoredSession(TestStoredSession, std.testing.allocator, json), ); }}
test "legacy duplicate-key repair preserves allocation failures" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkLegacyToolArgumentRepairAllocationFailures, .{}, );}
fn legacySessionWithToolStep( alloc: Allocator, calls: []const u8, results: []const u8,) ![]u8 { return std.mem.concat(alloc, u8, &.{ "{\"schema_version\":1,\"id\":\"legacy-malformed\",\"created_at_ms\":1,\"updated_at_ms\":2,", "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[", "{\"kind\":\"assistant\",\"user\":{\"text\":\"inspect\",\"images\":[]},\"assistant\":\"failed\",", "\"execution\":{\"schema_version\":1,\"tool_steps\":[{\"assistant\":null,\"tool_calls\":[", calls, "],\"tool_results\":[", results, "]}],\"files\":[]}}]}", });}
fn checkLegacyToolArgumentRepairAllocationFailures(alloc: Allocator) !void { const json = "{\"schema_version\":1,\"id\":\"legacy-oom\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"assistant\",\"user\":{\"text\":\"inspect\",\"images\":[]},\"assistant\":\"failed\"," ++ "\"execution\":{\"schema_version\":1,\"tool_steps\":[{\"assistant\":null,\"tool_calls\":[" ++ "{\"id\":\"call_bad\",\"name\":\"read_file\",\"arguments_json\":\"{\\\"depth\\\":1,\\\"depth\\\":2}\",\"provider_result\":null}" ++ "],\"tool_results\":[{\"tool_call_id\":\"call_bad\",\"tool_name\":\"read_file\",\"status\":\"success\"," ++ "\"output\":\"stale\",\"output_handle\":null,\"preview\":null,\"output_bytes\":5,\"stored_output_bytes\":5," ++ "\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1}]}],\"files\":[]}}]}";
var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); const step = loaded.history[0].assistant.execution.tool_steps[0]; try std.testing.expectEqualStrings("{}", step.tool_calls[0].arguments_json); try std.testing.expectEqual(session.PersistedToolStatus.failure, step.tool_results[0].status);}
test "session JSON round-trips large result handle metadata and accepts old inline results" { const alloc = std.testing.allocator;
var calls = [_]session.ToolCall{.{ .id = "call_large", .name = "read_file", .arguments_json = "{\"path\":\"large.txt\"}", }}; var results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast("call_large"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("<tool_result_preview handle=\"result-call_large-abc.txt\">\nevidence\n</tool_result_preview>"), .output_bytes = 90_000, .stored_output_bytes = 90_000, .truncated = true, .provider_native = false, .created_at_ms = 1234, .output_handle = @constCast("result-call_large-abc.txt"), .preview = @constCast("evidence"), }}; var steps = [_]session.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; var files = [_]session.FileEvidence{.{ .path = @constCast("large.txt"), .tool_call_id = @constCast("call_large"), .tool_name = @constCast("read_file"), .action = .read, .status = .success, .model_view_covers_full_file = false, }}; const history = [_]session.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("read large file") }, .assistant = @constCast("captured"), .execution = .{ .tool_steps = steps[0..], .files = files[0..], }, } }};
const json = try renderSessionJson(alloc, "large-json", 1, 2, session.ConversationLanguage.literal("en"), "/tmp/workspace", &history, .{}); defer alloc.free(json); try std.testing.expect(std.mem.find(u8, json, "\"output_handle\"") != null); try std.testing.expect(std.mem.find(u8, json, "\"preview\"") != null);
var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); const result = loaded.history[0].assistant.execution.tool_steps[0].tool_results[0]; try std.testing.expect(result.truncated); try std.testing.expectEqualStrings("result-call_large-abc.txt", result.output_handle.?); try std.testing.expectEqualStrings("evidence", result.preview.?); try std.testing.expectEqual(@as(usize, 1), loaded.history[0].assistant.execution.files.len); try std.testing.expect(!loaded.history[0].assistant.execution.files[0].model_view_covers_full_file);
const old_json = "{\"schema_version\":1,\"id\":\"old-inline\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"assistant\",\"user\":{\"text\":\"u\",\"images\":[]},\"assistant\":\"a\",\"execution\":{\"schema_version\":1,\"tool_steps\":[" ++ "{\"assistant\":null,\"tool_calls\":[{\"id\":\"c\",\"name\":\"read_file\",\"arguments_json\":\"{}\",\"provider_result\":null}],\"tool_results\":[" ++ "{\"tool_call_id\":\"c\",\"tool_name\":\"read_file\",\"status\":\"success\",\"output\":\"inline\",\"output_bytes\":6,\"stored_output_bytes\":6,\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1}" ++ "]}],\"files\":[]}}" ++ "]}"; var old_loaded = try parseStoredSession(TestStoredSession, alloc, old_json); defer old_loaded.deinit(alloc); const old_result = old_loaded.history[0].assistant.execution.tool_steps[0].tool_results[0]; try std.testing.expect(old_result.output_handle == null); try std.testing.expect(old_result.preview == null);}
test "interactive session round trips total web search requests and loads absent value as zero" { const alloc = std.testing.allocator;
const json = try renderSessionJson(alloc, "search-usage", 1, 2, session.ConversationLanguage.literal("en"), "/tmp/workspace", &.{}, .{ .web_search_requests = 7, }); defer alloc.free(json); var loaded = try parseStoredSession(TestStoredSession, alloc, json); defer loaded.deinit(alloc); try std.testing.expectEqual(@as(u64, 7), loaded.total_web_search_requests);
var old_loaded = try parseStoredSession( TestStoredSession, alloc, "{\"schema_version\":1,\"id\":\"old\",\"created_at_ms\":1,\"updated_at_ms\":2,\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":0,\"history\":[]}", ); defer old_loaded.deinit(alloc); try std.testing.expectEqual(@as(u64, 0), old_loaded.total_web_search_requests);}
test "legacy summary streaming stops before writer-produced history payload" { const session_store = @import("session_store.zig"); const alloc = std.testing.allocator; const large_text = try alloc.alloc(u8, 128 * 1024); defer alloc.free(large_text); @memset(large_text, 'h'); const history = [_]session.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("prompt") }, .assistant = large_text, } }}; const json = try renderSessionJson( alloc, "stream-summary", 10, 20, session.ConversationLanguage.literal("en"), "/tmp/workspace", &history, .{}, ); defer alloc.free(json); const history_start = std.mem.indexOf(u8, json, "\"history\":[") orelse return error.TestExpectedEqual;
const summary_prefix_end = history_start + "\"history\":".len; var source = std.Io.Reader.fixed(json[0..summary_prefix_end]); var summary = try parseLegacySummaryStreaming( session_store.SessionSummary, alloc, &source, ); defer summary.deinit(alloc); try std.testing.expectEqualStrings("stream-summary", summary.id); try std.testing.expectEqual(@as(usize, 1), summary.history_len);}
test "legacy summary streaming structurally skips externally reordered history" { const session_store = @import("session_store.zig"); const alloc = std.testing.allocator; const json = "{\"schema_version\":2,\"history\":[{\"kind\":\"compacted_summary\",\"summary\":\"x\"," ++ "\"removed_turn_count\":1,\"compaction_count\":1}],\"id\":\"reordered\"," ++ "\"created_at_ms\":1,\"updated_at_ms\":2,\"workspace_root\":null," ++ "\"conversation_language\":\"en\",\"history_len\":1}"; var source = std.Io.Reader.fixed(json); var summary = try parseLegacySummaryStreaming( session_store.SessionSummary, alloc, &source, ); defer summary.deinit(alloc); try std.testing.expectEqualStrings("reordered", summary.id); try std.testing.expect(summary.workspace_root == null); try std.testing.expectEqual(@as(usize, 1), summary.history_len);}
test "schema v2 legacy exact reader migrates background ownership to inert history" { const alloc = std.testing.allocator; const json = "{\"schema_version\":2,\"id\":\"legacy-v2\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":2,\"history\":[" ++ "{\"kind\":\"assistant\",\"user\":{\"text\":\"u\",\"images\":[{\"id\":7,\"path\":\"/tmp/a.png\",\"media_type\":\"image/png\"}]}," ++ "\"assistant\":\"a\",\"execution\":{\"schema_version\":1,\"tool_steps\":[],\"files\":[]}}," ++ "{\"kind\":\"background_command\",\"user\":{\"text\":\"run\",\"images\":[]},\"log_path\":\"/tmp/log\"," ++ "\"expect_url\":false,\"url\":null,\"background_record_id\":\"00112233445566778899aabbccddeeff\"}" ++ "]}"; var loaded = try parseLegacyExact(TestStoredSession, alloc, json); defer loaded.deinit(alloc); try std.testing.expectEqual(@as(usize, 7), loaded.history[0].assistant.user.images[0].id); try std.testing.expect(loaded.history[1] == .assistant); try std.testing.expect(std.mem.find( u8, loaded.history[1].assistant.assistant, "fx no longer owns or controls this process", ) != null); try std.testing.expect(std.mem.find( u8, loaded.history[1].assistant.assistant, "former log=/tmp/log", ) != null);}
test "legacy exact reader rejects durable byte objects in string fields" { const alloc = std.testing.allocator; const snapshots = [_][]const u8{ "{\"schema_version\":1,\"id\":\"legacy-v1\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"compacted_summary\",\"summary\":{\"encoding\":\"base64\",\"data\":\"/w==\"}," ++ "\"removed_turn_count\":1,\"compaction_count\":1}]}", "{\"schema_version\":2,\"id\":\"legacy-v2\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":1,\"history\":[" ++ "{\"kind\":\"compacted_summary\",\"summary\":{\"encoding\":\"base64\",\"data\":\"/w==\"}," ++ "\"removed_turn_count\":1,\"compaction_count\":1}]}", };
for (snapshots) |snapshot| { try std.testing.expectError( error.InvalidSessionFormat, parseLegacyExact(TestStoredSession, alloc, snapshot), ); }}
test "legacy exact and summary readers preserve resource exhaustion" { const session_store = @import("session_store.zig"); const json = "{\"schema_version\":1,\"id\":\"oom\",\"created_at_ms\":1,\"updated_at_ms\":2," ++ "\"workspace_root\":\"/tmp/workspace\",\"conversation_language\":\"en\",\"history_len\":0,\"history\":[]}";
var exact_failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); try std.testing.expectError( error.OutOfMemory, parseLegacyExact(TestStoredSession, exact_failing.allocator(), json), );
var source = std.Io.Reader.fixed(json); var summary_failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); try std.testing.expectError( error.OutOfMemory, parseLegacySummaryStreaming( session_store.SessionSummary, summary_failing.allocator(), &source, ), );}
test { _ = @import("session_codec.zig"); _ = @import("session_event.zig"); _ = @import("session_projection.zig");}