@@ -26,8 +26,30 @@ export interface SpawnConfig {
2626 * Handles process spawning, output parsing, and lifecycle management.
2727 * Subclasses implement prepare() and getSpawnConfig() for mode-specific behavior.
2828 */
29+ /** Keep the last 20 stderr lines so we can surface them if the process crashes.
30+ * 20 lines is enough to capture a typical Python traceback or error context
31+ * without accumulating unbounded memory for long-running servers. */
32+ const STDERR_BUFFER_SIZE = 20 ;
33+
34+ /** Paths that indicate internal framework frames (not user code) */
35+ const INTERNAL_FRAME_PATTERNS = [
36+ '/site-packages/' ,
37+ '<frozen ' ,
38+ '/multiprocessing/' ,
39+ '/asyncio/' ,
40+ '/concurrent/' ,
41+ '/importlib/' ,
42+ ] ;
43+
44+ function isInternalFrame ( line : string ) : boolean {
45+ return INTERNAL_FRAME_PATTERNS . some ( p => line . includes ( p ) ) ;
46+ }
47+
2948export abstract class DevServer {
3049 protected child : ChildProcess | null = null ;
50+ private recentStderr : string [ ] = [ ] ;
51+ private inTraceback = false ;
52+ private tracebackBuffer : string [ ] = [ ] ;
3153
3254 constructor (
3355 protected readonly config : DevConfig ,
@@ -71,6 +93,41 @@ export abstract class DevServer {
7193 /** Returns the command, args, cwd, and environment for the child process. */
7294 protected abstract getSpawnConfig ( ) : SpawnConfig ;
7395
96+ /**
97+ * Emit a filtered Python traceback: only user code frames and the exception line.
98+ * Internal frames (site-packages, frozen modules, asyncio, etc.) are stripped out.
99+ */
100+ private emitFilteredTraceback ( onLog : ( level : LogLevel , message : string ) => void ) : void {
101+ const buf = this . tracebackBuffer ;
102+ if ( buf . length === 0 ) return ;
103+
104+ // The last line is the exception (e.g., "ModuleNotFoundError: ...")
105+ const exceptionLine = buf [ buf . length - 1 ] ! ;
106+
107+ // Collect user-code frames: a "File ..." line followed by its code line.
108+ // Frames come in pairs: " File "path", line N, in func" + " code_line"
109+ const userFrames : string [ ] = [ ] ;
110+ for ( let i = 0 ; i < buf . length - 1 ; i ++ ) {
111+ const frameLine = buf [ i ] ! ;
112+ const trimmed = frameLine . trimStart ( ) ;
113+ if ( trimmed . startsWith ( 'File ' ) && ! isInternalFrame ( frameLine ) ) {
114+ userFrames . push ( frameLine ) ;
115+ // Include the next line (source code) if it exists and is indented
116+ const nextLine = buf [ i + 1 ] ;
117+ if ( nextLine && nextLine . startsWith ( ' ' ) && ! nextLine . trimStart ( ) . startsWith ( 'File ' ) ) {
118+ userFrames . push ( nextLine ) ;
119+ }
120+ }
121+ }
122+
123+ if ( userFrames . length > 0 ) {
124+ for ( const frame of userFrames ) {
125+ onLog ( 'error' , frame ) ;
126+ }
127+ }
128+ onLog ( 'error' , exceptionLine ) ;
129+ }
130+
74131 /** Attach stdout/stderr/error/exit handlers to the child process. */
75132 private attachHandlers ( ) : void {
76133 const { onLog, onExit } = this . options . callbacks ;
@@ -88,6 +145,31 @@ export abstract class DevServer {
88145 if ( ! output ) return ;
89146 for ( const line of output . split ( '\n' ) ) {
90147 if ( ! line ) continue ;
148+ // Buffer recent stderr for crash context
149+ this . recentStderr . push ( line ) ;
150+ if ( this . recentStderr . length > STDERR_BUFFER_SIZE ) {
151+ this . recentStderr . shift ( ) ;
152+ }
153+ // Detect Python traceback blocks: buffer all lines, then emit a
154+ // filtered version showing only user code frames + the exception.
155+ if ( line . startsWith ( 'Traceback (most recent call last)' ) ) {
156+ this . inTraceback = true ;
157+ this . tracebackBuffer = [ ] ;
158+ }
159+ if ( this . inTraceback ) {
160+ this . tracebackBuffer . push ( line ) ;
161+ const isStackFrame = line . startsWith ( ' ' ) || line . startsWith ( 'File ' ) ;
162+ const isTracebackHeader = line . startsWith ( 'Traceback ' ) ;
163+ if ( ! isStackFrame && ! isTracebackHeader ) {
164+ // Traceback ended — emit filtered summary and clear the
165+ // stderr buffer so these lines aren't re-emitted on exit.
166+ this . emitFilteredTraceback ( onLog ) ;
167+ this . inTraceback = false ;
168+ this . tracebackBuffer = [ ] ;
169+ this . recentStderr = [ ] ;
170+ }
171+ continue ;
172+ }
91173 const lower = line . toLowerCase ( ) ;
92174 if ( lower . includes ( 'warning' ) ) onLog ( 'warn' , line ) ;
93175 else if ( lower . includes ( 'error' ) ) onLog ( 'error' , line ) ;
@@ -100,6 +182,14 @@ export abstract class DevServer {
100182 onExit ( 1 ) ;
101183 } ) ;
102184
103- this . child ?. on ( 'exit' , code => onExit ( code ) ) ;
185+ this . child ?. on ( 'exit' , code => {
186+ if ( code !== 0 && code !== null && this . recentStderr . length > 0 ) {
187+ for ( const line of this . recentStderr ) {
188+ onLog ( 'error' , line ) ;
189+ }
190+ this . recentStderr = [ ] ;
191+ }
192+ onExit ( code ) ;
193+ } ) ;
104194 }
105195}
0 commit comments