From 4b3e057efe01ac22aa0c52b1cdb494a14e876466 Mon Sep 17 00:00:00 2001 From: Gon Date: Wed, 19 Aug 2026 20:12:43 +0700 Subject: [PATCH] perf: replace sed subprocess with bash-native SQL literal escaping sed "s/'/''/g" spawns a process to double a single quote for SQL literal embedding - bash's own ${var//pattern/replacement} does the same transform without spawning. On Windows/Git Bash a single sed call costs ~221ms vs ~12ms for the bash-native equivalent (measured, 20-run average). Swaps the 8 call sites on the ordinary local send/inbox/history/watch path (drivers/storage/sqlite.sh _sqlite_lit, send.sh roster-check escape, inbox.sh/history.sh(x2)/watch.sh/check-inbox.sh JSONL-array escape, lib/sqlpath.sh agmsg_sql_readfile_path). Does not touch the remote-sync path (#780/#799 already fixed that one). Measured end-to-end on a real two-agent team, same machine: send.sh: 10.5-11.8s -> 6.4-7.6s full send-to-ack round trip: 35-123s -> ~20s Verified byte-identical to the sed version against 19 edge cases (Thai text, unicode, embedded quotes/newlines/tabs, SQL injection attempt) before patching, and live-verified with a real message containing an apostrophe + Thai text round-tripping correctly between two real agents. See https://github.com/fujibee/agmsg/issues/896 --- scripts/check-inbox.sh | 2 +- scripts/drivers/storage/sqlite.sh | 2 +- scripts/history.sh | 4 ++-- scripts/inbox.sh | 2 +- scripts/lib/sqlpath.sh | 2 +- scripts/send.sh | 2 +- scripts/watch.sh | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/check-inbox.sh b/scripts/check-inbox.sh index d13476f39..b19f4d6ca 100755 --- a/scripts/check-inbox.sh +++ b/scripts/check-inbox.sh @@ -238,7 +238,7 @@ for team in "${TEAM_LIST[@]}"; do replace(replace(json_extract(value,'\$.body'), char(10), '\n'), char(9), '\t') || char(31) || json_extract(value,'\$.at') || char(31) || json_extract(value,'\$.id') - FROM json_each('$(printf '%s' "$_arr" | sed "s/'/''/g")'); + FROM json_each('${_arr//\'/\'\'}'); " ) _rc=$? diff --git a/scripts/drivers/storage/sqlite.sh b/scripts/drivers/storage/sqlite.sh index a3ee3e46b..68f5f5772 100755 --- a/scripts/drivers/storage/sqlite.sh +++ b/scripts/drivers/storage/sqlite.sh @@ -23,7 +23,7 @@ _sqlite_now() { date -u +%Y-%m-%dT%H:%M:%SZ; } # than held in a driver-wide variable: these run inside command substitutions, # where an assignment made by a caller would not be visible anyway. _sqlite_db() { agmsg_db_path "$1"; } -_sqlite_lit() { printf '%s' "$1" | sed "s/'/''/g"; } +_sqlite_lit() { local s="$1" q="'"; printf '%s' "${s//$q/$q$q}"; } # Run a record-returning query: strip CR but PRESERVE the sqlite exit status # (pipefail), so a backend failure surfaces as a non-zero return instead of diff --git a/scripts/history.sh b/scripts/history.sh index 7d4040e3c..122cff212 100755 --- a/scripts/history.sh +++ b/scripts/history.sh @@ -44,7 +44,7 @@ ROWS=$(agmsg_sqlite ':memory:' " replace(replace(json_extract(value,'\$.body'), char(10), '\n'), char(9), '\t') || char(31) || json_extract(value,'\$.at') || char(31) || json_extract(value,'\$.id') - FROM json_each('$(printf '%s' "$_arr" | sed "s/'/''/g")'); + FROM json_each('${_arr//\'/\'\'}'); ") # Read-state for the ●(unread)/○(read) marker (G2(c)): read-state is @@ -62,7 +62,7 @@ while IFS= read -r r; do [ -n "$u" ] || continue uarr="[$(printf '%s' "$u" | paste -sd, -)]" ids=$(agmsg_sqlite ':memory:' " - SELECT json_extract(value,'\$.id') FROM json_each('$(printf '%s' "$uarr" | sed "s/'/''/g")'); + SELECT json_extract(value,'\$.id') FROM json_each('${uarr//\'/\'\'}'); ") UNREAD_IDS+="$ids"$'\n' done <<< "$RECIPIENTS" diff --git a/scripts/inbox.sh b/scripts/inbox.sh index c4c740f72..5509cab40 100755 --- a/scripts/inbox.sh +++ b/scripts/inbox.sh @@ -48,7 +48,7 @@ ROWS=$(agmsg_sqlite ':memory:' " replace(replace(json_extract(value,'\$.body'), char(10), '\n'), char(9), '\t') || char(31) || json_extract(value,'\$.at') || char(31) || json_extract(value,'\$.id') - FROM json_each('$(printf '%s' "$_arr" | sed "s/'/''/g")'); + FROM json_each('${_arr//\'/\'\'}'); ") COUNT=$(printf '%s\n' "$ROWS" | wc -l | tr -d ' ') diff --git a/scripts/lib/sqlpath.sh b/scripts/lib/sqlpath.sh index 047850ef4..e4e7754b9 100644 --- a/scripts/lib/sqlpath.sh +++ b/scripts/lib/sqlpath.sh @@ -29,7 +29,7 @@ agmsg_sql_readfile_path() { if command -v cygpath >/dev/null 2>&1; then path=$(cygpath -w "$path" 2>/dev/null || printf '%s' "$path") fi - printf '%s' "$path" | sed "s/'/''/g" + printf '%s' "${path//\'/\'\'}" } # True when sqlite can actually open . diff --git a/scripts/send.sh b/scripts/send.sh index 4b4cfd5ac..762475d77 100755 --- a/scripts/send.sh +++ b/scripts/send.sh @@ -47,7 +47,7 @@ if [ "$FORCE" -ne 1 ]; then fi local cfg_sql name_sql found roster cfg_sql=$(agmsg_sql_readfile_path "$TEAM_CONFIG") - name_sql=$(printf '%s' "$name" | sed "s/'/''/g") + name_sql="${name//\'/\'\'}" found=$(agmsg_sqlite_mem " WITH raw(json) AS (SELECT CAST(readfile('$cfg_sql') AS TEXT)), cfg(json) AS (SELECT CASE WHEN json_valid(json) THEN json END FROM raw) diff --git a/scripts/watch.sh b/scripts/watch.sh index 55b917659..e599f2b6e 100755 --- a/scripts/watch.sh +++ b/scripts/watch.sh @@ -702,7 +702,7 @@ while true; do COALESCE(json_extract(value,'\$.to'),'') || char(31) || replace(replace(replace(COALESCE(json_extract(value,'\$.body'),''), char(13), ''), char(10), '\\n'), char(9), '\t') || char(31) || COALESCE(json_extract(value,'\$.cursor'),'') - FROM json_each('$(printf '%s' "$_arr" | sed "s/'/''/g")'); + FROM json_each('${_arr//\'/\'\'}'); " 2>/dev/null || true)" FINAL_CURSOR=""