const std = @import("std"); const image_data = @import("../images/image_data.zig"); const session = @import("session.zig"); const session_usage = @import("session_usage.zig"); const session_permission_state = @import("../permissions/session_permission_state.zig"); const mem_utils = @import("../shared/mem_utils.zig"); const types = @import("../shared/types.zig"); const captured_command = @import("../tooling/captured_command.zig"); const model_provider = @import("../config/model_provider.zig"); const context_limits = @import("../config/context_limits.zig"); const credential_authority = @import("../auth/credential_authority.zig"); const Allocator = std.mem.Allocator; const Sha256 = std.crypto.hash.sha2.Sha256; pub const DurableSessionPreferences = struct { provider: model_provider.ProviderId = .gateway, model: []u8, effort: types.ReasoningEffort, fast_mode: bool, pub fn deinit(self: *DurableSessionPreferences, alloc: Allocator) void { alloc.free(self.model); self.* = undefined; } pub fn dupe(self: DurableSessionPreferences, alloc: Allocator) !DurableSessionPreferences { return .{ .provider = self.provider, .model = try alloc.dupe(u8, self.model), .effort = self.effort, .fast_mode = self.fast_mode, }; } }; /// Decodes persisted preferences; the caller owns the returned model. pub fn parse_preferences(alloc: Allocator, value: std.json.Value) !DurableSessionPreferences { const raw = try requireObject(value); const legacy = raw.contains("connection_id") or raw.contains("model_id"); const object = if (legacy) try exactObject(value, &.{ "connection_id", "model_id", "effort", "fast_mode" }) else if (raw.contains("provider")) try exactObject(value, &.{ "provider", "model", "effort", "fast_mode" }) else try exactObject(value, &.{ "model", "effort", "fast_mode" }); const provider: model_provider.ProviderId = if (legacy) blk: { if (!std.mem.eql(u8, try requireString(object, "connection_id"), "vercel")) return error.InvalidDurableField; break :blk .gateway; } else if (object.contains("provider")) model_provider.parse_saved(object.get("provider").?) catch return error.InvalidDurableField else .gateway; const model = try requireString(object, if (legacy) "model_id" else "model"); try validateModel(model); const effort = types.ReasoningEffort.parse(try requireString(object, "effort")) orelse return error.InvalidDurableField; const fast_mode = try requireBool(object, "fast_mode"); return .{ .provider = provider, .model = try alloc.dupe(u8, model), .effort = effort, .fast_mode = fast_mode }; } test "legacy connection preferences decode through the shared state codec" { const alloc = std.testing.allocator; const state_json = "{\"id\":\"legacy\",\"origin_workspace_root\":\"/workspace\",\"workspace_root\":\"/workspace\",\"created_at_ms\":1,\"updated_at_ms\":2,\"conversation_language\":\"en\",\"preferences\":{\"connection_id\":\"vercel\",\"model_id\":\"test/model\",\"effort\":\"high\",\"fast_mode\":true},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}"; try std.testing.checkAllAllocationFailures(alloc, struct { fn check(a: Allocator, bytes: []const u8) !void { var source = std.Io.Reader.fixed(bytes); var state = try decodeState(a, &source, .{}); defer state.deinit(a); try std.testing.expectEqual(model_provider.ProviderId.gateway, state.preferences.provider); try std.testing.expectEqualStrings("test/model", state.preferences.model); try std.testing.expectEqualStrings("high", state.preferences.effort.label()); try std.testing.expect(state.preferences.fast_mode); } }.check, .{state_json}); } test "legacy connection preferences reject unknown identities and mixed fields" { const alloc = std.testing.allocator; for ([_][]const u8{ "{\"connection_id\":\"custom\",\"model_id\":\"test\",\"effort\":\"auto\",\"fast_mode\":false}", "{\"connection_id\":\"vercel\",\"model_id\":\"test\",\"model\":\"other\",\"effort\":\"auto\",\"fast_mode\":false}", "{\"connection_id\":\"vercel\",\"model_id\":\"test\",\"provider\":\"codex\",\"effort\":\"auto\",\"fast_mode\":false}", "{\"connection_id\":\"vercel\",\"model_id\":\"\",\"effort\":\"auto\",\"fast_mode\":false}", }) |bytes| { var parsed = try std.json.parseFromSlice(std.json.Value, alloc, bytes, .{}); defer parsed.deinit(); if (parse_preferences(alloc, parsed.value)) |value| { var owned = value; owned.deinit(alloc); return error.TestUnexpectedResult; } else |err| try std.testing.expect(err == error.InvalidSessionFormat or err == error.InvalidDurableField); } } pub const RecoveryToolState = enum { none, proven_unexecuted, confirmed, uncertain, }; pub const TurnAuthority = struct { provider: model_provider.ProviderId, model: []u8, credential_source: ?types.CredentialSource = null, credential_identity: ?credential_authority.Identity = null, pub fn deinit(self: *TurnAuthority, alloc: Allocator) void { alloc.free(self.model); self.* = undefined; } pub fn dupe(self: TurnAuthority, alloc: Allocator) !TurnAuthority { return .{ .provider = self.provider, .model = try alloc.dupe(u8, self.model), .credential_source = self.credential_source, .credential_identity = self.credential_identity, }; } }; pub const RecoveryCheckpoint = struct { version: u8 = 2, disposition: enum { continuable, history_only } = .continuable, turn_id: u64, user: session.UserTurn, assistant_source: []u8, execution: session.ExecutionMemory = .{}, cause: types.ModelRecoveryCause, action: types.ModelRecoveryAction, tool_state: RecoveryToolState = .none, authority: TurnAuthority, requested_fast_mode: bool, fast_mode: bool, max_provider_attempts: usize, consumed_provider_attempts: usize, outstanding_reservation: bool = false, /// Borrows the checkpoint's exact user and uncommitted response suffix. pub fn interruptedTurn(self: RecoveryCheckpoint) types.HistoryTurn { return .{ .interrupted = .{ .user = self.user, .assistant = if (self.assistant_source.len > 0) self.assistant_source else null, .execution = self.execution, .terminal_reason = .failed, } }; } pub fn deinit(self: *RecoveryCheckpoint, alloc: Allocator) void { session.freeUserTurn(alloc, self.user); alloc.free(self.assistant_source); session.freeExecutionMemory(alloc, self.execution); self.authority.deinit(alloc); self.* = undefined; } pub fn dupe(self: RecoveryCheckpoint, alloc: Allocator) !RecoveryCheckpoint { const user = try session.dupeUserTurn(alloc, self.user); errdefer session.freeUserTurn(alloc, user); const assistant_source = try alloc.dupe(u8, self.assistant_source); errdefer alloc.free(assistant_source); const execution = try types.dupeExecutionMemory(alloc, self.execution); errdefer session.freeExecutionMemory(alloc, execution); const authority = try self.authority.dupe(alloc); return .{ .version = self.version, .disposition = self.disposition, .turn_id = self.turn_id, .user = user, .assistant_source = assistant_source, .execution = execution, .cause = self.cause, .action = self.action, .tool_state = self.tool_state, .authority = authority, .requested_fast_mode = self.requested_fast_mode, .fast_mode = self.fast_mode, .max_provider_attempts = self.max_provider_attempts, .consumed_provider_attempts = self.consumed_provider_attempts, .outstanding_reservation = self.outstanding_reservation, }; } }; pub const DurableSessionState = struct { id: []u8, origin_workspace_root: []u8, workspace_root: []u8, created_at_ms: i64, updated_at_ms: i64, conversation_language: session.ConversationLanguage, preferences: DurableSessionPreferences, history: []session.HistoryTurn, context_history_start: usize = 0, total_input_tokens: u64, total_output_tokens: u64, permission_state: session_permission_state.State = .{}, /// Last ordinary history commit correlated to durable subagent work. This /// control-only marker is not included in model history. last_subagent_work_id: ?[]u8 = null, /// True only for a parent-owned subagent session. This durable identity /// keeps the session private even if owner sidecar publication is /// interrupted immediately after creation. subagent_child: bool = false, /// Null only for sessions written before durable usage accounting. usage: ?session_usage.Snapshot = null, /// Active control state. It is never projected into model history and a /// restored checkpoint requires explicit host authority before any send. recovery_checkpoint: ?RecoveryCheckpoint = null, pub fn deinit(self: *DurableSessionState, alloc: Allocator) void { alloc.free(self.id); alloc.free(self.origin_workspace_root); alloc.free(self.workspace_root); self.preferences.deinit(alloc); session.freeHistoryTurnSlice(alloc, self.history); self.permission_state.deinit(alloc); if (self.last_subagent_work_id) |id| alloc.free(id); if (self.usage) |*usage| usage.deinit(alloc); if (self.recovery_checkpoint) |*checkpoint| checkpoint.deinit(alloc); self.* = undefined; } /// Retains legacy recovery evidence without granting permission to replay its route. pub fn archive_legacy_recovery(self: *DurableSessionState, alloc: Allocator) !bool { const checkpoint = if (self.recovery_checkpoint) |*value| value else return false; if (checkpoint.disposition != .history_only) return false; const turn = try session.dupeHistoryTurn(alloc, checkpoint.interruptedTurn()); errdefer session.freeHistoryTurn(alloc, turn); const len = try std.math.add(usize, self.history.len, 1); self.history = try alloc.realloc(self.history, len); self.history[len - 1] = turn; checkpoint.deinit(alloc); self.recovery_checkpoint = null; return true; } pub fn dupe(self: DurableSessionState, alloc: Allocator) !DurableSessionState { const id = try alloc.dupe(u8, self.id); errdefer alloc.free(id); const origin_workspace_root = try alloc.dupe(u8, self.origin_workspace_root); errdefer alloc.free(origin_workspace_root); const workspace_root = try alloc.dupe(u8, self.workspace_root); errdefer alloc.free(workspace_root); const preferences = try self.preferences.dupe(alloc); errdefer { var owned_preferences = preferences; owned_preferences.deinit(alloc); } const history = try dupeHistory(alloc, self.history); errdefer session.freeHistoryTurnSlice(alloc, history); const last_subagent_work_id = if (self.last_subagent_work_id) |work_id| try alloc.dupe(u8, work_id) else null; errdefer if (last_subagent_work_id) |work_id| alloc.free(work_id); var usage = if (self.usage) |snapshot| try session_usage.dupeSnapshotOwned(alloc, snapshot) else null; errdefer if (usage) |*snapshot| snapshot.deinit(alloc); const recovery_checkpoint = if (self.recovery_checkpoint) |checkpoint| try checkpoint.dupe(alloc) else null; const permission_state = try session_permission_state.dupe( alloc, self.permission_state, ); return .{ .id = id, .origin_workspace_root = origin_workspace_root, .workspace_root = workspace_root, .created_at_ms = self.created_at_ms, .updated_at_ms = self.updated_at_ms, .conversation_language = self.conversation_language, .preferences = preferences, .history = history, .context_history_start = self.context_history_start, .total_input_tokens = self.total_input_tokens, .total_output_tokens = self.total_output_tokens, .permission_state = permission_state, .last_subagent_work_id = last_subagent_work_id, .subagent_child = self.subagent_child, .usage = usage, .recovery_checkpoint = recovery_checkpoint, }; } }; pub const session_metadata_schema_version: u8 = 4; pub const max_session_metadata_bytes: usize = 64 * 1024; pub const max_session_title_bytes: usize = 240; pub const SessionMetadata = struct { schema_version: u8 = session_metadata_schema_version, id: []const u8, origin_workspace_root: []const u8, workspace_root: []const u8, created_at_ms: i64, updated_at_ms: i64, conversation_language: []const u8, provider: model_provider.ProviderId, model: []const u8, effort: []const u8, fast_mode: bool, title: ?[]const u8 = null, subagent_child: bool = false, }; pub const DecodedSessionMetadata = std.json.Parsed(SessionMetadata); pub const max_permission_state_bytes: usize = 1024 * 1024; pub const max_recovery_checkpoint_bytes: usize = context_limits.emergency_ceiling_bytes; pub fn encodeSessionMetadata( alloc: Allocator, metadata: SessionMetadata, ) ![]u8 { try validateSessionMetadata(metadata); var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try std.json.Stringify.value(metadata, .{}, &out.writer); if (out.written().len == 0 or out.written().len > max_session_metadata_bytes) { return error.SessionMetadataTooLarge; } return out.toOwnedSlice() catch return error.OutOfMemory; } pub fn decodeSessionMetadata( alloc: Allocator, bytes: []const u8, ) !DecodedSessionMetadata { if (bytes.len == 0 or bytes.len > max_session_metadata_bytes) { return error.SessionMetadataTooLarge; } var parsed = std.json.parseFromSlice(SessionMetadata, alloc, bytes, .{ .allocate = .alloc_always, .ignore_unknown_fields = false, .max_value_len = max_session_metadata_bytes, }) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.InvalidSessionMetadata, }; errdefer parsed.deinit(); validateSessionMetadata(parsed.value) catch return error.InvalidSessionMetadata; return parsed; } pub fn encodePermissionState( alloc: Allocator, state: session_permission_state.State, ) ![]u8 { try session_permission_state.validate(state); var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try writePermissionState(&out.writer, state); if (out.written().len == 0 or out.written().len > max_permission_state_bytes) { return error.PermissionStateTooLarge; } return out.toOwnedSlice() catch return error.OutOfMemory; } pub fn decodePermissionState( alloc: Allocator, bytes: []const u8, ) !session_permission_state.State { if (bytes.len == 0 or bytes.len > max_permission_state_bytes) { return error.PermissionStateTooLarge; } var parsed = std.json.parseFromSlice(std.json.Value, alloc, bytes, .{ .max_value_len = max_permission_state_bytes, }) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.InvalidPermissionState, }; defer parsed.deinit(); return parsePermissionState(alloc, parsed.value) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.InvalidPermissionState, }; } pub fn encodeRecoveryCheckpoint( alloc: Allocator, checkpoint: RecoveryCheckpoint, ) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try writeRecoveryCheckpoint(&out.writer, checkpoint); if (out.written().len == 0 or out.written().len > max_recovery_checkpoint_bytes) { return error.RecoveryCheckpointTooLarge; } return out.toOwnedSlice() catch return error.OutOfMemory; } fn validateSessionMetadata(metadata: SessionMetadata) !void { if (metadata.schema_version != session_metadata_schema_version) { return error.UnsupportedSessionSchema; } try validateSessionId(metadata.id); try validateWorkspaceRoot(metadata.origin_workspace_root); try validateWorkspaceRoot(metadata.workspace_root); if (metadata.created_at_ms < 0 or metadata.updated_at_ms < metadata.created_at_ms) { return error.InvalidSessionMetadata; } try validateConversationLanguageBytes(metadata.conversation_language); if (metadata.provider == .configured and metadata.provider.configured.binding == null) return error.InvalidSessionMetadata; try validateModel(metadata.model); if (types.ReasoningEffort.parse(metadata.effort) == null) { return error.InvalidSessionMetadata; } if (metadata.title) |title| { if (title.len == 0 or title.len > max_session_title_bytes or !std.unicode.utf8ValidateSlice(title)) { return error.InvalidSessionMetadata; } } } pub const EncodeSummary = struct { encoded_bytes: u64, sha256: [Sha256.digest_length]u8, }; pub const DecodeLimits = struct { max_history_turns: usize = std.math.maxInt(usize), max_value_bytes: usize = std.math.maxInt(usize) / 2, }; const CountingSha256 = struct { sha256: Sha256 = Sha256.init(.{}), bytes: u64 = 0, pub fn update(self: *CountingSha256, bytes: []const u8) void { self.sha256.update(bytes); self.bytes += bytes.len; } }; pub fn writeDurableBytes(writer: *std.Io.Writer, bytes: []const u8) !void { if (std.unicode.utf8ValidateSlice(bytes)) { return writeJsonString(writer, bytes); } try writer.writeAll("{\"encoding\":\"base64\",\"data\":\""); try std.base64.standard.Encoder.encodeWriter(writer, bytes); try writer.writeAll("\"}"); } pub fn parseDurableBytes(alloc: Allocator, value: std.json.Value) ![]u8 { switch (value) { .string => |text| return try alloc.dupe(u8, text), .object => |object| { if (object.count() != 2) return error.InvalidDurableBytes; var iterator = object.iterator(); const encoding_entry = iterator.next() orelse return error.InvalidDurableBytes; const data_entry = iterator.next() orelse return error.InvalidDurableBytes; if (!std.mem.eql(u8, encoding_entry.key_ptr.*, "encoding") or !std.mem.eql(u8, data_entry.key_ptr.*, "data")) { return error.InvalidDurableBytes; } if (encoding_entry.value_ptr.* != .string or !std.mem.eql(u8, encoding_entry.value_ptr.string, "base64") or data_entry.value_ptr.* != .string) { return error.InvalidDurableBytes; } const encoded = data_entry.value_ptr.string; const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(encoded) catch return error.InvalidDurableBytes; const decoded = try alloc.alloc(u8, decoded_len); errdefer alloc.free(decoded); std.base64.standard.Decoder.decode(decoded, encoded) catch return error.InvalidDurableBytes; if (std.unicode.utf8ValidateSlice(decoded)) return error.InvalidDurableBytes; const canonical_len = std.base64.standard.Encoder.calcSize(decoded.len); const canonical = try alloc.alloc(u8, canonical_len); defer alloc.free(canonical); const written = std.base64.standard.Encoder.encode(canonical, decoded); if (!std.mem.eql(u8, written, encoded)) return error.InvalidDurableBytes; return decoded; }, else => return error.InvalidDurableBytes, } } pub fn encodeState(state: DurableSessionState, writer: *std.Io.Writer) !EncodeSummary { try validateState(state); var hash_buffer: [4096]u8 = undefined; var hashed = std.Io.Writer.hashed(writer, CountingSha256{}, &hash_buffer); try writeState(&hashed.writer, state); try hashed.writer.flush(); return .{ .encoded_bytes = hashed.hasher.bytes, .sha256 = hashed.hasher.sha256.finalResult(), }; } pub fn decodeState(alloc: Allocator, source: *std.Io.Reader, limits: DecodeLimits) !DurableSessionState { return decodeStateWithUsageContract(alloc, source, limits, false); } /// Reads an old persisted state without requiring modern cache-token totals. /// Caller owns the state; all non-usage validation remains unchanged. pub fn decodeLegacyState(alloc: Allocator, source: *std.Io.Reader, limits: DecodeLimits) !DurableSessionState { return decodeStateWithUsageContract(alloc, source, limits, true); } fn decodeStateWithUsageContract(alloc: Allocator, source: *std.Io.Reader, limits: DecodeLimits, legacy_usage: bool) !DurableSessionState { return decodeStateImpl(alloc, source, limits, legacy_usage) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.InvalidDurableField => return error.InvalidDurableField, error.InvalidDurableBytes => return error.InvalidDurableBytes, else => return error.InvalidSessionFormat, }; } pub fn writeHistoryTurn(writer: *std.Io.Writer, turn: session.HistoryTurn) !void { switch (turn) { .compacted_summary => |entry| { try writer.writeAll("{\"kind\":\"compacted_summary\",\"summary\":"); try writeDurableBytes(writer, entry.summary); 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 writeUserTurn(writer, entry.user); try writer.writeAll(",\"assistant\":"); try writeDurableBytes(writer, entry.assistant); try writer.writeAll(",\"provider_replay\":"); try std.json.Stringify.value(entry.provider_replay, .{}, writer); try writer.writeAll(",\"execution\":"); try writeExecutionMemory(writer, entry.execution); try writer.writeByte('}'); }, .interrupted => |entry| { try writer.writeAll("{\"kind\":\"interrupted\",\"user\":"); try writeUserTurn(writer, entry.user); try writer.writeAll(",\"assistant\":"); try writeOptionalDurableBytes(writer, entry.assistant); try writer.writeAll(",\"tool_call\":"); if (entry.tool_call) |tool_call| { try writeToolCall(writer, tool_call); } else { try writer.writeAll("null"); } try writer.writeAll(",\"completed_tool_names\":["); for (entry.completed_tool_names, 0..) |name, i| { if (i > 0) try writer.writeByte(','); try writeDurableBytes(writer, name); } try writer.writeByte(']'); try writer.writeAll(",\"terminal_reason\":"); try writeJsonString(writer, @tagName(entry.terminal_reason)); if (entry.cancellation_origin == .compaction) { try writer.writeAll(",\"cancellation_origin\":\"compaction\""); } if (hasDurableExecutionMemory(entry.execution)) { try writer.writeAll(",\"execution\":"); try writeExecutionMemory(writer, entry.execution); } if (entry.cancelled_command) |presentation| { try writer.writeAll(",\"cancelled_command\":"); try writeCancelledCommandPresentation(writer, presentation); } try writer.writeByte('}'); }, } } 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.writeAll("]"); return out.toOwnedSlice(); } pub fn parseHistoryTurn(alloc: Allocator, value: std.json.Value) !session.HistoryTurn { const kind = try requireString(try requireObject(value), "kind"); if (std.mem.eql(u8, kind, "compacted_summary")) { const raw_object = try requireObject(value); const has_root_user_messages = raw_object.get("root_user_messages") != null; const has_completeness = raw_object.get("root_user_messages_complete") != null; const has_permission_feedback = raw_object.get("permission_feedback") != null; const has_feedback_completeness = raw_object.get("permission_feedback_complete") != null; if (has_permission_feedback != has_feedback_completeness) { return error.InvalidSessionFormat; } const object = if (has_feedback_completeness) try exactObject(value, &.{ "kind", "summary", "removed_turn_count", "compaction_count", "root_user_messages", "root_user_messages_complete", "permission_feedback", "permission_feedback_complete" }) else if (has_completeness) try exactObject(value, &.{ "kind", "summary", "removed_turn_count", "compaction_count", "root_user_messages", "root_user_messages_complete" }) else if (has_root_user_messages) try exactObject(value, &.{ "kind", "summary", "removed_turn_count", "compaction_count", "root_user_messages" }) else try exactObject(value, &.{ "kind", "summary", "removed_turn_count", "compaction_count" }); const summary = try parseRequiredDurableBytes(alloc, object, "summary"); errdefer mem_utils.free(alloc, summary); const root_user_messages: [][]u8 = if (has_root_user_messages) try parseDurableBytesArray( alloc, object.get("root_user_messages") orelse return error.InvalidSessionFormat, ) else &.{}; errdefer types.freeCompletedToolNames(alloc, root_user_messages); const permission_feedback: [][]u8 = if (has_permission_feedback) try parseDurableBytesArray( alloc, object.get("permission_feedback") orelse return error.InvalidSessionFormat, ) else &.{}; errdefer types.freePermissionFeedback(alloc, permission_feedback); const removed_turn_count = try requireUsize(object, "removed_turn_count"); return .{ .compacted_summary = .{ .summary = 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 (has_completeness) try requireBool(object, "root_user_messages_complete") else has_root_user_messages or removed_turn_count == 0, .permission_feedback = permission_feedback, .permission_feedback_complete = if (has_feedback_completeness) try requireBool(object, "permission_feedback_complete") else removed_turn_count == 0, } }; } if (std.mem.eql(u8, kind, "assistant")) { const shape = try exactVariantObject(value, &.{ "kind", "user", "assistant", "execution" }, &.{ "kind", "user", "assistant", "execution", "provider_replay" }); const object = shape.object; const user = try parseUserTurn(alloc, object.get("user") orelse return error.InvalidSessionFormat); errdefer session.freeUserTurn(alloc, user); const assistant = try parseRequiredDurableBytes(alloc, object, "assistant"); errdefer mem_utils.free(alloc, assistant); const execution = try parseExecutionMemory(alloc, object.get("execution") orelse return error.InvalidSessionFormat); errdefer session.freeExecutionMemory(alloc, execution); const replay = try parseProviderReplay(alloc, object.get("provider_replay") orelse .null); return .{ .assistant = .{ .user = user, .assistant = assistant, .execution = execution, .provider_replay = replay, } }; } if (std.mem.eql(u8, kind, "background_command")) { const shape = try exactVariantObject( value, &.{ "kind", "user", "log_path", "expect_url", "url", "background_record_id" }, &.{ "kind", "user", "log_path", "expect_url", "url", "background_record_id", "assistant", "execution" }, ); const object = shape.object; const user = try parseUserTurn(alloc, object.get("user") orelse return error.InvalidSessionFormat); errdefer session.freeUserTurn(alloc, user); const legacy_assistant = if (shape.extended) try parseOptionalDurableBytes(alloc, object.get("assistant") orelse return error.InvalidSessionFormat) else null; defer if (legacy_assistant) |owned| mem_utils.free(alloc, owned); const execution = if (shape.extended) try parseExecutionMemory(alloc, object.get("execution") orelse return error.InvalidSessionFormat) else session.ExecutionMemory{}; errdefer session.freeExecutionMemory(alloc, execution); const log_path = try parseRequiredDurableBytes(alloc, object, "log_path"); defer mem_utils.free(alloc, log_path); const url = try parseOptionalDurableBytes(alloc, object.get("url") orelse return error.InvalidSessionFormat); defer if (url) |owned| mem_utils.free(alloc, owned); _ = try requireBool(object, "expect_url"); _ = try parseOptionalIdentifier(object.get("background_record_id") orelse return error.InvalidSessionFormat); 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 source = try requireObject(value); const has_execution = source.get("execution") != null; const has_presentation = source.get("cancelled_command") != null; const has_terminal_reason = source.get("terminal_reason") != null; const has_cancellation_origin = source.get("cancellation_origin") != null; const object = try exactInterruptedHistoryObject( value, has_execution, has_presentation, has_terminal_reason, has_cancellation_origin, ); const user = try parseUserTurn(alloc, object.get("user") orelse return error.InvalidSessionFormat); errdefer session.freeUserTurn(alloc, user); const assistant = try parseOptionalDurableBytes(alloc, object.get("assistant") orelse return error.InvalidSessionFormat); errdefer if (assistant) |owned| mem_utils.free(alloc, owned); const tool_call = try parseOptionalToolCall(alloc, object.get("tool_call") orelse return error.InvalidSessionFormat); errdefer if (tool_call) |call| session.freeToolCall(alloc, call); const completed_tool_names = try parseDurableBytesArray( alloc, object.get("completed_tool_names") orelse return error.InvalidSessionFormat, ); errdefer session.freeCompletedToolNames(alloc, completed_tool_names); const terminal_reason = if (has_terminal_reason) try parseInterruptedTerminalReason(object, "terminal_reason") else types.InterruptedTerminalReason.cancelled; const cancellation_origin: types.CancellationOrigin = if (has_cancellation_origin) std.meta.stringToEnum(types.CancellationOrigin, try requireString(object, "cancellation_origin")) orelse return error.InvalidSessionFormat else .turn; const execution = if (has_execution) try parseExecutionMemory(alloc, object.get("execution") orelse return error.InvalidSessionFormat) else session.ExecutionMemory{}; errdefer session.freeExecutionMemory(alloc, execution); const cancelled_command = if (has_presentation) try parseCancelledCommandPresentation( alloc, object.get("cancelled_command") orelse return error.InvalidSessionFormat, ) else null; errdefer if (cancelled_command) |presentation| { types.freeCancelledCommandPresentation(alloc, presentation); }; if (cancelled_command != null and (terminal_reason != .cancelled or tool_call == null or !try isCapturedCommandToolCall(alloc, tool_call.?))) { return error.InvalidSessionFormat; } return .{ .interrupted = .{ .user = user, .assistant = assistant, .tool_call = tool_call, .completed_tool_names = completed_tool_names, .execution = execution, .cancelled_command = cancelled_command, .terminal_reason = terminal_reason, .cancellation_origin = cancellation_origin, } }; } return error.InvalidSessionFormat; } fn isCapturedCommandToolCall(alloc: Allocator, call: types.ToolCall) !bool { return captured_command.isToolCall(alloc, call.name, call.arguments_json); } pub fn validateState(state: DurableSessionState) !void { return validateStateWithPermissionMigration(state, false); } fn validateStateWithPermissionMigration( state: DurableSessionState, allow_legacy_permission_state: bool, ) !void { try validateSessionId(state.id); try validateWorkspaceRoot(state.origin_workspace_root); try validateWorkspaceRoot(state.workspace_root); if (state.created_at_ms < 0 or state.updated_at_ms < 0) return error.InvalidDurableField; try validateConversationLanguage(state.conversation_language); try validateModel(state.preferences.model); if (state.context_history_start > state.history.len) return error.InvalidDurableField; if (state.last_subagent_work_id) |id| { session.validateWorkId(id) catch return error.InvalidDurableField; } for (state.history) |turn| { if (session.historyTurnWorkId(turn)) |id| { session.validateWorkId(id) catch return error.InvalidDurableField; } if (types.historyTurnSummary(turn)) |summary| { if (summary.started_at_ms < 0 or summary.completed_at_ms < summary.started_at_ms) { return error.InvalidDurableField; } } } if (state.usage) |usage| try session_usage.validateSnapshot(usage); if (allow_legacy_permission_state and state.permission_state.version == 1) { session_permission_state.validateSchema(state.permission_state, 1) catch return error.InvalidDurableField; } else { session_permission_state.validate(state.permission_state) catch return error.InvalidDurableField; } if (state.recovery_checkpoint) |checkpoint| { if (checkpoint.version != 2 or checkpoint.turn_id == 0 or checkpoint.max_provider_attempts == 0 or checkpoint.consumed_provider_attempts > checkpoint.max_provider_attempts or (checkpoint.outstanding_reservation and checkpoint.consumed_provider_attempts >= checkpoint.max_provider_attempts)) { return error.InvalidDurableField; } try validateModel(checkpoint.authority.model); if (checkpoint.authority.credential_identity != null and checkpoint.authority.credential_source == null) { return error.InvalidDurableField; } if (checkpoint.authority.credential_source) |source| { if (!model_provider.authorizesCredential(checkpoint.authority.provider, source)) { return error.InvalidDurableField; } } } } pub fn validateModelPreference(value: []const u8) !void { try validateModel(value); } pub fn parseConversationLanguage(raw: []const u8) !session.ConversationLanguage { try validateConversationLanguageBytes(raw); return session.ConversationLanguage.fromSlice(raw) catch error.InvalidDurableField; } fn writeState(writer: *std.Io.Writer, state: DurableSessionState) !void { try writer.writeAll("{\"id\":"); try writeJsonString(writer, state.id); try writer.writeAll(",\"origin_workspace_root\":"); try writeJsonString(writer, state.origin_workspace_root); try writer.writeAll(",\"workspace_root\":"); try writeJsonString(writer, state.workspace_root); try writer.print(",\"created_at_ms\":{d},\"updated_at_ms\":{d}", .{ state.created_at_ms, state.updated_at_ms, }); try writer.writeAll(",\"conversation_language\":"); try writeJsonString(writer, state.conversation_language.view()); try writer.writeAll(",\"preferences\":{\"model\":"); try writeJsonString(writer, state.preferences.model); try writer.writeAll(",\"effort\":"); try writeJsonString(writer, state.preferences.effort.label()); try writer.print(",\"fast_mode\":{s},\"provider\":", .{ if (state.preferences.fast_mode) "true" else "false", }); try std.json.Stringify.value(state.preferences.provider, .{}, writer); try writer.writeAll("},\"history\":["); for (state.history, 0..) |turn, i| { if (i > 0) try writer.writeByte(','); try writeHistoryTurn(writer, turn); } try writer.print("],\"total_input_tokens\":{d},\"total_output_tokens\":{d},\"context_history_start\":{d}", .{ state.total_input_tokens, state.total_output_tokens, state.context_history_start, }); try writer.writeAll(",\"permission_state\":"); try writePermissionState(writer, state.permission_state); if (state.usage) |usage| { try writer.writeAll(",\"usage\":"); try session_usage.writeSnapshot(writer, usage); } if (state.last_subagent_work_id) |id| { try writer.writeAll(",\"last_subagent_work_id\":"); try writeJsonString(writer, id); } if (state.subagent_child) { try writer.writeAll(",\"subagent_child\":true"); } if (state.recovery_checkpoint) |checkpoint| { try writer.writeAll(",\"recovery_checkpoint\":"); try writeRecoveryCheckpoint(writer, checkpoint); } try writer.writeByte('}'); } fn writePermissionState( writer: *std.Io.Writer, state: session_permission_state.State, ) !void { try writer.print("{{\"schema_version\":{d},\"next_generation\":{d},\"rules\":[", .{ state.version, state.next_generation, }); for (state.rules.items, 0..) |rule, index| { if (index > 0) try writer.writeByte(','); try writer.print("{{\"id\":{d},\"kind\":", .{rule.id.value}); try writeJsonString(writer, @tagName(rule.key.kind)); try writer.writeAll(",\"canonical\":"); try writeDurableBytes(writer, rule.key.canonical); try writer.writeAll(",\"display_identity\":"); try writeDurableBytes(writer, rule.display_identity); try writer.writeAll(",\"decision\":"); try writeJsonString(writer, @tagName(rule.decision)); try writer.print(",\"generation\":{d}}}", .{rule.generation}); } try writer.writeAll("]}"); } fn parsePermissionState( alloc: Allocator, value: std.json.Value, ) !session_permission_state.State { const object = try exactObject(value, &.{ "schema_version", "next_generation", "rules", }); const version = std.math.cast( u8, try requireU64(object, "schema_version"), ) orelse return error.InvalidDurableField; const rules_value = object.get("rules") orelse return error.InvalidSessionFormat; if (rules_value != .array) return error.InvalidSessionFormat; var state: session_permission_state.State = .{ .version = version, .next_generation = try requireU64(object, "next_generation"), }; errdefer state.deinit(alloc); try state.rules.ensureTotalCapacity( alloc, @min(rules_value.array.items.len, session_permission_state.max_rules), ); for (rules_value.array.items) |rule_value| { if (state.rules.items.len == session_permission_state.max_rules) { return error.InvalidDurableField; } const rule_object = try exactObject(rule_value, &.{ "id", "kind", "canonical", "display_identity", "decision", "generation", }); const kind = std.meta.stringToEnum( session_permission_state.RuleKey.Kind, try requireString(rule_object, "kind"), ) orelse return error.InvalidDurableField; const canonical = try parseRequiredDurableBytes( alloc, rule_object, "canonical", ); errdefer alloc.free(canonical); const key = session_permission_state.RuleKey.init( kind, canonical, ) catch return error.InvalidDurableField; const display_identity = try parseRequiredDurableBytes( alloc, rule_object, "display_identity", ); errdefer alloc.free(display_identity); const decision = std.meta.stringToEnum( session_permission_state.Decision, try requireString(rule_object, "decision"), ) orelse return error.InvalidDurableField; try state.rules.append(alloc, .{ .id = .{ .value = try requireU64(rule_object, "id") }, .key = key, .display_identity = display_identity, .decision = decision, .generation = try requireU64(rule_object, "generation"), }); } session_permission_state.validateSchema(state, version) catch return error.InvalidDurableField; return state; } pub fn writeRecoveryCheckpoint(writer: *std.Io.Writer, checkpoint: RecoveryCheckpoint) !void { if (checkpoint.disposition == .history_only) return error.InvalidDurableField; try writer.print("{{\"version\":{d},\"turn_id\":{d},\"user\":", .{ checkpoint.version, checkpoint.turn_id, }); try writeUserTurn(writer, checkpoint.user); try writer.writeAll(",\"assistant_source\":"); try writeDurableBytes(writer, checkpoint.assistant_source); try writer.writeAll(",\"execution\":"); try writeExecutionMemory(writer, checkpoint.execution); try writer.writeAll(",\"cause\":"); try writeJsonString(writer, @tagName(checkpoint.cause)); try writer.writeAll(",\"action\":"); try writeJsonString(writer, @tagName(checkpoint.action)); try writer.writeAll(",\"tool_state\":"); try writeJsonString(writer, @tagName(checkpoint.tool_state)); try writer.writeAll(",\"authority\":{\"provider\":"); try std.json.Stringify.value(checkpoint.authority.provider, .{}, writer); try writer.writeAll(",\"model\":"); try writeDurableBytes(writer, checkpoint.authority.model); try writer.writeAll(",\"credential_source\":"); if (checkpoint.authority.credential_source) |source| { try writeJsonString(writer, @tagName(source)); } else { try writer.writeAll("null"); } try writer.writeAll(",\"credential_identity\":"); if (checkpoint.authority.credential_identity) |identity| { const hex = std.fmt.bytesToHex(identity.bytes, .lower); try writeJsonString(writer, &hex); } else { try writer.writeAll("null"); } try writer.writeByte('}'); try writer.print(",\"requested_fast_mode\":{s},\"fast_mode\":{s},\"max_provider_attempts\":{d},\"consumed_provider_attempts\":{d},\"outstanding_reservation\":{s}}}", .{ if (checkpoint.requested_fast_mode) "true" else "false", if (checkpoint.fast_mode) "true" else "false", checkpoint.max_provider_attempts, checkpoint.consumed_provider_attempts, if (checkpoint.outstanding_reservation) "true" else "false", }); } fn decodeStateImpl(alloc: Allocator, source: *std.Io.Reader, limits: DecodeLimits, legacy_usage: bool) !DurableSessionState { var json_reader = std.json.Reader.init(alloc, source); defer json_reader.deinit(); try expectToken(try json_reader.next(), .object_begin); try expectKey(&json_reader, alloc, "id"); const id = try readStringOwned(&json_reader, alloc, 255); errdefer mem_utils.free(alloc, id); try expectKey(&json_reader, alloc, "origin_workspace_root"); const origin_workspace_root = try readStringOwned(&json_reader, alloc, std.Io.Dir.max_path_bytes); errdefer mem_utils.free(alloc, origin_workspace_root); try expectKey(&json_reader, alloc, "workspace_root"); const workspace_root = try readStringOwned(&json_reader, alloc, std.Io.Dir.max_path_bytes); errdefer mem_utils.free(alloc, workspace_root); try expectKey(&json_reader, alloc, "created_at_ms"); const created_at_ms = try readI64(&json_reader, alloc); try expectKey(&json_reader, alloc, "updated_at_ms"); const updated_at_ms = try readI64(&json_reader, alloc); try expectKey(&json_reader, alloc, "conversation_language"); const language_raw = try readStringOwned(&json_reader, alloc, session.ConversationLanguage.max_len); defer mem_utils.free(alloc, language_raw); const conversation_language = try parseConversationLanguage(language_raw); try expectKey(&json_reader, alloc, "preferences"); var preferences = preferences: { var buffer: [8 * 1024]u8 = undefined; var fixed = std.heap.FixedBufferAllocator.init(&buffer); const value = std.json.Value.jsonParse(fixed.allocator(), &json_reader, .{ .max_value_len = 1024, .allocate = .alloc_always, }) catch |err| switch (err) { error.OutOfMemory => return error.InvalidSessionFormat, else => return err, }; break :preferences try parse_preferences(alloc, value); }; errdefer preferences.deinit(alloc); try expectKey(&json_reader, alloc, "history"); try expectToken(try json_reader.next(), .array_begin); var history: std.ArrayList(session.HistoryTurn) = .empty; errdefer { for (history.items) |turn| session.freeHistoryTurn(alloc, turn); mem_utils.deinitList(alloc, &history); } while (try json_reader.peekNextTokenType() != .array_end) { if (history.items.len == limits.max_history_turns) return error.InvalidSessionFormat; var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const value = try std.json.Value.jsonParse(arena.allocator(), &json_reader, .{ .max_value_len = limits.max_value_bytes, .allocate = .alloc_always, .parse_numbers = false, }); const turn = try parseHistoryTurn(alloc, value); errdefer session.freeHistoryTurn(alloc, turn); try history.append(alloc, turn); } try expectToken(try json_reader.next(), .array_end); try expectKey(&json_reader, alloc, "total_input_tokens"); const total_input_tokens = try readU64(&json_reader, alloc); try expectKey(&json_reader, alloc, "total_output_tokens"); const total_output_tokens = try readU64(&json_reader, alloc); var context_history_start: usize = 0; var context_seen = false; var permission_state: session_permission_state.State = .{}; errdefer permission_state.deinit(alloc); var permission_state_seen = false; var usage: ?session_usage.Snapshot = null; errdefer if (usage) |*snapshot| snapshot.deinit(alloc); var usage_seen = false; var last_subagent_work_id: ?[]u8 = null; errdefer if (last_subagent_work_id) |work_id| mem_utils.free(alloc, work_id); var subagent_child = false; var subagent_child_seen = false; var recovery_checkpoint: ?RecoveryCheckpoint = null; errdefer if (recovery_checkpoint) |*checkpoint| checkpoint.deinit(alloc); while (try json_reader.peekNextTokenType() != .object_end) { const key = try readStringOwned(&json_reader, alloc, 64); defer mem_utils.free(alloc, key); if (std.mem.eql(u8, key, "context_history_start")) { if (context_seen or permission_state_seen or usage_seen or last_subagent_work_id != null or subagent_child_seen or recovery_checkpoint != null) { return error.InvalidSessionFormat; } const raw = try readU64(&json_reader, alloc); context_history_start = std.math.cast(usize, raw) orelse return error.InvalidSessionFormat; context_seen = true; } else if (std.mem.eql(u8, key, "permission_state")) { if (permission_state_seen or usage_seen or last_subagent_work_id != null or subagent_child_seen or recovery_checkpoint != null) { return error.InvalidSessionFormat; } var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const value = try std.json.Value.jsonParse(arena.allocator(), &json_reader, .{ .max_value_len = limits.max_value_bytes, .allocate = .alloc_always, .parse_numbers = false, }); permission_state = try parsePermissionState(alloc, value); permission_state_seen = true; } else if (std.mem.eql(u8, key, "usage")) { if (usage_seen or last_subagent_work_id != null or subagent_child_seen or recovery_checkpoint != null) return error.InvalidSessionFormat; const parse_limit = @min( limits.max_value_bytes, session_usage.max_snapshot_bytes, ); const parse_buffer = try alloc.alloc(u8, parse_limit); defer mem_utils.free(alloc, parse_buffer); var fixed = std.heap.FixedBufferAllocator.init(parse_buffer); const value = try std.json.Value.jsonParse(fixed.allocator(), &json_reader, .{ .max_value_len = parse_limit, .allocate = .alloc_always, .parse_numbers = false, }); usage = if (legacy_usage) try session_usage.parseLegacySnapshotValue(alloc, value) else try session_usage.parseSnapshotValue(alloc, value); usage_seen = true; } else if (std.mem.eql(u8, key, "last_subagent_work_id")) { if (last_subagent_work_id != null or subagent_child_seen or recovery_checkpoint != null) return error.InvalidSessionFormat; last_subagent_work_id = try readStringOwned(&json_reader, alloc, 128); } else if (std.mem.eql(u8, key, "subagent_child")) { if (subagent_child_seen or recovery_checkpoint != null) return error.InvalidSessionFormat; subagent_child = try readBool(&json_reader); if (!subagent_child) return error.InvalidDurableField; subagent_child_seen = true; } else if (std.mem.eql(u8, key, "recovery_checkpoint")) { if (recovery_checkpoint != null) return error.InvalidSessionFormat; var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const value = try std.json.Value.jsonParse(arena.allocator(), &json_reader, .{ .max_value_len = limits.max_value_bytes, .allocate = .alloc_always, .parse_numbers = false, }); recovery_checkpoint = try parseRecoveryCheckpoint(alloc, value); } else return error.InvalidSessionFormat; } try expectToken(try json_reader.next(), .object_end); try expectToken(try json_reader.next(), .end_of_document); const owned_history = try history.toOwnedSlice(alloc); errdefer session.freeHistoryTurnSlice(alloc, owned_history); const state = DurableSessionState{ .id = id, .origin_workspace_root = origin_workspace_root, .workspace_root = workspace_root, .created_at_ms = created_at_ms, .updated_at_ms = updated_at_ms, .conversation_language = conversation_language, .preferences = preferences, .history = owned_history, .context_history_start = context_history_start, .total_input_tokens = total_input_tokens, .total_output_tokens = total_output_tokens, .permission_state = permission_state, .last_subagent_work_id = last_subagent_work_id, .subagent_child = subagent_child, .usage = usage, .recovery_checkpoint = recovery_checkpoint, }; try validateStateWithPermissionMigration(state, true); return state; } pub fn parseRecoveryCheckpoint(alloc: Allocator, value: std.json.Value) !RecoveryCheckpoint { const raw_object = try requireObject(value); const version = try requireU64(raw_object, "version"); const legacy_route = raw_object.contains("route_identity"); const object = if (legacy_route) legacy: { try validate_legacy_recovery_route(raw_object.get("route_identity").?, version); const delivery = try requireString(raw_object, "delivery"); if (!std.mem.eql(u8, delivery, "possibly_sent") and !std.mem.eql(u8, delivery, "definitely_unsent")) return error.InvalidDurableField; break :legacy try exactObject(value, &.{ "version", "route_identity", "delivery", "turn_id", "user", "assistant_source", "execution", "cause", "action", "tool_state", "route_model", "requested_fast_mode", "fast_mode", "max_provider_attempts", "consumed_provider_attempts", "outstanding_reservation", }); } else switch (version) { 1 => if (raw_object.get("route_provider") != null) try exactObject(value, &.{ "version", "turn_id", "user", "assistant_source", "execution", "cause", "action", "tool_state", "route_model", "route_provider", "requested_fast_mode", "fast_mode", "max_provider_attempts", "consumed_provider_attempts", "outstanding_reservation", }) else try exactObject(value, &.{ "version", "turn_id", "user", "assistant_source", "execution", "cause", "action", "tool_state", "route_model", "requested_fast_mode", "fast_mode", "max_provider_attempts", "consumed_provider_attempts", "outstanding_reservation", }), 2 => try exactObject(value, &.{ "version", "turn_id", "user", "assistant_source", "execution", "cause", "action", "tool_state", "authority", "requested_fast_mode", "fast_mode", "max_provider_attempts", "consumed_provider_attempts", "outstanding_reservation", }), else => return error.InvalidDurableField, }; const user = try parseUserTurn(alloc, object.get("user") orelse return error.InvalidSessionFormat); errdefer session.freeUserTurn(alloc, user); const assistant_source = try parseDurableBytes(alloc, object.get("assistant_source") orelse return error.InvalidSessionFormat); errdefer alloc.free(assistant_source); const execution = try parseExecutionMemory(alloc, object.get("execution") orelse return error.InvalidSessionFormat); errdefer session.freeExecutionMemory(alloc, execution); const authority = if (legacy_route) TurnAuthority{ .provider = .gateway, .model = try parseDurableBytes(alloc, object.get("route_model") orelse return error.InvalidSessionFormat), } else if (version == 1) legacy: { const model = try parseDurableBytes(alloc, object.get("route_model") orelse return error.InvalidSessionFormat); break :legacy TurnAuthority{ .provider = if (object.get("route_provider")) |provider_value| blk: { if (provider_value != .string) return error.InvalidDurableField; break :blk model_provider.parse(provider_value.string) orelse return error.InvalidDurableField; } else .gateway, .model = model, }; } else try parseTurnAuthority(alloc, object.get("authority") orelse return error.InvalidSessionFormat); errdefer { var owned_authority = authority; owned_authority.deinit(alloc); } const max_provider_attempts = std.math.cast(usize, try requireU64(object, "max_provider_attempts")) orelse return error.InvalidDurableField; const consumed_provider_attempts = std.math.cast(usize, try requireU64(object, "consumed_provider_attempts")) orelse return error.InvalidDurableField; return .{ .version = 2, .disposition = if (legacy_route) .history_only else .continuable, .turn_id = try requireU64(object, "turn_id"), .user = user, .assistant_source = assistant_source, .execution = execution, .cause = std.meta.stringToEnum(types.ModelRecoveryCause, try requireString(object, "cause")) orelse return error.InvalidDurableField, .action = std.meta.stringToEnum(types.ModelRecoveryAction, try requireString(object, "action")) orelse return error.InvalidDurableField, .tool_state = std.meta.stringToEnum(RecoveryToolState, try requireString(object, "tool_state")) orelse return error.InvalidDurableField, .authority = authority, .requested_fast_mode = try requireBool(object, "requested_fast_mode"), .fast_mode = try requireBool(object, "fast_mode"), .max_provider_attempts = max_provider_attempts, .consumed_provider_attempts = consumed_provider_attempts, .outstanding_reservation = try requireBool(object, "outstanding_reservation"), }; } test "legacy route checkpoints retain history without resumable authority" { const alloc = std.testing.allocator; const routes = [_][]const u8{ "{\"connection_id\":\"vercel\",\"adapter_kind\":\"vercel_ai_gateway\",\"permission_review_model_id\":\"review\"}", "{\"connection_id\":\"vercel\",\"adapter_kind\":\"vercel_ai_gateway\",\"permission_review_model_id\":\"review\",\"vision_model_id\":\"vision\",\"subagent_model_id\":\"child\"}", "{\"version\":1,\"connection_id\":\"vercel\",\"adapter_kind\":\"vercel_ai_gateway\",\"endpoint\":\"https://unused.invalid\",\"protocol\":\"vercel_ai_gateway\",\"credential_ref\":\"unused\",\"permission_review_model_id\":\"review\",\"vision_model_id\":\"vision\",\"subagent_model_id\":\"child\"}", }; const fields = "\"turn_id\":1,\"user\":{\"text\":\"saved request\",\"images\":[]},\"assistant_source\":\"saved partial\",\"execution\":{\"schema_version\":3,\"tool_steps\":[],\"files\":[]},\"cause\":\"response_interrupted\",\"action\":\"continuing_response\",\"tool_state\":\"uncertain\",\"route_model\":\"test/model\",\"requested_fast_mode\":false,\"fast_mode\":false,\"max_provider_attempts\":3,\"consumed_provider_attempts\":0,\"outstanding_reservation\":false}"; for (routes, 2..) |route, version| { for ([_][]const u8{ "possibly_sent", "definitely_unsent" }) |delivery| { const bytes = try std.fmt.allocPrint(alloc, "{{\"version\":{d},\"route_identity\":{s},\"delivery\":\"{s}\",{s}", .{ version, route, delivery, fields }); defer alloc.free(bytes); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, bytes, .{}); defer parsed.deinit(); var checkpoint = try parseRecoveryCheckpoint(alloc, parsed.value); defer checkpoint.deinit(alloc); try std.testing.expectEqual(.history_only, checkpoint.disposition); try std.testing.expect(checkpoint.authority.credential_source == null); try std.testing.expect(checkpoint.authority.credential_identity == null); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try std.testing.expectError(error.InvalidDurableField, writeRecoveryCheckpoint(&out.writer, checkpoint)); try std.testing.expectEqual(@as(usize, 0), out.written().len); const borrowed = DurableSessionState{ .id = @constCast("legacy"), .origin_workspace_root = @constCast("/workspace"), .workspace_root = @constCast("/workspace"), .created_at_ms = 1, .updated_at_ms = 2, .conversation_language = .literal("en"), .preferences = .{ .model = @constCast("test/model"), .effort = .auto, .fast_mode = false }, .history = &.{}, .total_input_tokens = 0, .total_output_tokens = 0, .recovery_checkpoint = checkpoint, }; try std.testing.checkAllAllocationFailures(alloc, struct { fn check(a: Allocator, original: DurableSessionState) !void { var state = try original.dupe(a); defer state.deinit(a); try std.testing.expect(try state.archive_legacy_recovery(a)); try std.testing.expect(!(try state.archive_legacy_recovery(a))); try std.testing.expect(state.recovery_checkpoint == null); try std.testing.expectEqual(@as(usize, 1), state.history.len); try std.testing.expectEqualStrings("saved request", state.history[0].interrupted.user.text); try std.testing.expectEqualStrings("saved partial", state.history[0].interrupted.assistant.?); } }.check, .{borrowed}); } } } fn validate_legacy_recovery_route(value: std.json.Value, version: u64) !void { const object = switch (version) { 2 => try exactObject(value, &.{ "connection_id", "adapter_kind", "permission_review_model_id" }), 3 => try exactObject(value, &.{ "connection_id", "adapter_kind", "permission_review_model_id", "vision_model_id", "subagent_model_id" }), 4 => try exactObject(value, &.{ "version", "connection_id", "adapter_kind", "endpoint", "protocol", "credential_ref", "permission_review_model_id", "vision_model_id", "subagent_model_id" }), else => return error.InvalidDurableField, }; if (!std.mem.eql(u8, try requireString(object, "connection_id"), "vercel") or !std.mem.eql(u8, try requireString(object, "adapter_kind"), "vercel_ai_gateway")) return error.InvalidDurableField; if (version == 4 and (try requireU64(object, "version") != 1 or !std.mem.eql(u8, try requireString(object, "protocol"), "vercel_ai_gateway"))) return error.InvalidDurableField; var fields = object.iterator(); while (fields.next()) |field| { if (std.mem.eql(u8, field.key_ptr.*, "version")) continue; if (field.value_ptr.* != .string or field.value_ptr.string.len > 4096 or !std.unicode.utf8ValidateSlice(field.value_ptr.string)) return error.InvalidDurableField; } } fn parseTurnAuthority(alloc: Allocator, value: std.json.Value) !TurnAuthority { const object = try exactObject(value, &.{ "provider", "model", "credential_source", "credential_identity", }); const provider = model_provider.parse_saved(object.get("provider").?) catch return error.InvalidDurableField; const model = try parseDurableBytes(alloc, object.get("model") orelse return error.InvalidSessionFormat); errdefer alloc.free(model); const credential_source = if (object.get("credential_source")) |source| switch (source) { .null => null, .string => |text| types.parseRuntimeCredentialSource(text) orelse return error.InvalidDurableField, else => return error.InvalidDurableField, } else return error.InvalidSessionFormat; const credential_identity = if (object.get("credential_identity")) |identity| switch (identity) { .null => null, .string => |hex| identity: { var bytes: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; _ = std.fmt.hexToBytes(&bytes, hex) catch return error.InvalidDurableField; const canonical = std.fmt.bytesToHex(bytes, .lower); if (!std.mem.eql(u8, &canonical, hex)) return error.InvalidDurableField; break :identity credential_authority.Identity{ .bytes = bytes }; }, else => return error.InvalidDurableField, } else return error.InvalidSessionFormat; if (credential_identity != null and credential_source == null) { return error.InvalidDurableField; } return .{ .provider = provider, .model = model, .credential_source = credential_source, .credential_identity = credential_identity, }; } fn writeUserTurn(writer: *std.Io.Writer, user: session.UserTurn) !void { try writer.writeAll("{\"text\":"); try writeDurableBytes(writer, user.text); try writer.writeAll(",\"images\":["); for (user.images, 0..) |image, i| { if (i > 0) try writer.writeByte(','); try writer.print("{{\"id\":{d},\"path\":", .{image.id}); try writeDurableBytes(writer, image.path); try writer.writeAll(",\"media_type\":"); try writeDurableBytes(writer, image.media_type); try writer.writeAll(",\"snapshot_path\":"); try writeSnapshotLocator(writer, image.snapshot_path); try writer.writeAll(",\"snapshot_sha256\":"); try writeOptionalDurableBytes(writer, image.snapshot_sha256); try writer.writeByte('}'); } try writer.writeByte(']'); if (user.work_id) |work_id| { try writer.writeAll(",\"work_id\":"); try writeJsonString(writer, work_id); } try writer.writeByte('}'); } fn writeSnapshotLocator(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 writeDurableBytes(writer, locator); } fn writeExecutionMemory(writer: *std.Io.Writer, execution: session.ExecutionMemory) !void { try writer.writeAll("{\"schema_version\":10,\"tool_steps\":["); for (execution.tool_steps, 0..) |step, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"assistant\":"); try writeOptionalDurableBytes(writer, step.assistant); try writer.writeAll(",\"provider_replay\":"); try std.json.Stringify.value(step.provider_replay, .{}, writer); try writer.writeAll(",\"tool_calls\":["); for (step.tool_calls, 0..) |tool_call, call_index| { if (call_index > 0) try writer.writeByte(','); try writeToolCall(writer, tool_call); } try writer.writeAll("],\"tool_results\":["); for (step.tool_results, 0..) |result, result_index| { if (result_index > 0) try writer.writeByte(','); try writePersistedToolResult(writer, result); } try writer.writeAll("]}"); } try writer.writeAll("],\"files\":["); for (execution.files, 0..) |file, i| { if (i > 0) try writer.writeByte(','); try writeFileEvidence(writer, file); } try writer.writeAll("],\"steering\":["); for (execution.steering, 0..) |steering, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"text\":"); try writeDurableBytes(writer, steering.text); try writer.writeAll(",\"assistant_prefix\":"); try writeOptionalDurableBytes(writer, steering.assistant_prefix); try writer.print(",\"after_tool_step_count\":{d}}}", .{steering.after_tool_step_count}); } try writer.writeAll("],\"turn_summary\":"); if (execution.turn_summary) |summary| { try writeTurnSummary(writer, summary); } else { try writer.writeAll("null"); } try writer.writeByte('}'); } fn hasDurableExecutionMemory(execution: session.ExecutionMemory) bool { return !execution.isEmpty() or execution.turn_summary != null; } fn writeTurnSummary(writer: *std.Io.Writer, summary: types.TurnSummary) !void { try writer.print( "{{\"started_at_ms\":{d},\"completed_at_ms\":{d},\"thinking_duration_ms\":{d},\"turn_duration_ms\":{d},\"token_progress\":{{\"input_tokens\":{d},\"output_tokens\":{d},\"input_exact\":{s},\"output_exact\":{s}}}}}", .{ summary.started_at_ms, summary.completed_at_ms, summary.thinking_duration_ms, summary.turn_duration_ms, summary.token_progress.input_tokens, summary.token_progress.output_tokens, if (summary.token_progress.input_exact) "true" else "false", if (summary.token_progress.output_exact) "true" else "false", }, ); } fn writeToolCall(writer: *std.Io.Writer, tool_call: session.ToolCall) !void { try writer.writeAll("{\"id\":"); try writeDurableBytes(writer, tool_call.id); try writer.writeAll(",\"name\":"); try writeDurableBytes(writer, tool_call.name); try writer.writeAll(",\"arguments_json\":"); try writeDurableBytes(writer, tool_call.arguments_json); try writer.writeAll(",\"provider_result\":"); try writeOptionalDurableBytes(writer, tool_call.provider_result); try writer.writeByte('}'); } fn writePersistedToolResult(writer: *std.Io.Writer, result: session.PersistedToolResult) !void { if (result.review_feedback and (result.status != .failure or result.provider_native)) { return error.InvalidSessionFormat; } try writer.writeAll("{\"tool_call_id\":"); try writeDurableBytes(writer, result.tool_call_id); try writer.writeAll(",\"tool_name\":"); try writeDurableBytes(writer, result.tool_name); try writer.writeAll(",\"status\":"); try writeJsonString(writer, @tagName(result.status)); try writer.writeAll(",\"output\":"); try writeDurableBytes(writer, result.output); try writer.writeAll(",\"output_handle\":"); try writeOptionalDurableBytes(writer, result.output_handle); try writer.writeAll(",\"preview\":"); try writeOptionalDurableBytes(writer, result.preview); try writer.print( ",\"output_bytes\":{d},\"stored_output_bytes\":{d},\"truncated\":{s},\"provider_native\":{s},\"review_feedback\":{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", if (result.review_feedback) "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 writeDurableBytes(writer, feedback); } try writer.writeAll("],\"committed_file_presentation\":"); if (result.committed_file_presentation) |presentation| { try writeCommittedFilePresentation(writer, presentation); } else { try writer.writeAll("null"); } try writer.writeAll(",\"command_output_replay\":"); try writeOptionalCommandOutputReplay(writer, result.command_output_replay); try writer.writeAll(",\"command_process_presentation\":"); try writeOptionalCommandProcessPresentation( writer, result.command_process_presentation, ); try writer.writeAll(",\"terminal_action_presentation\":"); try writeOptionalTerminalActionPresentation( writer, result.terminal_action_presentation, ); if (result.tool_image_handle) |handle| { try writer.writeAll(",\"tool_image_handle\":"); try writeDurableBytes(writer, handle); } else if (result.tool_images.len > 0) { try writer.writeAll(",\"tool_images\":["); for (result.tool_images, 0..) |image, index| { if (index > 0) try writer.writeByte(','); try writer.writeAll("{\"type\":\"image\",\"mimeType\":"); try std.json.Stringify.value(image.mime_type, .{}, writer); try writer.writeAll(",\"data\":"); try std.json.Stringify.value(image.data, .{}, writer); try writer.writeByte('}'); } try writer.writeByte(']'); } try writer.writeByte('}'); } fn writeCancelledCommandPresentation( writer: *std.Io.Writer, presentation: session.CancelledCommandPresentation, ) !void { try writer.writeAll("{\"output_replay\":"); try writeOptionalCommandOutputReplay(writer, presentation.output_replay); try writer.writeAll(",\"command_artifact_handle\":"); try writeOptionalDurableBytes(writer, presentation.command_artifact_handle); try writer.writeByte('}'); } fn parseCancelledCommandPresentation( alloc: Allocator, value: std.json.Value, ) !session.CancelledCommandPresentation { const object = try exactObject(value, &.{ "output_replay", "command_artifact_handle", }); const output_replay = try parseOptionalCommandOutputReplay( alloc, object.get("output_replay") orelse return error.InvalidSessionFormat, ); errdefer if (output_replay) |replay| types.freeCommandOutputReplay(alloc, replay); return .{ .output_replay = output_replay, .command_artifact_handle = try parseOptionalDurableBytes( alloc, object.get("command_artifact_handle") orelse return error.InvalidSessionFormat, ), }; } fn writeOptionalCommandOutputReplay( writer: *std.Io.Writer, replay: ?types.CommandOutputReplay, ) !void { const value = replay orelse { try writer.writeAll("null"); return; }; switch (value) { .available => |descriptor| { try writer.writeAll("{\"kind\":\"available\",\"handle\":"); try writeDurableBytes(writer, descriptor.handle); try writer.print(",\"framed_bytes\":{d}}}", .{descriptor.framed_bytes}); }, .unavailable => try writer.writeAll("{\"kind\":\"unavailable\"}"), } } fn writeOptionalCommandProcessPresentation( writer: *std.Io.Writer, presentation: ?types.CommandProcessPresentation, ) !void { const value = presentation orelse { try writer.writeAll("null"); return; }; switch (value) { .exit_code => |exit_code| try writer.print( "{{\"kind\":\"exit_code\",\"value\":{d}}}", .{exit_code}, ), .signal => |signal| try writer.print( "{{\"kind\":\"signal\",\"value\":{d}}}", .{signal}, ), .timed_out => try writer.writeAll("{\"kind\":\"timed_out\",\"value\":null}"), .output_capture_failed => try writer.writeAll("{\"kind\":\"output_capture_failed\",\"value\":null}"), } } fn writeOptionalTerminalActionPresentation( writer: *std.Io.Writer, presentation: ?types.TerminalActionPresentation, ) !void { const value = presentation orelse { try writer.writeAll("null"); return; }; switch (value) { .returned => |returned| { try writer.writeAll("{\"kind\":\"returned\",\"outcome\":"); try writeTerminalReturnPresentation(writer, returned); try writer.writeByte('}'); }, .failed => |failed| { try writer.writeAll("{\"kind\":\"failed\",\"code\":"); try writeJsonString(writer, @tagName(failed)); try writer.writeByte('}'); }, } } fn writeTerminalReturnPresentation( writer: *std.Io.Writer, outcome: types.TerminalReturnPresentation, ) !void { switch (outcome) { .started, .condition_met, .safety_ceiling, .cancelled => { try writer.writeAll("{\"kind\":"); try writeJsonString(writer, @tagName(outcome)); try writer.writeAll(",\"value\":null}"); }, .exited => |code| try writer.print( "{{\"kind\":\"exited\",\"value\":{d}}}", .{code}, ), .signal => |signal| try writer.print( "{{\"kind\":\"signal\",\"value\":{d}}}", .{signal}, ), } } fn writeCommittedFilePresentation( writer: *std.Io.Writer, presentation: types.CommittedFilePresentation, ) !void { try writer.writeAll("{\"path\":"); try writeDurableBytes(writer, presentation.path); try writer.writeAll(",\"kind\":"); try writeJsonString(writer, @tagName(presentation.kind)); try writer.writeAll(",\"lines\":["); for (presentation.lines, 0..) |line, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"kind\":"); try writeJsonString(writer, @tagName(line.kind)); try writer.writeAll(",\"old_line\":"); try writeOptionalU32(writer, line.old_line); try writer.writeAll(",\"new_line\":"); try writeOptionalU32(writer, line.new_line); try writer.writeAll(",\"text\":"); try writeDurableBytes(writer, line.text); 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 writeOptionalDurableBytes(writer, presentation.previous_content); try writer.writeAll(",\"after_content\":"); try writeOptionalDurableBytes(writer, presentation.after_content); try writer.writeAll(",\"lifecycle_id\":"); if (presentation.lifecycle_id) |lifecycle_id| { try writer.print("{{\"turn_id\":{d},\"call_id\":", .{lifecycle_id.turn_id}); try writeDurableBytes(writer, lifecycle_id.call_id); try writer.writeByte('}'); } else { try writer.writeAll("null"); } try writer.writeByte('}'); } fn writeFileEvidence(writer: *std.Io.Writer, file: session.FileEvidence) !void { try writer.writeAll("{\"path\":"); try writeDurableBytes(writer, file.path); try writer.writeAll(",\"new_path\":"); try writeOptionalDurableBytes(writer, file.new_path); try writer.writeAll(",\"tool_call_id\":"); try writeDurableBytes(writer, file.tool_call_id); try writer.writeAll(",\"tool_name\":"); try writeDurableBytes(writer, file.tool_name); try writer.writeAll(",\"action\":"); try writeJsonString(writer, @tagName(file.action)); try writer.writeAll(",\"status\":"); try writeJsonString(writer, @tagName(file.status)); 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 parseUserTurn(alloc: Allocator, value: std.json.Value) !session.UserTurn { const source = try requireObject(value); const object = if (source.count() == 2) try exactObject(value, &.{ "text", "images" }) else try exactObject(value, &.{ "text", "images", "work_id" }); const text = try parseRequiredDurableBytes(alloc, object, "text"); errdefer mem_utils.free(alloc, text); const work_id = if (object.get("work_id")) |_| try alloc.dupe(u8, try requireString(object, "work_id")) else null; errdefer if (work_id) |id| mem_utils.free(alloc, id); if (work_id) |id| session.validateWorkId(id) catch return error.InvalidSessionFormat; const images_value = object.get("images") orelse return error.InvalidSessionFormat; if (images_value != .array) return error.InvalidSessionFormat; if (images_value.array.items.len == 0) return .{ .text = text, .work_id = work_id }; const images = try alloc.alloc(session.ImageAttachment, images_value.array.items.len); errdefer mem_utils.free(alloc, images); var parsed_count: usize = 0; errdefer { for (images[0..parsed_count]) |image| { types.freeImageAttachment(alloc, image); } } for (images_value.array.items, 0..) |image_value, i| { const image = try imageAttachmentObject(image_value); const path = try parseRequiredDurableBytes(alloc, image, "path"); errdefer mem_utils.free(alloc, path); const media_type = try parseRequiredDurableBytes(alloc, image, "media_type"); errdefer mem_utils.free(alloc, media_type); const snapshot_path = try parseOptionalDurableBytes( alloc, image.get("snapshot_path") orelse .null, ); errdefer if (snapshot_path) |path_bytes| mem_utils.free(alloc, path_bytes); const snapshot_sha256 = try parseOptionalDurableBytes( alloc, image.get("snapshot_sha256") orelse .null, ); errdefer if (snapshot_sha256) |sha256_bytes| mem_utils.free(alloc, sha256_bytes); if ((snapshot_path == null) != (snapshot_sha256 == null)) return error.InvalidSessionFormat; images[i] = .{ .id = try requireUsize(image, "id"), .path = path, .media_type = media_type, .snapshot_path = snapshot_path, .snapshot_sha256 = snapshot_sha256, }; parsed_count += 1; } return .{ .text = text, .images = images, .work_id = work_id }; } fn imageAttachmentObject(value: std.json.Value) !std.json.ObjectMap { if (value != .object) return error.InvalidSessionFormat; var has_id = false; var has_path = false; var has_media_type = false; var iterator = value.object.iterator(); while (iterator.next()) |entry| { if (std.mem.eql(u8, entry.key_ptr.*, "id")) { has_id = true; } else if (std.mem.eql(u8, entry.key_ptr.*, "path")) { has_path = true; } else if (std.mem.eql(u8, entry.key_ptr.*, "media_type")) { has_media_type = true; } else if (std.mem.eql(u8, entry.key_ptr.*, "snapshot_path") or std.mem.eql(u8, entry.key_ptr.*, "snapshot_sha256")) { continue; } else { return error.InvalidSessionFormat; } } if (!has_id or !has_path or !has_media_type) return error.InvalidSessionFormat; return value.object; } fn parseExecutionMemory(alloc: Allocator, value: std.json.Value) !session.ExecutionMemory { const source = try requireObject(value); const schema_version = try requireU64(source, "schema_version"); const object = switch (schema_version) { 1...4 => try exactObject(value, &.{ "schema_version", "tool_steps", "files" }), 5 => try exactObject(value, &.{ "schema_version", "tool_steps", "files", "turn_summary" }), 6 => try exactObject(value, &.{ "schema_version", "tool_steps", "files", "steering", "turn_summary" }), 7...10 => try exactObject(value, &.{ "schema_version", "tool_steps", "files", "steering", "turn_summary" }), else => return error.InvalidSessionFormat, }; const tool_steps = try parseToolSteps( alloc, object.get("tool_steps") orelse return error.InvalidSessionFormat, schema_version, ); errdefer session.freeExecutionMemory(alloc, .{ .tool_steps = tool_steps }); const files = try parseFiles(alloc, object.get("files") orelse return error.InvalidSessionFormat); errdefer types.freeFileEvidenceSlice(alloc, files); const steering: []types.PersistedSteering = if (schema_version >= 6) try parsePersistedSteering( alloc, object.get("steering") orelse return error.InvalidSessionFormat, schema_version, tool_steps.len, ) else &.{}; errdefer types.freePersistedSteering(alloc, steering); const turn_summary = if (schema_version >= 5) try parseOptionalTurnSummary(object.get("turn_summary").?) else null; return .{ .tool_steps = tool_steps, .files = files, .steering = steering, .turn_summary = turn_summary, }; } fn parsePersistedSteering( alloc: Allocator, value: std.json.Value, schema_version: u64, tool_step_count: usize, ) ![]types.PersistedSteering { 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 alloc.free(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 == 6) types.PersistedSteering{ .text = try parseDurableBytes(alloc, item), .after_tool_step_count = tool_step_count, } else blk: { const object = try exactObject(item, &.{ "text", "assistant_prefix", "after_tool_step_count" }); 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 parseDurableBytes( alloc, object.get("text") orelse return error.InvalidSessionFormat, ); const assistant_prefix = parseOptionalDurableBytes( alloc, object.get("assistant_prefix") orelse return error.InvalidSessionFormat, ) 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 parseOptionalTurnSummary(value: std.json.Value) !?types.TurnSummary { if (value == .null) return null; const object = try exactObject(value, &.{ "started_at_ms", "completed_at_ms", "thinking_duration_ms", "turn_duration_ms", "token_progress", }); const token_progress = try exactObject( object.get("token_progress") orelse return error.InvalidSessionFormat, &.{ "input_tokens", "output_tokens", "input_exact", "output_exact" }, ); const summary = types.TurnSummary{ .started_at_ms = try requireI64(object, "started_at_ms"), .completed_at_ms = try requireI64(object, "completed_at_ms"), .thinking_duration_ms = try requireU64(object, "thinking_duration_ms"), .turn_duration_ms = try requireU64(object, "turn_duration_ms"), .token_progress = .{ .input_tokens = try requireU64(token_progress, "input_tokens"), .output_tokens = try requireU64(token_progress, "output_tokens"), .input_exact = try requireBool(token_progress, "input_exact"), .output_exact = try requireBool(token_progress, "output_exact"), }, }; if (summary.started_at_ms < 0 or summary.completed_at_ms < summary.started_at_ms) { return error.InvalidSessionFormat; } return summary; } fn parseToolSteps( alloc: Allocator, value: std.json.Value, schema_version: u64, ) ![]session.ToolExecutionStep { 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 for (steps[0..parsed_count]) |step| { if (step.assistant) |assistant| mem_utils.free(alloc, assistant); if (step.provider_replay) |replay| types.freeProviderReplay(alloc, replay); session.freeToolCallSlice(alloc, step.tool_calls); session.freePersistedToolResults(alloc, step.tool_results); }; for (value.array.items, 0..) |step_value, i| { const object = if (schema_version >= 9) try exactObject(step_value, &.{ "assistant", "tool_calls", "tool_results", "provider_replay" }) else try exactObject(step_value, &.{ "assistant", "tool_calls", "tool_results" }); const assistant = try parseOptionalDurableBytes(alloc, object.get("assistant") orelse return error.InvalidSessionFormat); errdefer if (assistant) |owned| mem_utils.free(alloc, owned); const tool_calls = try parseToolCalls(alloc, object.get("tool_calls") orelse return error.InvalidSessionFormat); errdefer session.freeToolCallSlice(alloc, tool_calls); const tool_results = try parseToolResults( alloc, object.get("tool_results") orelse return error.InvalidSessionFormat, schema_version, ); errdefer session.freePersistedToolResults(alloc, tool_results); try session.repairPersistedToolArguments(alloc, tool_calls, tool_results, .schema_v3); const replay = try parseProviderReplay(alloc, object.get("provider_replay") orelse .null); steps[i] = .{ .assistant = assistant, .provider_replay = replay, .tool_calls = tool_calls, .tool_results = tool_results, }; parsed_count += 1; } return steps; } fn parseProviderReplay(alloc: Allocator, value: std.json.Value) !?types.ProviderReplay { if (value == .null) return null; const parsed = std.json.parseFromValue(types.ProviderReplay, alloc, value, .{}) catch |err| switch (err) { error.OutOfMemory => return err, else => return error.InvalidSessionFormat, }; defer parsed.deinit(); const replay = parsed.value; if (types.ConversationIdentity.invalidReason(replay.source.model) != null or replay.parts_json.len == 0 or replay.parts_json.len > types.ProviderReplay.max_bytes or !std.unicode.utf8ValidateSlice(replay.parts_json)) return error.InvalidSessionFormat; return try types.dupeProviderReplay(alloc, replay); } fn parseToolCalls(alloc: Allocator, value: std.json.Value) ![]session.ToolCall { 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 for (calls[0..parsed_count]) |call| session.freeToolCall(alloc, call); for (value.array.items, 0..) |call_value, i| { calls[i] = try parseToolCall(alloc, call_value); parsed_count += 1; } return calls; } fn parseToolCall(alloc: Allocator, value: std.json.Value) !session.ToolCall { const object = try exactObject(value, &.{ "id", "name", "arguments_json", "provider_result", }); const id = try parseRequiredDurableBytes(alloc, object, "id"); errdefer mem_utils.free(alloc, id); const name = try parseRequiredDurableBytes(alloc, object, "name"); errdefer mem_utils.free(alloc, name); var arguments_json = try parseRequiredDurableBytes(alloc, object, "arguments_json"); errdefer mem_utils.free(alloc, arguments_json); const argument_integrity = try types.ToolArgumentIntegrity.classifyFunctionInput(alloc, arguments_json); if (argument_integrity == .malformed_json) { const safe_arguments = try alloc.dupe(u8, "{}"); mem_utils.free(alloc, arguments_json); arguments_json = safe_arguments; } const provider_result = try parseOptionalDurableBytes( alloc, object.get("provider_result") orelse return error.InvalidSessionFormat, ); return .{ .id = id, .name = name, .arguments_json = arguments_json, .argument_integrity = argument_integrity, .provider_result = provider_result, }; } fn parseOptionalToolCall(alloc: Allocator, value: std.json.Value) !?session.ToolCall { return switch (value) { .null => null, .object => blk: { var call = try parseToolCall(alloc, value); errdefer session.freeToolCall(alloc, call); try session.repairPersistedInterruptedToolArguments(alloc, &call, .schema_v3); break :blk call; }, else => error.InvalidSessionFormat, }; } fn parseToolResults( alloc: Allocator, value: std.json.Value, schema_version: u64, ) ![]session.PersistedToolResult { 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 for (results[0..parsed_count]) |result| { mem_utils.free(alloc, result.tool_call_id); mem_utils.free(alloc, result.tool_name); mem_utils.free(alloc, result.output); types.freeToolImages(alloc, result.tool_images); if (result.tool_image_handle) |handle| mem_utils.free(alloc, handle); if (result.output_handle) |handle| mem_utils.free(alloc, handle); if (result.preview) |preview| mem_utils.free(alloc, preview); types.freePermissionFeedback(alloc, result.permission_feedback); if (result.committed_file_presentation) |presentation| { types.freeCommittedFilePresentation(alloc, presentation); } if (result.command_output_replay) |replay| { types.freeCommandOutputReplay(alloc, replay); } }; for (value.array.items, 0..) |result_value, i| { results[i] = try parseToolResult(alloc, result_value, schema_version); parsed_count += 1; } return results; } fn parseToolResult( alloc: Allocator, value: std.json.Value, schema_version: u64, ) !session.PersistedToolResult { const v1_keys = &.{ "tool_call_id", "tool_name", "status", "output", "output_handle", "preview", "output_bytes", "stored_output_bytes", "truncated", "provider_native", "created_at_ms", }; const v2_keys = &.{ "tool_call_id", "tool_name", "status", "output", "output_handle", "preview", "output_bytes", "stored_output_bytes", "truncated", "provider_native", "created_at_ms", "permission_feedback", }; const v2_extended_keys = &.{ "tool_call_id", "tool_name", "status", "output", "output_handle", "preview", "output_bytes", "stored_output_bytes", "truncated", "provider_native", "created_at_ms", "permission_feedback", "committed_file_presentation", }; const v3_keys = &.{ "tool_call_id", "tool_name", "status", "output", "output_handle", "preview", "output_bytes", "stored_output_bytes", "truncated", "provider_native", "created_at_ms", "permission_feedback", "committed_file_presentation", "command_output_replay", "command_process_presentation", }; const v4_keys = &[_][]const u8{ "tool_call_id", "tool_name", "status", "output", "output_handle", "preview", "output_bytes", "stored_output_bytes", "truncated", "provider_native", "created_at_ms", "permission_feedback", "committed_file_presentation", "command_output_replay", "command_process_presentation", "terminal_action_presentation", }; const result_shape: ExactVariantObject = switch (schema_version) { 1 => .{ .object = try exactObject(value, v1_keys), .extended = false }, 2 => try exactVariantObject(value, v2_keys, v2_extended_keys), 3 => .{ .object = try exactObject(value, v3_keys), .extended = true }, 4, 5, 6, 7 => .{ .object = try exactObject(value, v4_keys), .extended = true }, 8, 9 => .{ .object = if (value == .object and value.object.contains("tool_images")) try exactObject(value, &(v4_keys.* ++ [_][]const u8{"tool_images"})) else if (value == .object and value.object.contains("tool_image_handle")) try exactObject(value, &(v4_keys.* ++ [_][]const u8{"tool_image_handle"})) else try exactObject(value, v4_keys), .extended = true }, 10 => blk: { const keys = v4_keys.* ++ [_][]const u8{"review_feedback"}; break :blk .{ .object = if (value == .object and value.object.contains("tool_images")) try exactObject(value, &(keys ++ [_][]const u8{"tool_images"})) else if (value == .object and value.object.contains("tool_image_handle")) try exactObject(value, &(keys ++ [_][]const u8{"tool_image_handle"})) else try exactObject(value, &keys), .extended = true }; }, else => return error.InvalidSessionFormat, }; const object = result_shape.object; const status = std.meta.stringToEnum( session.PersistedToolStatus, try requireString(object, "status"), ) orelse return error.InvalidSessionFormat; const provider_native = try requireBool(object, "provider_native"); const review_feedback = if (schema_version >= 10) try requireBool(object, "review_feedback") else false; if (review_feedback and (status != .failure or provider_native)) return error.InvalidSessionFormat; const tool_call_id = try parseRequiredDurableBytes(alloc, object, "tool_call_id"); errdefer mem_utils.free(alloc, tool_call_id); const tool_name = try parseRequiredDurableBytes(alloc, object, "tool_name"); errdefer mem_utils.free(alloc, tool_name); var output = try parseRequiredDurableBytes(alloc, object, "output"); errdefer mem_utils.free(alloc, output); const output_handle = try parseOptionalDurableBytes( alloc, object.get("output_handle") orelse return error.InvalidSessionFormat, ); errdefer if (output_handle) |owned| mem_utils.free(alloc, owned); const preview = try parseOptionalDurableBytes( alloc, object.get("preview") orelse return error.InvalidSessionFormat, ); errdefer if (preview) |owned| mem_utils.free(alloc, owned); const permission_feedback: [][]u8 = if (schema_version == 1) &.{} else try parseDurableBytesArray( alloc, object.get("permission_feedback") orelse return error.InvalidSessionFormat, ); errdefer types.freePermissionFeedback(alloc, permission_feedback); const committed_file_presentation = if (result_shape.extended) try parseOptionalCommittedFilePresentation( alloc, object.get("committed_file_presentation") orelse return error.InvalidSessionFormat, ) else null; errdefer if (committed_file_presentation) |presentation| { types.freeCommittedFilePresentation(alloc, presentation); }; const command_output_replay = if (schema_version >= 3) try parseOptionalCommandOutputReplay( alloc, object.get("command_output_replay") orelse return error.InvalidSessionFormat, ) else null; errdefer if (command_output_replay) |replay| { types.freeCommandOutputReplay(alloc, replay); }; const command_process_presentation = if (schema_version >= 3) try parseOptionalCommandProcessPresentation( object.get("command_process_presentation") orelse return error.InvalidSessionFormat, ) else null; const terminal_action_presentation = if (schema_version >= 4) try parseOptionalTerminalActionPresentation( object.get("terminal_action_presentation") orelse return error.InvalidSessionFormat, ) else null; const tool_image_handle = if (object.get("tool_image_handle")) |value_handle| try parseOptionalDurableBytes(alloc, value_handle) else null; errdefer if (tool_image_handle) |handle| mem_utils.free(alloc, handle); const tool_images = if (object.get("tool_images")) |images| images: { if (images != .array) return error.InvalidSessionFormat; const parsed_images = image_data.parseToolImages(alloc, images.array.items) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; const notice = try std.fmt.allocPrint(alloc, "{s}\n[Saved tool image unavailable: {s}]", .{ output, @errorName(err) }); mem_utils.free(alloc, output); output = notice; break :images try alloc.alloc(types.ToolImage, 0); }; if (parsed_images.len != images.array.items.len) { types.freeToolImages(alloc, parsed_images); const notice = try std.fmt.allocPrint(alloc, "{s}\n[Saved tool image unavailable: unsupported content]", .{output}); mem_utils.free(alloc, output); output = notice; break :images try alloc.alloc(types.ToolImage, 0); } break :images parsed_images; } else try alloc.alloc(types.ToolImage, 0); errdefer types.freeToolImages(alloc, tool_images); return .{ .tool_images = tool_images, .tool_image_handle = tool_image_handle, .tool_call_id = tool_call_id, .tool_name = tool_name, .status = 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 = provider_native, .review_feedback = review_feedback, .created_at_ms = try requireI64(object, "created_at_ms"), .permission_feedback = permission_feedback, .committed_file_presentation = committed_file_presentation, .command_output_replay = command_output_replay, .command_process_presentation = command_process_presentation, .terminal_action_presentation = terminal_action_presentation, }; } fn parseOptionalCommandOutputReplay( alloc: Allocator, value: std.json.Value, ) !?types.CommandOutputReplay { if (value == .null) return null; const object = switch (value) { .object => |object| object, else => return error.InvalidSessionFormat, }; const kind = try requireString(object, "kind"); if (std.mem.eql(u8, kind, "unavailable")) { _ = try exactObject(value, &.{"kind"}); return .unavailable; } if (!std.mem.eql(u8, kind, "available")) return error.InvalidSessionFormat; const exact = try exactObject(value, &.{ "kind", "handle", "framed_bytes" }); const handle = try parseRequiredDurableBytes(alloc, exact, "handle"); errdefer alloc.free(handle); return .{ .available = .{ .handle = handle, .framed_bytes = try requireUsize(exact, "framed_bytes"), } }; } fn parseOptionalCommandProcessPresentation( value: std.json.Value, ) !?types.CommandProcessPresentation { if (value == .null) return null; const object = try exactObject(value, &.{ "kind", "value" }); const kind = try requireString(object, "kind"); if (std.mem.eql(u8, kind, "exit_code")) { return .{ .exit_code = try requireI64(object, "value") }; } if (std.mem.eql(u8, kind, "signal")) { const signal = try requireU64(object, "value"); return .{ .signal = std.math.cast(u32, signal) orelse return error.InvalidSessionFormat }; } if (std.mem.eql(u8, kind, "timed_out")) { if (object.get("value").? != .null) return error.InvalidSessionFormat; return .timed_out; } if (std.mem.eql(u8, kind, "output_capture_failed")) { if (object.get("value").? != .null) return error.InvalidSessionFormat; return .output_capture_failed; } return error.InvalidSessionFormat; } fn parseOptionalTerminalActionPresentation( value: std.json.Value, ) !?types.TerminalActionPresentation { if (value == .null) return null; const object = switch (value) { .object => |object| object, else => return error.InvalidSessionFormat, }; const kind = try requireString(object, "kind"); if (std.mem.eql(u8, kind, "returned")) { _ = try exactObject(value, &.{ "kind", "outcome" }); return .{ .returned = try parseTerminalReturnPresentation( object.get("outcome") orelse return error.InvalidSessionFormat, ) }; } if (std.mem.eql(u8, kind, "failed")) { _ = try exactObject(value, &.{ "kind", "code" }); const code = std.meta.stringToEnum( types.TerminalFailurePresentation, try requireString(object, "code"), ) orelse return error.InvalidSessionFormat; return .{ .failed = code }; } return error.InvalidSessionFormat; } fn parseTerminalReturnPresentation( value: std.json.Value, ) !types.TerminalReturnPresentation { const object = try exactObject(value, &.{ "kind", "value" }); const kind = try requireString(object, "kind"); if (std.mem.eql(u8, kind, "started") or std.mem.eql(u8, kind, "condition_met") or std.mem.eql(u8, kind, "safety_ceiling") or std.mem.eql(u8, kind, "cancelled")) { if (object.get("value").? != .null) return error.InvalidSessionFormat; if (std.mem.eql(u8, kind, "started")) return .started; if (std.mem.eql(u8, kind, "condition_met")) return .condition_met; if (std.mem.eql(u8, kind, "safety_ceiling")) return .safety_ceiling; return .cancelled; } if (std.mem.eql(u8, kind, "exited")) { return .{ .exited = std.math.cast(i32, try requireI64(object, "value")) orelse return error.InvalidSessionFormat }; } if (std.mem.eql(u8, kind, "signal")) { return .{ .signal = std.math.cast(u32, try requireU64(object, "value")) orelse return error.InvalidSessionFormat }; } return error.InvalidSessionFormat; } fn parseOptionalCommittedFilePresentation( alloc: Allocator, value: std.json.Value, ) !?types.CommittedFilePresentation { return switch (value) { .null => null, .object => try parseCommittedFilePresentation(alloc, value), else => error.InvalidSessionFormat, }; } fn parseCommittedFilePresentation( alloc: Allocator, value: std.json.Value, ) !types.CommittedFilePresentation { const object = try exactObject(value, &.{ "path", "kind", "lines", "additions", "deletions", "truncated", "previous_content", "after_content", "lifecycle_id", }); const path = try parseRequiredDurableBytes(alloc, object, "path"); errdefer mem_utils.free(alloc, path); const lines = try parseCommittedFilePresentationLines( alloc, object.get("lines") orelse return error.InvalidSessionFormat, ); errdefer freeCommittedFilePresentationLines(alloc, lines); const previous_content = try parseOptionalDurableBytes( alloc, object.get("previous_content") orelse return error.InvalidSessionFormat, ); errdefer if (previous_content) |content| mem_utils.free(alloc, content); const after_content = try parseOptionalDurableBytes( alloc, object.get("after_content") orelse return error.InvalidSessionFormat, ); errdefer if (after_content) |content| mem_utils.free(alloc, content); const lifecycle_id = try parseOptionalToolLifecycleId( alloc, object.get("lifecycle_id") orelse return error.InvalidSessionFormat, ); errdefer if (lifecycle_id) |id| mem_utils.free(alloc, id.call_id); return .{ .path = path, .kind = std.meta.stringToEnum( types.CommittedFilePresentationKind, try requireString(object, "kind"), ) orelse return error.InvalidSessionFormat, .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, value: std.json.Value, ) ![]types.CommittedFilePresentationLine { if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{}; const lines = try alloc.alloc(types.CommittedFilePresentationLine, value.array.items.len); var parsed_count: usize = 0; errdefer { for (lines[0..parsed_count]) |line| mem_utils.free(alloc, @constCast(line.text)); mem_utils.free(alloc, lines); } for (value.array.items, 0..) |line_value, index| { const object = try exactObject(line_value, &.{ "kind", "old_line", "new_line", "text" }); const text = try parseRequiredDurableBytes(alloc, object, "text"); errdefer mem_utils.free(alloc, text); lines[index] = .{ .kind = std.meta.stringToEnum( types.CommittedFilePresentationLineKind, try requireString(object, "kind"), ) orelse return error.InvalidSessionFormat, .old_line = try parseOptionalU32(object.get("old_line") orelse return error.InvalidSessionFormat), .new_line = try parseOptionalU32(object.get("new_line") orelse return error.InvalidSessionFormat), .text = text, }; parsed_count += 1; } return lines; } fn freeCommittedFilePresentationLines( alloc: Allocator, lines: []const types.CommittedFilePresentationLine, ) void { for (lines) |line| mem_utils.free(alloc, @constCast(line.text)); if (lines.len > 0) mem_utils.free(alloc, @constCast(lines)); } fn parseOptionalToolLifecycleId( alloc: Allocator, value: std.json.Value, ) !?types.ToolLifecycleId { return switch (value) { .null => null, .object => { const object = try exactObject(value, &.{ "turn_id", "call_id" }); const call_id = try parseRequiredDurableBytes(alloc, object, "call_id"); errdefer mem_utils.free(alloc, call_id); return .{ .turn_id = try requireU64(object, "turn_id"), .call_id = call_id, }; }, else => error.InvalidSessionFormat, }; } fn parseFiles(alloc: Allocator, value: std.json.Value) ![]session.FileEvidence { 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 for (files[0..parsed_count]) |file| { mem_utils.free(alloc, file.path); if (file.new_path) |new_path| mem_utils.free(alloc, new_path); mem_utils.free(alloc, file.tool_call_id); mem_utils.free(alloc, file.tool_name); }; for (value.array.items, 0..) |file_value, i| { files[i] = try parseFile(alloc, file_value); parsed_count += 1; } return files; } fn parseFile(alloc: Allocator, value: std.json.Value) !session.FileEvidence { const object = try exactObject(value, &.{ "path", "new_path", "tool_call_id", "tool_name", "action", "status", "model_view_covers_full_file", "stale", }); const path = try parseRequiredDurableBytes(alloc, object, "path"); errdefer mem_utils.free(alloc, path); const new_path = try parseOptionalDurableBytes( alloc, object.get("new_path") orelse return error.InvalidSessionFormat, ); errdefer if (new_path) |owned| mem_utils.free(alloc, owned); const tool_call_id = try parseRequiredDurableBytes(alloc, object, "tool_call_id"); errdefer mem_utils.free(alloc, tool_call_id); const tool_name = try parseRequiredDurableBytes(alloc, 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 = std.meta.stringToEnum( session.FileEvidenceAction, try requireString(object, "action"), ) orelse return error.InvalidSessionFormat, .status = std.meta.stringToEnum( session.PersistedToolStatus, try requireString(object, "status"), ) orelse return error.InvalidSessionFormat, .model_view_covers_full_file = try requireBool(object, "model_view_covers_full_file"), .stale = try requireBool(object, "stale"), }; } fn parseDurableBytesArray(alloc: Allocator, value: std.json.Value) ![][]u8 { if (value != .array) return error.InvalidSessionFormat; if (value.array.items.len == 0) return &.{}; const items = try alloc.alloc([]u8, value.array.items.len); errdefer alloc.free(items); var parsed_count: usize = 0; errdefer for (items[0..parsed_count]) |item| alloc.free(item); for (value.array.items, 0..) |item, i| { items[i] = try parseDurableBytes(alloc, item); parsed_count += 1; } return items; } noinline fn parseRequiredDurableBytes(alloc: Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 { return parseDurableBytes(alloc, object.get(key) orelse return error.InvalidSessionFormat); } noinline fn parseOptionalDurableBytes(alloc: Allocator, value: std.json.Value) !?[]u8 { return switch (value) { .null => null, .string, .object => try parseDurableBytes(alloc, value), else => error.InvalidSessionFormat, }; } fn parseOptionalIdentifier(value: std.json.Value) !?[16]u8 { return switch (value) { .null => null, .string => |hex| try parseHexIdentifier(hex), else => error.InvalidSessionFormat, }; } fn parseHexIdentifier(hex: []const u8) ![16]u8 { if (hex.len != 32) return error.InvalidSessionFormat; var result: [16]u8 = undefined; _ = std.fmt.hexToBytes(&result, hex) catch return error.InvalidSessionFormat; const canonical = std.fmt.bytesToHex(result, .lower); if (!std.mem.eql(u8, &canonical, hex)) return error.InvalidSessionFormat; return result; } noinline fn writeOptionalDurableBytes(writer: *std.Io.Writer, value: anytype) !void { if (value) |bytes| { try writeDurableBytes(writer, bytes); } else { try writer.writeAll("null"); } } fn writeOptionalU32(writer: *std.Io.Writer, value: ?u32) !void { if (value) |number| { try writer.print("{d}", .{number}); } else { try writer.writeAll("null"); } } noinline fn writeJsonString(writer: *std.Io.Writer, bytes: []const u8) !void { try std.json.Stringify.value(bytes, .{}, writer); } noinline fn validateSessionId(value: []const u8) !void { if (value.len == 0 or value.len > 255 or std.mem.eql(u8, value, ".") or std.mem.eql(u8, value, "..")) { return error.InvalidDurableField; } for (value) |byte| { if (!std.ascii.isAlphanumeric(byte) and byte != '.' and byte != '_' and byte != '-') { return error.InvalidDurableField; } } } fn validateWorkspaceRoot(value: []const u8) !void { if (value.len == 0 or value.len > std.Io.Dir.max_path_bytes or !std.unicode.utf8ValidateSlice(value) or !std.Io.Dir.path.isAbsolute(value)) { return error.InvalidDurableField; } } fn validateModel(value: []const u8) !void { if (value.len == 0 or value.len > 1024 or !std.unicode.utf8ValidateSlice(value)) { return error.InvalidDurableField; } if (std.ascii.isWhitespace(value[0]) or std.ascii.isWhitespace(value[value.len - 1])) { return error.InvalidDurableField; } for (value) |byte| if (std.ascii.isControl(byte)) return error.InvalidDurableField; } fn validateConversationLanguage(language: session.ConversationLanguage) !void { try validateConversationLanguageBytes(language.view()); } fn validateConversationLanguageBytes(value: []const u8) !void { if (value.len == 0 or value.len > session.ConversationLanguage.max_len or !std.unicode.utf8ValidateSlice(value) or !std.mem.eql(u8, value, std.mem.trim(u8, value, " \t\r\n"))) { return error.InvalidDurableField; } for (value) |byte| if (std.ascii.isControl(byte)) return error.InvalidDurableField; } fn dupeHistory(alloc: Allocator, history: []const session.HistoryTurn) ![]session.HistoryTurn { if (history.len == 0) return &.{}; const copy = try alloc.alloc(session.HistoryTurn, history.len); errdefer alloc.free(copy); var copied: usize = 0; errdefer for (copy[0..copied]) |turn| session.freeHistoryTurn(alloc, turn); for (history, 0..) |turn, i| { copy[i] = try session.dupeHistoryTurn(alloc, turn); copied += 1; } return copy; } fn expectKey(reader: *std.json.Reader, alloc: Allocator, expected: []const u8) !void { const token = try reader.nextAllocMax(alloc, .alloc_if_needed, 64); defer freeToken(alloc, token); const actual = switch (token) { .string => |value| value, .allocated_string => |value| value, else => return error.InvalidSessionFormat, }; if (!std.mem.eql(u8, actual, expected)) return error.InvalidSessionFormat; } 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 readI64(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 readU64(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 readBool(reader: *std.json.Reader) !bool { return switch (try reader.next()) { .true => true, .false => false, else => 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 expectToken(actual: std.json.Token, comptime expected: std.meta.Tag(std.json.Token)) !void { if (std.meta.activeTag(actual) != expected) return error.InvalidSessionFormat; } noinline fn requireObject(value: std.json.Value) !std.json.ObjectMap { if (value != .object) return error.InvalidSessionFormat; return value.object; } pub fn exactObject(value: std.json.Value, keys: []const []const u8) !std.json.ObjectMap { const object = try requireObject(value); if (object.count() != keys.len) return error.InvalidSessionFormat; var iterator = object.iterator(); while (iterator.next()) |entry| { var known = false; for (keys) |key| { if (std.mem.eql(u8, entry.key_ptr.*, key)) { known = true; break; } } if (!known) return error.InvalidSessionFormat; } return object; } fn exactInterruptedHistoryObject( value: std.json.Value, has_execution: bool, has_presentation: bool, has_terminal_reason: bool, has_cancellation_origin: bool, ) !std.json.ObjectMap { var keys: [9][]const u8 = undefined; keys[0] = "kind"; keys[1] = "user"; keys[2] = "assistant"; keys[3] = "tool_call"; keys[4] = "completed_tool_names"; var len: usize = 5; if (has_execution) { keys[len] = "execution"; len += 1; } if (has_presentation) { keys[len] = "cancelled_command"; len += 1; } if (has_terminal_reason) { keys[len] = "terminal_reason"; len += 1; } if (has_cancellation_origin) { keys[len] = "cancellation_origin"; len += 1; } return exactObject(value, keys[0..len]); } fn parseInterruptedTerminalReason( object: std.json.ObjectMap, key: []const u8, ) !types.InterruptedTerminalReason { const raw = try requireString(object, key); if (std.mem.eql(u8, raw, "cancelled")) return .cancelled; if (std.mem.eql(u8, raw, "failed")) return .failed; return error.InvalidSessionFormat; } const ExactVariantObject = struct { object: std.json.ObjectMap, extended: bool, }; fn exactVariantObject( value: std.json.Value, legacy_keys: []const []const u8, extended_keys: []const []const u8, ) !ExactVariantObject { const object = try requireObject(value); if (object.count() == legacy_keys.len) { return .{ .object = try exactObject(value, legacy_keys), .extended = false, }; } if (object.count() == extended_keys.len) { return .{ .object = try exactObject(value, extended_keys), .extended = true, }; } return error.InvalidSessionFormat; } pub 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; } fn requireI64(object: std.json.ObjectMap, key: []const u8) !i64 { const value = object.get(key) orelse return error.InvalidSessionFormat; return switch (value) { .integer => |number| number, .number_string => |raw| std.fmt.parseInt(i64, raw, 10) catch error.InvalidSessionFormat, else => error.InvalidSessionFormat, }; } fn requireU64(object: std.json.ObjectMap, key: []const u8) !u64 { const value = object.get(key) orelse return error.InvalidSessionFormat; return switch (value) { .integer => |number| if (number >= 0) @intCast(number) else error.InvalidSessionFormat, .number_string => |raw| std.fmt.parseUnsigned(u64, raw, 10) catch error.InvalidSessionFormat, else => error.InvalidSessionFormat, }; } noinline fn requireUsize(object: std.json.ObjectMap, key: []const u8) !usize { const number = try requireU64(object, key); if (number > std.math.maxInt(usize)) return error.InvalidSessionFormat; return @intCast(number); } fn parseOptionalU32(value: std.json.Value) !?u32 { const number: u64 = switch (value) { .null => return null, .integer => |integer| if (integer >= 0) @intCast(integer) else return error.InvalidSessionFormat, .number_string => |raw| std.fmt.parseUnsigned(u64, raw, 10) catch return error.InvalidSessionFormat, else => return error.InvalidSessionFormat, }; if (number > std.math.maxInt(u32)) return error.InvalidSessionFormat; return @intCast(number); } test "durable byte fields use canonical string or padded base64 representation" { const alloc = std.testing.allocator; var utf8_out: std.Io.Writer.Allocating = .init(alloc); defer utf8_out.deinit(); try writeDurableBytes(&utf8_out.writer, "hello\nworld"); try std.testing.expectEqualStrings("\"hello\\nworld\"", utf8_out.written()); const invalid = [_]u8{ 0xff, 0x00 }; var binary_out: std.Io.Writer.Allocating = .init(alloc); defer binary_out.deinit(); try writeDurableBytes(&binary_out.writer, &invalid); try std.testing.expectEqualStrings("{\"encoding\":\"base64\",\"data\":\"/wA=\"}", binary_out.written()); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, binary_out.written(), .{}); defer parsed.deinit(); const decoded = try parseDurableBytes(alloc, parsed.value); defer alloc.free(decoded); try std.testing.expectEqualSlices(u8, &invalid, decoded); const invalid_values = [_][]const u8{ "{\"encoding\":\"base64\",\"data\":\"/wA\"}", "{\"encoding\":\"base64\",\"data\":\"***=\"}", "{\"encoding\":\"base64\",\"data\":\"aGk=\"}", "{\"data\":\"/wA=\",\"encoding\":\"base64\"}", "{\"encoding\":\"base64\",\"data\":\"/wA=\",\"extra\":true}", }; for (invalid_values) |json_text| { var bad = try std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}); defer bad.deinit(); try std.testing.expectError(error.InvalidDurableBytes, parseDurableBytes(alloc, bad.value)); } } test "durable state round trips live history while discarding legacy authority" { const alloc = std.testing.allocator; const invalid_a = [_]u8{ 'a', 0xff }; const invalid_b = [_]u8{ 0xfe, 'b', 0x00 }; const invalid_c = [_]u8{ 'c', 0xf8 }; const exact_arguments = " \n{\"number\":1e+02}\t"; var images = [_]session.ImageAttachment{.{ .id = 7, .path = @constCast(invalid_a[0..]), .media_type = @constCast(invalid_b[0..]), .snapshot_path = @constCast(invalid_c[0..]), .snapshot_sha256 = @constCast(invalid_a[0..]), }}; var tool_calls = [_]session.ToolCall{.{ .id = invalid_a[0..], .name = invalid_b[0..], .arguments_json = @constCast(exact_arguments), .provider_result = invalid_a[0..], }}; var tool_results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast(invalid_a[0..]), .tool_name = @constCast(invalid_b[0..]), .status = .failure, .output = @constCast(invalid_c[0..]), .output_handle = @constCast(invalid_a[0..]), .preview = @constCast(invalid_b[0..]), .output_bytes = 99, .stored_output_bytes = 42, .truncated = true, .provider_native = true, .created_at_ms = 123, }}; var tool_steps = [_]session.ToolExecutionStep{.{ .assistant = @constCast(invalid_c[0..]), .tool_calls = tool_calls[0..], .tool_results = tool_results[0..], }}; var files = [_]session.FileEvidence{.{ .path = @constCast(invalid_a[0..]), .new_path = @constCast(invalid_b[0..]), .tool_call_id = @constCast(invalid_c[0..]), .tool_name = @constCast(invalid_a[0..]), .action = .rename, .status = .failure, .model_view_covers_full_file = true, .stale = true, }}; var completed_tool_names = [_][]u8{ @constCast(invalid_a[0..]), @constCast(invalid_b[0..]), }; const history = [_]session.HistoryTurn{ .{ .assistant = .{ .user = .{ .text = @constCast(invalid_a[0..]), .images = images[0..], .work_id = @constCast("work-assistant"), }, .assistant = @constCast(invalid_b[0..]), .execution = .{ .tool_steps = tool_steps[0..], .files = files[0..], }, } }, .{ .assistant = .{ .user = .{ .text = @constCast(invalid_b[0..]), .work_id = @constCast("work-background"), }, .assistant = @constCast(invalid_a[0..]), .execution = .{ .tool_steps = tool_steps[0..], .files = files[0..], }, } }, .{ .interrupted = .{ .user = .{ .text = @constCast(invalid_c[0..]), .work_id = @constCast("work-interrupted"), }, .assistant = @constCast(invalid_a[0..]), .tool_call = tool_calls[0], .completed_tool_names = completed_tool_names[0..], .execution = .{ .tool_steps = tool_steps[0..], .files = files[0..], }, } }, .{ .compacted_summary = .{ .summary = @constCast(invalid_b[0..]), .removed_turn_count = 3, .compaction_count = 2, .root_user_messages = completed_tool_names[0..], } }, }; var usage_runtime = session_usage.Usage.initFresh(); defer usage_runtime.deinit(alloc); const usage_sequence = try usage_runtime.reserveInvocation(); try usage_runtime.finishObservedInvocation( alloc, usage_sequence, 25, .observed_generation, "gen_01ARZ3NDEKTSV4RRFFQ69G5FAV", "https://ai-gateway.vercel.sh", null, ); var usage = try usage_runtime.snapshot(alloc); defer usage.deinit(alloc); const state = DurableSessionState{ .id = @constCast("1700000000000-42-session"), .origin_workspace_root = @constCast("/tmp/origin"), .workspace_root = @constCast("/tmp/current"), .created_at_ms = 100, .updated_at_ms = 90, .conversation_language = session.ConversationLanguage.literal("en"), .preferences = .{ .model = @constCast("openai/gpt-test"), .effort = types.ReasoningEffort.literal("future-tier"), .fast_mode = true, }, .history = @constCast(history[0..]), .context_history_start = 3, .total_input_tokens = 1234, .total_output_tokens = 567, .last_subagent_work_id = @constCast("work-17"), .subagent_child = true, .usage = usage, }; var first: std.Io.Writer.Allocating = .init(alloc); defer first.deinit(); const first_summary = try encodeState(state, &first.writer); try std.testing.expectEqual(@as(u64, first.written().len), first_summary.encoded_bytes); try std.testing.expect(first.written().len > 0); try std.testing.expect(first.written()[first.written().len - 1] != '\n'); var second: std.Io.Writer.Allocating = .init(alloc); defer second.deinit(); _ = try encodeState(state, &second.writer); try std.testing.expectEqualStrings(first.written(), second.written()); var source = std.Io.Reader.fixed(first.written()); var decoded = try decodeState(alloc, &source, .{}); defer decoded.deinit(alloc); try expectStateEqual(state, decoded); } test "durable state repairs duplicate-key execution and interrupted tool arguments" { const alloc = std.testing.allocator; const duplicate_arguments = "{\"depth\":1,\"depth\":2}"; var calls = [_]session.ToolCall{.{ .id = "call_bad", .name = "read_file", .arguments_json = duplicate_arguments, }}; var results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast("call_bad"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("stale success"), .output_bytes = 13, .stored_output_bytes = 13, .truncated = false, .provider_native = false, .created_at_ms = 1, }}; var steps = [_]session.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; const history = [_]session.HistoryTurn{ .{ .assistant = .{ .user = .{ .text = @constCast("inspect") }, .assistant = @constCast("failed"), .execution = .{ .tool_steps = steps[0..] }, } }, .{ .interrupted = .{ .user = .{ .text = @constCast("continue") }, .tool_call = .{ .id = "call_interrupted", .name = "glob_files", .arguments_json = duplicate_arguments, }, } }, }; const state = DurableSessionState{ .id = @constCast("session-1"), .origin_workspace_root = @constCast("/tmp/origin"), .workspace_root = @constCast("/tmp/current"), .created_at_ms = 1, .updated_at_ms = 2, .conversation_language = session.ConversationLanguage.literal("en"), .preferences = .{ .model = @constCast("openai/gpt-test"), .effort = .auto, .fast_mode = false, }, .history = @constCast(history[0..]), .total_input_tokens = 3, .total_output_tokens = 4, }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); _ = try encodeState(state, &encoded.writer); var source = std.Io.Reader.fixed(encoded.written()); var decoded = try decodeState(alloc, &source, .{}); defer decoded.deinit(alloc); const decoded_step = decoded.history[0].assistant.execution.tool_steps[0]; try std.testing.expectEqualStrings("{}", decoded_step.tool_calls[0].arguments_json); try std.testing.expectEqual(types.ToolArgumentIntegrity.valid, decoded_step.tool_calls[0].argument_integrity); try std.testing.expectEqual(session.PersistedToolStatus.failure, decoded_step.tool_results[0].status); try std.testing.expect(std.mem.find(u8, decoded_step.tool_results[0].output, "tool_execution_failed") != null); try std.testing.expect(std.mem.find(u8, decoded_step.tool_results[0].output, duplicate_arguments) == null); const interrupted = decoded.history[1].interrupted.tool_call.?; try std.testing.expectEqualStrings("{}", interrupted.arguments_json); try std.testing.expectEqual(types.ToolArgumentIntegrity.valid, interrupted.argument_integrity); } test "non-object saved function inputs are repaired without changing recorded outcomes" { const alloc = std.testing.allocator; for ([_][]const u8{ "[]", "42", "null", "true", "\"text\"" }) |arguments| { for ([_]session.PersistedToolStatus{ .success, .failure }) |status| { for (0..3) |native_kind| { var calls = [_]session.ToolCall{.{ .id = "call", .name = "read_file", .arguments_json = arguments }}; calls[0].provider_result = if (native_kind == 1) "provider result" else null; var results = [_]session.PersistedToolResult{persistedResultForTest("call", "read_file")}; results[0].status = status; results[0].provider_native = native_kind == 2; results[0].output_handle = @constCast("retained-handle"); results[0].preview = @constCast("retained-preview"); var steps = [_]session.ToolExecutionStep{.{ .tool_calls = &calls, .tool_results = &results }}; const turn: session.HistoryTurn = .{ .assistant = .{ .user = .{ .text = @constCast("read") }, .assistant = @constCast("done"), .execution = .{ .tool_steps = &steps }, } }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, turn); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); const decoded = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, decoded); const step = decoded.assistant.execution.tool_steps[0]; try std.testing.expectEqualStrings(if (native_kind == 0) "{}" else arguments, 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("stale", step.tool_results[0].output); try std.testing.expectEqualStrings("retained-handle", step.tool_results[0].output_handle.?); try std.testing.expectEqualStrings("retained-preview", step.tool_results[0].preview.?); try std.testing.expectEqualStrings(arguments, calls[0].arguments_json); try session.repairPersistedToolArguments(alloc, step.tool_calls, step.tool_results, .schema_v3); try std.testing.expectEqualStrings(if (native_kind == 0) "{}" else arguments, step.tool_calls[0].arguments_json); if (native_kind == 0 and status == .failure and std.mem.eql(u8, arguments, "[]")) { try std.testing.checkAllAllocationFailures(alloc, checkNonObjectHistoryAllocationFailures, .{parsed.value}); } } } } } fn checkNonObjectHistoryAllocationFailures(alloc: Allocator, value: std.json.Value) !void { const decoded = try parseHistoryTurn(alloc, value); defer session.freeHistoryTurn(alloc, decoded); try std.testing.expectEqualStrings("{}", decoded.assistant.execution.tool_steps[0].tool_calls[0].arguments_json); try std.testing.expectEqual(session.PersistedToolStatus.failure, decoded.assistant.execution.tool_steps[0].tool_results[0].status); } test "non-object interrupted inputs repair locally and preserve provider-owned records" { const alloc = std.testing.allocator; for ([_]bool{ false, true }) |native| { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeToolCall(&out.writer, .{ .id = "interrupted", .name = "read_file", .arguments_json = "[]", .provider_result = if (native) "provider result" else null, }); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, out.written(), .{}); defer parsed.deinit(); const repaired = (try parseOptionalToolCall(alloc, parsed.value)).?; defer session.freeToolCall(alloc, repaired); try std.testing.expectEqualStrings(if (native) "[]" else "{}", repaired.arguments_json); if (!native) try std.testing.checkAllAllocationFailures(alloc, checkNonObjectInterruptedAllocationFailures, .{parsed.value}); } } fn checkNonObjectInterruptedAllocationFailures(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 "current history decode rejects ambiguous malformed tool result pairings" { var duplicate_calls = [_]session.ToolCall{ .{ .id = "call_bad", .name = "read_file", .arguments_json = "{]" }, .{ .id = "call_bad", .name = "read_file", .arguments_json = "{]" }, }; var one_result = [_]session.PersistedToolResult{persistedResultForTest("call_bad", "read_file")}; try expectMalformedPairRejected(duplicate_calls[0..], one_result[0..]); var missing_call = [_]session.ToolCall{ .{ .id = "call_bad", .name = "read_file", .arguments_json = "{]" }, }; var no_results = [_]session.PersistedToolResult{}; try expectMalformedPairRejected(missing_call[0..], no_results[0..]); var multiple_call = [_]session.ToolCall{ .{ .id = "call_bad", .name = "read_file", .arguments_json = "{]" }, }; var multiple_results = [_]session.PersistedToolResult{ persistedResultForTest("call_bad", "read_file"), persistedResultForTest("call_bad", "read_file"), }; try expectMalformedPairRejected(multiple_call[0..], multiple_results[0..]); var mismatched_call = [_]session.ToolCall{ .{ .id = "call_bad", .name = "read_file", .arguments_json = "{]" }, }; var mismatched_result = [_]session.PersistedToolResult{persistedResultForTest("call_bad", "glob_files")}; try expectMalformedPairRejected(mismatched_call[0..], mismatched_result[0..]); } test "current history duplicate-key repair preserves allocation failures" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkCurrentToolArgumentRepairAllocationFailures, .{}, ); } test "legacy durable state defaults context history start to zero" { const alloc = std.testing.allocator; const legacy = "{\"id\":\"legacy\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\"," ++ "\"created_at_ms\":1,\"updated_at_ms\":2,\"conversation_language\":\"en\"," ++ "\"preferences\":{\"model\":\"model\",\"effort\":\"auto\",\"fast_mode\":false}," ++ "\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}"; var source = std.Io.Reader.fixed(legacy); var decoded = try decodeState(alloc, &source, .{}); defer decoded.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), decoded.context_history_start); const invalid = "{\"id\":\"invalid\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\"," ++ "\"created_at_ms\":1,\"updated_at_ms\":2,\"conversation_language\":\"en\"," ++ "\"preferences\":{\"model\":\"model\",\"effort\":\"auto\",\"fast_mode\":false}," ++ "\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0,\"context_history_start\":1}"; var invalid_source = std.Io.Reader.fixed(invalid); try std.testing.expectError( error.InvalidDurableField, decodeState(alloc, &invalid_source, .{}), ); } test "per-turn work provenance is strict and ordinary state omits it" { const alloc = std.testing.allocator; const ordinary = "{\"id\":\"session\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"test/model\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[{\"kind\":\"assistant\",\"user\":{\"text\":\"hello\",\"images\":[]},\"assistant\":\"world\",\"execution\":{\"schema_version\":3,\"tool_steps\":[],\"files\":[]}}],\"total_input_tokens\":0,\"total_output_tokens\":0}"; var ordinary_source = std.Io.Reader.fixed(ordinary); var state = try decodeState(alloc, &ordinary_source, .{}); defer state.deinit(alloc); try std.testing.expect(state.history[0].assistant.user.work_id == null); var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); _ = try encodeState(state, &encoded.writer); try std.testing.expect(std.mem.find(u8, encoded.written(), "work_id") == null); const invalid = [_][]const u8{ "{\"kind\":\"assistant\",\"user\":{\"text\":\"hello\",\"images\":[],\"work_id\":\"\"},\"assistant\":\"world\",\"execution\":{\"schema_version\":3,\"tool_steps\":[],\"files\":[]}}", "{\"kind\":\"assistant\",\"user\":{\"text\":\"hello\",\"images\":[],\"work_id\":null},\"assistant\":\"world\",\"execution\":{\"schema_version\":3,\"tool_steps\":[],\"files\":[]}}", "{\"kind\":\"assistant\",\"user\":{\"text\":\"hello\",\"images\":[],\"work_id\":\"bad\\u0000id\"},\"assistant\":\"world\",\"execution\":{\"schema_version\":3,\"tool_steps\":[],\"files\":[]}}", }; for (invalid) |json| { var parsed = try std.json.parseFromSlice(std.json.Value, alloc, json, .{}); defer parsed.deinit(); try std.testing.expectError( error.InvalidSessionFormat, parseHistoryTurn(alloc, parsed.value), ); } var too_long: [session.max_work_id_bytes + 1]u8 = undefined; @memset(&too_long, 'x'); state.history[0].assistant.user.work_id = &too_long; try std.testing.expectError(error.InvalidDurableField, validateState(state)); state.history[0].assistant.user.work_id = null; } test "durable session IDs accept safe opaque basenames" { const valid_ids = [_][]const u8{ "session.v3", ".hidden-session", "session..branch", }; for (valid_ids) |id| try validateSessionId(id); var max_id: [255]u8 = undefined; @memset(&max_id, 'a'); try validateSessionId(&max_id); const state = DurableSessionState{ .id = max_id[0..], .origin_workspace_root = @constCast("/tmp/origin"), .workspace_root = @constCast("/tmp/current"), .created_at_ms = 1, .updated_at_ms = 2, .conversation_language = session.ConversationLanguage.literal("en"), .preferences = .{ .model = @constCast("model"), .effort = .auto, .fast_mode = false, }, .history = &.{}, .total_input_tokens = 0, .total_output_tokens = 0, }; var encoded: std.Io.Writer.Allocating = .init(std.testing.allocator); defer encoded.deinit(); _ = try encodeState(state, &encoded.writer); var source = std.Io.Reader.fixed(encoded.written()); var decoded = try decodeState(std.testing.allocator, &source, .{}); defer decoded.deinit(std.testing.allocator); try std.testing.expectEqualSlices(u8, &max_id, decoded.id); } test "durable session IDs reject unsafe basenames" { const invalid_ids = [_][]const u8{ "", ".", "..", "path/session", "path\\session", "session:name", "session name", }; for (invalid_ids) |id| { try std.testing.expectError(error.InvalidDurableField, validateSessionId(id)); } const unsupported = [_]u8{ 0xff, 'a' }; try std.testing.expectError(error.InvalidDurableField, validateSessionId(&unsupported)); var too_long: [256]u8 = undefined; @memset(&too_long, 'a'); try std.testing.expectError(error.InvalidDurableField, validateSessionId(&too_long)); } test "durable state rejects invalid metadata fields before returning" { const alloc = std.testing.allocator; const invalid_documents = [_][]const u8{ "{\"id\":\"../bad\",\"origin_workspace_root\":\"/tmp/a\",\"workspace_root\":\"/tmp/a\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"m\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}", "{\"id\":\"good\",\"origin_workspace_root\":\"relative\",\"workspace_root\":\"/tmp/a\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"m\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}", "{\"id\":\"good\",\"origin_workspace_root\":\"/tmp/a\",\"workspace_root\":\"/tmp/a\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\" en \",\"preferences\":{\"model\":\"m\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}", "{\"id\":\"good\",\"origin_workspace_root\":\"/tmp/a\",\"workspace_root\":\"/tmp/a\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"m\",\"effort\":\"not valid\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}", "{\"id\":\"good\",\"origin_workspace_root\":\"/tmp/a\",\"workspace_root\":\"/tmp/a\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\" m\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}", }; for (invalid_documents) |document| { var source = std.Io.Reader.fixed(document); try std.testing.expectError(error.InvalidDurableField, decodeState(alloc, &source, .{})); } } test "review feedback survives durable execution memory round trip" { const alloc = std.testing.allocator; for ([_]bool{ false, true }) |review_feedback| { var calls = [_]session.ToolCall{.{ .id = "call-review", .name = "shell", .arguments_json = "{}", }}; var results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast("call-review"), .tool_name = @constCast("shell"), .status = .failure, .output = @constCast("Security review held this action."), .output_bytes = "Security review held this action.".len, .stored_output_bytes = "Security review held this action.".len, .review_feedback = review_feedback, }}; var steps = [_]session.ToolExecutionStep{.{ .tool_calls = &calls, .tool_results = &results }}; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeExecutionMemory(&encoded.writer, .{ .tool_steps = &steps }); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); const decoded = try parseExecutionMemory(alloc, parsed.value); defer session.freeExecutionMemory(alloc, decoded); const result = decoded.tool_steps[0].tool_results[0]; try std.testing.expectEqual(review_feedback, result.review_feedback); try std.testing.expectEqualStrings("Security review held this action.", result.output); try std.testing.expectEqual(session.PersistedToolStatus.failure, result.status); } } test "review feedback durable codec rejects invalid provenance and unknown fields" { const alloc = std.testing.allocator; var result = persistedResultForTest("call-review", "shell"); result.status = .failure; result.review_feedback = true; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writePersistedToolResult(&encoded.writer, result); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); const cases = [_]struct { status: []const u8, provider_native: bool, marker: std.json.Value }{ .{ .status = "success", .provider_native = false, .marker = .{ .bool = true } }, .{ .status = "failure", .provider_native = true, .marker = .{ .bool = true } }, .{ .status = "failure", .provider_native = false, .marker = .null }, .{ .status = "failure", .provider_native = false, .marker = .{ .integer = 1 } }, .{ .status = "failure", .provider_native = false, .marker = .{ .string = "true" } }, }; for (cases) |case| { parsed.value.object.getPtr("status").?.* = .{ .string = case.status }; parsed.value.object.getPtr("provider_native").?.* = .{ .bool = case.provider_native }; parsed.value.object.getPtr("review_feedback").?.* = case.marker; try std.testing.expectError(error.InvalidSessionFormat, parseToolResult(alloc, parsed.value, 10)); } parsed.value.object.getPtr("status").?.* = .{ .string = "failure" }; parsed.value.object.getPtr("review_feedback").?.* = .{ .bool = true }; try parsed.value.object.put(parsed.arena.allocator(), "unknown_feedback", .{ .bool = true }); try std.testing.expectError(error.InvalidSessionFormat, parseToolResult(alloc, parsed.value, 10)); for ([_]bool{ false, true }) |native| { var invalid = result; invalid.status = if (native) .failure else .success; invalid.provider_native = native; try std.testing.expectError(error.InvalidSessionFormat, writePersistedToolResult(&encoded.writer, invalid)); } } test "review feedback defaults old durable results without inspecting output" { const alloc = std.testing.allocator; var result = persistedResultForTest("call-review", "shell"); result.status = .failure; result.output = @constCast("Security review held this action."); var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writePersistedToolResult(&encoded.writer, result); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); try std.testing.expect(parsed.value.object.swapRemove("review_feedback")); for ([_]u64{ 8, 9 }) |version| { const decoded = try parseToolResult(alloc, parsed.value, version); defer { alloc.free(decoded.tool_call_id); alloc.free(decoded.tool_name); alloc.free(decoded.output); types.freeToolImages(alloc, decoded.tool_images); } try std.testing.expect(!decoded.review_feedback); try std.testing.expectEqualStrings("Security review held this action.", decoded.output); } try parsed.value.object.put(parsed.arena.allocator(), "review_feedback", .{ .bool = true }); try std.testing.expectError(error.InvalidSessionFormat, parseToolResult(alloc, parsed.value, 9)); } test "execution memory codec preserves feedback and reads v1 results without it" { const alloc = std.testing.allocator; const provider_state = types.ProviderReplay{ .source = .{ .provider = .gateway, .model = "test" }, .parts_json = "[{\"type\":\"reasoning\",\"text\":\"kept\"}]" }; var feedback = [_][]u8{@constCast("read it after writing")}; const presentation_lines = [_]types.CommittedFilePresentationLine{ .{ .kind = .addition, .new_line = 1, .text = "persisted line" }, .{ .kind = .notice, .text = "… 118 lines omitted" }, }; var calls = [_]session.ToolCall{.{ .id = @constCast("call_feedback"), .name = @constCast("write_file"), .arguments_json = @constCast("{\"path\":\"note.txt\"}"), }}; var results = [_]session.PersistedToolResult{.{ .tool_call_id = @constCast("call_feedback"), .tool_name = @constCast("write_file"), .status = .success, .output = @constCast("wrote note.txt"), .tool_image_handle = @constCast("saved-tool-image"), .output_bytes = 15, .stored_output_bytes = 15, .permission_feedback = feedback[0..], .committed_file_presentation = .{ .path = "note.txt", .kind = .added, .lines = presentation_lines[0..], .additions = 120, .deletions = 0, .truncated = true, .after_content = "persisted line\n", .lifecycle_id = .{ .turn_id = 9, .call_id = "call_feedback", }, }, .command_output_replay = .{ .available = .{ .handle = "fx-command-replay-private.bin", .framed_bytes = 321, } }, .command_process_presentation = .{ .exit_code = 7 }, }}; var steps = [_]session.ToolExecutionStep{.{ .provider_replay = provider_state, .tool_calls = calls[0..], .tool_results = results[0..], }}; var steering = [_]types.PersistedSteering{.{ .text = @constCast("focus on persistence"), .assistant_prefix = @constCast("partial assistant"), .after_tool_step_count = 1, }}; const turn: session.HistoryTurn = .{ .assistant = .{ .provider_replay = provider_state, .user = .{ .text = @constCast("write a note") }, .assistant = @constCast("done"), .execution = .{ .tool_steps = steps[0..], .steering = steering[0..], .turn_summary = .{ .started_at_ms = 100, .completed_at_ms = 250, .thinking_duration_ms = 40, .turn_duration_ms = 150, .token_progress = .{ .input_tokens = 12, .output_tokens = 34 }, }, }, } }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, turn); try std.testing.expect(std.mem.find(u8, encoded.written(), "\"schema_version\":10") != null); try std.testing.expect(std.mem.find(u8, encoded.written(), "\"text\":\"focus on persistence\"") != null); try std.testing.expect(std.mem.find(u8, encoded.written(), "\"assistant_prefix\":\"partial assistant\"") != null); try std.testing.expect(std.mem.find(u8, encoded.written(), "\"after_tool_step_count\":1") != null); try std.testing.expect(std.mem.find(u8, encoded.written(), "\"permission_feedback\"") != null); try std.testing.expect(std.mem.find(u8, encoded.written(), "\"committed_file_presentation\"") != null); try std.testing.expect(std.mem.find(u8, encoded.written(), "\"command_output_replay\"") != null); try std.testing.expect(std.mem.find(u8, encoded.written(), "fx-command-replay-private.bin") != null); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); const decoded = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, decoded); const decoded_result = decoded.assistant.execution.tool_steps[0].tool_results[0]; try std.testing.expectEqualStrings(provider_state.parts_json, decoded.assistant.provider_replay.?.parts_json); try std.testing.expectEqualStrings(provider_state.parts_json, decoded.assistant.execution.tool_steps[0].provider_replay.?.parts_json); try std.testing.expectEqualStrings("saved-tool-image", decoded_result.tool_image_handle.?); try std.testing.expectEqual( turn.assistant.execution.turn_summary, decoded.assistant.execution.turn_summary, ); try std.testing.expectEqual(@as(usize, 1), decoded.assistant.execution.steering.len); try std.testing.expectEqualStrings( "focus on persistence", decoded.assistant.execution.steering[0].text, ); try std.testing.expectEqual( @as(usize, 1), decoded.assistant.execution.steering[0].after_tool_step_count, ); try std.testing.expectEqualStrings( "partial assistant", decoded.assistant.execution.steering[0].assistant_prefix.?, ); try std.testing.expectEqual(@as(usize, 1), decoded_result.permission_feedback.len); try std.testing.expectEqualStrings("read it after writing", decoded_result.permission_feedback[0]); const presentation = decoded_result.committed_file_presentation orelse return error.TestExpectedPresentation; try std.testing.expectEqualStrings("note.txt", presentation.path); try std.testing.expectEqual(types.CommittedFilePresentationKind.added, presentation.kind); try std.testing.expectEqual(@as(usize, 2), presentation.lines.len); try std.testing.expectEqual(types.CommittedFilePresentationLineKind.notice, presentation.lines[1].kind); try std.testing.expectEqualStrings("… 118 lines omitted", presentation.lines[1].text); try std.testing.expectEqual(@as(usize, 120), presentation.additions); try std.testing.expectEqualStrings("persisted line\n", presentation.after_content.?); try std.testing.expectEqual(@as(u64, 9), presentation.lifecycle_id.?.turn_id); try std.testing.expectEqualStrings("call_feedback", presentation.lifecycle_id.?.call_id); const replay = decoded_result.command_output_replay orelse return error.TestExpectedReplay; const descriptor = switch (replay) { .available => |value| value, .unavailable => return error.TestExpectedReplay, }; try std.testing.expectEqualStrings("fx-command-replay-private.bin", descriptor.handle); try std.testing.expectEqual(@as(usize, 321), descriptor.framed_bytes); try std.testing.expectEqual( types.CommandProcessPresentation{ .exit_code = 7 }, decoded_result.command_process_presentation.?, ); try std.testing.expect(parsed.value.object.swapRemove("provider_replay")); const legacy_execution = parsed.value.object.getPtr("execution").?; legacy_execution.object.getPtr("schema_version").?.* = .{ .integer = 8 }; try std.testing.expect(legacy_execution.object.getPtr("tool_steps").?.array.items[0].object.swapRemove("provider_replay")); try std.testing.expect(legacy_execution.object.getPtr("tool_steps").?.array.items[0].object.getPtr("tool_results").?.array.items[0].object.swapRemove("review_feedback")); const v8_decoded = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, v8_decoded); try std.testing.expect(v8_decoded.assistant.provider_replay == null); try std.testing.expect(v8_decoded.assistant.execution.tool_steps[0].provider_replay == null); try std.testing.expectEqualStrings("saved-tool-image", v8_decoded.assistant.execution.tool_steps[0].tool_results[0].tool_image_handle.?); const v1 = "{\"kind\":\"assistant\",\"user\":{\"text\":\"write a note\",\"images\":[]},\"assistant\":\"done\",\"execution\":{\"schema_version\":1,\"tool_steps\":[{\"assistant\":null,\"tool_calls\":[{\"id\":\"call_feedback\",\"name\":\"write_file\",\"arguments_json\":\"{\\\"path\\\":\\\"note.txt\\\"}\",\"provider_result\":null}],\"tool_results\":[{\"tool_call_id\":\"call_feedback\",\"tool_name\":\"write_file\",\"status\":\"success\",\"output\":\"wrote note.txt\",\"output_handle\":null,\"preview\":null,\"output_bytes\":15,\"stored_output_bytes\":15,\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1}]}],\"files\":[]}}"; var v1_parsed = try std.json.parseFromSlice(std.json.Value, alloc, v1, .{}); defer v1_parsed.deinit(); const v1_decoded = try parseHistoryTurn(alloc, v1_parsed.value); defer session.freeHistoryTurn(alloc, v1_decoded); try std.testing.expectEqual( @as(usize, 0), v1_decoded.assistant.execution.tool_steps[0].tool_results[0].permission_feedback.len, ); try std.testing.expect(v1_decoded.assistant.execution.tool_steps[0].tool_results[0].command_output_replay == null); try std.testing.expect(v1_decoded.assistant.execution.tool_steps[0].tool_results[0].command_process_presentation == null); const v2 = "{\"kind\":\"assistant\",\"user\":{\"text\":\"write a note\",\"images\":[]},\"assistant\":\"done\",\"execution\":{\"schema_version\":2,\"tool_steps\":[{\"assistant\":null,\"tool_calls\":[{\"id\":\"call_feedback\",\"name\":\"write_file\",\"arguments_json\":\"{\\\"path\\\":\\\"note.txt\\\"}\",\"provider_result\":null}],\"tool_results\":[{\"tool_call_id\":\"call_feedback\",\"tool_name\":\"write_file\",\"status\":\"success\",\"output\":\"wrote note.txt\",\"output_handle\":null,\"preview\":null,\"output_bytes\":15,\"stored_output_bytes\":15,\"truncated\":false,\"provider_native\":false,\"created_at_ms\":1,\"permission_feedback\":[]}]}],\"files\":[]}}"; var v2_parsed = try std.json.parseFromSlice(std.json.Value, alloc, v2, .{}); defer v2_parsed.deinit(); const v2_decoded = try parseHistoryTurn(alloc, v2_parsed.value); defer session.freeHistoryTurn(alloc, v2_decoded); try std.testing.expect(v2_decoded.assistant.execution.tool_steps[0].tool_results[0].committed_file_presentation == null); try std.testing.expect(v2_decoded.assistant.execution.tool_steps[0].tool_results[0].command_output_replay == null); try std.testing.expect(v2_decoded.assistant.execution.tool_steps[0].tool_results[0].command_process_presentation == null); const v5 = "{\"kind\":\"assistant\",\"user\":{\"text\":\"prompt\",\"images\":[]},\"assistant\":\"done\",\"execution\":{\"schema_version\":5,\"tool_steps\":[],\"files\":[],\"turn_summary\":null}}"; var v5_parsed = try std.json.parseFromSlice(std.json.Value, alloc, v5, .{}); defer v5_parsed.deinit(); const v5_decoded = try parseHistoryTurn(alloc, v5_parsed.value); defer session.freeHistoryTurn(alloc, v5_decoded); try std.testing.expectEqual(@as(usize, 0), v5_decoded.assistant.execution.steering.len); const v6 = "{\"kind\":\"assistant\",\"user\":{\"text\":\"prompt\",\"images\":[]},\"assistant\":\"done\",\"execution\":{\"schema_version\":6,\"tool_steps\":[],\"files\":[],\"steering\":[\"legacy steer\"],\"turn_summary\":null}}"; var v6_parsed = try std.json.parseFromSlice(std.json.Value, alloc, v6, .{}); defer v6_parsed.deinit(); const v6_decoded = try parseHistoryTurn(alloc, v6_parsed.value); defer session.freeHistoryTurn(alloc, v6_decoded); try std.testing.expectEqualStrings("legacy steer", v6_decoded.assistant.execution.steering[0].text); try std.testing.expectEqual(@as(usize, 0), v6_decoded.assistant.execution.steering[0].after_tool_step_count); const invalid_v7 = "{\"kind\":\"assistant\",\"user\":{\"text\":\"prompt\",\"images\":[]},\"assistant\":\"done\",\"execution\":{\"schema_version\":7,\"tool_steps\":[],\"files\":[],\"steering\":[{\"text\":\"invalid\",\"assistant_prefix\":null,\"after_tool_step_count\":1}],\"turn_summary\":null}}"; var invalid_v7_parsed = try std.json.parseFromSlice(std.json.Value, alloc, invalid_v7, .{}); defer invalid_v7_parsed.deinit(); try std.testing.expectError( error.InvalidSessionFormat, parseHistoryTurn(alloc, invalid_v7_parsed.value), ); } fn checkV7SteeringAllocationFailures(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 parsePersistedSteering(alloc, parsed.value, 7, 1); defer types.freePersistedSteering(alloc, steering); try std.testing.expectEqual(@as(usize, 2), steering.len); } test "v7 steering cleans up every allocation failure" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkV7SteeringAllocationFailures, .{}, ); } test "private codec preserves summary-only interrupted turns" { const alloc = std.testing.allocator; const summary = types.TurnSummary{ .started_at_ms = 100, .completed_at_ms = 250, .thinking_duration_ms = 40, .turn_duration_ms = 150, .token_progress = .{ .input_tokens = 12, .output_tokens = 34 }, }; const turns = [_]session.HistoryTurn{.{ .interrupted = .{ .user = .{ .text = @constCast("inspect repository") }, .execution = .{ .turn_summary = summary }, } }}; for (turns) |turn| { var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, turn); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); const decoded = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, decoded); try std.testing.expectEqual(summary, types.historyTurnSummary(decoded).?); } } test "command process presentation codec preserves every terminal cause" { const alloc = std.testing.allocator; const cases = [_]types.CommandProcessPresentation{ .{ .exit_code = 7 }, .{ .signal = 9 }, .timed_out, .output_capture_failed, }; for (cases) |case| { var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeOptionalCommandProcessPresentation(&encoded.writer, case); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); try std.testing.expectEqual( case, (try parseOptionalCommandProcessPresentation(parsed.value)).?, ); } } test "terminal action presentation codec preserves return and failure causes" { const alloc = std.testing.allocator; const cases = [_]types.TerminalActionPresentation{ .{ .returned = .started }, .{ .returned = .condition_met }, .{ .returned = .safety_ceiling }, .{ .returned = .cancelled }, .{ .returned = .{ .exited = 7 } }, .{ .returned = .{ .signal = 9 } }, .{ .failed = .session_not_found }, .{ .failed = .capacity_exceeded }, }; for (cases) |case| { var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeOptionalTerminalActionPresentation(&encoded.writer, case); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); try std.testing.expectEqual( case, (try parseOptionalTerminalActionPresentation(parsed.value)).?, ); } } test "durable cancellation provenance defaults old records and preserves ordinary bytes" { const alloc = std.testing.allocator; const old = "{\"kind\":\"interrupted\",\"user\":{\"text\":\"request\",\"images\":[]},\"assistant\":\"partial\",\"tool_call\":null,\"completed_tool_names\":[]}"; var parsed = try std.json.parseFromSlice(std.json.Value, alloc, old, .{}); defer parsed.deinit(); const turn = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, turn); try std.testing.expectEqual(types.CancellationOrigin.turn, turn.interrupted.cancellation_origin); try std.testing.expectEqual(types.InterruptedTerminalReason.cancelled, turn.interrupted.terminal_reason); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeHistoryTurn(&out.writer, turn); try std.testing.expectEqualStrings(old[0 .. old.len - 1] ++ ",\"terminal_reason\":\"cancelled\"}", out.written()); } test "durable cancellation provenance roundtrips and cleans allocation failures" { const Case = struct { fn run(alloc: Allocator, origin: types.CancellationOrigin, reason: types.InterruptedTerminalReason) !void { const turn: session.HistoryTurn = .{ .interrupted = .{ .user = .{ .text = @constCast("request") }, .assistant = @constCast("partial"), .terminal_reason = reason, .cancellation_origin = origin, } }; const copy = try session.dupeHistoryTurn(alloc, turn); defer session.freeHistoryTurn(alloc, copy); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); // This in-memory writer reports injected allocation failure as WriteFailed. writeHistoryTurn(&out.writer, copy) catch |err| return if (err == error.WriteFailed) error.OutOfMemory else err; try std.testing.expectEqual(origin == .compaction, std.mem.find(u8, out.written(), "\"cancellation_origin\"") != null); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, out.written(), .{}); defer parsed.deinit(); const decoded = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, decoded); try std.testing.expectEqual(origin, decoded.interrupted.cancellation_origin); try std.testing.expectEqual(reason, decoded.interrupted.terminal_reason); try std.testing.expectEqualStrings("request", decoded.interrupted.user.text); try std.testing.expectEqualStrings("partial", decoded.interrupted.assistant.?); var again: std.Io.Writer.Allocating = .init(alloc); defer again.deinit(); writeHistoryTurn(&again.writer, decoded) catch |err| return if (err == error.WriteFailed) error.OutOfMemory else err; try std.testing.expectEqualStrings(out.written(), again.written()); } }; for (std.enums.values(types.CancellationOrigin)) |origin| { for (std.enums.values(types.InterruptedTerminalReason)) |reason| { try std.testing.checkAllAllocationFailures(std.testing.allocator, Case.run, .{ origin, reason }); } } } test "durable cancellation provenance accepts explicit turn and rejects invalid or unknown fields" { const alloc = std.testing.allocator; const prefix = "{\"kind\":\"interrupted\",\"user\":{\"text\":\"request\",\"images\":[]},\"assistant\":\"partial\",\"tool_call\":null,\"completed_tool_names\":[\"read_file\"],\"cancellation_origin\":"; for ([_][]const u8{ "\"turn\"", "\"compaction\"", "\"unknown\"", "null", "0", "true", "{}", "[]", "\"compaction\",\"unknown_field\":true" }, 0..) |value, i| { const bytes = try std.fmt.allocPrint(alloc, "{s}{s}}}", .{ prefix, value }); defer alloc.free(bytes); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, bytes, .{}); defer parsed.deinit(); if (i < 2) { const turn = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, turn); try std.testing.expectEqual(if (i == 0) types.CancellationOrigin.turn else .compaction, turn.interrupted.cancellation_origin); } else { try std.testing.expectError(error.InvalidSessionFormat, parseHistoryTurn(alloc, parsed.value)); } } } test "durable history rejects unknown fields instead of silently dropping bytes" { const alloc = std.testing.allocator; var parsed = try std.json.parseFromSlice( std.json.Value, alloc, "{\"kind\":\"compacted_summary\",\"summary\":\"kept\",\"removed_turn_count\":1,\"compaction_count\":1,\"future_bytes\":\"lost\"}", .{}, ); defer parsed.deinit(); try std.testing.expectError( error.InvalidSessionFormat, parseHistoryTurn(alloc, parsed.value), ); } test "legacy compacted authority absence remains incomplete across durable reload" { const alloc = std.testing.allocator; var legacy_parsed = try std.json.parseFromSlice( std.json.Value, alloc, "{\"kind\":\"compacted_summary\",\"summary\":\"legacy\",\"removed_turn_count\":2,\"compaction_count\":1}", .{}, ); defer legacy_parsed.deinit(); const legacy = try parseHistoryTurn(alloc, legacy_parsed.value); defer session.freeHistoryTurn(alloc, legacy); try std.testing.expect(!legacy.compacted_summary.root_user_messages_complete); try std.testing.expect(!legacy.compacted_summary.permission_feedback_complete); var known_messages = [_][]u8{@constCast("newer exact request")}; const incomplete: session.HistoryTurn = .{ .compacted_summary = .{ .summary = @constCast("legacy plus newer context"), .removed_turn_count = 3, .compaction_count = 2, .root_user_messages = &known_messages, .root_user_messages_complete = false, .permission_feedback = &known_messages, .permission_feedback_complete = false, } }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, incomplete); var reloaded_parsed = try std.json.parseFromSlice( std.json.Value, alloc, encoded.written(), .{}, ); defer reloaded_parsed.deinit(); const reloaded = try parseHistoryTurn(alloc, reloaded_parsed.value); defer session.freeHistoryTurn(alloc, reloaded); try std.testing.expect(!reloaded.compacted_summary.root_user_messages_complete); try std.testing.expect(!reloaded.compacted_summary.permission_feedback_complete); try std.testing.expectEqual(@as(usize, 0), reloaded.compacted_summary.root_user_messages.len); var first_generation_parsed = try std.json.parseFromSlice( std.json.Value, alloc, "{\"kind\":\"compacted_summary\",\"summary\":\"first generation\",\"removed_turn_count\":2,\"compaction_count\":1,\"root_user_messages\":[\"original request\"]}", .{}, ); defer first_generation_parsed.deinit(); const first_generation = try parseHistoryTurn(alloc, first_generation_parsed.value); defer session.freeHistoryTurn(alloc, first_generation); try std.testing.expect(first_generation.compacted_summary.root_user_messages_complete); try std.testing.expect(!first_generation.compacted_summary.permission_feedback_complete); try std.testing.expectEqualStrings( "original request", first_generation.compacted_summary.root_user_messages[0], ); } fn persistedResultForTest(call_id: []const u8, tool_name: []const u8) session.PersistedToolResult { return .{ .tool_call_id = @constCast(call_id), .tool_name = @constCast(tool_name), .status = .success, .output = @constCast("stale"), .output_bytes = 5, .stored_output_bytes = 5, .truncated = false, .provider_native = false, .created_at_ms = 1, }; } fn expectMalformedPairRejected( calls: []session.ToolCall, results: []session.PersistedToolResult, ) !void { var steps = [_]session.ToolExecutionStep{.{ .tool_calls = calls, .tool_results = results, }}; const turn: session.HistoryTurn = .{ .assistant = .{ .user = .{ .text = @constCast("inspect") }, .assistant = @constCast("failed"), .execution = .{ .tool_steps = steps[0..] }, } }; var encoded: std.Io.Writer.Allocating = .init(std.testing.allocator); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, turn); var parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, encoded.written(), .{}); defer parsed.deinit(); try std.testing.expectError( error.InvalidSessionFormat, parseHistoryTurn(std.testing.allocator, parsed.value), ); } fn checkCurrentToolArgumentRepairAllocationFailures(alloc: Allocator) !void { var feedback = [_][]u8{@constCast("read the repaired file")}; var calls = [_]session.ToolCall{.{ .id = @constCast("call_bad"), .name = @constCast("read_file"), .arguments_json = @constCast("{\"depth\":1,\"depth\":2}"), }}; var results = [_]session.PersistedToolResult{persistedResultForTest("call_bad", "read_file")}; results[0].permission_feedback = feedback[0..]; var steps = [_]session.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; const turn: session.HistoryTurn = .{ .assistant = .{ .user = .{ .text = @constCast("inspect") }, .assistant = @constCast("failed"), .execution = .{ .tool_steps = steps[0..] }, } }; var encoded: std.Io.Writer.Allocating = .init(std.testing.allocator); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, turn); var parsed = try std.json.parseFromSlice( std.json.Value, std.testing.allocator, encoded.written(), .{}, ); defer parsed.deinit(); const decoded = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, decoded); const step = decoded.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); try std.testing.expectEqual(@as(usize, 1), step.tool_results[0].permission_feedback.len); } test "durable specialized history accepts only legacy or complete extended shapes" { const alloc = std.testing.allocator; const empty_execution = "{\"schema_version\":1,\"tool_steps\":[],\"files\":[]}"; const cases = [_]struct { json: []const u8, expected: enum { background_legacy, background_extended, interrupted_legacy, interrupted_extended }, }{ .{ .json = "{\"kind\":\"background_command\",\"user\":{\"text\":\"run\",\"images\":[]},\"log_path\":\"/tmp/log\",\"expect_url\":false,\"url\":null,\"background_record_id\":null}", .expected = .background_legacy, }, .{ .json = "{\"kind\":\"background_command\",\"user\":{\"text\":\"run\",\"images\":[]},\"log_path\":\"/tmp/log\",\"expect_url\":false,\"url\":null,\"background_record_id\":null,\"assistant\":\"candidate\",\"execution\":" ++ empty_execution ++ "}", .expected = .background_extended, }, .{ .json = "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":null,\"tool_call\":null,\"completed_tool_names\":[]}", .expected = .interrupted_legacy, }, .{ .json = "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":\"candidate\",\"tool_call\":null,\"completed_tool_names\":[],\"execution\":" ++ empty_execution ++ "}", .expected = .interrupted_extended, }, }; for (cases) |case| { var parsed = try std.json.parseFromSlice(std.json.Value, alloc, case.json, .{}); defer parsed.deinit(); const turn = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, turn); switch (case.expected) { .background_legacy => { try std.testing.expect(turn == .assistant); try std.testing.expect(std.mem.find(u8, turn.assistant.assistant, "no longer owns") != null); try std.testing.expect(turn.assistant.execution.isEmpty()); }, .background_extended => { try std.testing.expect(turn == .assistant); try std.testing.expect(std.mem.find(u8, turn.assistant.assistant, "candidate") != null); try std.testing.expect(std.mem.find(u8, turn.assistant.assistant, "no longer owns") != null); try std.testing.expect(turn.assistant.execution.isEmpty()); }, .interrupted_legacy => try std.testing.expect(turn.interrupted.execution.isEmpty()), .interrupted_extended => { try std.testing.expectEqualStrings("candidate", turn.interrupted.assistant.?); try std.testing.expect(turn.interrupted.execution.isEmpty()); }, } } const invalid = [_][]const u8{ "{\"kind\":\"background_command\",\"user\":{\"text\":\"run\",\"images\":[]},\"log_path\":\"/tmp/log\",\"expect_url\":false,\"url\":null,\"background_record_id\":null,\"assistant\":\"partial\"}", "{\"kind\":\"background_command\",\"user\":{\"text\":\"run\",\"images\":[]},\"log_path\":\"/tmp/log\",\"expect_url\":false,\"url\":null,\"background_record_id\":null,\"assistant\":\"candidate\",\"execution\":" ++ empty_execution ++ ",\"future\":true}", "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":null,\"tool_call\":null,\"completed_tool_names\":[],\"future\":true}", }; for (invalid) |json| { var parsed = try std.json.parseFromSlice(std.json.Value, alloc, json, .{}); defer parsed.deinit(); try std.testing.expectError(error.InvalidSessionFormat, parseHistoryTurn(alloc, parsed.value)); } const migrated = session.HistoryTurn{ .assistant = .{ .user = .{ .text = @constCast("run") }, .assistant = @constCast("historical command"), } }; var legacy_encoded: std.Io.Writer.Allocating = .init(alloc); defer legacy_encoded.deinit(); try writeHistoryTurn(&legacy_encoded.writer, migrated); try std.testing.expect(std.mem.find(u8, legacy_encoded.written(), "\"kind\":\"assistant\"") != null); try std.testing.expect(std.mem.find(u8, legacy_encoded.written(), "background_command") == null); } test "interrupted command presentation is strict and round trips" { const alloc = std.testing.allocator; const turn: session.HistoryTurn = .{ .interrupted = .{ .user = .{ .text = @constCast("stop") }, .tool_call = .{ .id = "call-command", .name = "run_command", .arguments_json = "{\"command\":\"slow\"}", }, .cancelled_command = .{ .output_replay = .{ .available = .{ .handle = "fx-command-replay.bin", .framed_bytes = 42, } }, .command_artifact_handle = "fx-command.log", }, } }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, turn); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, encoded.written(), .{}); defer parsed.deinit(); const decoded = try parseHistoryTurn(alloc, parsed.value); defer session.freeHistoryTurn(alloc, decoded); const presentation = decoded.interrupted.cancelled_command.?; const descriptor = switch (presentation.output_replay.?) { .available => |value| value, .unavailable => return error.TestExpectedReplay, }; try std.testing.expectEqualStrings("fx-command-replay.bin", descriptor.handle); try std.testing.expectEqual(@as(usize, 42), descriptor.framed_bytes); try std.testing.expectEqualStrings("fx-command.log", presentation.command_artifact_handle.?); try std.testing.expectEqual(types.InterruptedTerminalReason.cancelled, decoded.interrupted.terminal_reason); const failed_turn: session.HistoryTurn = .{ .interrupted = .{ .user = .{ .text = @constCast("stream") }, .assistant = @constCast("partial"), .terminal_reason = .failed, } }; var failed_encoded: std.Io.Writer.Allocating = .init(alloc); defer failed_encoded.deinit(); try writeHistoryTurn(&failed_encoded.writer, failed_turn); try std.testing.expect(std.mem.find(u8, failed_encoded.written(), "\"terminal_reason\":\"failed\"") != null); var failed_parsed = try std.json.parseFromSlice(std.json.Value, alloc, failed_encoded.written(), .{}); defer failed_parsed.deinit(); const failed_decoded = try parseHistoryTurn(alloc, failed_parsed.value); defer session.freeHistoryTurn(alloc, failed_decoded); try std.testing.expectEqual(types.InterruptedTerminalReason.failed, failed_decoded.interrupted.terminal_reason); const zero_output = "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]}," ++ "\"assistant\":null,\"tool_call\":{\"id\":\"call\",\"name\":\"run_command\"," ++ "\"arguments_json\":\"{}\",\"provider_result\":null},\"completed_tool_names\":[]," ++ "\"cancelled_command\":{\"output_replay\":null,\"command_artifact_handle\":null}}"; var zero_parsed = try std.json.parseFromSlice(std.json.Value, alloc, zero_output, .{}); defer zero_parsed.deinit(); const zero_turn = try parseHistoryTurn(alloc, zero_parsed.value); defer session.freeHistoryTurn(alloc, zero_turn); try std.testing.expect(zero_turn.interrupted.cancelled_command != null); try std.testing.expectEqual(types.InterruptedTerminalReason.cancelled, zero_turn.interrupted.terminal_reason); const invalid = [_][]const u8{ "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":null,\"tool_call\":null,\"completed_tool_names\":[],\"cancelled_command\":{\"output_replay\":null,\"command_artifact_handle\":null}}", "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":null,\"tool_call\":{\"id\":\"call\",\"name\":\"read_file\",\"arguments_json\":\"{}\",\"provider_result\":null},\"completed_tool_names\":[],\"cancelled_command\":{\"output_replay\":null,\"command_artifact_handle\":null}}", "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":null,\"tool_call\":{\"id\":\"call\",\"name\":\"run_command\",\"arguments_json\":\"{}\",\"provider_result\":null},\"completed_tool_names\":[],\"cancelled_command\":{\"output_replay\":null,\"command_artifact_handle\":null,\"future\":true}}", "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":null,\"tool_call\":{\"id\":\"call\",\"name\":\"run_command\",\"arguments_json\":\"{}\",\"provider_result\":null},\"completed_tool_names\":[],\"terminal_reason\":\"failed\",\"cancelled_command\":{\"output_replay\":null,\"command_artifact_handle\":null}}", "{\"kind\":\"interrupted\",\"user\":{\"text\":\"stop\",\"images\":[]},\"assistant\":null,\"tool_call\":null,\"completed_tool_names\":[],\"terminal_reason\":\"future\"}", }; for (invalid) |json| { var invalid_parsed = try std.json.parseFromSlice(std.json.Value, alloc, json, .{}); defer invalid_parsed.deinit(); try std.testing.expectError( error.InvalidSessionFormat, parseHistoryTurn(alloc, invalid_parsed.value), ); } } fn expectStateEqual(expected: DurableSessionState, actual: DurableSessionState) !void { try std.testing.expectEqualStrings(expected.id, actual.id); try std.testing.expectEqualStrings(expected.origin_workspace_root, actual.origin_workspace_root); try std.testing.expectEqualStrings(expected.workspace_root, actual.workspace_root); try std.testing.expectEqual(expected.created_at_ms, actual.created_at_ms); try std.testing.expectEqual(expected.updated_at_ms, actual.updated_at_ms); try std.testing.expectEqualStrings(expected.conversation_language.view(), actual.conversation_language.view()); try std.testing.expectEqualStrings(expected.preferences.model, actual.preferences.model); try std.testing.expectEqual(expected.preferences.provider, actual.preferences.provider); try std.testing.expectEqual(expected.preferences.effort, actual.preferences.effort); try std.testing.expectEqual(expected.preferences.fast_mode, actual.preferences.fast_mode); try std.testing.expectEqual(expected.context_history_start, actual.context_history_start); try std.testing.expectEqual(expected.total_input_tokens, actual.total_input_tokens); try std.testing.expectEqual(expected.total_output_tokens, actual.total_output_tokens); try std.testing.expectEqual(expected.subagent_child, actual.subagent_child); try expectPermissionStateEqual(expected.permission_state, actual.permission_state); try std.testing.expectEqual(expected.last_subagent_work_id != null, actual.last_subagent_work_id != null); if (expected.last_subagent_work_id) |work_id| { try std.testing.expectEqualStrings(work_id, actual.last_subagent_work_id.?); } try std.testing.expectEqual(expected.usage != null, actual.usage != null); if (expected.usage) |expected_usage| { var expected_json: std.Io.Writer.Allocating = .init(std.testing.allocator); defer expected_json.deinit(); try session_usage.writeSnapshot(&expected_json.writer, expected_usage); var actual_json: std.Io.Writer.Allocating = .init(std.testing.allocator); defer actual_json.deinit(); try session_usage.writeSnapshot(&actual_json.writer, actual.usage.?); try std.testing.expectEqualStrings(expected_json.written(), actual_json.written()); } try std.testing.expectEqual(expected.history.len, actual.history.len); for (expected.history, actual.history) |expected_turn, actual_turn| { try expectHistoryTurnEqual(expected_turn, actual_turn); } } fn expectPermissionStateEqual( expected: session_permission_state.State, actual: session_permission_state.State, ) !void { try std.testing.expectEqual(expected.version, actual.version); try std.testing.expectEqual(expected.next_generation, actual.next_generation); try std.testing.expectEqual(expected.rules.items.len, actual.rules.items.len); for (expected.rules.items, actual.rules.items) |expected_rule, actual_rule| { try std.testing.expectEqual(expected_rule.id, actual_rule.id); try std.testing.expectEqual(expected_rule.key.kind, actual_rule.key.kind); try std.testing.expectEqualSlices(u8, &expected_rule.key.digest, &actual_rule.key.digest); try std.testing.expectEqualSlices(u8, expected_rule.key.canonical, actual_rule.key.canonical); try std.testing.expectEqualStrings(expected_rule.display_identity, actual_rule.display_identity); try std.testing.expectEqual(expected_rule.decision, actual_rule.decision); try std.testing.expectEqual(expected_rule.generation, actual_rule.generation); } } fn expectHistoryTurnEqual(expected: session.HistoryTurn, actual: session.HistoryTurn) !void { try std.testing.expectEqual(std.meta.activeTag(expected), std.meta.activeTag(actual)); switch (expected) { .compacted_summary => |entry| { try std.testing.expectEqualSlices(u8, entry.summary, actual.compacted_summary.summary); try std.testing.expectEqual(entry.removed_turn_count, actual.compacted_summary.removed_turn_count); try std.testing.expectEqual(entry.compaction_count, actual.compacted_summary.compaction_count); try std.testing.expect(!actual.compacted_summary.root_user_messages_complete); try std.testing.expectEqual( @as(usize, 0), actual.compacted_summary.root_user_messages.len, ); try std.testing.expect(!actual.compacted_summary.permission_feedback_complete); try std.testing.expectEqual( @as(usize, 0), actual.compacted_summary.permission_feedback.len, ); }, .assistant => |entry| { const got = actual.assistant; try expectUserTurnEqual(entry.user, got.user); try std.testing.expectEqualSlices(u8, entry.assistant, got.assistant); try expectExecutionMemoryEqual(entry.execution, got.execution); }, .interrupted => |entry| { const got = actual.interrupted; try expectUserTurnEqual(entry.user, got.user); try expectOptionalBytesEqual(entry.assistant, got.assistant); try std.testing.expectEqual(entry.tool_call != null, got.tool_call != null); if (entry.tool_call) |call| try expectToolCallEqual(call, got.tool_call.?); try std.testing.expectEqual(entry.completed_tool_names.len, got.completed_tool_names.len); for (entry.completed_tool_names, got.completed_tool_names) |name, got_name| { try std.testing.expectEqualSlices(u8, name, got_name); } try expectExecutionMemoryEqual(entry.execution, got.execution); try std.testing.expectEqualDeep(entry.cancelled_command, got.cancelled_command); try std.testing.expectEqual(entry.terminal_reason, got.terminal_reason); }, } } fn expectExecutionMemoryEqual(expected: session.ExecutionMemory, actual: session.ExecutionMemory) !void { try std.testing.expectEqual(expected.turn_summary, actual.turn_summary); try std.testing.expectEqual(expected.steering.len, actual.steering.len); for (expected.steering, actual.steering) |steering, got_steering| { try std.testing.expectEqualSlices(u8, steering.text, got_steering.text); try expectOptionalBytesEqual(steering.assistant_prefix, got_steering.assistant_prefix); try std.testing.expectEqual(steering.after_tool_step_count, got_steering.after_tool_step_count); } try std.testing.expectEqual(expected.tool_steps.len, actual.tool_steps.len); for (expected.tool_steps, actual.tool_steps) |step, got_step| { try expectOptionalBytesEqual(step.assistant, got_step.assistant); try std.testing.expectEqual(step.tool_calls.len, got_step.tool_calls.len); for (step.tool_calls, got_step.tool_calls) |call, got_call| try expectToolCallEqual(call, got_call); try std.testing.expectEqual(step.tool_results.len, got_step.tool_results.len); for (step.tool_results, got_step.tool_results) |result, got_result| { try std.testing.expectEqualSlices(u8, result.tool_call_id, got_result.tool_call_id); try std.testing.expectEqualSlices(u8, result.tool_name, got_result.tool_name); try std.testing.expectEqual(result.status, got_result.status); try std.testing.expectEqualSlices(u8, result.output, got_result.output); try expectOptionalBytesEqual(result.output_handle, got_result.output_handle); try expectOptionalBytesEqual(result.preview, got_result.preview); try std.testing.expectEqual(result.output_bytes, got_result.output_bytes); try std.testing.expectEqual(result.stored_output_bytes, got_result.stored_output_bytes); try std.testing.expectEqual(result.truncated, got_result.truncated); try std.testing.expectEqual(result.provider_native, got_result.provider_native); try std.testing.expectEqual(result.review_feedback, got_result.review_feedback); try std.testing.expectEqual(result.created_at_ms, got_result.created_at_ms); } } try std.testing.expectEqual(expected.files.len, actual.files.len); for (expected.files, actual.files) |file, got_file| { try std.testing.expectEqualSlices(u8, file.path, got_file.path); try expectOptionalBytesEqual(file.new_path, got_file.new_path); try std.testing.expectEqualSlices(u8, file.tool_call_id, got_file.tool_call_id); try std.testing.expectEqualSlices(u8, file.tool_name, got_file.tool_name); try std.testing.expectEqual(file.action, got_file.action); try std.testing.expectEqual(file.status, got_file.status); try std.testing.expectEqual(file.model_view_covers_full_file, got_file.model_view_covers_full_file); try std.testing.expectEqual(file.stale, got_file.stale); } } fn expectUserTurnEqual(expected: session.UserTurn, actual: session.UserTurn) !void { try std.testing.expectEqualSlices(u8, expected.text, actual.text); try expectOptionalBytesEqual(expected.work_id, actual.work_id); try std.testing.expectEqual(expected.images.len, actual.images.len); for (expected.images, actual.images) |image, got| { try std.testing.expectEqual(image.id, got.id); try std.testing.expectEqualSlices(u8, image.path, got.path); try std.testing.expectEqualSlices(u8, image.media_type, got.media_type); try expectOptionalBytesEqual(image.snapshot_path, got.snapshot_path); try expectOptionalBytesEqual(image.snapshot_sha256, got.snapshot_sha256); } } fn expectToolCallEqual(expected: session.ToolCall, actual: session.ToolCall) !void { try std.testing.expectEqualSlices(u8, expected.id, actual.id); try std.testing.expectEqualSlices(u8, expected.name, actual.name); try std.testing.expectEqualSlices(u8, expected.arguments_json, actual.arguments_json); try expectOptionalBytesEqual(expected.provider_result, actual.provider_result); } fn expectOptionalBytesEqual(expected: anytype, actual: @TypeOf(expected)) !void { try std.testing.expectEqual(expected != null, actual != null); if (expected) |bytes| try std.testing.expectEqualSlices(u8, bytes, actual.?); } test "durable image snapshots serialize as session-relative locators" { const alloc = std.testing.allocator; var images = [_]session.ImageAttachment{.{ .id = 1, .path = @constCast("/Users/private/source.png"), .media_type = @constCast("image/png"), .snapshot_path = @constCast("/tmp/fx/sessions/session-id/images/image-1-deadbeef.bin"), .snapshot_sha256 = @constCast("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), }}; const turn: session.HistoryTurn = .{ .assistant = .{ .user = .{ .text = @constCast("[Image #1]"), .images = &images }, .assistant = @constCast("done"), } }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writeHistoryTurn(&encoded.writer, turn); try std.testing.expect(std.mem.find( u8, encoded.written(), "/tmp/fx/sessions/session-id/images", ) == null); try std.testing.expect(std.mem.find( u8, encoded.written(), "\"snapshot_path\":\"images/image-1-deadbeef.bin\"", ) != null); } test "codec structural helpers enforce exact objects and required strings" { const alloc = std.testing.allocator; var unknown_key = try std.json.parseFromSlice( std.json.Value, alloc, "{\"known\":\"value\",\"unexpected\":true}", .{}, ); defer unknown_key.deinit(); try std.testing.expectError( error.InvalidSessionFormat, exactObject(unknown_key.value, &.{"known"}), ); var non_string = try std.json.parseFromSlice( std.json.Value, alloc, "{\"known\":1}", .{}, ); defer non_string.deinit(); const non_string_object = try exactObject(non_string.value, &.{"known"}); try std.testing.expectError( error.InvalidSessionFormat, requireString(non_string_object, "known"), ); var valid = try std.json.parseFromSlice( std.json.Value, alloc, "{\"known\":\"value\"}", .{}, ); defer valid.deinit(); const valid_object = try exactObject(valid.value, &.{"known"}); try std.testing.expectEqualStrings("value", try requireString(valid_object, "known")); } test "recovery checkpoint round trips while legacy state stays absent" { const alloc = std.testing.allocator; const checkpoint = RecoveryCheckpoint{ .turn_id = 42, .user = .{ .text = @constCast("keep working") }, .assistant_source = @constCast("partial answer"), .cause = .response_interrupted, .action = .continuing_response, .tool_state = .confirmed, .authority = .{ .provider = .codex, .model = @constCast("gpt-5.4-mini"), .credential_source = .chatgpt_subscription, .credential_identity = credential_authority.derive( .chatgpt_subscription, "acct_1", ), }, .requested_fast_mode = true, .fast_mode = true, .max_provider_attempts = 10, .consumed_provider_attempts = 4, .outstanding_reservation = true, }; const state = DurableSessionState{ .id = @constCast("session-recovery"), .origin_workspace_root = @constCast("/tmp/origin"), .workspace_root = @constCast("/tmp/current"), .created_at_ms = 1, .updated_at_ms = 2, .conversation_language = session.ConversationLanguage.literal("en"), .preferences = .{ .provider = .codex, .model = @constCast("gpt-5.4-mini"), .effort = .auto, .fast_mode = false, }, .history = &.{}, .total_input_tokens = 0, .total_output_tokens = 0, .recovery_checkpoint = checkpoint, }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); _ = try encodeState(state, &encoded.writer); var source = std.Io.Reader.fixed(encoded.written()); var decoded = try decodeState(alloc, &source, .{}); defer decoded.deinit(alloc); const restored = decoded.recovery_checkpoint.?; try std.testing.expectEqual(@as(u64, 42), restored.turn_id); try std.testing.expectEqualStrings("keep working", restored.user.text); try std.testing.expectEqualStrings("partial answer", restored.assistant_source); try std.testing.expectEqual(types.ModelRecoveryCause.response_interrupted, restored.cause); try std.testing.expectEqual(types.ModelRecoveryAction.continuing_response, restored.action); try std.testing.expectEqual(RecoveryToolState.confirmed, restored.tool_state); try std.testing.expectEqual(model_provider.ProviderId.codex, restored.authority.provider); try std.testing.expectEqualStrings("gpt-5.4-mini", restored.authority.model); try std.testing.expectEqual(types.CredentialSource.chatgpt_subscription, restored.authority.credential_source.?); try std.testing.expect(restored.authority.credential_identity != null); try std.testing.expectEqual(model_provider.ProviderId.codex, decoded.preferences.provider); try std.testing.expect(restored.requested_fast_mode); try std.testing.expect(restored.fast_mode); try std.testing.expectEqual(@as(usize, 4), restored.consumed_provider_attempts); try std.testing.expect(restored.outstanding_reservation); const legacy = "{\"id\":\"legacy\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\",\"created_at_ms\":1,\"updated_at_ms\":2,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"openai/gpt-test\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}"; var legacy_source = std.Io.Reader.fixed(legacy); var legacy_state = try decodeState(alloc, &legacy_source, .{}); defer legacy_state.deinit(alloc); try std.testing.expectEqual(model_provider.ProviderId.gateway, legacy_state.preferences.provider); try std.testing.expectEqual(@as(?RecoveryCheckpoint, null), legacy_state.recovery_checkpoint); } test "recovery checkpoint accepts the exact composer byte limit" { const alloc = std.testing.allocator; const prompt = try alloc.alloc(u8, 8 * 1024 * 1024); defer alloc.free(prompt); @memset(prompt, 'x'); const checkpoint = RecoveryCheckpoint{ .turn_id = 1, .user = .{ .text = prompt }, .assistant_source = @constCast(""), .cause = .network_interrupted, .action = .retrying_request, .authority = .{ .provider = .gateway, .model = @constCast("test/model") }, .requested_fast_mode = false, .fast_mode = false, .max_provider_attempts = 3, .consumed_provider_attempts = 0, }; const encoded = try encodeRecoveryCheckpoint(alloc, checkpoint); defer alloc.free(encoded); try std.testing.expect(encoded.len > prompt.len); try std.testing.expect(encoded.len <= max_recovery_checkpoint_bytes); } test "session permission state round trips while legacy state stays empty" { const alloc = std.testing.allocator; const key = try session_permission_state.RuleKey.init( .command, "command\x00/workspace\x00git status", ); var empty_permission_state: session_permission_state.State = .{}; defer empty_permission_state.deinit(alloc); var applied = try session_permission_state.apply( alloc, empty_permission_state, .{ .set = .{ .key = key, .display_identity = "git status in /workspace", .decision = .deny, .expected_generation = null, } }, ); var permission_state = applied.takeApplied() orelse return error.TestExpectedAppliedState; defer permission_state.deinit(alloc); const state = DurableSessionState{ .id = @constCast("session-permission-state"), .origin_workspace_root = @constCast("/tmp/origin"), .workspace_root = @constCast("/tmp/current"), .created_at_ms = 1, .updated_at_ms = 2, .conversation_language = session.ConversationLanguage.literal("en"), .preferences = .{ .model = @constCast("openai/gpt-test"), .effort = .auto, .fast_mode = false, }, .history = &.{}, .total_input_tokens = 0, .total_output_tokens = 0, .permission_state = permission_state, }; var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); _ = try encodeState(state, &encoded.writer); var source = std.Io.Reader.fixed(encoded.written()); var decoded = try decodeState(alloc, &source, .{}); defer decoded.deinit(alloc); try std.testing.expectEqual( session_permission_state.StateDecision.deny, session_permission_state.decide(decoded.permission_state, key), ); const legacy = "{\"id\":\"legacy-permission-state\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\",\"created_at_ms\":1,\"updated_at_ms\":2,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"openai/gpt-test\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}"; var legacy_source = std.Io.Reader.fixed(legacy); var legacy_state = try decodeState(alloc, &legacy_source, .{}); defer legacy_state.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), legacy_state.permission_state.rules.items.len); } test "recovery checkpoint rejects an outstanding attempt beyond its budget" { const invalid = DurableSessionState{ .id = @constCast("session-recovery"), .origin_workspace_root = @constCast("/tmp/origin"), .workspace_root = @constCast("/tmp/current"), .created_at_ms = 1, .updated_at_ms = 2, .conversation_language = session.ConversationLanguage.literal("en"), .preferences = .{ .model = @constCast("openai/gpt-test"), .effort = .auto, .fast_mode = false, }, .history = &.{}, .total_input_tokens = 0, .total_output_tokens = 0, .recovery_checkpoint = .{ .turn_id = 1, .user = .{ .text = @constCast("prompt") }, .assistant_source = @constCast(""), .cause = .network_interrupted, .action = .retrying_request, .authority = .{ .provider = .gateway, .model = @constCast("openai/gpt-test") }, .requested_fast_mode = false, .fast_mode = false, .max_provider_attempts = 1, .consumed_provider_attempts = 1, .outstanding_reservation = true, }, }; try std.testing.expectError(error.InvalidDurableField, validateState(invalid)); } test "permission state schema two round trips before activation" { const alloc = std.testing.allocator; var state: session_permission_state.State = .{ .version = session_permission_state.schema_version, .next_generation = 2, }; defer state.deinit(alloc); const key = try session_permission_state.commandKeyV2( alloc, "git status", "/workspace", "foreground", "macos", ); try state.rules.append(alloc, .{ .id = .{ .value = 1 }, .key = key, .display_identity = try alloc.dupe(u8, "git status"), .decision = .deny, .generation = 1, }); var encoded: std.Io.Writer.Allocating = .init(alloc); defer encoded.deinit(); try writePermissionState(&encoded.writer, state); var json = try std.json.parseFromSlice( std.json.Value, alloc, encoded.written(), .{}, ); defer json.deinit(); var decoded = try parsePermissionState(alloc, json.value); defer decoded.deinit(alloc); try std.testing.expectEqual(session_permission_state.schema_version, decoded.version); try std.testing.expectEqual(@as(usize, 1), decoded.rules.items.len); try std.testing.expectEqual(session_permission_state.StateDecision.deny, session_permission_state.decide(decoded, key)); } test "durable state usage remains strict and releases partial allocations" { const alloc = std.testing.allocator; const state_json = \\{"id":"usage-state","origin_workspace_root":"/workspace","workspace_root":"/workspace","created_at_ms":1,"updated_at_ms":2, \\"conversation_language":"en","preferences":{"model":"test/model","effort":"auto","fast_mode":false},"history":[],"total_input_tokens":0,"total_output_tokens":0, \\"usage":{"billing":"complete","api_duration_complete":true,"wall_duration_complete":true,"code_complete":true,"next_sequence":2,"settled_through_sequence":1, \\"api_duration_ms":10,"wall_duration_ms":20,"total_cost":1,"input_tokens":10,"output_tokens":3,"cache_read_tokens":2,"cache_write_tokens":0,"billable_web_search_calls":0,"lines_added":0,"lines_removed":0, \\"models":[{"model":"test/model","first_sequence":1,"total_cost":1,"input_tokens":10,"output_tokens":3,"cache_read_tokens":2,"cache_write_tokens":0,"billable_web_search_calls":0}],"pending":[]}, \\"last_subagent_work_id":"legacy-work"} ; try std.testing.checkAllAllocationFailures(alloc, struct { fn check(a: Allocator, bytes: []const u8) !void { var source = std.Io.Reader.fixed(bytes); var decoded = try decodeState(a, &source, .{}); defer decoded.deinit(a); try std.testing.expectEqual(@as(u64, 10), decoded.usage.?.input_tokens); try std.testing.expectEqualStrings("legacy-work", decoded.last_subagent_work_id.?); } }.check, .{state_json}); const incompatible = try std.mem.replaceOwned(u8, alloc, state_json, "\"cache_read_tokens\":2", "\"cache_read_tokens\":11"); defer alloc.free(incompatible); var strict_source = std.Io.Reader.fixed(incompatible); try std.testing.expectError(error.InvalidSessionFormat, decodeState(alloc, &strict_source, .{})); } test "durable session optional fields handle fuzzed ownership paths" { try std.testing.fuzz({}, fuzzDurableSessionOptionalFields, .{ .corpus = &.{ "{\"id\":\"session\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"test/model\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0}", "{\"id\":\"session\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"test/model\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0,\"context_history_start\":0,\"last_subagent_work_id\":\"work\"}", "{\"id\":\"session\",\"origin_workspace_root\":\"/tmp/origin\",\"workspace_root\":\"/tmp/current\",\"created_at_ms\":1,\"updated_at_ms\":1,\"conversation_language\":\"en\",\"preferences\":{\"model\":\"test/model\",\"effort\":\"auto\",\"fast_mode\":false},\"history\":[],\"total_input_tokens\":0,\"total_output_tokens\":0,\"last_subagent_work_id\":\"work\",\"context_history_start\":0}", "", "{}", "{\"last_subagent_work_id\":null}", "{\"usage\":null,\"last_subagent_work_id\":\"work\"}", "{\"last_subagent_work_id\":\"work\",\"usage\":null}", } }); } test "per-turn work provenance parser handles fuzzed ownership paths" { try std.testing.fuzz({}, fuzzWorkProvenanceHistoryTurn, .{ .corpus = &.{ "{\"kind\":\"assistant\",\"user\":{\"text\":\"prompt\",\"images\":[],\"work_id\":\"work-1\"},\"assistant\":\"reply\",\"execution\":{\"schema_version\":3,\"tool_steps\":[],\"files\":[]}}", "{\"kind\":\"assistant\",\"user\":{\"text\":\"prompt\",\"images\":[]},\"assistant\":\"reply\",\"execution\":{\"schema_version\":3,\"tool_steps\":[],\"files\":[]}}", "{\"kind\":\"assistant\",\"user\":{\"text\":\"prompt\",\"images\":[],\"work_id\":\"\"}}", "", "{}", } }); } fn fuzzWorkProvenanceHistoryTurn(_: void, smith: *std.testing.Smith) !void { var buffer: [4096]u8 = undefined; const len: usize = @intCast(smith.slice(&buffer)); var parsed = std.json.parseFromSlice( std.json.Value, std.testing.allocator, buffer[0..len], .{ .allocate = .alloc_always }, ) catch return; defer parsed.deinit(); const turn = parseHistoryTurn(std.testing.allocator, parsed.value) catch return; session.freeHistoryTurn(std.testing.allocator, turn); } fn fuzzDurableSessionOptionalFields(_: void, smith: *std.testing.Smith) !void { var buffer: [8192]u8 = undefined; const len: usize = @intCast(smith.slice(&buffer)); var source = std.Io.Reader.fixed(buffer[0..len]); var state = decodeState(std.testing.allocator, &source, .{ .max_history_turns = 8, .max_value_bytes = buffer.len, }) catch return; state.deinit(std.testing.allocator); } test "session metadata round trips without conversation or control state" { const alloc = std.testing.allocator; const encoded = try encodeSessionMetadata(alloc, .{ .id = "session-1", .origin_workspace_root = "/tmp/origin", .workspace_root = "/tmp/current", .created_at_ms = 10, .updated_at_ms = 20, .conversation_language = "en", .provider = .gateway, .model = "openai/gpt-5.6", .effort = "high", .fast_mode = false, .title = "Compaction work", .subagent_child = false, }); defer alloc.free(encoded); try std.testing.expect(std.mem.find(u8, encoded, "history") == null); try std.testing.expect(std.mem.find(u8, encoded, "usage") == null); try std.testing.expect(std.mem.find(u8, encoded, "permission") == null); try std.testing.expect(std.mem.find(u8, encoded, "checkpoint") == null); var decoded = try decodeSessionMetadata(alloc, encoded); defer decoded.deinit(); try std.testing.expectEqualStrings("session-1", decoded.value.id); try std.testing.expectEqualStrings("/tmp/current", decoded.value.workspace_root); try std.testing.expectEqualStrings("openai/gpt-5.6", decoded.value.model); try std.testing.expectEqualStrings("Compaction work", decoded.value.title.?); }