@rulemetric/hooks 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/_post.sh +70 -2
- package/scripts/post-retry.test.sh +16 -1
- package/scripts/session-end.sh +24 -12
package/package.json
CHANGED
package/scripts/_post.sh
CHANGED
|
@@ -39,10 +39,13 @@ _rulemetric_post_event() {
|
|
|
39
39
|
http_code=$(printf '%s' "$body" | tail -1)
|
|
40
40
|
body=$(printf '%s' "$body" | sed '$d')
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
# Any 2xx is success: the batched events route (Phase 2 admission control)
|
|
43
|
+
# returns 202 "buffered", the legacy path 200, session create 201.
|
|
44
|
+
case "$http_code" in 2??)
|
|
43
45
|
[ -n "$hdr_file" ] && rm -f "$hdr_file"
|
|
44
46
|
return 0
|
|
45
|
-
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
46
49
|
|
|
47
50
|
# Transient (service busy / rate-limited / network) → retry unless this was
|
|
48
51
|
# the last allowed attempt. Everything else (4xx) is permanent.
|
|
@@ -73,3 +76,68 @@ _rulemetric_post_event() {
|
|
|
73
76
|
[ -n "$hdr_file" ] && rm -f "$hdr_file"
|
|
74
77
|
return 1
|
|
75
78
|
}
|
|
79
|
+
|
|
80
|
+
# _rulemetric_send <method> <url> <auth_header> <payload> <log_tag> [max_time_s]
|
|
81
|
+
# Method-aware sibling of _rulemetric_post_event for the session-END writes that
|
|
82
|
+
# previously used a bare `curl -sf` and SILENTLY DROPPED on any failure — enrich
|
|
83
|
+
# (PATCH), reimport (POST), derive-enrichment (POST). When the API fast-503s under
|
|
84
|
+
# transient DB-pool load, those calls dropped, leaving sessions un-enriched / empty
|
|
85
|
+
# (the "insights stuck at 06/30" cause). Same bounded 503/429/network retry +
|
|
86
|
+
# capped Retry-After as _rulemetric_post_event; 4xx permanent; logs final failure.
|
|
87
|
+
# Returns 0 on 2xx. `max_time_s` defaults to 4 (reimport passes 8 for large bodies).
|
|
88
|
+
# `max_attempts` defaults to $_RM_POST_MAX_ATTEMPTS; the FOREGROUND reimport passes
|
|
89
|
+
# 2 to cap its worst-case session-end tail-latency (it blocks the hook — see below).
|
|
90
|
+
_rulemetric_send() {
|
|
91
|
+
local method="$1" url="$2" auth="$3" payload="$4" tag="$5" max_time="${6:-4}" max_attempts="${7:-$_RM_POST_MAX_ATTEMPTS}"
|
|
92
|
+
local attempt=1 http_code="000" body="" hdr_file retry_after sleep_s
|
|
93
|
+
|
|
94
|
+
hdr_file=$(mktemp "${TMPDIR:-/tmp}/rm-send.XXXXXX" 2>/dev/null || true)
|
|
95
|
+
|
|
96
|
+
while [ "$attempt" -le "$max_attempts" ]; do
|
|
97
|
+
if [ -n "$hdr_file" ]; then
|
|
98
|
+
body=$(curl -s --noproxy '*' --max-time "$max_time" -D "$hdr_file" -w '\n%{http_code}' \
|
|
99
|
+
-X "$method" "$url" -H "$auth" -H "Content-Type: application/json" \
|
|
100
|
+
-d "$payload" 2>&1) || true
|
|
101
|
+
else
|
|
102
|
+
body=$(curl -s --noproxy '*' --max-time "$max_time" -w '\n%{http_code}' \
|
|
103
|
+
-X "$method" "$url" -H "$auth" -H "Content-Type: application/json" \
|
|
104
|
+
-d "$payload" 2>&1) || true
|
|
105
|
+
fi
|
|
106
|
+
http_code=$(printf '%s' "$body" | tail -1)
|
|
107
|
+
body=$(printf '%s' "$body" | sed '$d')
|
|
108
|
+
|
|
109
|
+
# Any 2xx is success: the batched events route (Phase 2 admission control)
|
|
110
|
+
# returns 202 "buffered", the legacy path 200, session create 201.
|
|
111
|
+
case "$http_code" in 2??)
|
|
112
|
+
[ -n "$hdr_file" ] && rm -f "$hdr_file"
|
|
113
|
+
return 0
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
|
|
117
|
+
if [ "$http_code" = "503" ] || [ "$http_code" = "429" ] || [ "$http_code" = "000" ]; then
|
|
118
|
+
if [ "$attempt" -lt "$max_attempts" ]; then
|
|
119
|
+
retry_after=""
|
|
120
|
+
if [ -n "$hdr_file" ]; then
|
|
121
|
+
retry_after=$(grep -i '^Retry-After:' "$hdr_file" 2>/dev/null | tail -1 | tr -dc '0-9' || true)
|
|
122
|
+
fi
|
|
123
|
+
if [ -n "$retry_after" ] && [ "$retry_after" -gt 0 ] 2>/dev/null; then
|
|
124
|
+
sleep_s="$retry_after"
|
|
125
|
+
else
|
|
126
|
+
sleep_s="$attempt"
|
|
127
|
+
fi
|
|
128
|
+
if [ "$sleep_s" -gt "$_RM_POST_RETRY_CAP_S" ] 2>/dev/null; then
|
|
129
|
+
sleep_s="$_RM_POST_RETRY_CAP_S"
|
|
130
|
+
fi
|
|
131
|
+
sleep "$sleep_s" 2>/dev/null || true
|
|
132
|
+
attempt=$((attempt + 1))
|
|
133
|
+
continue
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
break
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
_rulemetric_log "$tag" "error" "http_${http_code}_attempt_${attempt}: ${body}"
|
|
141
|
+
[ -n "$hdr_file" ] && rm -f "$hdr_file"
|
|
142
|
+
return 1
|
|
143
|
+
}
|
|
@@ -32,7 +32,7 @@ trap cleanup EXIT
|
|
|
32
32
|
# --- Stub server: counts requests, programmed by $MODE in a control file --------
|
|
33
33
|
cat > "$WORK_ROOT/stub.py" <<'PY'
|
|
34
34
|
import http.server, json, os, sys, threading
|
|
35
|
-
mode = sys.argv[1] # fail-once | always-503 | always-400
|
|
35
|
+
mode = sys.argv[1] # fail-once | always-503 | always-400 | always-202
|
|
36
36
|
count_file = sys.argv[2]
|
|
37
37
|
n = 0
|
|
38
38
|
lock = threading.Lock()
|
|
@@ -50,6 +50,9 @@ class H(http.server.BaseHTTPRequestHandler):
|
|
|
50
50
|
if mode == "always-503":
|
|
51
51
|
self.send_response(503); self.send_header("Retry-After", "1"); self.end_headers()
|
|
52
52
|
self.wfile.write(b'{"error":"busy"}'); return
|
|
53
|
+
if mode == "always-202":
|
|
54
|
+
self.send_response(202); self.end_headers()
|
|
55
|
+
self.wfile.write(b'{"buffered":true,"count":1,"durable":false}'); return
|
|
53
56
|
# fail-once: 503 on first request, 200 thereafter
|
|
54
57
|
if cur == 1:
|
|
55
58
|
self.send_response(503); self.send_header("Retry-After", "1"); self.end_headers()
|
|
@@ -112,6 +115,18 @@ assert_eq "always-503: server saw exactly MAX_ATTEMPTS (3) requests" "3" "$(req_
|
|
|
112
115
|
grep -q "http_503" "$LOG_FILE" && ok "always-503: logged an http_503 error" || fail "always-503: expected http_503 in log"
|
|
113
116
|
stop_server
|
|
114
117
|
|
|
118
|
+
# --- Test 4: batched-ingest 202 is SUCCESS (no retry, no error log) -----------
|
|
119
|
+
# The Phase 2 batched events route returns 202 { buffered: true } instead of
|
|
120
|
+
# 200 { recorded: true }; the helper must treat ANY 2xx as delivered.
|
|
121
|
+
start_server always-202
|
|
122
|
+
: > "$LOG_FILE"
|
|
123
|
+
_rulemetric_post_event "http://127.0.0.1:$SERVER_PORT/events" "$AUTH" "$PAYLOAD" "test" 4
|
|
124
|
+
rc=$?
|
|
125
|
+
assert_eq "always-202: returns success (0) — buffered counts as delivered" "0" "$rc"
|
|
126
|
+
assert_eq "always-202: server saw exactly 1 request (no retry)" "1" "$(req_count)"
|
|
127
|
+
assert_eq "always-202: no error logged" "0" "$(wc -l < "$LOG_FILE" | tr -d ' ')"
|
|
128
|
+
stop_server
|
|
129
|
+
|
|
115
130
|
# --- Summary -----------------------------------------------------------------
|
|
116
131
|
echo
|
|
117
132
|
echo "Passed: $PASSES Failed: $FAILURES"
|
package/scripts/session-end.sh
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
# RuleMetric: Finalize session with full transcript import
|
|
3
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
4
|
source "$SCRIPT_DIR/_log.sh" 2>/dev/null || _rulemetric_log() { :; }
|
|
5
|
+
# Bounded retry-on-503 for the session-end writes (reimport/enrich/derive) so a
|
|
6
|
+
# transient DB-pool wedge doesn't SILENTLY drop them (the "insights stuck" cause).
|
|
7
|
+
# Log if the helper can't load (packaging regression) — otherwise the writes below
|
|
8
|
+
# would hit `command not found` and degrade silently (backgrounded ones with NO log).
|
|
9
|
+
source "$SCRIPT_DIR/_post.sh" 2>/dev/null || _rulemetric_log "session-end" "warn" "post_helper_unavailable"
|
|
5
10
|
|
|
6
11
|
set -euo pipefail
|
|
7
12
|
trap 'echo "rulemetric session-end failed at line $LINENO: $BASH_COMMAND" >&2; _rulemetric_log "session-end" "error" "line $LINENO: $BASH_COMMAND"; exit 1' ERR
|
|
@@ -41,10 +46,19 @@ fi
|
|
|
41
46
|
# Try to reimport with full transcript, fall back to close
|
|
42
47
|
if [ -n "$HOOK_TRANSCRIPT_PATH" ] && [ -f "$HOOK_TRANSCRIPT_PATH" ]; then
|
|
43
48
|
CONTENT=$(cat "$HOOK_TRANSCRIPT_PATH")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
REIMPORT_PAYLOAD="{\"content\":$(echo "$CONTENT" | jq -Rs .),\"tool\":\"$HOOK_TOOL\"}"
|
|
50
|
+
# Retry-on-503 (max-time 8 for the large transcript body); this is FOREGROUND and
|
|
51
|
+
# blocks the hook, so cap at 2 attempts (worst-case tail ~18s on a hard API hang,
|
|
52
|
+
# vs ~32s at 3) — the common fast-503 wedge still recovers in <1s. Fall back to
|
|
53
|
+
# close if it still fails after retries — a dropped reimport leaves the session EMPTY.
|
|
54
|
+
# NOTE: under headless `claude -p`, Claude Code's print-mode teardown cancels this
|
|
55
|
+
# hook ("SessionEnd hook … failed: Hook cancelled") regardless of whether this runs
|
|
56
|
+
# in the foreground or is backgrounded — verified 2026-07-05. It is a Claude Code
|
|
57
|
+
# print-mode behavior, not a capture failure (per-turn hooks still land events); do
|
|
58
|
+
# NOT try to "fix" it by detaching this call (adds no benefit, and headless is a
|
|
59
|
+
# deliberately-excluded capture vehicle).
|
|
60
|
+
_rulemetric_send POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/reimport" \
|
|
61
|
+
"$AUTH_HEADER" "$REIMPORT_PAYLOAD" "session-end-reimport" 8 2 \
|
|
48
62
|
> /dev/null 2>&1 || {
|
|
49
63
|
_rulemetric_log "session-end" "fallback" "reimport_failed"
|
|
50
64
|
curl -sf --noproxy '*' --max-time 4 -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/close" \
|
|
@@ -127,18 +141,16 @@ except:
|
|
|
127
141
|
" 2>/dev/null)
|
|
128
142
|
|
|
129
143
|
if [ -n "$ENRICH_PAYLOAD" ]; then
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
# Backgrounded retry-on-503 so a transient wedge doesn't drop enrichment
|
|
145
|
+
# (leaving insights stuck at the last-enriched session).
|
|
146
|
+
(_rulemetric_send PATCH "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/enrich" \
|
|
147
|
+
"$AUTH_HEADER" "$ENRICH_PAYLOAD" "session-end-enrich" > /dev/null 2>&1) &
|
|
134
148
|
fi
|
|
135
149
|
fi
|
|
136
150
|
else
|
|
137
151
|
# Non-Claude tools: derive enrichment from stored session events
|
|
138
|
-
(
|
|
139
|
-
|
|
140
|
-
-H "Content-Type: application/json" \
|
|
141
|
-
-d '{}' > /dev/null 2>&1) &
|
|
152
|
+
(_rulemetric_send POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/derive-enrichment" \
|
|
153
|
+
"$AUTH_HEADER" '{}' "session-end-derive" 8 > /dev/null 2>&1) &
|
|
142
154
|
fi
|
|
143
155
|
|
|
144
156
|
# ── Tail transcript for 429 rate-limit events ──
|