const std = @import("std");const core_types = @import("../shared/types.zig");const debug_trace = @import("../shared/debug_trace.zig");const io_mod = @import("../shared/io.zig");const message = @import("../shared/message.zig");const text_utils = @import("../shared/text_utils.zig");const tool_result_errors = @import("../tooling/tool_result_errors.zig");const session_permission_state = @import("../permissions/session_permission_state.zig");const image_attachments = @import("../images/image_attachments.zig");const generation_usage_provider = @import("generation_usage_provider.zig");const web_fetch_artifacts = @import("web_fetch_artifacts.zig");pub const session_usage = @import("session_usage.zig");pub const profile_usage_runtime = @import("profile_usage_runtime.zig");const command_contract = @import("../execution/command_contract.zig");const sort_utils = @import("../shared/sort_utils.zig");const Allocator = std.mem.Allocator; test { _ = session_usage; _ = profile_usage_runtime;} const compact_continuation_preamble = "This session is being continued from earlier compacted context. The summary below covers the earlier portion of the conversation.\n\n";const compact_recent_messages_note = "Recent conversation turns are preserved verbatim.";const compact_direct_resume_instruction = "Continue the conversation from where it left off without asking the user to repeat context. Resume directly.";const compact_summary_max_chars: usize = 1200;const compact_summary_max_lines: usize = 24;const compact_summary_max_line_chars: usize = 160; pub const interrupted_turn_notice: core_types.SemanticNotice = .{ .topic = "system", .tone = .cancelled, .body = "cancelled",}; pub const failed_turn_notice: core_types.SemanticNotice = .{ .topic = "system", .tone = .@"error", .body = "failed",}; pub fn interruptedTurnNotice(entry: InterruptedHistoryTurn) core_types.SemanticNotice { return switch (entry.terminal_reason) { .cancelled => interrupted_turn_notice, .failed => failed_turn_notice, };} pub const interrupted_turn_context = "<turn_aborted>\n" ++ "The previous turn ended before completion. Any tools or commands may have partially executed. " ++ "Do not continue this request unless the user explicitly asks to continue.\n" ++ "</turn_aborted>"; pub fn projectSnapshotLocator(buffer: []u8, path: []const u8) ![]const u8 { if (!std.fs.path.isAbsolute(path)) return path; return std.fmt.bufPrint(buffer, "images/{s}", .{std.fs.path.basename(path)});} pub const interrupted_before_completion_output = "The previous response ended before completion.";pub const aborted_tool_output = "aborted by user"; /// Conversation language tag stored in session records.pub const ConversationLanguage = core_types.ConversationLanguage;/// Image attachment metadata stored on user turns.pub const ImageAttachment = core_types.ImageAttachment;/// User-side text and image attachments for a stored history turn.pub const UserTurn = core_types.UserTurn;/// Gateway tool call metadata stored when an interrupted turn is cancelled during a tool call.pub const ToolCall = core_types.ToolCall;/// Stored assistant response paired with the user turn that produced it.pub const AssistantHistoryTurn = core_types.AssistantHistoryTurn;/// Stored background command metadata paired with the user turn that produced it.pub const BackgroundCommandHistoryTurn = core_types.BackgroundCommandHistoryTurn;pub const StableBackgroundRecordId = core_types.StableBackgroundRecordId;pub const CancelledCommandPresentation = core_types.CancelledCommandPresentation;/// Stored interrupted turn marker, optionally paired with the active tool call.pub const InterruptedHistoryTurn = core_types.InterruptedHistoryTurn;pub const InterruptedTerminalReason = core_types.InterruptedTerminalReason;/// Stored compacted context summary produced by core history compaction.pub const CompactedSummaryHistoryTurn = core_types.CompactedSummaryHistoryTurn;/// One persisted session history record.pub const HistoryTurn = core_types.HistoryTurn;pub const freeUserTurn = core_types.freeUserTurn;pub const dupeUserTurn = core_types.dupeUserTurn;pub const max_work_id_bytes: usize = 128; pub const WorkIdAssociation = enum { none, copy_event, already_associated,}; pub const WorkIdError = error{ InvalidWorkId, ConflictingWorkId,}; pub fn validateWorkId(work_id: []const u8) WorkIdError!void { if (work_id.len == 0 or work_id.len > max_work_id_bytes or !std.unicode.utf8ValidateSlice(work_id) or std.mem.indexOfScalar(u8, work_id, 0) != null) { return error.InvalidWorkId; }} pub fn historyTurnWorkId(turn: HistoryTurn) ?[]const u8 { return switch (turn) { .assistant => |entry| entry.user.work_id, .background_command => |entry| entry.user.work_id, .interrupted => |entry| entry.user.work_id, .compacted_summary => null, };} /// Purely decides how authoritative event provenance relates to one turn.pub fn decideWorkIdAssociation( turn: HistoryTurn, event_work_id: ?[]const u8,) WorkIdError!WorkIdAssociation { const turn_work_id = historyTurnWorkId(turn); if (turn_work_id) |work_id| try validateWorkId(work_id); if (event_work_id) |work_id| try validateWorkId(work_id); if (event_work_id == null) { return if (turn_work_id == null) .none else error.ConflictingWorkId; } if (turn == .compacted_summary) return error.ConflictingWorkId; if (turn_work_id) |work_id| { return if (std.mem.eql(u8, work_id, event_work_id.?)) .already_associated else error.ConflictingWorkId; } return .copy_event;} /// Installs a separately owned work ID after pure association succeeds.pub fn copyWorkIdToTurn( alloc: Allocator, turn: *HistoryTurn, work_id: []const u8,) (Allocator.Error || WorkIdError)!void { if (try decideWorkIdAssociation(turn.*, work_id) != .copy_event) return; const owned = try alloc.dupe(u8, work_id); switch (turn.*) { .assistant => |*entry| entry.user.work_id = owned, .background_command => |*entry| entry.user.work_id = owned, .interrupted => |*entry| entry.user.work_id = owned, .compacted_summary => unreachable, }} pub const freeHistoryTurn = core_types.freeHistoryTurn;pub const dupeToolCall = core_types.dupeToolCall;pub const freeToolCall = core_types.freeToolCall;pub const dupeCompletedToolNames = core_types.dupeCompletedToolNames;pub const freeCompletedToolNames = core_types.freeCompletedToolNames;pub const ExecutionMemory = core_types.ExecutionMemory;pub const ToolExecutionStep = core_types.ToolExecutionStep;pub const PersistedToolResult = core_types.PersistedToolResult;pub const PersistedToolStatus = core_types.PersistedToolStatus;pub const FileEvidence = core_types.FileEvidence;pub const FileEvidenceAction = core_types.FileEvidenceAction;pub const freeExecutionMemory = core_types.freeExecutionMemory;pub const freeToolCallSlice = core_types.freeToolCallSlice;pub const freePersistedToolResults = core_types.freePersistedToolResults; /// Builds a sorted, independently owned attachment catalog from complete/// canonical history plus current-turn images. The caller frees the returned/// slice with `core_types.freeImageAttachmentSlice` using the same allocator.pub fn collect_image_catalog( alloc: Allocator, canonical_history: []const HistoryTurn, current_images: []const ImageAttachment,) (Allocator.Error || error{ ImageCatalogTooLarge, InvalidImageId, DuplicateImageId,})![]ImageAttachment { var attachment_count = current_images.len; for (canonical_history) |turn| { attachment_count = std.math.add( usize, attachment_count, images_for_history_turn(turn).len, ) catch return error.ImageCatalogTooLarge; } if (attachment_count == 0) return &.{}; const catalog = try alloc.alloc(ImageAttachment, attachment_count); errdefer alloc.free(catalog); var copied: usize = 0; errdefer for (catalog[0..copied]) |attachment| { core_types.freeImageAttachment(alloc, attachment); }; for (canonical_history) |turn| { for (images_for_history_turn(turn)) |attachment| { catalog[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } } for (current_images) |attachment| { catalog[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } sort_utils.sort(ImageAttachment, catalog, {}, image_id_less_than); var previous_id: ?usize = null; for (catalog) |attachment| { if (attachment.id == 0) return error.InvalidImageId; if (previous_id == attachment.id) return error.DuplicateImageId; previous_id = attachment.id; } std.debug.assert(image_attachments.imageAttachmentsSortedById(catalog)); return catalog;} /// Rebuilds an owned catalog after queue review replaces the current-turn/// attachments. Historical authority is retained; the old current slice must/// still match the catalog exactly or the replacement is rejected as stale.pub fn replace_image_catalog_current_images( alloc: Allocator, catalog: []const ImageAttachment, old_current_images: []const ImageAttachment, new_current_images: []const ImageAttachment,) (Allocator.Error || error{ ImageCatalogTooLarge, InvalidImageId, DuplicateImageId, StaleImageCatalog,})![]ImageAttachment { try validate_image_catalog(catalog); for (old_current_images, 0..) |old_current, index| { if (old_current.id == 0) return error.InvalidImageId; for (old_current_images[0..index]) |prior| { if (prior.id == old_current.id) return error.DuplicateImageId; } const authorized = image_attachment_for_id(catalog, old_current.id) orelse return error.StaleImageCatalog; if (!image_attachments_equal(authorized, old_current)) return error.StaleImageCatalog; } const retained_count = std.math.sub(usize, catalog.len, old_current_images.len) catch return error.StaleImageCatalog; const attachment_count = std.math.add( usize, retained_count, new_current_images.len, ) catch return error.ImageCatalogTooLarge; if (attachment_count == 0) return &.{}; const replacement = try alloc.alloc(ImageAttachment, attachment_count); errdefer alloc.free(replacement); var copied: usize = 0; errdefer for (replacement[0..copied]) |attachment| { core_types.freeImageAttachment(alloc, attachment); }; for (catalog) |attachment| { if (image_attachment_for_id(old_current_images, attachment.id) != null) continue; replacement[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } std.debug.assert(copied == retained_count); for (new_current_images) |attachment| { replacement[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } sort_utils.sort(ImageAttachment, replacement, {}, image_id_less_than); try validate_image_catalog(replacement); return replacement;} /// Returns a new owned catalog containing any image authority introduced by a/// completed history turn. Existing IDs must either match exactly or the/// queued catalog is rejected as stale.pub fn merge_image_catalog_history_turn( alloc: Allocator, catalog: []const ImageAttachment, turn: HistoryTurn,) (Allocator.Error || error{ ImageCatalogTooLarge, InvalidImageId, DuplicateImageId, StaleImageCatalog,})![]ImageAttachment { try validate_image_catalog(catalog); const additions = images_for_history_turn(turn); var missing_count: usize = 0; for (additions, 0..) |addition, index| { if (addition.id == 0) return error.InvalidImageId; for (additions[0..index]) |prior| { if (prior.id == addition.id) return error.DuplicateImageId; } if (image_attachment_for_id(catalog, addition.id)) |existing| { if (!image_attachments_equal(existing, addition)) return error.StaleImageCatalog; } else { missing_count += 1; } } const attachment_count = std.math.add(usize, catalog.len, missing_count) catch return error.ImageCatalogTooLarge; if (attachment_count == 0) return &.{}; const merged = try alloc.alloc(ImageAttachment, attachment_count); errdefer alloc.free(merged); var copied: usize = 0; errdefer for (merged[0..copied]) |attachment| { core_types.freeImageAttachment(alloc, attachment); }; for (catalog) |attachment| { merged[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } for (additions) |attachment| { if (image_attachment_for_id(catalog, attachment.id) != null) continue; merged[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } sort_utils.sort(ImageAttachment, merged, {}, image_id_less_than); try validate_image_catalog(merged); return merged;} fn validate_image_catalog(catalog: []const ImageAttachment) error{ InvalidImageId, DuplicateImageId,}!void { var previous_id: ?usize = null; for (catalog) |attachment| { if (attachment.id == 0) return error.InvalidImageId; if (previous_id != null and previous_id.? >= attachment.id) { if (previous_id.? == attachment.id) return error.DuplicateImageId; return error.InvalidImageId; } previous_id = attachment.id; }} fn image_attachment_for_id( attachments: []const ImageAttachment, id: usize,) ?ImageAttachment { for (attachments) |attachment| { if (attachment.id == id) return attachment; } return null;} fn image_attachments_equal(lhs: ImageAttachment, rhs: ImageAttachment) bool { return lhs.id == rhs.id and std.mem.eql(u8, lhs.path, rhs.path) and std.mem.eql(u8, lhs.media_type, rhs.media_type) and optionalBytesEqual(lhs.snapshot_path, rhs.snapshot_path) and optionalBytesEqual(lhs.snapshot_sha256, rhs.snapshot_sha256);} fn optionalBytesEqual(lhs: ?[]const u8, rhs: ?[]const u8) bool { if (lhs == null or rhs == null) return lhs == null and rhs == null; return std.mem.eql(u8, lhs.?, rhs.?);} fn images_for_history_turn(turn: HistoryTurn) []const ImageAttachment { return switch (turn) { .compacted_summary => &.{}, .assistant => |entry| entry.user.images, .background_command => |entry| entry.user.images, .interrupted => |entry| entry.user.images, };} fn mutable_images_for_history_turn(turn: *HistoryTurn) []ImageAttachment { return switch (turn.*) { .compacted_summary => &.{}, .assistant => |*entry| entry.user.images, .background_command => |*entry| entry.user.images, .interrupted => |*entry| entry.user.images, };} fn mutable_images_slice_for_history_turn(turn: *HistoryTurn) ?*[]ImageAttachment { return switch (turn.*) { .compacted_summary => null, .assistant => |*entry| &entry.user.images, .background_command => |*entry| &entry.user.images, .interrupted => |*entry| &entry.user.images, };} fn user_text_for_history_turn(turn: HistoryTurn) []const u8 { return switch (turn) { .compacted_summary => "", .assistant => |entry| entry.user.text, .background_command => |entry| entry.user.text, .interrupted => |entry| entry.user.text, };} const LegacyImagePlaceholderIterator = struct { text: []const u8, offset: usize = 0, const prefix = "[Image #"; const Slot = struct { id: ?usize, start: usize, length: usize, }; fn next(self: *@This()) ?Slot { const relative_start = std.mem.indexOf(u8, self.text[self.offset..], prefix) orelse return null; const start = self.offset + relative_start; if (image_attachments.matchImagePlaceholder(self.text, start)) |match| { self.offset = start + match.length; return .{ .id = match.id, .start = start, .length = match.length }; } const body_start = start + prefix.len; self.offset = if (std.mem.indexOfScalar(u8, self.text[body_start..], ']')) |relative_end| body_start + relative_end + 1 else body_start; return .{ .id = null, .start = start, .length = self.offset - start }; }}; const LegacyImageRepair = struct { turn_index: usize, image_index: usize, placeholder: ?LegacyImagePlaceholderIterator.Slot, assigned_id: usize = 0,}; const LegacyTurnTextRepair = struct { turn_index: usize, text: []u8,}; fn history_placeholder_id_count(history: []const HistoryTurn, id: usize) usize { var count: usize = 0; for (history) |turn| { var placeholders = LegacyImagePlaceholderIterator{ .text = user_text_for_history_turn(turn), }; while (placeholders.next()) |placeholder| { if (placeholder.id != null and placeholder.id.? == id) count += 1; } } return count;} fn legacy_placeholder_claim( history: []const HistoryTurn, placeholder: ?LegacyImagePlaceholderIterator.Slot,) ?usize { const slot = placeholder orelse return null; const placeholder_id = slot.id orelse return null; if (placeholder_id == 0) return null; if (history_placeholder_id_count(history, placeholder_id) != 1) return null; if (history_contains_image_id(history, placeholder_id)) return null; return placeholder_id;} fn legacy_repairs_contain_id(repairs: []const LegacyImageRepair, id: usize) bool { for (repairs) |repair| { if (repair.assigned_id == id) return true; } return false;} fn assign_legacy_image_ids( history: []const HistoryTurn, repairs: []LegacyImageRepair,) error{ImageIdOverflow}!void { for (repairs) |*repair| { repair.assigned_id = legacy_placeholder_claim(history, repair.placeholder) orelse 0; } var candidate: usize = 1; for (repairs) |*repair| { if (repair.assigned_id != 0) continue; while (history_contains_image_id(history, candidate) or legacy_repairs_contain_id(repairs, candidate)) { candidate = std.math.add(usize, candidate, 1) catch return error.ImageIdOverflow; } repair.assigned_id = candidate; }} fn rebuilt_legacy_turn_text( alloc: Allocator, history: []const HistoryTurn,Lines 1–500 of 6283. Load more
const std = @import("std"); const core_types = @import("../shared/types.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const io_mod = @import("../shared/io.zig"); const message = @import("../shared/message.zig"); const text_utils = @import("../shared/text_utils.zig"); const tool_result_errors = @import("../tooling/tool_result_errors.zig"); const session_permission_state = @import("../permissions/session_permission_state.zig"); const image_attachments = @import("../images/image_attachments.zig"); const generation_usage_provider = @import("generation_usage_provider.zig"); const web_fetch_artifacts = @import("web_fetch_artifacts.zig"); pub const session_usage = @import("session_usage.zig"); pub const profile_usage_runtime = @import("profile_usage_runtime.zig"); const command_contract = @import("../execution/command_contract.zig"); const sort_utils = @import("../shared/sort_utils.zig"); const Allocator = std.mem.Allocator; test { _ = session_usage; _ = profile_usage_runtime; } const compact_continuation_preamble = "This session is being continued from earlier compacted context. The summary below covers the earlier portion of the conversation.\n\n"; const compact_recent_messages_note = "Recent conversation turns are preserved verbatim."; const compact_direct_resume_instruction = "Continue the conversation from where it left off without asking the user to repeat context. Resume directly."; const compact_summary_max_chars: usize = 1200; const compact_summary_max_lines: usize = 24; const compact_summary_max_line_chars: usize = 160; pub const interrupted_turn_notice: core_types.SemanticNotice = .{ .topic = "system", .tone = .cancelled, .body = "cancelled", }; pub const failed_turn_notice: core_types.SemanticNotice = .{ .topic = "system", .tone = .@"error", .body = "failed", }; pub fn interruptedTurnNotice(entry: InterruptedHistoryTurn) core_types.SemanticNotice { return switch (entry.terminal_reason) { .cancelled => interrupted_turn_notice, .failed => failed_turn_notice, }; } pub const interrupted_turn_context = "<turn_aborted>\n" ++ "The previous turn ended before completion. Any tools or commands may have partially executed. " ++ "Do not continue this request unless the user explicitly asks to continue.\n" ++ "</turn_aborted>"; pub fn projectSnapshotLocator(buffer: []u8, path: []const u8) ![]const u8 { if (!std.fs.path.isAbsolute(path)) return path; return std.fmt.bufPrint(buffer, "images/{s}", .{std.fs.path.basename(path)}); } pub const interrupted_before_completion_output = "The previous response ended before completion."; pub const aborted_tool_output = "aborted by user"; /// Conversation language tag stored in session records. pub const ConversationLanguage = core_types.ConversationLanguage; /// Image attachment metadata stored on user turns. pub const ImageAttachment = core_types.ImageAttachment; /// User-side text and image attachments for a stored history turn. pub const UserTurn = core_types.UserTurn; /// Gateway tool call metadata stored when an interrupted turn is cancelled during a tool call. pub const ToolCall = core_types.ToolCall; /// Stored assistant response paired with the user turn that produced it. pub const AssistantHistoryTurn = core_types.AssistantHistoryTurn; /// Stored background command metadata paired with the user turn that produced it. pub const BackgroundCommandHistoryTurn = core_types.BackgroundCommandHistoryTurn; pub const StableBackgroundRecordId = core_types.StableBackgroundRecordId; pub const CancelledCommandPresentation = core_types.CancelledCommandPresentation; /// Stored interrupted turn marker, optionally paired with the active tool call. pub const InterruptedHistoryTurn = core_types.InterruptedHistoryTurn; pub const InterruptedTerminalReason = core_types.InterruptedTerminalReason; /// Stored compacted context summary produced by core history compaction. pub const CompactedSummaryHistoryTurn = core_types.CompactedSummaryHistoryTurn; /// One persisted session history record. pub const HistoryTurn = core_types.HistoryTurn; pub const freeUserTurn = core_types.freeUserTurn; pub const dupeUserTurn = core_types.dupeUserTurn; pub const max_work_id_bytes: usize = 128; pub const WorkIdAssociation = enum { none, copy_event, already_associated, }; pub const WorkIdError = error{ InvalidWorkId, ConflictingWorkId, }; pub fn validateWorkId(work_id: []const u8) WorkIdError!void { if (work_id.len == 0 or work_id.len > max_work_id_bytes or !std.unicode.utf8ValidateSlice(work_id) or std.mem.indexOfScalar(u8, work_id, 0) != null) { return error.InvalidWorkId; } } pub fn historyTurnWorkId(turn: HistoryTurn) ?[]const u8 { return switch (turn) { .assistant => |entry| entry.user.work_id, .background_command => |entry| entry.user.work_id, .interrupted => |entry| entry.user.work_id, .compacted_summary => null, }; } /// Purely decides how authoritative event provenance relates to one turn. pub fn decideWorkIdAssociation( turn: HistoryTurn, event_work_id: ?[]const u8, ) WorkIdError!WorkIdAssociation { const turn_work_id = historyTurnWorkId(turn); if (turn_work_id) |work_id| try validateWorkId(work_id); if (event_work_id) |work_id| try validateWorkId(work_id); if (event_work_id == null) { return if (turn_work_id == null) .none else error.ConflictingWorkId; } if (turn == .compacted_summary) return error.ConflictingWorkId; if (turn_work_id) |work_id| { return if (std.mem.eql(u8, work_id, event_work_id.?)) .already_associated else error.ConflictingWorkId; } return .copy_event; } /// Installs a separately owned work ID after pure association succeeds. pub fn copyWorkIdToTurn( alloc: Allocator, turn: *HistoryTurn, work_id: []const u8, ) (Allocator.Error || WorkIdError)!void { if (try decideWorkIdAssociation(turn.*, work_id) != .copy_event) return; const owned = try alloc.dupe(u8, work_id); switch (turn.*) { .assistant => |*entry| entry.user.work_id = owned, .background_command => |*entry| entry.user.work_id = owned, .interrupted => |*entry| entry.user.work_id = owned, .compacted_summary => unreachable, } } pub const freeHistoryTurn = core_types.freeHistoryTurn; pub const dupeToolCall = core_types.dupeToolCall; pub const freeToolCall = core_types.freeToolCall; pub const dupeCompletedToolNames = core_types.dupeCompletedToolNames; pub const freeCompletedToolNames = core_types.freeCompletedToolNames; pub const ExecutionMemory = core_types.ExecutionMemory; pub const ToolExecutionStep = core_types.ToolExecutionStep; pub const PersistedToolResult = core_types.PersistedToolResult; pub const PersistedToolStatus = core_types.PersistedToolStatus; pub const FileEvidence = core_types.FileEvidence; pub const FileEvidenceAction = core_types.FileEvidenceAction; pub const freeExecutionMemory = core_types.freeExecutionMemory; pub const freeToolCallSlice = core_types.freeToolCallSlice; pub const freePersistedToolResults = core_types.freePersistedToolResults; /// Builds a sorted, independently owned attachment catalog from complete /// canonical history plus current-turn images. The caller frees the returned /// slice with `core_types.freeImageAttachmentSlice` using the same allocator. pub fn collect_image_catalog( alloc: Allocator, canonical_history: []const HistoryTurn, current_images: []const ImageAttachment, ) (Allocator.Error || error{ ImageCatalogTooLarge, InvalidImageId, DuplicateImageId, })![]ImageAttachment { var attachment_count = current_images.len; for (canonical_history) |turn| { attachment_count = std.math.add( usize, attachment_count, images_for_history_turn(turn).len, ) catch return error.ImageCatalogTooLarge; } if (attachment_count == 0) return &.{}; const catalog = try alloc.alloc(ImageAttachment, attachment_count); errdefer alloc.free(catalog); var copied: usize = 0; errdefer for (catalog[0..copied]) |attachment| { core_types.freeImageAttachment(alloc, attachment); }; for (canonical_history) |turn| { for (images_for_history_turn(turn)) |attachment| { catalog[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } } for (current_images) |attachment| { catalog[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } sort_utils.sort(ImageAttachment, catalog, {}, image_id_less_than); var previous_id: ?usize = null; for (catalog) |attachment| { if (attachment.id == 0) return error.InvalidImageId; if (previous_id == attachment.id) return error.DuplicateImageId; previous_id = attachment.id; } std.debug.assert(image_attachments.imageAttachmentsSortedById(catalog)); return catalog; } /// Rebuilds an owned catalog after queue review replaces the current-turn /// attachments. Historical authority is retained; the old current slice must /// still match the catalog exactly or the replacement is rejected as stale. pub fn replace_image_catalog_current_images( alloc: Allocator, catalog: []const ImageAttachment, old_current_images: []const ImageAttachment, new_current_images: []const ImageAttachment, ) (Allocator.Error || error{ ImageCatalogTooLarge, InvalidImageId, DuplicateImageId, StaleImageCatalog, })![]ImageAttachment { try validate_image_catalog(catalog); for (old_current_images, 0..) |old_current, index| { if (old_current.id == 0) return error.InvalidImageId; for (old_current_images[0..index]) |prior| { if (prior.id == old_current.id) return error.DuplicateImageId; } const authorized = image_attachment_for_id(catalog, old_current.id) orelse return error.StaleImageCatalog; if (!image_attachments_equal(authorized, old_current)) return error.StaleImageCatalog; } const retained_count = std.math.sub(usize, catalog.len, old_current_images.len) catch return error.StaleImageCatalog; const attachment_count = std.math.add( usize, retained_count, new_current_images.len, ) catch return error.ImageCatalogTooLarge; if (attachment_count == 0) return &.{}; const replacement = try alloc.alloc(ImageAttachment, attachment_count); errdefer alloc.free(replacement); var copied: usize = 0; errdefer for (replacement[0..copied]) |attachment| { core_types.freeImageAttachment(alloc, attachment); }; for (catalog) |attachment| { if (image_attachment_for_id(old_current_images, attachment.id) != null) continue; replacement[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } std.debug.assert(copied == retained_count); for (new_current_images) |attachment| { replacement[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } sort_utils.sort(ImageAttachment, replacement, {}, image_id_less_than); try validate_image_catalog(replacement); return replacement; } /// Returns a new owned catalog containing any image authority introduced by a /// completed history turn. Existing IDs must either match exactly or the /// queued catalog is rejected as stale. pub fn merge_image_catalog_history_turn( alloc: Allocator, catalog: []const ImageAttachment, turn: HistoryTurn, ) (Allocator.Error || error{ ImageCatalogTooLarge, InvalidImageId, DuplicateImageId, StaleImageCatalog, })![]ImageAttachment { try validate_image_catalog(catalog); const additions = images_for_history_turn(turn); var missing_count: usize = 0; for (additions, 0..) |addition, index| { if (addition.id == 0) return error.InvalidImageId; for (additions[0..index]) |prior| { if (prior.id == addition.id) return error.DuplicateImageId; } if (image_attachment_for_id(catalog, addition.id)) |existing| { if (!image_attachments_equal(existing, addition)) return error.StaleImageCatalog; } else { missing_count += 1; } } const attachment_count = std.math.add(usize, catalog.len, missing_count) catch return error.ImageCatalogTooLarge; if (attachment_count == 0) return &.{}; const merged = try alloc.alloc(ImageAttachment, attachment_count); errdefer alloc.free(merged); var copied: usize = 0; errdefer for (merged[0..copied]) |attachment| { core_types.freeImageAttachment(alloc, attachment); }; for (catalog) |attachment| { merged[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } for (additions) |attachment| { if (image_attachment_for_id(catalog, attachment.id) != null) continue; merged[copied] = try dupeImageAttachment(alloc, attachment); copied += 1; } sort_utils.sort(ImageAttachment, merged, {}, image_id_less_than); try validate_image_catalog(merged); return merged; } fn validate_image_catalog(catalog: []const ImageAttachment) error{ InvalidImageId, DuplicateImageId, }!void { var previous_id: ?usize = null; for (catalog) |attachment| { if (attachment.id == 0) return error.InvalidImageId; if (previous_id != null and previous_id.? >= attachment.id) { if (previous_id.? == attachment.id) return error.DuplicateImageId; return error.InvalidImageId; } previous_id = attachment.id; } } fn image_attachment_for_id( attachments: []const ImageAttachment, id: usize, ) ?ImageAttachment { for (attachments) |attachment| { if (attachment.id == id) return attachment; } return null; } fn image_attachments_equal(lhs: ImageAttachment, rhs: ImageAttachment) bool { return lhs.id == rhs.id and std.mem.eql(u8, lhs.path, rhs.path) and std.mem.eql(u8, lhs.media_type, rhs.media_type) and optionalBytesEqual(lhs.snapshot_path, rhs.snapshot_path) and optionalBytesEqual(lhs.snapshot_sha256, rhs.snapshot_sha256); } fn optionalBytesEqual(lhs: ?[]const u8, rhs: ?[]const u8) bool { if (lhs == null or rhs == null) return lhs == null and rhs == null; return std.mem.eql(u8, lhs.?, rhs.?); } fn images_for_history_turn(turn: HistoryTurn) []const ImageAttachment { return switch (turn) { .compacted_summary => &.{}, .assistant => |entry| entry.user.images, .background_command => |entry| entry.user.images, .interrupted => |entry| entry.user.images, }; } fn mutable_images_for_history_turn(turn: *HistoryTurn) []ImageAttachment { return switch (turn.*) { .compacted_summary => &.{}, .assistant => |*entry| entry.user.images, .background_command => |*entry| entry.user.images, .interrupted => |*entry| entry.user.images, }; } fn mutable_images_slice_for_history_turn(turn: *HistoryTurn) ?*[]ImageAttachment { return switch (turn.*) { .compacted_summary => null, .assistant => |*entry| &entry.user.images, .background_command => |*entry| &entry.user.images, .interrupted => |*entry| &entry.user.images, }; } fn user_text_for_history_turn(turn: HistoryTurn) []const u8 { return switch (turn) { .compacted_summary => "", .assistant => |entry| entry.user.text, .background_command => |entry| entry.user.text, .interrupted => |entry| entry.user.text, }; } const LegacyImagePlaceholderIterator = struct { text: []const u8, offset: usize = 0, const prefix = "[Image #"; const Slot = struct { id: ?usize, start: usize, length: usize, }; fn next(self: *@This()) ?Slot { const relative_start = std.mem.indexOf(u8, self.text[self.offset..], prefix) orelse return null; const start = self.offset + relative_start; if (image_attachments.matchImagePlaceholder(self.text, start)) |match| { self.offset = start + match.length; return .{ .id = match.id, .start = start, .length = match.length }; } const body_start = start + prefix.len; self.offset = if (std.mem.indexOfScalar(u8, self.text[body_start..], ']')) |relative_end| body_start + relative_end + 1 else body_start; return .{ .id = null, .start = start, .length = self.offset - start }; } }; const LegacyImageRepair = struct { turn_index: usize, image_index: usize, placeholder: ?LegacyImagePlaceholderIterator.Slot, assigned_id: usize = 0, }; const LegacyTurnTextRepair = struct { turn_index: usize, text: []u8, }; fn history_placeholder_id_count(history: []const HistoryTurn, id: usize) usize { var count: usize = 0; for (history) |turn| { var placeholders = LegacyImagePlaceholderIterator{ .text = user_text_for_history_turn(turn), }; while (placeholders.next()) |placeholder| { if (placeholder.id != null and placeholder.id.? == id) count += 1; } } return count; } fn legacy_placeholder_claim( history: []const HistoryTurn, placeholder: ?LegacyImagePlaceholderIterator.Slot, ) ?usize { const slot = placeholder orelse return null; const placeholder_id = slot.id orelse return null; if (placeholder_id == 0) return null; if (history_placeholder_id_count(history, placeholder_id) != 1) return null; if (history_contains_image_id(history, placeholder_id)) return null; return placeholder_id; } fn legacy_repairs_contain_id(repairs: []const LegacyImageRepair, id: usize) bool { for (repairs) |repair| { if (repair.assigned_id == id) return true; } return false; } fn assign_legacy_image_ids( history: []const HistoryTurn, repairs: []LegacyImageRepair, ) error{ImageIdOverflow}!void { for (repairs) |*repair| { repair.assigned_id = legacy_placeholder_claim(history, repair.placeholder) orelse 0; } var candidate: usize = 1; for (repairs) |*repair| { if (repair.assigned_id != 0) continue; while (history_contains_image_id(history, candidate) or legacy_repairs_contain_id(repairs, candidate)) { candidate = std.math.add(usize, candidate, 1) catch return error.ImageIdOverflow; } repair.assigned_id = candidate; } } fn rebuilt_legacy_turn_text( alloc: Allocator, history: []const HistoryTurn,