From 305db55cd7b4fba861800f36292e41d328a69c9f Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 26 Jun 2026 09:39:53 +0200 Subject: [PATCH] fix(coding-agent/eval): surfaced the exception type and message in the - Surface the exception type and message in the error display by prepending the formatted error string to the traceback array. - Prevent the host from hiding the actual error by ensuring the traceback is not empty, consistent with other language runners. --- packages/coding-agent/CHANGELOG.md | 4 + .../src/eval/__tests__/julia-prelude.test.ts | 18 + packages/coding-agent/src/eval/jl/runner.jl | 8 +- scripts/bash_corpus_stats.jl | 357 ++++++++++++++++++ 4 files changed, 386 insertions(+), 1 deletion(-) create mode 100755 scripts/bash_corpus_stats.jl diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 007b8c39a..d039cd828 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the `eval` Julia kernel showing only runner-internal backtrace frames (`at top-level scope (./none:N)`, `at main (…runner-…jl:635)`) with no exception type or message, making cell errors undebuggable. The host renderer (`packages/coding-agent/src/eval/kernel-base.ts`) displays a non-empty `traceback` verbatim and only falls back to `ename: evalue` when it is empty; the Python and Ruby runners embed the rendered error in `traceback`, but the Julia runner (`packages/coding-agent/src/eval/jl/runner.jl`) built `traceback` from stack frames only, so the message was dropped. `emit_error` now seeds `traceback` with the `showerror` output (matching the REPL's `ERROR:` text) ahead of the frames. + ## [16.1.22] - 2026-06-26 ### Fixed diff --git a/packages/coding-agent/src/eval/__tests__/julia-prelude.test.ts b/packages/coding-agent/src/eval/__tests__/julia-prelude.test.ts index 3775d8b2c..11225aea6 100644 --- a/packages/coding-agent/src/eval/__tests__/julia-prelude.test.ts +++ b/packages/coding-agent/src/eval/__tests__/julia-prelude.test.ts @@ -45,4 +45,22 @@ nothing expect(result.output).toContain("META=alpha:true"); expect(result.output).toContain("MULTI=2:alpha:json"); }, 30_000); + + it("surfaces the exception type and message in the error output, not just stack frames", async () => { + using tempDir = TempDir.createSync("@omp-eval-julia-error-"); + const result = await executeJulia(`println("="^8)\nmissing_var_xyz + 1`, { + cwd: tempDir.path(), + sessionId: `julia-prelude-error:${crypto.randomUUID()}`, + kernelOwnerId: OWNER_ID, + reset: true, + }); + + // The rendered error must carry the actual exception, not only the + // runner-internal backtrace frames (regression: traceback-only output + // hid `ename`/`evalue`). + expect(result.output).toContain("UndefVarError"); + expect(result.output).toContain("missing_var_xyz"); + // Frames are still present alongside the message. + expect(result.output).toContain("top-level scope"); + }, 30_000); }); diff --git a/packages/coding-agent/src/eval/jl/runner.jl b/packages/coding-agent/src/eval/jl/runner.jl index 3e8ffc6a2..886637077 100644 --- a/packages/coding-agent/src/eval/jl/runner.jl +++ b/packages/coding-agent/src/eval/jl/runner.jl @@ -522,7 +522,13 @@ function emit_error(rid, err, bt) end err_str = String(take!(io)) - tb = String[] + # Seed the traceback with the rendered exception text so the array is a + # self-contained error display, matching the Python and Ruby runners. The + # host shows `traceback` verbatim when present and only falls back to + # `ename: evalue` when it is empty, so a frames-only traceback would hide + # the real error. Julia's `showerror` output already embeds the exception + # type for nearly every error and mirrors what the REPL prints after `ERROR: `. + tb = isempty(err_str) ? String[] : String[err_str] for frame in stacktrace(bt) file = string(frame.file) line = frame.line diff --git a/scripts/bash_corpus_stats.jl b/scripts/bash_corpus_stats.jl new file mode 100755 index 000000000..f82a0b3da --- /dev/null +++ b/scripts/bash_corpus_stats.jl @@ -0,0 +1,357 @@ +#!/usr/bin/env julia +# bash_corpus_stats.jl +# Extract every bash execution from ~/.omp/stats.db and analyze it. +# sources: (1) bash-tool calls (2) full-file writes to *.sh +# Reports: (1) bash constructs (2) CLI utilities (3) flags per utility +# +# julia bash_corpus_stats.jl +# deps: import Pkg; Pkg.add(["SQLite","JSON3","DBInterface"]) + +using SQLite, JSON3, DBInterface, Printf + +# ─────────────────────────── extraction ─────────────────────────── +const DB = SQLite.DB(joinpath(homedir(), ".omp", "stats.db")) + +field(j, k) = try + v = get(JSON3.read(j), k, nothing); v isa AbstractString ? String(v) : nothing +catch; nothing end + +function extract() + bash, sh = String[], String[] + for r in DBInterface.execute(DB, "SELECT arg_json FROM ss_tool_calls WHERE tool_name IN " * + "('bash','Bash','shell','bash_exec','execute_command','run_command')") + c = field(r.arg_json, :command); c !== nothing && !isempty(strip(c)) && push!(bash, c) + end + for r in DBInterface.execute(DB, "SELECT arg_json FROM ss_tool_calls WHERE tool_name IN " * + "('write','write_file','create_file','file_write','replace_file','edit_file') " * + "AND json_extract(arg_json,'\$.path') LIKE '%.sh'") + c = field(r.arg_json, :content); c !== nothing && !isempty(strip(c)) && push!(sh, c) + end + return bash, sh +end + +# ─────────────────────────── lexer ─────────────────────────── +# Splits a bash string into words + control/redirection operators, tracks +# expansion/construct features, and captures the inner text of command & +# process substitutions (and shell-source heredoc bodies) for recursion. +const WS = (' ', '\t') +const SHELLS = Set(["sh","bash","zsh","dash","ksh","mksh","ash"]) +struct Tok; kind::Symbol; text::String; end # :W word :O operator :C comment + +function read_balanced(cs::Vector{Char}, j::Int, openc::Char, closec::Char) + n = length(cs); depth = 1; buf = Char[] + while j <= n && depth > 0 + c = cs[j] + if c == '\\' && j+1 <= n + push!(buf, c, cs[j+1]); j += 2; continue + elseif c == '\'' + k = j+1; while k <= n && cs[k] != '\''; k += 1; end + if k > n; append!(buf, @view cs[j:n]); return String(buf), n+1; end + append!(buf, @view cs[j:k]); j = k+1; continue + elseif c == '"' + k = j+1 + while k <= n && cs[k] != '"'; cs[k] == '\\' ? (k += 2) : (k += 1); end + kk = min(k, n); append!(buf, @view cs[j:kk]); j = kk+1; continue + end + if c == openc; depth += 1 + elseif c == closec; depth -= 1; depth == 0 && return String(buf), j+1 + end + push!(buf, c); j += 1 + end + return String(buf), j +end + +function lex(s::String) + cs = collect(s); n = length(cs) + toks = Tok[]; feats = String[]; subs = String[] + hd_queue = Tuple{String,Bool}[] + word = Char[]; started = false + expect_delim::Union{Nothing,Bool} = nothing + i = 1 + + flush!() = begin + if started + w = String(word) + if expect_delim !== nothing + d = replace(strip(strip(w), ['\'','"']), "\\"=>"") + push!(hd_queue, (String(d), expect_delim)); expect_delim = nothing + end + push!(toks, Tok(:W, w)) + end + empty!(word); started = false + end + line_is_shell() = begin + e = length(toks) - 1; st = e - 1 + while st >= 1 && !(toks[st].kind == :O && toks[st].text == "\n"); st -= 1; end + for idx in (st+1):e + t = toks[idx] + t.kind == :W && (last(split(t.text, '/')) in SHELLS) && return true + if t.kind == :O && (t.text == ">" || t.text == ">>") && idx < e + nt = toks[idx+1] + nt.kind == :W && endswith(strip(nt.text, ['\'','"']), ".sh") && return true + end + end + false + end + consume_heredocs(j) = begin + shell = line_is_shell() + for (delim, stripmode) in hd_queue + j += 1; body = Char[]; first = true + while j <= n + e = j; while e <= n && cs[e] != '\n'; e += 1; end + ln = String(@view cs[j:min(e-1,n)]) + test = stripmode ? lstrip(ln, '\t') : ln + if test == delim; j = e; break; end + first ? (first = false) : push!(body, '\n'); append!(body, collect(ln)) + if e > n; j = n+1; break; end + j = e + 1 + end + shell && !isempty(body) && push!(subs, String(body)) + end + empty!(hd_queue); j + end + + while i <= n + c = cs[i] + if c == '\\' && i+1 <= n && cs[i+1] == '\n'; i += 2; continue; end + if c == '\\' && i+1 <= n; push!(word, c, cs[i+1]); started = true; i += 2; continue; end + if c in WS; flush!(); i += 1; continue; end + if c == '\n' + flush!(); push!(toks, Tok(:O, "\n")) + i = isempty(hd_queue) ? i+1 : consume_heredocs(i); continue + end + if c == '#' && !started + j = i; while j <= n && cs[j] != '\n'; j += 1; end + push!(toks, Tok(:C, String(@view cs[i:j-1]))); i = j; continue + end + if c == '\'' + k = i+1; while k <= n && cs[k] != '\''; k += 1; end + kk = min(k, n); append!(word, @view cs[i:kk]); started = true; i = kk+1; continue + end + if c == '"' + j = i+1; push!(word, '"') + while j <= n && cs[j] != '"' + if cs[j] == '\\' && j+1 <= n; push!(word, cs[j], cs[j+1]); j += 2; continue; end + if cs[j] == '$' && j+1 <= n && cs[j+1] == '(' + inner, j2 = read_balanced(cs, j+2, '(', ')') + push!(feats, "cmdsub_\$()"); push!(subs, inner) + append!(word, collect("\$(" * inner * ")")); j = j2; continue + end + if cs[j] == '$' && j+1 <= n && cs[j+1] == '{' + inner, j2 = read_balanced(cs, j+2, '{', '}') + push!(feats, "paramexp_\${}"); append!(word, collect("\${" * inner * "}")); j = j2; continue + end + if cs[j] == '`' + k = j+1; while k <= n && cs[k] != '`'; cs[k] == '\\' ? (k += 2) : (k += 1); end + kk = min(k, n); push!(feats, "cmdsub_backtick"); push!(subs, String(@view cs[j+1:min(kk-1,n)])) + append!(word, @view cs[j:kk]); j = kk+1; continue + end + cs[j] == '$' && push!(feats, "var_in_dq") + push!(word, cs[j]); j += 1 + end + push!(word, '"'); started = true; i = min(j+1, n+1); continue + end + if c == '$' && i+1 <= n + nx = cs[i+1] + if nx == '(' + if i+2 <= n && cs[i+2] == '(' + inner, j = read_balanced(cs, i+3, '(', ')') + push!(feats, "arith_\$(())"); append!(word, collect("\$((" * inner * "))")); started = true + i = (j <= n && cs[j] == ')') ? j+1 : j; continue + end + inner, j = read_balanced(cs, i+2, '(', ')') + push!(feats, "cmdsub_\$()"); push!(subs, inner) + append!(word, collect("\$(" * inner * ")")); started = true; i = j; continue + end + if nx == '{' + inner, j = read_balanced(cs, i+2, '{', '}') + push!(feats, "paramexp_\${}"); append!(word, collect("\${" * inner * "}")); started = true; i = j; continue + end + if nx == '\'' + k = i+2; while k <= n && cs[k] != '\''; cs[k] == '\\' ? (k += 2) : (k += 1); end + kk = min(k, n); push!(feats, "ansi_c_quote"); append!(word, @view cs[i:kk]); started = true; i = kk+1; continue + end + push!(feats, "var_\$"); push!(word, '$'); started = true; i += 1; continue + end + if c == '`' + k = i+1; while k <= n && cs[k] != '`'; cs[k] == '\\' ? (k += 2) : (k += 1); end + kk = min(k, n); push!(feats, "cmdsub_backtick"); push!(subs, String(@view cs[i+1:min(kk-1,n)])) + append!(word, @view cs[i:kk]); started = true; i = kk+1; continue + end + if c in ('|','&',';','<','>','(',')') + flush!() + if (c == '<' || c == '>') && i+1 <= n && cs[i+1] == '(' + inner, j = read_balanced(cs, i+2, '(', ')') + push!(feats, c == '<' ? "procsub_<()" : "procsub_>()"); push!(subs, inner) + push!(toks, Tok(:W, string(c) * "(" * inner * ")")); i = j; continue + end + three = String(@view cs[i:min(i+2,n)]); two = String(@view cs[i:min(i+1,n)]) + if three == "<<<"; push!(toks, Tok(:O, "<<<")); i += 3; continue; end + if two == "<<" + op = three == "<<-" ? "<<-" : "<<" + push!(toks, Tok(:O, op)); expect_delim = (op == "<<-"); i += length(op); continue + end + if three == "&>>"; push!(toks, Tok(:O, "&>>")); i += 3; continue; end + if two in ("&&","||",";;","|&",">>","<&",">&","&>","<>"); push!(toks, Tok(:O, two)); i += 2; continue; end + push!(toks, Tok(:O, string(c))); i += 1; continue + end + push!(word, c); started = true; i += 1 + end + flush!() + return toks, feats, subs +end + +# ─────────────────────────── analyzer ─────────────────────────── +const KW = Set(["if","then","else","elif","fi","for","while","until","do","done", + "case","esac","select","function","in","time","coproc"]) +const WRAPPERS = Set(["sudo","env","timeout","command","builtin","exec","nohup","nice", + "ionice","stdbuf","setsid","doas","xargs","watch"]) +const REDIRS = Set(["<",">",">>","<<","<<-","<<<","<&",">&","&>","&>>","<>"]) +const OPC = Dict( + "|"=>"pipe |","|&"=>"pipe |&","&&"=>"and &&","||"=>"or ||",";"=>"seq ;",";;"=>"case-clause ;;", + "&"=>"background &",">"=>"redir >",">>"=>"redir-append >>","<"=>"redir-in <","<&"=>"fd-dup <&", + ">&"=>"fd-dup >&","&>"=>"redir-both &>","&>>"=>"redir-both-append &>>","<>"=>"redir-rw <>", + "<<<"=>"herestring <<<","<<"=>"heredoc <<","<<-"=>"heredoc <<-") +const FEATC = Dict( + "cmdsub_\$()"=>"cmdsub \$()","cmdsub_backtick"=>"cmdsub ``","arith_\$(())"=>"arith-exp \$(())", + "paramexp_\${}"=>"paramexp \${}","procsub_<()"=>"procsub <()","procsub_>()"=>"procsub >()", + "var_\$"=>"var \$x","var_in_dq"=>"var-in-dquotes","ansi_c_quote"=>"ansi-c \$'..'") + +inc!(d, k) = (d[k] = get(d, k, 0) + 1) + +function normutil(w::AbstractString) + isempty(w) && return nothing + (startswith(w,"\$")||startswith(w,"`")||occursin("\$(",w)||occursin("\${",w)|| + startswith(w,"\"")||startswith(w,"'")) && return "" + base = String(last(split(w, '/'))) + isempty(base) ? nothing : base +end +function recordflag!(flags, util, w) + util == "" && return + f = w; eq = findfirst('=', f); eq !== nothing && (f = f[1:prevind(f,eq)]) + (isempty(f) || f == "-" || f == "--") && return + d = get!(flags, util, Dict{String,Int}()); d[f] = get(d,f,0)+1 +end +function dwc!(w, constructs) + u = replace(w, r"'[^']*'"=>"", "\""=>"") + startswith(w, "~") && inc!(constructs, "tilde ~") + (occursin(r"\{[^{}]*\.\.[^{}]*\}", u) || occursin(r"\{[^{}]*,[^{}]*\}", u)) && inc!(constructs, "brace-expansion {}") + (occursin('*', u) || occursin('?', u) || occursin(r"\[[^]]+\]", u)) && inc!(constructs, "glob */?/[]") +end +function unquote_script(w) # strip outer quotes of a `bash -c '