diff --git a/scripts/amp-helper.sh b/scripts/amp-helper.sh index b5f83eb..2727723 100755 --- a/scripts/amp-helper.sh +++ b/scripts/amp-helper.sh @@ -327,6 +327,25 @@ AMP_MAX_TOTAL_ATTACHMENT_SIZE="${AMP_MAX_TOTAL_ATTACHMENT_SIZE:-104857600}" # 1 # AI Maestro connection AMP_MAESTRO_URL="${AMP_MAESTRO_URL:-http://localhost:23000}" +# Tell a local AI Maestro that a message just landed in a same-host recipient's +# inbox, so it wakes the agent now instead of at its next inbox sweep. The inbox +# file is already written and stays the source of truth: this is a best-effort +# nudge. It never prints, never fails, and never waits (backgrounded, 3 s cap), +# so with no server running, or an older one, sending behaves exactly as before. +# Opt out with AMP_DOORBELL=0. +amp_ring_doorbell() { + local recipient="$1" msg_id="$2" body + [ "${AMP_DOORBELL:-1}" = "0" ] && return 0 + [ -n "$recipient" ] && [ -n "$msg_id" ] || return 0 + command -v curl >/dev/null 2>&1 || return 0 + command -v jq >/dev/null 2>&1 || return 0 + body=$(jq -nc --arg r "$recipient" --arg m "$msg_id" '{recipient:$r,messageId:$m}' 2>/dev/null) || return 0 + ( curl -s -m 3 --connect-timeout 1 -o /dev/null -X POST \ + "${AMP_MAESTRO_URL}/api/messages/doorbell" \ + -H 'Content-Type: application/json' -d "$body" >/dev/null 2>&1 & ) >/dev/null 2>&1 + return 0 +} + # Provider domain (AMP v1) AMP_PROVIDER_DOMAIN="${AMP_PROVIDER_DOMAIN:-aimaestro.local}" AMP_LOCAL_DOMAIN="${AMP_PROVIDER_DOMAIN}" diff --git a/scripts/amp-send.sh b/scripts/amp-send.sh index 6575df9..7f57b55 100755 --- a/scripts/amp-send.sh +++ b/scripts/amp-send.sh @@ -590,6 +590,10 @@ if [ "$ROUTE" = "local" ]; then echo "$DELIVERY_MSG" > "${RECIPIENT_INBOX}/${SENDER_DIR}/${MSG_ID}.json" + # Wake the recipient now if a local AI Maestro is running (best effort, silent) + + amp_ring_doorbell "${RECIPIENT_UUID:-$ADDR_NAME}" "$MSG_ID" + echo "✅ Message sent (local filesystem delivery)" echo "" echo " To: ${FULL_RECIPIENT}" @@ -743,6 +747,10 @@ if [ "$ROUTE" = "local" ]; then echo "$DELIVERY_MSG" > "${RECIPIENT_INBOX}/${SENDER_DIR}/${MSG_ID}.json" + # Wake the recipient now if a local AI Maestro is running (best effort, silent) + + amp_ring_doorbell "${RECIPIENT_UUID:-$ADDR_NAME}" "$MSG_ID" + echo "✅ Message sent (local filesystem delivery)" echo "" echo " To: ${FULL_RECIPIENT}" diff --git a/tests/unit/send_doorbell.bats b/tests/unit/send_doorbell.bats new file mode 100644 index 0000000..4c315ab --- /dev/null +++ b/tests/unit/send_doorbell.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats +# ============================================================================= +# Tests: amp_ring_doorbell (best-effort wake after a same-host inbox write) +# ============================================================================= + +setup() { + load '../test_helper' + setup_amp_env + create_test_config + source_amp_helper + unset SCRIPT_DIR + + STUBS="${BATS_TEST_TMPDIR}/stubs" + mkdir -p "$STUBS" + cat > "${STUBS}/curl" <> "${BATS_TEST_TMPDIR}/curl.calls" +[ -n "\$CURL_STUB_SLEEP" ] && sleep "\$CURL_STUB_SLEEP" +exit "\${CURL_STUB_EXIT:-0}" +STUB + chmod +x "${STUBS}/curl" + export PATH="${STUBS}:${PATH}" + export AMP_MAESTRO_URL="http://localhost:23000" +} + +wait_for_calls() { + for _ in $(seq 1 30); do + [ -s "${BATS_TEST_TMPDIR}/curl.calls" ] && return 0 + sleep 0.1 + done + return 1 +} + +@test "rings the doorbell with the recipient and message id" { + run amp_ring_doorbell "agent-uuid-1" "msg_123_abc" + assert_success + assert_output "" + wait_for_calls + run cat "${BATS_TEST_TMPDIR}/curl.calls" + assert_output --partial "http://localhost:23000/api/messages/doorbell" + assert_output --partial '"recipient":"agent-uuid-1"' + assert_output --partial '"messageId":"msg_123_abc"' +} + +@test "prints nothing and succeeds when the server is unreachable" { + export CURL_STUB_EXIT=7 + run amp_ring_doorbell "a" "m" + assert_success + assert_output "" +} + +@test "does not wait for a slow server" { + export CURL_STUB_SLEEP=5 + start=$SECONDS + run amp_ring_doorbell "a" "m" + assert_success + [ $((SECONDS - start)) -lt 3 ] +} + +@test "AMP_DOORBELL=0 opts out" { + export AMP_DOORBELL=0 + run amp_ring_doorbell "a" "m" + assert_success + sleep 0.3 + [ ! -e "${BATS_TEST_TMPDIR}/curl.calls" ] +} + +@test "does nothing without a recipient or a message id" { + run amp_ring_doorbell "" "m" + assert_success + run amp_ring_doorbell "a" "" + assert_success + sleep 0.3 + [ ! -e "${BATS_TEST_TMPDIR}/curl.calls" ] +} + +@test "succeeds with curl missing" { + # Make `command -v curl` fail without touching PATH (other tools stay available) + command() { if [ "$1" = "-v" ] && [ "$2" = "curl" ]; then return 1; fi; builtin command "$@"; } + run amp_ring_doorbell "a" "m" + unset -f command + assert_success + assert_output "" + sleep 0.3 + [ ! -e "${BATS_TEST_TMPDIR}/curl.calls" ] +} + +@test "amp-send rings after both local filesystem writes" { + run grep -c 'amp_ring_doorbell "${RECIPIENT_UUID:-$ADDR_NAME}" "$MSG_ID"' "${SCRIPTS_DIR}/amp-send.sh" + assert_output "2" +}