fix(lib.sh): line breaks in a name become '-' like Python's; load_state_json strict (Issue #71)

- The ASCII branch of _sanitize_herdr_agent_name translates line breaks
  before sed (line by line) and $(...) (drops a trailing one) can lose
  them: "newline-creator-claude\n" was started as newline-creator-claude
  while its attach_command named newline-creator-claude-.
- `load_state_json strict` exits 1 (stderr) when the YAML or DB cannot be
  read instead of printing {}; without the argument nothing changes.

Refs #71

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-25 19:31:10 +00:00
1 parent a6fb39ba9f
commit 0fc162e8cd
1 file changed
+18 -9
+18 -9
View File
@@ -39,7 +39,8 @@ AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.
# shasum, sha1sum, openssl or python3 to make it, this fails (stderr, nonzero) # shasum, sha1sum, openssl or python3 to make it, this fails (stderr, nonzero)
# rather than return a name herdr never registered. A name that is not ASCII # rather than return a name herdr never registered. A name that is not ASCII
# is lowercased and replaced by python3 (tr/sed differ from str.lower() there), # is lowercased and replaced by python3 (tr/sed differ from str.lower() there),
# and fails the same way without it. # and fails the same way without it. Line breaks become '-' before sed (which
# works line by line) and before $(...) could drop a trailing one.
_sanitize_herdr_agent_name() { _sanitize_herdr_agent_name() {
local n="$1" local n="$1"
if [ -z "$n" ]; then if [ -z "$n" ]; then
@@ -48,7 +49,7 @@ _sanitize_herdr_agent_name() {
fi fi
local s="" local s=""
if [ -z "$(printf '%s' "$n" | LC_ALL=C tr -d '\000-\177')" ]; then if [ -z "$(printf '%s' "$n" | LC_ALL=C tr -d '\000-\177')" ]; then
s=$(printf '%s' "$n" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g') s=$(printf '%s' "$n" | tr '\n' '-' | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g')
elif command -v python3 >/dev/null 2>&1; then elif command -v python3 >/dev/null 2>&1; then
s=$(python3 -c 'import re,sys; sys.stdout.write(re.sub(r"[^a-z0-9_-]", "-", sys.argv[1].lower()))' "$n") || s="" s=$(python3 -c 'import re,sys; sys.stdout.write(re.sub(r"[^a-z0-9_-]", "-", sys.argv[1].lower()))' "$n") || s=""
fi fi
@@ -1590,10 +1591,14 @@ herdr() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# NOTE: Retained as small inline PYEOF block (39 lines) per Plan Rev.2 (Job 98393a97). # NOTE: Retained as small inline PYEOF block (39 lines) per Plan Rev.2 (Job 98393a97).
# Small blocks have minimal overhead and high local cohesion; extraction into lib_py is reserved for large blocks (>200 lines). # Small blocks have minimal overhead and high local cohesion; extraction into lib_py is reserved for large blocks (>200 lines).
# `load_state_json strict` (Issue #71) fails (stderr, exit 1) when the YAML or
# DB cannot be read, instead of printing {} - for a caller that acts on a
# session being absent.
load_state_json() { load_state_json() {
env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' env_python "$AGENT_SESSIONS_YAML" "MAM_STATE_READ=${1:-}" <<'PYEOF'
import os, sys, sqlite3, json, yaml import os, sys, sqlite3, json, yaml
yaml_path = os.environ['YAML_PATH'] yaml_path = os.environ['YAML_PATH']
strict = os.environ.get('MAM_STATE_READ') == 'strict'
db_path = os.path.splitext(yaml_path)[0] + '.db' db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {} d = {}
db_sessions = [] db_sessions = []
@@ -1605,21 +1610,25 @@ try:
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
if row: if row:
d = json.loads(row[0]) d = json.loads(row[0])
except sqlite3.OperationalError: except sqlite3.OperationalError as e:
pass if strict and 'no such table' not in str(e):
raise
try: try:
cursor = conn.execute('SELECT data FROM sessions') cursor = conn.execute('SELECT data FROM sessions')
for r in cursor.fetchall(): for r in cursor.fetchall():
db_sessions.append(json.loads(r[0])) db_sessions.append(json.loads(r[0]))
d['herdr_sessions'] = db_sessions d['herdr_sessions'] = db_sessions
except sqlite3.OperationalError: except sqlite3.OperationalError as e:
pass if strict and 'no such table' not in str(e):
raise
conn.close() conn.close()
elif os.path.exists(yaml_path): elif os.path.exists(yaml_path):
with open(yaml_path) as f: with open(yaml_path) as f:
d = yaml.safe_load(f) or {} d = yaml.safe_load(f) or {}
except Exception: except Exception as e:
pass if strict:
print('ERROR: cannot read the registry %s: %s' % (yaml_path, e), file=sys.stderr)
sys.exit(1)
# Clean lone surrogates to prevent UnicodeEncodeError on stdout print # Clean lone surrogates to prevent UnicodeEncodeError on stdout print
def clean_surrogates(obj): def clean_surrogates(obj):
if isinstance(obj, str): if isinstance(obj, str):