const std = @import("std");
const kernel_agent = @import("../agent/runtime/agent.zig");
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 language_script = @import("../shared/language_script.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");
const command_replay_store = @import("command_replay_store.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 managed_execution = @import("../execution/managed_execution.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 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;
fn formatInterruptedToolOutput(
alloc: Allocator,
entry: InterruptedHistoryTurn,
) ![]u8 {
const presentation = entry.cancelled_command orelse
return alloc.dupe(u8, aborted_tool_output);
const replay = presentation.output_replay orelse
return alloc.dupe(u8, aborted_tool_output);
const descriptor = switch (replay) {
.available => |value| value,
.unavailable => return alloc.dupe(u8, aborted_tool_output),
};
return command_replay_store.appendModelHandleNotice(
alloc,
aborted_tool_output,
descriptor.handle,
) catch |err| switch (err) {
error.InvalidReplayHandle => {
debug_trace.logf(
"session",
"cancelled command replay handle omitted from model context handle_bytes={d}",
.{descriptor.handle.len},
);
return alloc.dupe(u8, aborted_tool_output);
},
else => return err,
};
}
/// 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,
.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,
.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 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;
}
/// 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,
.interrupted => |entry| entry.user.images,
};
}
fn mutable_images_for_history_turn(turn: *HistoryTurn) []ImageAttachment {
return switch (turn.*) {
.compacted_summary => &.{},
.assistant => |*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,
.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,
.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,
.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"),
} },
.{ .assistant = .{
.user = .{ .text = background_text, .images = &background_images },
.assistant = @constCast("historical command"),
} },
.{ .interrupted = .{
.user = .{ .text = interrupted_text, .images = &interrupted_images },
.assistant = @constCast("partial"),
} },
};
defer alloc.free(history[0].assistant.user.text);
defer alloc.free(history[1].assistant.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].assistant.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 != .valid) {
_ = try persistedResultForMalformedCall(calls, results, call);
}
}
for (calls) |*call| {
if (call.argument_integrity == .valid) continue;
const result = try persistedResultForMalformedCall(calls, results, call.*);
const integrity = call.argument_integrity;
if (integrity == .non_object_json) {
// Repair replay input without changing what the stored result says happened.
if (call.provider_result != null or result.provider_native) {
call.provenance = .provider_executed;
call.argument_integrity = .valid;
continue;
}
const safe_arguments = try alloc.dupe(u8, "{}");
alloc.free(call.arguments_json);
call.arguments_json = safe_arguments;
call.argument_integrity = .valid;
tracePersistedToolArgumentsRepair(call.*, source, true, integrity);
continue;
}
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, integrity);
}
}
pub fn repairPersistedInterruptedToolArguments(
alloc: Allocator,
call: *ToolCall,
source: PersistedToolArgumentsSource,
) Allocator.Error!void {
if (call.argument_integrity == .valid) return;
const integrity = call.argument_integrity;
if (integrity == .non_object_json) {
if (call.provider_result != null) {
call.provenance = .provider_executed;
call.argument_integrity = .valid;
return;
}
const safe_arguments = try alloc.dupe(u8, "{}");
alloc.free(call.arguments_json);
call.arguments_json = safe_arguments;
}
call.argument_integrity = .valid;
tracePersistedToolArgumentsRepair(call.*, source, false, integrity);
}
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,
integrity: core_types.ToolArgumentIntegrity,
) void {
debug_trace.eventf(
"session",
"persisted_tool_arguments_repaired",
.{},
"source={s} call_id={s} tool={s} failure={s} paired_result={s}",
.{ @tagName(source), call.id, call.name, @tagName(integrity), if (paired_result) "true" else "false" },
);
}
pub const WebFetchArtifactState = union(enum) {
none,
store: web_fetch_artifacts.Store,
unavailable: anyerror,
};
const shell_id_key_raw = "\"session_id\":\"shell-";
const shell_id_key_escaped = "\\\"session_id\\\":\\\"shell-";
/// Collect every shell execution id referenced by restored history turns into
/// `out` (deduplicated; slices borrow from the turns, so history must outlive
/// the list). Matches both raw and JSON-escaped tool result envelopes.
fn collectReferencedShellIds(
alloc: Allocator,
history: []const HistoryTurn,
out: *std.ArrayList([]const u8),
) !void {
for (history) |turn| {
switch (turn) {
.assistant => |entry| {
if (entry.provider_replay) |replay|
try collectShellIdsFromText(alloc, replay.parts_json, out);
try collectShellIdsFromExecution(alloc, entry.execution, out);
},
.interrupted => |entry| {
if (entry.tool_call) |call| {
try collectShellIdsFromText(alloc, call.arguments_json, out);
if (call.provider_result) |result|
try collectShellIdsFromText(alloc, result, out);
}
try collectShellIdsFromExecution(alloc, entry.execution, out);
},
.compacted_summary => {},
}
}
}
fn collectShellIdsFromExecution(
alloc: Allocator,
execution: core_types.ExecutionMemory,
out: *std.ArrayList([]const u8),
) !void {
for (execution.tool_steps) |step| {
for (step.tool_calls) |call| {
try collectShellIdsFromText(alloc, call.arguments_json, out);
if (call.provider_result) |result|
try collectShellIdsFromText(alloc, result, out);
}
for (step.tool_results) |result| {
try collectShellIdsFromText(alloc, result.output, out);
if (result.preview) |preview|
try collectShellIdsFromText(alloc, preview, out);
}
if (step.provider_replay) |replay|
try collectShellIdsFromText(alloc, replay.parts_json, out);
}
}
/// True when restored history references shell execution handles that `exec`
/// does not own. Registry membership, not the resume event, decides staleness:
/// in-process resumes share the registry with executions started by this
/// process, so those handles stay valid and stay quiet.
pub fn detectStaleShellHandles(
alloc: Allocator,
history: []const HistoryTurn,
exec: *managed_execution.Runtime,
) !bool {
var ids: std.ArrayList([]const u8) = .empty;
defer ids.deinit(alloc);
try collectReferencedShellIds(alloc, history, &ids);
for (ids.items) |id| {
if (exec.stateFor(id) == null) return true;
}
return false;
}
fn collectShellIdsFromText(alloc: Allocator, text: []const u8, out: *std.ArrayList([]const u8)) !void {
try collectShellIdsWithKey(alloc, text, shell_id_key_raw, out);
try collectShellIdsWithKey(alloc, text, shell_id_key_escaped, out);
}
fn collectShellIdsWithKey(alloc: Allocator, text: []const u8, key: []const u8, out: *std.ArrayList([]const u8)) !void {
var from: usize = 0;
while (std.mem.find(u8, text[from..], key)) |rel| {
const at = from + rel;
from = at + key.len;
const id_start = at + key.len - "shell-".len;
var id_end = id_start + "shell-".len;
// Counter ids are digits; interactive terminal ids are base64url. Both
// share the charset checked by validHistoricalSessionId.
while (id_end < text.len) : (id_end += 1) {
const byte = text[id_end];
if (!std.ascii.isAlphanumeric(byte) and byte != '-' and byte != '_') break;
}
if (id_end == id_start + "shell-".len) continue;
const id = text[id_start..id_end];
for (out.items) |existing| {
if (std.mem.eql(u8, existing, id)) break;
} else try out.append(alloc, id);
}
}
pub const SessionRuntime = struct {
agent: kernel_agent.Agent = .{},
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,
/// In-memory boundary for handle-free history loaded without writer
/// provenance. It is never persisted; accepted checkpoints move the model
/// window beyond it.
unversioned_history_len: usize = 0,
/// True when restored history references shell execution handles that this
/// process does not own. Resume seams set it once after restore, before the
/// first agent turn of the resumed session, so no lock is required.
has_stale_shell_handles: bool = false,
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 initWithProviders(
max_history_turns: usize,
providers: generation_usage_provider.Set,
) SessionRuntime {
return .{
.usage = session_usage.Usage.initFreshWithProviders(providers),
.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.agent.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());
self.has_stale_shell_handles = false;
}
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);
self.has_stale_shell_handles = false;
for (history) |turn| {
try self.appendHistoryEntry(alloc, turn);
}
self.unversioned_history_len = if (self.agent.history.items.len > 0 and
isCurrentCompactionCheckpoint(self.agent.history.items[0]))
0
else
self.agent.history.items.len;
}
pub fn restoreWithPermissionState(
self: *SessionRuntime,
alloc: Allocator,
language: ConversationLanguage,
history: []const HistoryTurn,
permission_state: session_permission_state.State,
) !void {
var permission_copy = try session_permission_state.dupe(
alloc,
permission_state,
);
errdefer permission_copy.deinit(alloc);
try self.restore(alloc, language, history);
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 {
self.agent.clearHistory(alloc);
self.unversioned_history_len = 0;
}
pub fn historyLen(self: *const SessionRuntime) usize {
return self.agent.history.items.len;
}
pub fn unversionedHistoryEnd(self: *const SessionRuntime) usize {
return @min(self.unversioned_history_len, self.agent.history.items.len);
}
pub fn hasContextToCompact(self: *const SessionRuntime) bool {
for (self.agent.history.items) |turn| {
if (turn != .compacted_summary) return true;
}
return false;
}
pub fn compactedTurnCount(self: *const SessionRuntime) usize {
if (self.agent.history.items.len == 0) return 0;
return switch (self.agent.history.items[0]) {
.compacted_summary => |entry| entry.removed_turn_count,
else => 0,
};
}
pub fn compactionCount(self: *const SessionRuntime) usize {
if (self.agent.history.items.len == 0) return 0;
return switch (self.agent.history.items[0]) {
.compacted_summary => |entry| entry.compaction_count,
else => 0,
};
}
pub fn snapshotHistory(self: *const SessionRuntime, alloc: Allocator) ![]HistoryTurn {
return self.agent.snapshotHistory(alloc);
}
pub fn snapshotImageCatalog(
self: *const SessionRuntime,
alloc: Allocator,
current_images: []const ImageAttachment,
) ![]ImageAttachment {
return collect_image_catalog(alloc, self.agent.history.items, current_images);
}
pub fn snapshotContextHistory(self: *const SessionRuntime, alloc: Allocator) ![]HistoryTurn {
return snapshotOwnedContextHistory(
alloc,
self.agent.history.items,
0,
self.max_history_turns,
);
}
pub fn appendHistoryEntry(self: *SessionRuntime, alloc: Allocator, turn: HistoryTurn) !void {
const prepared = try self.prepareHistoryEntry(alloc, turn);
self.commitPreparedHistoryEntry(alloc, prepared);
}
pub fn prepareHistoryEntry(
self: *SessionRuntime,
alloc: Allocator,
turn: HistoryTurn,
) !HistoryTurn {
try self.agent.history.ensureUnusedCapacity(alloc, 1);
return dupeHistoryTurn(alloc, turn);
}
pub fn commitPreparedHistoryEntry(
self: *SessionRuntime,
alloc: Allocator,
turn: HistoryTurn,
) void {
if (isCurrentCompactionCheckpoint(turn)) {
self.agent.clearHistory(alloc);
self.unversioned_history_len = 0;
}
self.agent.history.appendAssumeCapacity(turn);
self.agent.fresh = false;
if (turn == .compacted_summary and isCurrentCompactionCheckpoint(turn)) {
self.unversioned_history_len = 0;
}
}
/// Takes ownership of a fully prepared replacement only after durable commit.
pub fn commitCompactedHistory(self: *SessionRuntime, alloc: Allocator, history: []HistoryTurn) void {
const uncertain = self.unversioned_history_len != 0;
self.agent.clearHistory(alloc);
self.agent.history.deinit(alloc);
self.agent.history = std.ArrayList(HistoryTurn).fromOwnedSlice(history);
self.agent.fresh = false;
self.unversioned_history_len = if (uncertain) history.len else 0;
}
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.agent.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);
}
pub fn appendHistoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
) !void {
try appendHistoryChatMessagesImpl(alloc, messages, history, .closed);
}
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.agent.history.items.len;
while (i > 0) {
i -= 1;
switch (self.agent.history.items[i]) {
.assistant => |entry| {
if (entry.assistant.len > 0) return entry.assistant;
},
else => {},
}
}
return null;
}
pub fn lastSummary(self: *const SessionRuntime) ?[]const u8 {
var index = self.agent.history.items.len;
while (index > 0) {
index -= 1;
if (self.agent.history.items[index] == .compacted_summary) {
return self.agent.history.items[index].compacted_summary.summary;
}
}
return null;
}
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,
_: usize,
_: usize,
) ![]HistoryTurn {
var copy: std.ArrayList(HistoryTurn) = .empty;
errdefer {
for (copy.items) |turn| freeHistoryTurn(alloc, turn);
copy.deinit(alloc);
}
try appendHistoryCopies(alloc, ©, canonical_history);
return copy.toOwnedSlice(alloc);
}
/// Borrows payloads. Only descriptors and rebased steering are arena-owned.
pub fn contextHistoryRange(
arena: Allocator,
history: []const HistoryTurn,
start: core_types.ContextHistoryCut,
end: ?core_types.ContextHistoryCut,
) ![]HistoryTurn {
var view: std.ArrayList(HistoryTurn) = .empty;
var raw_index: usize = 0;
for (history) |original| {
if (original == .compacted_summary) continue;
const index = raw_index;
raw_index += 1;
if (index < start.turns) continue;
if (end) |limit| {
if (index > limit.turns or (index == limit.turns and limit.tool_steps == 0 and limit.steering == 0)) break;
}
var turn = original;
const execution = switch (turn) {
.assistant => |*entry| &entry.execution,
.interrupted => |*entry| &entry.execution,
.compacted_summary => unreachable,
};
const first_step = if (index == start.turns) start.tool_steps else 0;
const first_steering = if (index == start.turns) start.steering else 0;
const partial_end = if (end) |limit| index == limit.turns else false;
const last_step = if (partial_end) end.?.tool_steps else execution.tool_steps.len;
const last_steering = if (partial_end) end.?.steering else execution.steering.len;
if (first_step > last_step or last_step > execution.tool_steps.len or
first_steering > last_steering or last_steering > execution.steering.len)
return error.InvalidContextHistoryStart;
execution.tool_steps = execution.tool_steps[first_step..last_step];
execution.steering = execution.steering[first_steering..last_steering];
if (first_step > 0 and execution.steering.len > 0) {
execution.steering = try arena.dupe(core_types.PersistedSteering, execution.steering);
for (execution.steering) |*item| {
if (item.after_tool_step_count < first_step) return error.InvalidContextHistoryStart;
item.after_tool_step_count -= first_step;
}
}
if (partial_end) {
execution.files = &.{};
execution.turn_summary = null;
switch (turn) {
.assistant => |*entry| {
entry.assistant = @constCast("");
entry.provider_replay = null;
},
.interrupted => |*entry| {
entry.assistant = null;
entry.tool_call = null;
entry.completed_tool_names = &.{};
entry.cancelled_command = null;
},
.compacted_summary => unreachable,
}
}
try view.append(arena, turn);
}
return view.toOwnedSlice(arena);
}
pub fn prepareCompactedHistory(
alloc: Allocator,
history: []const HistoryTurn,
summary: CompactedSummaryHistoryTurn,
cut: core_types.ContextHistoryCut,
) ![]HistoryTurn {
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
const retained = try contextHistoryRange(arena_state.allocator(), history, cut, null);
var next: std.ArrayList(HistoryTurn) = .empty;
errdefer {
for (next.items) |turn| freeHistoryTurn(alloc, turn);
next.deinit(alloc);
}
try next.ensureTotalCapacity(alloc, retained.len + 1);
next.appendAssumeCapacity(try dupeHistoryTurn(alloc, .{ .compacted_summary = summary }));
for (retained) |turn| next.appendAssumeCapacity(try dupeHistoryTurn(alloc, turn));
return next.toOwnedSlice(alloc);
}
test "retained context history replacement is allocation-failure atomic" {
const Fixture = struct {
fn run(alloc: Allocator) !void {
const replay = core_types.ProviderReplay{ .source = .{ .provider = .gateway, .model = "test" }, .parts_json = "[{\"type\":\"reasoning\",\"text\":\"retained\"}]" };
const source = [_]HistoryTurn{
.{ .assistant = .{ .user = .{ .text = @constCast("older request") }, .assistant = @constCast("older reply") } },
.{ .assistant = .{ .user = .{ .text = @constCast("recent request") }, .assistant = @constCast("recent reply"), .provider_replay = replay } },
};
const prepared = try prepareCompactedHistory(alloc, &source, .{
.summary = @constCast("older facts"),
.removed_turn_count = 1,
.compaction_count = 1,
}, .{ .turns = 1 });
defer freeHistoryTurnSlice(alloc, prepared);
try std.testing.expectEqual(@as(usize, 2), prepared.len);
try std.testing.expectEqualStrings("recent reply", prepared[1].assistant.assistant);
try std.testing.expectEqualStrings(replay.parts_json, prepared[1].assistant.provider_replay.?.parts_json);
try std.testing.expectEqualStrings("older reply", source[0].assistant.assistant);
}
};
try std.testing.checkAllAllocationFailures(std.testing.allocator, Fixture.run, .{});
}
/// 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 const dupeHistoryTurn = core_types.dupeHistoryTurn;
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.
/// 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);
}
pub fn appendHistoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
) !void {
try appendHistoryChatMessagesImpl(alloc, messages, history, .closed);
}
/// Projects the latest current checkpoint and its suffix for semantic
/// compaction. Without a current checkpoint, projects raw canonical turns and
/// returns the message boundary whose result provenance remains uncertain.
pub fn appendCompactionHistoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
uncertain_history_count: usize,
) !usize {
var checkpoint_index: ?usize = null;
for (history, 0..) |turn, index| {
if (isCurrentCompactionCheckpoint(turn)) checkpoint_index = index;
}
if (checkpoint_index) |start| {
const boundary = @max(start, @min(uncertain_history_count, history.len));
try appendHistoryChatMessagesImpl(alloc, messages, history[start..boundary], .closed);
const uncertain_message_count = messages.items.len;
try appendHistoryChatMessagesImpl(alloc, messages, history[boundary..], .closed);
return uncertain_message_count;
}
const boundary = @min(uncertain_history_count, history.len);
for (history[0..boundary], 0..) |turn, index| {
if (turn == .compacted_summary) continue;
try appendHistoryChatMessagesImpl(alloc, messages, history[index .. index + 1], .closed);
}
const uncertain_message_count = messages.items.len;
for (history[boundary..], boundary..) |turn, index| {
if (turn == .compacted_summary) continue;
try appendHistoryChatMessagesImpl(alloc, messages, history[index .. index + 1], .closed);
}
return uncertain_message_count;
}
fn isCurrentCompactionCheckpoint(turn: HistoryTurn) bool {
return switch (turn) {
.compacted_summary => |entry| std.mem.startsWith(
u8,
entry.summary,
core_types.context_handoff_open,
),
else => false,
};
}
/// Projects the active model window from one complete canonical snapshot.
/// New checkpoints may retain raw turns immediately before the appended
/// checkpoint; those turns are emitted after the checkpoint without
/// duplicating them in canonical storage.
pub fn appendActiveContextHistoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
context_history_start: usize,
) !void {
return appendActiveContextHistoryChatMessagesWithTrailingProjection(
alloc,
messages,
history,
context_history_start,
.closed,
);
}
pub fn appendSteeringActiveContextHistoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
context_history_start: usize,
) !void {
return appendActiveContextHistoryChatMessagesWithTrailingProjection(
alloc,
messages,
history,
context_history_start,
.steering_continuation,
);
}
fn appendActiveContextHistoryChatMessagesWithTrailingProjection(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
context_history_start: usize,
interrupted_projection: InterruptedChatProjection,
) !void {
if (context_history_start > history.len) {
return error.InvalidContextHistoryStart;
}
if (context_history_start == 0) {
return appendHistoryChatMessagesWithTrailingProjection(
alloc,
messages,
history,
interrupted_projection,
);
}
if (context_history_start == history.len or
history[context_history_start] != .compacted_summary)
{
return error.InvalidContextHistoryStart;
}
const checkpoint = history[context_history_start].compacted_summary;
var raw_before: usize = 0;
for (history[0..context_history_start]) |turn| {
if (turn != .compacted_summary) raw_before += 1;
}
if (checkpoint.removed_turn_count > raw_before) {
return error.InvalidContextHistoryStart;
}
const retained_count = raw_before - checkpoint.removed_turn_count;
try appendHistoryChatMessagesImpl(
alloc,
messages,
history[context_history_start .. context_history_start + 1],
.closed,
);
if (retained_count > 0) {
var retained_start = context_history_start;
var remaining = retained_count;
while (retained_start > 0 and remaining > 0) {
retained_start -= 1;
if (history[retained_start] != .compacted_summary) remaining -= 1;
}
if (remaining != 0) return error.InvalidContextHistoryStart;
for (history[retained_start..context_history_start], retained_start..) |turn, index| {
if (turn == .compacted_summary) continue;
try appendHistoryChatMessagesImpl(
alloc,
messages,
history[index .. index + 1],
.closed,
);
}
}
if (context_history_start + 1 < history.len) {
try appendHistoryChatMessagesImpl(
alloc,
messages,
history[context_history_start + 1 ..],
interrupted_projection,
);
}
}
pub fn rawHistoryTurnCount(history: []const HistoryTurn) usize {
var count: usize = 0;
for (history) |turn| if (turn != .compacted_summary) {
count += 1;
};
return count;
}
pub fn retainedHistoryTurnCountForMessageTail(
alloc: Allocator,
history: []const HistoryTurn,
wanted_messages: usize,
) !usize {
if (wanted_messages == 0) return 0;
var remaining = wanted_messages;
var retained_turns: usize = 0;
var index = history.len;
while (index > 0 and remaining > 0) {
index -= 1;
if (history[index] == .compacted_summary) continue;
var projected: std.ArrayList(core_types.ChatMessage) = .empty;
defer projected.deinit(alloc);
try appendHistoryChatMessagesImpl(
alloc,
&projected,
history[index .. index + 1],
.closed,
);
if (projected.items.len == 0) continue;
retained_turns += 1;
remaining -|= projected.items.len;
}
return retained_turns;
}
pub const RetainedHistoryTail = struct {
turn_count: usize,
message_count: usize,
};
pub fn retainedHistoryTailForMessageCount(
alloc: Allocator,
history: []const HistoryTurn,
wanted_messages: usize,
) !RetainedHistoryTail {
const turn_count = try retainedHistoryTurnCountForMessageTail(
alloc,
history,
wanted_messages,
);
if (turn_count == 0) return .{ .turn_count = 0, .message_count = 0 };
if (turn_count == rawHistoryTurnCount(history)) {
return .{ .turn_count = 0, .message_count = 0 };
}
var retained_start = history.len;
var remaining = turn_count;
while (retained_start > 0 and remaining > 0) {
retained_start -= 1;
if (history[retained_start] != .compacted_summary) remaining -= 1;
}
if (remaining != 0) return error.InvalidContextHistoryStart;
var projected: std.ArrayList(core_types.ChatMessage) = .empty;
defer projected.deinit(alloc);
_ = try appendCompactionHistoryChatMessages(
alloc,
&projected,
history[retained_start..],
0,
);
return .{
.turn_count = turn_count,
.message_count = projected.items.len,
};
}
test "retained compaction tail expands requested messages to complete raw turns" {
const alloc = std.testing.allocator;
var calls = [_]core_types.ToolCall{.{
.id = @constCast("tail-call"),
.name = @constCast("terminal"),
.arguments_json = @constCast("{\"action\":\"exec\",\"command\":\"printf tail\"}"),
}};
var results = [_]core_types.PersistedToolResult{.{
.tool_call_id = @constCast("tail-call"),
.tool_name = @constCast("terminal"),
.status = .success,
.output = @constCast("tail result"),
.output_bytes = 11,
.stored_output_bytes = 11,
}};
var steps = [_]core_types.ToolExecutionStep{.{
.assistant = @constCast("running"),
.tool_calls = &calls,
.tool_results = &results,
}};
var history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("older raw user") },
.assistant = @constCast("older raw assistant"),
} },
.{ .compacted_summary = .{
.summary = @constCast("older summary"),
.removed_turn_count = 1,
.compaction_count = 1,
} },
.{ .assistant = .{
.user = .{ .text = @constCast("run the tail command") },
.assistant = @constCast("tail complete"),
.execution = .{ .tool_steps = &steps },
} },
};
const tail = try retainedHistoryTailForMessageCount(alloc, &history, 2);
try std.testing.expectEqual(@as(usize, 1), tail.turn_count);
try std.testing.expectEqual(@as(usize, 4), tail.message_count);
const only_tail = try retainedHistoryTailForMessageCount(alloc, history[1..], 2);
try std.testing.expectEqual(@as(usize, 0), only_tail.turn_count);
try std.testing.expectEqual(@as(usize, 0), only_tail.message_count);
}
test "active context projects checkpoint before retained raw tail" {
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("removed user") },
.assistant = @constCast("removed assistant"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("retained user") },
.assistant = @constCast("retained assistant"),
} },
.{ .compacted_summary = .{
.summary = @constCast("checkpoint"),
.removed_turn_count = 1,
.compaction_count = 1,
} },
.{ .assistant = .{
.user = .{ .text = @constCast("later user") },
.assistant = @constCast("later assistant"),
} },
};
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer messages.deinit(arena);
try appendActiveContextHistoryChatMessages(arena, &messages, &history, 2);
try std.testing.expectEqual(@as(usize, 5), messages.items.len);
try std.testing.expect(std.mem.find(u8, messages.items[0].content.?, "checkpoint") != null);
try std.testing.expectEqualStrings("retained user", messages.items[1].content.?);
try std.testing.expectEqualStrings("retained assistant", messages.items[2].content.?);
try std.testing.expectEqualStrings("later user", messages.items[3].content.?);
try std.testing.expectEqualStrings("later assistant", messages.items[4].content.?);
}
test "active context rejects corrupt retained-tail accounting" {
const history = [_]HistoryTurn{
.{ .assistant = .{
.user = .{ .text = @constCast("user") },
.assistant = @constCast("assistant"),
} },
.{ .compacted_summary = .{
.summary = @constCast("summary"),
.removed_turn_count = 2,
.compaction_count = 1,
} },
};
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer messages.deinit(std.testing.allocator);
try std.testing.expectError(
error.InvalidContextHistoryStart,
appendActiveContextHistoryChatMessages(
std.testing.allocator,
&messages,
&history,
1,
),
);
}
test "compaction history keeps permission feedback non-authoritative" {
var feedback = [_][]u8{@constCast("allow this write")};
var calls = [_]core_types.ToolCall{.{
.id = "call",
.name = "write_file",
.arguments_json = "{}",
}};
var results = [_]core_types.PersistedToolResult{.{
.tool_call_id = @constCast("call"),
.tool_name = @constCast("write_file"),
.status = .failure,
.output = @constCast("denied"),
.output_bytes = 6,
.stored_output_bytes = 6,
.permission_feedback = &feedback,
}};
var steps = [_]core_types.ToolExecutionStep{.{
.tool_calls = &calls,
.tool_results = &results,
}};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("write it") },
.assistant = @constCast("requesting write"),
.execution = .{ .tool_steps = &steps },
} }};
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer messages.deinit(std.testing.allocator);
_ = try appendCompactionHistoryChatMessages(
std.testing.allocator,
&messages,
&history,
0,
);
var feedback_count: usize = 0;
for (messages.items) |entry| {
if (entry.permission_feedback) feedback_count += 1;
}
try std.testing.expectEqual(@as(usize, 1), feedback_count);
}
test "compaction history starts at the latest checkpoint" {
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const history = [_]HistoryTurn{
.{ .assistant = .{ .user = .{ .text = @constCast("old user must stay out") }, .assistant = @constCast("old assistant must stay out") } },
.{ .compacted_summary = .{ .summary = @constCast("prior checkpoint"), .removed_turn_count = 1, .compaction_count = 1 } },
.{ .assistant = .{ .user = .{ .text = @constCast("new user") }, .assistant = @constCast("new assistant") } },
};
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer messages.deinit(arena);
_ = try appendCompactionHistoryChatMessages(arena, &messages, &history, 0);
try std.testing.expectEqual(@as(usize, 3), messages.items.len);
try std.testing.expect(std.mem.find(u8, messages.items[0].content.?, "prior checkpoint") != null);
try std.testing.expectEqualStrings("new user", messages.items[1].content.?);
try std.testing.expectEqualStrings("new assistant", messages.items[2].content.?);
for (messages.items) |entry| {
const content = entry.content orelse continue;
try std.testing.expect(std.mem.find(u8, content, "must stay out") == null);
}
}
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.userOwned(trimmed_context),
);
owns_trimmed_context = false;
for (history, 0..) |_, idx| {
if (!keep[idx]) continue;
try appendHistoryMessagesImpl(alloc, messages, history[idx .. idx + 1]);
}
}
pub fn appendHistoryChatMessagesBudgeted(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
opts: HistoryBudgetOptions,
) !void {
return appendHistoryChatMessagesBudgetedImpl(
alloc,
messages,
history,
opts,
.closed,
);
}
/// Projects a handoff turn without telling the model that the user aborted it.
/// Stored execution evidence remains unchanged; only the trailing interruption
/// closure is omitted because the current prompt already carries steering intent.
pub fn appendSteeringContinuationHistoryChatMessagesBudgeted(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
opts: HistoryBudgetOptions,
) !void {
return appendHistoryChatMessagesBudgetedImpl(
alloc,
messages,
history,
opts,
.steering_continuation,
);
}
const InterruptedChatProjection = enum {
closed,
steering_continuation,
};
fn appendHistoryChatMessagesBudgetedImpl(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
opts: HistoryBudgetOptions,
trailing_interrupted_projection: InterruptedChatProjection,
) !void {
const keep = try selectBudgetedHistoryTurns(alloc, history, opts) orelse
return appendHistoryChatMessagesWithTrailingProjection(
alloc,
messages,
history,
trailing_interrupted_projection,
);
defer alloc.free(keep);
const trimmed_context = try formatBudgetTrimmedHistoryContext(alloc, history, keep);
errdefer alloc.free(trimmed_context);
try messages.append(alloc, .{ .role = .user, .content = trimmed_context });
for (history, 0..) |_, idx| {
if (!keep[idx]) continue;
try appendHistoryChatMessagesImpl(
alloc,
messages,
history[idx .. idx + 1],
if (idx + 1 == history.len)
trailing_interrupted_projection
else
.closed,
);
}
}
fn appendHistoryChatMessagesWithTrailingProjection(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
trailing_interrupted_projection: InterruptedChatProjection,
) !void {
if (history.len == 0 or trailing_interrupted_projection == .closed) {
try appendHistoryChatMessagesImpl(alloc, messages, history, .closed);
return;
}
if (history.len > 1) {
try appendHistoryChatMessagesImpl(
alloc,
messages,
history[0 .. history.len - 1],
.closed,
);
}
try appendHistoryChatMessagesImpl(
alloc,
messages,
history[history.len - 1 ..],
trailing_interrupted_projection,
);
}
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,
) !void {
for (history) |turn| {
switch (turn) {
.compacted_summary => |entry| {
const text = try formatCompactedContinuationMessage(alloc, entry.summary);
errdefer alloc.free(text);
try messages.append(alloc, 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, &.{}));
}
},
.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;
const tool_output = try formatInterruptedToolOutput(alloc, entry);
var owns_tool_output = true;
errdefer if (owns_tool_output) alloc.free(tool_output);
try messages.append(alloc, .{
.role = .tool,
.content = .{ .text = tool_output },
.tool_call_id = tool_call.id,
.tool_name = tool_call.name,
.tool_result_status = .failure,
.owns_content = true,
});
owns_tool_output = false;
} 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));
},
}
}
}
fn appendExecutionMemoryMessages(
alloc: Allocator,
messages: *std.ArrayList(message.Message),
execution: core_types.ExecutionMemory,
) !void {
var steering_index: usize = 0;
for (execution.tool_steps, 0..) |step, step_index| {
while (steering_index < execution.steering.len and
execution.steering[steering_index].after_tool_step_count == step_index)
{
const steering = execution.steering[steering_index];
if (steering.assistant_prefix) |prefix| {
if (prefix.len > 0) {
try messages.append(alloc, message.Message.assistantBorrowed(prefix, &.{}));
}
}
if (steering.text.len > 0) {
try messages.append(alloc, message.Message.userText(steering.text));
}
steering_index += 1;
}
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));
}
while (steering_index < execution.steering.len) : (steering_index += 1) {
const steering = execution.steering[steering_index];
if (steering.assistant_prefix) |prefix| {
if (prefix.len > 0) {
try messages.append(alloc, message.Message.assistantBorrowed(prefix, &.{}));
}
}
if (steering.text.len == 0) continue;
try messages.append(alloc, message.Message.userText(steering.text));
}
}
pub fn appendExecutionMemoryChatMessages(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
execution: core_types.ExecutionMemory,
) !void {
var steering_index: usize = 0;
for (execution.tool_steps, 0..) |step, step_index| {
while (steering_index < execution.steering.len and
execution.steering[steering_index].after_tool_step_count == step_index)
{
const steering = execution.steering[steering_index];
if (steering.assistant_prefix) |prefix| {
if (prefix.len > 0) {
try messages.append(alloc, .{ .role = .assistant, .content = prefix });
}
}
if (steering.text.len > 0) {
try messages.append(alloc, .{ .role = .user, .content = steering.text, .context_origin = .user_turn });
}
steering_index += 1;
}
if (step.tool_calls.len == 0 and step.provider_replay == null and step.assistant == null) continue;
try messages.append(alloc, .{
.role = .assistant,
.content = step.assistant,
.tool_calls = step.tool_calls,
.provider_replay = step.provider_replay,
.standalone_response = step.tool_calls.len == 0,
});
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,
.tool_result_memory = toolResultMemory(result),
});
}
for (step.tool_results) |result| {
for (result.permission_feedback) |feedback| {
try messages.append(alloc, .{
.role = .user,
.content = feedback,
.permission_feedback = true,
});
}
}
}
if (execution.files.len > 0) {
const text = try formatExecutionFileContext(alloc, execution.files);
errdefer alloc.free(text);
try messages.append(alloc, .{ .role = .user, .content = text });
}
while (steering_index < execution.steering.len) : (steering_index += 1) {
const steering = execution.steering[steering_index];
if (steering.assistant_prefix) |prefix| {
if (prefix.len > 0) {
try messages.append(alloc, .{ .role = .assistant, .content = prefix });
}
}
if (steering.text.len == 0) continue;
try messages.append(alloc, .{ .role = .user, .content = steering.text, .context_origin = .user_turn });
}
}
fn toolResultMemory(result: core_types.PersistedToolResult) core_types.ToolResultMemory {
return .{
.review_feedback = result.review_feedback,
.tool_images = result.tool_images,
.tool_image_handle = result.tool_image_handle,
.output_handle = result.output_handle,
.preview = result.preview,
.output_bytes = result.output_bytes,
.stored_output_bytes = result.stored_output_bytes,
.truncated = result.truncated,
.committed_file_presentation = result.committed_file_presentation,
.command_output_replay = result.command_output_replay,
.command_process_presentation = result.command_process_presentation,
.terminal_action_presentation = result.terminal_action_presentation,
};
}
fn appendHistoryChatMessagesImpl(
alloc: Allocator,
messages: *std.ArrayList(core_types.ChatMessage),
history: []const HistoryTurn,
interrupted_projection: InterruptedChatProjection,
) !void {
for (history) |turn| {
switch (turn) {
.compacted_summary => |entry| {
const text = try formatCompactedContinuationMessage(alloc, entry.summary);
errdefer alloc.free(text);
try messages.append(alloc, .{
.role = .user,
.content = text,
.context_origin = .handoff,
});
},
.assistant => |entry| {
try messages.append(alloc, .{ .role = .user, .content = entry.user.text, .images = entry.user.images, .context_origin = .user_turn });
try appendExecutionMemoryChatMessages(alloc, messages, entry.execution);
if (entry.assistant.len > 0 or entry.provider_replay != null) {
try messages.append(alloc, .{ .role = .assistant, .content = entry.assistant, .provider_replay = entry.provider_replay });
}
},
.interrupted => |entry| {
try messages.append(alloc, .{ .role = .user, .content = entry.user.text, .images = entry.user.images, .context_origin = .user_turn });
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 });
const tool_output = try formatInterruptedToolOutput(alloc, entry);
try messages.append(alloc, .{
.role = .tool,
.content = tool_output,
.tool_call_id = tool_call.id,
.tool_name = tool_call.name,
.tool_result_status = .failure,
.tool_result_memory = .{
.output_bytes = tool_output.len,
.stored_output_bytes = tool_output.len,
.truncated = false,
},
});
} else if (interrupted_projection == .steering_continuation) {
if (entry.assistant) |assistant| {
if (assistant.len > 0) {
try messages.append(alloc, .{ .role = .assistant, .content = assistant });
}
}
} else {
const assistant_content = try formatInterruptedAssistantClosedContent(alloc, entry);
try messages.append(alloc, .{ .role = .assistant, .content = assistant_content });
}
if (interrupted_projection == .steering_continuation) continue;
const text = try formatInterruptedHistoryContext(alloc, entry);
errdefer alloc.free(text);
try messages.append(alloc, .{ .role = .user, .content = text });
},
}
}
}
/// Infers a conversation language tag from text using core's script-counting heuristic.
pub fn inferConversationLanguage(text: []const u8, fallback: ConversationLanguage) ConversationLanguage {
const script = language_script.profile(text).script orelse return fallback;
return switch (script) {
.japanese => ConversationLanguage.literal("ja"),
.hangul => ConversationLanguage.literal("ko"),
.han => ConversationLanguage.literal("und-Hani"),
.arabic => ConversationLanguage.literal("und-Arab"),
.hebrew => ConversationLanguage.literal("und-Hebr"),
.cyrillic => ConversationLanguage.literal("und-Cyrl"),
.greek => ConversationLanguage.literal("und-Grek"),
.devanagari => ConversationLanguage.literal("und-Deva"),
.thai => ConversationLanguage.literal("und-Thai"),
.latin => ConversationLanguage.literal("und-Latn"),
};
}
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 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 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 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,
.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),
.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 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 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);
}
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));
}
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 "history projection keeps system role only for leading summaries" {
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(.user, 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);
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(.user, budgeted_messages.items[0].role);
try std.testing.expect(saw_nonleading_summary);
try std.testing.expect(saw_interruption_marker);
}
test "resume history projects steering before the final assistant" {
const alloc = std.testing.allocator;
var steering = [_]core_types.PersistedSteering{
.{ .text = @constCast("use file B instead"), .after_tool_step_count = 0 },
.{ .text = @constCast("keep the result concise"), .after_tool_step_count = 0 },
};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("modify file A") },
.assistant = @constCast("updated file B"),
.execution = .{ .steering = steering[0..] },
} }};
var messages: std.ArrayList(message.Message) = .empty;
defer deinitMessages(alloc, &messages);
try appendHistoryMessages(alloc, &messages, &history);
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), messages.items.len);
try std.testing.expectEqual(messages.items.len, chat_messages.items.len);
const expected_roles = [_]message.Role{ .user, .user, .user, .assistant };
const expected_text = [_][]const u8{
"modify file A",
"use file B instead",
"keep the result concise",
"updated file B",
};
for (messages.items, chat_messages.items, expected_roles, expected_text) |projected, chat, role, text| {
try std.testing.expectEqual(role, projected.role);
try std.testing.expectEqualStrings(@tagName(role), @tagName(chat.role));
try std.testing.expectEqualStrings(text, projected.content.?.asText());
try std.testing.expectEqualStrings(text, chat.content.?);
}
}
test "resume history preserves steering between tool episodes" {
const alloc = std.testing.allocator;
var first_calls = [_]ToolCall{.{
.id = "call_first",
.name = "read_file",
.arguments_json = "{\"path\":\"first\"}",
}};
var first_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_first"),
.tool_name = @constCast("read_file"),
.status = .success,
.output = @constCast("first result"),
.output_bytes = "first result".len,
.stored_output_bytes = "first result".len,
}};
var second_calls = [_]ToolCall{.{
.id = "call_second",
.name = "read_file",
.arguments_json = "{\"path\":\"second\"}",
}};
var second_results = [_]PersistedToolResult{.{
.tool_call_id = @constCast("call_second"),
.tool_name = @constCast("read_file"),
.status = .success,
.output = @constCast("second result"),
.output_bytes = "second result".len,
.stored_output_bytes = "second result".len,
}};
var steps = [_]ToolExecutionStep{
.{ .tool_calls = &first_calls, .tool_results = &first_results },
.{ .tool_calls = &second_calls, .tool_results = &second_results },
};
var steering = [_]core_types.PersistedSteering{
.{ .text = @constCast("after first"), .after_tool_step_count = 1 },
.{ .text = @constCast("after second"), .after_tool_step_count = 2 },
};
const history = [_]HistoryTurn{.{ .assistant = .{
.user = .{ .text = @constCast("run both") },
.assistant = @constCast("finished"),
.execution = .{
.tool_steps = &steps,
.steering = &steering,
},
} }};
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);
const expected_roles = [_]message.Role{
.user,
.assistant,
.tool,
.user,
.assistant,
.tool,
.user,
.assistant,
};
for (messages.items, expected_roles) |projected, role| {
try std.testing.expectEqual(role, projected.role);
}
try std.testing.expectEqualStrings("after first", messages.items[3].content.?.asText());
try std.testing.expectEqualStrings("after second", messages.items[6].content.?.asText());
}
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 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);
}
test "execution token estimate includes permission feedback" {
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..] };
try std.testing.expect(estimateExecutionTokens(with_feedback) > estimateExecutionTokens(without_feedback));
}
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.user, 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.user, 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..],
},
} },
.{ .assistant = .{
.user = .{
.text = @constCast("run retained"),
.images = background_images[0..],
},
.assistant = @constCast("server ready"),
.execution = .{
.tool_steps = retained_steps[0..],
.files = retained_files[0..],
},
} },
.{ .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_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_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_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 "active context snapshot preserves large result handles exactly" {
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.agent.history.items[0].assistant.execution.tool_steps[0].tool_calls[0].id,
);
try std.testing.expectEqualStrings(
"result-call_large-abc.txt",
runtime.agent.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, 3), context.len);
try std.testing.expectEqual(HistoryTurn.assistant, std.meta.activeTag(context[0]));
try std.testing.expectEqualStrings(
"result-call_large-abc.txt",
context[0].assistant.execution.tool_steps[0].tool_results[0].output_handle.?,
);
try std.testing.expectEqualStrings(
"important preview",
context[0].assistant.execution.tool_steps[0].tool_results[0].preview.?,
);
}
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.expect(
std.mem.find(u8, messages.items[2].content.?.asText(), aborted_tool_output) != null,
);
try std.testing.expectEqual(
@as(usize, 1),
std.mem.count(u8, messages.items[2].content.?.asText(), replay_sentinel),
);
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(), 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.expect(
std.mem.find(u8, chat_messages.items[2].content.?, aborted_tool_output) != null,
);
try std.testing.expectEqual(
@as(usize, 1),
std.mem.count(u8, chat_messages.items[2].content.?, replay_sentinel),
);
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, artifact_sentinel) == null);
}
}
}
test "interrupted tool diagnostics remain complete for compaction" {
const compaction = @import("../agent/runtime/context_compaction.zig");
const replay_handle = "cancelled-output.bin";
const oversized_handle = "h" ** 129;
const presentations = [_]?CancelledCommandPresentation{
null,
.{},
.{ .output_replay = .unavailable },
.{ .output_replay = .{ .available = .{ .handle = replay_handle, .framed_bytes = 42 } } },
.{ .output_replay = .{ .available = .{ .handle = oversized_handle, .framed_bytes = 42 } } },
};
for (presentations, 0..) |presentation, index| {
for ([_]InterruptedTerminalReason{ .cancelled, .failed }) |reason| {
for ([_]InterruptedChatProjection{ .closed, .steering_continuation }) |projection| {
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const alloc = arena_state.allocator();
const history = [_]HistoryTurn{.{ .interrupted = .{
.user = .{ .text = @constCast("Stop the current tool.") },
.tool_call = .{ .id = "cancelled-call", .name = "subagent", .arguments_json = "{}" },
.cancelled_command = presentation,
.terminal_reason = reason,
} }};
var messages: std.ArrayList(core_types.ChatMessage) = .empty;
try appendHistoryChatMessagesImpl(alloc, &messages, &history, projection);
// Compaction must accept the complete diagnostic, not claim the tool completed.
try compaction.promoteMessageResults(alloc, messages.items, .unavailable, 0);
const result = messages.items[2];
try std.testing.expectEqual(.failure, result.tool_result_status.?);
try std.testing.expectEqualStrings("cancelled-call", result.tool_call_id.?);
const memory = result.tool_result_memory.?;
try std.testing.expectEqual(result.content.?.len, memory.output_bytes);
try std.testing.expectEqual(result.content.?.len, memory.stored_output_bytes);
try std.testing.expect(!memory.truncated);
try std.testing.expect(memory.output_handle == null);
try std.testing.expect(std.mem.startsWith(u8, result.content.?, aborted_tool_output));
try std.testing.expectEqual(@as(usize, if (index == 3) 1 else 0), std.mem.count(u8, result.content.?, replay_handle));
try std.testing.expect(std.mem.find(u8, result.content.?, oversized_handle) == null);
try std.testing.expectEqual(reason, history[0].interrupted.terminal_reason);
}
}
}
}
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 "steering continuation omits only the trailing interrupted closure" {
const alloc = std.testing.allocator;
const history = [_]HistoryTurn{
.{ .interrupted = .{
.user = .{ .text = @constCast("older interrupted request") },
.assistant = @constCast("older partial"),
} },
.{ .interrupted = .{
.user = .{ .text = @constCast("active request") },
.assistant = @constCast("active partial"),
} },
};
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 appendSteeringContinuationHistoryChatMessagesBudgeted(
arena,
&messages,
&history,
.{},
);
try std.testing.expectEqual(@as(usize, 5), messages.items.len);
try std.testing.expect(std.mem.find(u8, messages.items[2].content.?, "") != null);
try std.testing.expectEqual(core_types.ChatRole.user, messages.items[3].role);
try std.testing.expectEqualStrings("active request", messages.items[3].content.?);
try std.testing.expectEqual(core_types.ChatRole.assistant, messages.items[4].role);
try std.testing.expectEqualStrings("active partial", messages.items[4].content.?);
try std.testing.expect(std.mem.find(u8, messages.items[4].content.?, interrupted_before_completion_output) == 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("glob_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: glob_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: glob_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.agent.history.items[0].assistant.user.text);
try std.testing.expectEqualStrings("reply one", runtime.agent.history.items[0].assistant.assistant);
try std.testing.expectEqualStrings("three", runtime.agent.history.items[2].assistant.user.text);
try std.testing.expectEqualStrings("reply three", runtime.agent.history.items[2].assistant.assistant);
try std.testing.expect(runtime.agent.history.items[0].assistant.user.text.ptr != restore_history[0].assistant.user.text.ptr);
try std.testing.expect(runtime.agent.history.items[2].assistant.assistant.ptr != restore_history[2].assistant.assistant.ptr);
}
test "collectReferencedShellIds extracts raw and escaped envelopes from restored history" {
const alloc = std.testing.allocator;
var replay_turn = try makeAssistantTurn(alloc, "restart the server", "starting it");
replay_turn.assistant.provider_replay = .{
.source = .{ .provider = .gateway, .model = try alloc.dupe(u8, "test") },
.parts_json = try alloc.dupe(
u8,
"[{\"type\":\"tool_result\",\"content\":\"{\\\"session_id\\\":\\\"shell-3\\\",\\\"state\\\":\\\"running\\\"}\"}," ++
"{\"type\":\"tool_result\",\"content\":\"{\\\"session_id\\\":\\\"shell-3\\\"}\"}," ++
"{\"type\":\"tool_result\",\"content\":\"{\\\"session_id\\\":null}\"}]",
),
};
const tool_turn: HistoryTurn = .{ .interrupted = .{
.user = .{ .text = try alloc.dupe(u8, "stop it") },
.tool_call = .{
.id = try alloc.dupe(u8, "call-1"),
.name = try alloc.dupe(u8, "shell"),
.arguments_json = try alloc.dupe(u8, "{\"action\":\"stop\",\"session_id\":\"shell-7\"}"),
.provider_result = try alloc.dupe(u8, "{\"session_id\":\"shell-X9ab_cd-12\",\"state\":\"running\"}"),
},
} };
const summary_turn: HistoryTurn = .{ .compacted_summary = .{
.summary = try alloc.dupe(u8, "prose mentioning shell-9 without an envelope"),
.removed_turn_count = 1,
.compaction_count = 1,
} };
const history = [_]HistoryTurn{ replay_turn, tool_turn, summary_turn };
defer {
for (history) |turn| freeHistoryTurn(alloc, turn);
}
var ids: std.ArrayList([]const u8) = .empty;
defer ids.deinit(alloc);
try collectReferencedShellIds(alloc, &history, &ids);
try std.testing.expectEqual(@as(usize, 3), ids.items.len);
try std.testing.expectEqualStrings("shell-3", ids.items[0]);
try std.testing.expectEqualStrings("shell-7", ids.items[1]);
try std.testing.expectEqualStrings("shell-X9ab_cd-12", ids.items[2]);
}
test "collectReferencedShellIds scans persisted tool step previews" {
const alloc = std.testing.allocator;
var turn = try makeAssistantTurn(alloc, "check the watcher", "still running");
defer freeHistoryTurn(alloc, turn);
const envelope = "{\"session_id\":\"shell-4\",\"state\":\"running\"}";
const results = try alloc.alloc(core_types.PersistedToolResult, 1);
results[0] = .{
.tool_call_id = try alloc.dupe(u8, "call-1"),
.tool_name = try alloc.dupe(u8, "shell"),
.status = .success,
.output = try alloc.dupe(u8, envelope),
.output_bytes = envelope.len,
.stored_output_bytes = envelope.len,
.preview = try alloc.dupe(u8, envelope),
};
const steps = try alloc.alloc(core_types.ToolExecutionStep, 1);
steps[0] = .{ .tool_results = results };
turn.assistant.execution = .{ .tool_steps = steps };
const history = [_]HistoryTurn{turn};
var ids: std.ArrayList([]const u8) = .empty;
defer ids.deinit(alloc);
try collectReferencedShellIds(alloc, &history, &ids);
try std.testing.expectEqual(@as(usize, 1), ids.items.len);
try std.testing.expectEqualStrings("shell-4", ids.items[0]);
}
test "collectReferencedShellIds scans inline tool outputs without previews" {
const alloc = std.testing.allocator;
var turn = try makeAssistantTurn(alloc, "start the server", "running");
defer freeHistoryTurn(alloc, turn);
const envelope = "{\"session_id\":\"shell-11\",\"state\":\"running\"}";
const results = try alloc.alloc(core_types.PersistedToolResult, 1);
results[0] = .{
.tool_call_id = try alloc.dupe(u8, "call-1"),
.tool_name = try alloc.dupe(u8, "shell"),
.status = .success,
.output = try alloc.dupe(u8, envelope),
.output_bytes = envelope.len,
.stored_output_bytes = envelope.len,
};
const steps = try alloc.alloc(core_types.ToolExecutionStep, 1);
steps[0] = .{ .tool_results = results };
turn.assistant.execution = .{ .tool_steps = steps };
const history = [_]HistoryTurn{turn};
var ids: std.ArrayList([]const u8) = .empty;
defer ids.deinit(alloc);
try collectReferencedShellIds(alloc, &history, &ids);
try std.testing.expectEqual(@as(usize, 1), ids.items.len);
try std.testing.expectEqualStrings("shell-11", ids.items[0]);
}
test "collectReferencedShellIds yields nothing for shell-free history" {
const alloc = std.testing.allocator;
const history = [_]HistoryTurn{try makeAssistantTurn(alloc, "hello", "hi")};
defer {
for (history) |turn| freeHistoryTurn(alloc, turn);
}
var ids: std.ArrayList([]const u8) = .empty;
defer ids.deinit(alloc);
try collectReferencedShellIds(alloc, &history, &ids);
try std.testing.expectEqual(@as(usize, 0), ids.items.len);
}
test "SessionRuntime restore and reset clear stale shell handle state" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 2 };
defer runtime.deinit(alloc);
runtime.has_stale_shell_handles = true;
const restore_history = [_]HistoryTurn{try makeAssistantTurn(alloc, "one", "reply one")};
defer {
for (restore_history) |turn| freeHistoryTurn(alloc, turn);
}
try runtime.restore(alloc, ConversationLanguage.literal("en"), &restore_history);
try std.testing.expect(!runtime.has_stale_shell_handles);
runtime.has_stale_shell_handles = true;
runtime.reset(alloc);
try std.testing.expect(!runtime.has_stale_shell_handles);
}
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.agent.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 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 remains conversational context" {
const alloc = std.testing.allocator;
const context = [_]HistoryTurn{
.{ .compacted_summary = .{
.summary = @constCast(("a" ** 155) ++ "★"),
.removed_turn_count = 2,
.compaction_count = 1,
} },
.{ .assistant = .{
.user = .{ .text = @constCast("preserved user") },
.assistant = @constCast("preserved reply"),
} },
};
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" });
try std.testing.expectEqual(core_types.ChatRole.user, messages.items[0].role);
try std.testing.expect(messages.items[0].content != null);
try std.testing.expect(std.unicode.utf8ValidateSlice(messages.items[0].content.?));
try std.testing.expectEqualStrings("preserved user", messages.items[1].content.?);
}
test "SessionRuntime context snapshot preserves the active history exactly" {
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.agent.history.items[0].assistant.user.text);
try std.testing.expectEqualStrings("five", runtime.agent.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, 5), context.len);
try std.testing.expectEqualStrings("one", context[0].assistant.user.text);
try std.testing.expectEqualStrings("three", context[2].assistant.user.text);
try std.testing.expectEqualStrings("five", context[4].assistant.user.text);
}
test "accepted semantic checkpoint releases summarized model memory" {
const alloc = std.testing.allocator;
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.appendAssistantHistoryTurn(alloc, "first exact prompt", "first exact reply");
try runtime.appendAssistantHistoryTurn(alloc, "second exact prompt", "second exact reply");
try runtime.appendHistoryEntry(alloc, .{ .compacted_summary = .{
.summary = @constCast("\n# Objective\nContinue exact work.\n"),
.removed_turn_count = 2,
.compaction_count = 1,
} });
try std.testing.expectEqual(@as(usize, 1), runtime.historyLen());
try std.testing.expect(runtime.agent.history.items[0] == .compacted_summary);
const compacted = try runtime.snapshotHistory(alloc);
defer freeHistoryTurnSlice(alloc, compacted);
try std.testing.expectEqual(@as(usize, 1), compacted.len);
var projected_messages: std.ArrayList(core_types.ChatMessage) = .empty;
defer projected_messages.deinit(alloc);
try appendActiveContextHistoryChatMessages(
alloc,
&projected_messages,
compacted,
0,
);
defer alloc.free(@constCast(projected_messages.items[0].content.?));
try std.testing.expectEqual(core_types.ChatRole.user, projected_messages.items[0].role);
try runtime.appendAssistantHistoryTurn(alloc, "post-checkpoint prompt", "post-checkpoint reply");
const continued = try runtime.snapshotHistory(alloc);
defer freeHistoryTurnSlice(alloc, continued);
try std.testing.expectEqual(@as(usize, 2), continued.len);
try std.testing.expect(continued[0] == .compacted_summary);
try std.testing.expectEqualStrings("post-checkpoint prompt", continued[1].assistant.user.text);
}
test "restored history keeps an in-memory unversioned prefix boundary" {
const alloc = std.testing.allocator;
const restored_history = [_]HistoryTurn{
try makeAssistantTurn(alloc, "legacy one", "legacy reply one"),
try makeAssistantTurn(alloc, "legacy two", "legacy reply two"),
};
defer for (restored_history) |turn| freeHistoryTurn(alloc, turn);
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.restore(
alloc,
ConversationLanguage.literal("en"),
&restored_history,
);
try std.testing.expectEqual(@as(usize, 2), runtime.unversionedHistoryEnd());
try runtime.appendAssistantHistoryTurn(alloc, "fresh", "fresh reply");
try std.testing.expectEqual(@as(usize, 2), runtime.unversionedHistoryEnd());
runtime.reset(alloc);
try std.testing.expectEqual(@as(usize, 0), runtime.unversionedHistoryEnd());
}
test "current checkpoint clears restored unversioned history provenance" {
const alloc = std.testing.allocator;
const restored_history = [_]HistoryTurn{
try makeAssistantTurn(alloc, "legacy prompt", "legacy reply"),
.{ .compacted_summary = .{
.summary = try alloc.dupe(
u8,
"\ncurrent checkpoint\n",
),
.removed_turn_count = 1,
.compaction_count = 1,
} },
};
defer for (restored_history) |turn| freeHistoryTurn(alloc, turn);
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.restore(
alloc,
ConversationLanguage.literal("en"),
restored_history[1..],
);
try std.testing.expectEqual(@as(usize, 0), runtime.unversionedHistoryEnd());
try runtime.appendAssistantHistoryTurn(alloc, "fresh", "fresh reply");
try std.testing.expectEqual(@as(usize, 0), runtime.unversionedHistoryEnd());
}
test "legacy checkpoint keeps restored provenance conservative" {
const alloc = std.testing.allocator;
const restored_history = [_]HistoryTurn{
try makeAssistantTurn(alloc, "legacy prompt", "legacy reply"),
.{ .compacted_summary = .{
.summary = try alloc.dupe(u8, "legacy free-form summary"),
.removed_turn_count = 1,
.compaction_count = 1,
} },
};
defer for (restored_history) |turn| freeHistoryTurn(alloc, turn);
var runtime: SessionRuntime = .{ .max_history_turns = 8 };
defer runtime.deinit(alloc);
try runtime.restore(
alloc,
ConversationLanguage.literal("en"),
&restored_history,
);
try std.testing.expectEqual(@as(usize, 2), runtime.unversionedHistoryEnd());
}
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");
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.expectEqualStrings(
"first prompt",
runtime.agent.history.items[0].assistant.user.text,
);
try std.testing.expectEqualStrings(
"second prompt",
runtime.agent.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].assistant.user.text,
);
try std.testing.expectEqualStrings(
"background assistant",
canonical[1].assistant.assistant,
);
try std.testing.expectEqual(@as(usize, 1), canonical[1].assistant.execution.tool_steps.len);
try std.testing.expectEqualStrings(
"checking preserved evidence",
canonical[1].assistant.execution.tool_steps[0].assistant.?,
);
try std.testing.expectEqual(@as(usize, 1), canonical[1].assistant.execution.tool_steps[0].tool_calls.len);
try std.testing.expectEqualStrings(
"call_preserved",
canonical[1].assistant.execution.tool_steps[0].tool_calls[0].id,
);
try std.testing.expectEqualStrings(
"read_file",
canonical[1].assistant.execution.tool_steps[0].tool_calls[0].name,
);
try std.testing.expectEqualStrings(
"{\"path\":\"fixture.txt\"}",
canonical[1].assistant.execution.tool_steps[0].tool_calls[0].arguments_json,
);
try std.testing.expectEqual(@as(usize, 1), canonical[1].assistant.execution.tool_steps[0].tool_results.len);
try std.testing.expectEqual(
PersistedToolStatus.failure,
canonical[1].assistant.execution.tool_steps[0].tool_results[0].status,
);
try std.testing.expectEqualStrings(
"preserved failure",
canonical[1].assistant.execution.tool_steps[0].tool_results[0].output,
);
try std.testing.expectEqualStrings(
"result-call_preserved.txt",
canonical[1].assistant.execution.tool_steps[0].tool_results[0].output_handle.?,
);
try std.testing.expectEqualStrings(
"preserved preview",
canonical[1].assistant.execution.tool_steps[0].tool_results[0].preview.?,
);
try std.testing.expectEqual(@as(usize, 17), canonical[1].assistant.execution.tool_steps[0].tool_results[0].output_bytes);
try std.testing.expectEqual(@as(usize, 17), canonical[1].assistant.execution.tool_steps[0].tool_results[0].stored_output_bytes);
try std.testing.expectEqual(@as(usize, 1), canonical[1].assistant.execution.files.len);
try std.testing.expectEqualStrings(
"src/preserved.zig",
canonical[1].assistant.execution.files[0].path,
);
try std.testing.expectEqualStrings(
"src/preserved-renamed.zig",
canonical[1].assistant.execution.files[0].new_path.?,
);
try std.testing.expectEqualStrings(
"call_preserved",
canonical[1].assistant.execution.files[0].tool_call_id,
);
try std.testing.expectEqualStrings(
"read_file",
canonical[1].assistant.execution.files[0].tool_name,
);
try std.testing.expectEqual(
FileEvidenceAction.rename,
canonical[1].assistant.execution.files[0].action,
);
try std.testing.expectEqual(
PersistedToolStatus.failure,
canonical[1].assistant.execution.files[0].status,
);
try std.testing.expect(canonical[1].assistant.execution.files[0].model_view_covers_full_file);
try std.testing.expect(canonical[1].assistant.execution.files[0].stale);
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"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("canonical background") },
.assistant = @constCast("background assistant"),
.execution = .{
.tool_steps = steps[0..],
.files = files[0..],
},
} },
.{ .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 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");
const direct = try snapshotOwnedContextHistory(
alloc,
runtime.agent.history.items,
0,
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.expectEqual(@as(usize, 3), direct.len);
try std.testing.expectEqualStrings("one", direct[0].assistant.user.text);
try std.testing.expectEqualStrings("three", direct[2].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.agent.history.items[0].assistant.execution.tool_steps[0].tool_calls[0].id,
);
try std.testing.expectEqualStrings(
"first result sentinel",
runtime.agent.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, 9), context.len);
try std.testing.expectEqualStrings("first prompt", context[0].assistant.user.text);
try std.testing.expectEqualStrings("prompt 5", context[5].assistant.user.text);
try std.testing.expectEqualStrings("prompt 8", context[8].assistant.user.text);
try std.testing.expectEqual(@as(usize, 9), runtime.historyLen());
try std.testing.expectEqualStrings(
"call_first",
runtime.agent.history.items[0].assistant.execution.tool_steps[0].tool_calls[0].id,
);
}
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.appendAssistantHistoryTurn(alloc, "run", "historical command");
const snapshot = try runtime.snapshotHistory(alloc);
defer freeHistoryTurnSlice(alloc, snapshot);
try std.testing.expect(snapshot[0].assistant.user.text.ptr != runtime.agent.history.items[0].assistant.user.text.ptr);
try std.testing.expect(snapshot[1].assistant.assistant.ptr != runtime.agent.history.items[1].assistant.assistant.ptr);
runtime.clearHistory(alloc);
try std.testing.expectEqualStrings("hello", snapshot[0].assistant.user.text);
try std.testing.expectEqualStrings("historical command", snapshot[1].assistant.assistant);
}
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.agent.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 "active context and image catalog retain historical images" {
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, 2), budgeted.len);
try std.testing.expectEqual(@as(usize, 1), 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);
}
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("second summary", summary);
try std.testing.expect(reply.ptr == runtime.agent.history.items[3].assistant.assistant.ptr);
try std.testing.expect(summary.ptr == runtime.agent.history.items[2].compacted_summary.summary.ptr);
}
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"),
&.{},
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 "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"),
} },
.{ .assistant = .{
.user = .{ .text = @constCast("run dev") },
.assistant = @constCast("historical command"),
} },
};
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);
}
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 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);
}
test "image catalog merges a retained recovery image and rejects changed identity" {
const alloc = std.testing.allocator;
var images = [_]ImageAttachment{.{
.id = 7,
.path = @constCast("/missing/original.png"),
.media_type = @constCast("image/png"),
.snapshot_path = @constCast("/session/images/saved.bin"),
.snapshot_sha256 = @constCast("a" ** 64),
}};
const turn: HistoryTurn = .{ .interrupted = .{ .user = .{ .text = @constCast("recover"), .images = &images } } };
const catalog = try collect_image_catalog(alloc, &.{turn}, &.{});
defer core_types.freeImageAttachmentSlice(alloc, catalog);
const merged = try merge_image_catalog_history_turn(alloc, catalog, turn);
defer core_types.freeImageAttachmentSlice(alloc, merged);
try std.testing.expectEqual(@as(usize, 1), merged.len);
try std.testing.expectEqual(@as(usize, 7), merged[0].id);
images[0].snapshot_sha256 = @constCast("b" ** 64);
try std.testing.expectError(error.StaleImageCatalog, merge_image_catalog_history_turn(alloc, catalog, turn));
}