const std = @import("std");const builtin = @import("builtin");const config_runtime = @import("../config/config_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const image_attachments = @import("../images/image_attachments.zig");const io_mod = @import("../shared/io.zig");const profile_paths = @import("../shared/profile_paths.zig");const core_types = @import("../shared/types.zig");const artifact_digest = @import("artifact_digest.zig");const command_replay_store = @import("command_replay_store.zig");const result_store = @import("result_store.zig");const session = @import("session.zig");const session_codec = @import("session_codec.zig");const session_child_store = @import("session_child_store.zig");const relationship_index_codec = @import("session_relationship_index_codec.zig");const session_event = @import("session_event.zig");const session_json = @import("session_json.zig");const session_layout = @import("session_layout.zig");const session_log = @import("session_log.zig");const session_projection = @import("session_projection.zig");const session_display_metadata = @import("session_display_metadata.zig");const session_usage = @import("session_usage.zig");const Allocator = std.mem.Allocator; const authority_module = @import("session_authority.zig");const discovery = @import("session_discovery.zig");const latest_pointer = @import("session_latest_pointer.zig");const migration = @import("session_migration.zig");const paths = @import("session_store_paths.zig");const store_types = @import("session_store_types.zig");const summary_codec = @import("session_summary_codec.zig");const sort_utils = @import("../shared/sort_utils.zig"); const authorityTransitionsEqual = authority_module.authorityTransitionsEqual;const classifyAuthority = authority_module.classifyAuthority;const classifyAuthorityAllowingLargeLegacy = authority_module.classifyAuthorityAllowingLargeLegacy;const deleteSessionEntry = authority_module.deleteSessionEntry;const loadAuthorityTransitionOptional = authority_module.loadAuthorityTransitionOptional;const mapReplayError = authority_module.mapReplayError;const openSessionFile = authority_module.openSessionFile;const readExactLegacyFile = authority_module.readExactLegacyFile;const requireAuthorityFenceAbsent = authority_module.requireAuthorityFenceAbsent;const requireAuthorityTransitionSession = authority_module.requireAuthorityTransitionSession;const restoreLegacyAuthority = authority_module.restoreLegacyAuthority;const DiscoveryCandidateMetadata = discovery.DiscoveryCandidateMetadata;const DiscoveryMode = discovery.DiscoveryMode;const ReadOnlyCandidate = discovery.ReadOnlyCandidate;const WritableCandidate = discovery.WritableCandidate;const appendDoctorDiagnostic = discovery.appendDoctorDiagnostic;const classifyLegacyCandidate = discovery.classifyLegacyCandidate;const classifyReadOnlyCandidate = discovery.classifyReadOnlyCandidate;const classifySchemaV3Candidate = discovery.classifySchemaV3Candidate;const dupeWritableCandidate = discovery.dupeWritableCandidate;const freeDoctorDiagnostics = discovery.freeDoctorDiagnostics;const inspectDoctorSession = discovery.inspectDoctorSession;const logDiscovery = discovery.logDiscovery;const logDiscoveryError = discovery.logDiscoveryError;const storageFormatForLegacy = discovery.storageFormatForLegacy;const summaryFromState = discovery.summaryFromState;const writableCandidateNewer = discovery.writableCandidateNewer;const InitialIndexEffect = latest_pointer.InitialIndexEffect;const LatestCache = latest_pointer.LatestCache;const LatestPointer = latest_pointer.LatestPointer;const latestCacheAbort = latest_pointer.latestCacheAbort;const latestCacheDeinit = latest_pointer.latestCacheDeinit;const latestCachePrepare = latest_pointer.latestCachePrepare;const latestCachePublish = latest_pointer.latestCachePublish;const latest_sessions_lock_file = latest_pointer.latest_sessions_lock_file;const latest_sessions_dir = latest_pointer.latest_sessions_dir;const recovery_staging_dir = "recovery+staging";const recovery_staging_lock_file = "recovery-staging.lock";const usage_recovery_dir = profile_paths.usage_recovery_dir_name;const usage_recovery_marker_prefix = "v1 ";const max_usage_recovery_marker_bytes = usage_recovery_marker_prefix.len + 20 + 1;const max_usage_recovery_sessions: usize = 512;const synchronous_display_metadata_replay_max_bytes: u64 = 1024 * 1024;const readLatestPointerFromSessions = latest_pointer.readLatestPointerFromSessions;const readPendingLatestSessionIdFromSessions = latest_pointer.readPendingLatestSessionIdFromSessions;const LegacyStoredSession = migration.LegacyStoredSession;const MigrationPreferenceSource = migration.MigrationPreferenceSource;const legacyToDurableState = migration.legacyToDurableState;const loadedMigrationTarget = migration.loadedMigrationTarget;const migrateLegacyLocked = migration.migrateLegacyLocked;const migratedSourceBytes = migration.migratedSourceBytes;const migratedSourceSchemaVersion = migration.migratedSourceSchemaVersion;const validateMigrationTarget = migration.validateMigrationTarget;const normalizeWorkspaceRoot = paths.normalizeWorkspaceRoot;pub const sessionDirPath = paths.sessionDirPath;const sessionJsonPath = paths.sessionJsonPath;const session_list_json_file = paths.session_list_json_file;const session_summary_file = paths.session_summary_file;const summaryPath = paths.summaryPath;pub const validateSessionId = paths.validateSessionId;const validateWorkspaceRoot = paths.validateWorkspaceRoot;pub const generateSessionId = paths.generateSessionId;pub const CandidateStorage = store_types.CandidateStorage;pub const DiscoveryCause = store_types.DiscoveryCause;pub const DoctorDiagnostic = store_types.DoctorDiagnostic;pub const DoctorInspectionResult = store_types.DoctorInspectionResult;const DoctorInspectionOptions = store_types.DoctorInspectionOptions;pub const DoctorIssueKind = store_types.DoctorIssueKind;pub const LoadedWritableSession = store_types.LoadedWritableSession;pub const MigrationOptions = store_types.MigrationOptions;pub const ProjectionState = store_types.ProjectionState; pub const UsageRecoverySession = struct { id: []u8, protected_updated_at_ms: ?i64, pub fn deinit(self: *UsageRecoverySession, alloc: Allocator) void { alloc.free(self.id); self.* = undefined; }}; pub const UsageRecoveryCheckpoint = struct { recovery_pending: bool, timestamp_ms: i64,}; pub fn imageSnapshotStorageDir( alloc: Allocator, sessions_dir: ?[]const u8, session_id: ?[]const u8, temp_dir: *?[]u8,) ![]u8 { if ((sessions_dir == null) != (session_id == null)) return error.InvalidSessionSnapshotOwner; if (sessions_dir) |root| { const durable_dir = try sessionDirPath(alloc, root, session_id.?); defer alloc.free(durable_dir); return std.fs.path.join(alloc, &.{ durable_dir, "images" }); } if (temp_dir.* == null) { temp_dir.* = try image_attachments.createTempSnapshotDir(alloc); } return alloc.dupe(u8, temp_dir.*.?);}pub const ReadOnlyDetail = store_types.ReadOnlyDetail;pub const ResumeOptions = store_types.ResumeOptions;pub const ResumeTarget = store_types.ResumeTarget;pub const ResumeViewAdmission = session_log.ResumeViewAdmission;pub const SessionMigrationResult = store_types.SessionMigrationResult;pub const SessionMigrationStatus = store_types.SessionMigrationStatus;pub const SessionRecoveryResult = store_types.SessionRecoveryResult;pub const SessionRecoveryStatus = store_types.SessionRecoveryStatus;pub const SessionSummary = store_types.SessionSummary;pub const HistoryPage = store_types.HistoryPage; pub const LoadHistoryPageError = error{ OutOfMemory, InvalidSessionId, InvalidHistoryPageLimit, InvalidHistoryPageCursor, StaleHistoryPageCursor, SessionNotFound, SessionPathUnsafe, SessionStoreUnavailable, UnsupportedSessionFormat, CorruptSession,}; const HistoryPageCursor = struct { session_id: []const u8, history_len: usize, revision_ms: i64, prefix_digest: [32]u8, start: usize,}; fn parseHistoryPageCursor(raw: []const u8) LoadHistoryPageError!HistoryPageCursor { if (raw.len == 0 or raw.len > 512) return error.InvalidHistoryPageCursor; var fields = std.mem.splitScalar(u8, raw, ':'); if (!std.mem.eql(u8, fields.next() orelse return error.InvalidHistoryPageCursor, "v2")) return error.InvalidHistoryPageCursor; const session_id = fields.next() orelse return error.InvalidHistoryPageCursor; const history_len = std.fmt.parseInt(usize, fields.next() orelse return error.InvalidHistoryPageCursor, 10) catch return error.InvalidHistoryPageCursor; const revision_ms = std.fmt.parseInt(i64, fields.next() orelse return error.InvalidHistoryPageCursor, 10) catch return error.InvalidHistoryPageCursor; const digest_hex = fields.next() orelse return error.InvalidHistoryPageCursor; const start = std.fmt.parseInt(usize, fields.next() orelse return error.InvalidHistoryPageCursor, 10) catch return error.InvalidHistoryPageCursor; if (fields.next() != null) return error.InvalidHistoryPageCursor; if (digest_hex.len != 64 or revision_ms < 0 or start > history_len) return error.InvalidHistoryPageCursor; validateSessionId(session_id) catch return error.InvalidHistoryPageCursor; var prefix_digest: [32]u8 = undefined; _ = std.fmt.hexToBytes(&prefix_digest, digest_hex) catch return error.InvalidHistoryPageCursor; const cursor: HistoryPageCursor = .{ .session_id = session_id, .history_len = history_len, .revision_ms = revision_ms, .prefix_digest = prefix_digest, .start = start }; var canonical: [512]u8 = undefined; const encoded = formatHistoryPageCursor(&canonical, cursor) catch return error.InvalidHistoryPageCursor; if (!std.mem.eql(u8, raw, encoded)) return error.InvalidHistoryPageCursor; return cursor;} fn formatHistoryPageCursor(buffer: []u8, cursor: HistoryPageCursor) ![]u8 { return std.fmt.bufPrint(buffer, "v2:{s}:{d}:{d}:{x}:{d}", .{ cursor.session_id, cursor.history_len, cursor.revision_ms, cursor.prefix_digest, cursor.start });} fn duplicateHistoryPage(alloc: Allocator, turns: []const session.HistoryTurn) ![]session.HistoryTurn { const copy = try alloc.alloc(session.HistoryTurn, turns.len); var copied: usize = 0; errdefer { for (copy[0..copied]) |turn| session.freeHistoryTurn(alloc, turn); alloc.free(copy); } for (turns) |turn| { copy[copied] = try session.dupeHistoryTurn(alloc, turn); copied += 1; } return copy;} fn historyPrefixDigest(turns: []const session.HistoryTurn) error{ WriteFailed, NoSpaceLeft }![32]u8 { var buffer: [256]u8 = undefined; var hashing: std.Io.Writer.Hashing(std.crypto.hash.sha2.Sha256) = .init(&buffer); try hashing.writer.writeAll("fx.history-page-prefix.v2\x00"); for (turns) |turn| { try session_codec.writeHistoryTurn(&hashing.writer, turn); // Canonical JSON never contains a literal NUL, so this makes the // concatenation unambiguous without introducing another serializer. try hashing.writer.writeByte(0); } try hashing.writer.flush(); return hashing.hasher.finalResult();} fn mapHistoryPageLoadError(err: anyerror) LoadHistoryPageError { return switch (err) { error.OutOfMemory => error.OutOfMemory, error.InvalidSessionId => error.InvalidSessionId, error.SessionNotFound => error.SessionNotFound, error.SessionPathUnsafe => error.SessionPathUnsafe, error.UnsupportedSessionFormat, error.UnsupportedSessionSchema => error.UnsupportedSessionFormat, error.InvalidSessionFormat, error.InvalidDurableField, error.InvalidDurableBytes, error.InvalidSessionIndex, => error.CorruptSession, // `loadReadOnly` intentionally has an inferred upstream error set. // Its uncategorized I/O and replay failures are unavailable storage, // never evidence that the committed session itself is corrupt. else => error.SessionStoreUnavailable, };} const HistoryPageWindow = struct { start: usize, end: usize,}; fn selectHistoryPageWindow(history_len: usize, exclusive_end: usize, limit: usize) HistoryPageWindow { std.debug.assert(exclusive_end <= history_len); return .{ .start = exclusive_end -| limit, .end = exclusive_end };}pub const OpenSubagentControlError = error{ OutOfMemory, InvalidSessionId, SessionNotFound, SessionPathUnsafe, SessionStoreUnavailable, PrivateStatePermissionsUnsupported, SessionChildStoreFailed,};pub const LoadSubagentBootstrapError = error{ OutOfMemory, InvalidSessionId, SessionNotFound, SessionPathUnsafe, SessionMetadataUnavailable,};pub const ListSubagentControlIdsError = error{ OutOfMemory, SessionStoreUnavailable,};pub const SubagentBootstrapMetadata = struct { name: []u8, preferences: session_codec.DurableSessionPreferences, pub fn deinit(self: *SubagentBootstrapMetadata, alloc: Allocator) void { alloc.free(self.name); self.preferences.deinit(alloc); self.* = undefined; }};pub const ResumableSessionContinuation = store_types.ResumableSessionContinuation;pub const ResumableSessionPage = store_types.ResumableSessionPage;pub const SessionListScope = store_types.SessionListScope;pub const SessionListPage = store_types.SessionListPage;pub const session_list_default_limit: usize = 100;pub const session_list_max_limit: usize = 100;pub const ListWorkspacePageError = error{ OutOfMemory, InvalidSessionListLimit, SessionStoreUnavailable,};pub const RelationshipMigrationCursor = summary_codec.RelationshipMigrationCursor;pub const RelationshipMigrationCandidatePage = summary_codec.RelationshipMigrationCandidatePage;pub const relationship_migration_candidate_limit = summary_codec.relationship_migration_candidate_limit;const ResumableSessionScope = enum { all_workspaces, current_workspace,};pub const StateSummary = store_types.StateSummary;pub const StorageFormat = store_types.StorageFormat;const automatic_legacy_max_bytes = store_types.automatic_legacy_max_bytes;const max_session_bytes = store_types.max_session_bytes;const StoreContext = store_types.StoreContext;const freeSummaries = summary_codec.freeSummaries;const readSessionStateSummary = summary_codec.readSessionStateSummary;const readSessionIndex = summary_codec.readSessionIndex;const readSessionIndexWorkspaceCandidates = summary_codec.readSessionIndexWorkspaceCandidates;const removeSessionIndexMarker = summary_codec.removeSessionIndexMarker;const resumablePageFromSummaries = summary_codec.resumablePageFromSummaries;const sessionListPageFromSummaries = summary_codec.sessionListPageFromSummaries;const sortSummariesNewestFirst = summary_codec.sortSummariesNewestFirst;const writeSessionIndex = summary_codec.writeSessionIndex; const SessionSummaryScan = struct { summaries: std.ArrayList(SessionSummary) = .empty, skipped_invalid: usize = 0, fn deinit(self: *SessionSummaryScan, alloc: Allocator) void { freeSummaries(alloc, &self.summaries); self.* = undefined; }}; test { _ = session_child_store; _ = session_layout; _ = session_log; _ = session_display_metadata; _ = store_types; _ = paths; _ = authority_module; _ = summary_codec; _ = latest_pointer; _ = migration; _ = discovery;} fn retainWorkspaceSummaries(alloc: Allocator, summaries: *std.ArrayList(SessionSummary), workspace_root: []const u8) void { var write_index: usize = 0; for (summaries.items, 0..) |*summary, read_index| { const summary_workspace = summary.workspace_root orelse { summary.deinit(alloc); continue; }; if (!std.mem.eql(u8, summary_workspace, workspace_root)) { summary.deinit(alloc); continue; } if (write_index != read_index) summaries.items[write_index] = summary.*; write_index += 1; } summaries.items.len = write_index;} fn summariesMissingDisplayMetadata(summaries: []const SessionSummary) bool { for (summaries) |summary| { if (summaryNeedsDisplayMetadata(summary)) return true; } return false;} fn summaryNeedsDisplayMetadata(summary: SessionSummary) bool { if (summary.history_len == 0) return false; if (!summary.display_metadata_present) return true; return summary.title == null;} fn initialIndexEffect(state: session_codec.DurableSessionState) InitialIndexEffect { return if (state.history.len == 0) .preserve else .maintain;} pub const default_resume_page_limit: usize = 10; fn openUsageRecoveryProfileRoot( home_path: []const u8,) !?io_mod.VerifiedDir { const zio = io_mod.getIo(); var home = try std.Io.Dir.openDirAbsolute(zio, home_path, .{ .iterate = true, }); defer home.close(zio); var profile = home.openDir(zio, profile_paths.root_dir_name, .{ .iterate = true, .follow_symlinks = false, }) catch |err| switch (err) { error.FileNotFound => return null, error.NotDir, error.SymLinkLoop => return error.InvalidUsageRecoveryIndex, else => return err, }; errdefer profile.close(zio); const stat = try profile.stat(zio); if (stat.kind != .directory or stat.permissions.toMode() & 0o777 != 0o700) { return error.InvalidUsageRecoveryIndex; } return .{ .dir = profile };} fn openUsageRecoveryDir( home_path: []const u8,) !?io_mod.VerifiedDir { var profile = try openUsageRecoveryProfileRoot(home_path) orelse return null; defer profile.close(); var dir = profile.dir.openDir(io_mod.getIo(), usage_recovery_dir, .{ .iterate = true, .follow_symlinks = false, }) catch |err| switch (err) { error.FileNotFound => return null, error.NotDir, error.SymLinkLoop => return error.InvalidUsageRecoveryIndex, else => return err, }; errdefer dir.close(io_mod.getIo()); const stat = try dir.stat(io_mod.getIo()); if (stat.kind != .directory or stat.permissions.toMode() & 0o777 != 0o700) { return error.InvalidUsageRecoveryIndex; } return .{ .dir = dir };} fn validateUsageRecoveryMarker( recovery: *const io_mod.VerifiedDir, session_id: []const u8,) !?i64 { var marker = recovery.dir.openFile(io_mod.getIo(), session_id, .{ .mode = .read_only, .allow_directory = false, .follow_symlinks = false, .resolve_beneath = true, }) catch |err| switch (err) { error.FileNotFound => return error.UsageRecoveryMarkerNotFound, else => return error.InvalidUsageRecoveryIndex, }; defer marker.close(io_mod.getIo()); const stat = try marker.stat(io_mod.getIo()); if (stat.kind != .file or stat.nlink != 1 or stat.size == 0 or stat.size > max_usage_recovery_marker_bytes or stat.permissions.toMode() & 0o777 != 0o600) { return error.InvalidUsageRecoveryIndex; } var bytes: [max_usage_recovery_marker_bytes]u8 = undefined; const marker_len: usize = @intCast(stat.size); const read = marker.readPositionalAll( io_mod.getIo(), bytes[0..marker_len], 0, ) catch return error.InvalidUsageRecoveryIndex; if (read != marker_len) { return error.InvalidUsageRecoveryIndex; } const marker_bytes = bytes[0..marker_len]; if (!std.mem.startsWith( u8, marker_bytes, usage_recovery_marker_prefix, ) or !std.mem.endsWith(u8, marker_bytes, "\n")) { return error.InvalidUsageRecoveryIndex; } const timestamp_bytes = marker_bytes[usage_recovery_marker_prefix.len .. marker_bytes.len - 1]; if (timestamp_bytes.len == 0) return error.InvalidUsageRecoveryIndex; const timestamp_ms = std.fmt.parseInt( i64, timestamp_bytes, 10, ) catch return error.InvalidUsageRecoveryIndex; if (timestamp_ms < 0) return error.InvalidUsageRecoveryIndex; return timestamp_ms;} pub const Store = struct { sessions_dir: []u8, home_dir: []u8, workspace_root: []u8, canonical_root: session_log.Root, // How many sessions one resume page yields before flagging `has_more`. // Carried on the value so it propagates through the by-value page chain; // the UI sets it from the visible screen height. resume_page_limit: usize = default_resume_page_limit, /// Narrow, copyable view of this store for the discovery/migration helpers, /// so they depend on `StoreContext` instead of the full facade. fn ctx(self: Store) StoreContext { return .{ .sessions_dir = self.sessions_dir, .home_dir = self.home_dir, .workspace_root = self.workspace_root, .canonical_root = self.canonical_root, }; } /// Opens a writable store rooted at `$HOME`, creating the layout if needed. pub fn init(alloc: Allocator, workspace_root: []const u8) !Store {Lines 1–500 of 12396. Load more
const std = @import("std"); const builtin = @import("builtin"); const config_runtime = @import("../config/config_runtime.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const image_attachments = @import("../images/image_attachments.zig"); const io_mod = @import("../shared/io.zig"); const profile_paths = @import("../shared/profile_paths.zig"); const core_types = @import("../shared/types.zig"); const artifact_digest = @import("artifact_digest.zig"); const command_replay_store = @import("command_replay_store.zig"); const result_store = @import("result_store.zig"); const session = @import("session.zig"); const session_codec = @import("session_codec.zig"); const session_child_store = @import("session_child_store.zig"); const relationship_index_codec = @import("session_relationship_index_codec.zig"); const session_event = @import("session_event.zig"); const session_json = @import("session_json.zig"); const session_layout = @import("session_layout.zig"); const session_log = @import("session_log.zig"); const session_projection = @import("session_projection.zig"); const session_display_metadata = @import("session_display_metadata.zig"); const session_usage = @import("session_usage.zig"); const Allocator = std.mem.Allocator; const authority_module = @import("session_authority.zig"); const discovery = @import("session_discovery.zig"); const latest_pointer = @import("session_latest_pointer.zig"); const migration = @import("session_migration.zig"); const paths = @import("session_store_paths.zig"); const store_types = @import("session_store_types.zig"); const summary_codec = @import("session_summary_codec.zig"); const sort_utils = @import("../shared/sort_utils.zig"); const authorityTransitionsEqual = authority_module.authorityTransitionsEqual; const classifyAuthority = authority_module.classifyAuthority; const classifyAuthorityAllowingLargeLegacy = authority_module.classifyAuthorityAllowingLargeLegacy; const deleteSessionEntry = authority_module.deleteSessionEntry; const loadAuthorityTransitionOptional = authority_module.loadAuthorityTransitionOptional; const mapReplayError = authority_module.mapReplayError; const openSessionFile = authority_module.openSessionFile; const readExactLegacyFile = authority_module.readExactLegacyFile; const requireAuthorityFenceAbsent = authority_module.requireAuthorityFenceAbsent; const requireAuthorityTransitionSession = authority_module.requireAuthorityTransitionSession; const restoreLegacyAuthority = authority_module.restoreLegacyAuthority; const DiscoveryCandidateMetadata = discovery.DiscoveryCandidateMetadata; const DiscoveryMode = discovery.DiscoveryMode; const ReadOnlyCandidate = discovery.ReadOnlyCandidate; const WritableCandidate = discovery.WritableCandidate; const appendDoctorDiagnostic = discovery.appendDoctorDiagnostic; const classifyLegacyCandidate = discovery.classifyLegacyCandidate; const classifyReadOnlyCandidate = discovery.classifyReadOnlyCandidate; const classifySchemaV3Candidate = discovery.classifySchemaV3Candidate; const dupeWritableCandidate = discovery.dupeWritableCandidate; const freeDoctorDiagnostics = discovery.freeDoctorDiagnostics; const inspectDoctorSession = discovery.inspectDoctorSession; const logDiscovery = discovery.logDiscovery; const logDiscoveryError = discovery.logDiscoveryError; const storageFormatForLegacy = discovery.storageFormatForLegacy; const summaryFromState = discovery.summaryFromState; const writableCandidateNewer = discovery.writableCandidateNewer; const InitialIndexEffect = latest_pointer.InitialIndexEffect; const LatestCache = latest_pointer.LatestCache; const LatestPointer = latest_pointer.LatestPointer; const latestCacheAbort = latest_pointer.latestCacheAbort; const latestCacheDeinit = latest_pointer.latestCacheDeinit; const latestCachePrepare = latest_pointer.latestCachePrepare; const latestCachePublish = latest_pointer.latestCachePublish; const latest_sessions_lock_file = latest_pointer.latest_sessions_lock_file; const latest_sessions_dir = latest_pointer.latest_sessions_dir; const recovery_staging_dir = "recovery+staging"; const recovery_staging_lock_file = "recovery-staging.lock"; const usage_recovery_dir = profile_paths.usage_recovery_dir_name; const usage_recovery_marker_prefix = "v1 "; const max_usage_recovery_marker_bytes = usage_recovery_marker_prefix.len + 20 + 1; const max_usage_recovery_sessions: usize = 512; const synchronous_display_metadata_replay_max_bytes: u64 = 1024 * 1024; const readLatestPointerFromSessions = latest_pointer.readLatestPointerFromSessions; const readPendingLatestSessionIdFromSessions = latest_pointer.readPendingLatestSessionIdFromSessions; const LegacyStoredSession = migration.LegacyStoredSession; const MigrationPreferenceSource = migration.MigrationPreferenceSource; const legacyToDurableState = migration.legacyToDurableState; const loadedMigrationTarget = migration.loadedMigrationTarget; const migrateLegacyLocked = migration.migrateLegacyLocked; const migratedSourceBytes = migration.migratedSourceBytes; const migratedSourceSchemaVersion = migration.migratedSourceSchemaVersion; const validateMigrationTarget = migration.validateMigrationTarget; const normalizeWorkspaceRoot = paths.normalizeWorkspaceRoot; pub const sessionDirPath = paths.sessionDirPath; const sessionJsonPath = paths.sessionJsonPath; const session_list_json_file = paths.session_list_json_file; const session_summary_file = paths.session_summary_file; const summaryPath = paths.summaryPath; pub const validateSessionId = paths.validateSessionId; const validateWorkspaceRoot = paths.validateWorkspaceRoot; pub const generateSessionId = paths.generateSessionId; pub const CandidateStorage = store_types.CandidateStorage; pub const DiscoveryCause = store_types.DiscoveryCause; pub const DoctorDiagnostic = store_types.DoctorDiagnostic; pub const DoctorInspectionResult = store_types.DoctorInspectionResult; const DoctorInspectionOptions = store_types.DoctorInspectionOptions; pub const DoctorIssueKind = store_types.DoctorIssueKind; pub const LoadedWritableSession = store_types.LoadedWritableSession; pub const MigrationOptions = store_types.MigrationOptions; pub const ProjectionState = store_types.ProjectionState; pub const UsageRecoverySession = struct { id: []u8, protected_updated_at_ms: ?i64, pub fn deinit(self: *UsageRecoverySession, alloc: Allocator) void { alloc.free(self.id); self.* = undefined; } }; pub const UsageRecoveryCheckpoint = struct { recovery_pending: bool, timestamp_ms: i64, }; pub fn imageSnapshotStorageDir( alloc: Allocator, sessions_dir: ?[]const u8, session_id: ?[]const u8, temp_dir: *?[]u8, ) ![]u8 { if ((sessions_dir == null) != (session_id == null)) return error.InvalidSessionSnapshotOwner; if (sessions_dir) |root| { const durable_dir = try sessionDirPath(alloc, root, session_id.?); defer alloc.free(durable_dir); return std.fs.path.join(alloc, &.{ durable_dir, "images" }); } if (temp_dir.* == null) { temp_dir.* = try image_attachments.createTempSnapshotDir(alloc); } return alloc.dupe(u8, temp_dir.*.?); } pub const ReadOnlyDetail = store_types.ReadOnlyDetail; pub const ResumeOptions = store_types.ResumeOptions; pub const ResumeTarget = store_types.ResumeTarget; pub const ResumeViewAdmission = session_log.ResumeViewAdmission; pub const SessionMigrationResult = store_types.SessionMigrationResult; pub const SessionMigrationStatus = store_types.SessionMigrationStatus; pub const SessionRecoveryResult = store_types.SessionRecoveryResult; pub const SessionRecoveryStatus = store_types.SessionRecoveryStatus; pub const SessionSummary = store_types.SessionSummary; pub const HistoryPage = store_types.HistoryPage; pub const LoadHistoryPageError = error{ OutOfMemory, InvalidSessionId, InvalidHistoryPageLimit, InvalidHistoryPageCursor, StaleHistoryPageCursor, SessionNotFound, SessionPathUnsafe, SessionStoreUnavailable, UnsupportedSessionFormat, CorruptSession, }; const HistoryPageCursor = struct { session_id: []const u8, history_len: usize, revision_ms: i64, prefix_digest: [32]u8, start: usize, }; fn parseHistoryPageCursor(raw: []const u8) LoadHistoryPageError!HistoryPageCursor { if (raw.len == 0 or raw.len > 512) return error.InvalidHistoryPageCursor; var fields = std.mem.splitScalar(u8, raw, ':'); if (!std.mem.eql(u8, fields.next() orelse return error.InvalidHistoryPageCursor, "v2")) return error.InvalidHistoryPageCursor; const session_id = fields.next() orelse return error.InvalidHistoryPageCursor; const history_len = std.fmt.parseInt(usize, fields.next() orelse return error.InvalidHistoryPageCursor, 10) catch return error.InvalidHistoryPageCursor; const revision_ms = std.fmt.parseInt(i64, fields.next() orelse return error.InvalidHistoryPageCursor, 10) catch return error.InvalidHistoryPageCursor; const digest_hex = fields.next() orelse return error.InvalidHistoryPageCursor; const start = std.fmt.parseInt(usize, fields.next() orelse return error.InvalidHistoryPageCursor, 10) catch return error.InvalidHistoryPageCursor; if (fields.next() != null) return error.InvalidHistoryPageCursor; if (digest_hex.len != 64 or revision_ms < 0 or start > history_len) return error.InvalidHistoryPageCursor; validateSessionId(session_id) catch return error.InvalidHistoryPageCursor; var prefix_digest: [32]u8 = undefined; _ = std.fmt.hexToBytes(&prefix_digest, digest_hex) catch return error.InvalidHistoryPageCursor; const cursor: HistoryPageCursor = .{ .session_id = session_id, .history_len = history_len, .revision_ms = revision_ms, .prefix_digest = prefix_digest, .start = start }; var canonical: [512]u8 = undefined; const encoded = formatHistoryPageCursor(&canonical, cursor) catch return error.InvalidHistoryPageCursor; if (!std.mem.eql(u8, raw, encoded)) return error.InvalidHistoryPageCursor; return cursor; } fn formatHistoryPageCursor(buffer: []u8, cursor: HistoryPageCursor) ![]u8 { return std.fmt.bufPrint(buffer, "v2:{s}:{d}:{d}:{x}:{d}", .{ cursor.session_id, cursor.history_len, cursor.revision_ms, cursor.prefix_digest, cursor.start }); } fn duplicateHistoryPage(alloc: Allocator, turns: []const session.HistoryTurn) ![]session.HistoryTurn { const copy = try alloc.alloc(session.HistoryTurn, turns.len); var copied: usize = 0; errdefer { for (copy[0..copied]) |turn| session.freeHistoryTurn(alloc, turn); alloc.free(copy); } for (turns) |turn| { copy[copied] = try session.dupeHistoryTurn(alloc, turn); copied += 1; } return copy; } fn historyPrefixDigest(turns: []const session.HistoryTurn) error{ WriteFailed, NoSpaceLeft }![32]u8 { var buffer: [256]u8 = undefined; var hashing: std.Io.Writer.Hashing(std.crypto.hash.sha2.Sha256) = .init(&buffer); try hashing.writer.writeAll("fx.history-page-prefix.v2\x00"); for (turns) |turn| { try session_codec.writeHistoryTurn(&hashing.writer, turn); // Canonical JSON never contains a literal NUL, so this makes the // concatenation unambiguous without introducing another serializer. try hashing.writer.writeByte(0); } try hashing.writer.flush(); return hashing.hasher.finalResult(); } fn mapHistoryPageLoadError(err: anyerror) LoadHistoryPageError { return switch (err) { error.OutOfMemory => error.OutOfMemory, error.InvalidSessionId => error.InvalidSessionId, error.SessionNotFound => error.SessionNotFound, error.SessionPathUnsafe => error.SessionPathUnsafe, error.UnsupportedSessionFormat, error.UnsupportedSessionSchema => error.UnsupportedSessionFormat, error.InvalidSessionFormat, error.InvalidDurableField, error.InvalidDurableBytes, error.InvalidSessionIndex, => error.CorruptSession, // `loadReadOnly` intentionally has an inferred upstream error set. // Its uncategorized I/O and replay failures are unavailable storage, // never evidence that the committed session itself is corrupt. else => error.SessionStoreUnavailable, }; } const HistoryPageWindow = struct { start: usize, end: usize, }; fn selectHistoryPageWindow(history_len: usize, exclusive_end: usize, limit: usize) HistoryPageWindow { std.debug.assert(exclusive_end <= history_len); return .{ .start = exclusive_end -| limit, .end = exclusive_end }; } pub const OpenSubagentControlError = error{ OutOfMemory, InvalidSessionId, SessionNotFound, SessionPathUnsafe, SessionStoreUnavailable, PrivateStatePermissionsUnsupported, SessionChildStoreFailed, }; pub const LoadSubagentBootstrapError = error{ OutOfMemory, InvalidSessionId, SessionNotFound, SessionPathUnsafe, SessionMetadataUnavailable, }; pub const ListSubagentControlIdsError = error{ OutOfMemory, SessionStoreUnavailable, }; pub const SubagentBootstrapMetadata = struct { name: []u8, preferences: session_codec.DurableSessionPreferences, pub fn deinit(self: *SubagentBootstrapMetadata, alloc: Allocator) void { alloc.free(self.name); self.preferences.deinit(alloc); self.* = undefined; } }; pub const ResumableSessionContinuation = store_types.ResumableSessionContinuation; pub const ResumableSessionPage = store_types.ResumableSessionPage; pub const SessionListScope = store_types.SessionListScope; pub const SessionListPage = store_types.SessionListPage; pub const session_list_default_limit: usize = 100; pub const session_list_max_limit: usize = 100; pub const ListWorkspacePageError = error{ OutOfMemory, InvalidSessionListLimit, SessionStoreUnavailable, }; pub const RelationshipMigrationCursor = summary_codec.RelationshipMigrationCursor; pub const RelationshipMigrationCandidatePage = summary_codec.RelationshipMigrationCandidatePage; pub const relationship_migration_candidate_limit = summary_codec.relationship_migration_candidate_limit; const ResumableSessionScope = enum { all_workspaces, current_workspace, }; pub const StateSummary = store_types.StateSummary; pub const StorageFormat = store_types.StorageFormat; const automatic_legacy_max_bytes = store_types.automatic_legacy_max_bytes; const max_session_bytes = store_types.max_session_bytes; const StoreContext = store_types.StoreContext; const freeSummaries = summary_codec.freeSummaries; const readSessionStateSummary = summary_codec.readSessionStateSummary; const readSessionIndex = summary_codec.readSessionIndex; const readSessionIndexWorkspaceCandidates = summary_codec.readSessionIndexWorkspaceCandidates; const removeSessionIndexMarker = summary_codec.removeSessionIndexMarker; const resumablePageFromSummaries = summary_codec.resumablePageFromSummaries; const sessionListPageFromSummaries = summary_codec.sessionListPageFromSummaries; const sortSummariesNewestFirst = summary_codec.sortSummariesNewestFirst; const writeSessionIndex = summary_codec.writeSessionIndex; const SessionSummaryScan = struct { summaries: std.ArrayList(SessionSummary) = .empty, skipped_invalid: usize = 0, fn deinit(self: *SessionSummaryScan, alloc: Allocator) void { freeSummaries(alloc, &self.summaries); self.* = undefined; } }; test { _ = session_child_store; _ = session_layout; _ = session_log; _ = session_display_metadata; _ = store_types; _ = paths; _ = authority_module; _ = summary_codec; _ = latest_pointer; _ = migration; _ = discovery; } fn retainWorkspaceSummaries(alloc: Allocator, summaries: *std.ArrayList(SessionSummary), workspace_root: []const u8) void { var write_index: usize = 0; for (summaries.items, 0..) |*summary, read_index| { const summary_workspace = summary.workspace_root orelse { summary.deinit(alloc); continue; }; if (!std.mem.eql(u8, summary_workspace, workspace_root)) { summary.deinit(alloc); continue; } if (write_index != read_index) summaries.items[write_index] = summary.*; write_index += 1; } summaries.items.len = write_index; } fn summariesMissingDisplayMetadata(summaries: []const SessionSummary) bool { for (summaries) |summary| { if (summaryNeedsDisplayMetadata(summary)) return true; } return false; } fn summaryNeedsDisplayMetadata(summary: SessionSummary) bool { if (summary.history_len == 0) return false; if (!summary.display_metadata_present) return true; return summary.title == null; } fn initialIndexEffect(state: session_codec.DurableSessionState) InitialIndexEffect { return if (state.history.len == 0) .preserve else .maintain; } pub const default_resume_page_limit: usize = 10; fn openUsageRecoveryProfileRoot( home_path: []const u8, ) !?io_mod.VerifiedDir { const zio = io_mod.getIo(); var home = try std.Io.Dir.openDirAbsolute(zio, home_path, .{ .iterate = true, }); defer home.close(zio); var profile = home.openDir(zio, profile_paths.root_dir_name, .{ .iterate = true, .follow_symlinks = false, }) catch |err| switch (err) { error.FileNotFound => return null, error.NotDir, error.SymLinkLoop => return error.InvalidUsageRecoveryIndex, else => return err, }; errdefer profile.close(zio); const stat = try profile.stat(zio); if (stat.kind != .directory or stat.permissions.toMode() & 0o777 != 0o700) { return error.InvalidUsageRecoveryIndex; } return .{ .dir = profile }; } fn openUsageRecoveryDir( home_path: []const u8, ) !?io_mod.VerifiedDir { var profile = try openUsageRecoveryProfileRoot(home_path) orelse return null; defer profile.close(); var dir = profile.dir.openDir(io_mod.getIo(), usage_recovery_dir, .{ .iterate = true, .follow_symlinks = false, }) catch |err| switch (err) { error.FileNotFound => return null, error.NotDir, error.SymLinkLoop => return error.InvalidUsageRecoveryIndex, else => return err, }; errdefer dir.close(io_mod.getIo()); const stat = try dir.stat(io_mod.getIo()); if (stat.kind != .directory or stat.permissions.toMode() & 0o777 != 0o700) { return error.InvalidUsageRecoveryIndex; } return .{ .dir = dir }; } fn validateUsageRecoveryMarker( recovery: *const io_mod.VerifiedDir, session_id: []const u8, ) !?i64 { var marker = recovery.dir.openFile(io_mod.getIo(), session_id, .{ .mode = .read_only, .allow_directory = false, .follow_symlinks = false, .resolve_beneath = true, }) catch |err| switch (err) { error.FileNotFound => return error.UsageRecoveryMarkerNotFound, else => return error.InvalidUsageRecoveryIndex, }; defer marker.close(io_mod.getIo()); const stat = try marker.stat(io_mod.getIo()); if (stat.kind != .file or stat.nlink != 1 or stat.size == 0 or stat.size > max_usage_recovery_marker_bytes or stat.permissions.toMode() & 0o777 != 0o600) { return error.InvalidUsageRecoveryIndex; } var bytes: [max_usage_recovery_marker_bytes]u8 = undefined; const marker_len: usize = @intCast(stat.size); const read = marker.readPositionalAll( io_mod.getIo(), bytes[0..marker_len], 0, ) catch return error.InvalidUsageRecoveryIndex; if (read != marker_len) { return error.InvalidUsageRecoveryIndex; } const marker_bytes = bytes[0..marker_len]; if (!std.mem.startsWith( u8, marker_bytes, usage_recovery_marker_prefix, ) or !std.mem.endsWith(u8, marker_bytes, "\n")) { return error.InvalidUsageRecoveryIndex; } const timestamp_bytes = marker_bytes[usage_recovery_marker_prefix.len .. marker_bytes.len - 1]; if (timestamp_bytes.len == 0) return error.InvalidUsageRecoveryIndex; const timestamp_ms = std.fmt.parseInt( i64, timestamp_bytes, 10, ) catch return error.InvalidUsageRecoveryIndex; if (timestamp_ms < 0) return error.InvalidUsageRecoveryIndex; return timestamp_ms; } pub const Store = struct { sessions_dir: []u8, home_dir: []u8, workspace_root: []u8, canonical_root: session_log.Root, // How many sessions one resume page yields before flagging `has_more`. // Carried on the value so it propagates through the by-value page chain; // the UI sets it from the visible screen height. resume_page_limit: usize = default_resume_page_limit, /// Narrow, copyable view of this store for the discovery/migration helpers, /// so they depend on `StoreContext` instead of the full facade. fn ctx(self: Store) StoreContext { return .{ .sessions_dir = self.sessions_dir, .home_dir = self.home_dir, .workspace_root = self.workspace_root, .canonical_root = self.canonical_root, }; } /// Opens a writable store rooted at `$HOME`, creating the layout if needed. pub fn init(alloc: Allocator, workspace_root: []const u8) !Store {