const std = @import("std");const auth_runtime = @import("../auth/auth_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const io_mod = @import("../shared/io.zig");const agent_steps = @import("../config/agent_steps.zig");const config_runtime = @import("../config/config_runtime.zig");const host = @import("../hosts/host.zig");const mcp_contract = @import("../mcp/mcp_contract.zig");const sandbox = @import("../permissions/sandbox.zig");const session_store = @import("../session/session_store.zig");const types = @import("../shared/types.zig"); const Allocator = std.mem.Allocator;const default_session_diagnostics_limit: usize = 64; pub const CheckStatus = enum { ok, warn, fail,}; pub const Check = struct { name: []const u8, status: CheckStatus, detail: []const u8, owned_detail: ?[]u8 = null, pub fn deinit(self: *Check, alloc: Allocator) void { if (self.owned_detail) |detail| alloc.free(detail); self.* = undefined; }}; pub const Snapshot = struct { workspace_root: []u8, model: []const u8, owned_model: ?[]u8 = null, auth: auth_runtime.StatusSnapshot = .{}, permission_mode: types.PermissionMode, agent_step_limit: usize, checks: []Check, pub fn deinit(self: *Snapshot, alloc: Allocator) void { alloc.free(self.workspace_root); if (self.owned_model) |model| alloc.free(model); self.auth.deinit(alloc); for (self.checks) |*entry| entry.deinit(alloc); alloc.free(self.checks); self.* = undefined; }}; const ResolvedModel = struct { value: []const u8, owned: ?[]u8 = null,}; pub fn collect( alloc: Allocator, secret_store: host.SecretStore, default_model: []const u8, default_agent_step_limit: usize, mcp_config_diagnostic: mcp_contract.ProfileConfigDiagnostic,) !Snapshot { const workspace_root = try io_mod.realpathAlloc(alloc, "."); debug_trace.configureFromEnv(alloc, workspace_root); var checks: std.ArrayList(Check) = .empty; errdefer { for (checks.items) |*entry| entry.deinit(alloc); checks.deinit(alloc); } var snapshot = Snapshot{ .workspace_root = workspace_root, .model = default_model, .permission_mode = config_runtime.default_permission_mode, .agent_step_limit = default_agent_step_limit, .checks = &.{}, }; errdefer { alloc.free(snapshot.workspace_root); if (snapshot.owned_model) |model| alloc.free(model); snapshot.auth.deinit(alloc); } const workspace_detail = try std.fmt.allocPrint(alloc, "using workspace {s}", .{snapshot.workspace_root}); try appendCheckOwned(&checks, alloc, "workspace", .ok, workspace_detail); const paths = try config_runtime.discoverPaths(alloc, snapshot.workspace_root); defer { var mutable_paths = paths; mutable_paths.deinit(alloc); } var detailed = config_runtime.loadMergedSettingsDetailed(alloc, snapshot.workspace_root) catch |err| { // Settings are unreadable, so no remembered choice is available to honour. snapshot.auth = try auth_runtime.loadStatusSnapshot(alloc, secret_store, null); try appendConfigLoadFailureCheck(&checks, alloc, "config", "failed to load config", err); try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic); try appendAuthCheck(&checks, alloc, snapshot.auth); try appendConfigLoadFailureCheck(&checks, alloc, "startup", "failed to resolve startup settings", err); try appendStateChecks(&checks, alloc, snapshot.workspace_root); try appendGitCheck(&checks, alloc, snapshot.workspace_root); try appendGhCheck(&checks, alloc); snapshot.checks = try checks.toOwnedSlice(alloc); return snapshot; }; defer detailed.deinit(alloc); snapshot.auth = try auth_runtime.loadStatusSnapshot( alloc, secret_store, detailed.settings.credential_source, ); try appendConfigCheck(&checks, alloc, paths, detailed.diagnostics); try appendConfigDiagnosticChecks(&checks, alloc, detailed.diagnostics); try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic); try appendAuthCheck(&checks, alloc, snapshot.auth); try appendResolvedStartupCheck(&snapshot, &checks, alloc, .{ .model = detailed.settings.model, .permission_mode = detailed.settings.permission_mode, .max_agent_steps = detailed.settings.max_agent_steps, }, default_model, default_agent_step_limit); try appendStateChecks(&checks, alloc, snapshot.workspace_root); try appendGitCheck(&checks, alloc, snapshot.workspace_root); try appendGhCheck(&checks, alloc); snapshot.checks = try checks.toOwnedSlice(alloc); return snapshot;} fn appendConfigDiagnosticChecks( checks: *std.ArrayList(Check), alloc: Allocator, diagnostics: []const config_runtime.ConfigDiagnostic,) !void { for (diagnostics) |diagnostic| { var detail_writer: std.Io.Writer.Allocating = .init(alloc); defer detail_writer.deinit(); try detail_writer.writer.print( "{s} config diagnostic: {s}", .{ @tagName(diagnostic.layer), @tagName(diagnostic.cause) }, ); try config_runtime.writeDiagnosticMetadata(&detail_writer.writer, diagnostic); const detail = try detail_writer.toOwnedSlice(); try appendCheckOwned(checks, alloc, "config", .warn, detail); }} fn appendConfigCheck( checks: *std.ArrayList(Check), alloc: Allocator, paths: config_runtime.Paths, diagnostics: []const config_runtime.ConfigDiagnostic,) !void { const user_exists = if (paths.user_settings) |path| try fileExists(path) and !configLayerRejected(diagnostics, .user) else false; const repo_exists = try fileExists(paths.workspace_settings) and !configLayerRejected(diagnostics, .project); if (!user_exists and !repo_exists) { try appendCheck(checks, alloc, "config", .warn, "no config files found; using defaults and env overrides"); return; } const detail = try formatConfigPresence(alloc, user_exists, repo_exists); try appendCheckOwned(checks, alloc, "config", .ok, detail);} fn configLayerRejected( diagnostics: []const config_runtime.ConfigDiagnostic, layer: config_runtime.ConfigLayer,) bool { for (diagnostics) |diagnostic| { if (diagnostic.layer != layer) continue; switch (diagnostic.cause) { .malformed_settings, .settings_too_large, .durable_path_unsafe, .private_state_permissions_unsupported, .invalid_model_id, .retired_skill_match_fuzzy, .invalid_context_limits, => return true, .invalid_additional_directories, .ignored_project_user_only_setting, .legacy_workspace_preferences, .manual_backup_available, => {}, } } return false;} fn appendAuthCheck(checks: *std.ArrayList(Check), alloc: Allocator, auth: auth_runtime.StatusSnapshot) !void { if (auth.missingHelp(.cli)) |help| { try appendCheck(checks, alloc, "auth", .fail, help); return; } const detail = try auth.formatDoctorDetail(alloc); try appendCheckOwned(checks, alloc, "auth", if (auth.expired) .warn else .ok, detail);} fn appendResolvedStartupCheck( snapshot: *Snapshot, checks: *std.ArrayList(Check), alloc: Allocator, settings: config_runtime.StartupStatusSettings, default_model: []const u8, default_agent_step_limit: usize,) !void { const next_model = try resolveModel(alloc, default_model, settings.model); if (snapshot.owned_model) |model| alloc.free(model); snapshot.model = next_model.value; snapshot.owned_model = next_model.owned; snapshot.permission_mode = try resolvePermissionMode(settings.permission_mode); snapshot.agent_step_limit = try resolveAgentStepLimit(default_agent_step_limit, settings.max_agent_steps); const detail = try std.fmt.allocPrint( alloc, "resolved model={s}, permission_mode={s}, agent_step_limit={d}", .{ snapshot.model, permissionModeLabel(snapshot.permission_mode), snapshot.agent_step_limit }, ); try appendCheckOwned(checks, alloc, "startup", .ok, detail);} fn appendConfigLoadFailureCheck(checks: *std.ArrayList(Check), alloc: Allocator, name: []const u8, prefix: []const u8, err: anyerror) !void { const detail = try formatConfigLoadFailure(alloc, prefix, err); try appendCheckOwned(checks, alloc, name, .fail, detail);} fn formatConfigLoadFailure(alloc: Allocator, prefix: []const u8, err: anyerror) ![]u8 { return std.fmt.allocPrint(alloc, "{s}: {s}", .{ prefix, sandbox.configErrorMessage(err) orelse @errorName(err) });} fn appendStateChecks(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void { var store = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| { const status: CheckStatus = if (err == error.HomeNotSet) .warn else .fail; const detail = try std.fmt.allocPrint(alloc, "failed to inspect workspace state: {s}", .{@errorName(err)}); try appendCheckOwned(checks, alloc, "state", status, detail); return; }; defer store.deinit(alloc); if (store.canonical_root.sessions == null) { try appendCheck(checks, alloc, "state", .warn, "durable state is not initialized"); try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet"); return; } const state_detail = try std.fmt.allocPrint(alloc, "state dir ready at {s} (per-session managed state created on demand)", .{store.sessions_dir}); try appendCheckOwned(checks, alloc, "state", .ok, state_detail); var inspection = try store.inspectForDoctorBounded(alloc, default_session_diagnostics_limit); defer inspection.deinit(alloc); try appendSessionDiagnosticChecks( checks, alloc, &store, inspection.diagnostics.items, ); if (inspection.truncated) { try appendSessionDiagnosticsTruncatedCheck(checks, alloc, inspection.inspected_count); } if (try store.readStateSummary(alloc)) |summary_value| { var summary = summary_value; defer summary.deinit(alloc); try appendSessionsCountCheck(checks, alloc, summary.count, summary.latest_id orelse ""); return; } try appendSessionsCountUnavailableCheck(checks, alloc);} fn appendSessionDiagnosticsTruncatedCheck( checks: *std.ArrayList(Check), alloc: Allocator, inspected_count: usize,) !void { const detail = try std.fmt.allocPrint( alloc, "session diagnostics truncated after {d} session director{s} to keep doctor bounded", .{ inspected_count, if (inspected_count == 1) "y" else "ies" }, ); try appendCheckOwned(checks, alloc, "session", .warn, detail);} fn appendSessionsCountUnavailableCheck( checks: *std.ArrayList(Check), alloc: Allocator,) !void { try appendCheck( checks, alloc, "sessions", .warn, "exact saved-session count unavailable without a full session scan", );} fn appendSessionsCountCheck( checks: *std.ArrayList(Check), alloc: Allocator, count: usize, latest_id: []const u8,) !void { if (count == 0) { try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet"); return; } const detail = try std.fmt.allocPrint(alloc, "{d} saved session(s); latest={s}", .{ count, latest_id }); try appendCheckOwned(checks, alloc, "sessions", .ok, detail);} fn appendSessionDiagnosticChecks( checks: *std.ArrayList(Check), alloc: Allocator, read_store: *const session_store.Store, diagnostics: []const session_store.DoctorDiagnostic,) !void { for (diagnostics) |diagnostic| { if (diagnostic.kind == .cleanup_candidate) { const cleanup = read_store.cleanupForDoctor( alloc, diagnostic.session_id, ) catch |err| { const detail = try std.fmt.allocPrint( alloc, "session {s}: cleanup_candidate report_only=true error={s}", .{ diagnostic.session_id, @errorName(err) }, ); try appendCheckOwned(checks, alloc, "session", .warn, detail); continue; }; const detail = try std.fmt.allocPrint( alloc, "session {s}: cleanup_candidate cleanup_removed={d} report_only={d} ignored={d}", .{ diagnostic.session_id, cleanup.removed, cleanup.report_only, cleanup.ignored, }, ); try appendCheckOwned( checks, alloc, "session", if (cleanup.removed > 0) .ok else .warn, detail, ); continue; } const status: CheckStatus = switch (diagnostic.kind) { .authority_less_creation_orphan, .authority_transition_pending, .commit_intent_pending, .oversized_legacy_snapshot, .projection_missing, .projection_stale, .canonical_log_large, .canonical_log_compaction_overdue, .canonical_log_compaction_failed, .cleanup_candidate, => .warn, .invalid_authority_transition, .missing_authority, .invalid_authority, .invalid_commit_intent, .projection_invalid, .commit_watermark_missing, .commit_watermark_invalid, .commit_watermark_mismatched, .canonical_state_invalid, .unsafe_path, => .fail, }; var recovery_buffer: [512]u8 = undefined; const recovery = try recoveryActionForSessionDiagnostic( diagnostic.kind, diagnostic.session_id, &recovery_buffer, ); const detail = if (diagnostic.growth_bytes) |growth_bytes| try std.fmt.allocPrint( alloc, "session {s}: {s} bytes={d} growth_bytes={d} growth_frames={d}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), diagnostic.bytes.?, growth_bytes, diagnostic.growth_frames.?, recovery, }, ) else if (diagnostic.bytes) |bytes| try std.fmt.allocPrint( alloc, "session {s}: {s} bytes={d}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), bytes, recovery }, ) else try std.fmt.allocPrint( alloc, "session {s}: {s}{s}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), if (diagnostic.kind == .authority_transition_pending) " report_only=true" else "", recovery, }, ); try appendCheckOwned(checks, alloc, "session", status, detail); }} fn recoveryActionForSessionDiagnostic( kind: session_store.DoctorIssueKind, session_id: []const u8, buffer: []u8,) ![]const u8 { return switch (kind) { .authority_less_creation_orphan, .authority_transition_pending, .commit_intent_pending, .cleanup_candidate, => "rerun fx doctor after active writers exit; cleanup is guarded", .canonical_log_large, .canonical_log_compaction_overdue, => "resume or update the session to trigger compaction", .canonical_log_compaction_failed, => "inspect the failed compaction artifact; keep it until no writer is active", .oversized_legacy_snapshot, => "rerun migration with --allow-large only after verifying the legacy snapshot", .projection_missing, .projection_stale, => "open the session detail or resume it to rebuild projections", .commit_watermark_missing, .commit_watermark_invalid, .commit_watermark_mismatched, => std.fmt.bufPrint( buffer, "run fx session recover {s}; it creates a separate resumable copy and leaves the source unchanged", .{session_id}, ), .projection_invalid, .canonical_state_invalid, .invalid_commit_intent, => std.fmt.bufPrint( buffer, "back up ~/.fx/sessions, then inspect this session with fx session {s} --json", .{session_id}, ), .missing_authority, .invalid_authority, .invalid_authority_transition, .unsafe_path, => "back up ~/.fx/sessions and avoid opening this session until the path is repaired", };} fn appendGitCheck(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void { if (try hasGitMetadata(alloc, workspace_root)) { try appendCheck(checks, alloc, "git", .ok, "git metadata detected for this workspace"); return; } try appendCheck(checks, alloc, "git", .warn, "not a git repository; pr/issue workflows will be limited");} fn appendGhCheck(checks: *std.ArrayList(Check), alloc: Allocator) !void { if (try commandInPath(alloc, "gh")) { try appendCheck(checks, alloc, "gh", .ok, "GitHub CLI found in PATH"); return; } try appendCheck(checks, alloc, "gh", .warn, "GitHub CLI not found in PATH; publish workflows unavailable");} fn resolveModel(alloc: Allocator, default_model: []const u8, configured: ?[]const u8) !ResolvedModel { if (io_mod.getenv("FX_MODEL")) |model| {Lines 1–500 of 892. Load more
const std = @import("std"); const auth_runtime = @import("../auth/auth_runtime.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const io_mod = @import("../shared/io.zig"); const agent_steps = @import("../config/agent_steps.zig"); const config_runtime = @import("../config/config_runtime.zig"); const host = @import("../hosts/host.zig"); const mcp_contract = @import("../mcp/mcp_contract.zig"); const sandbox = @import("../permissions/sandbox.zig"); const session_store = @import("../session/session_store.zig"); const types = @import("../shared/types.zig"); const Allocator = std.mem.Allocator; const default_session_diagnostics_limit: usize = 64; pub const CheckStatus = enum { ok, warn, fail, }; pub const Check = struct { name: []const u8, status: CheckStatus, detail: []const u8, owned_detail: ?[]u8 = null, pub fn deinit(self: *Check, alloc: Allocator) void { if (self.owned_detail) |detail| alloc.free(detail); self.* = undefined; } }; pub const Snapshot = struct { workspace_root: []u8, model: []const u8, owned_model: ?[]u8 = null, auth: auth_runtime.StatusSnapshot = .{}, permission_mode: types.PermissionMode, agent_step_limit: usize, checks: []Check, pub fn deinit(self: *Snapshot, alloc: Allocator) void { alloc.free(self.workspace_root); if (self.owned_model) |model| alloc.free(model); self.auth.deinit(alloc); for (self.checks) |*entry| entry.deinit(alloc); alloc.free(self.checks); self.* = undefined; } }; const ResolvedModel = struct { value: []const u8, owned: ?[]u8 = null, }; pub fn collect( alloc: Allocator, secret_store: host.SecretStore, default_model: []const u8, default_agent_step_limit: usize, mcp_config_diagnostic: mcp_contract.ProfileConfigDiagnostic, ) !Snapshot { const workspace_root = try io_mod.realpathAlloc(alloc, "."); debug_trace.configureFromEnv(alloc, workspace_root); var checks: std.ArrayList(Check) = .empty; errdefer { for (checks.items) |*entry| entry.deinit(alloc); checks.deinit(alloc); } var snapshot = Snapshot{ .workspace_root = workspace_root, .model = default_model, .permission_mode = config_runtime.default_permission_mode, .agent_step_limit = default_agent_step_limit, .checks = &.{}, }; errdefer { alloc.free(snapshot.workspace_root); if (snapshot.owned_model) |model| alloc.free(model); snapshot.auth.deinit(alloc); } const workspace_detail = try std.fmt.allocPrint(alloc, "using workspace {s}", .{snapshot.workspace_root}); try appendCheckOwned(&checks, alloc, "workspace", .ok, workspace_detail); const paths = try config_runtime.discoverPaths(alloc, snapshot.workspace_root); defer { var mutable_paths = paths; mutable_paths.deinit(alloc); } var detailed = config_runtime.loadMergedSettingsDetailed(alloc, snapshot.workspace_root) catch |err| { // Settings are unreadable, so no remembered choice is available to honour. snapshot.auth = try auth_runtime.loadStatusSnapshot(alloc, secret_store, null); try appendConfigLoadFailureCheck(&checks, alloc, "config", "failed to load config", err); try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic); try appendAuthCheck(&checks, alloc, snapshot.auth); try appendConfigLoadFailureCheck(&checks, alloc, "startup", "failed to resolve startup settings", err); try appendStateChecks(&checks, alloc, snapshot.workspace_root); try appendGitCheck(&checks, alloc, snapshot.workspace_root); try appendGhCheck(&checks, alloc); snapshot.checks = try checks.toOwnedSlice(alloc); return snapshot; }; defer detailed.deinit(alloc); snapshot.auth = try auth_runtime.loadStatusSnapshot( alloc, secret_store, detailed.settings.credential_source, ); try appendConfigCheck(&checks, alloc, paths, detailed.diagnostics); try appendConfigDiagnosticChecks(&checks, alloc, detailed.diagnostics); try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic); try appendAuthCheck(&checks, alloc, snapshot.auth); try appendResolvedStartupCheck(&snapshot, &checks, alloc, .{ .model = detailed.settings.model, .permission_mode = detailed.settings.permission_mode, .max_agent_steps = detailed.settings.max_agent_steps, }, default_model, default_agent_step_limit); try appendStateChecks(&checks, alloc, snapshot.workspace_root); try appendGitCheck(&checks, alloc, snapshot.workspace_root); try appendGhCheck(&checks, alloc); snapshot.checks = try checks.toOwnedSlice(alloc); return snapshot; } fn appendConfigDiagnosticChecks( checks: *std.ArrayList(Check), alloc: Allocator, diagnostics: []const config_runtime.ConfigDiagnostic, ) !void { for (diagnostics) |diagnostic| { var detail_writer: std.Io.Writer.Allocating = .init(alloc); defer detail_writer.deinit(); try detail_writer.writer.print( "{s} config diagnostic: {s}", .{ @tagName(diagnostic.layer), @tagName(diagnostic.cause) }, ); try config_runtime.writeDiagnosticMetadata(&detail_writer.writer, diagnostic); const detail = try detail_writer.toOwnedSlice(); try appendCheckOwned(checks, alloc, "config", .warn, detail); } } fn appendConfigCheck( checks: *std.ArrayList(Check), alloc: Allocator, paths: config_runtime.Paths, diagnostics: []const config_runtime.ConfigDiagnostic, ) !void { const user_exists = if (paths.user_settings) |path| try fileExists(path) and !configLayerRejected(diagnostics, .user) else false; const repo_exists = try fileExists(paths.workspace_settings) and !configLayerRejected(diagnostics, .project); if (!user_exists and !repo_exists) { try appendCheck(checks, alloc, "config", .warn, "no config files found; using defaults and env overrides"); return; } const detail = try formatConfigPresence(alloc, user_exists, repo_exists); try appendCheckOwned(checks, alloc, "config", .ok, detail); } fn configLayerRejected( diagnostics: []const config_runtime.ConfigDiagnostic, layer: config_runtime.ConfigLayer, ) bool { for (diagnostics) |diagnostic| { if (diagnostic.layer != layer) continue; switch (diagnostic.cause) { .malformed_settings, .settings_too_large, .durable_path_unsafe, .private_state_permissions_unsupported, .invalid_model_id, .retired_skill_match_fuzzy, .invalid_context_limits, => return true, .invalid_additional_directories, .ignored_project_user_only_setting, .legacy_workspace_preferences, .manual_backup_available, => {}, } } return false; } fn appendAuthCheck(checks: *std.ArrayList(Check), alloc: Allocator, auth: auth_runtime.StatusSnapshot) !void { if (auth.missingHelp(.cli)) |help| { try appendCheck(checks, alloc, "auth", .fail, help); return; } const detail = try auth.formatDoctorDetail(alloc); try appendCheckOwned(checks, alloc, "auth", if (auth.expired) .warn else .ok, detail); } fn appendResolvedStartupCheck( snapshot: *Snapshot, checks: *std.ArrayList(Check), alloc: Allocator, settings: config_runtime.StartupStatusSettings, default_model: []const u8, default_agent_step_limit: usize, ) !void { const next_model = try resolveModel(alloc, default_model, settings.model); if (snapshot.owned_model) |model| alloc.free(model); snapshot.model = next_model.value; snapshot.owned_model = next_model.owned; snapshot.permission_mode = try resolvePermissionMode(settings.permission_mode); snapshot.agent_step_limit = try resolveAgentStepLimit(default_agent_step_limit, settings.max_agent_steps); const detail = try std.fmt.allocPrint( alloc, "resolved model={s}, permission_mode={s}, agent_step_limit={d}", .{ snapshot.model, permissionModeLabel(snapshot.permission_mode), snapshot.agent_step_limit }, ); try appendCheckOwned(checks, alloc, "startup", .ok, detail); } fn appendConfigLoadFailureCheck(checks: *std.ArrayList(Check), alloc: Allocator, name: []const u8, prefix: []const u8, err: anyerror) !void { const detail = try formatConfigLoadFailure(alloc, prefix, err); try appendCheckOwned(checks, alloc, name, .fail, detail); } fn formatConfigLoadFailure(alloc: Allocator, prefix: []const u8, err: anyerror) ![]u8 { return std.fmt.allocPrint(alloc, "{s}: {s}", .{ prefix, sandbox.configErrorMessage(err) orelse @errorName(err) }); } fn appendStateChecks(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void { var store = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| { const status: CheckStatus = if (err == error.HomeNotSet) .warn else .fail; const detail = try std.fmt.allocPrint(alloc, "failed to inspect workspace state: {s}", .{@errorName(err)}); try appendCheckOwned(checks, alloc, "state", status, detail); return; }; defer store.deinit(alloc); if (store.canonical_root.sessions == null) { try appendCheck(checks, alloc, "state", .warn, "durable state is not initialized"); try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet"); return; } const state_detail = try std.fmt.allocPrint(alloc, "state dir ready at {s} (per-session managed state created on demand)", .{store.sessions_dir}); try appendCheckOwned(checks, alloc, "state", .ok, state_detail); var inspection = try store.inspectForDoctorBounded(alloc, default_session_diagnostics_limit); defer inspection.deinit(alloc); try appendSessionDiagnosticChecks( checks, alloc, &store, inspection.diagnostics.items, ); if (inspection.truncated) { try appendSessionDiagnosticsTruncatedCheck(checks, alloc, inspection.inspected_count); } if (try store.readStateSummary(alloc)) |summary_value| { var summary = summary_value; defer summary.deinit(alloc); try appendSessionsCountCheck(checks, alloc, summary.count, summary.latest_id orelse ""); return; } try appendSessionsCountUnavailableCheck(checks, alloc); } fn appendSessionDiagnosticsTruncatedCheck( checks: *std.ArrayList(Check), alloc: Allocator, inspected_count: usize, ) !void { const detail = try std.fmt.allocPrint( alloc, "session diagnostics truncated after {d} session director{s} to keep doctor bounded", .{ inspected_count, if (inspected_count == 1) "y" else "ies" }, ); try appendCheckOwned(checks, alloc, "session", .warn, detail); } fn appendSessionsCountUnavailableCheck( checks: *std.ArrayList(Check), alloc: Allocator, ) !void { try appendCheck( checks, alloc, "sessions", .warn, "exact saved-session count unavailable without a full session scan", ); } fn appendSessionsCountCheck( checks: *std.ArrayList(Check), alloc: Allocator, count: usize, latest_id: []const u8, ) !void { if (count == 0) { try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet"); return; } const detail = try std.fmt.allocPrint(alloc, "{d} saved session(s); latest={s}", .{ count, latest_id }); try appendCheckOwned(checks, alloc, "sessions", .ok, detail); } fn appendSessionDiagnosticChecks( checks: *std.ArrayList(Check), alloc: Allocator, read_store: *const session_store.Store, diagnostics: []const session_store.DoctorDiagnostic, ) !void { for (diagnostics) |diagnostic| { if (diagnostic.kind == .cleanup_candidate) { const cleanup = read_store.cleanupForDoctor( alloc, diagnostic.session_id, ) catch |err| { const detail = try std.fmt.allocPrint( alloc, "session {s}: cleanup_candidate report_only=true error={s}", .{ diagnostic.session_id, @errorName(err) }, ); try appendCheckOwned(checks, alloc, "session", .warn, detail); continue; }; const detail = try std.fmt.allocPrint( alloc, "session {s}: cleanup_candidate cleanup_removed={d} report_only={d} ignored={d}", .{ diagnostic.session_id, cleanup.removed, cleanup.report_only, cleanup.ignored, }, ); try appendCheckOwned( checks, alloc, "session", if (cleanup.removed > 0) .ok else .warn, detail, ); continue; } const status: CheckStatus = switch (diagnostic.kind) { .authority_less_creation_orphan, .authority_transition_pending, .commit_intent_pending, .oversized_legacy_snapshot, .projection_missing, .projection_stale, .canonical_log_large, .canonical_log_compaction_overdue, .canonical_log_compaction_failed, .cleanup_candidate, => .warn, .invalid_authority_transition, .missing_authority, .invalid_authority, .invalid_commit_intent, .projection_invalid, .commit_watermark_missing, .commit_watermark_invalid, .commit_watermark_mismatched, .canonical_state_invalid, .unsafe_path, => .fail, }; var recovery_buffer: [512]u8 = undefined; const recovery = try recoveryActionForSessionDiagnostic( diagnostic.kind, diagnostic.session_id, &recovery_buffer, ); const detail = if (diagnostic.growth_bytes) |growth_bytes| try std.fmt.allocPrint( alloc, "session {s}: {s} bytes={d} growth_bytes={d} growth_frames={d}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), diagnostic.bytes.?, growth_bytes, diagnostic.growth_frames.?, recovery, }, ) else if (diagnostic.bytes) |bytes| try std.fmt.allocPrint( alloc, "session {s}: {s} bytes={d}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), bytes, recovery }, ) else try std.fmt.allocPrint( alloc, "session {s}: {s}{s}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), if (diagnostic.kind == .authority_transition_pending) " report_only=true" else "", recovery, }, ); try appendCheckOwned(checks, alloc, "session", status, detail); } } fn recoveryActionForSessionDiagnostic( kind: session_store.DoctorIssueKind, session_id: []const u8, buffer: []u8, ) ![]const u8 { return switch (kind) { .authority_less_creation_orphan, .authority_transition_pending, .commit_intent_pending, .cleanup_candidate, => "rerun fx doctor after active writers exit; cleanup is guarded", .canonical_log_large, .canonical_log_compaction_overdue, => "resume or update the session to trigger compaction", .canonical_log_compaction_failed, => "inspect the failed compaction artifact; keep it until no writer is active", .oversized_legacy_snapshot, => "rerun migration with --allow-large only after verifying the legacy snapshot", .projection_missing, .projection_stale, => "open the session detail or resume it to rebuild projections", .commit_watermark_missing, .commit_watermark_invalid, .commit_watermark_mismatched, => std.fmt.bufPrint( buffer, "run fx session recover {s}; it creates a separate resumable copy and leaves the source unchanged", .{session_id}, ), .projection_invalid, .canonical_state_invalid, .invalid_commit_intent, => std.fmt.bufPrint( buffer, "back up ~/.fx/sessions, then inspect this session with fx session {s} --json", .{session_id}, ), .missing_authority, .invalid_authority, .invalid_authority_transition, .unsafe_path, => "back up ~/.fx/sessions and avoid opening this session until the path is repaired", }; } fn appendGitCheck(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void { if (try hasGitMetadata(alloc, workspace_root)) { try appendCheck(checks, alloc, "git", .ok, "git metadata detected for this workspace"); return; } try appendCheck(checks, alloc, "git", .warn, "not a git repository; pr/issue workflows will be limited"); } fn appendGhCheck(checks: *std.ArrayList(Check), alloc: Allocator) !void { if (try commandInPath(alloc, "gh")) { try appendCheck(checks, alloc, "gh", .ok, "GitHub CLI found in PATH"); return; } try appendCheck(checks, alloc, "gh", .warn, "GitHub CLI not found in PATH; publish workflows unavailable"); } fn resolveModel(alloc: Allocator, default_model: []const u8, configured: ?[]const u8) !ResolvedModel { if (io_mod.getenv("FX_MODEL")) |model| {