12396 lines · 445.4 KB · Zig
RawDownload
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.