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 =
"\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" ++
"";
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,
turn_index: usize,
repairs: []const LegacyImageRepair,
) Allocator.Error!?[]u8 {
const text = user_text_for_history_turn(history[turn_index]);
var needs_rebuild = false;
for (repairs) |repair| {
if (repair.turn_index != turn_index) continue;
const placeholder = repair.placeholder orelse continue;
const legacy_id = placeholder.id orelse continue;
if (legacy_id != repair.assigned_id) needs_rebuild = true;
}
if (!needs_rebuild) return null;
var rebuilt: std.Io.Writer.Allocating = .init(alloc);
errdefer rebuilt.deinit();
var cursor: usize = 0;
for (repairs) |repair| {
if (repair.turn_index != turn_index) continue;
const placeholder = repair.placeholder orelse continue;
const legacy_id = placeholder.id orelse continue;
if (legacy_id == repair.assigned_id) continue;
std.debug.assert(placeholder.start >= cursor);
std.debug.assert(placeholder.start + placeholder.length <= text.len);
rebuilt.writer.writeAll(text[cursor..placeholder.start]) catch return error.OutOfMemory;
image_attachments.writeImagePlaceholder(&rebuilt.writer, repair.assigned_id) catch return error.OutOfMemory;
cursor = placeholder.start + placeholder.length;
}
rebuilt.writer.writeAll(text[cursor..]) catch return error.OutOfMemory;
return rebuilt.toOwnedSlice() catch return error.OutOfMemory;
}
fn mutable_user_for_history_turn(turn: *HistoryTurn) ?*UserTurn {
return switch (turn.*) {
.compacted_summary => null,
.assistant => |*entry| &entry.user,
.background_command => |*entry| &entry.user,
.interrupted => |*entry| &entry.user,
};
}
/// Repairs image IDs omitted by the legacy session deep-copy path. This is
/// only for allocator-owned history loaded from durable storage. Rebuilt text
/// remains owned by its history turn. Returns whether any attachment changed.
pub fn repair_legacy_zero_image_ids(
alloc: Allocator,
history: []HistoryTurn,
) (Allocator.Error || error{ImageIdOverflow})!bool {
var repairs: std.ArrayList(LegacyImageRepair) = .empty;
defer repairs.deinit(alloc);
for (history, 0..) |*turn, turn_index| {
var placeholders = LegacyImagePlaceholderIterator{
.text = user_text_for_history_turn(turn.*),
};
for (mutable_images_for_history_turn(turn), 0..) |attachment, image_index| {
const placeholder = placeholders.next();
if (attachment.id != 0) continue;
try repairs.append(alloc, .{
.turn_index = turn_index,
.image_index = image_index,
.placeholder = placeholder,
});
}
}
if (repairs.items.len == 0) return false;
try assign_legacy_image_ids(history, repairs.items);
var text_repairs: std.ArrayList(LegacyTurnTextRepair) = .empty;
defer text_repairs.deinit(alloc);
errdefer for (text_repairs.items) |repair| alloc.free(repair.text);
for (history, 0..) |_, turn_index| {
const rebuilt = try rebuilt_legacy_turn_text(
alloc,
history,
turn_index,
repairs.items,
) orelse continue;
text_repairs.append(alloc, .{ .turn_index = turn_index, .text = rebuilt }) catch |err| {
alloc.free(rebuilt);
return err;
};
}
for (repairs.items) |repair| {
mutable_images_for_history_turn(&history[repair.turn_index])[repair.image_index].id =
repair.assigned_id;
}
for (text_repairs.items) |repair| {
const user = mutable_user_for_history_turn(&history[repair.turn_index]).?;
alloc.free(user.text);
user.text = repair.text;
}
return true;
}
pub fn repair_legacy_image_snapshots(
alloc: Allocator,
history: []HistoryTurn,
snapshot_dir: []const u8,
) Allocator.Error!bool {
const snapshot_dir_existed = snapshot_directory_exists(snapshot_dir);
const candidate = try alloc.alloc(HistoryTurn, history.len);
var copied: usize = 0;
errdefer {
for (candidate[0..copied]) |turn| freeHistoryTurn(alloc, turn);
alloc.free(candidate);
}
while (copied < history.len) : (copied += 1) {
candidate[copied] = try dupeHistoryTurn(alloc, history[copied]);
}
const changed = repair_legacy_image_snapshots_in_place(
alloc,
candidate,
snapshot_dir,
) catch |err| {
cleanup_candidate_snapshot_files(candidate, history);
if (!snapshot_dir_existed) remove_empty_snapshot_directory(snapshot_dir);
return err;
};
if (!changed) {
freeHistoryTurnSlice(alloc, candidate);
return false;
}
for (history, candidate) |*current, *replacement| {
std.mem.swap(HistoryTurn, current, replacement);
}
freeHistoryTurnSlice(alloc, candidate);
return true;
}
pub fn repair_legacy_images_transactionally(
alloc: Allocator,
history: []HistoryTurn,
snapshot_dir: []const u8,
) (Allocator.Error || error{ImageIdOverflow})!bool {
const candidate = try alloc.alloc(HistoryTurn, history.len);
var copied: usize = 0;
errdefer {
for (candidate[0..copied]) |turn| freeHistoryTurn(alloc, turn);
alloc.free(candidate);
}
while (copied < history.len) : (copied += 1) {
candidate[copied] = try dupeHistoryTurn(alloc, history[copied]);
}
const ids_changed = try repair_legacy_zero_image_ids(alloc, candidate);
const snapshots_changed = try repair_legacy_image_snapshots(
alloc,
candidate,
snapshot_dir,
);
if (!ids_changed and !snapshots_changed) {
freeHistoryTurnSlice(alloc, candidate);
return false;
}
for (history, candidate) |*current, *replacement| {
std.mem.swap(HistoryTurn, current, replacement);
}
freeHistoryTurnSlice(alloc, candidate);
return true;
}
fn snapshot_directory_exists(path: []const u8) bool {
var dir = std.Io.Dir.openDirAbsolute(io_mod.getIo(), path, .{}) catch return false;
dir.close(io_mod.getIo());
return true;
}
fn remove_empty_snapshot_directory(path: []const u8) void {
std.Io.Dir.deleteDirAbsolute(io_mod.getIo(), path) catch |err| switch (err) {
error.FileNotFound => {},
else => debug_trace.logf(
"session",
"event=legacy_snapshot_directory_cleanup_failed err={s}",
.{@errorName(err)},
),
};
}
fn repair_legacy_image_snapshots_in_place(
alloc: Allocator,
history: []HistoryTurn,
snapshot_dir: []const u8,
) Allocator.Error!bool {
var changed = false;
for (history) |*turn| {
const images_ptr = mutable_images_slice_for_history_turn(turn) orelse continue;
if (images_ptr.*.len == 0) continue;
var drop_indexes: std.ArrayList(usize) = .empty;
defer drop_indexes.deinit(alloc);
for (images_ptr.*, 0..) |*image, image_index| {
if (image.snapshot_path != null and image.snapshot_sha256 != null) continue;
image_attachments.captureImageSnapshot(alloc, image, snapshot_dir) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => {
debug_trace.logf(
"session",
"legacy image snapshot unavailable id={d} err={s}",
.{ image.id, @errorName(err) },
);
try drop_indexes.append(alloc, image_index);
continue;
},
};
changed = true;
}
if (drop_indexes.items.len == 0) continue;
const retained_len = images_ptr.*.len - drop_indexes.items.len;
var retained: []ImageAttachment = if (retained_len > 0)
try alloc.alloc(ImageAttachment, retained_len)
else
&.{};
var retained_index: usize = 0;
var drop_cursor: usize = 0;
for (images_ptr.*, 0..) |image, image_index| {
const drop = drop_cursor < drop_indexes.items.len and
drop_indexes.items[drop_cursor] == image_index;
if (drop) {
core_types.freeImageAttachment(alloc, image);
drop_cursor += 1;
continue;
}
retained[retained_index] = image;
retained_index += 1;
}
alloc.free(images_ptr.*);
images_ptr.* = retained;
changed = true;
}
return changed;
}
fn cleanup_candidate_snapshot_files(
candidate: []const HistoryTurn,
original: []const HistoryTurn,
) void {
for (candidate, original) |candidate_turn, original_turn| {
image_attachments.deleteUnreferencedImageSnapshots(
images_for_history_turn(candidate_turn),
images_for_history_turn(original_turn),
);
}
}
fn make_owned_legacy_image_turn(
alloc: Allocator,
text: []const u8,
assistant: []const u8,
path: []const u8,
) !HistoryTurn {
var turn = try makeAssistantTurn(alloc, text, assistant);
errdefer freeHistoryTurn(alloc, turn);
turn.assistant.user.images = try core_types.dupeImageAttachmentSlice(alloc, &.{.{
.path = @constCast(path),
.media_type = @constCast("image/png"),
}});
return turn;
}
fn sessionTestSnapshotDir(alloc: Allocator, tmp: *std.testing.TmpDir) ![]u8 {
const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");
defer alloc.free(root);
return std.fs.path.join(alloc, &.{ root, "snapshots" });
}
fn writeSessionTestImage(
alloc: Allocator,
tmp: *std.testing.TmpDir,
name: []const u8,
data: []const u8,
) ![]u8 {
{
var file = try tmp.dir.createFile(std.testing.io, name, .{ .truncate = true });
defer file.close(std.testing.io);
try file.writeStreamingAll(std.testing.io, data);
}
return io_mod.dirRealpathAlloc(alloc, tmp.dir, name);
}
test "legacy image snapshot repair captures available path once" {
const alloc = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const image_a = "\x89PNG\r\n\x1a\nlegacy-a";
const image_b = "\x89PNG\r\n\x1a\nlegacy-b";
const path = try writeSessionTestImage(alloc, &tmp, "legacy.png", image_a);
defer alloc.free(path);
const snapshot_dir = try sessionTestSnapshotDir(alloc, &tmp);
defer alloc.free(snapshot_dir);
var turn = try make_owned_legacy_image_turn(alloc, "legacy [Image #1]", "answer", path);
var turn_owned = true;
errdefer if (turn_owned) freeHistoryTurn(alloc, turn);
turn.assistant.user.images[0].id = 1;
var history = [_]HistoryTurn{turn};
defer freeHistoryTurn(alloc, history[0]);
turn_owned = false;
try std.testing.expect(try repair_legacy_image_snapshots(alloc, &history, snapshot_dir));
try std.testing.expect(history[0].assistant.user.images[0].snapshot_path != null);
try std.testing.expect(history[0].assistant.user.images[0].snapshot_sha256 != null);
const replaced_path = try writeSessionTestImage(alloc, &tmp, "legacy.png", image_b);
defer alloc.free(replaced_path);
var out: std.Io.Writer.Allocating = .init(alloc);
defer out.deinit();
try image_attachments.writeImageFilePartJson(alloc, &out.writer, history[0].assistant.user.images[0]);
var expected_buf: [128]u8 = undefined;
const expected = std.base64.standard.Encoder.encode(&expected_buf, image_a);
var unexpected_buf: [128]u8 = undefined;
const unexpected = std.base64.standard.Encoder.encode(&unexpected_buf, image_b);
try std.testing.expect(std.mem.find(u8, out.written(), expected) != null);
try std.testing.expect(std.mem.find(u8, out.written(), unexpected) == null);
}
test "legacy image snapshot repair drops unavailable path-only images" {
const alloc = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");
defer alloc.free(root);
const missing_path = try std.fs.path.join(alloc, &.{ root, "missing.png" });
defer alloc.free(missing_path);
const snapshot_dir = try sessionTestSnapshotDir(alloc, &tmp);
defer alloc.free(snapshot_dir);
var turn = try make_owned_legacy_image_turn(alloc, "legacy [Image #1]", "answer", missing_path);
var turn_owned = true;
errdefer if (turn_owned) freeHistoryTurn(alloc, turn);
turn.assistant.user.images[0].id = 1;
var history = [_]HistoryTurn{turn};
defer freeHistoryTurn(alloc, history[0]);
turn_owned = false;
try std.testing.expect(try repair_legacy_image_snapshots(alloc, &history, snapshot_dir));
try std.testing.expectEqual(@as(usize, 0), history[0].assistant.user.images.len);
}
test "legacy image snapshot repair propagates OOM without changing memory or disk" {
const alloc = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const image_bytes = "\x89PNG\r\n\x1a\nlegacy-oom";
const path = try writeSessionTestImage(alloc, &tmp, "legacy-oom.png", image_bytes);
defer alloc.free(path);
var reached_success = false;
for (0..256) |fail_index| {
var name_buf: [64]u8 = undefined;
const snapshot_name = try std.fmt.bufPrint(
&name_buf,
"snapshots-{d}",
.{fail_index},
);
const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");
defer alloc.free(root);
const snapshot_dir = try std.fs.path.join(alloc, &.{ root, snapshot_name });
defer alloc.free(snapshot_dir);
const turn = try make_owned_legacy_image_turn(
alloc,
"legacy [Image #1]",
"answer",
path,
);
var history = [_]HistoryTurn{turn};
var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = fail_index });
if (repair_legacy_images_transactionally(
failing.allocator(),
&history,
snapshot_dir,
)) |changed| {
try std.testing.expect(changed);
try std.testing.expect(!failing.has_induced_failure);
freeHistoryTurn(failing.allocator(), history[0]);
reached_success = true;
break;
} else |err| {
try std.testing.expectEqual(error.OutOfMemory, err);
try std.testing.expect(failing.has_induced_failure);
try std.testing.expectEqualStrings("legacy [Image #1]", history[0].assistant.user.text);
try std.testing.expectEqual(@as(usize, 1), history[0].assistant.user.images.len);
const image = history[0].assistant.user.images[0];
try std.testing.expectEqual(@as(usize, 0), image.id);
try std.testing.expectEqualStrings(path, image.path);
try std.testing.expect(image.snapshot_path == null);
try std.testing.expect(image.snapshot_sha256 == null);
try std.testing.expectError(
error.FileNotFound,
std.Io.Dir.openDirAbsolute(std.testing.io, snapshot_dir, .{ .iterate = true }),
);
freeHistoryTurn(alloc, history[0]);
}
}
try std.testing.expect(reached_success);
}
test "legacy image repair rewrites repeated per-turn ordinals to stable session ids" {
const alloc = std.testing.allocator;
const first = try make_owned_legacy_image_turn(
alloc,
"first prose [Image #1] tail",
"first answer",
"/tmp/legacy-first.png",
);
var first_owned = true;
errdefer if (first_owned) freeHistoryTurn(alloc, first);
const second = try make_owned_legacy_image_turn(
alloc,
"second prose [Image #1] tail",
"second answer",
"/tmp/legacy-second.png",
);
var second_owned = true;
errdefer if (second_owned) freeHistoryTurn(alloc, second);
var history = [_]HistoryTurn{
first,
second,
};
defer for (history) |turn| freeHistoryTurn(alloc, turn);
first_owned = false;
second_owned = false;
try std.testing.expect(try repair_legacy_zero_image_ids(alloc, &history));
try std.testing.expectEqual(@as(usize, 1), history[0].assistant.user.images[0].id);
try std.testing.expectEqualStrings(
"first prose [Image #1] tail",
history[0].assistant.user.text,
);
try std.testing.expectEqual(@as(usize, 2), history[1].assistant.user.images[0].id);
try std.testing.expectEqualStrings(
"second prose [Image #2] tail",
history[1].assistant.user.text,
);
try std.testing.expectEqualStrings(
"/tmp/legacy-first.png",
history[0].assistant.user.images[0].path,
);
try std.testing.expectEqualStrings(
"/tmp/legacy-second.png",
history[1].assistant.user.images[0].path,
);
var first_badge: std.Io.Writer.Allocating = .init(alloc);
defer first_badge.deinit();
_ = try image_attachments.expandPlaceholdersWithBadges(
alloc,
&first_badge.writer,
history[0].assistant.user.text,
history[0].assistant.user.images,
null,
);
try std.testing.expect(std.mem.find(u8, first_badge.written(), "[Image 1]") != null);
try std.testing.expect(std.mem.find(u8, first_badge.written(), "file://") == null);
try std.testing.expect(std.mem.find(u8, first_badge.written(), "/tmp/legacy-first.png") == null);
try std.testing.expect(std.mem.find(u8, first_badge.written(), "/tmp/legacy-second.png") == null);
var second_badge: std.Io.Writer.Allocating = .init(alloc);
defer second_badge.deinit();
_ = try image_attachments.expandPlaceholdersWithBadges(
alloc,
&second_badge.writer,
history[1].assistant.user.text,
history[1].assistant.user.images,
null,
);
try std.testing.expect(std.mem.find(u8, second_badge.written(), "[Image 2]") != null);
try std.testing.expect(std.mem.find(u8, second_badge.written(), "file://") == null);
try std.testing.expect(std.mem.find(u8, second_badge.written(), "/tmp/legacy-second.png") == null);
try std.testing.expect(std.mem.find(u8, second_badge.written(), "/tmp/legacy-first.png") == null);
const catalog = try collect_image_catalog(alloc, &history, &.{});
defer freeImageAttachmentSlice(alloc, catalog);
const authorized = try image_attachments.resolve_authorized_images(alloc, catalog, &.{2});
defer alloc.free(authorized);
try std.testing.expectEqual(@as(usize, 1), authorized.len);
try std.testing.expectEqualStrings("/tmp/legacy-second.png", authorized[0].path);
const bounds = try image_attachments.calculate_next_image_id(catalog);
try std.testing.expectEqual(@as(?usize, 2), bounds.maximum_id);
try std.testing.expectEqual(@as(usize, 3), bounds.next_id);
}
test "legacy image repair rewrites three repeated ordinals across persisted turn variants" {
const alloc = std.testing.allocator;
const assistant_text = try alloc.dupe(u8, "assistant [Image #1]");
var assistant_text_owned = true;
errdefer if (assistant_text_owned) alloc.free(assistant_text);
const background_text = try alloc.dupe(u8, "background [Image #1]");
var background_text_owned = true;
errdefer if (background_text_owned) alloc.free(background_text);
const interrupted_text = try alloc.dupe(u8, "interrupted [Image #1]");
var interrupted_text_owned = true;
errdefer if (interrupted_text_owned) alloc.free(interrupted_text);
var assistant_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/assistant-one.png"),
.media_type = @constCast("image/png"),
}};
var background_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/background-one.png"),
.media_type = @constCast("image/png"),
}};
var interrupted_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/interrupted-one.png"),
.media_type = @constCast("image/png"),
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = assistant_text, .images = &assistant_images },
.assistant = @constCast("answer"),
} },
.{ .background_command = .{
.user = .{ .text = background_text, .images = &background_images },
.log_path = @constCast("/tmp/background.log"),
.expect_url = false,
} },
.{ .interrupted = .{
.user = .{ .text = interrupted_text, .images = &interrupted_images },
.assistant = @constCast("partial"),
} },
};
defer alloc.free(history[0].assistant.user.text);
defer alloc.free(history[1].background_command.user.text);
defer alloc.free(history[2].interrupted.user.text);
assistant_text_owned = false;
background_text_owned = false;
interrupted_text_owned = false;
try std.testing.expect(try repair_legacy_zero_image_ids(alloc, &history));
try std.testing.expectEqual(@as(usize, 1), assistant_images[0].id);
try std.testing.expectEqual(@as(usize, 2), background_images[0].id);
try std.testing.expectEqual(@as(usize, 3), interrupted_images[0].id);
try std.testing.expectEqualStrings("assistant [Image #1]", history[0].assistant.user.text);
try std.testing.expectEqualStrings(
"background [Image #2]",
history[1].background_command.user.text,
);
try std.testing.expectEqualStrings(
"interrupted [Image #3]",
history[2].interrupted.user.text,
);
}
test "legacy image repair leaves history unchanged when text reconstruction runs out of memory" {
const alloc = std.testing.allocator;
var first_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/first.png"),
.media_type = @constCast("image/png"),
}};
var second_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/second.png"),
.media_type = @constCast("image/png"),
}};
var third_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/third.png"),
.media_type = @constCast("image/png"),
}};
const first_text = try alloc.dupe(u8, "first [Image #1]");
var first_text_owned = true;
errdefer if (first_text_owned) alloc.free(first_text);
const second_text = try alloc.dupe(u8, "second [Image #1]");
var second_text_owned = true;
errdefer if (second_text_owned) alloc.free(second_text);
const third_text = try alloc.dupe(u8, "third [Image #1]");
var third_text_owned = true;
errdefer if (third_text_owned) alloc.free(third_text);
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = first_text, .images = &first_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = second_text, .images = &second_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = third_text, .images = &third_images },
.assistant = @constCast("answer"),
} },
};
defer alloc.free(history[0].assistant.user.text);
defer alloc.free(history[1].assistant.user.text);
defer alloc.free(history[2].assistant.user.text);
first_text_owned = false;
second_text_owned = false;
third_text_owned = false;
// Metadata and the first rebuilt turn succeed; the second rebuild fails.
var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 2 });
try std.testing.expectError(
error.OutOfMemory,
repair_legacy_zero_image_ids(failing.allocator(), &history),
);
try std.testing.expectEqual(@as(usize, 0), first_images[0].id);
try std.testing.expectEqual(@as(usize, 0), second_images[0].id);
try std.testing.expectEqual(@as(usize, 0), third_images[0].id);
try std.testing.expectEqualStrings("first [Image #1]", history[0].assistant.user.text);
try std.testing.expectEqualStrings("second [Image #1]", history[1].assistant.user.text);
try std.testing.expectEqualStrings("third [Image #1]", history[2].assistant.user.text);
}
test "legacy image repair preserves canonical placeholder identity by attachment order" {
var first_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/five.png"),
.media_type = @constCast("image/png"),
}};
var mixed_images = [_]ImageAttachment{
.{
.id = 0,
.path = @constCast("/tmp/nine.png"),
.media_type = @constCast("image/png"),
},
.{
.id = 3,
.path = @constCast("/tmp/three.png"),
.media_type = @constCast("image/png"),
},
};
var skipped_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/twelve.png"),
.media_type = @constCast("image/png"),
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("first [Image #5]"), .images = &first_images },
.assistant = @constCast("first answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("mixed [Image #9] [Image #3]"), .images = &mixed_images },
.assistant = @constCast("mixed answer"),
} },
.{ .interrupted = .{
.user = .{ .text = @constCast("later [Image #12]"), .images = &skipped_images },
.assistant = @constCast("partial"),
} },
};
_ = try repair_legacy_zero_image_ids(std.testing.allocator, &history);
try std.testing.expectEqual(@as(usize, 5), first_images[0].id);
try std.testing.expectEqual(@as(usize, 9), mixed_images[0].id);
try std.testing.expectEqual(@as(usize, 3), mixed_images[1].id);
try std.testing.expectEqual(@as(usize, 12), skipped_images[0].id);
try std.testing.expectEqualStrings("first [Image #5]", history[0].assistant.user.text);
try std.testing.expectEqualStrings("/tmp/five.png", first_images[0].path);
}
test "legacy image repair reserves a later low placeholder before an earlier fallback" {
var fallback_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/fallback.png"),
.media_type = @constCast("image/png"),
}};
var reserved_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/reserved-one.png"),
.media_type = @constCast("image/png"),
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("missing placeholder"), .images = &fallback_images },
.assistant = @constCast("fallback answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("reserved [Image #1]"), .images = &reserved_images },
.assistant = @constCast("reserved answer"),
} },
};
_ = try repair_legacy_zero_image_ids(std.testing.allocator, &history);
try std.testing.expectEqual(@as(usize, 2), fallback_images[0].id);
try std.testing.expectEqual(@as(usize, 1), reserved_images[0].id);
}
test "legacy image repair preserves a low reservation before a later fallback" {
var reserved_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/reserved-one.png"),
.media_type = @constCast("image/png"),
}};
var fallback_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/fallback.png"),
.media_type = @constCast("image/png"),
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("reserved [Image #1]"), .images = &reserved_images },
.assistant = @constCast("reserved answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("missing placeholder"), .images = &fallback_images },
.assistant = @constCast("fallback answer"),
} },
};
_ = try repair_legacy_zero_image_ids(std.testing.allocator, &history);
try std.testing.expectEqual(@as(usize, 1), reserved_images[0].id);
try std.testing.expectEqual(@as(usize, 2), fallback_images[0].id);
}
test "legacy image repair reserves low ids before interleaved fallbacks across turns" {
var missing_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/missing.png"),
.media_type = @constCast("image/png"),
}};
var reserved_one_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/reserved-one.png"),
.media_type = @constCast("image/png"),
}};
var malformed_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/malformed.png"),
.media_type = @constCast("image/png"),
}};
var reserved_two_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/reserved-two.png"),
.media_type = @constCast("image/png"),
}};
var later_missing_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/later-missing.png"),
.media_type = @constCast("image/png"),
}};
var reserved_four_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/reserved-four.png"),
.media_type = @constCast("image/png"),
}};
var positive_images = [_]ImageAttachment{.{
.id = 5,
.path = @constCast("/tmp/positive-five.png"),
.media_type = @constCast("image/png"),
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("missing placeholder"), .images = &missing_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("reserved [Image #1]"), .images = &reserved_one_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("malformed [Image #oops]"), .images = &malformed_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("reserved [Image #2]"), .images = &reserved_two_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("later missing placeholder"), .images = &later_missing_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("reserved [Image #4]"), .images = &reserved_four_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("existing positive"), .images = &positive_images },
.assistant = @constCast("answer"),
} },
};
_ = try repair_legacy_zero_image_ids(std.testing.allocator, &history);
try std.testing.expectEqual(@as(usize, 3), missing_images[0].id);
try std.testing.expectEqual(@as(usize, 1), reserved_one_images[0].id);
try std.testing.expectEqual(@as(usize, 6), malformed_images[0].id);
try std.testing.expectEqual(@as(usize, 2), reserved_two_images[0].id);
try std.testing.expectEqual(@as(usize, 7), later_missing_images[0].id);
try std.testing.expectEqual(@as(usize, 4), reserved_four_images[0].id);
try std.testing.expectEqual(@as(usize, 5), positive_images[0].id);
}
test "legacy image repair gives existing positive ids priority over placeholder claims" {
const alloc = std.testing.allocator;
const conflicting_text = try alloc.dupe(u8, "conflicting [Image #1]");
var positive_images = [_]ImageAttachment{.{
.id = 1,
.path = @constCast("/tmp/positive-one.png"),
.media_type = @constCast("image/png"),
}};
var conflicting_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/conflicting-one.png"),
.media_type = @constCast("image/png"),
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("existing positive"), .images = &positive_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = conflicting_text, .images = &conflicting_images },
.assistant = @constCast("answer"),
} },
};
defer alloc.free(history[1].assistant.user.text);
_ = try repair_legacy_zero_image_ids(alloc, &history);
try std.testing.expectEqual(@as(usize, 1), positive_images[0].id);
try std.testing.expectEqual(@as(usize, 2), conflicting_images[0].id);
try std.testing.expectEqualStrings("conflicting [Image #2]", history[1].assistant.user.text);
}
test "legacy image repair falls back for missing malformed zero duplicate and conflicting placeholders" {
const alloc = std.testing.allocator;
const conflicting_text = try alloc.dupe(u8, "conflict [Image #3]");
var conflicting_text_owned = true;
errdefer if (conflicting_text_owned) alloc.free(conflicting_text);
const zero_text = try alloc.dupe(u8, "zero [Image #0]");
var zero_text_owned = true;
errdefer if (zero_text_owned) alloc.free(zero_text);
const duplicate_text = try alloc.dupe(u8, "duplicate [Image #50] [Image #50]");
var duplicate_text_owned = true;
errdefer if (duplicate_text_owned) alloc.free(duplicate_text);
var positive_images = [_]ImageAttachment{.{
.id = 3,
.path = @constCast("/tmp/positive.png"),
.media_type = @constCast("image/png"),
}};
var conflicting_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/conflicting.png"),
.media_type = @constCast("image/png"),
}};
var missing_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/missing.png"),
.media_type = @constCast("image/png"),
}};
var malformed_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/malformed.png"),
.media_type = @constCast("image/png"),
}};
var zero_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/zero.png"),
.media_type = @constCast("image/png"),
}};
var duplicate_images = [_]ImageAttachment{
.{
.id = 0,
.path = @constCast("/tmp/duplicate-a.png"),
.media_type = @constCast("image/png"),
},
.{
.id = 0,
.path = @constCast("/tmp/duplicate-b.png"),
.media_type = @constCast("image/png"),
},
};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("owned [Image #3]"), .images = &positive_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = conflicting_text, .images = &conflicting_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("missing placeholder"), .images = &missing_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("malformed [Image #oops]"), .images = &malformed_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = zero_text, .images = &zero_images },
.assistant = @constCast("answer"),
} },
.{ .assistant = .{
.user = .{ .text = duplicate_text, .images = &duplicate_images },
.assistant = @constCast("answer"),
} },
};
defer alloc.free(history[1].assistant.user.text);
defer alloc.free(history[4].assistant.user.text);
defer alloc.free(history[5].assistant.user.text);
conflicting_text_owned = false;
zero_text_owned = false;
duplicate_text_owned = false;
_ = try repair_legacy_zero_image_ids(alloc, &history);
try std.testing.expectEqual(@as(usize, 3), positive_images[0].id);
try std.testing.expectEqual(@as(usize, 1), conflicting_images[0].id);
try std.testing.expectEqual(@as(usize, 2), missing_images[0].id);
try std.testing.expectEqual(@as(usize, 4), malformed_images[0].id);
try std.testing.expectEqual(@as(usize, 5), zero_images[0].id);
try std.testing.expectEqual(@as(usize, 6), duplicate_images[0].id);
try std.testing.expectEqual(@as(usize, 7), duplicate_images[1].id);
try std.testing.expectEqualStrings("owned [Image #3]", history[0].assistant.user.text);
try std.testing.expectEqualStrings("conflict [Image #1]", history[1].assistant.user.text);
try std.testing.expectEqualStrings("missing placeholder", history[2].assistant.user.text);
try std.testing.expectEqualStrings("malformed [Image #oops]", history[3].assistant.user.text);
try std.testing.expectEqualStrings("zero [Image #5]", history[4].assistant.user.text);
try std.testing.expectEqualStrings(
"duplicate [Image #6] [Image #7]",
history[5].assistant.user.text,
);
}
test "legacy image repair binds transcript and Vision authorization to reserved paths" {
const alloc = std.testing.allocator;
var fallback_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/fallback.png"),
.media_type = @constCast("image/png"),
}};
var reserved_images = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/reserved-one.png"),
.media_type = @constCast("image/png"),
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("missing placeholder"), .images = &fallback_images },
.assistant = @constCast("fallback answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("inspect [Image #1]"), .images = &reserved_images },
.assistant = @constCast("reserved answer"),
} },
};
_ = try repair_legacy_zero_image_ids(std.testing.allocator, &history);
var out: std.Io.Writer.Allocating = .init(alloc);
defer out.deinit();
const expanded = try image_attachments.expandPlaceholdersWithBadges(
alloc,
&out.writer,
history[1].assistant.user.text,
history[1].assistant.user.images,
null,
);
const catalog = try collect_image_catalog(alloc, &history, &.{});
defer freeImageAttachmentSlice(alloc, catalog);
const authorized = try image_attachments.resolve_authorized_images(alloc, catalog, &.{ 1, 2 });
defer alloc.free(authorized);
try std.testing.expect(expanded.expanded_any);
try std.testing.expect(std.mem.find(u8, out.written(), "[Image 1]") != null);
try std.testing.expect(std.mem.find(u8, out.written(), "file://") == null);
try std.testing.expect(std.mem.find(u8, out.written(), "/tmp/reserved-one.png") == null);
try std.testing.expect(std.mem.find(u8, out.written(), "/tmp/fallback.png") == null);
try std.testing.expect(std.mem.find(u8, out.written(), "[Image #1]") == null);
try std.testing.expectEqualStrings("/tmp/reserved-one.png", authorized[0].path);
try std.testing.expectEqualStrings("/tmp/fallback.png", authorized[1].path);
}
fn history_contains_image_id(history: []const HistoryTurn, id: usize) bool {
for (history) |turn| {
for (images_for_history_turn(turn)) |attachment| {
if (attachment.id == id) return true;
}
}
return false;
}
fn image_id_less_than(_: void, lhs: ImageAttachment, rhs: ImageAttachment) bool {
return lhs.id < rhs.id;
}
pub const PersistedToolArgumentsSource = enum {
schema_v3,
legacy,
};
pub fn repairPersistedToolArguments(
alloc: Allocator,
calls: []ToolCall,
results: []PersistedToolResult,
source: PersistedToolArgumentsSource,
) !void {
for (calls) |call| {
if (call.argument_integrity == .malformed_json) {
_ = try persistedResultForMalformedCall(calls, results, call);
}
}
for (calls) |*call| {
if (call.argument_integrity != .malformed_json) continue;
const result = try persistedResultForMalformedCall(calls, results, call.*);
const failure_output = try tool_result_errors.malformedToolArgumentsJson(alloc, call.name);
alloc.free(result.output);
if (result.output_handle) |handle| alloc.free(handle);
if (result.preview) |preview| alloc.free(preview);
result.status = .failure;
result.output = failure_output;
result.output_handle = null;
result.preview = null;
result.output_bytes = failure_output.len;
result.stored_output_bytes = failure_output.len;
result.truncated = false;
result.provider_native = false;
call.argument_integrity = .valid;
tracePersistedToolArgumentsRepair(call.*, source, true);
}
}
pub fn repairPersistedInterruptedToolArguments(
call: *ToolCall,
source: PersistedToolArgumentsSource,
) void {
if (call.argument_integrity != .malformed_json) return;
call.argument_integrity = .valid;
tracePersistedToolArgumentsRepair(call.*, source, false);
}
fn persistedResultForMalformedCall(
calls: []ToolCall,
results: []PersistedToolResult,
call: ToolCall,
) !*PersistedToolResult {
var matching_calls: usize = 0;
for (calls) |candidate| {
if (std.mem.eql(u8, candidate.id, call.id)) matching_calls += 1;
}
if (matching_calls != 1) return error.InvalidSessionFormat;
var matched_result: ?*PersistedToolResult = null;
for (results) |*result| {
if (!std.mem.eql(u8, result.tool_call_id, call.id)) continue;
if (matched_result != null) return error.InvalidSessionFormat;
matched_result = result;
}
const result = matched_result orelse return error.InvalidSessionFormat;
if (!std.mem.eql(u8, result.tool_name, call.name)) return error.InvalidSessionFormat;
return result;
}
fn tracePersistedToolArgumentsRepair(
call: ToolCall,
source: PersistedToolArgumentsSource,
paired_result: bool,
) void {
debug_trace.eventf(
"session",
"persisted_tool_arguments_repaired",
.{},
"source={s} call_id={s} tool={s} failure=malformed_json paired_result={s}",
.{ @tagName(source), call.id, call.name, if (paired_result) "true" else "false" },
);
}
pub const WebFetchArtifactState = union(enum) {
none,
store: web_fetch_artifacts.Store,
unavailable: anyerror,
};
pub const SessionRuntime = struct {
history: std.ArrayList(HistoryTurn) = .empty,
context_notice_hashes: std.AutoHashMapUnmanaged(u64, void) = .empty,
context_notice_lock: std.Io.Mutex = .init,
web_fetch_artifacts: WebFetchArtifactState = .none,
language_lock: std.Io.Mutex = .init,
conversation_language: ConversationLanguage = ConversationLanguage.default(),
usage: session_usage.Usage = session_usage.Usage.initFresh(),
profile_usage: profile_usage_runtime.Runtime = .{},
permission_state_lock: std.Io.Mutex = .init,
permission_state: session_permission_state.State = .{},
/// Count limit for owned model-context snapshots; canonical history is not truncated.
max_history_turns: usize,
context_history_start: usize = 0,
pub fn init(
max_history_turns: usize,
provider: generation_usage_provider.Provider,
) SessionRuntime {
return .{
.usage = session_usage.Usage.initFreshWithProvider(provider),
.max_history_turns = max_history_turns,
};
}
pub fn deinit(self: *SessionRuntime, alloc: Allocator) void {
self.clearWebFetchArtifacts();
self.usage.configurePublicationSink(null);
self.usage.configureCheckpointSink(null);
self.usage.deinit(alloc);
self.profile_usage.deinit(alloc);
self.permission_state.deinit(alloc);
self.clearHistory(alloc);
self.history.deinit(alloc);
self.context_notice_hashes.deinit(alloc);
}
pub fn initializeProfileUsage(
self: *SessionRuntime,
alloc: Allocator,
home_path: ?[]const u8,
) !profile_usage_runtime.InitializeOutcome {
self.usage.configurePublicationSink(null);
return self.profile_usage.initialize(alloc, home_path);
}
/// Attaches callbacks only after the host has placed SessionRuntime at its
/// final address.
pub fn attachProfileUsagePublisher(
self: *SessionRuntime,
alloc: Allocator,
) void {
self.usage.configurePublicationSink(
if (self.profile_usage.isAvailable())
self.profile_usage.publisherSink(alloc)
else
null,
);
}
pub fn ensureProfileUsageReadable(
self: *SessionRuntime,
alloc: Allocator,
home_path: ?[]const u8,
) !profile_usage_runtime.InitializeOutcome {
if (self.profile_usage.isAvailable()) return .available;
return self.initializeProfileUsage(alloc, home_path);
}
pub fn reset(self: *SessionRuntime, alloc: Allocator) void {
self.clearWebFetchArtifacts();
self.usage.resetFresh(alloc);
self.clearPermissionState(alloc);
self.clearHistory(alloc);
self.clearContextNotices();
self.setConversationLanguage(ConversationLanguage.default());
}
pub fn restore(self: *SessionRuntime, alloc: Allocator, language: ConversationLanguage, history: []const HistoryTurn) !void {
self.clearWebFetchArtifacts();
self.usage.resetLegacy(alloc);
self.clearPermissionState(alloc);
self.clearHistory(alloc);
self.clearContextNotices();
self.setConversationLanguage(language);
for (history) |turn| {
try self.appendHistoryEntry(alloc, turn);
}
}
pub fn restoreWithContextHistoryStart(
self: *SessionRuntime,
alloc: Allocator,
language: ConversationLanguage,
history: []const HistoryTurn,
context_history_start: usize,
) !void {
if (context_history_start > history.len) return error.InvalidContextHistoryStart;
try self.restore(alloc, language, history);
self.context_history_start = context_history_start;
}
pub fn restoreWithPermissionState(
self: *SessionRuntime,
alloc: Allocator,
language: ConversationLanguage,
history: []const HistoryTurn,
context_history_start: usize,
permission_state: session_permission_state.State,
) !void {
if (context_history_start > history.len) {
return error.InvalidContextHistoryStart;
}
var permission_copy = try session_permission_state.dupe(
alloc,
permission_state,
);
errdefer permission_copy.deinit(alloc);
try self.restoreWithContextHistoryStart(
alloc,
language,
history,
context_history_start,
);
self.permission_state_lock.lockUncancelable(io_mod.getIo());
defer self.permission_state_lock.unlock(io_mod.getIo());
self.permission_state.deinit(alloc);
self.permission_state = permission_copy;
}
pub fn snapshotPermissionState(
self: *SessionRuntime,
alloc: Allocator,
) !session_permission_state.State {
self.permission_state_lock.lockUncancelable(io_mod.getIo());
defer self.permission_state_lock.unlock(io_mod.getIo());
return session_permission_state.dupe(alloc, self.permission_state);
}
pub fn applyPermissionEvent(
self: *SessionRuntime,
alloc: Allocator,
event: session_permission_state.ConfirmedRuleEvent,
) !session_permission_state.ApplyStatus {
self.permission_state_lock.lockUncancelable(io_mod.getIo());
defer self.permission_state_lock.unlock(io_mod.getIo());
const result = try session_permission_state.apply(
alloc,
self.permission_state,
event,
);
return switch (result) {
.applied => |next| blk: {
self.permission_state.deinit(alloc);
self.permission_state = next;
break :blk .applied;
},
.stale => .stale,
.full => .full,
.invalid => .invalid,
};
}
pub fn replacePermissionStateOwned(
self: *SessionRuntime,
alloc: Allocator,
state: *session_permission_state.State,
) void {
self.permission_state_lock.lockUncancelable(io_mod.getIo());
defer self.permission_state_lock.unlock(io_mod.getIo());
self.permission_state.deinit(alloc);
self.permission_state = state.*;
state.* = .{};
}
fn clearPermissionState(self: *SessionRuntime, alloc: Allocator) void {
self.permission_state_lock.lockUncancelable(io_mod.getIo());
defer self.permission_state_lock.unlock(io_mod.getIo());
self.permission_state.deinit(alloc);
}
pub fn configureWebFetchArtifacts(self: *SessionRuntime, alloc: Allocator, session_dir: []const u8) void {
self.clearWebFetchArtifacts();
const store = web_fetch_artifacts.Store.init(alloc, session_dir) catch |err| {
self.web_fetch_artifacts = .{ .unavailable = err };
return;
};
self.web_fetch_artifacts = .{ .store = store };
}
pub fn clearWebFetchArtifacts(self: *SessionRuntime) void {
switch (self.web_fetch_artifacts) {
.store => |*store| store.deinit(),
.none, .unavailable => {},
}
self.web_fetch_artifacts = .none;
}
/// Returns true once for each distinct source-limit notice in this live
/// session. Size and limit are part of the rendered notice, so either
/// changing makes the updated condition visible again.
pub fn claimContextNotice(self: *SessionRuntime, alloc: Allocator, notice: []const u8) !bool {
self.context_notice_lock.lockUncancelable(io_mod.getIo());
defer self.context_notice_lock.unlock(io_mod.getIo());
const result = try self.context_notice_hashes.getOrPut(
alloc,
std.hash.Wyhash.hash(0, notice),
);
return !result.found_existing;
}
pub fn clearContextNotices(self: *SessionRuntime) void {
self.context_notice_lock.lockUncancelable(io_mod.getIo());
defer self.context_notice_lock.unlock(io_mod.getIo());
self.context_notice_hashes.clearRetainingCapacity();
}
pub fn webFetchArtifactStore(self: *SessionRuntime) ?*web_fetch_artifacts.Store {
return switch (self.web_fetch_artifacts) {
.store => |*store| store,
.none, .unavailable => null,
};
}
pub fn webFetchArtifactError(self: *SessionRuntime) ?anyerror {
return switch (self.web_fetch_artifacts) {
.unavailable => |err| err,
.none, .store => null,
};
}
pub fn clearHistory(self: *SessionRuntime, alloc: Allocator) void {
for (self.history.items) |turn| {
freeHistoryTurn(alloc, turn);
}
self.history.clearRetainingCapacity();
self.context_history_start = 0;
}
pub fn historyLen(self: *const SessionRuntime) usize {
return self.history.items.len;
}
pub fn contextHistoryStart(self: *const SessionRuntime) usize {
return self.context_history_start;
}
pub fn compactedTurnCount(self: *const SessionRuntime) usize {
if (self.history.items.len == 0) return 0;
return switch (self.history.items[0]) {
.compacted_summary => |entry| entry.removed_turn_count,
else => 0,
};
}
pub fn compactionCount(self: *const SessionRuntime) usize {
if (self.history.items.len == 0) return 0;
return switch (self.history.items[0]) {
.compacted_summary => |entry| entry.compaction_count,
else => 0,
};
}
pub fn snapshotHistory(self: *const SessionRuntime, alloc: Allocator) ![]HistoryTurn {
var copy: std.ArrayList(HistoryTurn) = .empty;
errdefer {
for (copy.items) |turn| {
freeHistoryTurn(alloc, turn);
}
copy.deinit(alloc);
}
try appendHistoryCopies(alloc, ©, self.history.items);
return copy.toOwnedSlice(alloc);
}
pub fn snapshotImageCatalog(
self: *const SessionRuntime,
alloc: Allocator,
current_images: []const ImageAttachment,
) ![]ImageAttachment {
return collect_image_catalog(alloc, self.history.items, current_images);
}
pub fn snapshotContextHistory(self: *const SessionRuntime, alloc: Allocator) ![]HistoryTurn {
return snapshotOwnedContextHistory(
alloc,
self.history.items,
self.context_history_start,
self.max_history_turns,
);
}
pub fn appendHistoryEntry(self: *SessionRuntime, alloc: Allocator, turn: HistoryTurn) !void {
const copy = try dupeHistoryTurn(alloc, turn);
var owns_copy = true;
errdefer if (owns_copy) freeHistoryTurn(alloc, copy);
try self.history.append(alloc, copy);
owns_copy = false;
}
pub fn appendAssistantHistoryTurn(self: *SessionRuntime, alloc: Allocator, user: []const u8, assistant: []const u8) !void {
const turn = try makeAssistantTurn(alloc, user, assistant);
var owns_turn = true;
errdefer if (owns_turn) freeHistoryTurn(alloc, turn);
try self.history.append(alloc, turn);
owns_turn = false;
}
pub fn appendBackgroundCommandHistoryTurn(self: *SessionRuntime, alloc: Allocator, user: []const u8, background: command_contract.BackgroundCommand) !void {
const user_text = try alloc.dupe(u8, user);
var owns_user_text = true;
errdefer if (owns_user_text) alloc.free(user_text);
const log_path = try alloc.dupe(u8, background.log_path);
var owns_log_path = true;
errdefer if (owns_log_path) alloc.free(log_path);
const url: ?[]u8 = if (background.url) |url_text| try alloc.dupe(u8, url_text) else null;
var owns_url = url != null;
errdefer if (owns_url) {
if (url) |url_text| alloc.free(url_text);
};
const turn = HistoryTurn{ .background_command = .{
.user = .{ .text = user_text, .images = &.{} },
.log_path = log_path,
.expect_url = background.expect_url,
.url = url,
.background_record_id = background.background_record_id,
} };
owns_user_text = false;
owns_log_path = false;
owns_url = false;
var owns_turn = true;
errdefer if (owns_turn) freeHistoryTurn(alloc, turn);
try self.history.append(alloc, turn);
owns_turn = false;
}
pub fn appendHistoryMessages(
alloc: Allocator,
messages: *std.ArrayList(message.Message),
history: []const HistoryTurn,
) !void {
_ = try appendHistoryMessagesImpl(alloc, messages, history, true);
}
pub fn appendHistoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
) !void {
_ = try appendHistoryChatMessagesImpl(alloc, messages, history, true);
}
pub fn setConversationLanguageFromUserMessage(self: *SessionRuntime, text: []const u8) void {
const fallback = self.languageSnapshot();
self.setConversationLanguage(inferConversationLanguage(text, fallback));
}
pub fn languageSnapshot(self: *const SessionRuntime) ConversationLanguage {
const mutable: *SessionRuntime = @constCast(self);
mutable.language_lock.lockUncancelable(io_mod.getIo());
defer mutable.language_lock.unlock(io_mod.getIo());
return mutable.conversation_language;
}
pub fn lastAssistantReply(self: *const SessionRuntime) ?[]const u8 {
var i = self.history.items.len;
while (i > 0) {
i -= 1;
switch (self.history.items[i]) {
.assistant => |entry| {
if (entry.assistant.len > 0) return entry.assistant;
},
else => {},
}
}
return null;
}
pub fn lastSummary(self: *const SessionRuntime) ?[]const u8 {
for (self.history.items) |turn| {
switch (turn) {
.compacted_summary => |entry| return entry.summary,
else => {},
}
}
return null;
}
pub fn forceCompaction(self: *SessionRuntime) void {
if (self.history.items.len <= 1) return;
self.context_history_start = self.history.items.len - 1;
}
fn setConversationLanguage(self: *SessionRuntime, language: ConversationLanguage) void {
self.language_lock.lockUncancelable(io_mod.getIo());
defer self.language_lock.unlock(io_mod.getIo());
self.conversation_language = language;
}
};
fn appendHistoryCopies(
alloc: Allocator,
destination: *std.ArrayList(HistoryTurn),
history: []const HistoryTurn,
) !void {
try destination.ensureUnusedCapacity(alloc, history.len);
for (history) |turn| {
const copy = try dupeHistoryTurn(alloc, turn);
destination.appendAssumeCapacity(copy);
}
}
/// Builds an independently owned prompt-history snapshot from canonical history.
pub fn snapshotOwnedContextHistory(
alloc: Allocator,
canonical_history: []const HistoryTurn,
context_history_start: usize,
max_history_turns: usize,
) ![]HistoryTurn {
var copy: std.ArrayList(HistoryTurn) = .empty;
errdefer {
for (copy.items) |turn| {
freeHistoryTurn(alloc, turn);
}
copy.deinit(alloc);
}
const start = @min(context_history_start, canonical_history.len);
try appendCompactedPrefix(
alloc,
©,
canonical_history[0..start],
);
try appendHistoryCopies(alloc, ©, canonical_history[start..]);
_ = try compactHistory(©, alloc, max_history_turns);
return copy.toOwnedSlice(alloc);
}
fn appendCompactedPrefix(
alloc: Allocator,
destination: *std.ArrayList(HistoryTurn),
history: []const HistoryTurn,
) !void {
if (history.len == 0) return;
const has_existing_summary = history[0] == .compacted_summary;
const existing_summary = if (has_existing_summary) history[0].compacted_summary else null;
const removed = history[@intFromBool(has_existing_summary)..];
if (removed.len == 0) {
try appendHistoryCopies(alloc, destination, history);
return;
}
const summary = try buildCompactedSummaryTurn(alloc, existing_summary, removed);
errdefer freeHistoryTurn(alloc, .{ .compacted_summary = summary });
try destination.append(alloc, .{ .compacted_summary = summary });
}
/// Frees an owned image attachment slice and each attachment field.
pub fn freeImageAttachmentSlice(alloc: Allocator, attachments: []ImageAttachment) void {
for (attachments) |attachment| {
core_types.freeImageAttachment(alloc, attachment);
}
if (attachments.len > 0) alloc.free(attachments);
}
/// Frees an owned history slice; callers pass slices returned by session helpers.
pub fn freeHistoryTurnSlice(alloc: Allocator, turns: []HistoryTurn) void {
for (turns) |turn| freeHistoryTurn(alloc, turn);
if (turns.len > 0) alloc.free(turns);
}
/// Deep-copies an image attachment slice; caller owns the returned slice and frees with freeImageAttachmentSlice.
pub fn dupeImageAttachmentSlice(alloc: Allocator, attachments: []const ImageAttachment) ![]ImageAttachment {
if (attachments.len == 0) return &.{};
const copy = try alloc.alloc(ImageAttachment, attachments.len);
errdefer alloc.free(copy);
var copied: usize = 0;
errdefer {
var i: usize = 0;
while (i < copied) : (i += 1) {
core_types.freeImageAttachment(alloc, copy[i]);
}
}
for (attachments, 0..) |attachment, i| {
copy[i] = try dupeImageAttachment(alloc, attachment);
copied += 1;
}
return copy;
}
fn dupeImageAttachment(alloc: Allocator, src: ImageAttachment) !ImageAttachment {
const path = try alloc.dupe(u8, src.path);
errdefer alloc.free(path);
const media_type = try alloc.dupe(u8, src.media_type);
errdefer alloc.free(media_type);
const snapshot_path = if (src.snapshot_path) |value|
try alloc.dupe(u8, value)
else
null;
errdefer if (snapshot_path) |value| alloc.free(value);
const snapshot_sha256 = if (src.snapshot_sha256) |value|
try alloc.dupe(u8, value)
else
null;
return .{
.id = src.id,
.path = path,
.media_type = media_type,
.snapshot_path = snapshot_path,
.snapshot_sha256 = snapshot_sha256,
};
}
/// Deep-copies one history turn; caller owns the returned turn and frees with freeHistoryTurn.
pub fn dupeHistoryTurn(alloc: Allocator, turn: HistoryTurn) !HistoryTurn {
switch (turn) {
.compacted_summary => |entry| {
const summary = try alloc.dupe(u8, entry.summary);
errdefer alloc.free(summary);
const root_user_messages = try core_types.dupeCompletedToolNames(
alloc,
entry.root_user_messages,
);
errdefer core_types.freeCompletedToolNames(alloc, root_user_messages);
const permission_feedback = try core_types.dupePermissionFeedback(
alloc,
entry.permission_feedback,
);
return .{ .compacted_summary = .{
.summary = summary,
.removed_turn_count = entry.removed_turn_count,
.compaction_count = entry.compaction_count,
.root_user_messages = root_user_messages,
.root_user_messages_complete = entry.root_user_messages_complete,
.permission_feedback = permission_feedback,
.permission_feedback_complete = entry.permission_feedback_complete,
} };
},
.assistant => |entry| {
const user = try dupeUserTurn(alloc, entry.user);
errdefer freeUserTurn(alloc, user);
const assistant_copy = try alloc.dupe(u8, entry.assistant);
errdefer alloc.free(assistant_copy);
const execution = try core_types.dupeExecutionMemory(alloc, entry.execution);
return .{ .assistant = .{
.user = user,
.assistant = assistant_copy,
.execution = execution,
} };
},
.background_command => |entry| {
const user = try dupeUserTurn(alloc, entry.user);
errdefer freeUserTurn(alloc, user);
const assistant = if (entry.assistant) |text| try alloc.dupe(u8, text) else null;
errdefer if (assistant) |text| alloc.free(text);
const execution = try core_types.dupeExecutionMemory(alloc, entry.execution);
errdefer core_types.freeExecutionMemory(alloc, execution);
const log_path = try alloc.dupe(u8, entry.log_path);
errdefer alloc.free(log_path);
const url: ?[]u8 = if (entry.url) |url_bytes| try alloc.dupe(u8, url_bytes) else null;
return .{ .background_command = .{
.user = user,
.assistant = assistant,
.execution = execution,
.log_path = log_path,
.expect_url = entry.expect_url,
.url = url,
.background_record_id = entry.background_record_id,
} };
},
.interrupted => |entry| {
const user = try dupeUserTurn(alloc, entry.user);
errdefer freeUserTurn(alloc, user);
const assistant = if (entry.assistant) |text| try alloc.dupe(u8, text) else null;
errdefer if (assistant) |text| alloc.free(text);
const tool_call = if (entry.tool_call) |call| try core_types.dupeToolCall(alloc, call) else null;
errdefer if (tool_call) |call| core_types.freeToolCall(alloc, call);
const completed_tool_names = try core_types.dupeCompletedToolNames(alloc, entry.completed_tool_names);
errdefer core_types.freeCompletedToolNames(alloc, completed_tool_names);
const cancelled_command = if (entry.cancelled_command) |presentation|
try core_types.dupeCancelledCommandPresentation(alloc, presentation)
else
null;
errdefer if (cancelled_command) |presentation| {
core_types.freeCancelledCommandPresentation(alloc, presentation);
};
const execution = try core_types.dupeExecutionMemory(alloc, entry.execution);
return .{ .interrupted = .{
.user = user,
.assistant = assistant,
.tool_call = tool_call,
.completed_tool_names = completed_tool_names,
.execution = execution,
.cancelled_command = cancelled_command,
.terminal_reason = entry.terminal_reason,
} };
},
}
}
pub fn appendAssistantTurnWithExecution(
alloc: Allocator,
current: []HistoryTurn,
user_text: []const u8,
assistant_text: []const u8,
execution: *core_types.ExecutionMemory,
) ![]HistoryTurn {
const user_copy = try alloc.dupe(u8, user_text);
errdefer alloc.free(user_copy);
const assistant_copy = try alloc.dupe(u8, assistant_text);
errdefer alloc.free(assistant_copy);
const next = try alloc.alloc(HistoryTurn, current.len + 1);
errdefer alloc.free(next);
std.mem.copyForwards(HistoryTurn, next[0..current.len], current);
next[current.len] = .{ .assistant = .{
.user = .{ .text = user_copy, .images = &.{} },
.assistant = assistant_copy,
.execution = execution.*,
} };
execution.* = .{};
if (current.len > 0) alloc.free(current);
return next;
}
/// Appends a finished turn to an owned prompt-context projection and reapplies its turn limit.
pub fn appendHistoryTurnToOwnedContext(
alloc: Allocator,
current: []HistoryTurn,
turn: HistoryTurn,
max_history_turns: usize,
) ![]HistoryTurn {
var list: std.ArrayList(HistoryTurn) = .empty;
errdefer {
for (list.items) |owned_turn| {
freeHistoryTurn(alloc, owned_turn);
}
list.deinit(alloc);
}
try list.ensureTotalCapacity(alloc, current.len + 1);
for (current) |owned_turn| {
list.appendAssumeCapacity(owned_turn);
}
if (current.len > 0) alloc.free(current);
const duplicate = try dupeHistoryTurn(alloc, turn);
var owns_duplicate = true;
errdefer if (owns_duplicate) freeHistoryTurn(alloc, duplicate);
try list.append(alloc, duplicate);
owns_duplicate = false;
_ = try compactHistory(&list, alloc, max_history_turns);
return try list.toOwnedSlice(alloc);
}
/// Builds one owned assistant history turn; caller frees with freeHistoryTurn.
pub fn makeAssistantTurn(alloc: Allocator, user_text: []const u8, assistant_text: []const u8) !HistoryTurn {
const user_copy = try alloc.dupe(u8, user_text);
errdefer alloc.free(user_copy);
const assistant_copy = try alloc.dupe(u8, assistant_text);
errdefer alloc.free(assistant_copy);
return .{ .assistant = .{
.user = .{ .text = user_copy, .images = &.{} },
.assistant = assistant_copy,
} };
}
test "unavailable profile usage keeps reconciled generation pending in host runtime" {
const alloc = std.testing.allocator;
const Checkpoint = struct {
calls: usize = 0,
fn persist(raw: *anyopaque, _: session_usage.Snapshot) !void {
const self: *@This() = @ptrCast(@alignCast(raw));
self.calls += 1;
}
};
var checkpoint = Checkpoint{};
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try std.testing.expectEqual(
profile_usage_runtime.InitializeOutcome.unavailable,
try runtime.initializeProfileUsage(alloc, null),
);
runtime.attachProfileUsagePublisher(alloc);
runtime.usage.configureCheckpointSink(.{
.context = &checkpoint,
.allocator = alloc,
.persist = Checkpoint.persist,
});
const sequence = try runtime.usage.reserveInvocation();
try runtime.usage.finishObservedInvocation(
alloc,
sequence,
1,
.observed_generation,
"gen_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"https://ai-gateway.vercel.sh",
null,
);
try runtime.usage.applyGeneration(alloc, .{
.id = "gen_01ARZ3NDEKTSV4RRFFQ69G5FAV",
.created_at_ms = 1000,
.model = "provider/model",
.total_cost = 0.25,
.input_tokens = 10,
.output_tokens = 2,
.cache_read_tokens = 1,
.cache_write_tokens = 0,
.reasoning_tokens = 1,
.billable_web_search_calls = 0,
});
var snapshot = try runtime.usage.snapshot(alloc);
defer snapshot.deinit(alloc);
try std.testing.expectEqual(session_usage.Availability.pending, snapshot.billing);
try std.testing.expectEqual(@as(usize, 1), snapshot.pending.len);
try std.testing.expectEqual(@as(u64, 0), snapshot.input_tokens);
try std.testing.expectEqual(@as(usize, 1), checkpoint.calls);
}
test "readable profile usage does not attach publishers or flush recovery" {
const alloc = std.testing.allocator;
const Checkpoint = struct {
calls: usize = 0,
fn persist(raw: *anyopaque, _: session_usage.Snapshot) !void {
const self: *@This() = @ptrCast(@alignCast(raw));
self.calls += 1;
}
};
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");
defer alloc.free(home);
var checkpoint = Checkpoint{};
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try std.testing.expectEqual(
profile_usage_runtime.InitializeOutcome.available,
try runtime.initializeProfileUsage(alloc, home),
);
runtime.usage.configureCheckpointSink(.{
.context = &checkpoint,
.allocator = alloc,
.persist = Checkpoint.persist,
});
const sequence = try runtime.usage.reserveInvocation();
try runtime.usage.finishObservedInvocation(
alloc,
sequence,
1,
.observed_generation,
"gen_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"https://ai-gateway.vercel.sh",
null,
);
try runtime.usage.applyGeneration(alloc, .{
.id = "gen_01ARZ3NDEKTSV4RRFFQ69G5FAV",
.created_at_ms = 1000,
.model = "provider/model",
.total_cost = 0.25,
.input_tokens = 10,
.output_tokens = 2,
.cache_read_tokens = 1,
.cache_write_tokens = 0,
.reasoning_tokens = 1,
.billable_web_search_calls = 0,
});
const checkpoint_calls = checkpoint.calls;
try std.testing.expectEqual(
profile_usage_runtime.InitializeOutcome.available,
try runtime.ensureProfileUsageReadable(alloc, home),
);
var saved = try runtime.usage.snapshot(alloc);
defer saved.deinit(alloc);
try std.testing.expectEqual(@as(usize, 1), saved.publication_backlog.len);
try std.testing.expectEqual(checkpoint_calls, checkpoint.calls);
try std.testing.expectError(
error.FileNotFound,
tmp.dir.access(io_mod.getIo(), ".fx/usage.jsonl", .{}),
);
}
test "appendAssistantTurnWithExecution transfers memory and clears the source" {
const alloc = std.testing.allocator;
var execution = core_types.ExecutionMemory{
.tool_steps = try alloc.alloc(core_types.ToolExecutionStep, 1),
};
execution.tool_steps[0] = .{
.assistant = try alloc.dupe(u8, "tool step"),
};
const history = try appendAssistantTurnWithExecution(
alloc,
&.{},
"user",
"assistant",
&execution,
);
defer freeHistoryTurnSlice(alloc, history);
try std.testing.expectEqual(@as(usize, 0), execution.tool_steps.len);
try std.testing.expectEqual(@as(usize, 1), history[0].assistant.execution.tool_steps.len);
try std.testing.expectEqualStrings(
"tool step",
history[0].assistant.execution.tool_steps[0].assistant.?,
);
}
test "appendAssistantTurnWithExecution keeps source memory on allocation failure" {
const alloc = std.testing.allocator;
for (0..3) |fail_index| {
var execution = core_types.ExecutionMemory{
.tool_steps = try alloc.alloc(core_types.ToolExecutionStep, 1),
};
execution.tool_steps[0] = .{
.assistant = try alloc.dupe(u8, "tool step"),
};
defer core_types.freeExecutionMemory(alloc, execution);
var failing = std.testing.FailingAllocator.init(
alloc,
.{ .fail_index = fail_index },
);
try std.testing.expectError(
error.OutOfMemory,
appendAssistantTurnWithExecution(
failing.allocator(),
&.{},
"user",
"assistant",
&execution,
),
);
try std.testing.expectEqual(@as(usize, 1), execution.tool_steps.len);
try std.testing.expectEqualStrings(
"tool step",
execution.tool_steps[0].assistant.?,
);
}
}
/// Projects stored history into request messages while preserving stored turn order.
pub fn appendHistoryMessages(
alloc: Allocator,
messages: *std.ArrayList(message.Message),
history: []const HistoryTurn,
) !void {
_ = try appendHistoryMessagesImpl(alloc, messages, history, true);
}
pub fn appendHistoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
) !void {
_ = try appendHistoryChatMessagesImpl(alloc, messages, history, true);
}
pub const HistoryBudgetOptions = struct {
max_tokens: usize = 0,
};
pub fn appendHistoryMessagesBudgeted(
alloc: Allocator,
messages: *std.ArrayList(message.Message),
history: []const HistoryTurn,
opts: HistoryBudgetOptions,
) !void {
const keep = try selectBudgetedHistoryTurns(alloc, history, opts) orelse
return appendHistoryMessages(alloc, messages, history);
defer alloc.free(keep);
const trimmed_context = try formatBudgetTrimmedHistoryContext(
alloc,
history,
keep,
);
var owns_trimmed_context = true;
errdefer if (owns_trimmed_context) alloc.free(trimmed_context);
try messages.append(
alloc,
message.Message.systemOwned(trimmed_context),
);
owns_trimmed_context = false;
var in_leading_summary_prefix = true;
for (history, 0..) |turn, idx| {
if (!keep[idx]) {
in_leading_summary_prefix = continuesLeadingSummaryPrefix(in_leading_summary_prefix, turn);
continue;
}
in_leading_summary_prefix = try appendHistoryMessagesImpl(
alloc,
messages,
history[idx .. idx + 1],
in_leading_summary_prefix,
);
}
}
pub fn appendHistoryChatMessagesBudgeted(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
opts: HistoryBudgetOptions,
) !void {
const keep = try selectBudgetedHistoryTurns(alloc, history, opts) orelse
return appendHistoryChatMessages(alloc, messages, history);
defer alloc.free(keep);
const trimmed_context = try formatBudgetTrimmedHistoryContext(alloc, history, keep);
errdefer alloc.free(trimmed_context);
try messages.append(alloc, .{ .role = .system, .content = trimmed_context });
var in_leading_summary_prefix = true;
for (history, 0..) |turn, idx| {
if (!keep[idx]) {
in_leading_summary_prefix = continuesLeadingSummaryPrefix(in_leading_summary_prefix, turn);
continue;
}
in_leading_summary_prefix = try appendHistoryChatMessagesImpl(
alloc,
messages,
history[idx .. idx + 1],
in_leading_summary_prefix,
);
}
}
fn continuesLeadingSummaryPrefix(in_leading_summary_prefix: bool, turn: HistoryTurn) bool {
return in_leading_summary_prefix and turn == .compacted_summary;
}
fn selectBudgetedHistoryTurns(
alloc: Allocator,
history: []const HistoryTurn,
opts: HistoryBudgetOptions,
) !?[]bool {
if (opts.max_tokens == 0 or history.len == 0) return null;
const keep = try alloc.alloc(bool, history.len);
errdefer alloc.free(keep);
@memset(keep, false);
var used_tokens: usize = 0;
var kept_count: usize = 0;
var i = history.len;
while (i > 0) {
i -= 1;
const cost = estimateHistoryTurnTokens(history[i]);
if (kept_count == 0 or used_tokens + cost <= opts.max_tokens) {
keep[i] = true;
used_tokens += cost;
kept_count += 1;
}
}
if (kept_count == history.len) {
alloc.free(keep);
return null;
}
return keep;
}
fn appendHistoryMessagesImpl(
alloc: Allocator,
messages: *std.ArrayList(message.Message),
history: []const HistoryTurn,
starts_in_leading_summary_prefix: bool,
) !bool {
var in_leading_summary_prefix = starts_in_leading_summary_prefix;
for (history) |turn| {
const summary_is_system = in_leading_summary_prefix;
in_leading_summary_prefix = continuesLeadingSummaryPrefix(in_leading_summary_prefix, turn);
switch (turn) {
.compacted_summary => |entry| {
const text = try formatCompactedContinuationMessage(alloc, entry.summary);
errdefer alloc.free(text);
try messages.append(
alloc,
if (summary_is_system)
message.Message.systemOwned(text)
else
message.Message.userOwned(text),
);
},
.assistant => |entry| {
try messages.append(alloc, .{
.role = .user,
.content = .{ .text = entry.user.text },
.images = entry.user.images,
});
try appendExecutionMemoryMessages(alloc, messages, entry.execution);
if (entry.assistant.len > 0) {
try messages.append(alloc, message.Message.assistantBorrowed(entry.assistant, &.{}));
}
},
.background_command => |entry| {
try messages.append(alloc, .{
.role = .user,
.content = .{ .text = entry.user.text },
.images = entry.user.images,
});
try appendExecutionMemoryMessages(alloc, messages, entry.execution);
if (entry.assistant) |assistant| {
if (assistant.len > 0) {
try messages.append(alloc, message.Message.assistantBorrowed(assistant, &.{}));
}
}
const text = try formatBackgroundHistoryContext(alloc, entry);
errdefer alloc.free(text);
try messages.append(alloc, message.Message.userOwned(text));
},
.interrupted => |entry| {
try messages.append(alloc, .{
.role = .user,
.content = .{ .text = entry.user.text },
.images = entry.user.images,
});
try appendExecutionMemoryMessages(alloc, messages, entry.execution);
if (entry.tool_call) |tool_call| {
const assistant_content = try formatInterruptedAssistantToolContent(alloc, entry);
var owns_assistant_content = assistant_content != null;
errdefer if (owns_assistant_content) {
if (assistant_content) |text| alloc.free(text);
};
const calls = try alloc.alloc(core_types.ToolCall, 1);
var owns_calls = true;
var calls_initialized = false;
errdefer if (owns_calls) {
if (calls_initialized) core_types.freeToolCall(alloc, calls[0]);
alloc.free(calls);
};
calls[0] = try core_types.dupeToolCall(alloc, tool_call);
calls_initialized = true;
try messages.append(alloc, .{
.role = .assistant,
.content = if (assistant_content) |text| .{ .text = text } else null,
.tool_calls = calls,
.owns_content = assistant_content != null,
.owns_tool_calls = true,
});
owns_assistant_content = false;
owns_calls = false;
try messages.append(alloc, .{
.role = .tool,
.content = .{ .text = aborted_tool_output },
.tool_call_id = tool_call.id,
.tool_name = tool_call.name,
.tool_result_status = .failure,
});
} else {
const assistant_content = try formatInterruptedAssistantClosedContent(alloc, entry);
var owns_assistant_content = true;
errdefer if (owns_assistant_content) alloc.free(assistant_content);
try messages.append(alloc, message.Message.assistantOwned(assistant_content, &.{}));
owns_assistant_content = false;
}
const text = try formatInterruptedHistoryContext(alloc, entry);
errdefer alloc.free(text);
try messages.append(alloc, message.Message.userOwned(text));
},
}
}
return in_leading_summary_prefix;
}
fn appendExecutionMemoryMessages(
alloc: Allocator,
messages: *std.ArrayList(message.Message),
execution: core_types.ExecutionMemory,
) !void {
for (execution.tool_steps) |step| {
if (step.tool_calls.len == 0) continue;
try messages.append(alloc, .{
.role = .assistant,
.content = if (step.assistant) |text| .{ .text = text } else null,
.tool_calls = step.tool_calls,
});
for (step.tool_results) |result| {
try messages.append(alloc, .{
.role = .tool,
.content = .{ .text = result.output },
.tool_call_id = result.tool_call_id,
.tool_name = result.tool_name,
.tool_result_status = result.status,
});
}
for (step.tool_results) |result| {
for (result.permission_feedback) |feedback| {
try messages.append(alloc, message.Message.userText(feedback));
}
}
}
if (execution.files.len > 0) {
const text = try formatExecutionFileContext(alloc, execution.files);
errdefer alloc.free(text);
try messages.append(alloc, message.Message.userOwned(text));
}
}
pub fn appendExecutionMemoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
execution: core_types.ExecutionMemory,
) !void {
for (execution.tool_steps) |step| {
if (step.tool_calls.len == 0) continue;
try messages.append(alloc, .{
.role = .assistant,
.content = step.assistant,
.tool_calls = step.tool_calls,
});
for (step.tool_results) |result| {
try messages.append(alloc, .{
.role = .tool,
.content = result.output,
.tool_call_id = result.tool_call_id,
.tool_name = result.tool_name,
.tool_result_status = result.status,
});
}
for (step.tool_results) |result| {
for (result.permission_feedback) |feedback| {
try messages.append(alloc, .{ .role = .user, .content = feedback });
}
}
}
if (execution.files.len > 0) {
const text = try formatExecutionFileContext(alloc, execution.files);
errdefer alloc.free(text);
try messages.append(alloc, .{ .role = .user, .content = text });
}
}
fn appendHistoryChatMessagesImpl(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
starts_in_leading_summary_prefix: bool,
) !bool {
var in_leading_summary_prefix = starts_in_leading_summary_prefix;
for (history) |turn| {
const summary_is_system = in_leading_summary_prefix;
in_leading_summary_prefix = continuesLeadingSummaryPrefix(in_leading_summary_prefix, turn);
switch (turn) {
.compacted_summary => |entry| {
const text = try formatCompactedContinuationMessage(alloc, entry.summary);
errdefer alloc.free(text);
try messages.append(alloc, .{
.role = if (summary_is_system) .system else .user,
.content = text,
});
},
.assistant => |entry| {
try messages.append(alloc, .{ .role = .user, .content = entry.user.text, .images = entry.user.images });
try appendExecutionMemoryChatMessages(alloc, messages, entry.execution);
if (entry.assistant.len > 0) {
try messages.append(alloc, .{ .role = .assistant, .content = entry.assistant });
}
},
.background_command => |entry| {
try messages.append(alloc, .{ .role = .user, .content = entry.user.text, .images = entry.user.images });
try appendExecutionMemoryChatMessages(alloc, messages, entry.execution);
if (entry.assistant) |assistant| {
if (assistant.len > 0) {
try messages.append(alloc, .{ .role = .assistant, .content = assistant });
}
}
const text = try formatBackgroundHistoryContext(alloc, entry);
errdefer alloc.free(text);
try messages.append(alloc, .{ .role = .user, .content = text });
},
.interrupted => |entry| {
try messages.append(alloc, .{ .role = .user, .content = entry.user.text, .images = entry.user.images });
try appendExecutionMemoryChatMessages(alloc, messages, entry.execution);
if (entry.tool_call) |tool_call| {
const assistant_content = try formatInterruptedAssistantToolContent(alloc, entry);
const calls = try alloc.alloc(core_types.ToolCall, 1);
calls[0] = try core_types.dupeToolCall(alloc, tool_call);
try messages.append(alloc, .{ .role = .assistant, .content = assistant_content, .tool_calls = calls });
try messages.append(alloc, .{
.role = .tool,
.content = aborted_tool_output,
.tool_call_id = tool_call.id,
.tool_name = tool_call.name,
.tool_result_status = .failure,
});
} else {
const assistant_content = try formatInterruptedAssistantClosedContent(alloc, entry);
try messages.append(alloc, .{ .role = .assistant, .content = assistant_content });
}
const text = try formatInterruptedHistoryContext(alloc, entry);
errdefer alloc.free(text);
try messages.append(alloc, .{ .role = .user, .content = text });
},
}
}
return in_leading_summary_prefix;
}
/// Infers a conversation language tag from text using core's script-counting heuristic.
pub fn inferConversationLanguage(text: []const u8, fallback: ConversationLanguage) ConversationLanguage {
var counts: ScriptCounts = .{};
var i: usize = 0;
while (i < text.len) {
const width = std.unicode.utf8ByteSequenceLength(text[i]) catch {
i += 1;
continue;
};
if (i + width > text.len) break;
const codepoint = std.unicode.utf8Decode(text[i .. i + width]) catch {
i += width;
continue;
};
classifyCodepoint(&counts, codepoint);
i += width;
}
if (counts.hiragana + counts.katakana > 0) return ConversationLanguage.literal("ja");
if (counts.hangul > 0) return ConversationLanguage.literal("ko");
const scores = [_]struct { count: usize, tag: ConversationLanguage }{
.{ .count = counts.han, .tag = ConversationLanguage.literal("und-Hani") },
.{ .count = counts.arabic, .tag = ConversationLanguage.literal("und-Arab") },
.{ .count = counts.hebrew, .tag = ConversationLanguage.literal("und-Hebr") },
.{ .count = counts.cyrillic, .tag = ConversationLanguage.literal("und-Cyrl") },
.{ .count = counts.greek, .tag = ConversationLanguage.literal("und-Grek") },
.{ .count = counts.devanagari, .tag = ConversationLanguage.literal("und-Deva") },
.{ .count = counts.thai, .tag = ConversationLanguage.literal("und-Thai") },
.{ .count = counts.latin, .tag = ConversationLanguage.literal("und-Latn") },
};
var best_count: usize = 0;
var best_tag = fallback;
var tied = false;
for (scores) |entry| {
if (entry.count == 0) continue;
if (entry.count > best_count) {
best_count = entry.count;
best_tag = entry.tag;
tied = false;
continue;
}
if (entry.count == best_count) tied = true;
}
if (best_count == 0 or tied) return fallback;
return best_tag;
}
pub fn formatCompactedContinuationMessage(alloc: Allocator, summary: []const u8) ![]u8 {
return std.fmt.allocPrint(
alloc,
"{s}{s}\n\n{s}\n{s}",
.{ compact_continuation_preamble, summary, compact_recent_messages_note, compact_direct_resume_instruction },
);
}
pub fn formatBackgroundHistoryContext(alloc: Allocator, entry: BackgroundCommandHistoryTurn) ![]u8 {
if (entry.url) |url| {
return std.fmt.allocPrint(alloc, "Session event: a previous user request launched a background server. Log: {s}. URL observed at launch: {s}. Re-check runtime context for current liveness before reusing it.", .{ entry.log_path, url });
}
if (entry.expect_url) {
return std.fmt.allocPrint(alloc, "Session event: a previous user request launched a background server. Log: {s}. Re-check runtime context for current liveness and URL state before reusing it.", .{entry.log_path});
}
return std.fmt.allocPrint(alloc, "Session event: a previous user request launched a background command. Log: {s}. Re-check runtime context for current liveness before treating it as running.", .{entry.log_path});
}
pub fn formatExecutionFileContext(alloc: Allocator, files: []const core_types.FileEvidence) ![]u8 {
var out: std.Io.Writer.Allocating = .init(alloc);
errdefer out.deinit();
out.writer.writeAll("Session file evidence from previous tool execution. Re-read stale paths before relying on exact contents:") catch return error.OutOfMemory;
for (files) |file| {
out.writer.print("\n- action={s} status={s} path={s}", .{ @tagName(file.action), @tagName(file.status), file.path }) catch return error.OutOfMemory;
if (file.new_path) |new_path| {
out.writer.print(" new_path={s}", .{new_path}) catch return error.OutOfMemory;
}
if (file.model_view_covers_full_file) {
out.writer.writeAll(" model_view=full") catch return error.OutOfMemory;
}
if (file.stale) {
out.writer.writeAll(" stale=true") catch return error.OutOfMemory;
}
out.writer.print(" tool={s}", .{file.tool_name}) catch return error.OutOfMemory;
}
return out.toOwnedSlice() catch return error.OutOfMemory;
}
pub fn formatExecutionReplayContext(
alloc: Allocator,
execution: core_types.ExecutionMemory,
) !?[]u8 {
if (execution.isEmpty()) return null;
var out: std.Io.Writer.Allocating = .init(alloc);
errdefer out.deinit();
out.writer.writeAll("Previous tool execution:") catch return error.OutOfMemory;
for (execution.tool_steps) |step| {
if (step.assistant) |assistant| {
if (assistant.len > 0) {
out.writer.print("\n\nAssistant:\n{s}", .{assistant}) catch return error.OutOfMemory;
}
}
for (step.tool_results) |result| {
out.writer.print(
"\n\nTool {s} ({s}):\n{s}",
.{ result.tool_name, @tagName(result.status), result.output },
) catch return error.OutOfMemory;
for (result.permission_feedback) |feedback| {
out.writer.print(
"\n\nUser permission feedback:\n{s}",
.{feedback},
) catch return error.OutOfMemory;
}
}
}
if (execution.files.len > 0) {
const files = try formatExecutionFileContext(alloc, execution.files);
defer alloc.free(files);
out.writer.print("\n\n{s}", .{files}) catch return error.OutOfMemory;
}
return out.toOwnedSlice() catch return error.OutOfMemory;
}
pub fn formatInterruptedHistoryContext(alloc: Allocator, entry: InterruptedHistoryTurn) ![]u8 {
_ = entry;
return alloc.dupe(u8, interrupted_turn_context);
}
pub fn formatCompletedToolSummary(alloc: Allocator, names: []const []u8) Allocator.Error![]u8 {
var out: std.Io.Writer.Allocating = .init(alloc);
errdefer out.deinit();
out.writer.print(
"Interrupted by user after completing {d} tool call{s}: ",
.{ names.len, if (names.len == 1) "" else "s" },
) catch return error.OutOfMemory;
for (names, 0..) |name, i| {
if (i > 0) out.writer.writeAll(", ") catch return error.OutOfMemory;
out.writer.writeAll(name) catch return error.OutOfMemory;
}
out.writer.writeByte('.') catch return error.OutOfMemory;
return out.toOwnedSlice() catch return error.OutOfMemory;
}
fn formatInterruptedAssistantToolContent(alloc: Allocator, entry: InterruptedHistoryTurn) Allocator.Error!?[]u8 {
const assistant = entry.assistant;
const has_assistant = assistant != null and assistant.?.len > 0;
const has_completed_tools = entry.completed_tool_names.len > 0;
if (!has_assistant and !has_completed_tools) return null;
if (!has_completed_tools) return @as(?[]u8, try alloc.dupe(u8, assistant.?));
const summary = try formatCompletedToolSummary(alloc, entry.completed_tool_names);
var owns_summary = true;
errdefer if (owns_summary) alloc.free(summary);
if (!has_assistant) return @as(?[]u8, summary);
var out: std.Io.Writer.Allocating = .init(alloc);
errdefer out.deinit();
out.writer.print("{s}\n\n{s}", .{ assistant.?, summary }) catch return error.OutOfMemory;
alloc.free(summary);
owns_summary = false;
return @as(?[]u8, out.toOwnedSlice() catch return error.OutOfMemory);
}
fn formatInterruptedAssistantClosedContent(alloc: Allocator, entry: InterruptedHistoryTurn) Allocator.Error![]u8 {
if (entry.completed_tool_names.len > 0) {
if (try formatInterruptedAssistantToolContent(alloc, entry)) |text| return text;
}
if (entry.assistant) |assistant| {
if (assistant.len > 0) return formatInterruptedPartialAssistantClosedContent(alloc, assistant);
}
return alloc.dupe(u8, interrupted_before_completion_output);
}
fn formatInterruptedPartialAssistantClosedContent(alloc: Allocator, assistant: []const u8) Allocator.Error![]u8 {
return std.fmt.allocPrint(alloc, "{s}\n\n{s}", .{ assistant, interrupted_before_completion_output });
}
fn compactHistory(history: *std.ArrayList(HistoryTurn), alloc: Allocator, max_history_turns: usize) !bool {
if (max_history_turns == 0 or history.items.len <= max_history_turns) return false;
if (max_history_turns <= 1) {
while (history.items.len > max_history_turns) {
const oldest = history.orderedRemove(0);
freeHistoryTurn(alloc, oldest);
}
return true;
}
const preserve_recent_turns = preservedRecentTurnCount(max_history_turns);
const existing_summary_len: usize = if (history.items.len > 0 and history.items[0] == .compacted_summary) 1 else 0;
if (history.items.len <= existing_summary_len + preserve_recent_turns) return false;
const keep_from = history.items.len - preserve_recent_turns;
const removed = history.items[existing_summary_len..keep_from];
if (removed.len == 0) return false;
const existing_summary = if (existing_summary_len == 1) history.items[0].compacted_summary else null;
const next_summary = try buildCompactedSummaryTurn(alloc, existing_summary, removed);
errdefer freeHistoryTurn(alloc, .{ .compacted_summary = next_summary });
const preserved_len = history.items.len - keep_from;
const next_items = try alloc.alloc(HistoryTurn, preserved_len + 1);
const old_allocated = history.allocatedSlice();
next_items[0] = .{ .compacted_summary = next_summary };
if (preserved_len > 0) {
std.mem.copyForwards(HistoryTurn, next_items[1 .. 1 + preserved_len], history.items[keep_from..]);
}
var i: usize = 0;
while (i < keep_from) : (i += 1) {
freeHistoryTurn(alloc, history.items[i]);
}
alloc.free(old_allocated);
history.items = next_items;
history.capacity = next_items.len;
return true;
}
fn buildCompactedSummaryTurn(
alloc: Allocator,
existing: ?CompactedSummaryHistoryTurn,
removed: []const HistoryTurn,
) !CompactedSummaryHistoryTurn {
return .{
.summary = try buildCompactedSummaryText(alloc, existing, removed),
.removed_turn_count = (if (existing) |entry| entry.removed_turn_count else 0) + removed.len,
.compaction_count = (if (existing) |entry| entry.compaction_count else 0) + 1,
.root_user_messages = &.{},
.root_user_messages_complete = false,
.permission_feedback = &.{},
.permission_feedback_complete = false,
};
}
test "history compaction discards root-user authority text" {
const alloc = std.testing.allocator;
const removed = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("first exact request") },
.assistant = @constCast("first response"),
} },
.{ .interrupted = .{
.user = .{ .text = @constCast("second exact request") },
} },
};
const first = try buildCompactedSummaryTurn(alloc, null, &removed);
defer freeHistoryTurn(alloc, .{ .compacted_summary = first });
try std.testing.expectEqual(@as(usize, 0), first.root_user_messages.len);
try std.testing.expect(!first.root_user_messages_complete);
const later = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("third exact request") },
.assistant = @constCast("third response"),
} }};
const second = try buildCompactedSummaryTurn(alloc, first, &later);
defer freeHistoryTurn(alloc, .{ .compacted_summary = second });
try std.testing.expectEqual(@as(usize, 0), second.root_user_messages.len);
try std.testing.expect(!second.root_user_messages_complete);
}
test "repeated compaction keeps historical authority discarded" {
const alloc = std.testing.allocator;
const legacy: CompactedSummaryHistoryTurn = .{
.summary = @constCast("legacy summary without exact authority"),
.removed_turn_count = 4,
.compaction_count = 1,
.root_user_messages_complete = false,
.permission_feedback_complete = false,
};
const first_removed = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("newer exact request") },
.assistant = @constCast("response"),
} }};
const first = try buildCompactedSummaryTurn(alloc, legacy, &first_removed);
defer freeHistoryTurn(alloc, .{ .compacted_summary = first });
try std.testing.expect(!first.root_user_messages_complete);
try std.testing.expect(!first.permission_feedback_complete);
try std.testing.expectEqual(@as(usize, 0), first.root_user_messages.len);
const second_removed = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("latest exact request") },
} }};
const second = try buildCompactedSummaryTurn(alloc, first, &second_removed);
defer freeHistoryTurn(alloc, .{ .compacted_summary = second });
try std.testing.expect(!second.root_user_messages_complete);
try std.testing.expect(!second.permission_feedback_complete);
try std.testing.expectEqual(@as(usize, 0), second.root_user_messages.len);
}
test "repeated compaction discards historical permission feedback" {
const alloc = std.testing.allocator;
var first_feedback = [_][]u8{@constCast("Do not write outside the workspace.")};
var first_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("first"),
.tool_name = @constCast("write_file"),
.status = .failure,
.output = @constCast("denied"),
.output_bytes = 6,
.stored_output_bytes = 6,
.permission_feedback = &first_feedback,
}};
var first_steps = [_]ToolExecutionStep{.{ .tool_results = &first_results }};
const first_removed = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("Prepare the report.") },
.assistant = @constCast("I need to write a file."),
.execution = .{ .tool_steps = &first_steps },
} }};
const first = try buildCompactedSummaryTurn(alloc, null, &first_removed);
defer freeHistoryTurn(alloc, .{ .compacted_summary = first });
try std.testing.expect(!first.permission_feedback_complete);
try std.testing.expectEqual(@as(usize, 0), first.permission_feedback.len);
var second_feedback = [_][]u8{@constCast("README changes require approval.")};
var second_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("second"),
.tool_name = @constCast("edit_file"),
.status = .failure,
.output = @constCast("denied"),
.output_bytes = 6,
.stored_output_bytes = 6,
.permission_feedback = &second_feedback,
}};
var second_steps = [_]ToolExecutionStep{.{ .tool_results = &second_results }};
const later = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("Continue with the report.") },
.execution = .{ .tool_steps = &second_steps },
} }};
const repeated = try buildCompactedSummaryTurn(alloc, first, &later);
defer freeHistoryTurn(alloc, .{ .compacted_summary = repeated });
try std.testing.expect(!repeated.permission_feedback_complete);
try std.testing.expectEqual(@as(usize, 0), repeated.permission_feedback.len);
}
fn buildCompactedSummaryText(
alloc: Allocator,
existing: ?CompactedSummaryHistoryTurn,
removed: []const HistoryTurn,
) ![]u8 {
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var lines: std.ArrayList([]const u8) = .empty;
defer lines.deinit(arena);
try lines.append(arena, "Conversation summary:");
try lines.append(arena, try std.fmt.allocPrint(arena, "- Earlier turns compacted: {d}", .{removed.len}));
if (existing) |entry| {
try lines.append(arena, "- Previously compacted context:");
var existing_lines = std.mem.splitScalar(u8, entry.summary, '\n');
while (existing_lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, " \t\r\n");
if (trimmed.len == 0) continue;
try lines.append(arena, try std.fmt.allocPrint(arena, " {s}", .{trimmed}));
}
}
try appendUserSummaryLines(arena, &lines, removed);
try appendAssistantSummaryLines(arena, &lines, removed);
try appendExecutionSummaryLines(arena, &lines, removed);
try appendBackgroundSummaryLines(arena, &lines, removed);
try appendInterruptedSummaryLines(arena, &lines, removed);
if (lines.items.len <= 2) {
try lines.append(arena, "- Earlier conversation context compacted.");
}
return compressSummaryLines(alloc, lines.items);
}
fn appendUserSummaryLines(arena: Allocator, lines: *std.ArrayList([]const u8), removed: []const HistoryTurn) !void {
var added: usize = 0;
var saw_header = false;
for (removed) |turn| {
const user_text = switch (turn) {
.assistant => |entry| entry.user.text,
.background_command => |entry| entry.user.text,
.interrupted => |entry| entry.user.text,
.compacted_summary => continue,
};
const summary = compactLineText(arena, user_text, compact_summary_max_line_chars - 4) catch continue;
if (summary.len == 0) continue;
if (!saw_header) {
try lines.append(arena, "- Recent user requests:");
saw_header = true;
}
try lines.append(arena, try std.fmt.allocPrint(arena, " - {s}", .{summary}));
added += 1;
if (added >= 4) break;
}
}
fn appendAssistantSummaryLines(arena: Allocator, lines: *std.ArrayList([]const u8), removed: []const HistoryTurn) !void {
var added: usize = 0;
var saw_header = false;
for (removed) |turn| {
const assistant_text = switch (turn) {
.assistant => |entry| entry.assistant,
.background_command => |entry| entry.assistant orelse continue,
.interrupted => |entry| entry.assistant orelse continue,
else => continue,
};
const summary = compactLineText(arena, assistant_text, compact_summary_max_line_chars - 4) catch continue;
if (summary.len == 0) continue;
if (!saw_header) {
try lines.append(arena, "- Assistant outcomes:");
saw_header = true;
}
try lines.append(arena, try std.fmt.allocPrint(arena, " - {s}", .{summary}));
added += 1;
if (added >= 3) break;
}
}
fn appendExecutionSummaryLines(arena: Allocator, lines: *std.ArrayList([]const u8), removed: []const HistoryTurn) !void {
var added: usize = 0;
var saw_header = false;
for (removed) |turn| {
const execution = switch (turn) {
.assistant => |entry| entry.execution,
.background_command => |entry| entry.execution,
.interrupted => |entry| entry.execution,
else => continue,
};
for (execution.tool_steps) |step| {
for (step.tool_results) |result| {
if (!saw_header) {
try lines.append(arena, "- Tool execution evidence:");
saw_header = true;
}
try lines.append(arena, try formatToolResultEvidenceLine(arena, result));
added += 1;
if (added >= 4) return;
}
}
for (execution.files) |file| {
if (!saw_header) {
try lines.append(arena, "- Tool execution evidence:");
saw_header = true;
}
const stale = if (file.stale) ", stale" else "";
try lines.append(arena, try std.fmt.allocPrint(arena, " - file {s}: {s}{s}", .{ @tagName(file.action), file.path, stale }));
added += 1;
if (added >= 4) return;
}
}
}
fn formatBudgetTrimmedHistoryContext(alloc: Allocator, history: []const HistoryTurn, keep: []const bool) ![]u8 {
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var lines: std.ArrayList([]const u8) = .empty;
defer lines.deinit(arena);
var omitted: usize = 0;
for (keep) |kept| {
if (!kept) omitted += 1;
}
try lines.append(arena, try std.fmt.allocPrint(arena, "Context budget trimmed {d} older history turn(s). Recent turns are preserved verbatim.", .{omitted}));
try appendBudgetCompactedSummaryLines(arena, &lines, history, keep);
try lines.append(arena, "Preserved key evidence from trimmed turns:");
var added: usize = 0;
for (history, 0..) |turn, idx| {
if (keep[idx]) continue;
added += try appendBudgetEvidenceForTurn(arena, &lines, turn, 6 - @min(added, 6));
if (added >= 6) break;
}
if (added == 0) try lines.append(arena, "- No tool evidence was present in trimmed turns.");
return compressSummaryLines(alloc, lines.items);
}
fn appendBudgetCompactedSummaryLines(arena: Allocator, lines: *std.ArrayList([]const u8), history: []const HistoryTurn, keep: []const bool) !void {
for (history, 0..) |turn, idx| {
if (keep[idx]) continue;
const entry = switch (turn) {
.compacted_summary => |value| value,
else => continue,
};
try lines.append(arena, "Preserved compacted summary from trimmed history:");
var added: usize = 0;
var summary_lines = std.mem.splitScalar(u8, entry.summary, '\n');
while (summary_lines.next()) |line| {
const summary = compactLineText(arena, line, compact_summary_max_line_chars - 4) catch continue;
if (summary.len == 0) continue;
try lines.append(arena, try std.fmt.allocPrint(arena, "- {s}", .{summary}));
added += 1;
if (added >= 6) break;
}
if (added == 0) try lines.append(arena, "- Compacted summary was present but empty.");
return;
}
}
fn appendBudgetEvidenceForTurn(arena: Allocator, lines: *std.ArrayList([]const u8), turn: HistoryTurn, remaining: usize) !usize {
if (remaining == 0) return 0;
const execution = switch (turn) {
.assistant => |entry| entry.execution,
.background_command => |entry| entry.execution,
.interrupted => |entry| entry.execution,
else => return 0,
};
var added: usize = 0;
for (execution.tool_steps) |step| {
for (step.tool_results) |result| {
try lines.append(arena, try formatToolResultEvidenceLine(arena, result));
added += 1;
if (added >= remaining) return added;
}
}
for (execution.files) |file| {
const stale = if (file.stale) ", stale" else "";
try lines.append(arena, try std.fmt.allocPrint(arena, "- file {s}: {s}{s}", .{ @tagName(file.action), file.path, stale }));
added += 1;
if (added >= remaining) return added;
}
return added;
}
fn formatToolResultEvidenceLine(arena: Allocator, result: core_types.PersistedToolResult) ![]const u8 {
if (result.output_handle) |handle| {
if (result.preview) |preview| {
const compact_preview = try compactLineText(arena, preview, 96);
return std.fmt.allocPrint(arena, "- {s} {s} ({d} stored bytes, handle={s}, preview={s})", .{ result.tool_name, @tagName(result.status), result.stored_output_bytes, handle, compact_preview });
}
return std.fmt.allocPrint(arena, "- {s} {s} ({d} stored bytes, handle={s})", .{ result.tool_name, @tagName(result.status), result.stored_output_bytes, handle });
}
return std.fmt.allocPrint(arena, "- {s} {s} ({d} stored bytes)", .{ result.tool_name, @tagName(result.status), result.stored_output_bytes });
}
fn estimateHistoryTurnTokens(turn: HistoryTurn) usize {
return switch (turn) {
.compacted_summary => |entry| estimateTextTokens(entry.summary),
.assistant => |entry| estimateTextTokens(entry.user.text) + estimateTextTokens(entry.assistant) + estimateExecutionTokens(entry.execution),
.background_command => |entry| estimateTextTokens(entry.user.text) +
(if (entry.assistant) |assistant| estimateTextTokens(assistant) else 0) +
estimateExecutionTokens(entry.execution) +
estimateTextTokens(entry.log_path) +
(if (entry.url) |url| estimateTextTokens(url) else 0),
.interrupted => |entry| estimateTextTokens(entry.user.text) +
(if (entry.assistant) |assistant| estimateTextTokens(assistant) else 0) +
estimateExecutionTokens(entry.execution),
};
}
fn estimateExecutionTokens(execution: core_types.ExecutionMemory) usize {
var total: usize = 0;
for (execution.tool_steps) |step| {
if (step.assistant) |assistant| total += estimateTextTokens(assistant);
for (step.tool_calls) |call| {
total += estimateTextTokens(call.name) + estimateTextTokens(call.arguments_json);
}
for (step.tool_results) |result| {
if (result.preview) |preview| {
total += estimateTextTokens(preview);
} else {
total += estimateTextTokens(result.output);
}
if (result.output_handle) |handle| total += estimateTextTokens(handle);
for (result.permission_feedback) |feedback| {
total += estimateTextTokens(feedback);
}
}
}
for (execution.files) |file| {
total += estimateTextTokens(file.path) + estimateTextTokens(file.tool_name);
}
return total;
}
fn estimateTextTokens(text: []const u8) usize {
var count: usize = 0;
var span_len: usize = 0;
for (text) |byte| {
if (std.ascii.isWhitespace(byte)) {
if (span_len > 0) {
count += (span_len + 3) / 4;
span_len = 0;
}
} else {
span_len += 1;
}
}
if (span_len > 0) count += (span_len + 3) / 4;
return count;
}
fn appendBackgroundSummaryLines(arena: Allocator, lines: *std.ArrayList([]const u8), removed: []const HistoryTurn) !void {
var added: usize = 0;
var saw_header = false;
for (removed) |turn| {
const entry = switch (turn) {
.background_command => |value| value,
else => continue,
};
if (!saw_header) {
try lines.append(arena, "- Background activity:");
saw_header = true;
}
const line = if (entry.url) |url|
try std.fmt.allocPrint(arena, " - log={s}, url={s}", .{ entry.log_path, url })
else if (entry.expect_url)
try std.fmt.allocPrint(arena, " - log={s}, local server started (URL pending)", .{entry.log_path})
else
try std.fmt.allocPrint(arena, " - log={s}", .{entry.log_path});
try lines.append(arena, line);
added += 1;
if (added >= 3) break;
}
}
fn appendInterruptedSummaryLines(arena: Allocator, lines: *std.ArrayList([]const u8), removed: []const HistoryTurn) !void {
var added: usize = 0;
var saw_header = false;
for (removed) |turn| {
const entry = switch (turn) {
.interrupted => |value| value,
else => continue,
};
if (!saw_header) {
try lines.append(arena, "- Incomplete turns:");
saw_header = true;
}
const line = try std.fmt.allocPrint(
arena,
" - {s}",
.{interruptedTurnNotice(entry).body},
);
try lines.append(arena, line);
added += 1;
if (added >= 3) break;
}
}
fn compressSummaryLines(alloc: Allocator, lines: []const []const u8) ![]u8 {
var out: std.Io.Writer.Allocating = .init(alloc);
defer out.deinit();
var seen: std.ArrayList([]const u8) = .empty;
defer seen.deinit(alloc);
var line_count: usize = 0;
var char_count: usize = 0;
var omitted: usize = 0;
for (lines) |line| {
const normalized = std.mem.trim(u8, line, " \t\r\n");
if (normalized.len == 0) continue;
if (containsLine(seen.items, normalized)) {
omitted += 1;
continue;
}
try seen.append(alloc, normalized);
const candidate_chars = normalized.len + @intFromBool(line_count > 0);
if (line_count >= compact_summary_max_lines or char_count + candidate_chars > compact_summary_max_chars) {
omitted += 1;
continue;
}
if (line_count > 0) try out.writer.writeByte('\n');
try out.writer.writeAll(normalized);
line_count += 1;
char_count += candidate_chars;
}
if (omitted > 0 and line_count < compact_summary_max_lines) {
const notice = try std.fmt.allocPrint(alloc, "- ... {d} additional line(s) omitted.", .{omitted});
defer alloc.free(notice);
const candidate_chars = notice.len + @intFromBool(line_count > 0);
if (char_count + candidate_chars <= compact_summary_max_chars) {
if (line_count > 0) try out.writer.writeByte('\n');
try out.writer.writeAll(notice);
}
}
return try out.toOwnedSlice();
}
fn containsLine(lines: []const []const u8, candidate: []const u8) bool {
for (lines) |line| {
if (std.mem.eql(u8, line, candidate)) return true;
}
return false;
}
fn preservedRecentTurnCount(max_history_turns: usize) usize {
if (max_history_turns <= 2) return 1;
return @min(max_history_turns - 1, 4);
}
fn compactLineText(arena: Allocator, text: []const u8, max_bytes: usize) ![]const u8 {
var out: std.Io.Writer.Allocating = .init(arena);
defer out.deinit();
var wrote_space = false;
var i: usize = 0;
while (i < text.len) {
const byte = text[i];
const is_space = byte == ' ' or byte == '\n' or byte == '\r' or byte == '\t';
if (is_space) {
if (out.written().len > 0) wrote_space = true;
i += 1;
continue;
}
if (wrote_space and out.written().len < max_bytes) {
try out.writer.writeByte(' ');
wrote_space = false;
}
if (out.written().len >= max_bytes) break;
const next_i = text_utils.utf8ForwardBoundary(text, i + 1);
const codepoint_len = next_i - i;
if (codepoint_len > max_bytes - out.written().len) break;
try out.writer.writeAll(text[i..next_i]);
i = next_i;
}
const written = std.mem.trim(u8, out.written(), " \t\r\n");
return if (written.len == 0) arena.dupe(u8, "") else arena.dupe(u8, written);
}
const ScriptCounts = struct {
latin: usize = 0,
cyrillic: usize = 0,
arabic: usize = 0,
hebrew: usize = 0,
devanagari: usize = 0,
thai: usize = 0,
greek: usize = 0,
hangul: usize = 0,
hiragana: usize = 0,
katakana: usize = 0,
han: usize = 0,
};
fn classifyCodepoint(counts: *ScriptCounts, codepoint: u21) void {
if (isLatinCodepoint(codepoint)) {
counts.latin += 1;
return;
}
if (isCyrillicCodepoint(codepoint)) {
counts.cyrillic += 1;
return;
}
if (isArabicCodepoint(codepoint)) {
counts.arabic += 1;
return;
}
if (isHebrewCodepoint(codepoint)) {
counts.hebrew += 1;
return;
}
if (isDevanagariCodepoint(codepoint)) {
counts.devanagari += 1;
return;
}
if (isThaiCodepoint(codepoint)) {
counts.thai += 1;
return;
}
if (isGreekCodepoint(codepoint)) {
counts.greek += 1;
return;
}
if (isHangulCodepoint(codepoint)) {
counts.hangul += 1;
return;
}
if (codepoint >= 0x3040 and codepoint <= 0x309F) {
counts.hiragana += 1;
return;
}
if ((codepoint >= 0x30A0 and codepoint <= 0x30FF) or (codepoint >= 0x31F0 and codepoint <= 0x31FF) or (codepoint >= 0xFF66 and codepoint <= 0xFF9F)) {
counts.katakana += 1;
return;
}
if (isHanCodepoint(codepoint)) {
counts.han += 1;
}
}
fn isLatinCodepoint(codepoint: u21) bool {
return (codepoint >= 'A' and codepoint <= 'Z') or
(codepoint >= 'a' and codepoint <= 'z') or
(codepoint >= 0x00C0 and codepoint <= 0x024F) or
(codepoint >= 0x1E00 and codepoint <= 0x1EFF);
}
fn isCyrillicCodepoint(codepoint: u21) bool {
return (codepoint >= 0x0400 and codepoint <= 0x052F) or
(codepoint >= 0x2DE0 and codepoint <= 0x2DFF) or
(codepoint >= 0xA640 and codepoint <= 0xA69F);
}
fn isArabicCodepoint(codepoint: u21) bool {
return (codepoint >= 0x0600 and codepoint <= 0x06FF) or
(codepoint >= 0x0750 and codepoint <= 0x077F) or
(codepoint >= 0x08A0 and codepoint <= 0x08FF) or
(codepoint >= 0xFB50 and codepoint <= 0xFDFF) or
(codepoint >= 0xFE70 and codepoint <= 0xFEFF);
}
fn isHebrewCodepoint(codepoint: u21) bool {
return codepoint >= 0x0590 and codepoint <= 0x05FF;
}
fn isDevanagariCodepoint(codepoint: u21) bool {
return (codepoint >= 0x0900 and codepoint <= 0x097F) or
(codepoint >= 0xA8E0 and codepoint <= 0xA8FF);
}
fn isThaiCodepoint(codepoint: u21) bool {
return codepoint >= 0x0E00 and codepoint <= 0x0E7F;
}
fn isGreekCodepoint(codepoint: u21) bool {
return (codepoint >= 0x0370 and codepoint <= 0x03FF) or
(codepoint >= 0x1F00 and codepoint <= 0x1FFF);
}
fn isHangulCodepoint(codepoint: u21) bool {
return (codepoint >= 0x1100 and codepoint <= 0x11FF) or
(codepoint >= 0x3130 and codepoint <= 0x318F) or
(codepoint >= 0xA960 and codepoint <= 0xA97F) or
(codepoint >= 0xAC00 and codepoint <= 0xD7AF) or
(codepoint >= 0xD7B0 and codepoint <= 0xD7FF);
}
fn isHanCodepoint(codepoint: u21) bool {
return (codepoint >= 0x3400 and codepoint <= 0x4DBF) or
(codepoint >= 0x4E00 and codepoint <= 0x9FFF) or
(codepoint >= 0xF900 and codepoint <= 0xFAFF) or
(codepoint >= 0x20000 and codepoint <= 0x2A6DF) or
(codepoint >= 0x2A700 and codepoint <= 0x2B73F) or
(codepoint >= 0x2B740 and codepoint <= 0x2B81F) or
(codepoint >= 0x2B820 and codepoint <= 0x2CEAF);
}
test "resume-history-to-request conversion preserves user assistant ordering" {
const alloc = std.testing.allocator;
var history = try alloc.alloc(HistoryTurn, 2);
history[0] = try makeAssistantTurn(alloc, "first", "one");
history[1] = try makeAssistantTurn(alloc, "second", "two");
defer freeHistoryTurnSlice(alloc, history);
var messages: std.ArrayList(message.Message) = .empty;
defer {
for (messages.items) |*msg| msg.deinit(alloc);
messages.deinit(alloc);
}
try appendHistoryMessages(alloc, &messages, history);
try std.testing.expectEqual(@as(usize, 4), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqualStrings("first", messages.items[0].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expectEqualStrings("one", messages.items[1].content.?.asText());
try std.testing.expectEqual(.user, messages.items[2].role);
try std.testing.expectEqualStrings("second", messages.items[2].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[3].role);
try std.testing.expectEqualStrings("two", messages.items[3].content.?.asText());
}
test "appendHistoryMessages frees owned projection text when append fails" {
const compact = [_]HistoryTurn{.{ .compacted_summary = .{ .summary = @constCast("summary"), .removed_turn_count = 1, .compaction_count = 1 } }};
var compact_failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
var compact_messages: std.ArrayList(message.Message) = .empty;
try std.testing.expectError(error.OutOfMemory, appendHistoryMessages(compact_failing.allocator(), &compact_messages, &compact));
var bg_failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
const bg_alloc = bg_failing.allocator();
var bg_messages: std.ArrayList(message.Message) = .empty;
defer bg_messages.deinit(bg_alloc);
try bg_messages.ensureTotalCapacity(bg_alloc, 1);
bg_failing.fail_index = bg_failing.alloc_index + 1;
bg_failing.resize_fail_index = bg_failing.resize_index;
const bg = [_]HistoryTurn{.{ .background_command = .{ .user = .{ .text = @constCast("run") }, .log_path = @constCast("/tmp/log"), .expect_url = true } }};
try std.testing.expectError(error.OutOfMemory, appendHistoryMessages(bg_alloc, &bg_messages, &bg));
for (bg_messages.items) |*msg| msg.deinit(bg_alloc);
}
test "dupeImageAttachment frees path when media_type allocation fails" {
var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
try std.testing.expectError(error.OutOfMemory, dupeImageAttachment(failing.allocator(), .{
.path = @constCast("/tmp/a.png"),
.media_type = @constCast("image/png"),
}));
}
test "dupeImageAttachment preserves stable image id" {
const alloc = std.testing.allocator;
const copy = try dupeImageAttachment(alloc, .{
.id = 41,
.path = @constCast("/tmp/a.png"),
.media_type = @constCast("image/png"),
});
defer {
alloc.free(copy.path);
alloc.free(copy.media_type);
}
try std.testing.expectEqual(@as(usize, 41), copy.id);
}
test "conversation-language inference matches script signals" {
try std.testing.expectEqual(ConversationLanguage.literal("und-Latn"), inferConversationLanguage("puoi aprire la landing page", ConversationLanguage.default()));
try std.testing.expectEqual(ConversationLanguage.literal("ja"), inferConversationLanguage("ランディングページを開いて", ConversationLanguage.default()));
try std.testing.expectEqual(ConversationLanguage.literal("und-Arab"), inferConversationLanguage("افتح الصفحة الرئيسية", ConversationLanguage.default()));
try std.testing.expectEqual(ConversationLanguage.literal("und-Latn"), inferConversationLanguage("12345 !!!", ConversationLanguage.literal("und-Latn")));
}
test "resume projection emits compacted summary before background command context" {
const alloc = std.testing.allocator;
const history = [_]HistoryTurn{
.{ .compacted_summary = .{ .summary = @constCast("summary"), .removed_turn_count = 2, .compaction_count = 1 } },
.{ .background_command = .{ .user = .{ .text = @constCast("run dev") }, .log_path = @constCast("/tmp/server.log"), .expect_url = true, .url = @constCast("http://localhost:3000") } },
};
var messages: std.ArrayList(message.Message) = .empty;
defer {
for (messages.items) |*msg| msg.deinit(alloc);
messages.deinit(alloc);
}
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 3), messages.items.len);
try std.testing.expectEqual(.system, messages.items[0].role);
try std.testing.expectEqualStrings(
"This session is being continued from earlier compacted context. The summary below covers the earlier portion of the conversation.\n\n" ++
"summary\n\n" ++
"Recent conversation turns are preserved verbatim.\n" ++
"Continue the conversation from where it left off without asking the user to repeat context. Resume directly.",
messages.items[0].content.?.asText(),
);
try std.testing.expectEqual(.user, messages.items[1].role);
try std.testing.expectEqualStrings("run dev", messages.items[1].content.?.asText());
try std.testing.expectEqual(.user, messages.items[2].role);
try std.testing.expectEqualStrings(
"Session event: a previous user request launched a background server. Log: /tmp/server.log. URL observed at launch: http://localhost:3000. Re-check runtime context for current liveness before reusing it.",
messages.items[2].content.?.asText(),
);
}
test "history projection keeps system role only for leading summaries" {
const gateway_json = @import("../gateway/gateway_json.zig");
const alloc = std.testing.allocator;
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
const history = [_]HistoryTurn{
.{ .compacted_summary = .{
.summary = @constCast("leading summary"),
.removed_turn_count = 2,
.compaction_count = 1,
} },
.{ .assistant = .{
.user = .{ .text = @constCast("old request " ++ ("u" ** 800)) },
.assistant = @constCast("old answer " ++ ("a" ** 800)),
} },
.{ .compacted_summary = .{
.summary = @constCast("nonleading summary marker"),
.removed_turn_count = 1,
.compaction_count = 2,
} },
.{ .interrupted = .{
.user = .{ .text = @constCast("interrupt me") },
.assistant = @constCast("partial answer"),
} },
};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(arena);
try appendHistoryChatMessages(arena, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 7), messages.items.len);
try std.testing.expectEqual(messages.items.len, chat_messages.items.len);
for (messages.items, chat_messages.items) |projected, chat| {
try std.testing.expectEqualStrings(@tagName(projected.role), @tagName(chat.role));
try std.testing.expectEqualStrings(projected.content.?.asText(), chat.content.?);
}
try std.testing.expectEqual(.system, messages.items[0].role);
try std.testing.expectEqual(.user, messages.items[3].role);
try std.testing.expect(std.mem.find(u8, messages.items[3].content.?.asText(), "nonleading summary marker") != null);
try std.testing.expectEqual(.user, messages.items[6].role);
try std.testing.expect(std.mem.find(u8, messages.items[6].content.?.asText(), "") != null);
try gateway_json.validateToolMessageHistory(alloc, chat_messages.items);
var budgeted_messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &budgeted_messages);
try appendHistoryMessagesBudgeted(alloc, &budgeted_messages, &history, .{ .max_tokens = 128 });
var budgeted_chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer budgeted_chat_messages.deinit(arena);
try appendHistoryChatMessagesBudgeted(arena, &budgeted_chat_messages, &history, .{ .max_tokens = 128 });
try std.testing.expectEqual(budgeted_messages.items.len, budgeted_chat_messages.items.len);
var saw_nonleading_summary = false;
var saw_interruption_marker = false;
var saw_non_system = false;
for (budgeted_messages.items, budgeted_chat_messages.items) |projected, chat| {
try std.testing.expectEqualStrings(@tagName(projected.role), @tagName(chat.role));
try std.testing.expectEqualStrings(projected.content.?.asText(), chat.content.?);
if (projected.role == .system) {
try std.testing.expect(!saw_non_system);
} else {
saw_non_system = true;
}
const text = projected.content.?.asText();
if (std.mem.find(u8, text, "nonleading summary marker") != null and
std.mem.startsWith(u8, text, compact_continuation_preamble))
{
try std.testing.expectEqual(.user, projected.role);
try std.testing.expect(!saw_nonleading_summary);
saw_nonleading_summary = true;
}
if (std.mem.find(u8, text, "") != null) {
try std.testing.expectEqual(.user, projected.role);
try std.testing.expect(!saw_interruption_marker);
saw_interruption_marker = true;
}
}
try std.testing.expectEqual(.system, budgeted_messages.items[0].role);
try std.testing.expect(saw_nonleading_summary);
try std.testing.expect(saw_interruption_marker);
try gateway_json.validateToolMessageHistory(alloc, budgeted_chat_messages.items);
}
test "resume projection replays assistant tool execution memory before final answer" {
const alloc = std.testing.allocator;
var calls = [_]ToolCall{.{
.id = "call_read",
.name = "read_file",
.arguments_json = "{\"path\":\"src/main.zig\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_read"),
.tool_name = @constCast("read_file"),
.status = .success,
.output = @constCast("file contents"),
.output_bytes = 13,
.stored_output_bytes = 13,
}};
var steps = [_]ToolExecutionStep{.{
.assistant = @constCast("I'll read it."),
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
var files = [_]FileEvidence{.{
.path = @constCast("src/main.zig"),
.tool_call_id = @constCast("call_read"),
.tool_name = @constCast("read_file"),
.action = .read,
.status = .success,
.model_view_covers_full_file = true,
}};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("inspect main") },
.assistant = @constCast("main wires the app"),
.execution = .{ .tool_steps = steps[0..], .files = files[0..] },
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 5), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expectEqualStrings("call_read", messages.items[1].tool_calls[0].id);
try std.testing.expectEqual(.tool, messages.items[2].role);
try std.testing.expectEqualStrings("file contents", messages.items[2].content.?.asText());
try std.testing.expectEqual(.user, messages.items[3].role);
try std.testing.expect(std.mem.find(u8, messages.items[3].content.?.asText(), "model_view=full") != null);
try std.testing.expectEqual(.assistant, messages.items[4].role);
try std.testing.expectEqualStrings("main wires the app", messages.items[4].content.?.asText());
}
test "resume projections omit empty terminal assistant after completed execution" {
const alloc = std.testing.allocator;
var calls = [_]ToolCall{.{
.id = "call_read",
.name = "read_file",
.arguments_json = "{\"path\":\"src/main.zig\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_read"),
.tool_name = @constCast("read_file"),
.status = .success,
.output = @constCast("file contents"),
.output_bytes = 13,
.stored_output_bytes = 13,
}};
var steps = [_]ToolExecutionStep{.{
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("inspect main") },
.assistant = @constCast(""),
.execution = .{ .tool_steps = steps[0..] },
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 3), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expectEqualStrings("call_read", messages.items[1].tool_calls[0].id);
try std.testing.expectEqual(.tool, messages.items[2].role);
try std.testing.expectEqualStrings("file contents", messages.items[2].content.?.asText());
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(alloc);
try appendHistoryChatMessages(alloc, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 3), chat_messages.items.len);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[0].role);
try std.testing.expectEqual(core_types.ChatRole.assistant, chat_messages.items[1].role);
try std.testing.expectEqualStrings("call_read", chat_messages.items[1].tool_calls[0].id);
try std.testing.expectEqual(core_types.ChatRole.tool, chat_messages.items[2].role);
try std.testing.expectEqualStrings("file contents", chat_messages.items[2].content.?);
}
test "resume projections place permission feedback after its tool result" {
const alloc = std.testing.allocator;
var feedback = [_][]u8{@constCast("read it after writing")};
var calls = [_]ToolCall{.{
.id = "call_feedback",
.name = "write_file",
.arguments_json = "{\"path\":\"note.txt\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_feedback"),
.tool_name = @constCast("write_file"),
.status = .success,
.output = @constCast("wrote note.txt"),
.output_bytes = 15,
.stored_output_bytes = 15,
.permission_feedback = feedback[0..],
}};
var steps = [_]ToolExecutionStep{.{
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("write a note") },
.assistant = @constCast("done"),
.execution = .{ .tool_steps = steps[0..] },
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 5), messages.items.len);
try std.testing.expectEqual(.tool, messages.items[2].role);
try std.testing.expectEqual(.user, messages.items[3].role);
try std.testing.expectEqualStrings(
"read it after writing",
messages.items[3].content.?.asText(),
);
try std.testing.expectEqual(.assistant, messages.items[4].role);
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(alloc);
try appendHistoryChatMessages(alloc, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 5), chat_messages.items.len);
try std.testing.expectEqual(core_types.ChatRole.tool, chat_messages.items[2].role);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[3].role);
try std.testing.expectEqualStrings("read it after writing", chat_messages.items[3].content.?);
try std.testing.expectEqual(core_types.ChatRole.assistant, chat_messages.items[4].role);
}
test "resume projections group two tool results before their feedback" {
const gateway_json = @import("../gateway/gateway_json.zig");
const alloc = std.testing.allocator;
var first_feedback = [_][]u8{@constCast("first command feedback marker")};
var second_feedback = [_][]u8{@constCast("second command feedback marker")};
var calls = [_]ToolCall{
.{ .id = "call_first", .name = "run_command", .arguments_json = "{\"command\":\"printf first\"}" },
.{ .id = "call_second", .name = "run_command", .arguments_json = "{\"command\":\"printf second\"}" },
};
var results = [_]PersistedToolResult{
.{
.tool_call_id = @constCast("call_first"),
.tool_name = @constCast("run_command"),
.status = .success,
.output = @constCast("first command completed"),
.output_bytes = 23,
.stored_output_bytes = 23,
.permission_feedback = first_feedback[0..],
},
.{
.tool_call_id = @constCast("call_second"),
.tool_name = @constCast("run_command"),
.status = .success,
.output = @constCast("second command completed"),
.output_bytes = 24,
.stored_output_bytes = 24,
.permission_feedback = second_feedback[0..],
},
};
var steps = [_]ToolExecutionStep{.{
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("run both commands") },
.assistant = @constCast("done"),
.execution = .{ .tool_steps = steps[0..] },
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 7), messages.items.len);
try std.testing.expectEqual(.tool, messages.items[2].role);
try std.testing.expectEqual(.tool, messages.items[3].role);
try std.testing.expectEqual(.user, messages.items[4].role);
try std.testing.expectEqual(.user, messages.items[5].role);
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(alloc);
try appendHistoryChatMessages(alloc, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 7), chat_messages.items.len);
try std.testing.expectEqual(core_types.ChatRole.tool, chat_messages.items[2].role);
try std.testing.expectEqual(core_types.ChatRole.tool, chat_messages.items[3].role);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[4].role);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[5].role);
try gateway_json.validateToolMessageHistory(alloc, chat_messages.items);
}
test "execution replay context and token estimate include permission feedback" {
const alloc = std.testing.allocator;
var feedback = [_][]u8{@constCast("read it after writing")};
var with_feedback_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_feedback"),
.tool_name = @constCast("write_file"),
.status = .success,
.output = @constCast("wrote note.txt"),
.output_bytes = 15,
.stored_output_bytes = 15,
.permission_feedback = feedback[0..],
}};
var without_feedback_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_feedback"),
.tool_name = @constCast("write_file"),
.status = .success,
.output = @constCast("wrote note.txt"),
.output_bytes = 15,
.stored_output_bytes = 15,
}};
var with_feedback_steps = [_]ToolExecutionStep{.{ .tool_results = with_feedback_results[0..] }};
var without_feedback_steps = [_]ToolExecutionStep{.{ .tool_results = without_feedback_results[0..] }};
const with_feedback = ExecutionMemory{ .tool_steps = with_feedback_steps[0..] };
const without_feedback = ExecutionMemory{ .tool_steps = without_feedback_steps[0..] };
const context = (try formatExecutionReplayContext(alloc, with_feedback)).?;
defer alloc.free(context);
try std.testing.expect(std.mem.find(u8, context, "wrote note.txt") != null);
try std.testing.expect(std.mem.find(u8, context, "read it after writing") != null);
try std.testing.expect(estimateExecutionTokens(with_feedback) > estimateExecutionTokens(without_feedback));
}
test "specialized history replays execution before visible terminal text and context" {
const alloc = std.testing.allocator;
var calls = [_]ToolCall{.{
.id = "call_read",
.name = "read_file",
.arguments_json = "{\"path\":\"src/main.zig\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_read"),
.tool_name = @constCast("read_file"),
.status = .success,
.output = @constCast("file contents"),
.output_bytes = 13,
.stored_output_bytes = 13,
}};
var steps = [_]ToolExecutionStep{.{
.assistant = @constCast("I'll inspect it."),
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
const execution = ExecutionMemory{ .tool_steps = steps[0..] };
const history = [_]HistoryTurn{
.{ .background_command = .{
.user = .{ .text = @constCast("run dev") },
.assistant = @constCast("The server is starting."),
.execution = execution,
.log_path = @constCast("/tmp/server.log"),
.expect_url = true,
} },
.{ .interrupted = .{
.user = .{ .text = @constCast("inspect") },
.assistant = @constCast("I inspected the entry point."),
.execution = execution,
} },
};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 10), messages.items.len);
try std.testing.expectEqualStrings("run dev", messages.items[0].content.?.asText());
try std.testing.expectEqualStrings("call_read", messages.items[1].tool_calls[0].id);
try std.testing.expectEqualStrings("file contents", messages.items[2].content.?.asText());
try std.testing.expectEqualStrings("The server is starting.", messages.items[3].content.?.asText());
try std.testing.expectEqual(.user, messages.items[4].role);
try std.testing.expectEqualStrings("inspect", messages.items[5].content.?.asText());
try std.testing.expectEqualStrings("call_read", messages.items[6].tool_calls[0].id);
try std.testing.expectEqualStrings("file contents", messages.items[7].content.?.asText());
try std.testing.expect(std.mem.startsWith(
u8,
messages.items[8].content.?.asText(),
"I inspected the entry point.",
));
try std.testing.expectEqual(.user, messages.items[9].role);
}
test "budgeted resume projection preserves latest turn and summarizes trimmed handle evidence" {
const alloc = std.testing.allocator;
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var large_calls = [_]ToolCall{.{
.id = "call_large",
.name = "run_command",
.arguments_json = "{\"command\":\"make lots of evidence\"}",
}};
var large_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_large"),
.tool_name = @constCast("run_command"),
.status = .success,
.output = @constCast("preview evidence"),
.output_bytes = 80_000,
.stored_output_bytes = 80_000,
.truncated = true,
.provider_native = false,
.created_at_ms = 1,
.output_handle = @constCast("result-call_large-abc.txt"),
.preview = @constCast("preview evidence"),
}};
var large_steps = [_]ToolExecutionStep{.{
.tool_calls = large_calls[0..],
.tool_results = large_results[0..],
}};
const history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("old large request") },
.assistant = @constCast("old large answer"),
.execution = .{ .tool_steps = large_steps[0..] },
} },
.{ .assistant = .{
.user = .{ .text = @constCast("latest request that must survive") },
.assistant = @constCast("latest answer"),
} },
};
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer messages.deinit(arena);
try appendHistoryChatMessagesBudgeted(arena, &messages, &history, .{ .max_tokens = 12 });
try std.testing.expect(messages.items.len >= 3);
try std.testing.expectEqual(core_types.ChatRole.system, messages.items[0].role);
try std.testing.expect(std.mem.find(u8, messages.items[0].content.?, "result-call_large-abc.txt") != null);
try std.testing.expect(std.mem.find(u8, messages.items[0].content.?, "preview evidence") != null);
try std.testing.expectEqualStrings("latest request that must survive", messages.items[messages.items.len - 2].content.?);
try std.testing.expectEqualStrings("latest answer", messages.items[messages.items.len - 1].content.?);
}
test "budgeted resume projection preserves compacted summary when older handle evidence is trimmed" {
const alloc = std.testing.allocator;
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var large_calls = [_]ToolCall{.{
.id = "call_large",
.name = "run_command",
.arguments_json = "{\"command\":\"make lots of evidence\"}",
}};
var large_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_large"),
.tool_name = @constCast("run_command"),
.status = .success,
.output = @constCast("summary preview evidence"),
.output_bytes = 80_000,
.stored_output_bytes = 80_000,
.truncated = true,
.provider_native = false,
.created_at_ms = 1,
.output_handle = @constCast("result-call_large-summary.txt"),
.preview = @constCast("summary preview evidence"),
}};
var large_steps = [_]ToolExecutionStep{.{
.tool_calls = large_calls[0..],
.tool_results = large_results[0..],
}};
const history = [_]HistoryTurn{
.{ .compacted_summary = .{
.summary = @constCast("Conversation summary:\n- Prior compacted decision needle " ++ ("x" ** 240)),
.removed_turn_count = 6,
.compaction_count = 2,
} },
.{ .assistant = .{
.user = .{ .text = @constCast("old large request") },
.assistant = @constCast("old large answer"),
.execution = .{ .tool_steps = large_steps[0..] },
} },
.{ .assistant = .{
.user = .{ .text = @constCast("latest request that must survive") },
.assistant = @constCast("latest answer"),
} },
};
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer messages.deinit(arena);
try appendHistoryChatMessagesBudgeted(arena, &messages, &history, .{ .max_tokens = 12 });
try std.testing.expect(messages.items.len >= 3);
try std.testing.expectEqual(core_types.ChatRole.system, messages.items[0].role);
try std.testing.expect(std.mem.find(u8, messages.items[0].content.?, "Prior compacted decision needle") != null);
try std.testing.expect(std.mem.find(u8, messages.items[0].content.?, "result-call_large-summary.txt") != null);
try std.testing.expect(std.mem.find(u8, messages.items[0].content.?, "summary preview evidence") != null);
try std.testing.expectEqualStrings("latest request that must survive", messages.items[messages.items.len - 2].content.?);
try std.testing.expectEqualStrings("latest answer", messages.items[messages.items.len - 1].content.?);
}
test "budgeted Message and Chat projections retain latest turn and identical trimmed evidence" {
const alloc = std.testing.allocator;
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var trimmed_calls = [_]ToolCall{.{
.id = "call_trimmed",
.name = "run_command",
.arguments_json = "{\"command\":\"produce extensive evidence\"}",
}};
var trimmed_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_trimmed"),
.tool_name = @constCast("run_command"),
.status = .failure,
.output = @constCast("trimmed failure output"),
.output_bytes = 22,
.stored_output_bytes = 22,
.output_handle = @constCast("result-call_trimmed.txt"),
.preview = @constCast("trimmed failure preview"),
}};
var trimmed_files = [_]FileEvidence{.{
.path = @constCast("src/trimmed.zig"),
.tool_call_id = @constCast("call_trimmed"),
.tool_name = @constCast("run_command"),
.action = .write,
.status = .failure,
.stale = true,
}};
var trimmed_steps = [_]ToolExecutionStep{.{
.assistant = @constCast("collecting extensive trimmed evidence"),
.tool_calls = trimmed_calls[0..],
.tool_results = trimmed_results[0..],
}};
var retained_calls = [_]ToolCall{.{
.id = "call_retained",
.name = "read_file",
.arguments_json = "{\"path\":\"src/retained.zig\"}",
}};
var retained_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_retained"),
.tool_name = @constCast("read_file"),
.status = .failure,
.output = @constCast("retained failure"),
.output_bytes = 16,
.stored_output_bytes = 16,
}};
var retained_files = [_]FileEvidence{.{
.path = @constCast("src/retained.zig"),
.tool_call_id = @constCast("call_retained"),
.tool_name = @constCast("read_file"),
.action = .read,
.status = .failure,
.model_view_covers_full_file = true,
}};
var retained_steps = [_]ToolExecutionStep{.{
.assistant = @constCast("reading retained"),
.tool_calls = retained_calls[0..],
.tool_results = retained_results[0..],
}};
var background_images = [_]ImageAttachment{.{
.id = 7,
.path = @constCast("/tmp/background.png"),
.media_type = @constCast("image/png"),
}};
var interrupted_images = [_]ImageAttachment{.{
.id = 8,
.path = @constCast("/tmp/interrupted.jpg"),
.media_type = @constCast("image/jpeg"),
}};
var latest_images = [_]ImageAttachment{.{
.id = 9,
.path = @constCast("/tmp/latest.webp"),
.media_type = @constCast("image/webp"),
}};
const history = [_]HistoryTurn{
.{ .compacted_summary = .{
.summary = @constCast("prior compacted decision needle " ++ ("x" ** 320)),
.removed_turn_count = 5,
.compaction_count = 2,
} },
.{ .assistant = .{
.user = .{ .text = @constCast("old request " ++ ("y" ** 240)) },
.assistant = @constCast("old answer " ++ ("z" ** 240)),
.execution = .{
.tool_steps = trimmed_steps[0..],
.files = trimmed_files[0..],
},
} },
.{ .background_command = .{
.user = .{
.text = @constCast("run retained"),
.images = background_images[0..],
},
.assistant = @constCast("server ready"),
.execution = .{
.tool_steps = retained_steps[0..],
.files = retained_files[0..],
},
.log_path = @constCast("/tmp/retained.log"),
.expect_url = true,
.url = @constCast("http://localhost:3000"),
} },
.{ .interrupted = .{
.user = .{
.text = @constCast("stop retained"),
.images = interrupted_images[0..],
},
.assistant = @constCast("partial retained"),
.tool_call = .{
.id = "call_interrupted",
.name = "browser_navigate",
.arguments_json = "{\"url\":\"http://localhost:3000\"}",
},
} },
.{ .assistant = .{
.user = .{
.text = @constCast("latest request"),
.images = latest_images[0..],
},
.assistant = @constCast("latest answer"),
} },
};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(arena, &messages);
try appendHistoryMessagesBudgeted(
arena,
&messages,
&history,
.{ .max_tokens = 64 },
);
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(arena);
try appendHistoryChatMessagesBudgeted(
arena,
&chat_messages,
&history,
.{ .max_tokens = 64 },
);
try std.testing.expectEqual(messages.items.len, chat_messages.items.len);
var saw_background = false;
var saw_interrupted = false;
var saw_failure_status = false;
var saw_file_evidence = false;
var saw_background_image = false;
var saw_interrupted_image = false;
var saw_latest_image = false;
for (messages.items, chat_messages.items) |projected, chat| {
try std.testing.expectEqualStrings(
@tagName(projected.role),
@tagName(chat.role),
);
try std.testing.expectEqual(projected.content == null, chat.content == null);
if (projected.content) |content| {
try std.testing.expectEqualStrings(content.asText(), chat.content.?);
saw_background = saw_background or
std.mem.find(u8, content.asText(), "/tmp/retained.log") != null;
saw_interrupted = saw_interrupted or
std.mem.find(u8, content.asText(), "") != null;
saw_file_evidence = saw_file_evidence or
std.mem.find(u8, content.asText(), "src/retained.zig") != null;
}
try std.testing.expectEqual(projected.images.len, chat.images.len);
for (projected.images, chat.images) |projected_image, chat_image| {
try std.testing.expectEqual(projected_image.id, chat_image.id);
try std.testing.expectEqualStrings(projected_image.path, chat_image.path);
try std.testing.expectEqualStrings(
projected_image.media_type,
chat_image.media_type,
);
saw_background_image = saw_background_image or
std.mem.eql(u8, projected_image.path, "/tmp/background.png");
saw_interrupted_image = saw_interrupted_image or
std.mem.eql(u8, projected_image.path, "/tmp/interrupted.jpg");
saw_latest_image = saw_latest_image or
std.mem.eql(u8, projected_image.path, "/tmp/latest.webp");
}
try std.testing.expectEqual(projected.tool_calls.len, chat.tool_calls.len);
for (projected.tool_calls, chat.tool_calls) |projected_call, chat_call| {
try std.testing.expectEqualStrings(projected_call.id, chat_call.id);
try std.testing.expectEqualStrings(projected_call.name, chat_call.name);
try std.testing.expectEqualStrings(
projected_call.arguments_json,
chat_call.arguments_json,
);
}
try std.testing.expectEqual(projected.tool_result_status, chat.tool_result_status);
saw_failure_status = saw_failure_status or
projected.tool_result_status == .failure;
}
const trimmed_context = messages.items[0].content.?.asText();
try std.testing.expect(std.mem.find(
u8,
trimmed_context,
"prior compacted decision needle",
) != null);
try std.testing.expect(std.mem.find(
u8,
trimmed_context,
"result-call_trimmed.txt",
) != null);
try std.testing.expect(std.mem.find(
u8,
trimmed_context,
"src/trimmed.zig",
) != null);
try std.testing.expect(saw_background);
try std.testing.expect(saw_interrupted);
try std.testing.expect(saw_failure_status);
try std.testing.expect(saw_file_evidence);
try std.testing.expect(saw_background_image);
try std.testing.expect(saw_interrupted_image);
try std.testing.expect(saw_latest_image);
try std.testing.expectEqualStrings(
"latest request",
messages.items[messages.items.len - 2].content.?.asText(),
);
try std.testing.expectEqualStrings(
"latest answer",
messages.items[messages.items.len - 1].content.?.asText(),
);
}
test "context compaction summary preserves large result handle without dropping canonical execution memory" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 2 };
defer runtime.deinit(alloc);
var calls = [_]ToolCall{.{
.id = "call_large",
.name = "run_command",
.arguments_json = "{\"command\":\"large\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_large"),
.tool_name = @constCast("run_command"),
.status = .success,
.output = @constCast("important preview"),
.output_bytes = 70_000,
.stored_output_bytes = 70_000,
.truncated = true,
.provider_native = false,
.created_at_ms = 1,
.output_handle = @constCast("result-call_large-abc.txt"),
.preview = @constCast("important preview"),
}};
var steps = [_]ToolExecutionStep{.{
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
const old_turn: HistoryTurn = .{ .assistant = .{
.user = .{ .text = @constCast("old") },
.assistant = @constCast("old answer"),
.execution = .{ .tool_steps = steps[0..] },
} };
try runtime.appendHistoryEntry(alloc, old_turn);
try runtime.appendAssistantHistoryTurn(alloc, "middle", "middle answer");
try runtime.appendAssistantHistoryTurn(alloc, "latest", "latest answer");
try std.testing.expectEqual(@as(usize, 3), runtime.historyLen());
try std.testing.expectEqualStrings(
"call_large",
runtime.history.items[0].assistant.execution.tool_steps[0].tool_calls[0].id,
);
try std.testing.expectEqualStrings(
"result-call_large-abc.txt",
runtime.history.items[0].assistant.execution.tool_steps[0].tool_results[0].output_handle.?,
);
const context = try runtime.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, context);
try std.testing.expectEqual(@as(usize, 2), context.len);
try std.testing.expectEqual(HistoryTurn.compacted_summary, std.meta.activeTag(context[0]));
try std.testing.expect(std.mem.find(u8, context[0].compacted_summary.summary, "result-call_large-abc.txt") != null);
try std.testing.expect(std.mem.find(u8, context[0].compacted_summary.summary, "important preview") != null);
}
test "interrupted history projects marker and aborted tool result" {
const alloc = std.testing.allocator;
const replay_sentinel = "PRESENTATION_REPLAY_SENTINEL.bin";
const artifact_sentinel = "PRESENTATION_ARTIFACT_SENTINEL.log";
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("run a command") },
.tool_call = .{
.id = "call-command",
.name = "run_command",
.arguments_json = "{\"command\":\"slow\"}",
},
.cancelled_command = .{
.output_replay = .{ .available = .{
.handle = replay_sentinel,
.framed_bytes = 42,
} },
.command_artifact_handle = artifact_sentinel,
},
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 4), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqualStrings("run a command", messages.items[0].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expect(messages.items[1].content == null);
try std.testing.expectEqual(@as(usize, 1), messages.items[1].tool_calls.len);
try std.testing.expectEqualStrings("call-command", messages.items[1].tool_calls[0].id);
try std.testing.expectEqualStrings("run_command", messages.items[1].tool_calls[0].name);
try std.testing.expectEqual(.tool, messages.items[2].role);
try std.testing.expectEqualStrings(aborted_tool_output, messages.items[2].content.?.asText());
try std.testing.expectEqualStrings("call-command", messages.items[2].tool_call_id.?);
try std.testing.expectEqual(.user, messages.items[3].role);
try std.testing.expect(std.mem.find(u8, messages.items[3].content.?.asText(), "") != null);
try std.testing.expect(std.mem.find(u8, messages.items[3].content.?.asText(), "Do not continue") != null);
for (messages.items) |entry| {
if (entry.content) |content| {
try std.testing.expect(std.mem.find(u8, content.asText(), replay_sentinel) == null);
try std.testing.expect(std.mem.find(u8, content.asText(), artifact_sentinel) == null);
}
}
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(arena);
try appendHistoryChatMessages(arena, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 4), chat_messages.items.len);
try std.testing.expectEqual(core_types.ChatRole.assistant, chat_messages.items[1].role);
try std.testing.expect(chat_messages.items[1].content == null);
try std.testing.expectEqualStrings("run_command", chat_messages.items[1].tool_calls[0].name);
try std.testing.expectEqual(core_types.ChatRole.tool, chat_messages.items[2].role);
try std.testing.expectEqualStrings(aborted_tool_output, chat_messages.items[2].content.?);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[3].role);
try std.testing.expect(std.mem.find(u8, chat_messages.items[3].content.?, "") != null);
for (chat_messages.items) |entry| {
if (entry.content) |content| {
try std.testing.expect(std.mem.find(u8, content, replay_sentinel) == null);
try std.testing.expect(std.mem.find(u8, content, artifact_sentinel) == null);
}
}
}
test "no-output interrupted history projects synthetic assistant before marker" {
const alloc = std.testing.allocator;
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("create test.md with 200 lines") },
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 3), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqualStrings("create test.md with 200 lines", messages.items[0].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expectEqualStrings(interrupted_before_completion_output, messages.items[1].content.?.asText());
try std.testing.expectEqual(.user, messages.items[2].role);
try std.testing.expect(std.mem.find(u8, messages.items[2].content.?.asText(), "") != null);
try std.testing.expect(std.mem.find(u8, messages.items[2].content.?.asText(), "user interrupted") == null);
try std.testing.expect(std.mem.find(u8, messages.items[2].content.?.asText(), "Do not continue this request unless") != null);
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(arena);
try appendHistoryChatMessages(arena, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 3), chat_messages.items.len);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[0].role);
try std.testing.expectEqualStrings("create test.md with 200 lines", chat_messages.items[0].content.?);
try std.testing.expectEqual(core_types.ChatRole.assistant, chat_messages.items[1].role);
try std.testing.expectEqualStrings(interrupted_before_completion_output, chat_messages.items[1].content.?);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[2].role);
try std.testing.expect(std.mem.find(u8, chat_messages.items[2].content.?, "") != null);
try std.testing.expect(std.mem.find(u8, chat_messages.items[2].content.?, "user interrupted") == null);
}
test "partial-text interrupted history projects partial assistant with closure before marker" {
const alloc = std.testing.allocator;
const expected_assistant =
"Once there was a partial answer.\n\n" ++
interrupted_before_completion_output;
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("tell me a story") },
.assistant = @constCast("Once there was a partial answer."),
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 3), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqualStrings("tell me a story", messages.items[0].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expectEqualStrings(expected_assistant, messages.items[1].content.?.asText());
try std.testing.expectEqual(.user, messages.items[2].role);
try std.testing.expect(std.mem.find(u8, messages.items[2].content.?.asText(), "") != null);
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(arena);
try appendHistoryChatMessages(arena, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 3), chat_messages.items.len);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[0].role);
try std.testing.expectEqualStrings("tell me a story", chat_messages.items[0].content.?);
try std.testing.expectEqual(core_types.ChatRole.assistant, chat_messages.items[1].role);
try std.testing.expectEqualStrings(expected_assistant, chat_messages.items[1].content.?);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[2].role);
try std.testing.expect(std.mem.find(u8, chat_messages.items[2].content.?, "") != null);
}
test "completed-tool interrupted history projects summary before marker" {
const alloc = std.testing.allocator;
var completed_tool_names = [_][]u8{ @constCast("list_files"), @constCast("glob_files") };
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("use all ur tools 1 by 1 - I need u to test them") },
.completed_tool_names = completed_tool_names[0..],
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 3), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqualStrings("use all ur tools 1 by 1 - I need u to test them", messages.items[0].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expectEqualStrings("Interrupted by user after completing 2 tool calls: list_files, glob_files.", messages.items[1].content.?.asText());
try std.testing.expectEqual(.user, messages.items[2].role);
try std.testing.expect(std.mem.find(u8, messages.items[2].content.?.asText(), "") != null);
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var chat_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer chat_messages.deinit(arena);
try appendHistoryChatMessages(arena, &chat_messages, &history);
try std.testing.expectEqual(@as(usize, 3), chat_messages.items.len);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[0].role);
try std.testing.expectEqualStrings("use all ur tools 1 by 1 - I need u to test them", chat_messages.items[0].content.?);
try std.testing.expectEqual(core_types.ChatRole.assistant, chat_messages.items[1].role);
try std.testing.expectEqualStrings("Interrupted by user after completing 2 tool calls: list_files, glob_files.", chat_messages.items[1].content.?);
try std.testing.expectEqual(core_types.ChatRole.user, chat_messages.items[2].role);
try std.testing.expect(std.mem.find(u8, chat_messages.items[2].content.?, "") != null);
}
test "two interrupted text turns project in order with partial assistant text" {
const alloc = std.testing.allocator;
const history = [_]HistoryTurn{
.{ .interrupted = .{
.user = .{ .text = @constCast("tell me a story in 200 words") },
.assistant = @constCast("Once there was a quiet signal in the woods."),
} },
.{ .interrupted = .{
.user = .{ .text = @constCast("go on") },
.assistant = @constCast("The signal crossed the valley and found a door."),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("go on") },
.assistant = @constCast("The door opened, and the story ended in daylight."),
} },
};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
try std.testing.expectEqual(@as(usize, 8), messages.items.len);
try std.testing.expectEqual(.user, messages.items[0].role);
try std.testing.expectEqualStrings("tell me a story in 200 words", messages.items[0].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[1].role);
try std.testing.expectEqualStrings(
"Once there was a quiet signal in the woods.\n\n" ++ interrupted_before_completion_output,
messages.items[1].content.?.asText(),
);
try std.testing.expectEqual(.user, messages.items[2].role);
try std.testing.expect(std.mem.find(u8, messages.items[2].content.?.asText(), "") != null);
try std.testing.expectEqual(.user, messages.items[3].role);
try std.testing.expectEqualStrings("go on", messages.items[3].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[4].role);
try std.testing.expectEqualStrings(
"The signal crossed the valley and found a door.\n\n" ++ interrupted_before_completion_output,
messages.items[4].content.?.asText(),
);
try std.testing.expectEqual(.user, messages.items[5].role);
try std.testing.expect(std.mem.find(u8, messages.items[5].content.?.asText(), "") != null);
try std.testing.expectEqual(.user, messages.items[6].role);
try std.testing.expectEqualStrings("go on", messages.items[6].content.?.asText());
try std.testing.expectEqual(.assistant, messages.items[7].role);
try std.testing.expectEqualStrings("The door opened, and the story ended in daylight.", messages.items[7].content.?.asText());
var joined = std.ArrayList(u8).empty;
defer joined.deinit(alloc);
for (messages.items) |msg| {
if (msg.content) |content| try joined.appendSlice(alloc, content.asText());
}
try std.testing.expect(std.mem.find(u8, joined.items, "storygo ongo on") == null);
try std.testing.expect(std.mem.find(u8, joined.items, "tell me a story in 200 wordsgo on") == null);
try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, joined.items, interrupted_before_completion_output));
}
test "SessionRuntime.restore replaces history, updates language, and preserves exact deep copies" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 2 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "old", "old reply");
var restore_history = [_]HistoryTurn{
try makeAssistantTurn(alloc, "one", "reply one"),
try makeAssistantTurn(alloc, "two", "reply two"),
try makeAssistantTurn(alloc, "three", "reply three"),
};
defer {
for (&restore_history) |turn| freeHistoryTurn(alloc, turn);
}
try runtime.restore(alloc, ConversationLanguage.literal("es"), &restore_history);
try std.testing.expectEqual(ConversationLanguage.literal("es"), runtime.languageSnapshot());
try std.testing.expectEqual(@as(usize, 3), runtime.historyLen());
try std.testing.expectEqualStrings("one", runtime.history.items[0].assistant.user.text);
try std.testing.expectEqualStrings("reply one", runtime.history.items[0].assistant.assistant);
try std.testing.expectEqualStrings("three", runtime.history.items[2].assistant.user.text);
try std.testing.expectEqualStrings("reply three", runtime.history.items[2].assistant.assistant);
try std.testing.expect(runtime.history.items[0].assistant.user.text.ptr != restore_history[0].assistant.user.text.ptr);
try std.testing.expect(runtime.history.items[2].assistant.assistant.ptr != restore_history[2].assistant.assistant.ptr);
}
test "SessionRuntime.restore may retain earlier restored turns after later append failure" {
var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 3 });
const alloc = failing.allocator();
var runtime: SessionRuntime = .{ .max_history_turns = 0 };
defer runtime.deinit(alloc);
const restore_history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("first") },
.assistant = @constCast("one"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("second") },
.assistant = @constCast("two"),
} },
};
try std.testing.expectError(error.OutOfMemory, runtime.restore(alloc, ConversationLanguage.literal("fr"), &restore_history));
try std.testing.expectEqual(ConversationLanguage.literal("fr"), runtime.languageSnapshot());
try std.testing.expectEqual(@as(usize, 1), runtime.historyLen());
try std.testing.expectEqualStrings("first", runtime.history.items[0].assistant.user.text);
}
test "SessionRuntime.reset clears history and restores default language" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "hola", "que tal");
runtime.setConversationLanguageFromUserMessage("ランディングページを開いて");
try std.testing.expectEqual(ConversationLanguage.literal("ja"), runtime.languageSnapshot());
runtime.reset(alloc);
try std.testing.expectEqual(@as(usize, 0), runtime.historyLen());
try std.testing.expectEqual(ConversationLanguage.default(), runtime.languageSnapshot());
}
test "SessionRuntime deduplicates context notices until facts change or the session resets" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try std.testing.expect(try runtime.claimContextNotice(alloc, "observed=20 effective=10"));
try std.testing.expect(!try runtime.claimContextNotice(alloc, "observed=20 effective=10"));
try std.testing.expect(try runtime.claimContextNotice(alloc, "observed=21 effective=10"));
try std.testing.expect(try runtime.claimContextNotice(alloc, "observed=21 effective=11"));
runtime.clearContextNotices();
try std.testing.expect(try runtime.claimContextNotice(alloc, "observed=20 effective=10"));
}
test "SessionRuntime.clearHistory empties owned turns and keeps runtime reusable" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "first", "one");
try runtime.appendAssistantHistoryTurn(alloc, "second", "two");
runtime.clearHistory(alloc);
try std.testing.expectEqual(@as(usize, 0), runtime.historyLen());
try runtime.appendAssistantHistoryTurn(alloc, "third", "three");
try std.testing.expectEqual(@as(usize, 1), runtime.historyLen());
try std.testing.expectEqualStrings("three", runtime.lastAssistantReply().?);
}
test "SessionRuntime.appendHistoryEntry stores exact deep copies for all variants" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 0 };
defer runtime.deinit(alloc);
var caller_assistant = try makeAssistantTurn(alloc, "alpha", "assistant alpha");
defer freeHistoryTurn(alloc, caller_assistant);
var caller_background = HistoryTurn{ .background_command = .{
.user = .{ .text = try alloc.dupe(u8, "run server") },
.assistant = try alloc.dupe(u8, "server ready"),
.execution = .{ .files = blk: {
const files = try alloc.alloc(FileEvidence, 1);
files[0] = .{
.path = try alloc.dupe(u8, "src/main.zig"),
.tool_call_id = try alloc.dupe(u8, "call_read"),
.tool_name = try alloc.dupe(u8, "read_file"),
.action = .read,
.status = .success,
};
break :blk files;
} },
.log_path = try alloc.dupe(u8, "/tmp/server.log"),
.expect_url = true,
.url = try alloc.dupe(u8, "http://localhost:3000"),
} };
defer freeHistoryTurn(alloc, caller_background);
var caller_summary = HistoryTurn{ .compacted_summary = .{
.summary = try alloc.dupe(u8, "prior summary"),
.removed_turn_count = 4,
.compaction_count = 2,
} };
defer freeHistoryTurn(alloc, caller_summary);
try runtime.appendHistoryEntry(alloc, caller_assistant);
try runtime.appendHistoryEntry(alloc, caller_background);
try runtime.appendHistoryEntry(alloc, caller_summary);
caller_assistant.assistant.user.text[0] = 'z';
caller_background.background_command.assistant.?[0] = '!';
caller_background.background_command.execution.files[0].path[0] = '!';
caller_background.background_command.log_path[0] = '!';
caller_background.background_command.url.?[0] = '!';
caller_summary.compacted_summary.summary[0] = '!';
try std.testing.expectEqualStrings("alpha", runtime.history.items[0].assistant.user.text);
try std.testing.expectEqualStrings("server ready", runtime.history.items[1].background_command.assistant.?);
try std.testing.expectEqualStrings("src/main.zig", runtime.history.items[1].background_command.execution.files[0].path);
try std.testing.expectEqualStrings("/tmp/server.log", runtime.history.items[1].background_command.log_path);
try std.testing.expectEqualStrings("http://localhost:3000", runtime.history.items[1].background_command.url.?);
try std.testing.expectEqualStrings("prior summary", runtime.history.items[2].compacted_summary.summary);
runtime.max_history_turns = 3;
const extra = try makeAssistantTurn(alloc, "omega", "assistant omega");
defer freeHistoryTurn(alloc, extra);
try runtime.appendHistoryEntry(alloc, extra);
try std.testing.expectEqual(@as(usize, 4), runtime.historyLen());
try std.testing.expectEqualStrings("alpha", runtime.history.items[0].assistant.user.text);
try std.testing.expectEqualStrings("prior summary", runtime.history.items[2].compacted_summary.summary);
try std.testing.expectEqualStrings("omega", runtime.history.items[3].assistant.user.text);
}
test "SessionRuntime appends every canonical history turn without compaction" {
const alloc = std.testing.allocator;
var runtime = SessionRuntime{ .max_history_turns = 2 };
defer runtime.deinit(alloc);
const first = try makeAssistantTurn(alloc, "one", "first");
defer freeHistoryTurn(alloc, first);
try runtime.appendHistoryEntry(alloc, first);
const second = try makeAssistantTurn(alloc, "two", "second");
defer freeHistoryTurn(alloc, second);
try runtime.appendHistoryEntry(alloc, second);
const third = try makeAssistantTurn(alloc, "three", "third");
defer freeHistoryTurn(alloc, third);
try runtime.appendHistoryEntry(alloc, third);
try std.testing.expectEqual(@as(usize, 3), runtime.historyLen());
}
test "compactLineText preserves complete UTF-8 codepoints at the byte cap" {
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const split = try compactLineText(arena, "ab★z", 3);
try std.testing.expectEqualStrings("ab", split);
try std.testing.expect(std.unicode.utf8ValidateSlice(split));
const exact = try compactLineText(arena, "ab★z", 5);
try std.testing.expectEqualStrings("ab★", exact);
const collapsed = try compactLineText(arena, " alpha\n\tbeta ", 10);
try std.testing.expectEqualStrings("alpha beta", collapsed);
}
test "compacted Unicode history serializes system content as a string" {
const gateway_json = @import("../gateway/gateway_json.zig");
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 2 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, ("a" ** 155) ++ "★", "first reply");
try runtime.appendAssistantHistoryTurn(alloc, "second user", "second reply");
try runtime.appendAssistantHistoryTurn(alloc, "preserved user", "preserved reply");
const context = try runtime.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, context);
try std.testing.expectEqual(@as(usize, 2), context.len);
try std.testing.expect(context[0] == .compacted_summary);
try std.testing.expectEqualStrings("preserved user", context[1].assistant.user.text);
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const arena = arena_state.allocator();
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer messages.deinit(arena);
try appendHistoryChatMessages(arena, &messages, context);
try messages.append(arena, .{ .role = .user, .content = "current user" });
const body = try gateway_json.buildGatewayRequestBody(arena, "[]", messages.items);
const shape = try gateway_json.formatGatewayRequestShapeSummary(arena, body);
try std.testing.expect(std.mem.find(u8, shape, "prompt.0 role=system content=string") != null);
try std.testing.expect(std.unicode.utf8ValidateSlice(context[0].compacted_summary.summary));
try std.testing.expectEqual(@as(usize, 3), runtime.historyLen());
}
test "SessionRuntime preserves canonical turns and derives a bounded request window" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 4 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "one", "reply one");
try runtime.appendAssistantHistoryTurn(alloc, "two", "reply two");
try runtime.appendAssistantHistoryTurn(alloc, "three", "reply three");
try runtime.appendAssistantHistoryTurn(alloc, "four", "reply four");
try runtime.appendAssistantHistoryTurn(alloc, "five", "reply five");
try std.testing.expectEqual(@as(usize, 5), runtime.historyLen());
try std.testing.expectEqualStrings("one", runtime.history.items[0].assistant.user.text);
try std.testing.expectEqualStrings("five", runtime.history.items[4].assistant.user.text);
try std.testing.expectEqualStrings("reply five", runtime.lastAssistantReply().?);
const context = try runtime.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, context);
try std.testing.expectEqual(@as(usize, 4), context.len);
try std.testing.expect(context[0] == .compacted_summary);
try std.testing.expectEqual(@as(usize, 2), context[0].compacted_summary.removed_turn_count);
try std.testing.expectEqualStrings("three", context[1].assistant.user.text);
try std.testing.expectEqualStrings("five", context[3].assistant.user.text);
}
test "SessionRuntime manual compaction summarizes the canonical prefix" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(
alloc,
"first prompt sentinel",
"first reply sentinel",
);
try runtime.appendAssistantHistoryTurn(
alloc,
"second prompt sentinel",
"second reply sentinel",
);
runtime.forceCompaction();
const context = try runtime.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, context);
try std.testing.expectEqual(@as(usize, 2), context.len);
try std.testing.expect(context[0] == .compacted_summary);
try std.testing.expect(std.mem.find(
u8,
context[0].compacted_summary.summary,
"first prompt sentinel",
) != null);
try std.testing.expect(std.mem.find(
u8,
context[0].compacted_summary.summary,
"first reply sentinel",
) != null);
try std.testing.expectEqualStrings(
"second prompt sentinel",
context[1].assistant.user.text,
);
try std.testing.expectEqual(@as(usize, 2), runtime.historyLen());
try std.testing.expectEqualStrings(
"first prompt sentinel",
runtime.history.items[0].assistant.user.text,
);
}
test "compacted failed turn does not claim user interruption" {
const alloc = std.testing.allocator;
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("stream a response") },
.assistant = @constCast("partial response"),
.terminal_reason = .failed,
} }};
const summary = try buildCompactedSummaryText(alloc, null, &history);
defer alloc.free(summary);
try std.testing.expect(std.mem.find(u8, summary, "failed") != null);
try std.testing.expect(std.mem.find(u8, summary, "user interrupted") == null);
}
test "compacted cancelled turn omits verbose interruption transcript" {
const alloc = std.testing.allocator;
var completed_tool_names = [_][]u8{@constCast("read_file")};
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("inspect the project") },
.completed_tool_names = completed_tool_names[0..],
} }};
const summary = try buildCompactedSummaryText(alloc, null, &history);
defer alloc.free(summary);
try std.testing.expect(std.mem.find(u8, summary, "cancelled") != null);
try std.testing.expect(std.mem.find(u8, summary, "Interrupted by user after completing") == null);
try std.testing.expect(std.mem.find(u8, summary, "") == null);
}
test "SessionRuntime manual compaction merges an existing prefix summary" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.appendHistoryEntry(alloc, .{ .compacted_summary = .{
.summary = @constCast("prior summary sentinel"),
.removed_turn_count = 2,
.compaction_count = 1,
} });
try runtime.appendAssistantHistoryTurn(
alloc,
"newly compacted prompt sentinel",
"newly compacted reply sentinel",
);
try runtime.appendAssistantHistoryTurn(
alloc,
"retained prompt sentinel",
"retained reply sentinel",
);
runtime.forceCompaction();
const context = try runtime.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, context);
try std.testing.expectEqual(@as(usize, 2), context.len);
try std.testing.expect(context[0] == .compacted_summary);
try std.testing.expectEqual(@as(usize, 3), context[0].compacted_summary.removed_turn_count);
try std.testing.expectEqual(@as(usize, 2), context[0].compacted_summary.compaction_count);
try std.testing.expect(std.mem.find(
u8,
context[0].compacted_summary.summary,
"prior summary sentinel",
) != null);
try std.testing.expect(std.mem.find(
u8,
context[0].compacted_summary.summary,
"newly compacted prompt sentinel",
) != null);
try std.testing.expectEqualStrings(
"retained prompt sentinel",
context[1].assistant.user.text,
);
try std.testing.expectEqual(@as(usize, 3), runtime.historyLen());
try std.testing.expectEqualStrings(
"prior summary sentinel",
runtime.history.items[0].compacted_summary.summary,
);
}
test "SessionRuntime context snapshot allocation failure preserves canonical state" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "first prompt", "first reply");
try runtime.appendAssistantHistoryTurn(alloc, "second prompt", "second reply");
runtime.forceCompaction();
const context_history_start = runtime.contextHistoryStart();
var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0 });
try std.testing.expectError(
error.OutOfMemory,
runtime.snapshotContextHistory(failing.allocator()),
);
try std.testing.expectEqual(@as(usize, 2), runtime.historyLen());
try std.testing.expectEqual(context_history_start, runtime.contextHistoryStart());
try std.testing.expectEqualStrings(
"first prompt",
runtime.history.items[0].assistant.user.text,
);
try std.testing.expectEqualStrings(
"second prompt",
runtime.history.items[1].assistant.user.text,
);
}
fn expectCanonicalHistoryFixtureUnchanged(
canonical: []const HistoryTurn,
context_history_start: usize,
) !void {
try std.testing.expectEqual(@as(usize, 1), context_history_start);
try std.testing.expectEqual(@as(usize, 3), canonical.len);
try std.testing.expectEqualStrings(
"canonical prefix",
canonical[0].assistant.user.text,
);
try std.testing.expectEqualStrings(
"prefix assistant",
canonical[0].assistant.assistant,
);
try std.testing.expectEqual(@as(usize, 1), canonical[0].assistant.user.images.len);
try std.testing.expectEqual(@as(usize, 1), canonical[0].assistant.user.images[0].id);
try std.testing.expectEqualStrings(
"/tmp/prefix.png",
canonical[0].assistant.user.images[0].path,
);
try std.testing.expectEqualStrings(
"image/png",
canonical[0].assistant.user.images[0].media_type,
);
try std.testing.expectEqualStrings(
"canonical background",
canonical[1].background_command.user.text,
);
try std.testing.expectEqualStrings(
"background assistant",
canonical[1].background_command.assistant.?,
);
try std.testing.expectEqual(@as(usize, 1), canonical[1].background_command.execution.tool_steps.len);
try std.testing.expectEqualStrings(
"checking preserved evidence",
canonical[1].background_command.execution.tool_steps[0].assistant.?,
);
try std.testing.expectEqual(@as(usize, 1), canonical[1].background_command.execution.tool_steps[0].tool_calls.len);
try std.testing.expectEqualStrings(
"call_preserved",
canonical[1].background_command.execution.tool_steps[0].tool_calls[0].id,
);
try std.testing.expectEqualStrings(
"read_file",
canonical[1].background_command.execution.tool_steps[0].tool_calls[0].name,
);
try std.testing.expectEqualStrings(
"{\"path\":\"fixture.txt\"}",
canonical[1].background_command.execution.tool_steps[0].tool_calls[0].arguments_json,
);
try std.testing.expectEqual(@as(usize, 1), canonical[1].background_command.execution.tool_steps[0].tool_results.len);
try std.testing.expectEqual(
PersistedToolStatus.failure,
canonical[1].background_command.execution.tool_steps[0].tool_results[0].status,
);
try std.testing.expectEqualStrings(
"preserved failure",
canonical[1].background_command.execution.tool_steps[0].tool_results[0].output,
);
try std.testing.expectEqualStrings(
"result-call_preserved.txt",
canonical[1].background_command.execution.tool_steps[0].tool_results[0].output_handle.?,
);
try std.testing.expectEqualStrings(
"preserved preview",
canonical[1].background_command.execution.tool_steps[0].tool_results[0].preview.?,
);
try std.testing.expectEqual(@as(usize, 17), canonical[1].background_command.execution.tool_steps[0].tool_results[0].output_bytes);
try std.testing.expectEqual(@as(usize, 17), canonical[1].background_command.execution.tool_steps[0].tool_results[0].stored_output_bytes);
try std.testing.expectEqual(@as(usize, 1), canonical[1].background_command.execution.files.len);
try std.testing.expectEqualStrings(
"src/preserved.zig",
canonical[1].background_command.execution.files[0].path,
);
try std.testing.expectEqualStrings(
"src/preserved-renamed.zig",
canonical[1].background_command.execution.files[0].new_path.?,
);
try std.testing.expectEqualStrings(
"call_preserved",
canonical[1].background_command.execution.files[0].tool_call_id,
);
try std.testing.expectEqualStrings(
"read_file",
canonical[1].background_command.execution.files[0].tool_name,
);
try std.testing.expectEqual(
FileEvidenceAction.rename,
canonical[1].background_command.execution.files[0].action,
);
try std.testing.expectEqual(
PersistedToolStatus.failure,
canonical[1].background_command.execution.files[0].status,
);
try std.testing.expect(canonical[1].background_command.execution.files[0].model_view_covers_full_file);
try std.testing.expect(canonical[1].background_command.execution.files[0].stale);
try std.testing.expectEqualStrings(
"/tmp/preserved.log",
canonical[1].background_command.log_path,
);
try std.testing.expect(canonical[1].background_command.expect_url);
try std.testing.expectEqualStrings(
"http://localhost:3000",
canonical[1].background_command.url.?,
);
try std.testing.expectEqualStrings(
"canonical interrupted",
canonical[2].interrupted.user.text,
);
try std.testing.expectEqualStrings(
"partial assistant",
canonical[2].interrupted.assistant.?,
);
try std.testing.expectEqualStrings(
"call_interrupted",
canonical[2].interrupted.tool_call.?.id,
);
try std.testing.expectEqualStrings(
"browser_navigate",
canonical[2].interrupted.tool_call.?.name,
);
try std.testing.expectEqualStrings(
"{\"url\":\"http://localhost:3000\"}",
canonical[2].interrupted.tool_call.?.arguments_json,
);
try std.testing.expectEqual(@as(usize, 1), canonical[2].interrupted.completed_tool_names.len);
try std.testing.expectEqualStrings(
"read_file",
canonical[2].interrupted.completed_tool_names[0],
);
}
fn checkPromptHistorySnapshotAllocationFailures(alloc: Allocator) !void {
var prefix_images = [_]ImageAttachment{.{
.id = 1,
.path = @constCast("/tmp/prefix.png"),
.media_type = @constCast("image/png"),
}};
var calls = [_]ToolCall{.{
.id = "call_preserved",
.name = "read_file",
.arguments_json = "{\"path\":\"fixture.txt\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_preserved"),
.tool_name = @constCast("read_file"),
.status = .failure,
.output = @constCast("preserved failure"),
.output_bytes = 17,
.stored_output_bytes = 17,
.output_handle = @constCast("result-call_preserved.txt"),
.preview = @constCast("preserved preview"),
}};
var files = [_]FileEvidence{.{
.path = @constCast("src/preserved.zig"),
.new_path = @constCast("src/preserved-renamed.zig"),
.tool_call_id = @constCast("call_preserved"),
.tool_name = @constCast("read_file"),
.action = .rename,
.status = .failure,
.model_view_covers_full_file = true,
.stale = true,
}};
var steps = [_]ToolExecutionStep{.{
.assistant = @constCast("checking preserved evidence"),
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
var completed_tool_names = [_][]u8{@constCast("read_file")};
var canonical = [_]HistoryTurn{
.{ .assistant = .{
.user = .{
.text = @constCast("canonical prefix"),
.images = prefix_images[0..],
},
.assistant = @constCast("prefix assistant"),
} },
.{ .background_command = .{
.user = .{ .text = @constCast("canonical background") },
.assistant = @constCast("background assistant"),
.execution = .{
.tool_steps = steps[0..],
.files = files[0..],
},
.log_path = @constCast("/tmp/preserved.log"),
.expect_url = true,
.url = @constCast("http://localhost:3000"),
} },
.{ .interrupted = .{
.user = .{ .text = @constCast("canonical interrupted") },
.assistant = @constCast("partial assistant"),
.tool_call = .{
.id = "call_interrupted",
.name = "browser_navigate",
.arguments_json = "{\"url\":\"http://localhost:3000\"}",
},
.completed_tool_names = completed_tool_names[0..],
} },
};
const context_history_start: usize = 1;
const prompt_history = snapshotOwnedContextHistory(
alloc,
&canonical,
context_history_start,
8,
) catch |err| {
try expectCanonicalHistoryFixtureUnchanged(
&canonical,
context_history_start,
);
return switch (err) {
error.WriteFailed => error.OutOfMemory,
else => err,
};
};
defer freeHistoryTurnSlice(alloc, prompt_history);
try expectCanonicalHistoryFixtureUnchanged(
&canonical,
context_history_start,
);
}
test "prompt history snapshot failure preserves canonical ownership" {
try std.testing.checkAllAllocationFailures(
std.testing.allocator,
checkPromptHistorySnapshotAllocationFailures,
.{},
);
}
fn checkPromptHistoryMessageProjectionAllocationFailures(
alloc: Allocator,
prompt_history: []const HistoryTurn,
) !void {
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
appendHistoryMessagesBudgeted(
alloc,
&messages,
prompt_history,
.{ .max_tokens = 1 },
) catch |err| {
try std.testing.expectEqualStrings(
"old user",
prompt_history[0].assistant.user.text,
);
try std.testing.expectEqualStrings(
"latest user",
prompt_history[1].assistant.user.text,
);
try std.testing.expectEqual(
PersistedToolStatus.failure,
prompt_history[1].assistant.execution.tool_steps[0].tool_results[0].status,
);
return switch (err) {
error.WriteFailed => error.OutOfMemory,
else => err,
};
};
try std.testing.expect(messages.items.len >= 6);
try std.testing.expectEqualStrings(
"latest user",
messages.items[messages.items.len - 5].content.?.asText(),
);
try std.testing.expectEqual(
PersistedToolStatus.failure,
messages.items[messages.items.len - 3].tool_result_status.?,
);
try std.testing.expect(std.mem.find(
u8,
messages.items[messages.items.len - 2].content.?.asText(),
"src/latest.zig",
) != null);
}
test "prompt history cleanup is independent after snapshot creation" {
const alloc = std.testing.allocator;
var calls = [_]ToolCall{.{
.id = "call_latest",
.name = "read_file",
.arguments_json = "{\"path\":\"src/latest.zig\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_latest"),
.tool_name = @constCast("read_file"),
.status = .failure,
.output = @constCast("latest failure"),
.output_bytes = 14,
.stored_output_bytes = 14,
}};
var files = [_]FileEvidence{.{
.path = @constCast("src/latest.zig"),
.tool_call_id = @constCast("call_latest"),
.tool_name = @constCast("read_file"),
.action = .read,
.status = .failure,
}};
var steps = [_]ToolExecutionStep{.{
.assistant = @constCast("checking latest"),
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
const canonical = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("old user") },
.assistant = @constCast("old assistant"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("latest user") },
.assistant = @constCast("latest assistant"),
.execution = .{
.tool_steps = steps[0..],
.files = files[0..],
},
} },
};
const prompt_history = try snapshotOwnedContextHistory(
alloc,
&canonical,
0,
8,
);
defer freeHistoryTurnSlice(alloc, prompt_history);
try std.testing.checkAllAllocationFailures(
alloc,
checkPromptHistoryMessageProjectionAllocationFailures,
.{prompt_history},
);
try std.testing.expectEqualStrings(
"old user",
canonical[0].assistant.user.text,
);
try std.testing.expectEqualStrings(
"latest user",
canonical[1].assistant.user.text,
);
}
test "owned prompt history selection matches SessionRuntime context snapshot" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 2 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "one", "reply one");
try runtime.appendAssistantHistoryTurn(alloc, "two", "reply two");
try runtime.appendAssistantHistoryTurn(alloc, "three", "reply three");
runtime.context_history_start = 1;
const direct = try snapshotOwnedContextHistory(
alloc,
runtime.history.items,
runtime.context_history_start,
runtime.max_history_turns,
);
defer freeHistoryTurnSlice(alloc, direct);
const through_runtime = try runtime.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, through_runtime);
try std.testing.expectEqual(through_runtime.len, direct.len);
for (through_runtime, direct) |runtime_turn, direct_turn| {
try std.testing.expectEqual(
std.meta.activeTag(runtime_turn),
std.meta.activeTag(direct_turn),
);
}
try std.testing.expectEqualStrings(
through_runtime[0].compacted_summary.summary,
direct[0].compacted_summary.summary,
);
try std.testing.expectEqualStrings(
through_runtime[1].assistant.user.text,
direct[1].assistant.user.text,
);
}
test "SessionRuntime context projection preserves nine typed canonical turns" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
var calls = [_]ToolCall{.{
.id = "call_first",
.name = "read_file",
.arguments_json = "{\"path\":\"fixture.txt\"}",
}};
var results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_first"),
.tool_name = @constCast("read_file"),
.status = .success,
.output = @constCast("first result sentinel"),
.output_bytes = 21,
.stored_output_bytes = 21,
}};
var steps = [_]ToolExecutionStep{.{
.tool_calls = calls[0..],
.tool_results = results[0..],
}};
try runtime.appendHistoryEntry(alloc, .{ .assistant = .{
.user = .{ .text = @constCast("first prompt") },
.assistant = @constCast("first reply"),
.execution = .{ .tool_steps = steps[0..] },
} });
for (1..9) |index| {
const user = try std.fmt.allocPrint(alloc, "prompt {d}", .{index});
defer alloc.free(user);
const assistant = try std.fmt.allocPrint(alloc, "reply {d}", .{index});
defer alloc.free(assistant);
try runtime.appendAssistantHistoryTurn(alloc, user, assistant);
}
try std.testing.expectEqual(@as(usize, 9), runtime.historyLen());
try std.testing.expectEqualStrings(
"call_first",
runtime.history.items[0].assistant.execution.tool_steps[0].tool_calls[0].id,
);
try std.testing.expectEqualStrings(
"first result sentinel",
runtime.history.items[0].assistant.execution.tool_steps[0].tool_results[0].output,
);
const context = try runtime.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, context);
try std.testing.expectEqual(@as(usize, 5), context.len);
try std.testing.expect(context[0] == .compacted_summary);
try std.testing.expectEqual(@as(usize, 5), context[0].compacted_summary.removed_turn_count);
try std.testing.expectEqualStrings("prompt 5", context[1].assistant.user.text);
try std.testing.expectEqualStrings("prompt 8", context[4].assistant.user.text);
try std.testing.expectEqual(@as(usize, 9), runtime.historyLen());
try std.testing.expectEqualStrings(
"call_first",
runtime.history.items[0].assistant.execution.tool_steps[0].tool_calls[0].id,
);
}
test "SessionRuntime.appendBackgroundCommandHistoryTurn duplicates fields and projects context" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
const user_text = try alloc.dupe(u8, "run dev server");
defer alloc.free(user_text);
const log_path = try alloc.dupe(u8, "/tmp/dev.log");
defer alloc.free(log_path);
const url = try alloc.dupe(u8, "http://localhost:5173");
defer alloc.free(url);
const background = command_contract.BackgroundCommand{
.pid = "123",
.background_record_id = .{
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
},
.command = "npm run dev",
.cwd = "/tmp/app",
.log_path = log_path,
.url = url,
.expect_url = true,
};
try runtime.appendBackgroundCommandHistoryTurn(alloc, user_text, background);
user_text[0] = '!';
log_path[0] = '!';
url[0] = '!';
const entry = runtime.history.items[0].background_command;
try std.testing.expectEqualStrings("run dev server", entry.user.text);
try std.testing.expectEqualStrings("/tmp/dev.log", entry.log_path);
try std.testing.expectEqualStrings("http://localhost:5173", entry.url.?);
try std.testing.expect(entry.expect_url);
try std.testing.expectEqualSlices(
u8,
&background.background_record_id.?,
&entry.background_record_id.?,
);
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try SessionRuntime.appendHistoryMessages(alloc, &messages, runtime.history.items);
try std.testing.expectEqual(@as(usize, 2), messages.items.len);
try std.testing.expectEqualStrings("run dev server", messages.items[0].content.?.asText());
try std.testing.expectEqualStrings(
"Session event: a previous user request launched a background server. Log: /tmp/dev.log. URL observed at launch: http://localhost:5173. Re-check runtime context for current liveness before reusing it.",
messages.items[1].content.?.asText(),
);
try std.testing.expect(messages.items[1].owns_content);
}
test "SessionRuntime.snapshotHistory returns deep copy that outlives runtime history" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "hello", "world");
try runtime.appendBackgroundCommandHistoryTurn(alloc, "run", .{
.pid = "1",
.command = "serve",
.cwd = "/tmp",
.log_path = "/tmp/run.log",
.url = null,
.expect_url = false,
});
const snapshot = try runtime.snapshotHistory(alloc);
defer freeHistoryTurnSlice(alloc, snapshot);
try std.testing.expect(snapshot[0].assistant.user.text.ptr != runtime.history.items[0].assistant.user.text.ptr);
try std.testing.expect(snapshot[1].background_command.log_path.ptr != runtime.history.items[1].background_command.log_path.ptr);
runtime.clearHistory(alloc);
try std.testing.expectEqualStrings("hello", snapshot[0].assistant.user.text);
try std.testing.expectEqualStrings("/tmp/run.log", snapshot[1].background_command.log_path);
}
test "work provenance survives owned runtime snapshots without entering model context" {
const alloc = std.testing.allocator;
var source = try makeAssistantTurn(alloc, "delegated prompt", "delegated reply");
defer freeHistoryTurn(alloc, source);
try copyWorkIdToTurn(alloc, &source, "work-λ");
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.restore(alloc, ConversationLanguage.literal("en"), &.{source});
try runtime.appendAssistantHistoryTurn(alloc, "direct human prompt", "direct reply");
const snapshot = try runtime.snapshotHistory(alloc);
defer freeHistoryTurnSlice(alloc, snapshot);
try std.testing.expectEqualStrings("work-λ", snapshot[0].assistant.user.work_id.?);
try std.testing.expect(snapshot[0].assistant.user.work_id.?.ptr != source.assistant.user.work_id.?.ptr);
try std.testing.expect(snapshot[0].assistant.user.work_id.?.ptr != runtime.history.items[0].assistant.user.work_id.?.ptr);
try std.testing.expect(snapshot[1].assistant.user.work_id == null);
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, snapshot);
try std.testing.expectEqualStrings(
"delegated prompt",
messages.items[0].content.?.asText(),
);
try std.testing.expect(std.mem.find(
u8,
messages.items[0].content.?.asText(),
"work-λ",
) == null);
}
fn checkWorkProvenanceOwnershipFailures(alloc: Allocator) !void {
var images = [_]ImageAttachment{.{
.id = 1,
.path = @constCast("/tmp/image.png"),
.media_type = @constCast("image/png"),
}};
const copy = try dupeUserTurn(alloc, .{
.text = @constCast("prompt"),
.images = &images,
.work_id = @constCast("work-owned"),
});
freeUserTurn(alloc, copy);
}
test "work provenance user ownership frees every partial allocation" {
try std.testing.checkAllAllocationFailures(
std.testing.allocator,
checkWorkProvenanceOwnershipFailures,
.{},
);
}
test "SessionRuntime.snapshotHistory frees partial copies on allocation failure" {
const base = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(base);
try runtime.appendAssistantHistoryTurn(base, "first", "one");
try runtime.appendAssistantHistoryTurn(base, "second", "two");
var failing = std.testing.FailingAllocator.init(base, .{ .fail_index = 3 });
try std.testing.expectError(error.OutOfMemory, runtime.snapshotHistory(failing.allocator()));
}
test "full image catalog retains history excluded from budgeted context" {
const alloc = std.testing.allocator;
var historical_images = [_]ImageAttachment{.{
.id = 3,
.path = @constCast("/tmp/historical.png"),
.media_type = @constCast("image/png"),
}};
const history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("old image"), .images = &historical_images },
.assistant = @constCast("old answer"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("recent text") },
.assistant = @constCast("recent answer"),
} },
};
const budgeted = try snapshotOwnedContextHistory(alloc, &history, 0, 1);
defer freeHistoryTurnSlice(alloc, budgeted);
try std.testing.expectEqual(@as(usize, 1), budgeted.len);
try std.testing.expectEqual(@as(usize, 0), budgeted[0].assistant.user.images.len);
const catalog = try collect_image_catalog(alloc, &history, &.{});
defer core_types.freeImageAttachmentSlice(alloc, catalog);
try std.testing.expectEqual(@as(usize, 1), catalog.len);
try std.testing.expectEqual(@as(usize, 3), catalog[0].id);
try std.testing.expectEqualStrings("/tmp/historical.png", catalog[0].path);
}
test "full image catalog merges canonical history and current images" {
const alloc = std.testing.allocator;
var historical_images = [_]ImageAttachment{.{
.id = 2,
.path = @constCast("/tmp/historical.png"),
.media_type = @constCast("image/png"),
}};
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("old image"), .images = &historical_images },
} }};
const current = [_]ImageAttachment{.{
.id = 8,
.path = @constCast("/tmp/current.jpg"),
.media_type = @constCast("image/jpeg"),
}};
const catalog = try collect_image_catalog(alloc, &history, ¤t);
defer core_types.freeImageAttachmentSlice(alloc, catalog);
try std.testing.expectEqual(@as(usize, 2), catalog.len);
try std.testing.expectEqual(@as(usize, 2), catalog[0].id);
try std.testing.expectEqual(@as(usize, 8), catalog[1].id);
}
test "image catalog replacement preserves history and replaces current authority" {
const alloc = std.testing.allocator;
const catalog = [_]ImageAttachment{
.{ .id = 2, .path = @constCast("/tmp/history.png"), .media_type = @constCast("image/png") },
.{ .id = 8, .path = @constCast("/tmp/old-current.png"), .media_type = @constCast("image/png") },
};
const old_current = [_]ImageAttachment{
.{ .id = 8, .path = @constCast("/tmp/old-current.png"), .media_type = @constCast("image/png") },
};
const new_current = [_]ImageAttachment{
.{ .id = 9, .path = @constCast("/tmp/new-current.jpg"), .media_type = @constCast("image/jpeg") },
};
const replaced = try replace_image_catalog_current_images(
alloc,
&catalog,
&old_current,
&new_current,
);
defer core_types.freeImageAttachmentSlice(alloc, replaced);
try std.testing.expectEqual(@as(usize, 2), replaced.len);
try std.testing.expectEqual(@as(usize, 2), replaced[0].id);
try std.testing.expectEqual(@as(usize, 9), replaced[1].id);
try std.testing.expectEqualStrings("/tmp/new-current.jpg", replaced[1].path);
}
test "image catalog replacement rejects stale current authority" {
const catalog = [_]ImageAttachment{
.{ .id = 2, .path = @constCast("/tmp/history.png"), .media_type = @constCast("image/png") },
};
const stale_current = [_]ImageAttachment{
.{ .id = 8, .path = @constCast("/tmp/stale.png"), .media_type = @constCast("image/png") },
};
try std.testing.expectError(
error.StaleImageCatalog,
replace_image_catalog_current_images(
std.testing.allocator,
&catalog,
&stale_current,
&.{},
),
);
}
test "image catalog replacement cleans up every allocation failure" {
const catalog = [_]ImageAttachment{
.{ .id = 2, .path = @constCast("/tmp/history.png"), .media_type = @constCast("image/png") },
.{ .id = 8, .path = @constCast("/tmp/old-current.png"), .media_type = @constCast("image/png") },
};
const old_current = [_]ImageAttachment{
.{ .id = 8, .path = @constCast("/tmp/old-current.png"), .media_type = @constCast("image/png") },
};
const new_current = [_]ImageAttachment{
.{ .id = 9, .path = @constCast("/tmp/new-current.jpg"), .media_type = @constCast("image/jpeg") },
};
var probe = std.testing.FailingAllocator.init(std.testing.allocator, .{});
const replaced = try replace_image_catalog_current_images(
probe.allocator(),
&catalog,
&old_current,
&new_current,
);
core_types.freeImageAttachmentSlice(probe.allocator(), replaced);
for (0..probe.alloc_index) |fail_index| {
var failing = std.testing.FailingAllocator.init(
std.testing.allocator,
.{ .fail_index = fail_index },
);
try std.testing.expectError(
error.OutOfMemory,
replace_image_catalog_current_images(
failing.allocator(),
&catalog,
&old_current,
&new_current,
),
);
}
}
fn checkImageCatalogHistoryMergeAllocationFailures(alloc: Allocator) !void {
const catalog = [_]ImageAttachment{.{
.id = 2,
.path = @constCast("/tmp/history.png"),
.media_type = @constCast("image/png"),
}};
var added = [_]ImageAttachment{.{
.id = 8,
.path = @constCast("/tmp/completed.png"),
.media_type = @constCast("image/png"),
}};
const turn: HistoryTurn = .{ .assistant = .{
.user = .{ .text = @constCast("completed"), .images = &added },
.assistant = @constCast("done"),
} };
const merged = try merge_image_catalog_history_turn(alloc, &catalog, turn);
defer core_types.freeImageAttachmentSlice(alloc, merged);
try std.testing.expectEqual(@as(usize, 2), merged.len);
}
test "image catalog history merge cleans up every allocation failure" {
try std.testing.checkAllAllocationFailures(
std.testing.allocator,
checkImageCatalogHistoryMergeAllocationFailures,
.{},
);
}
test "full image catalog rejects duplicate authority ids" {
var historical_images = [_]ImageAttachment{.{
.id = 5,
.path = @constCast("/tmp/historical.png"),
.media_type = @constCast("image/png"),
}};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("old image"), .images = &historical_images },
.assistant = @constCast("answer"),
} }};
const current = [_]ImageAttachment{.{
.id = 5,
.path = @constCast("/tmp/current.png"),
.media_type = @constCast("image/png"),
}};
try std.testing.expectError(
error.DuplicateImageId,
collect_image_catalog(std.testing.allocator, &history, ¤t),
);
}
test "full image catalog rejects zero ids outside durable resume repair" {
const current = [_]ImageAttachment{.{
.id = 0,
.path = @constCast("/tmp/live-zero.png"),
.media_type = @constCast("image/png"),
}};
try std.testing.expectError(
error.InvalidImageId,
collect_image_catalog(std.testing.allocator, &.{}, ¤t),
);
}
test "full image catalog cleans up every allocation failure" {
var historical_images = [_]ImageAttachment{.{
.id = 1,
.path = @constCast("/tmp/historical.png"),
.media_type = @constCast("image/png"),
}};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("old image"), .images = &historical_images },
.assistant = @constCast("answer"),
} }};
const current = [_]ImageAttachment{.{
.id = 2,
.path = @constCast("/tmp/current.jpg"),
.media_type = @constCast("image/jpeg"),
}};
var probe = std.testing.FailingAllocator.init(std.testing.allocator, .{});
const probed = try collect_image_catalog(probe.allocator(), &history, ¤t);
core_types.freeImageAttachmentSlice(probe.allocator(), probed);
for (0..probe.alloc_index) |fail_index| {
var failing = std.testing.FailingAllocator.init(
std.testing.allocator,
.{ .fail_index = fail_index },
);
try std.testing.expectError(
error.OutOfMemory,
collect_image_catalog(failing.allocator(), &history, ¤t),
);
}
}
test "SessionRuntime.setConversationLanguageFromUserMessage preserves fallback without script signal" {
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
runtime.setConversationLanguageFromUserMessage("привет");
try std.testing.expectEqual(ConversationLanguage.literal("und-Cyrl"), runtime.languageSnapshot());
runtime.setConversationLanguageFromUserMessage("12345 !!!");
try std.testing.expectEqual(ConversationLanguage.literal("und-Cyrl"), runtime.languageSnapshot());
}
test "SessionRuntime.languageSnapshot reads restored language through mutex path" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.restore(alloc, ConversationLanguage.literal("und-Grek"), &.{});
try std.testing.expectEqual(ConversationLanguage.literal("und-Grek"), runtime.languageSnapshot());
}
test "SessionRuntime.lastAssistantReply and lastSummary return borrowed stored slices" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 0 };
defer runtime.deinit(alloc);
try std.testing.expect(runtime.lastAssistantReply() == null);
try std.testing.expect(runtime.lastSummary() == null);
try runtime.appendHistoryEntry(alloc, .{ .compacted_summary = .{
.summary = @constCast("first summary"),
.removed_turn_count = 2,
.compaction_count = 1,
} });
try runtime.appendAssistantHistoryTurn(alloc, "first", "one");
try runtime.appendHistoryEntry(alloc, .{ .compacted_summary = .{
.summary = @constCast("second summary"),
.removed_turn_count = 4,
.compaction_count = 2,
} });
try runtime.appendAssistantHistoryTurn(alloc, "second", "two");
try runtime.appendAssistantHistoryTurn(alloc, "execution only", "");
const reply = runtime.lastAssistantReply().?;
const summary = runtime.lastSummary().?;
try std.testing.expectEqualStrings("two", reply);
try std.testing.expectEqualStrings("first summary", summary);
try std.testing.expect(reply.ptr == runtime.history.items[3].assistant.assistant.ptr);
try std.testing.expect(summary.ptr == runtime.history.items[0].compacted_summary.summary.ptr);
}
test "SessionRuntime.forceCompaction advances context without deleting canonical history" {
const alloc = std.testing.allocator;
var empty: SessionRuntime = .{ .max_history_turns = 8 };
empty.forceCompaction();
try std.testing.expectEqual(@as(usize, 0), empty.historyLen());
var one: SessionRuntime = .{ .max_history_turns = 8 };
defer one.deinit(alloc);
try one.appendAssistantHistoryTurn(alloc, "one", "reply one");
one.forceCompaction();
try std.testing.expectEqual(@as(usize, 1), one.historyLen());
try std.testing.expectEqualStrings("reply one", one.lastAssistantReply().?);
const c_alloc = std.heap.c_allocator;
var many: SessionRuntime = .{ .max_history_turns = 8 };
defer many.deinit(c_alloc);
try many.appendAssistantHistoryTurn(c_alloc, "one", "reply one");
try many.appendAssistantHistoryTurn(c_alloc, "two", "reply two");
try many.appendAssistantHistoryTurn(c_alloc, "three", "reply three");
many.forceCompaction();
try std.testing.expectEqual(@as(usize, 3), many.historyLen());
try std.testing.expectEqualStrings("reply three", many.lastAssistantReply().?);
const compacted = try many.snapshotContextHistory(c_alloc);
defer freeHistoryTurnSlice(c_alloc, compacted);
try std.testing.expectEqual(@as(usize, 2), compacted.len);
try std.testing.expect(compacted[0] == .compacted_summary);
try std.testing.expect(std.mem.find(
u8,
compacted[0].compacted_summary.summary,
"one",
) != null);
try std.testing.expect(std.mem.find(
u8,
compacted[0].compacted_summary.summary,
"two",
) != null);
try std.testing.expectEqualStrings("three", compacted[1].assistant.user.text);
try many.appendAssistantHistoryTurn(c_alloc, "four", "reply four");
const continued = try many.snapshotContextHistory(c_alloc);
defer freeHistoryTurnSlice(c_alloc, continued);
try std.testing.expectEqual(@as(usize, 3), continued.len);
try std.testing.expect(continued[0] == .compacted_summary);
try std.testing.expectEqualStrings("three", continued[1].assistant.user.text);
try std.testing.expectEqualStrings("four", continued[2].assistant.user.text);
}
test "SessionRuntime restores a durable context boundary without deleting canonical history" {
const alloc = std.testing.allocator;
var original: SessionRuntime = .{ .max_history_turns = 8 };
defer original.deinit(alloc);
try original.appendAssistantHistoryTurn(alloc, "one", "reply one");
try original.appendAssistantHistoryTurn(alloc, "two", "reply two");
try original.appendAssistantHistoryTurn(alloc, "three", "reply three");
original.forceCompaction();
const history = try original.snapshotHistory(alloc);
defer freeHistoryTurnSlice(alloc, history);
var restored: SessionRuntime = .{ .max_history_turns = 8 };
defer restored.deinit(alloc);
try restored.restoreWithContextHistoryStart(
alloc,
ConversationLanguage.literal("en"),
history,
original.contextHistoryStart(),
);
try std.testing.expectEqual(@as(usize, 3), restored.historyLen());
try std.testing.expectEqual(@as(usize, 2), restored.contextHistoryStart());
const context = try restored.snapshotContextHistory(alloc);
defer freeHistoryTurnSlice(alloc, context);
try std.testing.expectEqual(@as(usize, 2), context.len);
try std.testing.expect(context[0] == .compacted_summary);
try std.testing.expect(std.mem.find(
u8,
context[0].compacted_summary.summary,
"one",
) != null);
try std.testing.expect(std.mem.find(
u8,
context[0].compacted_summary.summary,
"two",
) != null);
try std.testing.expectEqualStrings("three", context[1].assistant.user.text);
try std.testing.expectError(
error.InvalidContextHistoryStart,
restored.restoreWithContextHistoryStart(
alloc,
ConversationLanguage.literal("en"),
history,
history.len + 1,
),
);
}
test "SessionRuntime owns permission transitions and immutable snapshots" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
const key = try session_permission_state.RuleKey.init(
.command,
"command\x00/workspace\x00git status",
);
try std.testing.expectEqual(
session_permission_state.ApplyStatus.applied,
try runtime.applyPermissionEvent(alloc, .{ .set = .{
.key = key,
.display_identity = "git status in /workspace",
.decision = .deny,
.expected_generation = null,
} }),
);
var snapshot = try runtime.snapshotPermissionState(alloc);
defer snapshot.deinit(alloc);
try std.testing.expectEqual(
session_permission_state.StateDecision.deny,
session_permission_state.decide(snapshot, key),
);
var restored: SessionRuntime = .{ .max_history_turns = 8 };
defer restored.deinit(alloc);
try restored.restoreWithPermissionState(
alloc,
ConversationLanguage.literal("en"),
&.{},
0,
snapshot,
);
var restored_snapshot = try restored.snapshotPermissionState(alloc);
defer restored_snapshot.deinit(alloc);
try std.testing.expectEqual(
session_permission_state.StateDecision.deny,
session_permission_state.decide(restored_snapshot, key),
);
}
test "appendHistoryTurnToOwnedContext moves old ownership, duplicates new turn, and compacts" {
const alloc = std.testing.allocator;
var current = try alloc.alloc(HistoryTurn, 2);
current[0] = try makeAssistantTurn(alloc, "one", "reply one");
current[1] = try makeAssistantTurn(alloc, "two", "reply two");
var caller_turn = try makeAssistantTurn(alloc, "three", "reply three");
defer freeHistoryTurn(alloc, caller_turn);
const next = try appendHistoryTurnToOwnedContext(alloc, current, caller_turn, 2);
defer freeHistoryTurnSlice(alloc, next);
caller_turn.assistant.user.text[0] = '!';
try std.testing.expectEqual(@as(usize, 2), next.len);
try std.testing.expect(next[0] == .compacted_summary);
try std.testing.expectEqualStrings("three", next[1].assistant.user.text);
try std.testing.expectEqualStrings("reply three", next[1].assistant.assistant);
}
test "appendHistoryTurnToOwnedContext frees duplicate fields on allocation failure" {
var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 2 });
const turn = HistoryTurn{ .assistant = .{
.user = .{ .text = @constCast("prompt") },
.assistant = @constCast("reply"),
} };
try std.testing.expectError(error.OutOfMemory, appendHistoryTurnToOwnedContext(failing.allocator(), &.{}, turn, 0));
}
test "SessionRuntime.appendHistoryMessages matches top-level projection and preserves ownership flags" {
const alloc = std.testing.allocator;
const history = [_]HistoryTurn{
.{ .compacted_summary = .{
.summary = @constCast("Conversation summary:\n- Earlier turns compacted: 2"),
.removed_turn_count = 2,
.compaction_count = 1,
} },
.{ .assistant = .{
.user = .{ .text = @constCast("question") },
.assistant = @constCast("answer"),
} },
.{ .background_command = .{
.user = .{ .text = @constCast("run dev") },
.log_path = @constCast("/tmp/dev.log"),
.expect_url = true,
.url = @constCast("http://localhost:3000"),
} },
};
var static_messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &static_messages);
var top_level_messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &top_level_messages);
try SessionRuntime.appendHistoryMessages(alloc, &static_messages, &history);
try appendHistoryMessages(alloc, &top_level_messages, &history);
try std.testing.expectEqual(static_messages.items.len, top_level_messages.items.len);
for (static_messages.items, top_level_messages.items) |static_msg, top_level_msg| {
try std.testing.expectEqual(static_msg.role, top_level_msg.role);
try std.testing.expectEqualStrings(static_msg.content.?.asText(), top_level_msg.content.?.asText());
try std.testing.expectEqual(static_msg.owns_content, top_level_msg.owns_content);
}
try std.testing.expect(static_messages.items[0].owns_content);
try std.testing.expect(!static_messages.items[1].owns_content);
try std.testing.expect(!static_messages.items[2].owns_content);
try std.testing.expect(!static_messages.items[3].owns_content);
try std.testing.expect(static_messages.items[4].owns_content);
}
test "SessionRuntime.appendHistoryMessages frees owned system text when append fails" {
const compact = [_]HistoryTurn{.{ .compacted_summary = .{
.summary = @constCast("summary"),
.removed_turn_count = 1,
.compaction_count = 1,
} }};
var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
var messages: std.ArrayList(message.Message) = .empty;
try std.testing.expectError(error.OutOfMemory, SessionRuntime.appendHistoryMessages(failing.allocator(), &messages, &compact));
}
test "history context formatters return exact text" {
const alloc = std.testing.allocator;
const with_url = try formatBackgroundHistoryContext(alloc, .{
.user = .{ .text = @constCast("run") },
.log_path = @constCast("/tmp/url.log"),
.expect_url = true,
.url = @constCast("http://localhost:3000"),
});
defer alloc.free(with_url);
try std.testing.expectEqualStrings(
"Session event: a previous user request launched a background server. Log: /tmp/url.log. URL observed at launch: http://localhost:3000. Re-check runtime context for current liveness before reusing it.",
with_url,
);
const pending = try formatBackgroundHistoryContext(alloc, .{
.user = .{ .text = @constCast("run") },
.log_path = @constCast("/tmp/pending.log"),
.expect_url = true,
});
defer alloc.free(pending);
try std.testing.expectEqualStrings(
"Session event: a previous user request launched a background server. Log: /tmp/pending.log. Re-check runtime context for current liveness and URL state before reusing it.",
pending,
);
const command = try formatBackgroundHistoryContext(alloc, .{
.user = .{ .text = @constCast("run") },
.log_path = @constCast("/tmp/command.log"),
.expect_url = false,
});
defer alloc.free(command);
try std.testing.expectEqualStrings(
"Session event: a previous user request launched a background command. Log: /tmp/command.log. Re-check runtime context for current liveness before treating it as running.",
command,
);
const compacted = try formatCompactedContinuationMessage(alloc, "summary");
defer alloc.free(compacted);
try std.testing.expectEqualStrings(
"This session is being continued from earlier compacted context. The summary below covers the earlier portion of the conversation.\n\n" ++
"summary\n\n" ++
"Recent conversation turns are preserved verbatim.\n" ++
"Continue the conversation from where it left off without asking the user to repeat context. Resume directly.",
compacted,
);
}
fn deinitMessages(alloc: Allocator, messages: *std.ArrayList(message.Message)) void {
for (messages.items) |*msg| msg.deinit(alloc);
messages.deinit(alloc);
}