@@ -676,7 +676,8 @@ pub async fn serve(
676676 let acp_state = AcpState {
677677 store : state. session_store . clone ( ) ,
678678 run_prompt : Arc :: new ( move |inputs : Value | -> Result < Value , String > {
679- run_agent_sync ( & acp_runner_state, inputs) . map_err ( |e| e. to_string ( ) )
679+ run_agent_sync ( & acp_runner_state, inputs)
680+ . map_err ( |e| agent_error_string ( & acp_runner_state. agent_path , & e) )
680681 } ) ,
681682 } ;
682683
@@ -1350,7 +1351,7 @@ async fn create_session(
13501351 } ;
13511352 match result {
13521353 Ok ( run_result) => apply_run_outcome ( & mut session, run_result) ,
1353- Err ( e) => session. error = Some ( e . to_string ( ) ) ,
1354+ Err ( e) => session. error = Some ( agent_error_string ( & state . agent_path , & e ) ) ,
13541355 }
13551356
13561357 if let Some ( err) = store_or_500 ( & state, & session) {
@@ -1911,7 +1912,8 @@ async fn stream_session(
19111912 Err ( e) => {
19121913 session. status = SessionStatus :: Failed ;
19131914 session. output = None ;
1914- session. error = Some ( e. to_string( ) ) ;
1915+ session. error =
1916+ Some ( agent_error_string( & state_for_stream. agent_path, & e) ) ;
19151917 }
19161918 }
19171919 if was_cancelled {
@@ -2107,6 +2109,18 @@ async fn cancel_session(
21072109 . into_response ( )
21082110}
21092111
2112+ /// Render an agent-run error for a session's stored/returned `error` field.
2113+ /// Uncaught-exception stack frames arrive from the engine in transpiled
2114+ /// coordinates; remap them to positions in the original TypeScript against
2115+ /// the served agent's workspace root — the same display-boundary remap the
2116+ /// CLI applies in `main::report_cli_error` (which resolves its root from a
2117+ /// thread-local these tokio handlers never set). Frames that can't be read
2118+ /// or remapped pass through unchanged, as does any error without frames.
2119+ fn agent_error_string ( agent_path : & FsPath , e : & anyhow:: Error ) -> String {
2120+ let root = crate :: runtime:: typescript:: transpile:: find_workspace_root ( agent_path) ;
2121+ crate :: runtime:: rust_engine:: remap_stack_frames_within ( & root, & e. to_string ( ) )
2122+ }
2123+
21102124/// Map a finished engine run onto a stored session: status, output, call log,
21112125/// and the pending-pause fields (input prompt / signal listen set + timeout
21122126/// deadline / approval). Shared by session creation, the durable resume tail,
@@ -2363,13 +2377,14 @@ async fn complete_pending_and_resume(
23632377 ( StatusCode :: OK , Json ( session_view ( & session) ) ) . into_response ( )
23642378 }
23652379 Err ( e) => {
2380+ let error = agent_error_string ( & state. agent_path , & e) ;
23662381 session. status = SessionStatus :: Failed ;
2367- session. error = Some ( e . to_string ( ) ) ;
2382+ session. error = Some ( error . clone ( ) ) ;
23682383 let _ = state. session_store . put ( & session) ;
23692384 state. warm_runs . lock ( ) . unwrap ( ) . remove ( & session. id ) ;
23702385 (
23712386 StatusCode :: INTERNAL_SERVER_ERROR ,
2372- Json ( json ! ( { "error" : e . to_string ( ) } ) ) ,
2387+ Json ( json ! ( { "error" : error } ) ) ,
23732388 )
23742389 . into_response ( )
23752390 }
@@ -2447,7 +2462,7 @@ async fn resume_session(
24472462 Some ( Ok ( run_result) ) => apply_run_outcome ( & mut session, run_result) ,
24482463 Some ( Err ( e) ) => {
24492464 session. status = SessionStatus :: Failed ;
2450- session. error = Some ( e . to_string ( ) ) ;
2465+ session. error = Some ( agent_error_string ( & state . agent_path , & e ) ) ;
24512466 }
24522467 None => {
24532468 session. status = SessionStatus :: Failed ;
@@ -2927,12 +2942,13 @@ async fn approve_session(
29272942 ( StatusCode :: OK , Json ( session_view ( & session) ) ) . into_response ( )
29282943 }
29292944 Err ( e) => {
2945+ let error = agent_error_string ( & state. agent_path , & e) ;
29302946 session. status = SessionStatus :: Failed ;
2931- session. error = Some ( e . to_string ( ) ) ;
2947+ session. error = Some ( error . clone ( ) ) ;
29322948 let _ = state. session_store . put ( & session) ;
29332949 (
29342950 StatusCode :: INTERNAL_SERVER_ERROR ,
2935- Json ( json ! ( { "error" : e . to_string ( ) } ) ) ,
2951+ Json ( json ! ( { "error" : error } ) ) ,
29362952 )
29372953 . into_response ( )
29382954 }
@@ -3012,7 +3028,7 @@ async fn handle_event(
30123028 }
30133029 Err ( e) => {
30143030 eprintln ! ( "Agent error: {e:#}" ) ;
3015- let error = json ! ( { "error" : e . to_string ( ) } ) ;
3031+ let error = json ! ( { "error" : agent_error_string ( & state . agent_path , & e ) } ) ;
30163032 ( StatusCode :: INTERNAL_SERVER_ERROR , Json ( error) ) . into_response ( )
30173033 }
30183034 }
@@ -3026,6 +3042,73 @@ mod tests {
30263042 } ;
30273043 use axum:: body;
30283044
3045+ /// A failed session's `error` must carry stack frames in ORIGINAL
3046+ /// TypeScript coordinates for every frame, not just the throwing one —
3047+ /// the engine hands the server transpiled-bundle positions, and the
3048+ /// server (whose tokio handlers never set the CLI's thread-local display
3049+ /// root) remaps them against the agent's workspace root.
3050+ #[ test]
3051+ fn agent_error_string_remaps_every_frame_to_original_source ( ) {
3052+ let dir = std:: env:: temp_dir ( ) . join ( format ! ( "chidori-srv-frames-{}" , uuid:: Uuid :: new_v4( ) ) ) ;
3053+ std:: fs:: create_dir_all ( & dir) . unwrap ( ) ;
3054+ let agent_path = dir. join ( "agent.ts" ) ;
3055+ // The interface block exists only in the original TypeScript, so every
3056+ // transpiled line number below it disagrees with the original.
3057+ let src = "interface Row {\n \
3058+ \x20 id: string;\n \
3059+ }\n \
3060+ function inner(row: Row): never {\n \
3061+ \x20 throw new Error(\" bad \" + row.id);\n \
3062+ }\n \
3063+ function outer(): never {\n \
3064+ \x20 return inner({ id: \" x\" });\n \
3065+ }\n \
3066+ export async function agent() { return outer(); }\n ";
3067+ std:: fs:: write ( & agent_path, src) . unwrap ( ) ;
3068+
3069+ // Derive the frames' transpiled coordinates the same way the engine
3070+ // stamps them: the emitted definition line of each function.
3071+ let ( js, _map) =
3072+ crate :: runtime:: typescript:: transpile:: transpile_source_with_map ( & agent_path, src)
3073+ . unwrap ( ) ;
3074+ let emitted_line = |name : & str | {
3075+ js. lines ( )
3076+ . position ( |l| l. contains ( & format ! ( "function {name}" ) ) )
3077+ . map ( |i| i as u32 + 1 )
3078+ . unwrap ( )
3079+ } ;
3080+ let ( inner_line, outer_line) = ( emitted_line ( "inner" ) , emitted_line ( "outer" ) ) ;
3081+ // The interface strip is what makes this test meaningful: emitted
3082+ // positions must disagree with the original definition lines (4, 7).
3083+ assert_ne ! (
3084+ inner_line, 4 ,
3085+ "transpile no longer shifts lines; rewrite this test"
3086+ ) ;
3087+
3088+ let path_str = agent_path. to_string_lossy ( ) ;
3089+ let err = anyhow:: anyhow!(
3090+ "JavaScript exception: Error: bad x\n at inner ({path_str}:{inner_line}:10)\n at outer ({path_str}:{outer_line}:10)"
3091+ ) ;
3092+ let remapped = agent_error_string ( & agent_path, & err) ;
3093+ assert ! (
3094+ remapped. contains( & format!( "at inner ({path_str}:4:" ) ) ,
3095+ "throwing frame lands on the original definition line: {remapped}"
3096+ ) ;
3097+ assert ! (
3098+ remapped. contains( & format!( "at outer ({path_str}:7:" ) ) ,
3099+ "the frame ABOVE the throwing one lands on its original line too: {remapped}"
3100+ ) ;
3101+
3102+ // Errors without frames pass through byte-identical.
3103+ let plain = anyhow:: anyhow!( "policy: `tool:x` denied" ) ;
3104+ assert_eq ! (
3105+ agent_error_string( & agent_path, & plain) ,
3106+ "policy: `tool:x` denied"
3107+ ) ;
3108+
3109+ let _ = std:: fs:: remove_dir_all ( dir) ;
3110+ }
3111+
30293112 #[ test]
30303113 fn bearer_token_matches_single_key ( ) {
30313114 assert ! ( bearer_token_matches( "Bearer sekrit" , "sekrit" ) ) ;
0 commit comments