No error is a dead end: crash, click, and your agent is already looking
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
+77
-7
@@ -16,10 +16,18 @@
|
||||
# few minutes for something the user can do nothing about. The first one is
|
||||
# news; the fortieth is why people turn notifications off. The health page
|
||||
# carries the running count for anyone who wants it.
|
||||
#
|
||||
# When an agent has been chosen, the notification stops being a dead end. It
|
||||
# carries the diagnosis command as data in a `panama-exec` hint, which the shell
|
||||
# runs on click. Command-as-data rather than a libnotify action, because an
|
||||
# action would tie the click to this process still being alive to hear it, and
|
||||
# this process is a `journalctl -f` that outlives nothing in particular. The
|
||||
# hint survives a shell restart and never blocks the watcher.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
SETTINGS="${PANAMA_AGENT_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}"
|
||||
|
||||
# systemd-coredump's MESSAGE_ID. Matching on this rather than on text keeps
|
||||
# working when the wording changes and never matches a program that merely
|
||||
@@ -37,6 +45,27 @@ for _ in $(seq 1 60); do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Read per crash rather than once at startup, so choosing an agent in Settings
|
||||
# takes effect on the next crash instead of on the next login. This service runs
|
||||
# for the life of the session; nothing restarts it when a preference changes.
|
||||
read_setting() {
|
||||
local key="$1" fallback="$2"
|
||||
[[ -r "$SETTINGS" ]] || { printf '%s' "$fallback"; return; }
|
||||
command -v jq >/dev/null 2>&1 || { printf '%s' "$fallback"; return; }
|
||||
jq -r --arg k "$key" --arg d "$fallback" \
|
||||
'if has($k) and (.[$k] != null) then (.[$k] | tostring) else $d end' \
|
||||
"$SETTINGS" 2>/dev/null || printf '%s' "$fallback"
|
||||
}
|
||||
|
||||
# What to call the agent in a sentence aimed at a person.
|
||||
agent_label() {
|
||||
case "$1" in
|
||||
claude) printf 'Claude Code' ;;
|
||||
codex) printf 'Codex' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -A reported=()
|
||||
|
||||
# -f from now, not from the boot: a session that starts after a crash should
|
||||
@@ -46,9 +75,15 @@ journalctl --user -f -n 0 --output=json MESSAGE_ID="$COREDUMP_MESSAGE_ID" 2>/dev
|
||||
| while IFS= read -r line; do
|
||||
[[ -n "$line" ]] || continue
|
||||
|
||||
uid="$(jq -r '.COREDUMP_UID // empty' <<<"$line" 2>/dev/null)"
|
||||
exe="$(jq -r '.COREDUMP_EXE // empty' <<<"$line" 2>/dev/null)"
|
||||
comm="$(jq -r '.COREDUMP_COMM // empty' <<<"$line" 2>/dev/null)"
|
||||
# One jq per entry rather than one per field: the fields are read
|
||||
# together, and the click payload needs all of them.
|
||||
IFS=$'\t' read -r uid exe comm pid signal < <(
|
||||
jq -r '[(.COREDUMP_UID // ""),
|
||||
(.COREDUMP_EXE // ""),
|
||||
(.COREDUMP_COMM // ""),
|
||||
(.COREDUMP_PID // ""),
|
||||
(.COREDUMP_SIGNAL_NAME // "")] | @tsv' <<<"$line" 2>/dev/null
|
||||
)
|
||||
|
||||
# Another user's crash is not this session's business, and reporting it
|
||||
# would leak what they are running.
|
||||
@@ -63,11 +98,46 @@ journalctl --user -f -n 0 --output=json MESSAGE_ID="$COREDUMP_MESSAGE_ID" 2>/dev
|
||||
else
|
||||
program="$comm"
|
||||
fi
|
||||
|
||||
# Never announce our own machinery. A crash watcher that notifies about
|
||||
# the crash watcher, or about the agent it just launched to investigate
|
||||
# the last crash, is a loop with a toast in it.
|
||||
[[ "$program" == panama-crash-* || "$program" == panama-agent* ]] && continue
|
||||
|
||||
[[ -z "${reported[$program]:-}" ]] || continue
|
||||
reported[$program]=1
|
||||
|
||||
notify-send --icon=dialog-error-symbolic --app-name=Panama \
|
||||
"$program stopped unexpectedly" \
|
||||
"It crashed and was not able to recover. System Health has the details." \
|
||||
2>/dev/null || true
|
||||
# The toast can only offer a diagnosis if there is something to diagnose
|
||||
# with. No agent, or the offer switched off, and it stays exactly the
|
||||
# actionless notification it has always been.
|
||||
agent="$(read_setting preferredAgent none)"
|
||||
offer="$(read_setting crashDiagnoseOffer true)"
|
||||
|
||||
if [[ -n "$agent" && "$agent" != "none" && "$offer" != "false" && "$pid" =~ ^[0-9]+$ ]]; then
|
||||
# By absolute path, not by name. The shell runs this hint, and the
|
||||
# shell is started by systemd -- whose environment does not carry
|
||||
# the repository's bin directory on PATH, so a bare name would
|
||||
# click into "command not found".
|
||||
exec_command="$(printf '%q %q %q %q %q' \
|
||||
"$PANAMA_PATH/bin/panama-agent-crash" \
|
||||
"$pid" "${comm:-$program}" "${exe:-unknown}" "${signal:-unknown}")"
|
||||
|
||||
# The hint is data, not privilege. Any process on this session bus
|
||||
# could send one, and running it grants nothing a local process
|
||||
# could not already do for itself.
|
||||
# Same urgency as the plain report, deliberately. Making the
|
||||
# clickable one critical would let a crash break through Do Not
|
||||
# Disturb, which is a louder desktop than anybody asked for in
|
||||
# exchange for an offer that keeps until it is read anyway.
|
||||
notify-send --icon=dialog-error-symbolic --app-name=Panama \
|
||||
--hint="string:panama-exec:$exec_command" \
|
||||
"$program stopped unexpectedly" \
|
||||
"Click to diagnose with $(agent_label "$agent")." \
|
||||
2>/dev/null || true
|
||||
else
|
||||
notify-send --icon=dialog-error-symbolic --app-name=Panama \
|
||||
"$program stopped unexpectedly" \
|
||||
"It crashed and was not able to recover. System Health has the details." \
|
||||
2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user