const std = @import("std"); const question_answer = @import("../agent/question_answer.zig"); const config_runtime = @import("../config/config_runtime.zig"); const model_capabilities = @import("../config/model_capabilities.zig"); const assistant_presentation = @import("../agent/assistant_presentation.zig"); const tool_presentation = @import("../agent/runtime/tool_presentation.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const runtime_profile = @import("../hosts/runtime_profile.zig"); const host_target = @import("../hosts/target.zig"); const diff = @import("../output/diff.zig"); const diagnostics = @import("../workspace/diagnostics.zig"); const app_lifecycle = @import("app_lifecycle.zig"); const input_completion_runtime = @import("input_completion_runtime.zig"); const input_queue_runtime = @import("input_queue_runtime.zig"); const image_attachments = @import("../images/image_attachments.zig"); const core_input_runtime = @import("../input/runtime.zig"); const io_mod = @import("../shared/io.zig"); const list_window = @import("../shared/list_window.zig"); const session_runtime = @import("../session/session.zig"); const session_catalog = @import("../session/session_catalog.zig"); const session_codec = @import("../session/session_codec.zig"); const js_host_session_store = @import("../session/js_host_session_store.zig"); const session_event = @import("../session/session_event.zig"); const session_usage = @import("../session/session_usage.zig"); const session_child_store = @import("../session/session_child_store.zig"); const result_store = @import("../session/result_store.zig"); const command_replay_store = @import("../session/command_replay_store.zig"); const command_output_content = @import("../tooling/command_output_content.zig"); const captured_command = @import("../tooling/captured_command.zig"); const session_display_metadata = @import("../session/session_display_metadata.zig"); const session_log = @import("../session/session_log.zig"); const session_resume_view = @import("../session/session_resume_view.zig"); const session_store = @import("../session/session_store.zig"); const session_summary_codec = @import("../session/session_summary_codec.zig"); const subagent_tool_host = @import("../subagent/tool_host.zig"); const subagent_authority = @import("../subagent/authority.zig"); const subagent_resume_admission = @import("../subagent/resume_admission.zig"); const tool_set_contract = @import("../tooling/tool_set.zig"); const builtin_tools = @import("../../builtins/tools.zig"); const types = @import("../shared/types.zig"); const permissions = @import("../permissions/permissions.zig"); const session_permission_state = @import("../permissions/session_permission_state.zig"); const mcp_access = @import("../mcp/access_policy.zig"); const shell_runtime = @import("../../ui/shell_runtime.zig"); const transcript_runtime = @import("../../ui/transcript/runtime.zig"); const ui_input = @import("../../ui/input/runtime.zig"); const ui_render = @import("../../ui/render.zig"); const Allocator = std.mem.Allocator; const BackgroundSessionPolicy = enum { carry_forward, stop_forget, }; fn nextImageIdForResumedHistory( alloc: Allocator, history: []const types.HistoryTurn, ) !usize { const restored_catalog = try session_runtime.collect_image_catalog( alloc, history, &.{}, ); defer types.freeImageAttachmentSlice(alloc, restored_catalog); const bounds = try image_attachments.calculate_next_image_id(restored_catalog); return bounds.next_id; } pub const SessionPickerScope = session_catalog.Scope; pub const HistoryAppendOutcome = enum { uncommitted, committed, committed_degraded, }; pub const ResumeHandoffIntent = enum { none, requested, upgrade_requested, }; const ResumeNotice = union(enum) { session, upgrade: []const u8, }; pub const ResumeViewStage = union(enum) { none, ready: u32, }; pub const ResumeHandoffState = struct { intent: ResumeHandoffIntent, has_writable_session: bool, is_pristine: bool, has_unresolved_degraded_tail: bool, }; pub fn shouldCreateResumeHandoff(state: ResumeHandoffState) bool { return state.intent != .none and state.has_writable_session and (!state.is_pristine or state.intent == .upgrade_requested) and !state.has_unresolved_degraded_tail; } test "resume handoff policy requires requested durable non-pristine state" { const cases = [_]struct { state: ResumeHandoffState, expected: bool, }{ .{ .state = .{ .intent = .requested, .has_writable_session = true, .is_pristine = false, .has_unresolved_degraded_tail = false, }, .expected = true, }, .{ .state = .{ .intent = .none, .has_writable_session = true, .is_pristine = false, .has_unresolved_degraded_tail = false, }, .expected = false, }, .{ .state = .{ .intent = .requested, .has_writable_session = false, .is_pristine = false, .has_unresolved_degraded_tail = false, }, .expected = false, }, .{ .state = .{ .intent = .upgrade_requested, .has_writable_session = true, .is_pristine = true, .has_unresolved_degraded_tail = false, }, .expected = true, }, .{ .state = .{ .intent = .requested, .has_writable_session = true, .is_pristine = true, .has_unresolved_degraded_tail = false, }, .expected = false, }, .{ .state = .{ .intent = .requested, .has_writable_session = true, .is_pristine = false, .has_unresolved_degraded_tail = true, }, .expected = false, }, }; for (cases) |case| { try std.testing.expectEqual(case.expected, shouldCreateResumeHandoff(case.state)); } } /// Owns `session_id`; callers must release it with `deinit`. pub const ResumeHandoff = struct { session_id: []u8, pub fn deinit(self: *ResumeHandoff, alloc: Allocator) void { alloc.free(self.session_id); self.* = undefined; } }; pub const SessionPreferencePatch = struct { model: ?[]const u8 = null, effort: ?types.ReasoningEffort = null, fast_mode: ?bool = null, fn userSettingsPatch(self: SessionPreferencePatch) config_runtime.UserSettingsPatch { return .{ .model = self.model, .effort = self.effort, .fast_mode = self.fast_mode, }; } }; pub const PreferenceCommitResult = struct { settings_outcome: ?config_runtime.CommitOutcome = null, settings_error: ?anyerror = null, settings_failure_cleanup: config_runtime.LegacyCleanup = .{}, session_error: ?anyerror = null, pub fn deinit(self: *PreferenceCommitResult, alloc: Allocator) void { if (self.settings_outcome) |*outcome| outcome.deinit(alloc); self.settings_failure_cleanup.deinit(alloc); self.* = undefined; } }; pub const SessionPicker = struct { active: bool = false, load_state: session_catalog.LoadState = .loading, generation: u64 = 0, has_more: bool = false, loading_more: bool = false, summaries: std.ArrayList(session_store.SessionSummary) = .empty, selected: usize = 0, window_start: usize = 0, scope: SessionPickerScope = .current_workspace, query_buf: [256]u8 = undefined, query_len: usize = 0, selection_failure: ?session_catalog.ResumeFailure = null, pub fn deinit(self: *SessionPicker, alloc: Allocator) void { for (self.summaries.items) |*summary| summary.deinit(alloc); self.summaries.deinit(alloc); self.* = .{}; } fn appendSummary( self: *SessionPicker, alloc: Allocator, source: *const session_store.SessionSummary, ) !void { if (source.history_len == 0 and !source.has_managed_children) return; var summary = try session_summary_codec.cloneSessionSummary(alloc, source.*); errdefer summary.deinit(alloc); try self.summaries.append(alloc, summary); } pub fn selectedId(self: *const SessionPicker) ?[]const u8 { if (!self.active or self.load_state != .ready) return null; if (self.selected >= self.filteredItemCount()) return null; const summary = session_catalog.summaryAt(self.summaries.items, self.query(), self.selected) orelse return null; return summary.id; } fn isLoading(self: *const SessionPicker) bool { return self.active and self.load_state == .loading; } pub fn query(self: *const SessionPicker) []const u8 { return self.query_buf[0..self.query_len]; } pub fn setQuery(self: *SessionPicker, query_text: []const u8) void { const len = @min(query_text.len, self.query_buf.len); if (len > 0) std.mem.copyForwards(u8, self.query_buf[0..len], query_text[0..len]); self.query_len = len; self.selected = 0; self.window_start = 0; self.selection_failure = null; } pub fn filteredItemCount(self: *const SessionPicker) usize { return session_catalog.filteredCount(self.summaries.items, self.query()); } pub fn navigationItemCount(self: *const SessionPicker) usize { return self.filteredItemCount() + @intFromBool(self.has_more); } fn isLoadMoreSelected(self: *const SessionPicker) bool { return self.active and self.load_state == .ready and self.has_more and self.selected == self.filteredItemCount(); } fn moveVisibleItems(self: *SessionPicker, delta: i32, visible_items: u16) bool { if (!self.active or self.load_state != .ready) return false; self.selection_failure = null; const count = self.navigationItemCount(); if (count == 0) return true; // Clamp at both ends instead of wrapping: at the top the selection // stays on the first session, at the bottom on the last row. const current: i32 = @intCast(self.selected % count); var next = current + delta; if (next < 0) next = 0; if (next >= @as(i32, @intCast(count))) next = @as(i32, @intCast(count)) - 1; self.selected = @intCast(next); self.syncWindowStart(visible_items); return true; } pub fn syncWindowStart(self: *SessionPicker, visible_items: u16) void { const session_count = self.filteredItemCount(); const action_items: u16 = @intFromBool(self.has_more and visible_items > 0); const visible_sessions = visible_items -| action_items; if (session_count == 0 or visible_sessions == 0) { self.window_start = 0; return; } if (self.selected >= session_count) { self.window_start = session_count -| visible_sessions; return; } self.window_start = list_window.updateEdgeStart( self.window_start, session_count, self.selected, visible_sessions, ); } fn appendPage( self: *SessionPicker, alloc: Allocator, summaries: []const session_store.SessionSummary, ) !void { const summary_len = self.summaries.items.len; errdefer { while (self.summaries.items.len > summary_len) { if (self.summaries.pop()) |summary_value| { var summary = summary_value; summary.deinit(alloc); } } } for (summaries) |*summary| try self.appendSummary(alloc, summary); } }; const session_picker_cache_ttl_ns: i128 = 5 * std.time.ns_per_s; const SessionPickerPageCache = struct { ready: bool = false, active_id: ?[]u8 = null, limit: usize = 0, loaded_at_ns: i128 = 0, page: session_store.ResumableSessionPage = .{}, fn deinit(self: *SessionPickerPageCache) void { const alloc = std.heap.c_allocator; if (self.active_id) |id| alloc.free(id); self.page.deinit(alloc); self.* = .{}; } fn matches( self: *const SessionPickerPageCache, active_id: ?[]const u8, limit: usize, ) bool { return self.ready and self.limit >= limit and optionalStringEql(self.active_id, active_id); } fn isFresh(self: *const SessionPickerPageCache, now_ns: i128) bool { return self.ready and now_ns >= self.loaded_at_ns and now_ns - self.loaded_at_ns <= session_picker_cache_ttl_ns; } fn install( self: *SessionPickerPageCache, source: *const session_store.ResumableSessionPage, active_id: ?[]const u8, limit: usize, loaded_at_ns: i128, ) !void { const alloc = std.heap.c_allocator; var replacement: SessionPickerPageCache = .{ .ready = true, .active_id = if (active_id) |id| try alloc.dupe(u8, id) else null, .limit = limit, .loaded_at_ns = loaded_at_ns, .page = .{ .has_more = source.has_more }, }; errdefer replacement.deinit(); for (source.summaries.items) |summary| { var copied = try session_summary_codec.cloneSessionSummary(alloc, summary); replacement.page.summaries.append(alloc, copied) catch |err| { copied.deinit(alloc); return err; }; } self.deinit(); self.* = replacement; } }; fn optionalStringEql(a: ?[]const u8, b: ?[]const u8) bool { if (a == null or b == null) return a == null and b == null; return std.mem.eql(u8, a.?, b.?); } fn sessionPickerCacheForScope( persistence: *Persistence, scope: SessionPickerScope, ) *SessionPickerPageCache { return switch (scope) { .current_workspace => &persistence.session_picker_current_cache, .all_workspaces => &persistence.session_picker_all_cache, }; } fn replaceSessionPickerPage( picker: *SessionPicker, alloc: Allocator, source: *const session_store.ResumableSessionPage, ) !void { var replacement: std.ArrayList(session_store.SessionSummary) = .empty; errdefer { for (replacement.items) |*summary| summary.deinit(alloc); replacement.deinit(alloc); } for (source.summaries.items) |*summary| { if (summary.history_len == 0 and !summary.has_managed_children) continue; var copied = try session_summary_codec.cloneSessionSummary(alloc, summary.*); replacement.append(alloc, copied) catch |err| { copied.deinit(alloc); return err; }; } for (picker.summaries.items) |*summary| summary.deinit(alloc); picker.summaries.deinit(alloc); picker.summaries = replacement; } fn selectionAfterLoadedPage( previous_filtered_count: usize, filtered_count: usize, has_more: bool, ) usize { if (filtered_count > previous_filtered_count) return previous_filtered_count; if (has_more) return filtered_count; return filtered_count -| 1; } const SessionPickerLoad = struct { const Continuation = struct { updated_at_ms: i64, id: []u8, fn deinit(self: *Continuation) void { std.heap.c_allocator.free(self.id); self.* = undefined; } }; const PageRequest = struct { generation: u64, scope: SessionPickerScope, active_id: ?[]u8 = null, continuation: ?Continuation = null, limit: usize = session_store.default_resume_page_limit, fn init( generation: u64, scope: SessionPickerScope, active_id: ?[]const u8, continuation: ?session_store.ResumableSessionContinuation, limit: usize, ) !PageRequest { const alloc = std.heap.c_allocator; var request: PageRequest = .{ .generation = generation, .scope = scope, .limit = limit }; errdefer request.deinit(); if (active_id) |id| request.active_id = try alloc.dupe(u8, id); if (continuation) |position| { request.continuation = .{ .updated_at_ms = position.updated_at_ms, .id = try alloc.dupe(u8, position.id), }; } return request; } fn deinit(self: *PageRequest) void { if (self.active_id) |id| std.heap.c_allocator.free(id); if (self.continuation) |*position| position.deinit(); self.* = undefined; } fn continuationValue(self: *const PageRequest) ?session_store.ResumableSessionContinuation { const continuation = self.continuation orelse return null; return .{ .updated_at_ms = continuation.updated_at_ms, .id = continuation.id, }; } }; const Task = struct { thread: ?std.Thread = null, done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), home_dir: []u8, workspace_root: []u8, request: PageRequest, page: ?session_store.ResumableSessionPage = null, failure: ?anyerror = null, fn deinit(self: *Task) void { if (self.thread) |thread| thread.join(); if (self.page) |*page| page.deinit(std.heap.c_allocator); self.request.deinit(); std.heap.c_allocator.free(self.home_dir); std.heap.c_allocator.free(self.workspace_root); std.heap.c_allocator.destroy(self); } }; task: ?*Task = null, pending: ?PageRequest = null, abandoned_generation: ?u64 = null, next_generation: u64 = 1, fn deinit(self: *SessionPickerLoad) void { if (self.task) |task| task.deinit(); if (self.pending) |*pending| pending.deinit(); self.* = .{}; } fn allocateGeneration(self: *SessionPickerLoad) u64 { const generation = self.next_generation; self.next_generation +%= 1; if (self.next_generation == 0) self.next_generation = 1; return generation; } fn schedule( self: *SessionPickerLoad, store: *const session_store.Store, request: PageRequest, ) !void { if (self.task != null) { self.replacePending(request); return; } try self.start(store, request); } fn replacePending(self: *SessionPickerLoad, request: PageRequest) void { if (self.pending) |*pending| { debug_trace.logf( "core", "session picker request dropped reason=superseded generation={d}", .{pending.generation}, ); pending.deinit(); } self.pending = request; } fn cancelGeneration(self: *SessionPickerLoad, generation: u64) void { if (self.task) |task| { if (task.request.generation == generation) { self.abandoned_generation = generation; debug_trace.logf( "core", "session picker request abandoned reason=cancelled generation={d}", .{generation}, ); } } if (self.pending) |pending| { if (pending.generation == generation) { debug_trace.logf( "core", "session picker request dropped reason=cancelled generation={d}", .{generation}, ); var owned = self.pending.?; self.pending = null; owned.deinit(); } } } fn pendingGeneration(self: *const SessionPickerLoad) ?u64 { return if (self.pending) |request| request.generation else null; } fn matchingInitialGeneration( self: *const SessionPickerLoad, scope: SessionPickerScope, active_id: ?[]const u8, limit: usize, ) ?u64 { if (self.task) |task| { if (self.abandoned_generation != task.request.generation and initialRequestMatches(task.request, scope, active_id, limit)) { return task.request.generation; } } if (self.pending) |request| { if (initialRequestMatches(request, scope, active_id, limit)) { return request.generation; } } return null; } fn initialRequestMatches( request: PageRequest, scope: SessionPickerScope, active_id: ?[]const u8, limit: usize, ) bool { return request.continuation == null and request.scope == scope and request.limit >= limit and optionalStringEql(request.active_id, active_id); } fn startPending(self: *SessionPickerLoad, store: *const session_store.Store) !?u64 { const request = self.pending orelse return null; self.pending = null; const generation = request.generation; try self.start(store, request); return generation; } fn start( self: *SessionPickerLoad, store: *const session_store.Store, request: PageRequest, ) !void { std.debug.assert(self.task == null); const alloc = std.heap.c_allocator; var owned_request = request; const home_dir = alloc.dupe(u8, store.home_dir) catch |err| { owned_request.deinit(); return err; }; const workspace_root = alloc.dupe(u8, store.workspace_root) catch |err| { alloc.free(home_dir); owned_request.deinit(); return err; }; const task = alloc.create(Task) catch |err| { alloc.free(workspace_root); alloc.free(home_dir); owned_request.deinit(); return err; }; task.* = .{ .home_dir = home_dir, .workspace_root = workspace_root, .request = owned_request, }; task.thread = std.Thread.spawn(.{}, threadMain, .{task}) catch |err| { task.request.deinit(); alloc.free(task.workspace_root); alloc.free(task.home_dir); alloc.destroy(task); return err; }; self.task = task; } fn takeCompleted(self: *SessionPickerLoad) ?*Task { const task = self.task orelse return null; if (!task.done.load(.acquire)) return null; self.task = null; if (self.abandoned_generation == task.request.generation) { self.abandoned_generation = null; } return task; } fn threadMain(task: *Task) void { var read_only = session_store.Store.initReadOnlyFromHome( std.heap.c_allocator, task.home_dir, task.workspace_root, ) catch |err| { task.failure = err; task.done.store(true, .release); return; }; defer read_only.deinit(std.heap.c_allocator); if (tryListResumableIndexPageForScope( read_only, std.heap.c_allocator, task.request.scope, task.request.active_id, task.request.continuationValue(), task.request.limit, ) catch |err| { task.failure = err; task.done.store(true, .release); return; }) |page| { task.page = page; task.done.store(true, .release); return; } debug_trace.logf("core", "session picker cache outcome=miss action=backfill", .{}); var writable = session_store.Store.initFromHome( std.heap.c_allocator, task.home_dir, task.workspace_root, ) catch |err| { debug_trace.logf( "core", "session picker cache backfill outcome=unavailable err={s}", .{@errorName(err)}, ); task.page = listResumablePageForScope( read_only, std.heap.c_allocator, task.request.scope, task.request.active_id, task.request.continuationValue(), task.request.limit, ) catch |fallback_err| { task.failure = fallback_err; task.done.store(true, .release); return; }; task.done.store(true, .release); return; }; defer writable.deinit(std.heap.c_allocator); task.page = listResumablePageForScope( writable, std.heap.c_allocator, task.request.scope, task.request.active_id, task.request.continuationValue(), task.request.limit, ) catch |err| { debug_trace.logf( "core", "session picker cache backfill outcome=failed err={s}", .{@errorName(err)}, ); task.page = listResumablePageForScope( read_only, std.heap.c_allocator, task.request.scope, task.request.active_id, task.request.continuationValue(), task.request.limit, ) catch |fallback_err| { task.failure = fallback_err; task.done.store(true, .release); return; }; task.done.store(true, .release); return; }; task.done.store(true, .release); } }; fn resumePageLimitForRows(rows: u16) usize { // Fill the resume screen: terminal rows minus the composer/divider/hint // chrome (4), the menu header (1), the top gap (1), and a trailing // "Load more" row (1). Floored so short terminals still page usefully. return @max(@as(usize, rows -| 7), session_store.default_resume_page_limit); } fn tryListResumableIndexPageForScope( store: session_store.Store, alloc: Allocator, scope: SessionPickerScope, active_id: ?[]const u8, continuation: ?session_store.ResumableSessionContinuation, limit: usize, ) !?session_store.ResumableSessionPage { var scoped = store; scoped.resume_page_limit = limit; return switch (scope) { .current_workspace => try scoped.tryListResumableWorkspaceIndexPage(alloc, active_id, continuation), .all_workspaces => try scoped.tryListResumableIndexPage(alloc, active_id, continuation), }; } fn listResumablePageForScope( store: session_store.Store, alloc: Allocator, scope: SessionPickerScope, active_id: ?[]const u8, continuation: ?session_store.ResumableSessionContinuation, limit: usize, ) !session_store.ResumableSessionPage { var scoped = store; scoped.resume_page_limit = limit; return switch (scope) { .current_workspace => try scoped.listResumableWorkspacePage(alloc, active_id, continuation), .all_workspaces => try scoped.listResumablePage(alloc, active_id, continuation), }; } const default_cancelled_command_replay_inline_limit: usize = 64 * 1024; const DisabledCancelledCommandCapture = struct { turn_id: u64, call_id_hash: u64, call_id_len: usize, fn init(id: types.ToolLifecycleId) DisabledCancelledCommandCapture { return .{ .turn_id = id.turn_id, .call_id_hash = std.hash.Wyhash.hash(0, id.call_id), .call_id_len = id.call_id.len, }; } fn matches(self: DisabledCancelledCommandCapture, id: types.ToolLifecycleId) bool { return self.turn_id == id.turn_id and self.call_id_len == id.call_id.len and self.call_id_hash == std.hash.Wyhash.hash(0, id.call_id); } }; const JsHostSessionStore = struct { ctx: ?*anyopaque = null, load_fn: *const fn (?*anyopaque, Allocator, []const u8) anyerror!?js_host_session_store.Loaded = if (host_target.is_wasm) loadDefault else loadUnavailable, commit_fn: *const fn (?*anyopaque, Allocator, session_codec.DurableSessionState, ?[]const u8) anyerror![]u8 = if (host_target.is_wasm) commitDefault else commitUnavailable, list_fn: *const fn (?*anyopaque, Allocator) anyerror![]js_host_session_store.Metadata = if (host_target.is_wasm) listDefault else listUnavailable, fn load(self: JsHostSessionStore, alloc: Allocator, id: []const u8) !?js_host_session_store.Loaded { return self.load_fn(self.ctx, alloc, id); } fn commit( self: JsHostSessionStore, alloc: Allocator, state: session_codec.DurableSessionState, expected_revision: ?[]const u8, ) ![]u8 { return self.commit_fn(self.ctx, alloc, state, expected_revision); } fn list(self: JsHostSessionStore, alloc: Allocator) ![]js_host_session_store.Metadata { return self.list_fn(self.ctx, alloc); } fn loadDefault(_: ?*anyopaque, alloc: Allocator, id: []const u8) !?js_host_session_store.Loaded { return js_host_session_store.load(alloc, id); } fn commitDefault( _: ?*anyopaque, alloc: Allocator, state: session_codec.DurableSessionState, expected_revision: ?[]const u8, ) ![]u8 { return js_host_session_store.commit(alloc, state, expected_revision); } fn listDefault(_: ?*anyopaque, alloc: Allocator) ![]js_host_session_store.Metadata { return js_host_session_store.list(alloc); } fn loadUnavailable(_: ?*anyopaque, _: Allocator, _: []const u8) !?js_host_session_store.Loaded { return error.SessionStoreUnavailable; } fn commitUnavailable( _: ?*anyopaque, _: Allocator, _: session_codec.DurableSessionState, _: ?[]const u8, ) ![]u8 { return error.SessionStoreUnavailable; } fn listUnavailable(_: ?*anyopaque, _: Allocator) ![]js_host_session_store.Metadata { return error.SessionStoreUnavailable; } }; const JsHostSessionOwner = struct { state: session_codec.DurableSessionState, revision: ?[]u8, fn deinit(self: *JsHostSessionOwner, alloc: Allocator) void { self.state.deinit(alloc); if (self.revision) |revision| alloc.free(revision); self.* = undefined; } }; const PendingCancelledCommand = struct { lifecycle_id: types.ToolLifecycleId, capture: ?*command_replay_store.Capture, cancelled: bool = false, completed: bool = false, command_artifact_handle: ?[]u8 = null, fn matches(self: PendingCancelledCommand, id: types.ToolLifecycleId) bool { return self.lifecycle_id.turn_id == id.turn_id and std.mem.eql(u8, self.lifecycle_id.call_id, id.call_id); } fn discard(self: *PendingCancelledCommand, alloc: Allocator) void { if (self.capture) |capture| { capture.discard(alloc); alloc.destroy(capture); } alloc.free(@constCast(self.lifecycle_id.call_id)); if (self.command_artifact_handle) |handle| alloc.free(handle); self.* = undefined; } fn releaseRetained(self: *PendingCancelledCommand, alloc: Allocator) void { if (self.capture) |capture| { capture.releaseRetained(alloc); alloc.destroy(capture); } alloc.free(@constCast(self.lifecycle_id.call_id)); if (self.command_artifact_handle) |handle| alloc.free(handle); self.* = undefined; } }; pub const Persistence = struct { // Serializes event-log mutations with worker usage callbacks. Usage takes // its checkpoint mutex first, so callers must not checkpoint while held. write_mutex: std.Io.Mutex = .init, store: ?session_store.Store = null, writable: ?session_store.LoadedWritableSession = null, subagent_host: ?*subagent_tool_host.Runtime = null, workspace_preferences: ?session_codec.DurableSessionPreferences = null, session_preferences: ?session_codec.DurableSessionPreferences = null, js_host_store: JsHostSessionStore = .{}, js_host_session: ?JsHostSessionOwner = null, process_model_override: ?[]u8 = null, session_picker: SessionPicker = .{}, session_picker_load: SessionPickerLoad = .{}, session_picker_current_cache: SessionPickerPageCache = .{}, session_picker_all_cache: SessionPickerPageCache = .{}, degraded_warning_emitted: bool = false, pending_cancelled_command: ?PendingCancelledCommand = null, disabled_cancelled_command_capture: ?DisabledCancelledCommandCapture = null, image_snapshot_temp_dir: ?[]u8 = null, resume_view_admission: ?session_store.ResumeViewAdmission = null, resume_handoff_intent: ResumeHandoffIntent = .none, pub fn deinit(self: *Persistence, alloc: Allocator) void { if (self.pending_cancelled_command) |*pending| pending.discard(alloc); if (self.resume_view_admission) |*admission| admission.deinit(alloc); if (self.image_snapshot_temp_dir) |path| { image_attachments.cleanupSnapshotDir(path); alloc.free(path); } if (self.subagent_host) |host| host.deinit(); if (self.writable) |*loaded| loaded.deinit(alloc); if (self.store) |*store| store.deinit(alloc); if (self.workspace_preferences) |*preferences| preferences.deinit(alloc); if (self.session_preferences) |*preferences| preferences.deinit(alloc); if (self.js_host_session) |*owner| owner.deinit(alloc); if (self.process_model_override) |model| alloc.free(model); self.session_picker.deinit(alloc); self.session_picker_load.deinit(); self.session_picker_current_cache.deinit(); self.session_picker_all_cache.deinit(); self.* = .{}; } }; pub fn Runtime(comptime App: type) type { return struct { pub fn stageRequestedResumeView(app: *App) ResumeViewStage { const requested = app.requested_resume orelse return .none; const store = app.session_persistence.store orelse return .none; const target: session_store.ResumeTarget = switch (requested) { .pick => return .none, .last => .last, .id => |session_id| .{ .id = session_id }, }; var admission = (subagent_resume_admission.admitResumeViewForExternalPrompt( store, app.alloc, target, ) catch |err| { traceResumeViewUnavailable(err); return .none; }) orelse { debug_trace.logf("session", "event=resume_view_cache outcome=missing", .{}); return .none; }; var admission_owned = true; defer if (admission_owned) admission.deinit(app.alloc); var stage: ResumeViewStage = .none; switch (admission.view) { .missing, .invalid, .stale => debug_trace.logf( "session", "event=resume_view_cache outcome={s}", .{@tagName(admission.view)}, ), .exact, .older => |view| { switch (requested) { .last => app.requested_resume = .{ .id = app.alloc.dupe(u8, view.session_id) catch |err| { traceResumeViewUnavailable(err); return .none; } }, .pick, .id => {}, } const freshness = std.meta.activeTag(admission.view); const paint_safe = freshness == .exact and view.capture.canPaintAt( app.shell.layout.rows, app.shell.layout.cols, ); if (paint_safe) { const entry_id = app.shell.appendRawTranscriptEntryClassified( app.alloc, view.text, .unknown_raw, ) catch |err| { traceResumeViewUnavailable(err); return .none; }; stage = .{ .ready = entry_id }; } app.session_persistence.resume_view_admission = admission; admission_owned = false; debug_trace.logf( "session", "event=resume_view_cache outcome={s} freshness={s} complete={} cached={d}x{d} terminal={d}x{d} bytes={d}", .{ if (paint_safe) "painted" else "skipped", @tagName(freshness), view.capture.complete, view.capture.terminal_cols, view.capture.terminal_rows, app.shell.layout.cols, app.shell.layout.rows, view.text.len, }, ); }, } return stage; } pub fn publishStagedResumeView(app: *App, entry_id: u32) !void { app.commitStartupResumeReplayAnchor() catch |err| { traceResumeViewUnavailable(err); }; _ = try app.shell.releaseStartupResumeViewEntry( app.alloc, entry_id, ); } fn traceResumeViewUnavailable(err: anyerror) void { debug_trace.logf( "session", "event=resume_view_cache outcome=unavailable err={s}", .{@errorName(err)}, ); } pub fn persistResumeViewAfterFrame(app: *App) void { if (app.stream.active) return; if (comptime @hasField(App, "terminal")) { if (app.terminal.alternate_screen_owner != .none) return; } app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return; if (!loaded.resume_view_stale) return; var snapshot = app.shell.snapshotVisibleTranscriptText( app.alloc, session_resume_view.max_text_bytes, ) catch |err| { debug_trace.logf( "session", "event=resume_view_cache outcome=capture_failed err={s}", .{@errorName(err)}, ); return; } orelse return; defer snapshot.deinit(app.alloc); loaded.writeResumeView(app.alloc, .{ .terminal_rows = snapshot.terminal_rows, .terminal_cols = snapshot.terminal_cols, .complete = snapshot.complete, }, snapshot.text) catch |err| { debug_trace.logf( "session", "event=resume_view_cache outcome=write_failed err={s}", .{@errorName(err)}, ); return; }; loaded.resume_view_stale = false; debug_trace.logf( "session", "event=resume_view_cache outcome=stored complete={} terminal={d}x{d} bytes={d}", .{ snapshot.complete, snapshot.terminal_cols, snapshot.terminal_rows, snapshot.text.len }, ); } pub fn captureImageAttachment( app: *App, attachment: *types.ImageAttachment, ) !void { const snapshot_dir = try imageSnapshotStorageDir(app); defer app.alloc.free(snapshot_dir); try image_attachments.captureImageSnapshot( app.alloc, attachment, snapshot_dir, ); } fn imageSnapshotStorageDir(app: *App) ![]u8 { const sessions_dir = if (app.session_persistence.store) |*store| store.sessions_dir else null; const session_id = if (app.session_persistence.writable) |*writable| writable.active_id else null; return session_store.imageSnapshotStorageDir( app.alloc, sessions_dir, session_id, &app.session_persistence.image_snapshot_temp_dir, ); } pub fn configureStartupPreferences( app: *App, configured_model: []const u8, model_source: config_runtime.ModelSource, selected_model: []const u8, effort: types.ReasoningEffort, fast_mode: bool, ) !void { try replacePreferences( app.alloc, &app.session_persistence.workspace_preferences, .{ .model = @constCast(configured_model), .effort = effort, .fast_mode = fast_mode, }, ); try replacePreferences( app.alloc, &app.session_persistence.session_preferences, app.session_persistence.workspace_preferences.?, ); if (app.session_persistence.process_model_override) |model| { app.alloc.free(model); app.session_persistence.process_model_override = null; } if (model_source == .process_override) { app.session_persistence.process_model_override = try app.alloc.dupe(u8, selected_model); } } pub fn initializePersistence( app: *App, required: bool, ) !void { if (comptime !runtime_profile.allows(App, .durable_sessions)) return; var store = session_store.Store.init( app.alloc, app.workspace_root, ) catch |err| { if (required) return err; return; }; errdefer store.deinit(app.alloc); app.session_persistence.store = store; } pub fn enableSessionStores(app: *App) void { if (comptime !runtime_profile.allows(App, .durable_sessions)) return; const loaded = if (app.session_persistence.writable) |*value| value else return; const capability = loaded.childCapability() catch |err| { debug_trace.logf( "session", "interactive child capability unavailable session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); return; }; configureWebFetchArtifacts(app, loaded); enableSubagentHost(app, loaded); restoreManagedBackground(app, loaded, capability); } fn restoreManagedBackground( app: *App, loaded: *session_store.LoadedWritableSession, capability: *session_child_store.SessionChildCapability, ) void { if (comptime @hasDecl( @TypeOf(app.background), "restoreFromManagedPersistence", )) { app.background.restoreFromManagedPersistence( std.heap.c_allocator, capability, loaded.active_id, app.workspace_root, ) catch |err| { debug_trace.logf( "background", "interactive managed background restore failed session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); }; } } pub fn beginFreshPersistedSession(app: *App) !void { closeWritableSession(app, .{}); if (comptime runtime_profile.allows(App, .js_host_sessions)) { try beginFreshJsHostSession(app); } if (comptime !runtime_profile.allows(App, .durable_sessions)) return; const store = app.session_persistence.store orelse return; const preferences = app.session_persistence.workspace_preferences orelse return error.SessionPreferencesUnavailable; try replacePreferences( app.alloc, &app.session_persistence.session_preferences, preferences, ); var state = try freshState(app, preferences); defer state.deinit(app.alloc); app.session_persistence.writable = store.startWritableSession( app.alloc, state, ) catch |err| { try warnNonDurable(app, "session creation failed", err); return; }; app.session_persistence.degraded_warning_emitted = false; app.total_input_tokens = 0; app.total_output_tokens = 0; app.total_web_search_requests = 0; } fn beginFreshJsHostSession(app: *App) !void { const preferences = app.session_persistence.workspace_preferences orelse return error.SessionPreferencesUnavailable; try replacePreferences( app.alloc, &app.session_persistence.session_preferences, preferences, ); var state = try freshState(app, preferences); errdefer state.deinit(app.alloc); if (app.session_persistence.js_host_session) |*owner| owner.deinit(app.alloc); app.session_persistence.js_host_session = .{ .state = state, .revision = null, }; state = undefined; app.total_input_tokens = 0; app.total_output_tokens = 0; app.total_web_search_requests = 0; } pub fn clearSession(app: *App) !void { try resetSessionWithBackgroundPolicy(app, .carry_forward); } pub fn newSession(app: *App) !void { try resetSessionWithBackgroundPolicy(app, .carry_forward); } pub fn resetSession(app: *App) !void { try resetSessionWithBackgroundPolicy(app, .stop_forget); } fn resetSessionWithBackgroundPolicy( app: *App, background_policy: BackgroundSessionPolicy, ) !void { clearCachedSessionTitle(app); try prepareLiveSessionTransition(app, background_policy, .{}); try beginFreshPersistedSession(app); enableSessionStores(app); refreshSubagentProjectionAfterSessionInstall(app); try finishLiveSessionTransition(app); } pub fn prepareLiveSessionResume( app: *App, log_options: session_log.Options, ) !void { try prepareLiveSessionTransition(app, .stop_forget, log_options); } pub fn finishLiveSessionResume(app: *App) !void { try app.shell.requestTerminalReset(&app.metrics); app.shell.render_requests.request(.footer); } fn prepareLiveSessionTransition( app: *App, background_policy: BackgroundSessionPolicy, log_options: session_log.Options, ) !void { app.worker.requestCancel(); app.pacer.clear(app.alloc); if (comptime @hasField(App, "queued_prompt_review")) { input_queue_runtime.Runtime(App).reset(app); } app.shell.render_requests.finishSubmittedPromptTransition(); app.worker.clearQueuedPrompts(std.heap.c_allocator, &.{}); app.worker.waitUntilIdle(); app.worker.discardEvents(std.heap.c_allocator); if (comptime @hasField(App, "subagents") and @hasDecl(@TypeOf(app.subagents), "clearProjection")) { app.subagents.clearProjection(app.alloc); } closeWritableSession(app, log_options); app.stopStream(); app.shell.clearTranscript(app.alloc); app.session.reset(app.alloc); app.input_runtime.inputResetState().resetForSession(app.alloc); app.terminal_input_runtime.resetEscapeDecoder(); app.permission_engine.clear(app.alloc); app.clearPendingImages(); app.change_tracker.clear(std.heap.c_allocator); diagnostics.resetSession(); switch (background_policy) { .carry_forward => app.background.carryForwardWorkspaceState( std.heap.c_allocator, app.workspace_root, ), .stop_forget => app.background.stopAndForgetWorkspace( std.heap.c_allocator, app.workspace_root, ), } app.context_snapshot.deinit(app.alloc); app.approval_prompt.clear(app.alloc); if (comptime @hasField(App, "approval_screen")) { app.approval_screen.clear(); } app.question_prompt.discard(app.alloc, "session_reset"); cancelSessionPicker(app); invalidateSessionPickerCaches(app); } fn finishLiveSessionTransition(app: *App) !void { const welcome = try ui_render.welcomeMessage(app.alloc); defer app.alloc.free(welcome); try app.writeTranscriptClassified(welcome, true, .welcome); app.shell.render_requests.request(.footer); try shell_runtime.requestRedraw(&app.shell, &app.metrics, .replay_viewport); } pub fn resumeRequestedSession(app: *App) !void { return resumeRequestedSessionWithNotice(app, .session); } pub fn resumeRequestedSessionAfterUpgrade( app: *App, version: []const u8, ) !void { return resumeRequestedSessionWithNotice(app, .{ .upgrade = version }); } fn resumeRequestedSessionWithNotice( app: *App, notice: ResumeNotice, ) !void { if (comptime runtime_profile.allows(App, .js_host_sessions) and !runtime_profile.allows(App, .durable_sessions)) { return resumeRequestedJsHostSessionWithNotice(app, notice); } var target = app.requested_resume orelse return; app.requested_resume = null; defer target.deinit(app.alloc); const resume_target: session_store.ResumeTarget = switch (target) { // Nothing to load yet: the picker asks which session to open. .pick => return openSessionPicker(app), .last => .last, .id => |session_id| .{ .id = session_id }, }; var loaded = if (app.session_persistence.resume_view_admission) |saved| admitted: { var admission = saved; app.session_persistence.resume_view_admission = null; defer admission.deinit(app.alloc); const session_id = switch (resume_target) { .id => |id| id, .last => return error.SessionTargetChanged, }; const store = app.session_persistence.store orelse return error.SessionStoreUnavailable; break :admitted try subagent_resume_admission.resumeAdmittedForExternalPrompt( store, app.alloc, &admission, session_id, app.workspace_root, .{ .seed_preferences = app.session_persistence.workspace_preferences }, ); } else try loadResumeTargetForWrite(app, resume_target, .{}); var loaded_owned = true; errdefer if (loaded_owned) loaded.deinit(app.alloc); loaded_owned = false; try installResumedSession(app, &loaded, notice); errdefer closeWritableSession(app, .{}); try app.commitStartupResumeReplayAnchor(); } fn resumeRequestedJsHostSession(app: *App) !void { return resumeRequestedJsHostSessionWithNotice(app, .session); } fn resumeRequestedJsHostSessionWithNotice( app: *App, notice: ResumeNotice, ) !void { var target = app.requested_resume orelse return; app.requested_resume = null; defer target.deinit(app.alloc); if (target == .pick) { debug_trace.logf( "session", "event=js_host_session_restore outcome=dropped reason=picker_unsupported", .{}, ); try continueWithFreshJsHostSession(app); try app.writeDomainNotice(.{ .topic = "session", .tone = .neutral, .body = "session picker is unavailable in this host; use --resume last or a session ID", }, true); return; } var listed: ?[]js_host_session_store.Metadata = null; defer if (listed) |entries| { for (entries) |*entry| entry.deinit(app.alloc); app.alloc.free(entries); }; const session_id = switch (target) { .pick => unreachable, .id => |id| id, .last => latest: { const entries = app.session_persistence.js_host_store.list(app.alloc) catch |err| { traceJsHostRestoreFailure("list", null, err); try continueWithFreshJsHostSession(app); return; }; listed = entries; if (entries.len == 0) { debug_trace.logf( "session", "event=js_host_session_restore outcome=dropped reason=missing_record target=last", .{}, ); try continueWithFreshJsHostSession(app); return; } var latest_index: usize = 0; for (entries[1..], 1..) |entry, index| { if (entry.updated_at_ms > entries[latest_index].updated_at_ms) { latest_index = index; } } break :latest entries[latest_index].id; }, }; var loaded = (app.session_persistence.js_host_store.load(app.alloc, session_id) catch |err| { traceJsHostRestoreFailure(jsHostLoadFailureStage(err), session_id, err); try continueWithFreshJsHostSession(app); return; }) orelse { debug_trace.logf( "session", "event=js_host_session_restore outcome=dropped reason=missing_record id={s}", .{session_id}, ); try continueWithFreshJsHostSession(app); return; }; var loaded_owned = true; defer if (loaded_owned) loaded.deinit(app.alloc); var display = session_display_metadata.deriveFromHistory( app.alloc, loaded.state.history, ) catch |err| { traceJsHostRestoreFailure("display", session_id, err); try continueWithFreshJsHostSession(app); return; }; defer display.deinit(app.alloc); hydrateResumedSession(app, loaded.state, display.title, notice) catch |err| { traceJsHostRestoreFailure("hydrate", session_id, err); try continueWithFreshJsHostSession(app); return; }; if (app.session_persistence.js_host_session) |*owner| owner.deinit(app.alloc); app.session_persistence.js_host_session = .{ .state = loaded.state, .revision = loaded.revision, }; loaded_owned = false; loaded = undefined; try app.commitStartupResumeReplayAnchor(); } fn continueWithFreshJsHostSession(app: *App) !void { app.session.reset(app.alloc); app.shell.clearTranscript(app.alloc); clearCachedSessionTitle(app); try beginFreshJsHostSession(app); try finishLiveSessionTransition(app); } fn jsHostLoadFailureStage(err: anyerror) []const u8 { return switch (err) { error.InvalidDurableField, error.InvalidDurableBytes, error.InvalidSessionFormat, error.InvalidSessionId, => "decode", else => "load", }; } fn traceJsHostRestoreFailure(stage: []const u8, id: ?[]const u8, err: anyerror) void { if (id) |session_id| { debug_trace.logf( "session", "event=js_host_session_restore outcome=dropped stage={s} id={s} err={s}", .{ stage, session_id, @errorName(err) }, ); } else { debug_trace.logf( "session", "event=js_host_session_restore outcome=dropped stage={s} err={s}", .{ stage, @errorName(err) }, ); } } pub fn startResumedSessionReconciliation(app: *App) void { if (comptime @hasField(App, "auth")) { if (app.auth.apiKey()) |api_key| { app.session.usage.startReconciliation( app.alloc, api_key, ); } } } pub fn resumeSelectedSession(app: *App) !bool { const selected_id = app.session_persistence.session_picker.selectedId() orelse return false; const log_options = session_log.Options{ .session_lock_deadline_ms = 0, .commit_lock_deadline_ms = 0, }; var loaded = try loadResumeTargetForWrite( app, .{ .id = selected_id }, log_options, ); var loaded_owned = true; errdefer if (loaded_owned) loaded.deinit(app.alloc); try app.prepareLiveSessionResume(log_options); loaded_owned = false; try installResumedSession(app, &loaded, .session); requestSubagentBackgroundRecovery(app); startResumedSessionReconciliation(app); try app.finishLiveSessionResume(); return true; } fn loadResumeTargetForWrite( app: *App, target: session_store.ResumeTarget, log_options: session_log.Options, ) !session_store.LoadedWritableSession { const store = app.session_persistence.store orelse return error.SessionStoreUnavailable; return subagent_resume_admission.resumeForExternalPrompt( store, app.alloc, target, app.workspace_root, .{ .seed_preferences = app.session_persistence.workspace_preferences, .log = log_options, }, ); } fn installResumedSession( app: *App, loaded: *session_store.LoadedWritableSession, notice: ResumeNotice, ) !void { var loaded_owned = true; errdefer if (loaded_owned) loaded.deinit(app.alloc); var display = try readNativeResumeDisplay(app, loaded); defer display.deinit(app.alloc); closeWritableSession(app, .{}); app.session_persistence.writable = loaded.*; loaded_owned = false; loaded.* = undefined; errdefer { app.session_persistence.writable.?.deinit(app.alloc); app.session_persistence.writable = null; } const active = &app.session_persistence.writable.?; try hydrateResumedSession(app, active.state, display.title, notice); active.resume_view_stale = true; enableSessionStores(app); refreshSubagentProjectionAfterSessionInstall(app); } fn hydrateResumedSession( app: *App, state: session_codec.DurableSessionState, display_title: []const u8, notice: ResumeNotice, ) !void { if (comptime @hasField(App, "next_image_id")) { app.next_image_id = try nextImageIdForResumedHistory( app.alloc, state.history, ); } try app.session.restoreWithPermissionState( app.alloc, state.conversation_language, state.history, state.context_history_start, state.permission_state, ); if (state.usage) |usage| { try app.session.usage.restore( app.alloc, usage, state.created_at_ms, ); } else { app.session.usage.restoreLegacyWallDuration(state.created_at_ms); } try replacePreferences( app.alloc, &app.session_persistence.session_preferences, state.preferences, ); try restoreRuntimePreferences(app, state.preferences); app.total_input_tokens = state.total_input_tokens; app.total_output_tokens = state.total_output_tokens; app.total_web_search_requests = 0; if (comptime @hasDecl(App, "beginResumeProjection")) { const projection_started_ns = io_mod.nanoTimestamp(); var projection = try app.beginResumeProjection(); defer projection.deinit(); var sink = DetachedHistorySink(@TypeOf(projection)){ .app = app, .projection = &projection, }; try writeResumeNotice(app, &sink, display_title, notice); try replayHistoryToSink(app, &sink, state.history); try writeRecoveryCheckpointToSink(app, &sink, state); const projection_finished_ns = io_mod.nanoTimestamp(); try projection.finalize(); const finalization_finished_ns = io_mod.nanoTimestamp(); try app.installResumeProjection(&projection); const install_finished_ns = io_mod.nanoTimestamp(); debug_trace.logf( "session", "event=resume_projection turns={d} project_us={d} finalize_us={d} install_us={d}", .{ state.history.len, @divTrunc(projection_finished_ns - projection_started_ns, std.time.ns_per_us), @divTrunc(finalization_finished_ns - projection_finished_ns, std.time.ns_per_us), @divTrunc(install_finished_ns - finalization_finished_ns, std.time.ns_per_us), }, ); } else { var sink = LiveHistorySink(App){ .app = app }; try writeResumeNotice(app, &sink, display_title, notice); try replayHistoryToSink(app, &sink, state.history); try writeRecoveryCheckpointToSink(app, &sink, state); } } fn refreshSubagentProjectionAfterSessionInstall(app: *App) void { if (comptime !@hasDecl(App, "refreshSubagentManagerProjectionNow")) return; app.refreshSubagentManagerProjectionNow() catch |err| { debug_trace.logf( "subagent", "session projection refresh unavailable err={s}", .{@errorName(err)}, ); }; } pub fn openSessionPicker(app: *App) !void { return openSessionPickerWithScope(app, .current_workspace); } pub fn openAllSessionPicker(app: *App) !void { return openSessionPickerWithScope(app, .all_workspaces); } fn openSessionPickerWithScope(app: *App, scope: SessionPickerScope) !void { if (app.stream.active) { try app.writeDomainNotice(.{ .topic = "session", .tone = .neutral, .body = "resume is unavailable until the response finishes", }, true); return; } var picker = &app.session_persistence.session_picker; if (picker.load_state != .ready) { app.session_persistence.session_picker_load.cancelGeneration(picker.generation); } picker.deinit(app.alloc); picker.active = true; picker.load_state = .loading; picker.scope = scope; picker.setQuery(app.input_runtime.edit_state.input.items); const store = if (app.session_persistence.store) |*value| value else { picker.load_state = .failed; try writeSessionPickerError(app, error.SessionStoreUnavailable); app.shell.render_requests.request(.footer); return; }; const active_id = if (app.session_persistence.writable) |*loaded| loaded.active_id else null; const limit = if (comptime @hasField(App, "shell")) resumePageLimitForRows(app.shell.layout.rows) else session_store.default_resume_page_limit; const cache = sessionPickerCacheForScope(&app.session_persistence, scope); var cache_visible = false; const now_ns = io_mod.nanoTimestamp(); if (cache.matches(active_id, limit)) { try replaceSessionPickerPage(picker, app.alloc, &cache.page); picker.has_more = cache.page.has_more; picker.load_state = .ready; cache_visible = true; if (cache.isFresh(now_ns)) { picker.generation = app.session_persistence.session_picker_load.allocateGeneration(); return; } } const loader = &app.session_persistence.session_picker_load; if (loader.matchingInitialGeneration(scope, active_id, limit)) |generation| { picker.generation = generation; return; } const request = SessionPickerLoad.PageRequest.init( loader.allocateGeneration(), picker.scope, active_id, null, limit, ) catch |err| { if (!cache_visible) picker.load_state = .failed; try writeSessionPickerError(app, err); return; }; picker.generation = request.generation; loader.schedule(store, request) catch |err| { if (!cache_visible) picker.load_state = .failed; try writeSessionPickerError(app, err); return; }; } pub fn primeSessionPicker(app: *App) void { const store = if (app.session_persistence.store) |*value| value else return; const active_id = if (app.session_persistence.writable) |*loaded| loaded.active_id else null; const limit = if (comptime @hasField(App, "shell")) resumePageLimitForRows(app.shell.layout.rows) else session_store.default_resume_page_limit; const loader = &app.session_persistence.session_picker_load; for ([_]SessionPickerScope{ .current_workspace, .all_workspaces }) |scope| { const cache = sessionPickerCacheForScope(&app.session_persistence, scope); if (cache.matches(active_id, limit) and cache.isFresh(io_mod.nanoTimestamp())) continue; if (loader.matchingInitialGeneration(scope, active_id, limit) != null) continue; const request = SessionPickerLoad.PageRequest.init( loader.allocateGeneration(), scope, active_id, null, limit, ) catch |err| { debug_trace.logf( "core", "session picker prewarm unavailable scope={s} err={s}", .{ @tagName(scope), @errorName(err) }, ); continue; }; loader.schedule(store, request) catch |err| { debug_trace.logf( "core", "session picker prewarm unavailable scope={s} err={s}", .{ @tagName(scope), @errorName(err) }, ); }; } } pub fn toggleSessionPickerScope(app: *App) !bool { const picker = &app.session_persistence.session_picker; if (!picker.active) return false; const next: SessionPickerScope = switch (picker.scope) { .current_workspace => .all_workspaces, .all_workspaces => .current_workspace, }; try openSessionPickerWithScope(app, next); return true; } pub fn pollSessionPicker(app: *App) !void { const loader = &app.session_persistence.session_picker_load; const task = loader.takeCompleted() orelse return; var task_owned = true; defer if (task_owned) task.deinit(); if (task.failure == null and task.page != null and task.request.continuation == null) { const cache = sessionPickerCacheForScope( &app.session_persistence, task.request.scope, ); cache.install( &task.page.?, task.request.active_id, task.request.limit, io_mod.nanoTimestamp(), ) catch |err| { debug_trace.logf( "core", "session picker cache install failed scope={s} err={s}", .{ @tagName(task.request.scope), @errorName(err) }, ); }; } const picker = &app.session_persistence.session_picker; const current = picker.active and picker.generation == task.request.generation; if (!current) { if (task.failure == null and task.page != null and task.request.continuation == null) { debug_trace.logf( "core", "session picker completion cached scope={s} generation={d}", .{ @tagName(task.request.scope), task.request.generation }, ); } else { debug_trace.logf( "core", "session picker completion dropped reason=stale generation={d}", .{task.request.generation}, ); } } else { if (task.failure) |err| { if (task.request.continuation != null) { picker.loading_more = false; } else if (picker.load_state != .ready) { picker.load_state = .failed; } try writeSessionPickerError(app, err); } else if (task.page) |*page| { applySessionPickerPage(app, picker, task, page) catch |err| { if (task.request.continuation != null) { picker.loading_more = false; } else if (picker.load_state != .ready) { picker.load_state = .failed; } try writeSessionPickerError(app, err); }; } } task.deinit(); task_owned = false; const store = if (app.session_persistence.store) |*value| value else return; const pending_generation = loader.pendingGeneration(); _ = loader.startPending(store) catch |err| { if (pending_generation) |generation| { if (picker.active and picker.generation == generation) { if (picker.loading_more) { picker.loading_more = false; } else { picker.load_state = .failed; } try writeSessionPickerError(app, err); } } return; }; if (comptime @hasField(App, "shell")) { if (picker.active) app.shell.render_requests.request(.footer); } } fn writeSessionPickerError(app: *App, err: anyerror) !void { const notice = try std.fmt.allocPrint( app.alloc, "unable to list saved sessions: {s}", .{@errorName(err)}, ); defer app.alloc.free(notice); try app.writeDomainNotice(.{ .topic = "session", .tone = .@"error", .body = notice, }, true); } fn applySessionPickerPage( app: *App, picker: *SessionPicker, task: *const SessionPickerLoad.Task, page: *const session_store.ResumableSessionPage, ) !void { const selected_load_more = task.request.continuation != null and picker.selected == picker.filteredItemCount(); const previous_filtered_count = picker.filteredItemCount(); if (task.request.continuation == null) { try replaceSessionPickerPage(picker, app.alloc, page); } else { try picker.appendPage(app.alloc, page.summaries.items); } picker.has_more = page.has_more; picker.loading_more = false; picker.load_state = .ready; if (selected_load_more) { picker.selected = selectionAfterLoadedPage( previous_filtered_count, picker.filteredItemCount(), picker.has_more, ); } input_completion_runtime.CompletionRuntime(App).syncSessionPickerWindowStart(app); } pub fn loadMoreSessionPicker(app: *App) !bool { const picker = &app.session_persistence.session_picker; if (!picker.isLoadMoreSelected()) return false; if (picker.loading_more) return true; try scheduleMoreSessions(app); return true; } fn scheduleMoreSessions(app: *App) !void { const picker = &app.session_persistence.session_picker; const store = if (app.session_persistence.store) |*value| value else return error.SessionStoreUnavailable; const last = picker.summaries.items[picker.summaries.items.len - 1]; const active_id = if (app.session_persistence.writable) |*loaded| loaded.active_id else null; const limit = if (comptime @hasField(App, "shell")) resumePageLimitForRows(app.shell.layout.rows) else session_store.default_resume_page_limit; const request = try SessionPickerLoad.PageRequest.init( picker.generation, picker.scope, active_id, .{ .updated_at_ms = last.updated_at_ms, .id = last.id, }, limit, ); picker.loading_more = true; app.session_persistence.session_picker_load.schedule(store, request) catch |err| { picker.loading_more = false; return err; }; if (comptime @hasField(App, "shell")) { app.shell.render_requests.request(.footer); } } // Prefetch the next page as the selection reaches the last loaded // session (or the load-more slot), so scrolling down fills the list in // without stopping to activate "Load more". fn maybePrefetchMoreSessions(app: *App) void { const picker = &app.session_persistence.session_picker; if (!picker.active or picker.load_state != .ready) return; if (!picker.has_more or picker.loading_more) return; if (picker.summaries.items.len == 0) return; if (picker.selected + 1 < picker.filteredItemCount()) return; scheduleMoreSessions(app) catch |err| { debug_trace.logf( "core", "session picker prefetch failed err={s}", .{@errorName(err)}, ); }; } pub fn cancelSessionPicker(app: *App) void { const picker = &app.session_persistence.session_picker; if (picker.load_state != .ready) { app.session_persistence.session_picker_load.cancelGeneration(picker.generation); } picker.deinit(app.alloc); } pub fn cancelSessionPickerToComposer(app: *App) void { cancelSessionPicker(app); if (comptime !@hasField(App, "session")) return; if (app.session_persistence.writable != null) return; beginFreshPersistedSession(app) catch |err| { debug_trace.logf( "session", "fresh session after picker cancel unavailable err={s}", .{@errorName(err)}, ); return; }; enableSessionStores(app); } fn invalidateSessionPickerCaches(app: *App) void { app.session_persistence.session_picker_current_cache.deinit(); app.session_persistence.session_picker_all_cache.deinit(); } pub fn moveSessionPicker(app: *App, delta: i32, visible_items: u16) bool { const moved = app.session_persistence.session_picker.moveVisibleItems(delta, visible_items); maybePrefetchMoreSessions(app); return moved; } pub fn recordAppliedCommandOutput( app: *App, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, bytes: []const u8, ) void { const id = lifecycle_id orelse return; if (app.session_persistence.disabled_cancelled_command_capture) |disabled| { if (disabled.matches(id)) return; debug_trace.logf( "session", "cancelled command replay disabled lifecycle replaced turn_id={d}", .{disabled.turn_id}, ); app.session_persistence.disabled_cancelled_command_capture = null; } const pending = ensurePendingCancelledCommand(app, id, true) orelse return; if (pending.capture) |capture| { capture.appendAccepted(app.alloc, stream, bytes); } } pub fn recordToolTerminal( app: *App, event: types.ToolLifecycleEvent, captured_command_call: bool, ) void { const terminal = switch (event) { .terminal => |value| value, .provisional, .authoritative_started, .progress, .turn_finished => return, }; const capture_disabled = if (app.session_persistence.disabled_cancelled_command_capture) |disabled| disabled.matches(terminal.id) else false; if (capture_disabled) { app.session_persistence.disabled_cancelled_command_capture = null; } if (!captured_command_call) { discardPendingCancelledCommand(app, terminal.id, "non_command_terminal"); return; } if (terminal.outcome.kind != .cancelled) { discardPendingCancelledCommand(app, terminal.id, "ordinary_command_terminal"); return; } const pending = ensurePendingCancelledCommand( app, terminal.id, !capture_disabled, ) orelse return; pending.cancelled = true; if (pending.command_artifact_handle) |handle| { app.alloc.free(handle); pending.command_artifact_handle = null; } if (terminal.command_artifact_handle) |handle| { pending.command_artifact_handle = app.alloc.dupe(u8, handle) catch |err| { debug_trace.logf( "session", "cancelled command artifact capture unavailable call_id={s} err={s}", .{ terminal.id.call_id, @errorName(err) }, ); return; }; } } pub fn recordCommandOutputComplete( app: *App, lifecycle_id: ?types.ToolLifecycleId, ) void { const id = lifecycle_id orelse return; const pending = if (app.session_persistence.pending_cancelled_command) |*value| value else return; if (!pending.matches(id)) return; if (pending.capture) |capture| capture.seal(app.alloc); pending.completed = true; } pub fn appendHistoryTurn(app: *App, turn: types.HistoryTurn) !void { _ = try appendHistoryTurnWithPendingPresentation(app, turn, .strict, null); } pub fn appendFinishedPrompt( app: *App, finished: types.FinishedPrompt, ) !void { _ = try appendHistoryTurnWithPendingPresentation( app, finished.turn, .strict, finished.snapshot_file_ownership, ); } pub fn persistUsageCheckpoint( app: *App, snapshot: session_usage.Snapshot, ) !void { if (comptime !@hasField(App, "session_persistence")) { return error.SessionPersistenceUnavailable; } app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const store = if (app.session_persistence.store) |value| value else return error.SessionPersistenceUnavailable; const loaded = if (app.session_persistence.writable) |*value| value else return error.SessionPersistenceUnavailable; const recovery_checkpoint = try store.prepareUsageRecoveryCheckpoint( app.alloc, loaded, snapshot, ); try convergeDegradedAt( app, loaded, .{}, recovery_checkpoint.timestamp_ms, ); _ = try loaded.appendEvent( app.alloc, .{ .usage_checkpointed = .{ .usage = snapshot } }, recovery_checkpoint.timestamp_ms, .retry_expected_tail, .{ .checkpoint_interval = 0 }, ); try store.finishUsageRecoveryCheckpoint( loaded.active_id, recovery_checkpoint, ); } pub fn setRecoveryCheckpoint( app: *App, checkpoint: session_codec.RecoveryCheckpoint, ) !void { if (comptime !@hasField(App, "session_persistence")) { return error.SessionPersistenceUnavailable; } app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return error.SessionPersistenceUnavailable; try convergeDegraded(app, loaded, .{}); const now_ms = io_mod.milliTimestamp(); _ = loaded.appendEvent( app.alloc, .{ .recovery_checkpoint_set = .{ .checkpoint = checkpoint } }, now_ms, .retry_expected_tail, .{}, ) catch |err| switch (err) { error.EventFrameTooLarge => { var current = try snapshotCurrentState(app, loaded.state, now_ms); defer current.deinit(app.alloc); if (current.recovery_checkpoint) |*old| old.deinit(app.alloc); current.recovery_checkpoint = try checkpoint.dupe(app.alloc); _ = try loaded.commitStateReplacement( app.alloc, current, .compaction, .retry_expected_tail, .{}, ); }, else => return err, }; } pub fn snapshotRecoveryCheckpoint( app: *App, alloc: Allocator, ) !?session_codec.RecoveryCheckpoint { if (comptime !@hasField(App, "session_persistence")) return null; app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return null; const checkpoint = loaded.state.recovery_checkpoint orelse return null; return try checkpoint.dupe(alloc); } pub fn continuePausedRecovery(app: *App) !bool { var checkpoint = (try snapshotRecoveryCheckpoint( app, std.heap.c_allocator, )) orelse return false; defer checkpoint.deinit(std.heap.c_allocator); app.session.setConversationLanguageFromUserMessage(checkpoint.user.text); return app.queueRecoveryCheckpoint(&checkpoint); } pub fn appendHistoryTurnForVisualEpoch( app: *App, turn: types.HistoryTurn, ) !HistoryAppendOutcome { return appendHistoryTurnWithPendingPresentation( app, turn, .visual_epoch, null, ); } pub fn appendFinishedPromptForVisualEpoch( app: *App, finished: types.FinishedPrompt, ) !HistoryAppendOutcome { return appendHistoryTurnWithPendingPresentation( app, finished.turn, .visual_epoch, finished.snapshot_file_ownership, ); } const AppendHistoryMode = enum { strict, visual_epoch, }; fn appendHistoryTurnWithPendingPresentation( app: *App, turn: types.HistoryTurn, mode: AppendHistoryMode, snapshot_file_ownership: ?types.SnapshotFileOwnership, ) !HistoryAppendOutcome { if (comptime !@hasField(App, "session_persistence")) { return appendHistoryTurnWithOutcome(app, turn, mode, snapshot_file_ownership); } const interrupted = switch (turn) { .interrupted => |entry| entry, else => return appendHistoryTurnWithOutcome(app, turn, mode, snapshot_file_ownership), }; var pending = app.session_persistence.pending_cancelled_command orelse { app.session_persistence.disabled_cancelled_command_capture = null; return appendHistoryTurnWithOutcome(app, turn, mode, snapshot_file_ownership); }; app.session_persistence.pending_cancelled_command = null; const call = interrupted.tool_call orelse { pending.discard(app.alloc); return appendHistoryTurnWithOutcome(app, turn, mode, snapshot_file_ownership); }; const is_command = try captured_command.isToolCall( app.alloc, call.name, call.arguments_json, ); if (!pending.cancelled or !pending.completed or !is_command or !std.mem.eql(u8, call.id, pending.lifecycle_id.call_id)) { pending.discard(app.alloc); return appendHistoryTurnWithOutcome(app, turn, mode, snapshot_file_ownership); } const output_replay = if (pending.capture) |capture| capture.retain(app.alloc) else null; var enriched_interrupted = interrupted; enriched_interrupted.cancelled_command = .{ .output_replay = output_replay, .command_artifact_handle = pending.command_artifact_handle, }; const enriched: types.HistoryTurn = .{ .interrupted = enriched_interrupted }; defer pending.releaseRetained(app.alloc); return appendHistoryTurnWithOutcome(app, enriched, mode, snapshot_file_ownership); } fn ensurePendingCancelledCommand( app: *App, id: types.ToolLifecycleId, capture_enabled: bool, ) ?*PendingCancelledCommand { if (app.session_persistence.pending_cancelled_command) |*pending| { if (pending.matches(id)) return pending; discardAnyPendingCancelledCommand(app, "lifecycle_replaced"); } const call_id = app.alloc.dupe(u8, id.call_id) catch |err| { app.session_persistence.disabled_cancelled_command_capture = DisabledCancelledCommandCapture.init(id); debug_trace.logf( "session", "cancelled command capture identity unavailable call_id={s} err={s}", .{ id.call_id, @errorName(err) }, ); return null; }; errdefer app.alloc.free(call_id); const capability = childCapability(app); const capture = if (capture_enabled) command_replay_store.Capture.create( app.alloc, default_cancelled_command_replay_inline_limit, capability, ) catch |err| blk: { debug_trace.logf( "session", "cancelled command replay capture unavailable call_id={s} err={s}", .{ id.call_id, @errorName(err) }, ); break :blk null; } else null; app.session_persistence.pending_cancelled_command = .{ .lifecycle_id = .{ .turn_id = id.turn_id, .call_id = call_id, }, .capture = capture, }; return &app.session_persistence.pending_cancelled_command.?; } fn discardPendingCancelledCommand( app: *App, id: types.ToolLifecycleId, reason: []const u8, ) void { const pending = if (app.session_persistence.pending_cancelled_command) |*value| value else return; if (!pending.matches(id)) return; discardAnyPendingCancelledCommand(app, reason); } fn discardAnyPendingCancelledCommand(app: *App, reason: []const u8) void { const pending = if (app.session_persistence.pending_cancelled_command) |*value| value else return; debug_trace.logf( "session", "cancelled command replay candidate discarded call_id={s} reason={s}", .{ pending.lifecycle_id.call_id, reason }, ); pending.discard(app.alloc); app.session_persistence.pending_cancelled_command = null; } fn appendHistoryTurnWithOutcome( app: *App, turn: types.HistoryTurn, mode: AppendHistoryMode, snapshot_file_ownership: ?types.SnapshotFileOwnership, ) !HistoryAppendOutcome { var inserted = false; defer { if (comptime @hasField(App, "session_persistence")) { if (!inserted) discardUncommittedCancelledReplay(app, turn); } } app.session.appendHistoryEntry(app.alloc, turn) catch |err| { return switch (mode) { .strict => err, .visual_epoch => .uncommitted, }; }; inserted = true; if (snapshot_file_ownership) |ownership| ownership.transfer(); // The footer title freezes at the first usable prompt, matching the // sidecar derivation the resume picker shows. ensureCachedSessionTitle(app) catch {}; commitJsHostSnapshot(app, "history_turn"); if (comptime !@hasField(App, "session_persistence")) return .committed; app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return .committed; try subagent_resume_admission.retainExternalRootUserTurn( app.alloc, loaded, turn, ); convergeDegraded(app, loaded, .{}) catch |err| { return switch (mode) { .strict => err, .visual_epoch => blk: { debug_trace.logf( "session", "visual epoch history committed with degraded persistence err={s}", .{@errorName(err)}, ); break :blk .committed_degraded; }, }; }; _ = loaded.appendEvent( app.alloc, .{ .history_turn_committed = .{ .conversation_language = app.session.languageSnapshot(), .total_input_tokens = app.total_input_tokens, .total_output_tokens = app.total_output_tokens, .turn = turn, } }, io_mod.milliTimestamp(), .retry_expected_tail, .{}, ) catch |err| switch (err) { error.EventFrameTooLarge => { commitCurrentStateReplacement( app, loaded, .compaction, .{}, true, ) catch |replacement_err| { return switch (mode) { .strict => replacement_err, .visual_epoch => .committed_degraded, }; }; return .committed; }, else => { switch (mode) { .strict => try warnDegraded(app, err), .visual_epoch => debug_trace.logf( "session", "visual epoch history committed with degraded persistence err={s}", .{@errorName(err)}, ), } return .committed_degraded; }, }; return .committed; } fn discardUncommittedCancelledReplay( app: *App, turn: types.HistoryTurn, ) void { const interrupted = switch (turn) { .interrupted => |entry| entry, else => return, }; const presentation = interrupted.cancelled_command orelse return; const replay = presentation.output_replay orelse return; const descriptor = switch (replay) { .available => |value| value, .unavailable => return, }; const capability = childCapability(app) orelse { debug_trace.logf( "session", "uncommitted cancelled replay cleanup unavailable handle_bytes={d}", .{descriptor.handle.len}, ); return; }; capability.delete(.command_artifacts, descriptor.handle) catch |err| { debug_trace.logf( "session", "uncommitted cancelled replay cleanup failed handle_bytes={d} err={s}", .{ descriptor.handle.len, @errorName(err) }, ); }; } pub fn compactHistory(app: *App) !void { const previous_start = app.session.contextHistoryStart(); app.session.forceCompaction(); if (app.session.contextHistoryStart() == previous_start) return; commitJsHostSnapshot(app, "compaction"); app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return; try convergeDegraded(app, loaded, .{}); try commitCurrentStateReplacement(app, loaded, .compaction, .{}, false); } pub fn commitRuntimePreferences( app: *App, patch: SessionPreferencePatch, ) PreferenceCommitResult { var result = PreferenceCommitResult{}; applySessionPreferencePatch(app, patch) catch |err| { result.session_error = err; }; var settings_attempt = config_runtime.attemptUserPreferences( app.alloc, patch.userSettingsPatch(), ); switch (settings_attempt) { .outcome => |outcome| { result.settings_outcome = outcome; settings_attempt = undefined; }, .failure => |failure| { result.settings_error = failure.err; result.settings_failure_cleanup = failure.cleanup; settings_attempt = undefined; }, } if (result.settings_error == null) { applyPreferencePatch( app.alloc, &app.session_persistence.workspace_preferences, patch, ) catch |err| { result.session_error = err; }; } if (result.session_error == null) { commitJsHostSnapshot(app, "preferences"); } app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return result; if (result.session_error != null) return result; convergeDegraded(app, loaded, .{}) catch |err| { result.session_error = err; return result; }; _ = loaded.appendEvent( app.alloc, .{ .preferences_changed = .{ .model = if (patch.model) |model| @constCast(model) else null, .effort = patch.effort, .fast_mode = patch.fast_mode, } }, io_mod.milliTimestamp(), .retry_expected_tail, .{}, ) catch |err| { result.session_error = err; warnDegraded(app, err) catch {}; }; return result; } pub fn activeSessionId(app: *App) ?[]const u8 { if (app.session_persistence.writable) |*loaded| return loaded.active_id; if (app.session_persistence.js_host_session) |*owner| return owner.state.id; return null; } /// Replaces the cached footer title. App owns the copy. pub fn setCachedSessionTitle(app: *App, title: []const u8) !void { if (comptime !@hasField(App, "session_title")) return; app.session_title.clearRetainingCapacity(); try app.session_title.appendSlice(app.alloc, title); } pub fn clearCachedSessionTitle(app: *App) void { if (comptime !@hasField(App, "session_title")) return; app.session_title.clearRetainingCapacity(); } pub fn cachedSessionTitle(app: *App) ?[]const u8 { if (comptime !@hasField(App, "session_title")) return null; return if (app.session_title.items.len == 0) null else app.session_title.items; } /// Derives and caches the title on the first turn of a fresh session. /// Derivation freezes at the first usable prompt, so this is a no-op /// once a title is cached. pub fn ensureCachedSessionTitle(app: *App) !void { if (comptime !@hasField(App, "session_title")) return; if (app.session_title.items.len > 0) return; var display = session_display_metadata.deriveFromHistory( app.alloc, app.session.history.items, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return, }; defer display.deinit(app.alloc); if (!display.present) return; try setCachedSessionTitle(app, display.title); } pub const RenameError = error{ EmptyTitle, TitleTooLong, InvalidTitle, NoActiveSession, }; /// Validates a user-supplied session title. Returns the trimmed slice, /// which borrows from `raw`. pub fn validateSessionTitle(raw: []const u8) RenameError![]const u8 { const trimmed = std.mem.trim(u8, raw, " \t\r\n"); if (trimmed.len == 0) return error.EmptyTitle; if (trimmed.len > session_display_metadata.max_title_bytes) return error.TitleTooLong; if (!std.unicode.utf8ValidateSlice(trimmed)) return error.InvalidTitle; for (trimmed) |byte| { if (byte < 0x20 or byte == 0x7f) return error.InvalidTitle; } return trimmed; } /// Renames the active session: caches the title for the footer and /// persists it to the display sidecar and the session index so the /// resume picker does not keep serving the derived title. pub fn renameActiveSession(app: *App, raw: []const u8) !void { const title = try validateSessionTitle(raw); if (comptime !@hasField(App, "session_persistence")) return error.NoActiveSession; if (app.session_persistence.writable == null) return error.NoActiveSession; try setCachedSessionTitle(app, title); app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = &app.session_persistence.writable.?; var display = session_display_metadata.readSidecarOrFallback( app.alloc, &loaded.log.dir, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => try session_display_metadata.missingFallback(app.alloc), }; defer display.deinit(app.alloc); const owned_title = try app.alloc.dupe(u8, title); app.alloc.free(display.title); display.title = owned_title; display.present = true; if (display.origin_workspace_root == null) { display.origin_workspace_root = try app.alloc.dupe(u8, loaded.state.origin_workspace_root); } try session_display_metadata.writeSidecar(app.alloc, &loaded.log.dir, display); if (app.session_persistence.store) |*store| { store.updateIndexedTitle(app.alloc, loaded.active_id, title) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => debug_trace.logf( "session", "event=rename_index_update_failed id={s} err={s}", .{ loaded.active_id, @errorName(err) }, ), }; } } pub fn activeSessionDisplayPath( app: *App, alloc: Allocator, ) !?[]u8 { const loaded = if (app.session_persistence.writable) |*value| value else return null; const store = app.session_persistence.store orelse return null; return try session_store.sessionDirPath( alloc, store.sessions_dir, loaded.active_id, ); } pub fn childCapability(app: *App) ?*session_child_store.SessionChildCapability { const loaded = if (app.session_persistence.writable) |*value| value else return null; return loaded.childCapability() catch null; } pub fn subagentHost(app: *App) ?*subagent_tool_host.Runtime { return app.session_persistence.subagent_host; } pub fn rebindSubagentHost(app: *App) void { const host = app.session_persistence.subagent_host orelse return; const store = if (app.session_persistence.store) |*value| value else return; host.rebind(store, app, subagentAuthorityResolver(app)); requestSubagentBackgroundRecovery(app); } fn requestSubagentBackgroundRecovery(app: *App) void { const host = app.session_persistence.subagent_host orelse return; host.requestBackgroundRecovery(io_mod.milliTimestamp()) catch |err| { debug_trace.logf( "subagent", "interactive background recovery unavailable root_id={s} outcome={s}", .{ host.root_id, @errorName(err) }, ); }; } pub fn disableSubagentHost(app: *App) void { if (app.session_persistence.subagent_host) |host| { host.deinit(); app.session_persistence.subagent_host = null; } } pub fn finalizePersistence(app: *App) void { if (app.session_persistence.writable) |*loaded| { if (loaded.log.isParked()) { app.session_persistence.resume_handoff_intent = .none; abandonParkedWritableSession(app); return; } } closeWritableSession(app, .{}); } pub fn finalizePersistenceWithResumeHandoff(app: *App) ?ResumeHandoff { if (app.session_persistence.writable) |*loaded| { if (loaded.log.isParked()) { app.session_persistence.resume_handoff_intent = .none; abandonParkedWritableSession(app); return null; } } return closeWritableSessionWithResumeHandoff(app, .{}); } pub fn requestResumeHandoff(app: *App) void { app.session_persistence.resume_handoff_intent = .requested; } pub fn requestUpgradeResumeHandoff(app: *App) void { app.session_persistence.resume_handoff_intent = .upgrade_requested; } pub fn prepareResumeHandoff(app: *App) !void { app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return error.SessionPersistenceUnavailable; loaded.validateResumeBoundary(app.alloc, .{}) catch |err| { if (err != error.InvalidSessionFormat and err != error.FileNotFound) { return err; } try loaded.repairResumeBoundary(app.alloc, .{}); debug_trace.logf( "session", "event=resume_boundary_repaired session={s}", .{loaded.active_id}, ); }; try settleResumeBoundary(app, loaded, .{}); } pub fn suspendToJobControl(app: *App, footer_rows: u16) !void { if (!shell_runtime.supports_resize_signal) return; if (!tryBeginIdleSessionPark(app)) { return app_lifecycle.suspendToJobControl( &app.terminal, &app.shell, &app.metrics, footer_rows, ); } defer app.worker.releaseTurnStartHold(); const loaded = &app.session_persistence.writable.?; detachManagedBackground(app, loaded); loaded.log.park(); debug_trace.logf( "session", "parked writer lock for suspend session={s}", .{loaded.active_id}, ); const lifecycle_result = app_lifecycle.suspendToJobControl( &app.terminal, &app.shell, &app.metrics, footer_rows, ); loaded.log.unpark() catch |err| { debug_trace.logf( "session", "unpark after suspend failed session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); abandonParkedWritableSession(app); app.worker.requestStop(); app.should_exit = true; try lifecycle_result; return; }; debug_trace.logf( "session", "unparked writer lock after suspend session={s}", .{loaded.active_id}, ); const capability = loaded.childCapability() catch |err| { debug_trace.logf( "session", "interactive child capability unavailable session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); try lifecycle_result; return; }; restoreManagedBackground(app, loaded, capability); try lifecycle_result; } fn tryBeginIdleSessionPark(app: *App) bool { if (app.session_persistence.writable == null or app.stream.active) { return false; } return app.worker.tryHoldTurnStart(); } /// Tear down a parked writable without converging or checkpointing. /// Background authority must already be detached (or is detached here). fn abandonParkedWritableSession(app: *App) void { discardAnyPendingCancelledCommand(app, "writable_session_abandon"); app.session_persistence.disabled_cancelled_command_capture = null; const loaded = if (app.session_persistence.writable) |*value| value else return; detachManagedBackground(app, loaded); if (comptime @hasDecl( @TypeOf(app.session), "clearWebFetchArtifacts", )) { app.session.clearWebFetchArtifacts(); } disableSubagentHost(app); debug_trace.logf( "session", "abandon parked writable session={s}", .{loaded.active_id}, ); loaded.deinit(app.alloc); app.session_persistence.writable = null; } fn detachManagedBackground( app: *App, loaded: *session_store.LoadedWritableSession, ) void { if (comptime @hasField(App, "background") and @hasDecl(@TypeOf(app.background), "detachManagedPersistence")) { app.background.detachManagedPersistence( std.heap.c_allocator, loaded.active_id, ); } } pub fn deinitPersistence(app: *App) void { closeWritableSession(app, .{}); app.session_persistence.deinit(app.alloc); } fn LiveHistorySink(comptime SinkApp: type) type { return struct { app: *SinkApp, const Self = @This(); fn appendNotice(self: *Self, notice: types.SemanticNotice) !void { try self.app.writeDomainNotice(notice, true); } fn appendUserTurn(self: *Self, user: types.UserTurn, has_prior_turns: bool) !void { try self.app.writeUserPromptCardWithSpacing(user, has_prior_turns); } fn appendRaw(self: *Self, text: []const u8) !void { try self.app.writeTranscript(text, true); } fn appendToolStatus( self: *Self, kind: types.ToolOutcomeKind, status: []const u8, ) !u32 { if (comptime @hasDecl(SinkApp, "writeCompletedToolStatusReturningEntryId")) { return self.app.writeCompletedToolStatusReturningEntryId(kind, status, true); } try self.app.writeCompletedToolStatus(kind, status, true); return 0; } fn appendCommandOutput( self: *Self, stream: command_output_content.Stream, text: []const u8, ) !void { try self.app.writeCommandOutputChunk(stream, text, true); } fn finishCommandOutput(self: *Self) !void { try self.app.flushCommandOutputSummary(true); } fn attachHistoricalToolDetail( self: *Self, entry_id: u32, call: types.ToolCall, result: types.PersistedToolResult, ) !void { if (comptime @hasDecl(SinkApp, "attachHistoricalToolDetail")) { try self.app.attachHistoricalToolDetail(entry_id, call, result); } } fn attachHistoricalToolDetailWithLifecycle( self: *Self, entry_id: u32, call: types.ToolCall, result: types.PersistedToolResult, lifecycle_id: types.ToolLifecycleId, ) !void { if (comptime @hasDecl(SinkApp, "attachHistoricalToolDetailWithLifecycle")) { try self.app.attachHistoricalToolDetailWithLifecycle( entry_id, call, result, lifecycle_id, ); } else { try self.attachHistoricalToolDetail(entry_id, call, result); } } fn attachHistoricalToolDetailAfterCommandOutput( self: *Self, entry_id: u32, call: types.ToolCall, result: types.PersistedToolResult, ) !void { if (comptime @hasDecl(SinkApp, "attachHistoricalToolDetailAfterCommandOutput")) { try self.app.attachHistoricalToolDetailAfterCommandOutput( entry_id, call, result, ); } else { try self.attachHistoricalToolDetail(entry_id, call, result); } } fn attachHistoricalToolCallWithoutResult( self: *Self, entry_id: u32, call: types.ToolCall, ) !void { if (comptime @hasDecl(SinkApp, "attachHistoricalToolCallWithoutResult")) { try self.app.attachHistoricalToolCallWithoutResult(entry_id, call); } } fn attachHistoricalCommandOutput(self: *Self, entry_id: u32) void { if (comptime @hasDecl(SinkApp, "attachHistoricalCommandOutput")) { self.app.attachHistoricalCommandOutput(entry_id); } } fn attachHistoricalCancelledCommandDetail( self: *Self, entry_id: u32, call: types.ToolCall, command_artifact_handle: ?[]const u8, replayed_output: bool, ) !void { if (comptime @hasDecl(SinkApp, "attachHistoricalCancelledCommandDetail")) { try self.app.attachHistoricalCancelledCommandDetail( entry_id, call, command_artifact_handle, replayed_output, ); } } fn appendQuestionResolution( self: *Self, answers: []const types.QuestionAnswer, ) !u32 { return self.app.writeHistoricalQuestionResolution(answers); } fn appendDiff(self: *Self, payload: diff.DiffEntryPayload) !void { try self.app.registerAndEmitDiffBlock(payload); } fn appendAssistantText(self: *Self, text: []const u8) !void { try SinkApp.pacerEmit(self.app, text); } fn appendAssistantTable( self: *Self, table: assistant_presentation.TablePayload, ) !void { try self.app.appendAssistantTable(table); } fn appendAssistantCodeBlock( self: *Self, block: assistant_presentation.CodeBlockPayload, ) !void { try self.app.appendAssistantCodeBlock(block); } fn appendAssistantThematicRule(self: *Self) !void { try self.app.appendAssistantThematicRule(); } }; } fn DetachedHistorySink(comptime Projection: type) type { return struct { app: *App, projection: *Projection, const Self = @This(); fn activityKind(self: *Self, call: types.ToolCall) types.ToolActivityKind { return self.app.historicalToolActivityKind(call); } fn appendNotice(self: *Self, notice: types.SemanticNotice) !void { _ = try self.projection.appendNotice(notice); } fn appendUserTurn(self: *Self, user: types.UserTurn, _: bool) !void { _ = try self.projection.appendUserTurn(user); } fn appendRaw(self: *Self, text: []const u8) !void { _ = try self.projection.appendRawClassified(text, .unknown_raw); } fn appendToolStatus( self: *Self, kind: types.ToolOutcomeKind, status: []const u8, ) !u32 { return self.projection.appendToolStatus(kind, status); } fn appendCommandOutput( self: *Self, stream: command_output_content.Stream, text: []const u8, ) !void { try self.projection.appendCommandOutput(null, stream, text); } fn finishCommandOutput(self: *Self) !void { try self.projection.finishCommandOutput(null); } fn attachHistoricalToolDetail( self: *Self, entry_id: u32, call: types.ToolCall, result: types.PersistedToolResult, ) !void { try self.projection.attachHistoricalToolDetail( entry_id, call, self.activityKind(call), result, ); } fn attachHistoricalToolDetailWithLifecycle( self: *Self, entry_id: u32, call: types.ToolCall, result: types.PersistedToolResult, lifecycle_id: types.ToolLifecycleId, ) !void { try self.projection.attachHistoricalToolDetailWithLifecycle( entry_id, call, self.activityKind(call), result, lifecycle_id, ); } fn attachHistoricalToolDetailAfterCommandOutput( self: *Self, entry_id: u32, call: types.ToolCall, result: types.PersistedToolResult, ) !void { try self.projection.attachHistoricalToolDetailAfterCommandOutput( entry_id, call, self.activityKind(call), result, ); } fn attachHistoricalToolCallWithoutResult( self: *Self, entry_id: u32, call: types.ToolCall, ) !void { try self.projection.attachHistoricalToolCallWithoutResult(entry_id, call); } fn attachHistoricalCommandOutput(self: *Self, entry_id: u32) void { self.projection.attachHistoricalCommandOutput(entry_id); } fn attachHistoricalCancelledCommandDetail( self: *Self, entry_id: u32, call: types.ToolCall, command_artifact_handle: ?[]const u8, replayed_output: bool, ) !void { try self.projection.attachHistoricalCancelledCommandDetail( entry_id, call, command_artifact_handle, replayed_output, ); } fn appendQuestionResolution( self: *Self, answers: []const types.QuestionAnswer, ) !u32 { const text = try self.app.prepareHistoricalQuestionResolution(answers); defer self.app.alloc.free(text); return self.projection.appendReplaceableLine(text); } fn appendDiff(self: *Self, payload: diff.DiffEntryPayload) !void { try self.projection.appendDiff(payload); } fn appendAssistantText(self: *Self, text: []const u8) !void { try self.projection.appendAssistantText(text); } fn appendAssistantTable( self: *Self, table: assistant_presentation.TablePayload, ) !void { try self.projection.appendAssistantTable(table); } fn appendAssistantCodeBlock( self: *Self, block: assistant_presentation.CodeBlockPayload, ) !void { try self.projection.appendAssistantCodeBlock(block); } fn appendAssistantThematicRule(self: *Self) !void { try self.projection.appendAssistantThematicRule(); } }; } fn readNativeResumeDisplay( app: *App, session: *session_store.LoadedWritableSession, ) !session_display_metadata.DisplayMetadata { // The sidecar title is frozen at first derivation; prefer it so the // notice matches the picker row the user selected. The notice is // cosmetic, so read failures fall back instead of failing the resume. var display = session_display_metadata.readSidecarOrFallback(app.alloc, &session.log.dir) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => blk: { debug_trace.logf( "session", "event=resume_notice_sidecar_read_failed id={s} err={s}", .{ session.log.session_id, @errorName(err) }, ); break :blk try session_display_metadata.deriveFromHistory(app.alloc, session.state.history); }, }; if (!display.present) { display.deinit(app.alloc); display = try session_display_metadata.deriveFromHistory(app.alloc, session.state.history); } return display; } fn writeResumeNotice( app: *App, sink: anytype, display_title: []const u8, notice: ResumeNotice, ) !void { try setCachedSessionTitle(app, display_title); switch (notice) { .session => try sink.appendNotice(.{ .topic = "session resumed", .tone = .neutral, .body = display_title, }), .upgrade => |version| { const body = try std.fmt.allocPrint( app.alloc, "𝒇x has been updated to v{s}", .{version}, ); defer app.alloc.free(body); try sink.appendNotice(.{ .topic = "", .tone = .success, .body = body, }); }, } } fn writeRecoveryCheckpointToSink( app: *App, sink: anytype, state: session_codec.DurableSessionState, ) !void { const checkpoint = state.recovery_checkpoint orelse return; var has_prior_turns = false; for (state.history) |turn| switch (turn) { .compacted_summary => {}, .assistant, .background_command, .interrupted => { has_prior_turns = true; break; }, }; try sink.appendUserTurn(checkpoint.user, has_prior_turns); try writeExecutionHistoryToSink(app, sink, checkpoint.execution); if (checkpoint.assistant_source.len > 0) { try writeAssistantHistoryMarkdownToSink( app, sink, checkpoint.assistant_source, ); } const recovery_notice = if (checkpoint.tool_state == .uncertain) try std.fmt.allocPrint( app.alloc, "model response recovery is paused at attempt {d}/{d}; inspect the uncertain tool state before /continue", .{ checkpoint.consumed_provider_attempts +| @intFromBool(checkpoint.outstanding_reservation), checkpoint.max_provider_attempts, }, ) else try std.fmt.allocPrint( app.alloc, "model response recovery is paused at attempt {d}/{d}; run /continue to resume the preserved turn", .{ checkpoint.consumed_provider_attempts +| @intFromBool(checkpoint.outstanding_reservation), checkpoint.max_provider_attempts, }, ); defer app.alloc.free(recovery_notice); try sink.appendNotice(.{ .topic = "recovery", .tone = .warning, .body = recovery_notice, }); } fn replayHistory(app: *App, history: []const types.HistoryTurn) !void { var sink = LiveHistorySink(App){ .app = app }; return replayHistoryToSink(app, &sink, history); } fn replayHistoryToSink(app: *App, sink: anytype, history: []const types.HistoryTurn) !void { var has_prior_turns = false; return replayHistoryToSinkIncremental(app, sink, history, &has_prior_turns); } fn replayHistoryToSinkIncremental( app: *App, sink: anytype, history: []const types.HistoryTurn, has_prior_turns: *bool, ) !void { for (history) |turn| { switch (turn) { .compacted_summary => |entry| { const text = try session_runtime.formatCompactedContinuationMessage(app.alloc, entry.summary); defer app.alloc.free(text); try sink.appendNotice(.{ .topic = "session", .tone = .neutral, .body = text, }); }, .assistant => |entry| { try sink.appendUserTurn(entry.user, has_prior_turns.*); has_prior_turns.* = true; try writeExecutionHistoryToSink(app, sink, entry.execution); if (entry.assistant.len > 0) { try writeAssistantHistoryMarkdownToSink(app, sink, entry.assistant); } }, .background_command => |entry| { try sink.appendUserTurn(entry.user, has_prior_turns.*); has_prior_turns.* = true; try writeExecutionHistoryToSink(app, sink, entry.execution); if (entry.assistant) |assistant| { if (assistant.len > 0) try writeAssistantHistoryMarkdownToSink(app, sink, assistant); } const text = try formatBackgroundReplayContext(app, entry); defer app.alloc.free(text); try sink.appendNotice(.{ .topic = "session", .tone = .neutral, .body = text, }); }, .interrupted => |entry| { try sink.appendUserTurn(entry.user, has_prior_turns.*); has_prior_turns.* = true; try writeExecutionHistoryToSink(app, sink, entry.execution); if (entry.assistant) |assistant| { if (assistant.len > 0) try writeAssistantHistoryMarkdownToSink(app, sink, assistant); } if (entry.cancelled_command) |presentation| { try writeCancelledCommandPresentation(app, sink, entry.tool_call.?, presentation); } try sink.appendNotice(session_runtime.interruptedTurnNotice(entry)); }, } } } /// Replay canonical session turns into a detached presentation using /// the exact same tool, message, markdown, diff, and command-output /// adapters as ordinary session resume. pub fn appendHistoryToDetachedProjection( app: *App, projection: anytype, history: []const types.HistoryTurn, has_prior_turns: *bool, ) !void { var sink = DetachedHistorySink(@TypeOf(projection.*)){ .app = app, .projection = projection, }; return replayHistoryToSinkIncremental( app, &sink, history, has_prior_turns, ); } fn writeCancelledCommandPresentation( app: *App, sink: anytype, call: types.ToolCall, presentation: types.CancelledCommandPresentation, ) !void { var action_arena = std.heap.ArenaAllocator.init(app.alloc); defer action_arena.deinit(); const action = try app.describeToolActionDeniedWithAdvertised( action_arena.allocator(), call, null, "Cancelled", &.{}, ); const entry_id = try writeCompletedToolStatus(sink, .cancelled, action); const replayed_output = try writeCommandReplay( app, sink, presentation.output_replay, call.id, ); try sink.attachHistoricalCancelledCommandDetail( entry_id, call, presentation.command_artifact_handle, replayed_output, ); } fn writeExecutionHistory(app: *App, execution: types.ExecutionMemory) !void { var sink = LiveHistorySink(App){ .app = app }; return writeExecutionHistoryToSink(app, &sink, execution); } fn writeExecutionHistoryToSink( app: *App, sink: anytype, execution: types.ExecutionMemory, ) !void { for (execution.tool_steps) |step| { if (step.assistant) |assistant| { if (assistant.len > 0) try writeAssistantHistoryMarkdownToSink(app, sink, assistant); } for (step.tool_calls) |call| { if (findPersistedToolResult(step.tool_results, call.id)) |result| { try writeCompletedToolResult(app, sink, call, result); try writePermissionFeedback(sink, result.permission_feedback); } else { try writeUnreportedToolStatus(app, sink, call); } } for (step.tool_results) |result| { if (!hasPersistedToolCall(step.tool_calls, result.tool_call_id)) { try writeUnpairedToolResult(app, sink, result); try writePermissionFeedback(sink, result.permission_feedback); } } } } fn writePermissionFeedback(sink: anytype, feedback: []const []const u8) !void { for (feedback) |text| { if (text.len == 0) continue; try sink.appendUserTurn(.{ .text = @constCast(text), }, true); } } fn findPersistedToolResult( results: []const types.PersistedToolResult, tool_call_id: []const u8, ) ?types.PersistedToolResult { for (results) |result| { if (std.mem.eql(u8, result.tool_call_id, tool_call_id)) return result; } return null; } fn hasPersistedToolCall( calls: []const types.ToolCall, tool_call_id: []const u8, ) bool { for (calls) |call| { if (std.mem.eql(u8, call.id, tool_call_id)) return true; } return false; } fn writeCompletedToolResult( app: *App, sink: anytype, call: types.ToolCall, result: types.PersistedToolResult, ) !void { if (try writeAnsweredQuestionResult(app, sink, call, result)) return; if (try writeCommittedFilePresentation(app, sink, call, result)) return; const is_command = try captured_command.isToolCall( app.alloc, call.name, call.arguments_json, ); const context_deferred = types.isContextDeferredToolResult(result); const deferred = types.isDeferredToolResult(result); const process_ran = result.command_process_presentation != null; var action_arena = std.heap.ArenaAllocator.init(app.alloc); defer action_arena.deinit(); const formatted_action = if (deferred) try app.describeToolActionDeniedWithAdvertised( action_arena.allocator(), call, null, if (context_deferred) types.context_deferred_tool_status_label else types.deferred_tool_result_output, &.{}, ) else if (result.status == .success or process_ran) try app.describeToolActionCompletedWithAdvertised( action_arena.allocator(), call, null, &.{}, ) else try app.describeToolActionDeniedWithAdvertised( action_arena.allocator(), call, null, "Failed", &.{}, ); const action = try app.alloc.dupe(u8, formatted_action); defer app.alloc.free(action); const outcome: types.ToolOutcomeKind = if (context_deferred) .deferred else if (deferred) .denied else if (result.status == .success or process_ran) .completed else .failed; const entry_id = try writeCompletedToolStatus( sink, outcome, action, ); if (!is_command or deferred) { try sink.attachHistoricalToolDetail(entry_id, call, result); return; } const wrote_command_output = try writeCommandReplay( app, sink, result.command_output_replay, result.tool_call_id, ) or try writeStoredCommandOutput(app, sink, result) or try writeSavedCommandOutput(sink, result.output); // Attach terminal detail after historical command chunks so typed // process presentation joins the existing block instead of // synthesizing a second, empty command-output block. if (wrote_command_output) { try sink.attachHistoricalToolDetailAfterCommandOutput( entry_id, call, result, ); } else { try sink.attachHistoricalToolDetail(entry_id, call, result); } if (wrote_command_output) { sink.attachHistoricalCommandOutput(entry_id); return; } debug_trace.logf( "session", "resume command replay fell back to raw result call_id={s}", .{result.tool_call_id}, ); try writeReplayResultFallback(sink, result.output); } fn writeCommandReplay( app: *App, sink: anytype, replay_value: ?types.CommandOutputReplay, call_id: []const u8, ) !bool { const replay = replay_value orelse return false; const descriptor = switch (replay) { .available => |value| value, .unavailable => { debug_trace.logf( "session", "resume command replay unavailable call_id={s}", .{call_id}, ); return false; }, }; const loaded = if (app.session_persistence.writable) |*value| value else return false; const capability = loaded.childCapability() catch |err| { debug_trace.logf( "session", "resume command replay capability unavailable handle_bytes={d} err={s}", .{ descriptor.handle.len, @errorName(err) }, ); return false; }; if (!validateCommandReplay(app.alloc, capability, descriptor)) { debug_trace.logf( "session", "resume command replay invalid handle_bytes={d}", .{descriptor.handle.len}, ); return false; } var reader = command_replay_store.Reader.open( app.alloc, capability, descriptor, ) catch |err| { debug_trace.logf( "session", "resume command replay reopen failed handle_bytes={d} err={s}", .{ descriptor.handle.len, @errorName(err) }, ); return false; }; defer reader.deinit(); var wrote = false; while (try reader.next(app.alloc)) |frame| { defer app.alloc.free(frame.payload); try sink.appendCommandOutput(frame.stream, frame.payload); wrote = true; } if (wrote) try sink.finishCommandOutput(); return wrote; } fn validateCommandReplay( alloc: Allocator, capability: *session_child_store.SessionChildCapability, descriptor: types.CommandOutputReplayDescriptor, ) bool { var reader = command_replay_store.Reader.open( alloc, capability, descriptor, ) catch return false; defer reader.deinit(); while (reader.nextByte() catch return false) |_| {} return true; } fn writeCommittedFilePresentation( app: *App, sink: anytype, call: types.ToolCall, result: types.PersistedToolResult, ) !bool { const presentation = result.committed_file_presentation orelse return false; if (result.status != .success or (!std.mem.eql(u8, result.tool_name, "write_file") and !std.mem.eql(u8, result.tool_name, "edit_file")) or !@hasDecl(App, "preparePersistedFileDiff") or !@hasDecl(App, "registerAndEmitDiffBlock")) return false; const payload = app.preparePersistedFileDiff(presentation) catch |err| { debug_trace.logf( "session", "resume committed file presentation unavailable call_id={s} err={s}", .{ result.tool_call_id, @errorName(err) }, ); return false; }; var owns_payload = true; errdefer if (owns_payload) diff.freeDiffEntryPayload(std.heap.c_allocator, payload); var action_arena = std.heap.ArenaAllocator.init(app.alloc); defer action_arena.deinit(); const base = try app.describeToolActionCompletedWithAdvertised( action_arena.allocator(), call, presentation.path, &.{}, ); const action = try tool_presentation.formatToolStatusWithStats( action_arena.allocator(), base, presentation.additions, presentation.deletions, .{ .added = ui_render.diff_added_marker_style, .removed = ui_render.diff_removed_marker_style, }, ); const entry_id = try writeCompletedToolStatus(sink, .completed, action); try sink.appendDiff(payload); owns_payload = false; if (presentation.lifecycle_id) |lifecycle_id| { try sink.attachHistoricalToolDetailWithLifecycle( entry_id, call, result, lifecycle_id, ); return true; } try sink.attachHistoricalToolDetail(entry_id, call, result); return true; } fn writeAnsweredQuestionResult( app: *App, sink: anytype, call: types.ToolCall, result: types.PersistedToolResult, ) !bool { if (result.status != .success or !std.mem.eql(u8, result.tool_name, "ask_user_question") or !@hasDecl(App, "writeHistoricalQuestionResolution")) return false; var answer_arena = std.heap.ArenaAllocator.init(app.alloc); defer answer_arena.deinit(); const answers = (try question_answer.decodeJson( answer_arena.allocator(), result.output, )) orelse return false; const entry_id = try sink.appendQuestionResolution(answers); try sink.attachHistoricalToolDetail(entry_id, call, result); return true; } fn writeStoredCommandOutput( app: *App, sink: anytype, result: types.PersistedToolResult, ) !bool { const handle = result.output_handle orelse return false; const loaded = if (app.session_persistence.writable) |*value| value else return false; const capability = loaded.childCapability() catch |err| { debug_trace.logf( "session", "resume command replay could not open result handle={s} err={s}", .{ handle, @errorName(err) }, ); return false; }; const output = result_store.readForReplayManaged( app.alloc, capability, handle, result.stored_output_bytes, ) catch |err| { debug_trace.logf( "session", "resume command replay could not read result handle={s} err={s}", .{ handle, @errorName(err) }, ); return false; }; defer app.alloc.free(output); return writeSavedCommandOutput(sink, output); } fn writeUnreportedToolStatus( app: *App, sink: anytype, call: types.ToolCall, ) !void { const status = try std.fmt.allocPrint( app.alloc, "● Tool completion was not reported: {s}", .{call.name}, ); defer app.alloc.free(status); const entry_id = try writeCompletedToolStatus(sink, .failed, status); try sink.attachHistoricalToolCallWithoutResult(entry_id, call); } fn writeUnpairedToolResult( app: *App, sink: anytype, result: types.PersistedToolResult, ) !void { const status = try std.fmt.allocPrint( app.alloc, "● {s} tool result: {s}", .{ @tagName(result.status), result.tool_name }, ); defer app.alloc.free(status); _ = try writeCompletedToolStatus(sink, switch (result.status) { .success => .completed, .failure => .failed, }, status); try writeReplayResultFallback(sink, result.output); } fn writeCompletedToolStatus( sink: anytype, kind: types.ToolOutcomeKind, status: []const u8, ) !u32 { return sink.appendToolStatus(kind, status); } fn writeSavedCommandOutput(sink: anytype, output: []const u8) !bool { var wrote = false; wrote = try writeCommandEnvelope(sink, output, "\n", "\n", .stdout) or wrote; wrote = try writeCommandEnvelope(sink, output, "\n", "\n", .stderr) or wrote; if (wrote) try sink.finishCommandOutput(); return wrote; } fn writeCommandEnvelope( sink: anytype, output: []const u8, open: []const u8, close: []const u8, stream: command_output_content.Stream, ) !bool { const text = envelopeBody(output, open, close) orelse return false; if (text.len == 0) return false; var remaining = text; while (std.mem.findScalar(u8, remaining, '\n')) |newline_index| { try sink.appendCommandOutput(stream, remaining[0 .. newline_index + 1]); remaining = remaining[newline_index + 1 ..]; } if (remaining.len > 0) try sink.appendCommandOutput(stream, remaining); return true; } fn envelopeBody(output: []const u8, open: []const u8, close: []const u8) ?[]const u8 { const start = std.mem.find(u8, output, open) orelse return null; const body = output[start + open.len ..]; const end = std.mem.find(u8, body, close) orelse return null; return body[0..end]; } fn writeReplayResultFallback(sink: anytype, output: []const u8) !void { if (output.len == 0) return; try sink.appendRaw(output); if (!std.mem.endsWith(u8, output, "\n")) try sink.appendRaw("\n"); } fn formatBackgroundReplayContext(app: *App, entry: types.BackgroundCommandHistoryTurn) ![]u8 { if (!@hasDecl(@TypeOf(app.background), "snapshotTaskByLogPath")) { return session_runtime.formatBackgroundHistoryContext(app.alloc, entry); } const task = try app.background.snapshotTaskByLogPath(app.alloc, entry.log_path); if (task) |snapshot| { defer snapshot.deinit(app.alloc); if (snapshot.state == .running) { if (snapshot.server_url) |url| { return std.fmt.allocPrint(app.alloc, "Session event: the previous background server is still running. Background #{d}; log: {s}; URL: {s}.", .{ snapshot.id, snapshot.log_path, url }); } if (snapshot.expect_url) { return std.fmt.allocPrint(app.alloc, "Session event: the previous background server is still running. Background #{d}; log: {s}; URL is still pending.", .{ snapshot.id, snapshot.log_path }); } return std.fmt.allocPrint(app.alloc, "Session event: the previous background command is still running. Background #{d}; log: {s}.", .{ snapshot.id, snapshot.log_path }); } } return std.fmt.allocPrint(app.alloc, "Session event: a previous background command was recorded at {s}, but it is no longer live in this workspace.", .{entry.log_path}); } fn AssistantHistoryMarkdownReplay(comptime Sink: type) type { return struct { sink: *Sink, const Self = @This(); fn flushText(self: *Self, out: *std.ArrayList(u8)) !void { if (out.items.len == 0) return; try self.sink.appendAssistantText(out.items); out.clearRetainingCapacity(); } fn deliverTable( raw: *anyopaque, table: assistant_presentation.TablePayload, out: *std.ArrayList(u8), ) anyerror!void { const self: *Self = @ptrCast(@alignCast(raw)); try self.flushText(out); try self.sink.appendAssistantTable(table); } fn deliverCodeBlock( raw: *anyopaque, block: assistant_presentation.CodeBlockPayload, out: *std.ArrayList(u8), ) anyerror!void { const self: *Self = @ptrCast(@alignCast(raw)); try self.flushText(out); try self.sink.appendAssistantCodeBlock(block); } fn deliverThematicRule( raw: *anyopaque, out: *std.ArrayList(u8), ) anyerror!void { const self: *Self = @ptrCast(@alignCast(raw)); try self.flushText(out); try self.sink.appendAssistantThematicRule(); } }; } fn writeAssistantHistoryMarkdown(app: *App, text: []const u8) !void { var sink = LiveHistorySink(App){ .app = app }; return writeAssistantHistoryMarkdownToSink(app, &sink, text); } fn writeAssistantHistoryMarkdownToSink( app: *App, sink: anytype, text: []const u8, ) !void { var processor = assistant_presentation.MarkdownProcessor{}; defer processor.deinit(app.alloc); var out: std.ArrayList(u8) = .empty; defer out.deinit(app.alloc); const Sink = @TypeOf(sink.*); const Replay = AssistantHistoryMarkdownReplay(Sink); var replay = Replay{ .sink = sink }; var table_completion = assistant_presentation.TableCompletion{ .ctx = &replay, .deliver = Replay.deliverTable, }; var code_completion = assistant_presentation.CodeBlockCompletion{ .ctx = &replay, .deliver = Replay.deliverCodeBlock, }; var thematic_rule_completion = assistant_presentation.ThematicRuleCompletion{ .ctx = &replay, .deliver = Replay.deliverThematicRule, }; const completions = assistant_presentation.MarkdownCompletions{ .table = &table_completion, .code = &code_completion, .thematic_rule = &thematic_rule_completion, }; try processor.pushWithCompletions(app.alloc, text, &out, completions); try processor.flushWithCompletions(app.alloc, &out, completions); try replay.flushText(&out); try sink.appendAssistantText("\n"); } fn closeWritableSession( app: *App, log_options: session_log.Options, ) void { app.session_persistence.resume_handoff_intent = .none; _ = closeWritableSessionWithResumeHandoff(app, log_options); } fn closeWritableSessionWithResumeHandoff( app: *App, log_options: session_log.Options, ) ?ResumeHandoff { const handoff_intent = app.session_persistence.resume_handoff_intent; app.session_persistence.resume_handoff_intent = .none; if (comptime @hasField(@TypeOf(app.session), "usage")) { app.session.usage.cancelReconciliation(); app.session.usage.finishProfilePublicationsBeforeShutdown(); app.session.usage.configureCheckpointSink(null); } app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); discardAnyPendingCancelledCommand(app, "writable_session_close"); app.session_persistence.disabled_cancelled_command_capture = null; const loaded = if (app.session_persistence.writable) |*value| value else return null; var resume_boundary_valid = false; if (handoff_intent != .none) { var settlement_failed = false; settleResumeBoundary(app, loaded, log_options) catch |err| { settlement_failed = true; debug_trace.logf( "session", "resume handoff boundary invalid session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); }; resume_boundary_valid = !settlement_failed; } else { settleDurableState(app, loaded, log_options) catch |err| { debug_trace.logf( "session", "final persistence settlement failed session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); }; } loaded.writeCheckpointIfDue(app.alloc, true, log_options) catch |err| { debug_trace.logf( "session", "final checkpoint failed session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); }; const should_create_handoff = resume_boundary_valid and shouldCreateResumeHandoff(.{ .intent = handoff_intent, .has_writable_session = true, .is_pristine = session_store.isPristineStartedSession(loaded), .has_unresolved_degraded_tail = loaded.degradedTail() != null, }); const handoff: ?ResumeHandoff = if (should_create_handoff) blk: { const session_id = app.alloc.dupe(u8, loaded.active_id) catch |err| { debug_trace.logf( "session", "resume handoff unavailable session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); break :blk null; }; break :blk .{ .session_id = session_id }; } else null; if (comptime @hasField(App, "background") and @hasDecl(@TypeOf(app.background), "detachManagedPersistence")) { app.background.detachManagedPersistence( std.heap.c_allocator, loaded.active_id, ); } if (comptime @hasDecl( @TypeOf(app.session), "clearWebFetchArtifacts", )) { app.session.clearWebFetchArtifacts(); } disableSubagentHost(app); loaded.deinit(app.alloc); app.session_persistence.writable = null; return handoff; } fn configureWebFetchArtifacts( app: *App, loaded: *session_store.LoadedWritableSession, ) void { if (comptime !@hasDecl( @TypeOf(app.session), "configureWebFetchArtifacts", )) return; const store = app.session_persistence.store orelse return; const session_dir = session_store.sessionDirPath( app.alloc, store.sessions_dir, loaded.active_id, ) catch |err| { debug_trace.logf( "session", "interactive web fetch artifact setup failed session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); app.session.clearWebFetchArtifacts(); return; }; defer app.alloc.free(session_dir); app.session.configureWebFetchArtifacts(app.alloc, session_dir); } fn enableSubagentHost( app: *App, loaded: *session_store.LoadedWritableSession, ) void { disableSubagentHost(app); const store = if (app.session_persistence.store) |*value| value else return; app.session_persistence.subagent_host = subagent_tool_host.Runtime.create( app.alloc, store, loaded.active_id, subagentAuthorityResolver(app), if (comptime @hasDecl(App, "runSubagentChild")) .{ .context = app, .run_fn = App.runSubagentChild } else .{}, ) catch |err| { debug_trace.logf( "session", "interactive subagent host unavailable session={s} err={s}", .{ loaded.active_id, @errorName(err) }, ); return; }; } fn subagentAuthorityResolver(app: *App) subagent_authority.HostResolver { return .{ .context = app, .resolve_fn = resolveSubagentAuthority }; } fn resolveSubagentAuthority( raw: ?*anyopaque, alloc: Allocator, root_id: []const u8, ) subagent_authority.HostResolveError!subagent_authority.HostAuthority { const app: *App = @ptrCast(@alignCast(raw.?)); if (comptime @hasField(@TypeOf(app.permission_state), "authority_mutex")) { app.permission_state.authority_mutex.lockUncancelable(io_mod.getIo()); } defer if (comptime @hasField(@TypeOf(app.permission_state), "authority_mutex")) { app.permission_state.authority_mutex.unlock(io_mod.getIo()); }; const writable = if (app.session_persistence.writable) |*value| value else return error.HostAuthorityUnavailable; if (!std.mem.eql(u8, writable.active_id, root_id)) { return error.HostAuthorityUnavailable; } const integrations = if (comptime @hasDecl(App, "snapshotMcpToolNames")) try app.snapshotMcpToolNames(alloc) else try alloc.alloc([]u8, 0); defer { for (integrations) |name| alloc.free(name); alloc.free(integrations); } var mcp_view: ?mcp_access.View = if (comptime @hasDecl(App, "snapshotMcpAccessView")) try app.snapshotMcpAccessView( alloc, root_id, root_id, app.permission_engine.rules, !permissions.rulesDenyAllTargetsForTool(app.permission_engine.rules, "mcp_features"), ) else null; defer if (mcp_view) |*view| view.deinit(alloc); var permission_state = app.session.snapshotPermissionState(alloc) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.HostAuthorityUnavailable, }; defer permission_state.deinit(alloc); return subagent_tool_host.captureHostAuthorityWithMcpView( alloc, .{ .tool_set = app.toolAdvertisementSet(), .mode = .full, }, if (comptime @hasField(App, "permission_state")) app.permission_state.sandbox_backend else .none, integrations, if (comptime @hasField(App, "permission_engine")) app.permission_engine.rules else .{}, if (comptime @hasField(App, "permission_engine")) app.permission_engine.grants.items else &.{}, permission_state, if (mcp_view) |*view| view else null, ); } fn convergeDegraded( app: *App, loaded: *session_store.LoadedWritableSession, log_options: session_log.Options, ) !void { try convergeDegradedAt( app, loaded, log_options, io_mod.milliTimestamp(), ); } fn convergeDegradedAt( app: *App, loaded: *session_store.LoadedWritableSession, log_options: session_log.Options, now_ms: i64, ) !void { if (loaded.degradedTail() == null) return; var current = try snapshotCurrentState(app, loaded.state, now_ms); defer current.deinit(app.alloc); try loaded.retryDegradedWithStateReplacement( app.alloc, current, log_options, ); app.session_persistence.degraded_warning_emitted = false; } fn settleResumeBoundary( app: *App, loaded: *session_store.LoadedWritableSession, log_options: session_log.Options, ) !void { try settleDurableState(app, loaded, log_options); try loaded.validateResumeBoundary(app.alloc, log_options); } fn settleDurableState( app: *App, loaded: *session_store.LoadedWritableSession, log_options: session_log.Options, ) !void { try convergeDegraded(app, loaded, log_options); const usage_dirty = if (comptime @hasField(@TypeOf(app.session), "usage")) app.session.usage.isDirty() else false; if (loaded.needsFinalStateReplacement(usage_dirty)) { try commitCurrentStateReplacementStrict( app, loaded, .compaction, log_options, false, ); } } fn commitCurrentStateReplacement( app: *App, loaded: *session_store.LoadedWritableSession, reason: session_event.ReplacementReason, log_options: session_log.Options, clear_recovery_checkpoint: bool, ) !void { commitCurrentStateReplacementStrict( app, loaded, reason, log_options, clear_recovery_checkpoint, ) catch |err| { try warnDegraded(app, err); }; } fn commitCurrentStateReplacementStrict( app: *App, loaded: *session_store.LoadedWritableSession, reason: session_event.ReplacementReason, log_options: session_log.Options, clear_recovery_checkpoint: bool, ) !void { const now_ms = io_mod.milliTimestamp(); var current = try snapshotCurrentState(app, loaded.state, now_ms); defer current.deinit(app.alloc); if (clear_recovery_checkpoint) { if (current.recovery_checkpoint) |*checkpoint| checkpoint.deinit(app.alloc); current.recovery_checkpoint = null; } _ = try loaded.commitStateReplacement( app.alloc, current, reason, .retry_expected_tail, log_options, ); if (comptime @hasField(@TypeOf(app.session), "usage")) { if (current.usage) |usage| { app.session.usage.markClean(usage); } } } pub fn commitPermissionState( app: *App, permission_state: session_permission_state.State, ) !void { app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const loaded = if (app.session_persistence.writable) |*value| value else return error.SessionPersistenceUnavailable; const now_ms = io_mod.milliTimestamp(); var current = try snapshotCurrentState(app, loaded.state, now_ms); defer current.deinit(app.alloc); current.permission_state.deinit(app.alloc); current.permission_state = try session_permission_state.dupe( app.alloc, permission_state, ); _ = try loaded.commitStateReplacement( app.alloc, current, .compaction, .retry_expected_tail, .{}, ); } fn commitJsHostSnapshot(app: *App, boundary: []const u8) void { if (comptime !@hasField(App, "session_persistence")) return; if (comptime !runtime_profile.allows(App, .js_host_sessions)) return; commitJsHostSnapshotEnabled(app, boundary); } fn commitJsHostSnapshotEnabled(app: *App, boundary: []const u8) void { app.session_persistence.write_mutex.lockUncancelable(io_mod.getIo()); defer app.session_persistence.write_mutex.unlock(io_mod.getIo()); const owner = if (app.session_persistence.js_host_session) |*value| value else return; var next = snapshotCurrentState( app, owner.state, io_mod.milliTimestamp(), ) catch |err| { debug_trace.logf( "session", "event=js_host_session_commit outcome=dropped boundary={s} id={s} err={s}", .{ boundary, owner.state.id, @errorName(err) }, ); return; }; var next_owned = true; defer if (next_owned) next.deinit(app.alloc); const revision = app.session_persistence.js_host_store.commit( app.alloc, next, owner.revision, ) catch |err| { debug_trace.logf( "session", "event=js_host_session_commit outcome=dropped boundary={s} id={s} expected_revision={s} err={s}", .{ boundary, owner.state.id, owner.revision orelse "", @errorName(err), }, ); return; }; if (owner.revision) |old_revision| app.alloc.free(old_revision); owner.state.deinit(app.alloc); owner.state = next; owner.revision = revision; next_owned = false; if (comptime @hasField(@TypeOf(app.session), "usage")) { if (owner.state.usage) |usage| app.session.usage.markClean(usage); } } fn snapshotCurrentState( app: *App, base_state: session_codec.DurableSessionState, now_ms: i64, ) !session_codec.DurableSessionState { const preferences = app.session_persistence.session_preferences orelse base_state.preferences; const id = try app.alloc.dupe(u8, base_state.id); errdefer app.alloc.free(id); const origin = try app.alloc.dupe( u8, base_state.origin_workspace_root, ); errdefer app.alloc.free(origin); const workspace = try app.alloc.dupe(u8, app.workspace_root); errdefer app.alloc.free(workspace); const owned_preferences = try preferences.dupe(app.alloc); errdefer { var value = owned_preferences; value.deinit(app.alloc); } const history = try app.session.snapshotHistory(app.alloc); errdefer session_runtime.freeHistoryTurnSlice(app.alloc, history); const permission_state = try app.session.snapshotPermissionState(app.alloc); errdefer { var value = permission_state; value.deinit(app.alloc); } const usage = try app.session.usage.snapshot(app.alloc); const recovery_checkpoint = if (base_state.recovery_checkpoint) |checkpoint| try checkpoint.dupe(app.alloc) else null; errdefer if (recovery_checkpoint) |*checkpoint| checkpoint.deinit(app.alloc); return .{ .id = id, .origin_workspace_root = origin, .workspace_root = workspace, .created_at_ms = base_state.created_at_ms, .updated_at_ms = now_ms, .conversation_language = app.session.languageSnapshot(), .preferences = owned_preferences, .history = history, .context_history_start = app.session.contextHistoryStart(), .total_input_tokens = app.total_input_tokens, .total_output_tokens = app.total_output_tokens, .permission_state = permission_state, .usage = usage, .recovery_checkpoint = recovery_checkpoint, }; } fn freshState( app: *App, preferences: session_codec.DurableSessionPreferences, ) !session_codec.DurableSessionState { const now = io_mod.milliTimestamp(); const id = try session_store.generateSessionId(app.alloc); errdefer app.alloc.free(id); const origin = try app.alloc.dupe(u8, app.workspace_root); errdefer app.alloc.free(origin); const workspace = try app.alloc.dupe(u8, app.workspace_root); errdefer app.alloc.free(workspace); const owned_preferences = try preferences.dupe(app.alloc); errdefer { var value = owned_preferences; value.deinit(app.alloc); } const history = try app.alloc.alloc(types.HistoryTurn, 0); errdefer app.alloc.free(history); const permission_state = try app.session.snapshotPermissionState(app.alloc); errdefer { var value = permission_state; value.deinit(app.alloc); } const usage = try app.session.usage.snapshot(app.alloc); return .{ .id = id, .origin_workspace_root = origin, .workspace_root = workspace, .created_at_ms = now, .updated_at_ms = now, .conversation_language = app.session.languageSnapshot(), .preferences = owned_preferences, .history = history, .total_input_tokens = 0, .total_output_tokens = 0, .permission_state = permission_state, .usage = usage, }; } fn restoreRuntimePreferences( app: *App, preferences: session_codec.DurableSessionPreferences, ) !void { app.selected_model.clearRetainingCapacity(); try app.selected_model.appendSlice(app.alloc, preferences.model); if (app.session_persistence.process_model_override) |model| { app.selected_model.clearRetainingCapacity(); try app.selected_model.appendSlice(app.alloc, model); } try app.worker.syncQueuedPromptModel( std.heap.c_allocator, app.selected_model.items, ); app.effort = preferences.effort; app.fast_mode = preferences.fast_mode; app.worker.syncQueuedPromptEffort(preferences.effort); app.worker.syncQueuedPromptFastMode(preferences.fast_mode); } fn applySessionPreferencePatch( app: *App, patch: SessionPreferencePatch, ) !void { try applyPreferencePatch( app.alloc, &app.session_persistence.session_preferences, patch, ); } fn warnDegraded(app: *App, err: anyerror) !void { if (app.session_persistence.degraded_warning_emitted) { debug_trace.logf( "session", "canonical persistence remains degraded err={s}", .{@errorName(err)}, ); return; } app.session_persistence.degraded_warning_emitted = true; try warnNonDurable(app, "session persistence degraded", err); } fn warnNonDurable( app: *App, label: []const u8, err: anyerror, ) !void { const notice = try std.fmt.allocPrint( app.alloc, "{s}: {s}; continuing non-durably", .{ label, @errorName(err) }, ); defer app.alloc.free(notice); try app.writeDomainNotice(.{ .topic = "session", .tone = .warning, .body = notice, }, true); } }; } fn replacePreferences( alloc: Allocator, target: *?session_codec.DurableSessionPreferences, source: session_codec.DurableSessionPreferences, ) !void { const replacement = try source.dupe(alloc); if (target.*) |*current| current.deinit(alloc); target.* = replacement; } fn applyPreferencePatch( alloc: Allocator, target: *?session_codec.DurableSessionPreferences, patch: SessionPreferencePatch, ) !void { var current = target.* orelse return error.SessionPreferencesUnavailable; if (patch.model) |model| { const replacement = try alloc.dupe(u8, model); alloc.free(current.model); current.model = replacement; } if (patch.effort) |effort| current.effort = effort; if (patch.fast_mode) |fast_mode| current.fast_mode = fast_mode; target.* = current; } var stable_test_environ: ?*std.process.Environ.Map = null; fn stableEmptyTestEnviron() !*const std.process.Environ.Map { if (stable_test_environ) |map| return map; const alloc = std.heap.page_allocator; const map = try alloc.create(std.process.Environ.Map); map.* = std.process.Environ.Map.init(alloc); stable_test_environ = map; return map; } const TestHome = struct { alloc: Allocator, map: std.process.Environ.Map, fn install(alloc: Allocator, home: ?[]const u8) !*TestHome { _ = try stableEmptyTestEnviron(); const self = try alloc.create(TestHome); errdefer alloc.destroy(self); self.* = .{ .alloc = alloc, .map = std.process.Environ.Map.init(alloc), }; errdefer self.map.deinit(); if (home) |value| { try self.map.put("HOME", value); } io_mod.setEnvironMap(&self.map); return self; } fn deinit(self: *TestHome) void { if (stable_test_environ) |map| { io_mod.setEnvironMap(map); } self.map.deinit(); const alloc = self.alloc; alloc.destroy(self); } }; const TestResumeTarget = union(enum) { pick, last, id: []u8, fn deinit(self: *TestResumeTarget, alloc: Allocator) void { switch (self.*) { .pick, .last => {}, .id => |id| alloc.free(id), } self.* = .last; } }; const FakeBackground = struct { source_session_id: []u8 = &.{}, detached: bool = false, fn deinit(self: *FakeBackground) void { if (self.source_session_id.len > 0) { std.heap.c_allocator.free(self.source_session_id); } self.* = .{}; } fn restoreFromManagedPersistence( self: *FakeBackground, alloc: Allocator, _: *session_child_store.SessionChildCapability, source_session_id: []const u8, _: []const u8, ) !void { if (self.source_session_id.len > 0) alloc.free(self.source_session_id); self.source_session_id = try alloc.dupe(u8, source_session_id); self.detached = false; } fn detachManagedPersistence( self: *FakeBackground, _: Allocator, source_session_id: []const u8, ) void { if (std.mem.eql(u8, self.source_session_id, source_session_id)) { self.detached = true; } } }; const FakeWorker = struct { model: std.ArrayList(u8) = .empty, effort: types.ReasoningEffort = .auto, fast_mode: bool = false, fn deinit(self: *FakeWorker, alloc: Allocator) void { self.model.deinit(alloc); self.* = .{}; } pub fn queuedPromptCount(self: *const FakeWorker) usize { _ = self; return 0; } fn syncQueuedPromptModel( self: *FakeWorker, alloc: Allocator, model: []const u8, ) !void { self.model.clearRetainingCapacity(); try self.model.appendSlice(alloc, model); } fn syncQueuedPromptEffort( self: *FakeWorker, effort: types.ReasoningEffort, ) void { self.effort = effort; } fn syncQueuedPromptFastMode(self: *FakeWorker, fast_mode: bool) void { self.fast_mode = fast_mode; } }; const PromptCard = struct { text: []u8, has_prior_turns: bool, fn deinit(self: *PromptCard, alloc: Allocator) void { alloc.free(self.text); self.* = undefined; } }; const AssistantPresentationEvent = enum { text, table, code_block, thematic_rule, raw_transcript, }; const CapturedCommandOutput = struct { stream: command_output_content.Stream, text: []u8, }; const ReplayEvent = enum { command_output_chunk, command_output_summary_flush, completed_tool_status, raw_transcript, }; const TestApp = struct { alloc: Allocator, workspace_root: []u8, session: session_runtime.SessionRuntime = .{ .max_history_turns = 8 }, session_persistence: Persistence = .{}, session_title: std.ArrayList(u8) = .empty, input_runtime: core_input_runtime.Runtime = .{}, terminal_input_runtime: ui_input.Runtime = .{}, shell: transcript_runtime.TranscriptRuntime = .{}, metrics: types.Metrics = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, next_image_id: usize = 1, requested_resume: ?TestResumeTarget = null, terminal: shell_runtime.TerminalState = .{}, stream: types.StreamState = .{}, background: FakeBackground = .{}, worker: FakeWorker = .{}, selected_model: std.ArrayList(u8) = .empty, effort: types.ReasoningEffort = .auto, fast_mode: bool = false, total_input_tokens: u64 = 0, total_output_tokens: u64 = 0, total_web_search_requests: u64 = 0, notices: std.ArrayList([]u8) = .empty, cards: std.ArrayList(PromptCard) = .empty, transcript: std.ArrayList(u8) = .empty, completed_tool_statuses: std.ArrayList([]u8) = .empty, completed_tool_outcomes: std.ArrayList(types.ToolOutcomeKind) = .empty, next_transcript_entry_id: u32 = 1, historical_question_entry_ids: std.ArrayList(u32) = .empty, historical_question_answer_count: usize = 0, historical_tool_detail_entry_ids: std.ArrayList(u32) = .empty, cancelled_command_detail_count: usize = 0, cancelled_command_artifact_handle: ?[]u8 = null, cancelled_command_replayed_output: bool = false, command_stdout: std.ArrayList(u8) = .empty, command_stderr: std.ArrayList(u8) = .empty, command_output_writes: std.ArrayList(CapturedCommandOutput) = .empty, command_output_flush_count: usize = 0, replay_events: std.ArrayList(ReplayEvent) = .empty, assistant_text: std.ArrayList(u8) = .empty, assistant_tables: std.ArrayList(assistant_presentation.TablePayload) = .empty, assistant_code_blocks: std.ArrayList(assistant_presentation.CodeBlockPayload) = .empty, assistant_thematic_rule_count: usize = 0, assistant_presentation_events: std.ArrayList(AssistantPresentationEvent) = .empty, fail_system_notice: bool = false, fail_assistant_table_append: bool = false, fail_assistant_code_block_append: bool = false, writable_seen_during_notice_failure: bool = false, fail_startup_resume_anchor: bool = false, startup_resume_anchor_count: usize = 0, startup_resume_anchor_saw_writable: bool = false, startup_resume_anchor_history_len: usize = 0, startup_resume_anchor_notice_count: usize = 0, action_description_allocates_scratch: bool = false, live_resume_prepare_count: usize = 0, live_resume_finish_count: usize = 0, live_resume_session_lock_deadline_ms: ?u64 = null, live_resume_commit_lock_deadline_ms: ?u64 = null, permission_engine: permissions.PermissionEngine = .{}, permission_state: struct { authority_mutex: std.Io.Mutex = .init, sandbox_backend: types.BackendKind = .none, } = .{}, mcp_tool_names: std.ArrayList([]u8) = .empty, fn init(alloc: Allocator, workspace_root: []const u8) !TestApp { return .{ .alloc = alloc, .workspace_root = try alloc.dupe(u8, workspace_root), }; } fn toolAdvertisementSet(_: *const TestApp) tool_set_contract.ToolSet { return builtin_tools.advertisement_set; } fn snapshotMcpToolNames(self: *TestApp, alloc: Allocator) ![][]u8 { const names = try alloc.alloc([]u8, self.mcp_tool_names.items.len); var initialized: usize = 0; errdefer { for (names[0..initialized]) |name| alloc.free(name); alloc.free(names); } for (self.mcp_tool_names.items, names) |name, *owned| { owned.* = try alloc.dupe(u8, name); initialized += 1; } return names; } fn deinit(self: *TestApp) void { self.input_runtime.deinit(self.alloc); self.terminal_input_runtime.deinit(self.alloc); self.session_title.deinit(self.alloc); self.shell.deinit(self.alloc); self.pending_images.deinit(self.alloc); for (self.notices.items) |notice| self.alloc.free(notice); self.notices.deinit(self.alloc); for (self.cards.items) |*card| card.deinit(self.alloc); self.cards.deinit(self.alloc); self.transcript.deinit(self.alloc); for (self.completed_tool_statuses.items) |status| self.alloc.free(status); self.completed_tool_statuses.deinit(self.alloc); self.completed_tool_outcomes.deinit(self.alloc); self.historical_question_entry_ids.deinit(self.alloc); self.historical_tool_detail_entry_ids.deinit(self.alloc); if (self.cancelled_command_artifact_handle) |handle| self.alloc.free(handle); self.command_stdout.deinit(self.alloc); self.command_stderr.deinit(self.alloc); for (self.command_output_writes.items) |write| self.alloc.free(write.text); self.command_output_writes.deinit(self.alloc); self.replay_events.deinit(self.alloc); self.assistant_text.deinit(self.alloc); for (self.assistant_tables.items) |*table| table.deinit(self.alloc); self.assistant_tables.deinit(self.alloc); for (self.assistant_code_blocks.items) |*block| block.deinit(self.alloc); self.assistant_code_blocks.deinit(self.alloc); self.assistant_presentation_events.deinit(self.alloc); Runtime(TestApp).deinitPersistence(self); if (self.requested_resume) |*target| target.deinit(self.alloc); self.session.deinit(self.alloc); self.background.deinit(); self.worker.deinit(std.heap.c_allocator); self.selected_model.deinit(self.alloc); self.permission_engine.deinit(self.alloc); for (self.mcp_tool_names.items) |name| self.alloc.free(name); self.mcp_tool_names.deinit(self.alloc); self.alloc.free(self.workspace_root); self.* = undefined; } fn writeDomainNotice(self: *TestApp, notice: types.SemanticNotice, _: bool) !void { if (self.fail_system_notice) { self.writable_seen_during_notice_failure = self.session_persistence.writable != null; return error.TestSystemNoticeFailure; } try self.notices.append(self.alloc, if (notice.topic.len > 0) try std.fmt.allocPrint( self.alloc, "● {c}{s}: {s}", .{ std.ascii.toUpper(notice.topic[0]), notice.topic[1..], notice.body }, ) else try std.fmt.allocPrint(self.alloc, "● {s}", .{notice.body})); } fn commitStartupResumeReplayAnchor(self: *TestApp) !void { self.startup_resume_anchor_count += 1; self.startup_resume_anchor_saw_writable = self.session_persistence.writable != null; self.startup_resume_anchor_history_len = self.session.historyLen(); self.startup_resume_anchor_notice_count = self.notices.items.len; if (self.fail_startup_resume_anchor) return error.TestStartupResumeAnchorFailure; } fn prepareLiveSessionResume( self: *TestApp, options: session_log.Options, ) !void { self.live_resume_prepare_count += 1; self.live_resume_session_lock_deadline_ms = options.session_lock_deadline_ms; self.live_resume_commit_lock_deadline_ms = options.commit_lock_deadline_ms; Runtime(TestApp).cancelSessionPicker(self); self.session.reset(self.alloc); } fn finishLiveSessionResume(self: *TestApp) !void { self.live_resume_finish_count += 1; } fn writeUserPromptCardWithSpacing(self: *TestApp, user: types.UserTurn, has_prior_turns: bool) !void { try self.cards.append(self.alloc, .{ .text = try self.alloc.dupe(u8, user.text), .has_prior_turns = has_prior_turns, }); } fn writeTranscript(self: *TestApp, text: []const u8, record: bool) !void { _ = record; try self.transcript.appendSlice(self.alloc, text); try self.replay_events.append(self.alloc, .raw_transcript); try self.assistant_presentation_events.append(self.alloc, .raw_transcript); } fn writeTranscriptClassified(self: *TestApp, text: []const u8, record: bool, _: anytype) !void { try self.writeTranscript(text, record); } fn writeCompletedToolStatus( self: *TestApp, kind: types.ToolOutcomeKind, text: []const u8, record: bool, ) !void { _ = record; const line = if (std.mem.endsWith(u8, text, "\n")) try self.alloc.dupe(u8, text) else try std.fmt.allocPrint(self.alloc, "{s}\n", .{text}); try self.completed_tool_statuses.append(self.alloc, line); try self.completed_tool_outcomes.append(self.alloc, kind); try self.replay_events.append(self.alloc, .completed_tool_status); } fn writeCompletedToolStatusReturningEntryId( self: *TestApp, kind: types.ToolOutcomeKind, text: []const u8, record: bool, ) !u32 { try self.writeCompletedToolStatus(kind, text, record); return self.allocateTranscriptEntryId(); } fn writeHistoricalQuestionResolution( self: *TestApp, answers: []const types.QuestionAnswer, ) !u32 { const entry_id = self.allocateTranscriptEntryId(); try self.historical_question_entry_ids.append(self.alloc, entry_id); self.historical_question_answer_count += answers.len; return entry_id; } fn attachHistoricalToolDetail( self: *TestApp, entry_id: u32, _: types.ToolCall, _: types.PersistedToolResult, ) !void { try self.historical_tool_detail_entry_ids.append(self.alloc, entry_id); } fn attachHistoricalCancelledCommandDetail( self: *TestApp, entry_id: u32, _: types.ToolCall, command_artifact_handle: ?[]const u8, replayed_output: bool, ) !void { try self.historical_tool_detail_entry_ids.append(self.alloc, entry_id); self.cancelled_command_detail_count += 1; self.cancelled_command_replayed_output = replayed_output; if (command_artifact_handle) |handle| { self.cancelled_command_artifact_handle = try self.alloc.dupe(u8, handle); } } fn allocateTranscriptEntryId(self: *TestApp) u32 { const entry_id = self.next_transcript_entry_id; self.next_transcript_entry_id +%= 1; return entry_id; } fn writeCommandOutputChunk(self: *TestApp, stream: command_output_content.Stream, text: []const u8, record: bool) !void { _ = record; const owned = try self.alloc.dupe(u8, text); errdefer self.alloc.free(owned); try self.command_output_writes.append(self.alloc, .{ .stream = stream, .text = owned, }); try self.replay_events.append(self.alloc, .command_output_chunk); switch (stream) { .stdout => try self.command_stdout.appendSlice(self.alloc, text), .stderr => try self.command_stderr.appendSlice(self.alloc, text), } } fn flushCommandOutputSummary(self: *TestApp, record: bool) !void { _ = record; self.command_output_flush_count += 1; try self.replay_events.append(self.alloc, .command_output_summary_flush); } fn describeToolActionCompletedWithAdvertised(self: *TestApp, arena: Allocator, call: types.ToolCall, _: ?[]const u8, _: []const []const u8) ![]const u8 { if (self.action_description_allocates_scratch) { _ = try arena.dupe(u8, "temporary parsed arguments"); } if (std.mem.eql(u8, call.name, "run_command")) { return arena.dupe(u8, "● Ran pwd"); } return std.fmt.allocPrint(arena, "● Completed {s}", .{call.name}); } fn describeToolActionDeniedWithAdvertised(self: *TestApp, arena: Allocator, call: types.ToolCall, _: ?[]const u8, label: []const u8, _: []const []const u8) ![]const u8 { if (self.action_description_allocates_scratch) { _ = try arena.dupe(u8, "temporary parsed arguments"); } return std.fmt.allocPrint(arena, "● {s} {s}", .{ label, call.name }); } fn pacerEmit(ctx: *anyopaque, text: []const u8) anyerror!void { const self: *TestApp = @ptrCast(@alignCast(ctx)); try self.assistant_text.appendSlice(self.alloc, text); try self.assistant_presentation_events.append(self.alloc, .text); } fn appendAssistantTable( self: *TestApp, table: assistant_presentation.TablePayload, ) !void { if (self.fail_assistant_table_append) { return error.TestAssistantTableAppendFailure; } try self.assistant_presentation_events.append(self.alloc, .table); try self.assistant_tables.append(self.alloc, table); } fn appendAssistantCodeBlock( self: *TestApp, block: assistant_presentation.CodeBlockPayload, ) !void { if (self.fail_assistant_code_block_append) { return error.TestAssistantCodeBlockAppendFailure; } try self.assistant_presentation_events.append(self.alloc, .code_block); try self.assistant_code_blocks.append(self.alloc, block); } fn appendAssistantThematicRule(self: *TestApp) !void { self.assistant_thematic_rule_count += 1; try self.assistant_presentation_events.append(self.alloc, .thematic_rule); } }; const FakeJsHostSessionStore = struct { state: ?session_codec.DurableSessionState = null, revision: []const u8 = "revision-1", updated_at_ms: i64 = 1, list_error: ?anyerror = null, load_error: ?anyerror = null, commit_error: ?anyerror = null, load_missing: bool = false, commit_count: usize = 0, committed_state: ?session_codec.DurableSessionState = null, expected_revision: ?[]u8 = null, fn deinit(self: *FakeJsHostSessionStore, alloc: Allocator) void { if (self.state) |*state| state.deinit(alloc); if (self.committed_state) |*state| state.deinit(alloc); if (self.expected_revision) |revision| alloc.free(revision); self.* = .{}; } fn store(self: *FakeJsHostSessionStore) JsHostSessionStore { return .{ .ctx = self, .load_fn = load, .commit_fn = commit, .list_fn = list, }; } fn load(raw: ?*anyopaque, alloc: Allocator, id: []const u8) !?js_host_session_store.Loaded { const self: *FakeJsHostSessionStore = @ptrCast(@alignCast(raw.?)); if (self.load_error) |err| return err; if (self.load_missing) return null; const state = self.state orelse return null; if (!std.mem.eql(u8, state.id, id)) return null; var owned_state = try state.dupe(alloc); errdefer owned_state.deinit(alloc); return .{ .state = owned_state, .revision = try alloc.dupe(u8, self.revision), }; } fn commit( raw: ?*anyopaque, alloc: Allocator, state: session_codec.DurableSessionState, expected_revision: ?[]const u8, ) ![]u8 { const self: *FakeJsHostSessionStore = @ptrCast(@alignCast(raw.?)); self.commit_count += 1; if (self.commit_error) |err| return err; if (self.expected_revision) |revision| alloc.free(revision); self.expected_revision = if (expected_revision) |revision| try alloc.dupe(u8, revision) else null; if (self.committed_state) |*committed| committed.deinit(alloc); self.committed_state = try state.dupe(alloc); return alloc.dupe(u8, "revision-next"); } fn list(raw: ?*anyopaque, alloc: Allocator) ![]js_host_session_store.Metadata { const self: *FakeJsHostSessionStore = @ptrCast(@alignCast(raw.?)); if (self.list_error) |err| return err; const state = self.state orelse return alloc.alloc(js_host_session_store.Metadata, 0); const entries = try alloc.alloc(js_host_session_store.Metadata, 1); errdefer alloc.free(entries); entries[0] = .{ .id = try alloc.dupe(u8, state.id), .updated_at_ms = self.updated_at_ms, }; return entries; } }; fn makeJsHostTestState( alloc: Allocator, id: []const u8, user: []const u8, assistant: []const u8, ) !session_codec.DurableSessionState { const history = try alloc.alloc(types.HistoryTurn, 1); errdefer alloc.free(history); history[0] = try session_runtime.makeAssistantTurn(alloc, user, assistant); errdefer session_runtime.freeHistoryTurn(alloc, history[0]); const owned_id = try alloc.dupe(u8, id); errdefer alloc.free(owned_id); const origin = try alloc.dupe(u8, "/origin"); errdefer alloc.free(origin); const workspace = try alloc.dupe(u8, "/workspace"); errdefer alloc.free(workspace); const model = try alloc.dupe(u8, "restored/model"); errdefer alloc.free(model); var usage = session_usage.Usage.initFresh(); defer usage.deinit(alloc); const sequence = try usage.reserveInvocation(); usage.finishInvocation(sequence, 41, .unbilled); var usage_snapshot = try usage.snapshot(alloc); errdefer usage_snapshot.deinit(alloc); return .{ .id = owned_id, .origin_workspace_root = origin, .workspace_root = workspace, .created_at_ms = 10, .updated_at_ms = 20, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .preferences = .{ .model = model, .effort = types.ReasoningEffort.literal("high"), .fast_mode = true, }, .history = history, .context_history_start = 1, .usage = usage_snapshot, .total_input_tokens = 17, .total_output_tokens = 23, }; } test "js-host resume restores transcript context preferences usage and revision" { const alloc = std.testing.allocator; var fake = FakeJsHostSessionStore{ .state = try makeJsHostTestState( alloc, "restored-session", "remember this prompt", "remembered reply", ), .updated_at_ms = 99, }; defer fake.deinit(alloc); var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); try Runtime(TestApp).configureStartupPreferences( &app, "startup/model", .user_global, "startup/model", .auto, false, ); app.session_persistence.js_host_store = fake.store(); app.requested_resume = .last; try Runtime(TestApp).resumeRequestedJsHostSession(&app); try std.testing.expectEqual(@as(usize, 1), app.session.historyLen()); try std.testing.expectEqual(@as(usize, 1), app.session.contextHistoryStart()); try std.testing.expectEqual(@as(usize, 1), app.cards.items.len); try std.testing.expectEqualStrings("remember this prompt", app.cards.items[0].text); try std.testing.expect(std.mem.find(u8, app.assistant_text.items, "remembered reply") != null); try std.testing.expectEqualStrings("restored/model", app.selected_model.items); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), app.effort); try std.testing.expect(app.fast_mode); try std.testing.expectEqual(@as(u64, 17), app.total_input_tokens); try std.testing.expectEqual(@as(u64, 23), app.total_output_tokens); var restored_usage = try app.session.usage.snapshot(alloc); defer restored_usage.deinit(alloc); try std.testing.expectEqual(@as(u64, 41), restored_usage.api_duration_ms); try std.testing.expectEqualStrings( "restored-session", app.session_persistence.js_host_session.?.state.id, ); try std.testing.expectEqualStrings( "revision-1", app.session_persistence.js_host_session.?.revision.?, ); } test "js-host resume store failures and missing records fall back to fresh sessions" { const alloc = std.testing.allocator; const cases = [_]enum { list_failure, load_missing, corrupt_payload }{ .list_failure, .load_missing, .corrupt_payload, }; for (cases) |case| { var fake = FakeJsHostSessionStore{ .state = try makeJsHostTestState(alloc, "unusable-session", "old prompt", "old reply"), }; defer fake.deinit(alloc); switch (case) { .list_failure => fake.list_error = error.SessionStoreUnavailable, .load_missing => fake.load_missing = true, .corrupt_payload => fake.load_error = error.InvalidSessionFormat, } var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); try Runtime(TestApp).configureStartupPreferences( &app, "fresh/model", .user_global, "fresh/model", .auto, false, ); app.session_persistence.js_host_store = fake.store(); app.requested_resume = .last; try Runtime(TestApp).resumeRequestedJsHostSession(&app); const owner = app.session_persistence.js_host_session orelse return error.TestExpectedFreshJsHostSession; try std.testing.expect(!std.mem.eql(u8, "unusable-session", owner.state.id)); try std.testing.expectEqual(@as(usize, 0), owner.state.history.len); try std.testing.expect(owner.revision == null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Run /help for commands") != null); } } test "js-host picker request stays unsupported and starts fresh" { const alloc = std.testing.allocator; var fake = FakeJsHostSessionStore{}; defer fake.deinit(alloc); var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); try Runtime(TestApp).configureStartupPreferences( &app, "fresh/model", .user_global, "fresh/model", .auto, false, ); app.session_persistence.js_host_store = fake.store(); app.requested_resume = .pick; try Runtime(TestApp).resumeRequestedJsHostSession(&app); try std.testing.expect(app.session_persistence.js_host_session != null); try std.testing.expectEqual(@as(usize, 1), app.notices.items.len); try std.testing.expect(std.mem.find(u8, app.notices.items[0], "picker is unavailable") != null); } test "js-host completed and interrupted turns propagate revisions preserve owner on conflict and reset fresh" { const alloc = std.testing.allocator; var fake = FakeJsHostSessionStore{}; defer fake.deinit(alloc); var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); try Runtime(TestApp).configureStartupPreferences( &app, "fresh/model", .user_global, "fresh/model", .auto, false, ); app.session_persistence.js_host_store = fake.store(); try Runtime(TestApp).beginFreshJsHostSession(&app); const first_turn = try session_runtime.makeAssistantTurn(alloc, "first prompt", "first reply"); defer session_runtime.freeHistoryTurn(alloc, first_turn); try Runtime(TestApp).appendHistoryTurn(&app, first_turn); Runtime(TestApp).commitJsHostSnapshotEnabled(&app, "history_turn"); try std.testing.expectEqual(@as(usize, 1), fake.commit_count); try std.testing.expect(fake.expected_revision == null); try std.testing.expectEqual(@as(usize, 1), fake.committed_state.?.history.len); try std.testing.expectEqualStrings( "revision-next", app.session_persistence.js_host_session.?.revision.?, ); fake.commit_error = error.SessionRevisionConflict; const second_turn: types.HistoryTurn = .{ .interrupted = .{ .user = .{ .text = @constCast("second prompt") }, .assistant = @constCast("partial reply"), } }; try Runtime(TestApp).appendHistoryTurn(&app, second_turn); Runtime(TestApp).commitJsHostSnapshotEnabled(&app, "history_turn"); try std.testing.expectEqual(@as(usize, 2), fake.commit_count); try std.testing.expectEqual(@as(usize, 2), app.session.historyLen()); try std.testing.expectEqual(@as(usize, 1), app.session_persistence.js_host_session.?.state.history.len); try std.testing.expectEqualStrings( "revision-next", app.session_persistence.js_host_session.?.revision.?, ); const previous_id = try alloc.dupe(u8, app.session_persistence.js_host_session.?.state.id); defer alloc.free(previous_id); app.session.reset(alloc); try Runtime(TestApp).beginFreshJsHostSession(&app); try std.testing.expect(!std.mem.eql( u8, previous_id, app.session_persistence.js_host_session.?.state.id, )); try std.testing.expect(app.session_persistence.js_host_session.?.revision == null); try std.testing.expectEqual(@as(usize, 0), app.session_persistence.js_host_session.?.state.history.len); } test "js-host preference changes snapshot the updated session preferences" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var fake = FakeJsHostSessionStore{}; defer fake.deinit(alloc); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try Runtime(TestApp).configureStartupPreferences( &app, "fresh/model", .user_global, "fresh/model", .auto, false, ); app.session_persistence.js_host_store = fake.store(); try Runtime(TestApp).beginFreshJsHostSession(&app); var result = Runtime(TestApp).commitRuntimePreferences(&app, .{ .model = "browser/model", .effort = types.ReasoningEffort.literal("high"), .fast_mode = true, }); defer result.deinit(alloc); Runtime(TestApp).commitJsHostSnapshotEnabled(&app, "preferences"); try std.testing.expect(result.session_error == null); try std.testing.expectEqual(@as(usize, 1), fake.commit_count); try std.testing.expectEqualStrings("browser/model", fake.committed_state.?.preferences.model); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), fake.committed_state.?.preferences.effort); try std.testing.expect(fake.committed_state.?.preferences.fast_mode); } test "cold resume image id rebase rejects overflow before admission" { var images = [_]types.ImageAttachment{.{ .id = std.math.maxInt(usize), .path = @constCast("/tmp/restored.png"), .media_type = @constCast("image/png"), }}; const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("restored"), .images = &images }, .assistant = @constCast("done"), } }}; try std.testing.expectError( error.ImageIdOverflow, nextImageIdForResumedHistory(std.testing.allocator, &history), ); } test "session runtime owns temporary interactive image snapshot capture" { const alloc = std.testing.allocator; const SnapshotCaptureApp = struct { alloc: Allocator, session_persistence: Persistence = .{}, }; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var source = try tmp.dir.createFile(std.testing.io, "interactive.png", .{}); defer source.close(std.testing.io); try source.writeStreamingAll(std.testing.io, "\x89PNG\r\n\x1a\ninteractive"); } const source_path = try io_mod.dirRealpathAlloc( alloc, tmp.dir, "interactive.png", ); defer alloc.free(source_path); var app = SnapshotCaptureApp{ .alloc = alloc }; defer app.session_persistence.deinit(alloc); var attachment = types.ImageAttachment{ .id = 1, .path = try alloc.dupe(u8, source_path), .media_type = try alloc.dupe(u8, "image/png"), }; defer image_attachments.discardImageAttachment(alloc, attachment); try Runtime(SnapshotCaptureApp).captureImageAttachment(&app, &attachment); try std.testing.expect(app.session_persistence.image_snapshot_temp_dir != null); try std.testing.expect(attachment.snapshot_path != null); try std.testing.expect(std.mem.startsWith( u8, attachment.snapshot_path.?, app.session_persistence.image_snapshot_temp_dir.?, )); } fn testPaths(alloc: Allocator, tmp: *std.testing.TmpDir) !struct { home: []u8, workspace: []u8 } { try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); return .{ .home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"), .workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"), }; } fn configureTestPreferences(app: *TestApp) !void { try Runtime(TestApp).configureStartupPreferences( app, "configured/model", .user_workspace, "configured/model", types.ReasoningEffort.literal("high"), true, ); } fn activeWatermarkPath( alloc: Allocator, app: *TestApp, ) ![]u8 { const session_dir = (try Runtime(TestApp).activeSessionDisplayPath( app, alloc, )) orelse return error.TestExpectedSessionDirectory; defer alloc.free(session_dir); const generation_hex = std.fmt.bytesToHex( app.session_persistence.writable.?.position.log_generation, .lower, ); const watermark_path = try std.fmt.allocPrint( alloc, "{s}/commit.{s}.json", .{ session_dir, generation_hex }, ); return watermark_path; } fn corruptActiveWatermark( alloc: Allocator, app: *TestApp, ) !void { const watermark_path = try activeWatermarkPath(alloc, app); defer alloc.free(watermark_path); var watermark = try std.Io.Dir.createFileAbsolute( io_mod.getIo(), watermark_path, .{ .truncate = true }, ); defer watermark.close(io_mod.getIo()); try watermark.writeStreamingAll(io_mod.getIo(), "{}\n"); try watermark.sync(io_mod.getIo()); } fn removeActiveWatermark( alloc: Allocator, app: *TestApp, ) !void { const watermark_path = try activeWatermarkPath(alloc, app); defer alloc.free(watermark_path); try std.Io.Dir.deleteFileAbsolute(io_mod.getIo(), watermark_path); } const FailingCommitLifecycle = struct { fn prepare( _: ?*anyopaque, _: Allocator, _: []const u8, _: []const u8, _: []const u8, _: u64, ) !void { return error.TestCommitPrepareFailure; } }; fn writeSessionFixture( alloc: Allocator, store: session_store.Store, id: []const u8, history: []const types.HistoryTurn, context_history_start: usize, ) !void { const owned_history = try alloc.alloc(types.HistoryTurn, history.len); var copied: usize = 0; errdefer { for (owned_history[0..copied]) |turn| { session_runtime.freeHistoryTurn(alloc, turn); } alloc.free(owned_history); } for (history, 0..) |turn, index| { owned_history[index] = try session_runtime.dupeHistoryTurn(alloc, turn); copied += 1; } var state = session_codec.DurableSessionState{ .id = try alloc.dupe(u8, id), .origin_workspace_root = try alloc.dupe(u8, store.workspace_root), .workspace_root = try alloc.dupe(u8, store.workspace_root), .created_at_ms = 123, .updated_at_ms = 456, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .preferences = .{ .model = try alloc.dupe(u8, "saved/model"), .effort = types.ReasoningEffort.literal("medium"), .fast_mode = false, }, .history = owned_history, .context_history_start = context_history_start, .total_input_tokens = 17, .total_output_tokens = 23, }; defer state.deinit(alloc); var loaded = try store.startWritableSession(alloc, state); defer loaded.deinit(alloc); _ = try loaded.commitStateReplacement( alloc, state, .migration, .rollback_before_adapter_continue, .{}, ); } test "initializePersistence silently skips optional missing home and errors when required" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, null); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try Runtime(TestApp).initializePersistence(&app, false); try std.testing.expect(app.session_persistence.store == null); try std.testing.expectError(error.HomeNotSet, Runtime(TestApp).initializePersistence(&app, true)); } test "beginFreshPersistedSession and enableSessionStores create per-session stores" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); try std.testing.expect(app.session_persistence.writable != null); Runtime(TestApp).enableSessionStores(&app); try std.testing.expect(app.background.source_session_id.len > 0); try std.testing.expect(app.session_persistence.subagent_host != null); const host = app.session_persistence.subagent_host.?; var host_authority = try host.host_authority.resolve_fn( host.host_authority.context, alloc, app.session_persistence.writable.?.active_id, ); defer host_authority.deinit(alloc); var found_subagent = false; for (host_authority.tools) |tool_name| { if (std.mem.eql(u8, tool_name, "subagent")) found_subagent = true; } try std.testing.expect(found_subagent); try std.testing.expectEqualStrings( app.session_persistence.writable.?.active_id, app.background.source_session_id, ); } test "resume handoff suppresses missing and pristine writable sessions" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); Runtime(TestApp).requestResumeHandoff(&app); try std.testing.expect(Runtime(TestApp).finalizePersistenceWithResumeHandoff(&app) == null); try std.testing.expectEqual( ResumeHandoffIntent.none, app.session_persistence.resume_handoff_intent, ); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); Runtime(TestApp).requestResumeHandoff(&app); try std.testing.expect(Runtime(TestApp).finalizePersistenceWithResumeHandoff(&app) == null); try std.testing.expect(app.session_persistence.writable == null); } test "upgrade resume handoff owns a validated pristine session" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const expected_id = try alloc.dupe( u8, app.session_persistence.writable.?.active_id, ); defer alloc.free(expected_id); Runtime(TestApp).requestUpgradeResumeHandoff(&app); var handoff = Runtime(TestApp).finalizePersistenceWithResumeHandoff(&app) orelse return error.TestExpectedResumeHandoff; defer handoff.deinit(alloc); try std.testing.expectEqualStrings(expected_id, handoff.session_id); } test "resume handoff owns the exact non-pristine session id after final replacement" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const turn = try session_runtime.makeAssistantTurn(alloc, "question", "answer"); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); app.session_persistence.writable.?.state_replacement_pending = true; const expected_id = try alloc.dupe( u8, app.session_persistence.writable.?.active_id, ); defer alloc.free(expected_id); Runtime(TestApp).requestResumeHandoff(&app); var handoff = Runtime(TestApp).finalizePersistenceWithResumeHandoff(&app) orelse return error.TestExpectedResumeHandoff; defer handoff.deinit(alloc); try std.testing.expectEqualStrings(expected_id, handoff.session_id); try std.testing.expect(app.session_persistence.writable == null); } test "resume handoff preparation repairs an exactly recoverable live commit watermark" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const turn = try session_runtime.makeAssistantTurn( alloc, "question", "answer", ); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); try corruptActiveWatermark(alloc, &app); try Runtime(TestApp).prepareResumeHandoff(&app); try std.testing.expect(app.session_persistence.writable != null); const next_turn = try session_runtime.makeAssistantTurn( alloc, "continued question", "continued answer", ); defer session_runtime.freeHistoryTurn(alloc, next_turn); try Runtime(TestApp).appendHistoryTurn(&app, next_turn); try app.session_persistence.writable.?.validateResumeBoundary(alloc, .{}); } test "resume handoff preparation restores a missing exact live commit watermark" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const turn = try session_runtime.makeAssistantTurn( alloc, "question", "answer", ); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); try removeActiveWatermark(alloc, &app); try Runtime(TestApp).prepareResumeHandoff(&app); try app.session_persistence.writable.?.validateResumeBoundary(alloc, .{}); } test "resume handoff revalidates after successful preparation" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const turn = try session_runtime.makeAssistantTurn( alloc, "question", "answer", ); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); try Runtime(TestApp).prepareResumeHandoff(&app); try corruptActiveWatermark(alloc, &app); Runtime(TestApp).requestUpgradeResumeHandoff(&app); try std.testing.expect( Runtime(TestApp).finalizePersistenceWithResumeHandoff(&app) == null, ); } test "resume handoff remains available when the derived checkpoint write fails" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const turn = try session_runtime.makeAssistantTurn(alloc, "question", "answer"); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); const expected_id = try alloc.dupe( u8, app.session_persistence.writable.?.active_id, ); defer alloc.free(expected_id); const session_dir = (try Runtime(TestApp).activeSessionDisplayPath(&app, alloc)) orelse return error.TestExpectedSessionDirectory; defer alloc.free(session_dir); const session_dir_z = try alloc.dupeZ(u8, session_dir); defer alloc.free(session_dir_z); if (std.c.chmod(session_dir_z.ptr, 0o500) != 0) return error.TestChmodFailed; defer _ = std.c.chmod(session_dir_z.ptr, 0o700); Runtime(TestApp).requestResumeHandoff(&app); var handoff = Runtime(TestApp).closeWritableSessionWithResumeHandoff( &app, .{ .checkpoint_interval = 0 }, ) orelse return error.TestExpectedResumeHandoff; defer handoff.deinit(alloc); try std.testing.expectEqualStrings(expected_id, handoff.session_id); } const HandoffDegradedFailure = struct { fn hit(_: ?*anyopaque, point: session_log.Boundary) !void { if (point == .after_event_sync or point == .before_recovery_proposed_validation or point == .before_recovery_prior_validation) { return error.TestHandoffDegradedFailure; } } fn controls() session_log.TestControls { return .{ .boundary_fn = hit }; } }; test "resume handoff suppresses unresolved degraded state" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const loaded = &app.session_persistence.writable.?; try std.testing.expectError( error.SessionPersistenceDegraded, loaded.appendEvent( alloc, .{ .preferences_changed = .{ .fast_mode = true } }, io_mod.milliTimestamp(), .retry_expected_tail, .{ .test_controls = HandoffDegradedFailure.controls() }, ), ); try std.testing.expect(loaded.degradedTail() != null); Runtime(TestApp).requestResumeHandoff(&app); try std.testing.expect(Runtime(TestApp).closeWritableSessionWithResumeHandoff( &app, .{ .test_controls = HandoffDegradedFailure.controls() }, ) == null); } test "resume handoff suppresses session id allocation failure" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const turn = try session_runtime.makeAssistantTurn(alloc, "question", "answer"); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); try app.session_persistence.writable.?.writeCheckpointIfDue(alloc, true, .{}); Runtime(TestApp).requestResumeHandoff(&app); var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0 }); app.alloc = failing.allocator(); const handoff = Runtime(TestApp).finalizePersistenceWithResumeHandoff(&app); app.alloc = alloc; try std.testing.expect(failing.has_induced_failure); try std.testing.expect(handoff == null); } test "enableSessionStores replaces existing subagent host without leaking" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); Runtime(TestApp).enableSessionStores(&app); try std.testing.expect(app.session_persistence.subagent_host != null); Runtime(TestApp).enableSessionStores(&app); try std.testing.expect(app.session_persistence.subagent_host != null); } test "interactive subagent host resolves current tools rules grants sandbox and integrations" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); Runtime(TestApp).enableSessionStores(&app); const host = app.session_persistence.subagent_host.?; const root_id = app.session_persistence.writable.?.active_id; var initial = try host.host_authority.resolve_fn( host.host_authority.context, alloc, root_id, ); defer initial.deinit(alloc); try std.testing.expectEqual(types.BackendKind.none, initial.sandbox_backend); try std.testing.expectEqual(@as(usize, 0), initial.integrations.len); app.permission_state.sandbox_backend = .vercel; try app.permission_engine.allow(alloc, "run_command", "zig build test"); try app.mcp_tool_names.append(alloc, try alloc.dupe(u8, "mcp_fixture_echo")); var changed = try host.host_authority.resolve_fn( host.host_authority.context, alloc, root_id, ); defer changed.deinit(alloc); try std.testing.expectEqual(types.BackendKind.vercel, changed.sandbox_backend); try std.testing.expectEqualStrings("mcp_fixture_echo", changed.integrations[0]); try std.testing.expectEqualStrings("zig build test", changed.grants[0].target_path); try std.testing.expect(initial.generation != changed.generation); app.permission_engine.clear(alloc); var denied_rules = [_]types.PermissionRule{.{ .permission = @constCast("read"), .pattern = @constCast("*"), .action = .deny, }}; app.permission_engine.replaceRules( alloc, try types.dupePermissionRuleSet(alloc, .{ .rules = &denied_rules }), ); alloc.free(app.mcp_tool_names.pop().?); var revoked = try host.host_authority.resolve_fn( host.host_authority.context, alloc, root_id, ); defer revoked.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), revoked.integrations.len); try std.testing.expectEqual(@as(usize, 0), revoked.grants.len); var found_read_file = false; for (revoked.tools) |tool_name| { if (std.mem.eql(u8, tool_name, "read_file")) found_read_file = true; } try std.testing.expect(!found_read_file); try std.testing.expect(changed.generation != revoked.generation); } test "execution replay keeps an inline command preview when its stored output is unavailable" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); var calls = [_]types.ToolCall{.{ .id = "call_pwd", .name = "run_command", .arguments_json = "{\"command\":\"pwd\"}", }}; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_pwd"), .tool_name = @constCast("run_command"), .status = .success, .output = @constCast("exit_code=0\n\n/workspace\n\n\nwarning\n\n"), .output_handle = @constCast("missing-command-output.txt"), .preview = @constCast("/workspace"), .output_bytes = 69, .stored_output_bytes = 69, .truncated = true, }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqual(@as(usize, 0), app.notices.items.len); try std.testing.expectEqual(@as(usize, 1), app.completed_tool_statuses.items.len); try std.testing.expectEqualStrings("● Ran pwd\n", app.completed_tool_statuses.items[0]); try std.testing.expectEqualStrings("/workspace", app.command_stdout.items); try std.testing.expectEqualStrings("warning", app.command_stderr.items); try std.testing.expectEqual(@as(usize, 1), app.command_output_flush_count); } test "execution replay renders persisted permission feedback after its tool result" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); var calls = [_]types.ToolCall{.{ .id = "call_write", .name = "write_file", .arguments_json = "{\"path\":\"demo.txt\"}", }}; var feedback = [_][]u8{@constCast("Then read the file and report its exact contents.")}; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_write"), .tool_name = @constCast("write_file"), .status = .success, .output = @constCast("wrote demo.txt"), .output_bytes = 14, .stored_output_bytes = 14, .permission_feedback = feedback[0..], }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqual(@as(usize, 1), app.completed_tool_statuses.items.len); try std.testing.expectEqualStrings("● Completed write_file\n", app.completed_tool_statuses.items[0]); try std.testing.expectEqual(@as(usize, 1), app.cards.items.len); try std.testing.expectEqualStrings("Then read the file and report its exact contents.", app.cards.items[0].text); try std.testing.expect(app.cards.items[0].has_prior_turns); } test "execution replay writes completed status before saved command output" { const line_alloc = std.testing.allocator; var line_app = try TestApp.init(line_alloc, "/workspace"); defer line_app.deinit(); var line_calls = [_]types.ToolCall{.{ .id = "call_multiline", .name = "run_command", .arguments_json = "{\"command\":\"printf\"}", }}; var line_results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_multiline"), .tool_name = @constCast("run_command"), .status = .success, .output = @constCast( "exit_code=0\n\none\n\ntwo\n\n\nwarning\n\n", ), .output_bytes = 72, .stored_output_bytes = 72, }}; var line_steps = [_]types.ToolExecutionStep{.{ .tool_calls = line_calls[0..], .tool_results = line_results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&line_app, .{ .tool_steps = line_steps[0..] }); try std.testing.expectEqual(@as(usize, 4), line_app.command_output_writes.items.len); try std.testing.expectEqual(.stdout, line_app.command_output_writes.items[0].stream); try std.testing.expectEqualStrings("one\n", line_app.command_output_writes.items[0].text); try std.testing.expectEqual(.stdout, line_app.command_output_writes.items[1].stream); try std.testing.expectEqualStrings("\n", line_app.command_output_writes.items[1].text); try std.testing.expectEqual(.stdout, line_app.command_output_writes.items[2].stream); try std.testing.expectEqualStrings("two", line_app.command_output_writes.items[2].text); try std.testing.expectEqual(.stderr, line_app.command_output_writes.items[3].stream); try std.testing.expectEqualStrings("warning", line_app.command_output_writes.items[3].text); try std.testing.expectEqual(@as(usize, 1), line_app.command_output_flush_count); try std.testing.expectEqualSlices( ReplayEvent, &.{ .completed_tool_status, .command_output_chunk, .command_output_chunk, .command_output_chunk, .command_output_chunk, .command_output_summary_flush, }, line_app.replay_events.items, ); } test "resume restores complete command output stored behind a result handle" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); var sidecar_output: std.ArrayList(u8) = .empty; defer sidecar_output.deinit(alloc); try sidecar_output.appendSlice(alloc, "exit_code=0\n\nSIDECAR_STDOUT_MARKER\n"); try sidecar_output.appendNTimes(alloc, 'x', result_store.read_max_bytes); try sidecar_output.appendSlice( alloc, "\n\n\nSIDECAR_STDERR_MARKER\n... [tool result truncated]\n\n", ); const partial_preview = "\n" ++ "exit_code=0\n\nPREVIEW_STDOUT_MARKER\n\n\nPREVIEW_STDERR_PARTIAL"; var calls = [_]types.ToolCall{.{ .id = "call_large_command", .name = "run_command", .arguments_json = "{\"command\":\"git diff\"}", }}; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_large_command"), .tool_name = @constCast("run_command"), .status = .success, .output = @constCast(partial_preview), .output_handle = @constCast("saved-command-output.txt"), .preview = @constCast("PREVIEW_STDOUT_MARKER"), .output_bytes = sidecar_output.items.len, .stored_output_bytes = sidecar_output.items.len, .truncated = true, }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("show the diff") }, .assistant = @constCast(""), .execution = .{ .tool_steps = steps[0..] }, } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "stored-command-output", &history, 0, ); var fixture = try app.session_persistence.store.?.resumeForWrite( alloc, "stored-command-output", ); const capability = try fixture.childCapability(); var entry = try capability.atomicReplace( alloc, .tool_results, "saved-command-output.txt", sidecar_output.items, ); entry.deinit(alloc); fixture.deinit(alloc); app.requested_resume = .{ .id = try alloc.dupe(u8, "stored-command-output") }; try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expect(std.mem.find(u8, app.command_stdout.items, "SIDECAR_STDOUT_MARKER") != null); try std.testing.expect(std.mem.find(u8, app.command_stdout.items, "PREVIEW_STDOUT_MARKER") == null); try std.testing.expect(std.mem.find(u8, app.command_stderr.items, "SIDECAR_STDERR_MARKER") != null); try std.testing.expect(std.mem.find(u8, app.command_stderr.items, "... [tool result truncated]") != null); try std.testing.expectEqual(@as(usize, 1), app.command_output_flush_count); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); } test "resume falls back to saved command output when replay contains an empty frame" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const saved_output = "exit_code=0\n\nFALLBACK_STDOUT_MARKER\n\n" ++ "\n\n"; const replay_handle = "fx-command-replay-empty-frame.bin"; var empty_replay = [_]u8{0} ** ("FXRPLY01".len + 9); @memcpy(empty_replay[0.."FXRPLY01".len], "FXRPLY01"); var calls = [_]types.ToolCall{.{ .id = "call_empty_replay", .name = "run_command", .arguments_json = "{\"command\":\"printf fallback\"}", }}; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_empty_replay"), .tool_name = @constCast("run_command"), .status = .success, .output = @constCast(saved_output), .output_bytes = saved_output.len, .stored_output_bytes = saved_output.len, .command_output_replay = .{ .available = .{ .handle = replay_handle, .framed_bytes = empty_replay.len, } }, .command_process_presentation = .{ .exit_code = 0 }, }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("show fallback output") }, .assistant = @constCast(""), .execution = .{ .tool_steps = steps[0..] }, } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "empty-command-replay", &history, 0, ); var fixture = try app.session_persistence.store.?.resumeForWrite( alloc, "empty-command-replay", ); const capability = try fixture.childCapability(); var entry = try capability.atomicReplace( alloc, .command_artifacts, replay_handle, &empty_replay, ); entry.deinit(alloc); fixture.deinit(alloc); app.requested_resume = .{ .id = try alloc.dupe(u8, "empty-command-replay") }; try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expectEqualStrings("FALLBACK_STDOUT_MARKER", app.command_stdout.items); try std.testing.expectEqual(@as(usize, 1), app.command_output_writes.items.len); try std.testing.expectEqual(.stdout, app.command_output_writes.items[0].stream); try std.testing.expectEqualStrings( "FALLBACK_STDOUT_MARKER", app.command_output_writes.items[0].text, ); try std.testing.expectEqual(@as(usize, 1), app.command_output_flush_count); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); } test "historical command replay prefers tagged frames and preserves Ran outcome" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const capability = try app.session_persistence.writable.?.childCapability(); var capture_arena = std.heap.ArenaAllocator.init(alloc); defer capture_arena.deinit(); const capture_alloc = capture_arena.allocator(); const capture = try command_replay_store.Capture.create( capture_alloc, 1, capability, ); capture.appendAccepted(capture_alloc, .stdout, "first"); capture.appendAccepted(capture_alloc, .stderr, "second\n"); capture.appendAccepted(capture_alloc, .stdout, " third\n"); capture.seal(capture_alloc); const replay = capture.retain(capture_alloc) orelse return error.TestExpectedReplay; var calls = [_]types.ToolCall{.{ .id = "call_nonzero", .name = "run_command", .arguments_json = "{\"command\":\"false\"}", }}; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_nonzero"), .tool_name = @constCast("run_command"), .status = .failure, .output = @constCast("tool_execution_failed metadata only"), .output_bytes = 35, .stored_output_bytes = 35, .command_output_replay = replay, .command_process_presentation = .{ .exit_code = 7 }, }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqualStrings("● Ran pwd\n", app.completed_tool_statuses.items[0]); try std.testing.expectEqual(@as(usize, 3), app.command_output_writes.items.len); try std.testing.expectEqual(.stdout, app.command_output_writes.items[0].stream); try std.testing.expectEqualStrings("first", app.command_output_writes.items[0].text); try std.testing.expectEqual(.stderr, app.command_output_writes.items[1].stream); try std.testing.expectEqualStrings("second\n", app.command_output_writes.items[1].text); try std.testing.expectEqual(.stdout, app.command_output_writes.items[2].stream); try std.testing.expectEqualStrings(" third\n", app.command_output_writes.items[2].text); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expectEqualSlices( ReplayEvent, &.{ .completed_tool_status, .command_output_chunk, .command_output_chunk, .command_output_chunk, .command_output_summary_flush, }, app.replay_events.items, ); } test "execution replay releases action-formatting scratch allocations" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); app.action_description_allocates_scratch = true; var calls = [_]types.ToolCall{.{ .id = "call_read", .name = "read_file", .arguments_json = "{\"path\":\"README.md\"}", }}; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_read"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("README"), .output_bytes = 6, .stored_output_bytes = 6, }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqualStrings("● Completed read_file\n", app.completed_tool_statuses.items[0]); try std.testing.expectEqualSlices( types.ToolOutcomeKind, &.{.completed}, app.completed_tool_outcomes.items, ); } test "execution replay projects answered questions as a semantic card and retains detail" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); var calls = [_]types.ToolCall{.{ .id = "call_question", .name = "ask_user_question", .arguments_json = "{\"questions\":[]}", }}; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_question"), .tool_name = @constCast("ask_user_question"), .status = .success, .output = @constCast("[{\"question\":\"Which verification depth should I use?\",\"answer\":\"Thorough\"}]"), .output_bytes = 82, .stored_output_bytes = 82, }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqual(@as(usize, 0), app.completed_tool_statuses.items.len); try std.testing.expectEqual(@as(usize, 1), app.historical_question_entry_ids.items.len); try std.testing.expectEqual(@as(usize, 1), app.historical_question_answer_count); try std.testing.expectEqual(@as(usize, 1), app.historical_tool_detail_entry_ids.items.len); try std.testing.expectEqual( app.historical_question_entry_ids.items[0], app.historical_tool_detail_entry_ids.items[0], ); } test "execution replay keeps invalid question results on the generic path" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); var calls = [_]types.ToolCall{ .{ .id = "cancelled", .name = "ask_user_question", .arguments_json = "{\"questions\":[]}" }, .{ .id = "malformed", .name = "ask_user_question", .arguments_json = "{\"questions\":[]}" }, .{ .id = "failed", .name = "ask_user_question", .arguments_json = "{\"questions\":[]}" }, }; var results = [_]types.PersistedToolResult{ .{ .tool_call_id = @constCast("cancelled"), .tool_name = @constCast("ask_user_question"), .status = .success, .output = @constCast("(user cancelled the question)"), .output_bytes = 29, .stored_output_bytes = 29, }, .{ .tool_call_id = @constCast("malformed"), .tool_name = @constCast("ask_user_question"), .status = .success, .output = @constCast("[{\"question\":\"Q\"}]"), .output_bytes = 18, .stored_output_bytes = 18, }, .{ .tool_call_id = @constCast("failed"), .tool_name = @constCast("ask_user_question"), .status = .failure, .output = @constCast("[{\"question\":\"Q\",\"answer\":\"A\"}]"), .output_bytes = 31, .stored_output_bytes = 31, }, }; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqual(@as(usize, 0), app.historical_question_entry_ids.items.len); try std.testing.expectEqual(@as(usize, 3), app.completed_tool_statuses.items.len); try std.testing.expectEqual(@as(usize, 3), app.historical_tool_detail_entry_ids.items.len); } test "execution replay preserves paired deferred tools without command output" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); var calls = [_]types.ToolCall{ .{ .id = "call_read", .name = "read_file", .arguments_json = "{\"path\":\"nested/input.txt\"}", }, .{ .id = "call_command", .name = "run_command", .arguments_json = "{\"command\":\"cat nested/input.txt\"}", }, .{ .id = "call_legacy", .name = "read_file", .arguments_json = "{\"path\":\"stale.txt\"}", }, }; var results = [_]types.PersistedToolResult{ .{ .tool_call_id = @constCast("call_read"), .tool_name = @constCast("read_file"), .status = .failure, .output = @constCast(types.context_deferred_tool_result_output), .output_bytes = types.context_deferred_tool_result_output.len, .stored_output_bytes = types.context_deferred_tool_result_output.len, }, .{ .tool_call_id = @constCast("call_command"), .tool_name = @constCast("run_command"), .status = .failure, .output = @constCast(types.context_deferred_tool_result_output), .output_handle = @constCast("should-not-be-read.txt"), .preview = @constCast("should not be rendered"), .output_bytes = types.context_deferred_tool_result_output.len, .stored_output_bytes = types.context_deferred_tool_result_output.len, .truncated = true, .command_output_replay = .unavailable, .command_process_presentation = .{ .exit_code = 7 }, }, .{ .tool_call_id = @constCast("call_legacy"), .tool_name = @constCast("read_file"), .status = .failure, .output = @constCast(types.deferred_tool_result_output), .output_bytes = types.deferred_tool_result_output.len, .stored_output_bytes = types.deferred_tool_result_output.len, }, }; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqualSlices( types.ToolOutcomeKind, &.{ .deferred, .deferred, .denied }, app.completed_tool_outcomes.items, ); try std.testing.expectEqual(@as(usize, 3), app.completed_tool_statuses.items.len); try std.testing.expectEqualStrings("● Context updated read_file\n", app.completed_tool_statuses.items[0]); try std.testing.expectEqualStrings("● Context updated run_command\n", app.completed_tool_statuses.items[1]); try std.testing.expectEqualStrings("● Not executed read_file\n", app.completed_tool_statuses.items[2]); try std.testing.expectEqual(@as(usize, 3), app.historical_tool_detail_entry_ids.items.len); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_output_writes.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_stdout.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_stderr.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_output_flush_count); try std.testing.expectEqualSlices( ReplayEvent, &.{ .completed_tool_status, .completed_tool_status, .completed_tool_status }, app.replay_events.items, ); } test "execution replay keeps failed and unpaired persisted results visible without a system notice" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); var calls = [_]types.ToolCall{.{ .id = "call_failed", .name = "run_command", .arguments_json = "{\"command\":\"false\"}", }}; var results = [_]types.PersistedToolResult{ .{ .tool_call_id = @constCast("call_failed"), .tool_name = @constCast("run_command"), .status = .failure, .output = @constCast("malformed command result"), .output_bytes = 24, .stored_output_bytes = 24, }, .{ .tool_call_id = @constCast("missing_call"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("unpaired result"), .output_bytes = 15, .stored_output_bytes = 15, }, .{ .tool_call_id = @constCast("missing_deferred_call"), .tool_name = @constCast("read_file"), .status = .failure, .output = @constCast(types.deferred_tool_result_output), .output_bytes = types.deferred_tool_result_output.len, .stored_output_bytes = types.deferred_tool_result_output.len, }, }; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..], }}; try Runtime(TestApp).writeExecutionHistory(&app, .{ .tool_steps = steps[0..] }); try std.testing.expectEqual(@as(usize, 0), app.notices.items.len); try std.testing.expectEqual(@as(usize, 3), app.completed_tool_statuses.items.len); try std.testing.expectEqualStrings("● Failed run_command\n", app.completed_tool_statuses.items[0]); try std.testing.expectEqualStrings("● success tool result: read_file\n", app.completed_tool_statuses.items[1]); try std.testing.expectEqualStrings("● failure tool result: read_file\n", app.completed_tool_statuses.items[2]); try std.testing.expectEqualSlices( types.ToolOutcomeKind, &.{ .failed, .completed, .failed }, app.completed_tool_outcomes.items, ); try std.testing.expectEqualStrings( "malformed command result\nunpaired result\nNot executed\n", app.transcript.items, ); try std.testing.expectEqualSlices( ReplayEvent, &.{ .completed_tool_status, .raw_transcript, .raw_transcript, .completed_tool_status, .raw_transcript, .raw_transcript, .completed_tool_status, .raw_transcript, .raw_transcript, }, app.replay_events.items, ); } test "safe last resume view paints and pins authoritative resume to its session id" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try writeSessionFixture( alloc, app.session_persistence.store.?, "cached-session", &.{}, 0, ); { var fixture = try app.session_persistence.store.?.resumeForWrite( alloc, "cached-session", ); defer fixture.deinit(alloc); try fixture.writeResumeView(alloc, .{ .terminal_rows = 24, .terminal_cols = 80, .complete = true, }, "cached transcript\n"); } app.shell.layout.rows = 24; app.shell.layout.cols = 80; app.requested_resume = .last; const stage = Runtime(TestApp).stageRequestedResumeView(&app); try std.testing.expectEqual(@as(u32, 1), stage.ready); try std.testing.expectEqualStrings("cached transcript\n", app.shell.transcript.items); switch (app.requested_resume.?) { .id => |session_id| try std.testing.expectEqualStrings("cached-session", session_id), .pick, .last => return error.TestExpectedPinnedResumeSession, } try std.testing.expectError( error.SessionBusy, app.session_persistence.store.?.resumeTargetForWrite( alloc, .{ .id = "cached-session" }, paths.workspace, .{ .log = .{ .session_lock_deadline_ms = 0 } }, ), ); try Runtime(TestApp).publishStagedResumeView(&app, stage.ready); try std.testing.expectEqual(@as(usize, 0), app.shell.transcript.items.len); try std.testing.expectEqual(@as(usize, 1), app.startup_resume_anchor_count); try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expect(app.session_persistence.resume_view_admission == null); try std.testing.expectEqualStrings( "cached-session", app.session_persistence.writable.?.active_id, ); } test "resume view policy denial does not paint or retain admission" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const store = app.session_persistence.store.?; try writeSessionFixture(alloc, store, "malformed-child", &.{}, 0); { var fixture = try store.resumeForWrite(alloc, "malformed-child"); defer fixture.deinit(alloc); try fixture.writeResumeView(alloc, .{ .terminal_rows = 24, .terminal_cols = 80, .complete = true, }, "private cached transcript\n"); const capability = try fixture.childCapability(); var entry = try capability.atomicReplace( alloc, .subagent_control, "control.json", "{", ); entry.deinit(alloc); } app.requested_resume = .last; try std.testing.expectEqual( ResumeViewStage.none, Runtime(TestApp).stageRequestedResumeView(&app), ); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expect(app.session_persistence.resume_view_admission == null); var loaded = try store.resumeForWrite(alloc, "malformed-child"); loaded.deinit(alloc); } test "resume view persistence waits for main frame and retries failed writes" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); app.shell.layout.rows = 6; app.shell.layout.cols = 12; try app.shell.enableShadowVt(alloc); try app.shell.shadow_vt.?.feed("\x1b[1;1Hcached transcript"); app.shell.has_painted_transcript = true; app.shell.last_visible_transcript_top_row = 1; app.shell.last_visible_transcript_last_row = 1; const loaded = &app.session_persistence.writable.?; loaded.resume_view_stale = true; app.terminal.alternate_screen_owner = .full_transcript; Runtime(TestApp).persistResumeViewAfterFrame(&app); try std.testing.expect(loaded.resume_view_stale); app.terminal.alternate_screen_owner = .none; try loaded.log.dir.dir.createDir( std.testing.io, "resume-view.bin", std.Io.File.Permissions.fromMode(0o700), ); Runtime(TestApp).persistResumeViewAfterFrame(&app); try std.testing.expect(loaded.resume_view_stale); try loaded.log.dir.dir.deleteDir(std.testing.io, "resume-view.bin"); Runtime(TestApp).persistResumeViewAfterFrame(&app); try std.testing.expect(!loaded.resume_view_stale); const stat = try loaded.log.dir.dir.statFile( std.testing.io, "resume-view.bin", .{ .follow_symlinks = false }, ); try std.testing.expectEqual(std.Io.File.Kind.file, stat.kind); } test "upgrade resume restores active session with the installed version notice" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).configureStartupPreferences( &app, "configured/model", .process_override, "env/model", types.ReasoningEffort.literal("high"), true, ); try Runtime(TestApp).initializePersistence(&app, true); var calls = [_]types.ToolCall{.{ .id = "call_read", .name = "read_file", .arguments_json = "{\"path\":\"src/main.zig\"}", }}; var results = [_]types.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 = [_]types.ToolExecutionStep{.{ .assistant = @constCast("I'll inspect it."), .tool_calls = calls[0..], .tool_results = results[0..], }}; var resumed_images = [_]types.ImageAttachment{.{ .id = 41, .path = @constCast("/tmp/resumed.png"), .media_type = @constCast("image/png"), .snapshot_path = @constCast("/tmp/fx-session/images/image-41-0123456789abcdef.bin"), .snapshot_sha256 = @constCast("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), }}; const history = [_]types.HistoryTurn{ .{ .compacted_summary = .{ .summary = @constCast("older context"), .removed_turn_count = 2, .compaction_count = 1, } }, .{ .assistant = .{ .user = .{ .text = @constCast("hello"), .images = &resumed_images }, .assistant = @constCast("hi"), } }, .{ .assistant = .{ .user = .{ .text = @constCast("inspect file") }, .assistant = @constCast(""), .execution = .{ .tool_steps = steps[0..] }, } }, .{ .background_command = .{ .user = .{ .text = @constCast("run server") }, .assistant = @constCast("The server is ready."), .execution = .{ .tool_steps = steps[0..] }, .log_path = @constCast("/tmp/server.log"), .expect_url = true, .url = null, } }, }; try writeSessionFixture( alloc, app.session_persistence.store.?, "session-1", &history, 2, ); app.requested_resume = .{ .id = try alloc.dupe(u8, "session-1") }; app.total_web_search_requests = 99; try Runtime(TestApp).resumeRequestedSessionAfterUpgrade(&app, "9.9.9"); try std.testing.expect(app.requested_resume == null); try std.testing.expectEqual(@as(usize, 1), app.startup_resume_anchor_count); try std.testing.expect(app.startup_resume_anchor_saw_writable); try std.testing.expectEqual(@as(usize, 4), app.startup_resume_anchor_history_len); try std.testing.expectEqual(@as(usize, 3), app.startup_resume_anchor_notice_count); try std.testing.expectEqualStrings( "session-1", app.session_persistence.writable.?.active_id, ); try std.testing.expect(app.session_persistence.subagent_host != null); try std.testing.expectEqual(@as(usize, 4), app.session.historyLen()); try std.testing.expectEqual(@as(usize, 42), app.next_image_id); try std.testing.expectEqual(@as(usize, 2), app.session.contextHistoryStart()); const context = try app.session.snapshotContextHistory(alloc); defer session_runtime.freeHistoryTurnSlice(alloc, context); try std.testing.expectEqual(@as(usize, 3), context.len); try std.testing.expect(context[0] == .compacted_summary); try std.testing.expect(std.mem.find(u8, context[0].compacted_summary.summary, "older context") != null); try std.testing.expect(std.mem.find(u8, context[0].compacted_summary.summary, "hello") != null); try std.testing.expectEqualStrings("inspect file", context[1].assistant.user.text); try std.testing.expectEqualStrings("run server", context[2].background_command.user.text); try std.testing.expectEqual(@as(usize, 3), app.notices.items.len); try std.testing.expectEqualStrings("● 𝒇x has been updated to v9.9.9", app.notices.items[0]); try std.testing.expect(std.mem.find(u8, app.notices.items[1], "older context") != null); try std.testing.expect(std.mem.find(u8, app.notices.items[2], "Re-check runtime context") != null); try std.testing.expectEqual(@as(usize, 2), app.completed_tool_statuses.items.len); try std.testing.expectEqualStrings("● Completed read_file\n", app.completed_tool_statuses.items[0]); try std.testing.expectEqualStrings("● Completed read_file\n", app.completed_tool_statuses.items[1]); try std.testing.expectEqual(@as(usize, 3), app.cards.items.len); try std.testing.expect(!app.cards.items[0].has_prior_turns); try std.testing.expect(app.cards.items[1].has_prior_turns); try std.testing.expect(app.cards.items[2].has_prior_turns); try std.testing.expectEqualStrings("hello", app.cards.items[0].text); try std.testing.expectEqualStrings("inspect file", app.cards.items[1].text); try std.testing.expectEqualStrings("run server", app.cards.items[2].text); try std.testing.expectEqualStrings( "hi\nI'll inspect it.\nI'll inspect it.\nThe server is ready.\n", app.assistant_text.items, ); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expectEqual(@as(u64, 17), app.total_input_tokens); try std.testing.expectEqual(@as(u64, 23), app.total_output_tokens); try std.testing.expectEqual(@as(u64, 0), app.total_web_search_requests); try std.testing.expectEqualStrings("env/model", app.selected_model.items); try std.testing.expectEqualStrings( "saved/model", app.session_persistence.session_preferences.?.model, ); try std.testing.expectEqual(types.ReasoningEffort.literal("medium"), app.effort); try std.testing.expect(!app.fast_mode); } test "resumed recovery checkpoint replays its unfinished turn once" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("completed prompt") }, .assistant = @constCast("completed answer"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "paused-recovery", &history, 0, ); { var writable = try app.session_persistence.store.?.resumeForWrite( alloc, "paused-recovery", ); defer writable.deinit(alloc); const checkpoint = session_codec.RecoveryCheckpoint{ .turn_id = 41, .user = .{ .text = @constCast("unfinished prompt") }, .assistant_source = @constCast("Partial output before EOF."), .cause = .response_interrupted, .action = .continuing_response, .route_model = @constCast("saved/model"), .requested_fast_mode = false, .fast_mode = false, .max_provider_attempts = 10, .consumed_provider_attempts = 2, }; _ = try writable.appendEvent( alloc, .{ .recovery_checkpoint_set = .{ .checkpoint = checkpoint } }, 789, .retry_expected_tail, .{}, ); } app.requested_resume = .{ .id = try alloc.dupe(u8, "paused-recovery") }; try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expectEqual(@as(usize, 2), app.cards.items.len); try std.testing.expectEqualStrings("completed prompt", app.cards.items[0].text); try std.testing.expectEqualStrings("unfinished prompt", app.cards.items[1].text); try std.testing.expectEqualStrings( "completed answer\nPartial output before EOF.\n", app.assistant_text.items, ); try std.testing.expectEqual(@as(usize, 2), app.notices.items.len); try std.testing.expect(std.mem.find(u8, app.notices.items[1], "attempt 2/10") != null); } test "unchanged resumed session finalization preserves durable boundaries" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("saved prompt") }, .assistant = @constCast("saved response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "unchanged-finalization", &history, 0, ); var checkpointed = try app.session_persistence.store.?.resumeForWrite( alloc, "unchanged-finalization", ); try checkpointed.writeCheckpointIfDue(alloc, true, .{}); const expected_position = checkpointed.position; const expected_checkpoint_seq = checkpointed.checkpoint_seq.?; const expected_checkpoint_sha256 = checkpointed.checkpoint_sha256.?; checkpointed.deinit(alloc); app.requested_resume = .{ .id = try alloc.dupe(u8, "unchanged-finalization"), }; try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expect(!app.session.usage.isDirty()); Runtime(TestApp).finalizePersistence(&app); var reopened = try app.session_persistence.store.?.resumeForWrite( alloc, "unchanged-finalization", ); defer reopened.deinit(alloc); try std.testing.expectEqual(expected_position.through_seq, reopened.position.through_seq); try std.testing.expectEqual( expected_position.through_event_log_bytes, reopened.position.through_event_log_bytes, ); try std.testing.expectEqual(expected_checkpoint_seq, reopened.checkpoint_seq.?); try std.testing.expectEqualSlices( u8, &expected_checkpoint_sha256, &reopened.checkpoint_sha256.?, ); try std.testing.expectEqual(@as(usize, 1), reopened.state.history.len); try std.testing.expectEqualStrings( "saved response", reopened.state.history[0].assistant.assistant, ); } test "resumeRequestedSession releases the writer when the startup replay anchor fails" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("saved prompt") }, .assistant = @constCast("saved response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "anchor-failure", &history, 0, ); app.requested_resume = .{ .id = try alloc.dupe(u8, "anchor-failure") }; app.fail_startup_resume_anchor = true; try std.testing.expectError( error.TestStartupResumeAnchorFailure, Runtime(TestApp).resumeRequestedSession(&app), ); try std.testing.expectEqual(@as(usize, 1), app.startup_resume_anchor_count); try std.testing.expect(app.startup_resume_anchor_saw_writable); try std.testing.expectEqual(@as(usize, 1), app.startup_resume_anchor_history_len); try std.testing.expectEqual(@as(usize, 1), app.startup_resume_anchor_notice_count); try std.testing.expect(app.requested_resume == null); try std.testing.expect(app.session_persistence.writable == null); try std.testing.expectEqual(@as(usize, 1), app.session.historyLen()); try std.testing.expectEqual(@as(usize, 1), app.notices.items.len); var reopened = try app.session_persistence.store.?.resumeForWrite( alloc, "anchor-failure", ); reopened.deinit(alloc); } test "requested pick target opens the session picker instead of loading a session" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("saved prompt") }, .assistant = @constCast("saved response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "saved-session", &history, 0, ); app.requested_resume = .pick; try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expectEqual(@as(?TestResumeTarget, null), app.requested_resume); try std.testing.expect(app.session_persistence.session_picker.active); try waitForSessionPickerLoad(&app); try std.testing.expectEqual(@as(usize, 0), app.session.historyLen()); try std.testing.expect(app.session_persistence.writable == null); } test "canceling a startup session picker starts a writable fresh session" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).openSessionPicker(&app); try std.testing.expect(app.session_persistence.session_picker.active); try std.testing.expect(app.session_persistence.writable == null); Runtime(TestApp).cancelSessionPickerToComposer(&app); try std.testing.expect(!app.session_persistence.session_picker.active); try std.testing.expect(app.session_persistence.writable != null); } test "interactive session resume uses the live transition and shared restore path" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("saved prompt") }, .assistant = @constCast("saved response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "saved-session", &history, 0, ); try Runtime(TestApp).beginFreshPersistedSession(&app); try Runtime(TestApp).openSessionPicker(&app); try std.testing.expect(app.session_persistence.session_picker.isLoading()); try waitForSessionPickerLoad(&app); try std.testing.expect(try Runtime(TestApp).resumeSelectedSession(&app)); try std.testing.expectEqual(@as(usize, 1), app.live_resume_prepare_count); try std.testing.expectEqual(@as(usize, 1), app.live_resume_finish_count); try std.testing.expectEqual(@as(?u64, 0), app.live_resume_session_lock_deadline_ms); try std.testing.expectEqual(@as(?u64, 0), app.live_resume_commit_lock_deadline_ms); try std.testing.expectEqualStrings( "saved-session", app.session_persistence.writable.?.active_id, ); try std.testing.expectEqual(@as(usize, 1), app.session.historyLen()); try std.testing.expectEqual(@as(usize, 1), app.notices.items.len); try std.testing.expectEqualStrings("● Session resumed: saved prompt", app.notices.items[0]); try std.testing.expect(!app.session_persistence.session_picker.active); const host = app.session_persistence.subagent_host.?; const recovery_deadline = io_mod.milliTimestamp() + 5_000; while ((host.recoveryState() == .scheduled or host.recoveryState() == .running) and io_mod.milliTimestamp() < recovery_deadline) { std.Thread.yield() catch std.atomic.spinLoopHint(); } try std.testing.expectEqual( subagent_tool_host.RecoveryState.complete, host.recoveryState(), ); } test "interactive session resume preserves the current writer when the target is unavailable" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const current_id = try alloc.dupe( u8, app.session_persistence.writable.?.active_id, ); defer alloc.free(current_id); const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; var missing_summary = session_store.SessionSummary{ .id = try alloc.dupe(u8, "missing-session"), .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }; defer missing_summary.deinit(alloc); try picker.appendSummary(alloc, &missing_summary); try std.testing.expectError( error.SessionNotFound, Runtime(TestApp).resumeSelectedSession(&app), ); try std.testing.expectEqualStrings( current_id, app.session_persistence.writable.?.active_id, ); try std.testing.expect(picker.active); try std.testing.expectEqualStrings("missing-session", picker.selectedId().?); try std.testing.expectEqual(@as(usize, 0), app.live_resume_prepare_count); } test "resumeRequestedSession replays persisted model Markdown without parsing generated summaries" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); var completed_tool_names = [_][]u8{@constCast("**RAW_COMPLETED_TOOL_MARKER**")}; const history = [_]types.HistoryTurn{ .{ .assistant = .{ .user = .{ .text = @constCast("assistant model turn") }, .assistant = @constCast( "# ASSISTANT_HEADING_MARKER\n" ++ "- [x] ASSISTANT_TASK_MARKER\n", ), } }, .{ .background_command = .{ .user = .{ .text = @constCast("background model turn") }, .assistant = @constCast( "| Name | Value |\n" ++ "| --- | --- |\n" ++ "| BACKGROUND_TABLE_MARKER | 42 |\n", ), .log_path = @constCast("/tmp/background.log"), .expect_url = false, } }, .{ .interrupted = .{ .user = .{ .text = @constCast("interrupted model turn") }, .assistant = @constCast( "```zig\n" ++ "const INTERRUPTED_CODE_MARKER = true;\n" ++ "```\n" ++ "---", ), .completed_tool_names = completed_tool_names[0..], } }, }; try writeSessionFixture( alloc, app.session_persistence.store.?, "markdown-replay", &history, 0, ); app.requested_resume = .{ .id = try alloc.dupe(u8, "markdown-replay") }; try Runtime(TestApp).resumeRequestedSession(&app); const expected_events = [_]AssistantPresentationEvent{ .text, .text, .table, .text, .code_block, .thematic_rule, .text, }; try std.testing.expectEqualSlices( AssistantPresentationEvent, &expected_events, app.assistant_presentation_events.items, ); try std.testing.expect(std.mem.find(u8, app.assistant_text.items, "ASSISTANT_HEADING_MARKER") != null); try std.testing.expect(std.mem.find(u8, app.assistant_text.items, "ASSISTANT_TASK_MARKER") != null); try std.testing.expect(std.mem.find(u8, app.assistant_text.items, "# ASSISTANT_HEADING_MARKER") == null); try std.testing.expect(std.mem.find(u8, app.assistant_text.items, "- [x] ASSISTANT_TASK_MARKER") == null); try std.testing.expect(std.mem.endsWith(u8, app.assistant_text.items, "\n\n\n")); try std.testing.expectEqual(@as(usize, 1), app.assistant_tables.items.len); try std.testing.expectEqualStrings( "BACKGROUND_TABLE_MARKER", app.assistant_tables.items[0].rows[1].cells[0], ); try std.testing.expectEqual(@as(usize, 1), app.assistant_code_blocks.items.len); try std.testing.expectEqualStrings("zig", app.assistant_code_blocks.items[0].language); try std.testing.expectEqualStrings( "const INTERRUPTED_CODE_MARKER = true;\n", app.assistant_code_blocks.items[0].code, ); try std.testing.expectEqual(@as(usize, 1), app.assistant_thematic_rule_count); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expectEqual(@as(usize, 3), app.notices.items.len); try std.testing.expectEqualStrings("● System: cancelled", app.notices.items[2]); } test "resume Markdown replay releases undelivered table payloads once" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); app.fail_assistant_table_append = true; try std.testing.expectError( error.TestAssistantTableAppendFailure, Runtime(TestApp).writeAssistantHistoryMarkdown( &app, "| Name | Value |\n| --- | --- |\n| table | 42 |\n", ), ); } test "resume Markdown replay releases undelivered code payloads once" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); app.fail_assistant_code_block_append = true; try std.testing.expectError( error.TestAssistantCodeBlockAppendFailure, Runtime(TestApp).writeAssistantHistoryMarkdown( &app, "```zig\nconst value = 42;\n```\n", ), ); } test "resumeRequestedSession cleans final owner after post-transfer failure" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try writeSessionFixture( alloc, app.session_persistence.store.?, "session-1", &.{.{ .compacted_summary = .{ .summary = @constCast("post-transfer failure"), .removed_turn_count = 1, .compaction_count = 1, } }}, 0, ); app.requested_resume = .{ .id = try alloc.dupe(u8, "session-1") }; app.fail_system_notice = true; try std.testing.expectError( error.TestSystemNoticeFailure, Runtime(TestApp).resumeRequestedSession(&app), ); try std.testing.expect(app.writable_seen_during_notice_failure); try std.testing.expect(app.requested_resume == null); try std.testing.expect(app.session_persistence.writable == null); var reopened = try app.session_persistence.store.?.resumeForWrite( alloc, "session-1", ); defer reopened.deinit(alloc); try std.testing.expectEqualStrings("session-1", reopened.active_id); } test "resumeRequestedSession replays active-tool interruption with live cancellation notice" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{ .{ .interrupted = .{ .user = .{ .text = @constCast("inspect the browser") }, .tool_call = .{ .id = "call_browser", .name = "browser_snapshot", .arguments_json = "{\"url\":\"http://localhost:3000\"}", }, } }, }; try writeSessionFixture( alloc, app.session_persistence.store.?, "session-1", &history, 0, ); app.requested_resume = .{ .id = try alloc.dupe(u8, "session-1") }; try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expectEqual(@as(usize, 1), app.cards.items.len); try std.testing.expectEqualStrings("inspect the browser", app.cards.items[0].text); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expectEqual(@as(usize, 2), app.notices.items.len); try std.testing.expectEqualStrings("● Session resumed: inspect the browser", app.notices.items[0]); try std.testing.expectEqualStrings("● System: cancelled", app.notices.items[1]); try std.testing.expect(std.mem.find(u8, app.transcript.items, "localhost") == null); } test "resumeRequestedSession preserves failed partial terminal reason" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{ .{ .interrupted = .{ .user = .{ .text = @constCast("stream a response") }, .assistant = @constCast("partial response"), .terminal_reason = .failed, } }, }; try writeSessionFixture( alloc, app.session_persistence.store.?, "session-1", &history, 0, ); app.requested_resume = .{ .id = try alloc.dupe(u8, "session-1") }; try Runtime(TestApp).resumeRequestedSession(&app); try std.testing.expect(std.mem.find(u8, app.assistant_text.items, "partial response") != null); try std.testing.expectEqual(@as(usize, 2), app.notices.items.len); try std.testing.expectEqualStrings("● System: failed", app.notices.items[1]); try std.testing.expect(std.mem.find(u8, app.notices.items[1], "cancelled") == null); } test "cancelled command presentation survives a persisted session restart" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var session_id: ?[]u8 = null; defer if (session_id) |id| alloc.free(id); { var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const lifecycle_id = types.ToolLifecycleId{ .turn_id = 7, .call_id = "cancelled-command", }; Runtime(TestApp).recordAppliedCommandOutput( &app, lifecycle_id, .stdout, "INTERRUPT_START\n", ); Runtime(TestApp).recordAppliedCommandOutput( &app, lifecycle_id, .stderr, "warning\n", ); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = lifecycle_id, .outcome = .{ .kind = .cancelled, .summary = "Cancelled slow" }, .command_artifact_handle = "fx-command-cancelled.log", } }, true, ); Runtime(TestApp).recordCommandOutputComplete(&app, lifecycle_id); try Runtime(TestApp).appendHistoryTurn(&app, .{ .interrupted = .{ .user = .{ .text = @constCast("run the slow command") }, .assistant = @constCast("I started it."), .tool_call = .{ .id = "cancelled-command", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"slow\"}", }, } }); session_id = try alloc.dupe(u8, app.session_persistence.writable.?.active_id); Runtime(TestApp).finalizePersistence(&app); } var resumed = try TestApp.init(alloc, paths.workspace); defer resumed.deinit(); try configureTestPreferences(&resumed); try Runtime(TestApp).initializePersistence(&resumed, true); resumed.requested_resume = .{ .id = try alloc.dupe(u8, session_id.?) }; try Runtime(TestApp).resumeRequestedSession(&resumed); try std.testing.expectEqualStrings("INTERRUPT_START\n", resumed.command_stdout.items); try std.testing.expectEqualStrings("warning\n", resumed.command_stderr.items); try std.testing.expectEqual(@as(usize, 1), resumed.command_output_flush_count); try std.testing.expectEqual(@as(usize, 1), resumed.cancelled_command_detail_count); try std.testing.expect(resumed.cancelled_command_replayed_output); try std.testing.expectEqualStrings( "fx-command-cancelled.log", resumed.cancelled_command_artifact_handle.?, ); try std.testing.expectEqualSlices( ReplayEvent, &.{ .completed_tool_status, .command_output_chunk, .command_output_chunk, .command_output_summary_flush, }, resumed.replay_events.items, ); } test "cancelled command capture creation failure preserves the row" { const alloc = std.testing.allocator; var failing = std.testing.FailingAllocator.init(alloc, .{}); const app_alloc = failing.allocator(); var app = try TestApp.init(app_alloc, "/workspace"); defer app.deinit(); const lifecycle_id = types.ToolLifecycleId{ .turn_id = 8, .call_id = "capture-unavailable", }; failing.fail_index = failing.alloc_index + 1; Runtime(TestApp).recordAppliedCommandOutput( &app, lifecycle_id, .stdout, "applied before capture failure\n", ); failing.fail_index = std.math.maxInt(usize); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = lifecycle_id, .outcome = .{ .kind = .cancelled, .summary = "Cancelled" }, } }, true, ); Runtime(TestApp).recordCommandOutputComplete(&app, lifecycle_id); try Runtime(TestApp).appendHistoryTurn(&app, .{ .interrupted = .{ .user = .{ .text = @constCast("cancel") }, .tool_call = .{ .id = "capture-unavailable", .name = "run_command", .arguments_json = "{\"command\":\"slow\"}", }, } }); const history = try app.session.snapshotHistory(alloc); defer session_runtime.freeHistoryTurnSlice(alloc, history); const presentation = history[0].interrupted.cancelled_command orelse return error.TestExpectedCancelledCommandPresentation; try std.testing.expect(presentation.output_replay == null); } test "cancelled command identity failure never persists a replay suffix" { const alloc = std.testing.allocator; var failing = std.testing.FailingAllocator.init(alloc, .{}); const app_alloc = failing.allocator(); var app = try TestApp.init(app_alloc, "/workspace"); defer app.deinit(); const lifecycle_id = types.ToolLifecycleId{ .turn_id = 9, .call_id = "identity-unavailable", }; failing.fail_index = failing.alloc_index; Runtime(TestApp).recordAppliedCommandOutput( &app, lifecycle_id, .stdout, "first chunk\n", ); failing.fail_index = std.math.maxInt(usize); Runtime(TestApp).recordAppliedCommandOutput( &app, lifecycle_id, .stdout, "must not become a suffix\n", ); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = lifecycle_id, .outcome = .{ .kind = .cancelled, .summary = "Cancelled" }, } }, true, ); Runtime(TestApp).recordCommandOutputComplete(&app, lifecycle_id); try Runtime(TestApp).appendHistoryTurn(&app, .{ .interrupted = .{ .user = .{ .text = @constCast("cancel") }, .tool_call = .{ .id = "identity-unavailable", .name = "run_command", .arguments_json = "{\"command\":\"slow\"}", }, } }); const history = try app.session.snapshotHistory(alloc); defer session_runtime.freeHistoryTurnSlice(alloc, history); const presentation = history[0].interrupted.cancelled_command orelse return error.TestExpectedCancelledCommandPresentation; try std.testing.expect(presentation.output_replay == null); } test "cancelled command spill failure degrades without losing the row" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); const lifecycle_id = types.ToolLifecycleId{ .turn_id = 10, .call_id = "spill-unavailable", }; const output = try alloc.alloc(u8, default_cancelled_command_replay_inline_limit + 1); defer alloc.free(output); @memset(output, 'x'); Runtime(TestApp).recordAppliedCommandOutput(&app, lifecycle_id, .stdout, output); Runtime(TestApp).recordAppliedCommandOutput( &app, lifecycle_id, .stderr, "must not become a suffix\n", ); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = lifecycle_id, .outcome = .{ .kind = .cancelled, .summary = "Cancelled" }, } }, true, ); Runtime(TestApp).recordCommandOutputComplete(&app, lifecycle_id); try Runtime(TestApp).appendHistoryTurn(&app, .{ .interrupted = .{ .user = .{ .text = @constCast("cancel") }, .tool_call = .{ .id = "spill-unavailable", .name = "run_command", .arguments_json = "{\"command\":\"slow\"}", }, } }); const history = try app.session.snapshotHistory(alloc); defer session_runtime.freeHistoryTurnSlice(alloc, history); const replay = history[0].interrupted.cancelled_command.?.output_replay orelse return error.TestExpectedReplay; try std.testing.expect(replay == .unavailable); } test "reactive interrupted history discards the app replay candidate" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const lifecycle_id = types.ToolLifecycleId{ .turn_id = 9, .call_id = "reactive-cancelled-command", }; const output = try alloc.alloc(u8, default_cancelled_command_replay_inline_limit + 1); defer alloc.free(output); @memset(output, 'x'); Runtime(TestApp).recordAppliedCommandOutput(&app, lifecycle_id, .stdout, output); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = lifecycle_id, .outcome = .{ .kind = .cancelled, .summary = "Cancelled" }, } }, true, ); Runtime(TestApp).recordCommandOutputComplete(&app, lifecycle_id); var before = try Runtime(TestApp).childCapability(&app).?.iterate( alloc, .command_artifacts, ); defer before.deinit(); try std.testing.expectEqual(@as(usize, 1), before.names.len); var execution_calls = [_]types.ToolCall{.{ .id = "reactive-execution-command", .name = "run_command", .arguments_json = "{\"command\":\"restricted then broader\"}", }}; var execution_results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("reactive-execution-command"), .tool_name = @constCast("run_command"), .status = .failure, .output = @constCast("restricted and broader attempts retained"), .output_bytes = 40, .stored_output_bytes = 40, }}; var execution_steps = [_]types.ToolExecutionStep{.{ .tool_calls = execution_calls[0..], .tool_results = execution_results[0..], }}; try Runtime(TestApp).appendHistoryTurn(&app, .{ .interrupted = .{ .user = .{ .text = @constCast("cancel") }, .tool_call = null, .execution = .{ .tool_steps = execution_steps[0..] }, } }); const history = try app.session.snapshotHistory(alloc); defer session_runtime.freeHistoryTurnSlice(alloc, history); try std.testing.expect(history[0].interrupted.cancelled_command == null); try Runtime(TestApp).replayHistory(&app, history); try std.testing.expectEqual(@as(usize, 1), app.completed_tool_statuses.items.len); var after = try Runtime(TestApp).childCapability(&app).?.iterate( alloc, .command_artifacts, ); defer after.deinit(); try std.testing.expectEqual(@as(usize, 0), after.names.len); } test "ordinary command terminal discards the app replay candidate" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); const lifecycle_id = types.ToolLifecycleId{ .turn_id = 10, .call_id = "completed-command", }; Runtime(TestApp).recordAppliedCommandOutput(&app, lifecycle_id, .stdout, "done\n"); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = lifecycle_id, .outcome = .{ .kind = .completed, .summary = "Completed" }, } }, true, ); try std.testing.expect(app.session_persistence.pending_cancelled_command == null); } test "cancelled durable terminal action preserves the exec replay candidate" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); const exec_id = types.ToolLifecycleId{ .turn_id = 11, .call_id = "cancelled-exec", }; const durable_id = types.ToolLifecycleId{ .turn_id = 11, .call_id = "cancelled-read", }; Runtime(TestApp).recordAppliedCommandOutput( &app, exec_id, .stdout, "captured before cancellation\n", ); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = exec_id, .outcome = .{ .kind = .cancelled, .summary = "Cancelled exec" }, } }, true, ); Runtime(TestApp).recordCommandOutputComplete(&app, exec_id); Runtime(TestApp).recordToolTerminal( &app, .{ .terminal = .{ .id = durable_id, .outcome = .{ .kind = .cancelled, .summary = "Cancelled read" }, } }, false, ); const pending = app.session_persistence.pending_cancelled_command orelse return error.TestExpectedEqual; try std.testing.expect(pending.matches(exec_id)); try std.testing.expect(pending.cancelled); try std.testing.expect(pending.completed); } test "zero-output cancelled command restores detail without an output block" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); const history = [_]types.HistoryTurn{.{ .interrupted = .{ .user = .{ .text = @constCast("cancel immediately") }, .tool_call = .{ .id = "zero-output", .name = "run_command", .arguments_json = "{\"command\":\"sleep 20\"}", }, .cancelled_command = .{}, } }}; try Runtime(TestApp).replayHistory(&app, &history); try std.testing.expectEqual(@as(usize, 1), app.completed_tool_statuses.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_output_writes.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_output_flush_count); try std.testing.expectEqual(@as(usize, 1), app.cancelled_command_detail_count); try std.testing.expect(!app.cancelled_command_replayed_output); try std.testing.expectEqual(@as(usize, 1), app.historical_tool_detail_entry_ids.items.len); } test "cancelled command replay rejects unsafe handles without partial output" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const unsafe_handles = [_][]const u8{ "../escape.bin", "/tmp/escape.bin", "nested/escape.bin", }; for (unsafe_handles, 0..) |handle, index| { var call_id_buf: [32]u8 = undefined; const call_id = try std.fmt.bufPrint(&call_id_buf, "unsafe-{d}", .{index}); const history = [_]types.HistoryTurn{.{ .interrupted = .{ .user = .{ .text = @constCast("cancel") }, .tool_call = .{ .id = call_id, .name = "run_command", .arguments_json = "{\"command\":\"slow\"}", }, .cancelled_command = .{ .output_replay = .{ .available = .{ .handle = handle, .framed_bytes = 32, } } }, } }}; try Runtime(TestApp).replayHistory(&app, &history); } try std.testing.expectEqual(unsafe_handles.len, app.completed_tool_statuses.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_output_writes.items.len); try std.testing.expectEqual(@as(usize, 0), app.command_output_flush_count); try std.testing.expectEqual(unsafe_handles.len, app.cancelled_command_detail_count); try std.testing.expect(!app.cancelled_command_replayed_output); try std.testing.expectEqual(unsafe_handles.len, app.historical_tool_detail_entry_ids.items.len); } test "cancelled replay survives event persistence failure after history insertion" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const capability = Runtime(TestApp).childCapability(&app).?; var capture_arena = std.heap.ArenaAllocator.init(alloc); defer capture_arena.deinit(); const capture_alloc = capture_arena.allocator(); const capture = try command_replay_store.Capture.create(capture_alloc, 1, capability); capture.appendAccepted(capture_alloc, .stdout, "accepted\n"); const replay = capture.retain(capture_alloc) orelse return error.TestExpectedReplay; const descriptor = switch (replay) { .available => |value| value, .unavailable => return error.TestExpectedReplay, }; const loaded = &app.session_persistence.writable.?; if (loaded.commit_lifecycle) |*lifecycle| lifecycle.deinit(alloc); loaded.commit_lifecycle = .{ .prepare_fn = FailingCommitLifecycle.prepare }; try Runtime(TestApp).appendHistoryTurn(&app, .{ .interrupted = .{ .user = .{ .text = @constCast("cancel") }, .tool_call = .{ .id = "degraded-cancel", .name = "run_command", .arguments_json = "{\"command\":\"slow\"}", }, .cancelled_command = .{ .output_replay = replay }, } }); try std.testing.expectEqual(@as(usize, 1), app.session.historyLen()); _ = try capability.stat(.command_artifacts, descriptor.handle); } test "cancelled replay is deleted when history insertion fails" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const capability = Runtime(TestApp).childCapability(&app).?; var capture_arena = std.heap.ArenaAllocator.init(alloc); defer capture_arena.deinit(); const capture_alloc = capture_arena.allocator(); const capture = try command_replay_store.Capture.create(capture_alloc, 1, capability); capture.appendAccepted(capture_alloc, .stdout, "accepted\n"); const replay = capture.retain(capture_alloc) orelse return error.TestExpectedReplay; const descriptor = switch (replay) { .available => |value| value, .unavailable => return error.TestExpectedReplay, }; var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0 }); app.alloc = failing.allocator(); defer app.alloc = alloc; try std.testing.expectError( error.OutOfMemory, Runtime(TestApp).appendHistoryTurn(&app, .{ .interrupted = .{ .user = .{ .text = @constCast("cancel") }, .tool_call = .{ .id = "failed-insert", .name = "run_command", .arguments_json = "{\"command\":\"slow\"}", }, .cancelled_command = .{ .output_replay = replay }, } }), ); try std.testing.expectError( error.FileNotFound, capability.stat(.command_artifacts, descriptor.handle), ); } test "appendHistoryTurn commits canonical history event and totals" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); app.total_web_search_requests = 3; app.total_input_tokens = 7; app.total_output_tokens = 11; const turn = try session_runtime.makeAssistantTurn(alloc, "question", "answer"); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); var loaded = try app.session_persistence.store.?.loadReadOnly( alloc, app.session_persistence.writable.?.active_id, ); defer loaded.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), loaded.history.len); try std.testing.expectEqualStrings("question", loaded.history[0].assistant.user.text); try std.testing.expectEqualStrings("answer", loaded.history[0].assistant.assistant); try std.testing.expectEqual(@as(u64, 7), loaded.total_input_tokens); try std.testing.expectEqual(@as(u64, 11), loaded.total_output_tokens); } const SnapshotOwnershipProbe = struct { transfers: usize = 0, fn retain(_: *anyopaque) void {} fn release(_: *anyopaque) void {} fn transfer(raw: *anyopaque) void { const self: *SnapshotOwnershipProbe = @ptrCast(@alignCast(raw)); self.transfers += 1; } fn handle(self: *SnapshotOwnershipProbe) types.SnapshotFileOwnership { return .{ .ctx = self, .retain_fn = retain, .release_fn = release, .transfer_fn = transfer, }; } }; test "appendFinishedPrompt transfers snapshot ownership after history acceptance" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/tmp/workspace"); defer app.deinit(); var probe = SnapshotOwnershipProbe{}; try Runtime(TestApp).appendFinishedPrompt(&app, .{ .turn = .{ .assistant = .{ .user = .{ .text = @constCast("inspect") }, .assistant = @constCast("done"), } }, .snapshot_file_ownership = probe.handle(), }); try std.testing.expectEqual(@as(usize, 1), app.session.historyLen()); try std.testing.expectEqual(@as(usize, 1), probe.transfers); } test "appendFinishedPrompt does not transfer snapshot ownership when history insertion fails" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/tmp/workspace"); defer app.deinit(); var probe = SnapshotOwnershipProbe{}; var failing = std.testing.FailingAllocator.init( alloc, .{ .fail_index = 0 }, ); app.alloc = failing.allocator(); defer app.alloc = alloc; try std.testing.expectError( error.OutOfMemory, Runtime(TestApp).appendFinishedPrompt(&app, .{ .turn = .{ .assistant = .{ .user = .{ .text = @constCast("inspect") }, .assistant = @constCast("done"), } }, .snapshot_file_ownership = probe.handle(), }), ); try std.testing.expectEqual(@as(usize, 0), app.session.historyLen()); try std.testing.expectEqual(@as(usize, 0), probe.transfers); } test "visual epoch history append reports committed without persistence" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var app = try TestApp.init(alloc, workspace); defer app.deinit(); const turn = try session_runtime.makeAssistantTurn(alloc, "question", "answer"); defer session_runtime.freeHistoryTurn(alloc, turn); try std.testing.expectEqual( HistoryAppendOutcome.committed, try Runtime(TestApp).appendHistoryTurnForVisualEpoch(&app, turn), ); try std.testing.expectEqual(@as(usize, 1), app.session.historyLen()); } test "appendHistoryTurn preserves canonical turns above the context limit" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); app.session.max_history_turns = 2; try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const users = [_][]const u8{ "one", "two", "three" }; const assistants = [_][]const u8{ "first", "second", "third" }; for (users, assistants) |user, assistant| { const turn = try session_runtime.makeAssistantTurn(alloc, user, assistant); defer session_runtime.freeHistoryTurn(alloc, turn); try Runtime(TestApp).appendHistoryTurn(&app, turn); } var loaded = try app.session_persistence.store.?.loadReadOnly( alloc, app.session_persistence.writable.?.active_id, ); defer loaded.deinit(alloc); try std.testing.expectEqual(@as(usize, 3), loaded.history.len); try std.testing.expectEqualStrings("one", loaded.history[0].assistant.user.text); try std.testing.expectEqualStrings("three", loaded.history[2].assistant.user.text); } test "fresh interactive session retains one writable schema-v3 handle" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try Runtime(TestApp).configureStartupPreferences( &app, "configured/model", .user_workspace, "configured/model", types.ReasoningEffort.literal("high"), true, ); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const loaded = app.session_persistence.writable orelse return error.ExpectedWritableSession; try std.testing.expectEqualStrings( "configured/model", loaded.state.preferences.model, ); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), loaded.state.preferences.effort); try std.testing.expect(loaded.state.preferences.fast_mode); } test "runtime-first preference targets commit independently with one session event" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const prior_seq = app.session_persistence.writable.?.position.through_seq; var result = Runtime(TestApp).commitRuntimePreferences( &app, .{ .model = "configured/model" }, ); defer result.deinit(alloc); try std.testing.expect(result.settings_error == null); try std.testing.expect(result.session_error == null); try std.testing.expectEqual( prior_seq + 1, app.session_persistence.writable.?.position.through_seq, ); try std.testing.expectEqualStrings( "configured/model", app.session_persistence.writable.?.state.preferences.model, ); } test "combined preference patch writes user defaults cleans legacy fields and appends one session event" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const fixture = try std.fmt.allocPrint( alloc, "{{\"workspaces\":{{\"{s}\":{{\"model\":\"legacy/model\",\"effort\":\"low\"}}}}}}\n", .{paths.workspace}, ); defer alloc.free(fixture); var settings_file = try tmp.dir.createFile(io_mod.getIo(), "home/.fx/settings.json", .{ .truncate = true }); try settings_file.writeStreamingAll(io_mod.getIo(), fixture); settings_file.close(io_mod.getIo()); const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const prior_seq = app.session_persistence.writable.?.position.through_seq; var result = Runtime(TestApp).commitRuntimePreferences( &app, .{ .model = "user/model", .effort = types.ReasoningEffort.literal("high"), .fast_mode = false, }, ); defer result.deinit(alloc); try std.testing.expect(result.settings_error == null); try std.testing.expect(result.session_error == null); try std.testing.expectEqual(@as(usize, 2), result.settings_outcome.?.committed.cleanup.fields_removed); try std.testing.expectEqual(@as(usize, 1), result.settings_outcome.?.committed.cleanup.workspaces_changed); try std.testing.expectEqual(@as(usize, 2), result.settings_outcome.?.committed.cleanup.recovery_paths.len); try std.testing.expectEqual(prior_seq + 1, app.session_persistence.writable.?.position.through_seq); try std.testing.expectEqualStrings("user/model", app.session_persistence.writable.?.state.preferences.model); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), app.session_persistence.writable.?.state.preferences.effort); try std.testing.expect(!app.session_persistence.writable.?.state.preferences.fast_mode); var detailed = try config_runtime.loadMergedSettingsDetailed(alloc, paths.workspace); defer detailed.deinit(alloc); try std.testing.expectEqualStrings("user/model", detailed.settings.model.?); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), detailed.settings.effort.?); try std.testing.expectEqual(false, detailed.settings.fast_mode.?); try std.testing.expectEqual(config_runtime.ConfigSource.user_global, detailed.sources.model); try std.testing.expectEqual(config_runtime.ConfigSource.user_global, detailed.sources.effort); try std.testing.expectEqual(config_runtime.ConfigSource.user_global, detailed.sources.fast_mode); } test "session picker refuses activation while a response is active" { const alloc = std.testing.allocator; var app = try TestApp.init(alloc, "/workspace"); defer app.deinit(); app.stream.active = true; try Runtime(TestApp).openSessionPicker(&app); try std.testing.expect(!app.session_persistence.session_picker.active); try std.testing.expectEqual(@as(usize, 1), app.notices.items.len); try std.testing.expect(std.mem.find(u8, app.notices.items[0], "unavailable") != null); } test "session picker owns non-current session summaries" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const prior_id = "prior-session"; const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("saved prompt") }, .assistant = @constCast("saved response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, prior_id, &history, 0, ); try Runtime(TestApp).beginFreshPersistedSession(&app); const active_id = app.session_persistence.writable.?.active_id; try Runtime(TestApp).openSessionPicker(&app); try std.testing.expect(app.session_persistence.session_picker.isLoading()); try waitForSessionPickerLoad(&app); const picker = &app.session_persistence.session_picker; try std.testing.expect(picker.active); try std.testing.expectEqual(@as(usize, 1), picker.summaries.items.len); try std.testing.expectEqualStrings(prior_id, picker.selectedId().?); try std.testing.expect(!std.mem.eql(u8, picker.selectedId().?, active_id)); try std.testing.expectEqualStrings("saved prompt", picker.summaries.items[0].title.?); try std.testing.expectEqualStrings(paths.workspace, picker.summaries.items[0].workspace_root.?); Runtime(TestApp).cancelSessionPicker(&app); try std.testing.expect(!picker.active); try std.testing.expectEqual(@as(usize, 0), picker.summaries.items.len); } test "session picker reopen keeps the loaded page visible while refreshing" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("cached prompt") }, .assistant = @constCast("cached response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "cached-session", &history, 0, ); try Runtime(TestApp).beginFreshPersistedSession(&app); try Runtime(TestApp).openSessionPicker(&app); try waitForSessionPickerLoad(&app); try std.testing.expectEqual(.ready, app.session_persistence.session_picker.load_state); try std.testing.expectEqualStrings( "cached-session", app.session_persistence.session_picker.selectedId().?, ); Runtime(TestApp).cancelSessionPicker(&app); try Runtime(TestApp).openSessionPicker(&app); try std.testing.expectEqual(.ready, app.session_persistence.session_picker.load_state); try std.testing.expectEqualStrings( "cached-session", app.session_persistence.session_picker.selectedId().?, ); } test "session picker prewarms current and all workspace pages before opening" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("prewarmed prompt") }, .assistant = @constCast("prewarmed response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "prewarmed-session", &history, 0, ); try Runtime(TestApp).beginFreshPersistedSession(&app); Runtime(TestApp).primeSessionPicker(&app); try waitForSessionPickerPrewarm(&app); try std.testing.expect(!app.session_persistence.session_picker.active); try std.testing.expect(app.session_persistence.session_picker_current_cache.ready); try std.testing.expect(app.session_persistence.session_picker_all_cache.ready); try Runtime(TestApp).openSessionPicker(&app); try std.testing.expectEqual(.ready, app.session_persistence.session_picker.load_state); try std.testing.expectEqualStrings( "prewarmed-session", app.session_persistence.session_picker.selectedId().?, ); try std.testing.expect(try Runtime(TestApp).toggleSessionPickerScope(&app)); try std.testing.expectEqual(.ready, app.session_persistence.session_picker.load_state); try std.testing.expectEqual(SessionPickerScope.all_workspaces, app.session_persistence.session_picker.scope); try std.testing.expectEqualStrings( "prewarmed-session", app.session_persistence.session_picker.selectedId().?, ); } test "session picker rejects a load more completion after switching to a fresh cached scope" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); const current_page: session_store.ResumableSessionPage = .{}; var all_page: session_store.ResumableSessionPage = .{ .has_more = true }; defer all_page.deinit(alloc); try all_page.summaries.append(alloc, .{ .id = try alloc.dupe(u8, "foreign-session"), .created_at_ms = 1, .updated_at_ms = 1, .workspace_root = try alloc.dupe(u8, "/foreign-workspace"), .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }); const active_id = app.session_persistence.writable.?.active_id; const limit = resumePageLimitForRows(app.shell.layout.rows); const loaded_at_ns = io_mod.nanoTimestamp(); try app.session_persistence.session_picker_current_cache.install( ¤t_page, active_id, limit, loaded_at_ns, ); try app.session_persistence.session_picker_all_cache.install( &all_page, active_id, limit, loaded_at_ns, ); try Runtime(TestApp).openAllSessionPicker(&app); const picker = &app.session_persistence.session_picker; try std.testing.expectEqual(SessionPickerScope.all_workspaces, picker.scope); try std.testing.expectEqual(.ready, picker.load_state); const task_alloc = std.heap.c_allocator; const stale = try createHeldSessionPickerTask( picker.generation, active_id, app.session_persistence.store.?, ); stale.request.scope = .all_workspaces; stale.request.continuation = .{ .updated_at_ms = 1, .id = try task_alloc.dupe(u8, "stale-page-position"), }; stale.page = .{}; try stale.page.?.summaries.append(task_alloc, .{ .id = try task_alloc.dupe(u8, "stale-foreign-session"), .created_at_ms = 0, .updated_at_ms = 0, .workspace_root = try task_alloc.dupe(u8, "/foreign-workspace"), .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }); stale.done.store(true, .release); app.session_persistence.session_picker_load.task = stale; try std.testing.expect(try Runtime(TestApp).toggleSessionPickerScope(&app)); try std.testing.expectEqual(SessionPickerScope.current_workspace, picker.scope); try std.testing.expectEqual(@as(usize, 0), picker.summaries.items.len); try Runtime(TestApp).pollSessionPicker(&app); try std.testing.expectEqual(SessionPickerScope.current_workspace, picker.scope); try std.testing.expectEqual(@as(usize, 0), picker.summaries.items.len); } test "session picker current mode filters workspace and all mode includes every workspace" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace-a"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace-b"); const home_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home_path); const workspace_a = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace-a"); defer alloc.free(workspace_a); const workspace_b = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace-b"); defer alloc.free(workspace_b); const home = try TestHome.install(alloc, home_path); defer home.deinit(); var app = try TestApp.init(alloc, workspace_a); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history_a = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("alpha workspace prompt") }, .assistant = @constCast("saved response"), } }}; const history_b = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("beta workspace prompt") }, .assistant = @constCast("saved response"), } }}; try writeSessionFixture(alloc, app.session_persistence.store.?, "workspace-a-session", &history_a, 0); var store_b = try session_store.Store.initFromHome(alloc, home_path, workspace_b); defer store_b.deinit(alloc); try writeSessionFixture(alloc, store_b, "workspace-b-session", &history_b, 0); try Runtime(TestApp).beginFreshPersistedSession(&app); try Runtime(TestApp).openSessionPicker(&app); try waitForSessionPickerLoad(&app); const picker = &app.session_persistence.session_picker; try std.testing.expectEqual(SessionPickerScope.current_workspace, picker.scope); try std.testing.expectEqual(@as(usize, 1), picker.summaries.items.len); try std.testing.expectEqualStrings("workspace-a-session", picker.summaries.items[0].id); Runtime(TestApp).cancelSessionPicker(&app); try Runtime(TestApp).openAllSessionPicker(&app); try waitForSessionPickerLoad(&app); try std.testing.expectEqual(SessionPickerScope.all_workspaces, picker.scope); try std.testing.expectEqual(@as(usize, 2), picker.summaries.items.len); var found_workspace_a = false; var found_workspace_b = false; for (picker.summaries.items) |summary| { const workspace_root = summary.workspace_root orelse continue; found_workspace_a = found_workspace_a or std.mem.eql(u8, workspace_root, workspace_a); found_workspace_b = found_workspace_b or std.mem.eql(u8, workspace_root, workspace_b); } try std.testing.expect(found_workspace_a); try std.testing.expect(found_workspace_b); } test "session picker excludes empty sessions and reports loading" { const alloc = std.testing.allocator; var picker: SessionPicker = .{ .active = true, .load_state = .loading }; defer picker.deinit(alloc); try std.testing.expect(picker.isLoading()); var empty_summary = session_store.SessionSummary{ .id = try alloc.dupe(u8, "empty-session"), .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 0, }; defer empty_summary.deinit(alloc); try picker.appendSummary(alloc, &empty_summary); try std.testing.expectEqual(@as(usize, 0), picker.summaries.items.len); } test "session picker navigation clamps at both ends instead of wrapping" { const alloc = std.testing.allocator; var picker: SessionPicker = .{ .active = true, .load_state = .ready }; defer picker.deinit(alloc); for ([_][]const u8{ "first", "second" }) |id| { var summary = session_store.SessionSummary{ .id = try alloc.dupe(u8, id), .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }; defer summary.deinit(alloc); try picker.appendSummary(alloc, &summary); } // At the top, moving up stays on the first item (no wrap to the last). try std.testing.expect(picker.moveVisibleItems(-1, 2)); try std.testing.expectEqual(@as(usize, 0), picker.selected); // At the bottom, moving down stays on the last item (no wrap to the first). picker.selected = 1; try std.testing.expect(picker.moveVisibleItems(1, 2)); try std.testing.expectEqual(@as(usize, 1), picker.selected); } test "session picker selects the first visible session from a loaded page" { try std.testing.expectEqual( @as(usize, 10), selectionAfterLoadedPage(10, 20, true), ); try std.testing.expectEqual( @as(usize, 10), selectionAfterLoadedPage(10, 10, true), ); try std.testing.expectEqual( @as(usize, 9), selectionAfterLoadedPage(10, 10, false), ); try std.testing.expectEqual( @as(usize, 0), selectionAfterLoadedPage(0, 0, false), ); } test "session picker clears selection feedback when the target context changes" { const alloc = std.testing.allocator; var picker: SessionPicker = .{ .active = true, .load_state = .ready }; defer picker.deinit(alloc); var first = session_store.SessionSummary{ .id = try alloc.dupe(u8, "first"), .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }; defer first.deinit(alloc); try picker.appendSummary(alloc, &first); var second = session_store.SessionSummary{ .id = try alloc.dupe(u8, "second"), .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }; defer second.deinit(alloc); try picker.appendSummary(alloc, &second); picker.selection_failure = .open_elsewhere; try std.testing.expectEqual(session_catalog.ResumeFailure.open_elsewhere, picker.selection_failure.?); try std.testing.expect(picker.moveVisibleItems(1, 2)); try std.testing.expectEqual(@as(usize, 1), picker.selected); try std.testing.expectEqual(@as(?session_catalog.ResumeFailure, null), picker.selection_failure); picker.selection_failure = .unavailable; picker.setQuery("first"); try std.testing.expectEqualStrings("first", picker.query()); try std.testing.expectEqual(@as(?session_catalog.ResumeFailure, null), picker.selection_failure); } test "session picker summary append releases ownership when cloning fails" { const alloc = std.testing.allocator; var summary = session_store.SessionSummary{ .id = try alloc.dupe(u8, "allocation-failure"), .created_at_ms = 1, .updated_at_ms = 1, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }; defer summary.deinit(alloc); var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0, }); var picker: SessionPicker = .{}; try std.testing.expectError( error.OutOfMemory, picker.appendSummary(failing.allocator(), &summary), ); try std.testing.expectEqual(@as(usize, 0), picker.summaries.items.len); picker.deinit(failing.allocator()); try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); } test "session picker loads resumable sessions ten at a time" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); app.shell.layout = .{ .rows = 17, .cols = 80, .content_bottom = 14, .divider_top_row = 15, .input_row = 16, .divider_bottom_row = 17, .hint_row = 17, }; try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("saved prompt") }, .assistant = @constCast("saved response"), } }}; inline for (0..21) |index| { const id = try std.fmt.allocPrint(alloc, "paged-session-{d:0>2}", .{index}); defer alloc.free(id); try writeSessionFixture( alloc, app.session_persistence.store.?, id, &history, 0, ); } try Runtime(TestApp).beginFreshPersistedSession(&app); const picker = &app.session_persistence.session_picker; try Runtime(TestApp).openSessionPicker(&app); try waitForSessionPickerLoad(&app); try std.testing.expectEqual(@as(usize, 10), picker.summaries.items.len); try std.testing.expect(picker.has_more); try std.testing.expectEqual(.ready, picker.load_state); picker.selected = picker.filteredItemCount(); try std.testing.expect(try Runtime(TestApp).loadMoreSessionPicker(&app)); try std.testing.expect(picker.loading_more); try waitForSessionPickerLoad(&app); try std.testing.expectEqual(@as(usize, 20), picker.summaries.items.len); try std.testing.expect(picker.has_more); try std.testing.expectEqual(@as(usize, 10), picker.selected); try std.testing.expectEqualStrings( picker.summaries.items[10].id, picker.selectedId().?, ); picker.selected = picker.filteredItemCount(); try std.testing.expect(try Runtime(TestApp).loadMoreSessionPicker(&app)); try waitForSessionPickerLoad(&app); try std.testing.expectEqual(@as(usize, 21), picker.summaries.items.len); try std.testing.expect(!picker.has_more); try std.testing.expect(!picker.loading_more); try std.testing.expectEqual(@as(usize, 20), picker.selected); const index_path = try std.fs.path.join( alloc, &.{ app.session_persistence.store.?.sessions_dir, "index.json" }, ); defer alloc.free(index_path); try std.Io.Dir.accessAbsolute(io_mod.getIo(), index_path, .{}); } test "session picker discards a held stale request after cancellation and reopen" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); const history = [_]types.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("saved prompt") }, .assistant = @constCast("saved response"), } }}; try writeSessionFixture( alloc, app.session_persistence.store.?, "reopened-session", &history, 0, ); try Runtime(TestApp).beginFreshPersistedSession(&app); var picker = &app.session_persistence.session_picker; picker.active = true; picker.generation = 41; var old_summary = session_store.SessionSummary{ .id = try alloc.dupe(u8, "old-continuation"), .created_at_ms = 1, .updated_at_ms = 1, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }; defer old_summary.deinit(alloc); try picker.appendSummary(alloc, &old_summary); const held = try createHeldSessionPickerTask( picker.generation, app.session_persistence.writable.?.active_id, app.session_persistence.store.?, ); held.failure = error.SessionStoreUnavailable; app.session_persistence.session_picker_load.task = held; Runtime(TestApp).cancelSessionPicker(&app); try std.testing.expect(!picker.active); try std.testing.expectEqual(@as(usize, 0), picker.summaries.items.len); try std.testing.expectEqualStrings(app.session_persistence.writable.?.active_id, held.request.active_id.?); try Runtime(TestApp).openSessionPicker(&app); const reopened_generation = picker.generation; try std.testing.expect(picker.active); try std.testing.expect(picker.isLoading()); try std.testing.expect(app.session_persistence.session_picker_load.pending != null); try std.testing.expectEqual( reopened_generation, app.session_persistence.session_picker_load.pending.?.generation, ); held.done.store(true, .release); try Runtime(TestApp).pollSessionPicker(&app); try std.testing.expectEqual(@as(usize, 0), app.notices.items.len); try std.testing.expect(picker.isLoading()); try waitForSessionPickerLoad(&app); try std.testing.expect(picker.active); try std.testing.expectEqual(@as(usize, 1), picker.summaries.items.len); try std.testing.expectEqualStrings("reopened-session", picker.summaries.items[0].id); try std.testing.expectEqual(@as(usize, 0), app.notices.items.len); } test "session picker keeps stale cached rows ready when refresh fails" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); var cached_page: session_store.ResumableSessionPage = .{}; defer cached_page.deinit(alloc); try cached_page.summaries.append(alloc, .{ .id = try alloc.dupe(u8, "cached-session"), .created_at_ms = 1, .updated_at_ms = 1, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }); const active_id = app.session_persistence.writable.?.active_id; const limit = resumePageLimitForRows(app.shell.layout.rows); try app.session_persistence.session_picker_current_cache.install( &cached_page, active_id, limit, io_mod.nanoTimestamp() - session_picker_cache_ttl_ns - 1, ); const generation = app.session_persistence.session_picker_load.allocateGeneration(); const failed = try createHeldSessionPickerTask( generation, active_id, app.session_persistence.store.?, ); failed.request.limit = limit; failed.failure = error.SessionStoreUnavailable; app.session_persistence.session_picker_load.task = failed; try Runtime(TestApp).openSessionPicker(&app); const picker = &app.session_persistence.session_picker; try std.testing.expectEqual(.ready, picker.load_state); try std.testing.expectEqualStrings("cached-session", picker.selectedId().?); failed.done.store(true, .release); try Runtime(TestApp).pollSessionPicker(&app); try std.testing.expect(picker.active); try std.testing.expectEqual(.ready, picker.load_state); try std.testing.expectEqual(@as(usize, 1), picker.summaries.items.len); try std.testing.expectEqualStrings("cached-session", picker.summaries.items[0].id); try std.testing.expectEqual(@as(usize, 1), app.notices.items.len); } test "session picker page requests own the active session ID" { const alloc = std.testing.allocator; const active_id = try alloc.dupe(u8, "active-session"); var request = try SessionPickerLoad.PageRequest.init( 1, .current_workspace, active_id, null, session_store.default_resume_page_limit, ); defer request.deinit(); alloc.free(active_id); try std.testing.expectEqualStrings("active-session", request.active_id.?); } fn createHeldSessionPickerTask( generation: u64, active_id: []const u8, store: session_store.Store, ) !*SessionPickerLoad.Task { const alloc = std.heap.c_allocator; var request = try SessionPickerLoad.PageRequest.init( generation, .current_workspace, active_id, null, session_store.default_resume_page_limit, ); errdefer request.deinit(); const home_dir = try alloc.dupe(u8, store.home_dir); errdefer alloc.free(home_dir); const workspace_root = try alloc.dupe(u8, store.workspace_root); errdefer alloc.free(workspace_root); const task = try alloc.create(SessionPickerLoad.Task); errdefer alloc.destroy(task); task.* = .{ .home_dir = home_dir, .workspace_root = workspace_root, .request = request, }; return task; } fn waitForSessionPickerLoad(app: *TestApp) !void { const deadline_ms = io_mod.milliTimestamp() + 5_000; while (io_mod.milliTimestamp() < deadline_ms) { try Runtime(TestApp).pollSessionPicker(app); const picker = &app.session_persistence.session_picker; if (!picker.isLoading() and !picker.loading_more) return; io_mod.sleep(std.time.ns_per_ms); } return error.TestExpectedEqual; } fn waitForSessionPickerPrewarm(app: *TestApp) !void { const deadline_ms = io_mod.milliTimestamp() + 5_000; while (io_mod.milliTimestamp() < deadline_ms) { try Runtime(TestApp).pollSessionPicker(app); const loader = &app.session_persistence.session_picker_load; if (loader.task == null and loader.pending == null) return; io_mod.sleep(std.time.ns_per_ms); } return error.TestExpectedEqual; } test "new session releases the old writer and replaces the subagent host" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); Runtime(TestApp).enableSessionStores(&app); const old_id = try alloc.dupe( u8, app.session_persistence.writable.?.active_id, ); defer alloc.free(old_id); try Runtime(TestApp).beginFreshPersistedSession(&app); Runtime(TestApp).enableSessionStores(&app); try std.testing.expect(!std.mem.eql( u8, old_id, app.session_persistence.writable.?.active_id, )); try std.testing.expect(app.session_persistence.subagent_host != null); } test "validateSessionTitle accepts printable text and rejects malformed input" { const V = Runtime(TestApp).validateSessionTitle; try std.testing.expectEqualStrings("hello world", try V(" hello world \n")); try std.testing.expectEqualStrings("héllo ✓", try V("héllo ✓")); try std.testing.expectError(error.EmptyTitle, V(" ")); try std.testing.expectError(error.InvalidTitle, V("bad\ttitle")); try std.testing.expectError(error.InvalidTitle, V("bad\x07title")); const long = "x" ** (session_display_metadata.max_title_bytes + 1); try std.testing.expectError(error.TitleTooLong, V(long)); } test "renameActiveSession requires an active session" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try std.testing.expectError( error.NoActiveSession, Runtime(TestApp).renameActiveSession(&app, "no session yet"), ); } test "renameActiveSession persists the title to the sidecar and session index" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } const home = try TestHome.install(alloc, paths.home); defer home.deinit(); var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); try configureTestPreferences(&app); try Runtime(TestApp).initializePersistence(&app, true); try Runtime(TestApp).beginFreshPersistedSession(&app); Runtime(TestApp).enableSessionStores(&app); try Runtime(TestApp).renameActiveSession(&app, " deploy pipeline fix "); // Cached for the footer without re-reading the sidecar. try std.testing.expectEqualStrings( "deploy pipeline fix", Runtime(TestApp).cachedSessionTitle(&app).?, ); // Durable in the sidecar. const loaded = &app.session_persistence.writable.?; var display = try session_display_metadata.readSidecarOrFallback(alloc, &loaded.log.dir); defer display.deinit(alloc); try std.testing.expect(display.present); try std.testing.expectEqualStrings("deploy pipeline fix", display.title); } test "ensureCachedSessionTitle derives from the first prompt and then freezes" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const paths = try testPaths(alloc, &tmp); defer { alloc.free(paths.home); alloc.free(paths.workspace); } var app = try TestApp.init(alloc, paths.workspace); defer app.deinit(); // No history yet: nothing to derive. try Runtime(TestApp).ensureCachedSessionTitle(&app); try std.testing.expect(Runtime(TestApp).cachedSessionTitle(&app) == null); try app.session.appendHistoryEntry(alloc, .{ .assistant = .{ .user = .{ .text = @constCast("add a session name display to the bottom status row in fx") }, .assistant = @constCast("ok"), .execution = .{}, } }); try Runtime(TestApp).ensureCachedSessionTitle(&app); try std.testing.expectEqualStrings( "add a session name display to the bottom", Runtime(TestApp).cachedSessionTitle(&app).?, ); // A later turn must not move the frozen title. try app.session.appendHistoryEntry(alloc, .{ .assistant = .{ .user = .{ .text = @constCast("a totally different second prompt") }, .assistant = @constCast("ok"), .execution = .{}, } }); try Runtime(TestApp).ensureCachedSessionTitle(&app); try std.testing.expectEqualStrings( "add a session name display to the bottom", Runtime(TestApp).cachedSessionTitle(&app).?, ); // A fresh session clears it. Runtime(TestApp).clearCachedSessionTitle(&app); try std.testing.expect(Runtime(TestApp).cachedSessionTitle(&app) == null); }