| 1 | package boot |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | |
| 7 | "reasonix/internal/extension" |
| 8 | "reasonix/internal/skill/skillwatch" |
| 9 | ) |
| 10 | |
| 11 | // RuntimeDoctorReport is the structured runtime diagnostics document for |
| 12 | // `reasonix doctor runtime` and desktop status panels. |
| 13 | type RuntimeDoctorReport struct { |
| 14 | Status *extension.RuntimeStatus `json:"status,omitempty"` |
| 15 | Metrics extension.LifecycleMetricsSnapshot `json:"metrics"` |
| 16 | Recoverability extension.Recoverability `json:"recoverability"` |
| 17 | Resume extension.ResumeDecision `json:"resume"` |
| 18 | PublishedGen uint64 `json:"publishedGeneration"` |
| 19 | DrainingGens []uint64 `json:"drainingGenerations,omitempty"` |
| 20 | RuntimeOwnerFallbacks uint64 `json:"runtimeOwnerFallbacks"` |
| 21 | SkillWatch *skillwatch.Diagnostics `json:"skillWatch,omitempty"` |
| 22 | Text string `json:"-"` |
| 23 | } |
| 24 | |
| 25 | // CollectRuntimeDoctor builds a report from an optional live BuildResult and |
| 26 | // its session-lineage owner. Also sweeps expired drains so doctor is a natural |
| 27 | // product trigger for drain-timeout force-expire. |
| 28 | func CollectRuntimeDoctor(res *BuildResult) RuntimeDoctorReport { |
| 29 | owner := extension.DefaultRuntimeOwner |
| 30 | if res != nil && res.Owner != nil { |
| 31 | owner = res.Owner |
| 32 | } |
| 33 | gate := owner.Gate |
| 34 | _ = gate.SweepAndForceExpire() |
| 35 | gen := gate.Published() |
| 36 | report := RuntimeDoctorReport{ |
| 37 | Metrics: extension.DefaultLifecycleMetrics.Snapshot(), |
| 38 | PublishedGen: gen, |
| 39 | DrainingGens: gate.DrainingGenerations(), |
| 40 | RuntimeOwnerFallbacks: extension.RuntimeOwnerFallbackCount(), |
| 41 | Recoverability: owner.AssessRecoverability(gen), |
| 42 | Resume: owner.DecideResume(gen), |
| 43 | } |
| 44 | if res != nil { |
| 45 | if res.SkillWatchService != nil { |
| 46 | diagnostics := res.SkillWatchService.Diagnostics() |
| 47 | report.SkillWatch = &diagnostics |
| 48 | } |
| 49 | report.Status = res.Status |
| 50 | if res.Status != nil { |
| 51 | report.Text = FormatRuntimeStatus(res.Status) |
| 52 | } |
| 53 | if res.Snapshot != nil { |
| 54 | g := res.Snapshot.Generation() |
| 55 | report.Recoverability = owner.AssessRecoverability(g) |
| 56 | report.Resume = owner.DecideResume(g) |
| 57 | } |
| 58 | } |
| 59 | if report.Text == "" { |
| 60 | report.Text = FormatRuntimeStatus(report.Status) |
| 61 | } |
| 62 | return report |
| 63 | } |
| 64 | |
| 65 | // RenderRuntimeDoctorJSON encodes the report. |
| 66 | func RenderRuntimeDoctorJSON(report RuntimeDoctorReport) ([]byte, error) { |
| 67 | return json.MarshalIndent(report, "", " ") |
| 68 | } |
| 69 | |
| 70 | // RenderRuntimeDoctorText is the human-readable form. |
| 71 | func RenderRuntimeDoctorText(report RuntimeDoctorReport) string { |
| 72 | body := report.Text |
| 73 | if body == "" { |
| 74 | body = "runtime status: unavailable\n" |
| 75 | } |
| 76 | body += fmt.Sprintf("runtime owner fallbacks: %d\n", report.RuntimeOwnerFallbacks) |
| 77 | if watch := report.SkillWatch; watch != nil { |
| 78 | body += fmt.Sprintf("skill watch: physical=%d logical=%d scans=%d entries=%d events=%d notifications=%d degraded=%d helper-restarts=%d\n", |
| 79 | watch.PhysicalWatches, watch.LogicalSubscriptions, watch.Scans, watch.ScannedEntries, |
| 80 | watch.EventsReceived, watch.Notifications, watch.DegradedRoots, watch.HelperRestarts) |
| 81 | } |
| 82 | body += fmt.Sprintf("recoverability: clean=%v irreversible=%v\n", report.Recoverability.Clean, report.Recoverability.HasIrreversible) |
| 83 | body += fmt.Sprintf("resume: allow=%v cleanRollback=%v\n", report.Resume.AllowResume, report.Resume.CleanRollback) |
| 84 | for _, n := range report.Recoverability.Notes { |
| 85 | body += " note: " + n + "\n" |
| 86 | } |
| 87 | for _, n := range report.Resume.Notes { |
| 88 | body += " resume-note: " + n + "\n" |
| 89 | } |
| 90 | if len(report.DrainingGens) > 0 { |
| 91 | body += fmt.Sprintf("draining generations: %v\n", report.DrainingGens) |
| 92 | } |
| 93 | return body |
| 94 | } |
| 95 |