package obs import ( "fmt" "runtime/debug" ) // panic.go is SafeGo's opposite number, and the contrast is the point: SafeGo swallows so a background // telemetry write dies alone, while on the PAID path a swallowed crash is how a run that died mid-book // gets reported as a success. THE defect (row 176): unrecovered, a panic leaves through the Go runtime's // handler, which exits 2 — the number tmctl's contract gives to "completed with flags" — and the // supervisor records the crashed run as `ready`. Here a panic becomes an ordinary error instead. // PanicError is a recovered runtime panic, carrying the stack of the goroutine that panicked. // // It deliberately has NO Unwrap: the recovered value may itself be an error (a panicked context.Canceled, // a *pipeline.CeilingHalt thrown by accident), and exposing it to errors.Is/As would let a crash match a // sentinel of the exit-code dictionary and read as a deliberate stop. type PanicError struct { // Where names the goroutine that died, since the message is often read without the stack. Where string Value any Stack []byte } // Error renders the panic WITH its stack. The stack is part of the message rather than something a // caller has to fish out with errors.As, because the one guaranteed reader is `tmctl: ` on stderr, // and the wrapping between here and there is not under this type's control: a crash whose diagnosis // depends on nobody using %v on the way up is a diagnosis that will be missing when it is needed. func (e *PanicError) Error() string { return fmt.Sprintf("PANIC in %s: %v\n%s", e.Where, e.Value, e.Stack) } // NewPanicError converts a recover() value into an error. Call it only when recover() returned non-nil; // `where` names the goroutine ("wave worker", "tmctl"). func NewPanicError(where string, v any) *PanicError { return &PanicError{Where: where, Value: v, Stack: debug.Stack()} }