Skip to content

feat(bin): add a Telegram process-event adapter - #2966

Closed
bingb0t5 wants to merge 24 commits into
kunchenguid:mainfrom
bingb0t5:fm/fm-telegram-adapter-v2
Closed

bingb0t5 wants to merge 24 commits into
kunchenguid:mainfrom
bingb0t5:fm/fm-telegram-adapter-v2

Conversation

@bingb0t5

@bingb0t5 bingb0t5 commented Aug 24, 2026 •

Copy link
Copy Markdown

Intent

Make Telegram a first-class firstmate channel by adding a thin bin/fm-procevent-telegram.sh process-event adapter modeled on the existing adapter shape. It must provide arm, source-id, classify , terminal , and retire; terminal is never terminal, and answers is deliberately omitted because Telegram prose must not guess captain-held decision keys. The blocking child performs one bounded Telegram getUpdates long poll per invocation, reads the credential at runtime from the existing external mode-600 file without logging or persisting the bot token, exits silently zero when credentials are absent or unreadable, consumes non-text and unauthorized updates without waking, durably writes each captain message under state/telegram-inbox before advancing the shared offset, leaves the offset unchanged on any write failure, and preserves handled-message movement semantics. Do not change bin/fm-procevent.sh, the outbound path, credential files, or the home-local legacy check script. Tests must exercise executable behavior rather than implementation-source bytes and cover write-before-offset and retry, token absence from outputs/results/inbox, missing credentials, non-text offset consumption without wake, permanent terminal behavior, and the real generic runner. Preserve sender-identity hardening, blocked-marker lifecycle, and receipt-recovery no-resurrection protections. HTTP 401 is a sticky permanent block until a valid parsed Telegram response with ok=true and an accepted result; HTTP 409 is announced once per continuous overlap, with independent episodes. Invalid update identifiers, including booleans and zero, must be rejected by one shared strict validator in polling and receipt recovery, never clearing blocked episodes or advancing the offset. Validate credentials before pending delivery or receipt recovery, keeping those paths silent until credentials return. Pending and receipt cleanup must complete before any wake is printed so cleanup failures cannot duplicate wakes. Document blocked wake handling in the process-event skill. Keep the bounded poll timeout documented in the adapter header, run the full suite and bin/fm-lint.sh, and validate the upstream kunchenguid PR 2933 target without merging or leaving a live Telegram source armed.

What Changed

  • Adds bin/fm-procevent-telegram.sh, a thin adapter for the generic process-to-event runner exposing arm, source-id, classify, terminal, and retire (no answers, so Telegram prose never feeds the keyed-answer intake). Its blocking poll child runs one bounded getUpdates long poll per invocation, reads the mode-0600 credential file at runtime without logging or persisting the bot token, exits silently when credentials are absent or unreadable, requires both the configured sender and chat before a message is trusted, and durably writes each captain message under state/telegram-inbox/ (atomic temp-plus-hardlink claim with a per-update receipt) before the shared offset advances - any failed or unresolvable update leaves the whole batch's offset unchanged. terminal never exits 0, so only explicit operator retirement stops the channel; HTTP 401 and 409 each publish one durable blocked: <code> wake per episode, cleared only by a parsed ok: true success, and pending/receipt cleanup completes before the result line is printed.
  • Adds bin/fm_procevent_telegram_validation.py, one shared strict valid_update_id predicate used by both the poll parser and receipt recovery, rejecting booleans, zero, negatives, and out-of-range integers so an invalid identifier can never advance the offset or clear a blocked episode.
  • Adds tests/fm-procevent-telegram.test.sh, exercising the public adapter and the real generic runner in an isolated home: write-before-offset and retry without duplicate inbox content, cleanup failure withholding the wake, token absence from outputs/results/inbox, missing or non-private credentials keeping pending delivery and receipt recovery silent, non-text and unauthorized updates consuming the offset without a wake, permanent terminal behavior, sticky 401 and per-overlap 409 announcements, and invalid identifier rejection.
  • Documents the channel in .agents/skills/process-event-sources/SKILL.md (including blocked wake handling and the inbox read/reply/move loop), docs/configuration.md, AGENTS.md's state/ map, and the docs/verification/process-event-sources.md guarantee table.

Risk Assessment

✅ Low: The change is a self-contained new adapter that touches no existing runtime path, and every required intent constraint I could verify from source - sticky 401, independent 409 episodes, one shared strict update-id validator across both call sites, credential gating ahead of pending and receipt recovery, and cleanup completing before any wake is printed - holds under concrete traced sequences, backed by a behavior-only test suite with no source-content assertions.

Testing

I ran the Telegram adapter's own behavior suite (all 40 checks pass), plus the generic-runner suite and the documentation-audience suite that own the changed docs, all green. Because passing tests alone are not evidence of the intent, I also built an operator walkthrough that drives the real adapter through the real bin/fm-procevent.sh with a fake curl standing in for api.telegram.org, and captured the transcript: arming the channel, a captain text arriving and being durably written to state/telegram-inbox before the shared offset advances, the published wake and captured result classifying as message, the handler acknowledgement, terminal refusing to retire the channel, the bot token present in the curl config but absent from every file the run produced, a sticker and a group imposter consumed with no wake, HTTP 401 announced exactly once and staying sticky through a truncated HTTP 200 and an ok:false body, token rotation ending the episode and resuming delivery so a later 401 announces again, credentials removed giving a silent zero exit, and retire cleaning up. I additionally reproduced the head commit's fix for real rather than by simulation: a live poll SIGKILLed the moment it writes a temp payload leaves an unclaimed complete captain order visible to the documented inbox scan on the parent commit, and leaves the inbox clean on the target commit. This is a CLI and on-disk-state product with no rendered UI surface, so the reviewer-visible evidence is the CLI transcript and persisted state rather than screenshots. I did not run the full repository suite, bin/fm-lint.sh, or the upstream kunchenguid PR 2933 check: those sit outside this targeted test phase. No live Telegram source was left armed and no transient artifacts remain in the worktree.

Evidence: Telegram channel operator walkthrough (real adapter + real generic runner, fake curl for api.telegram.org)

Source: Telegram channel operator walkthrough (real adapter + real generic runner, fake curl for api.telegram.org)


== 1. The operator arms the Telegram channel
  credential file is the existing external mode-600 file, read at runtime:
$ ls -l '$FM_HOME/secrets/telegram.env'
  -rw------- 1 rich rich 126 Aug 25 07:30 $FM_HOME/secrets/telegram.env
$ bin/fm-procevent-telegram.sh source-id
  telegram
$ bin/fm-procevent-telegram.sh arm
  registered: telegram (telegram)
  armed: telegram
$ bin/fm-procevent.sh list
  SOURCE                       ADAPTER      OWNER      PENDING
  telegram                     telegram     none       0

== 2. The captain texts the bot from their phone
  Telegram getUpdates would return:
  | {"ok":true,"result":[{"update_id":1001,"message":{"message_id":5,"date":1700000000,
  |  "chat":{"id":555,"type":"private"},"from":{"id":909,"first_name":"Rich"},
  |  "text":"ship the release branch once CI is green"}}]}
  
  the real runner polls, captures, and publishes the wake:
$ bin/fm-procevent.sh reconcile
  reconciled: published=0 started=1 stopped=0 uncertain=0
  
  firstmate is woken - the queue entry the daemon consumes:
$ cat '$FM_HOME/home/state/.wake-queue'
  1787617832	1	check	procevent:telegram:1	check: procevent telegram telegram 1
  
  the durably captured result, and how the adapter classifies it:
$ cat '$FM_HOME/home/state/procevent-inbox/telegram.1.result'
  message: 1
$ bin/fm-procevent-telegram.sh classify '$FM_HOME/home/state/procevent-inbox/telegram.1.result'
  message
  
  the captain message itself, durably on disk under state/telegram-inbox
  before the shared offset moved past it:
$ ls -l '$FM_HOME/home/state/telegram-inbox'
  total 4
  -rw------- 1 rich rich 123 Aug 25 07:30 1001.json
$ cat '$FM_HOME/home/state/telegram-inbox/1001.json'
  {"update_id": 1001, "date": 1700000000, "chat_id": 555, "from_id": 909, "text": "ship the release branch once CI is green"}$ cat '$FM_HOME/home/state/.telegram-offset'
  1002
  
  terminal is never terminal - the channel stays armed forever:
  terminal -> not terminal (exit 1), source still registered:
$ bin/fm-procevent.sh list
  SOURCE                       ADAPTER      OWNER      PENDING
  telegram                     telegram     none       1
  
  the handler acknowledges the delivered result, exactly as the skill
  documents, which clears it from pending:
$ bin/fm-procevent.sh handled telegram 1
  handled: telegram 1
$ bin/fm-procevent.sh list
  SOURCE                       ADAPTER      OWNER      PENDING
  telegram                     telegram     none       0

== 3. The bot token never leaks into anything durable
  positive control - the token IS in what curl was handed:
$ sed 's/7719004431:AAF-REAL-LOOKING-BOT-TOKEN-do-not-log/<<<TOKEN PRESENT>>>/' '$FM_HOME/curl-config.txt'
  url = "https://api.telegram.org/bot<<<TOKEN PRESENT>>>/getUpdates?offset=1002&timeout=25"
  
  and it is absent from every file the run produced, and from the results:
  grep -r for the token across the whole firstmate home: no matches

== 4. Noise is consumed without waking the captain's crew
  a sticker (non-text) was polled above; a group message from an imposter
  in the captain-configured chat follows. Neither may wake firstmate.
$ bin/fm-procevent-telegram.sh poll || echo '  (exit '$?' - nothing to report)'
    (exit 1 - nothing to report)
$ ls -l '$FM_HOME/home/state/telegram-inbox'
  total 4
  -rw------- 1 rich rich 123 Aug 25 07:30 1001.json
  offset advanced past both, so Telegram will not redeliver them:
$ cat '$FM_HOME/home/state/.telegram-offset'
  1004
  no wake published

== 5. A revoked token is a sticky block, announced exactly once
  Telegram answers HTTP 401 Unauthorized:
$ bin/fm-procevent.sh reconcile
  reconciled: published=0 started=1 stopped=0 uncertain=0
  
  firstmate is woken with the blocked announcement, and the captured
  result tells the handler the channel is blocked, not merely quiet:
$ cat '$FM_HOME/home/state/.wake-queue'
  1787617833	2	check	procevent:telegram:2	check: procevent telegram telegram 2
$ cat '$FM_HOME/home/state/procevent-inbox/telegram.2.result'
  blocked: 401
$ bin/fm-procevent-telegram.sh classify '$FM_HOME/home/state/procevent-inbox/telegram.2.result'
  blocked
  the channel is NOT retired - a blocked Telegram channel keeps trying:
$ bin/fm-procevent.sh list
  SOURCE                       ADAPTER      OWNER      PENDING
  telegram                     telegram     none       1
$ bin/fm-procevent.sh handled telegram 2
  handled: telegram 2
  
  the very next 401 poll says nothing - one announcement per episode:
$ bin/fm-procevent-telegram.sh poll || echo '  (exit '$?' - silent, already announced)'
    (exit 1 - silent, already announced)
  
  a malformed HTTP 200 does not end the block either (no valid ok:true result):
$ bin/fm-procevent-telegram.sh poll || echo '  (exit '$?' - still blocked, nothing announced)'
    (exit 1 - still blocked, nothing announced)
  
  and a Telegram-level rejection (ok:false on HTTP 200) does not either:
$ bin/fm-procevent-telegram.sh poll || echo '  (exit '$?' - still blocked, nothing announced)'
    (exit 1 - still blocked, nothing announced)

== 6. Rotating the token ends the block and delivery resumes
$ bin/fm-procevent-telegram.sh poll
  message: 1
$ cat '$FM_HOME/home/state/telegram-inbox/1004.json'
  {"update_id": 1004, "date": 1700000180, "chat_id": 555, "from_id": 909, "text": "token rotated, are you back?"}  
  that valid success closed the episode, so a LATER revocation is a new
  episode and is announced again:
$ bin/fm-procevent-telegram.sh poll || true
  blocked: 401

== 7. Credentials removed: silent, inert, zero
  exit status: 0
  stdout+stderr: ''
  no wake published

== 8. The operator retires the channel
$ bin/fm-procevent.sh retire telegram
  retired: telegram
$ bin/fm-procevent.sh list
  no sources registered

== walkthrough complete
Evidence: Reproducible script for the walkthrough above

Source: Reproducible script for the walkthrough above

#!/usr/bin/env bash
# Operator-level walkthrough of the Telegram firstmate channel.
#
# Everything below is the REAL adapter (bin/fm-procevent-telegram.sh) driven
# through the REAL generic runner (bin/fm-procevent.sh). The only substitution
# is `curl`: a fake on PATH stands in for api.telegram.org so the walkthrough
# is deterministic and never talks to the network. It writes a canned
# getUpdates body to the path curl was told to write to, and prints a canned
# HTTP status - exactly the two things the adapter reads.
set -u

ROOT=${1:?usage: telegram-channel-e2e-demo.sh <firstmate-repo-root>}
DEMO=$(mktemp -d /tmp/fm-telegram-demo.XXXXXX)
trap 'rm -rf "$DEMO"' EXIT

FM_HOME="$DEMO/home"
ENV_FILE="$DEMO/secrets/telegram.env"
TOKEN='7719004431:AAF-REAL-LOOKING-BOT-TOKEN-do-not-log'
CAPTAIN_CHAT=555
CAPTAIN_USER=909
mkdir -p "$FM_HOME/state" "$DEMO/secrets" "$DEMO/bin" "$DEMO/api"
export FM_HOME FM_TELEGRAM_ENV_FILE="$ENV_FILE"
export FM_PROCEVENT_CLAIM_ROOT="$DEMO/claims"

cat > "$DEMO/bin/curl" <<'SH'
#!/usr/bin/env bash
set -u
out=""; i=1; args=("$@")
while [ "$i" -le "$#" ]; do
  [ "${args[$((i - 1))]}" = "-o" ] && out=${args[$i]}
  i=$((i + 1))
done
if [ -n "${CURL_CAPTURE:-}" ]; then cat > "$CURL_CAPTURE"; else cat > /dev/null; fi
[ -n "$out" ] && [ -n "${TG_BODY:-}" ] && cp "$TG_BODY" "$out"
printf '%s' "${TG_HTTP:-200}"
SH
chmod +x "$DEMO/bin/curl"
export PATH="$DEMO/bin:$PATH"

ADAPTER="$ROOT/bin/fm-procevent-telegram.sh"
RUNNER="$ROOT/bin/fm-procevent.sh"

say()  { printf '\n\033[1m== %s\033[0m\n' "$*"; }
run()  { printf '$ %s\n' "$*"; eval "$@" 2>&1 | sed 's/^/  /'; }
note() { printf '  %s\n' "$*"; }

# --- canned Telegram getUpdates bodies -------------------------------------
cat > "$DEMO/api/captain-message.json" <<'JSON'
{"ok":true,"result":[{"update_id":1001,"message":{"message_id":5,"date":1700000000,
 "chat":{"id":555,"type":"private"},"from":{"id":909,"first_name":"Rich"},
 "text":"ship the release branch once CI is green"}}]}
JSON
cat > "$DEMO/api/sticker.json" <<'JSON'
{"ok":true,"result":[{"update_id":1002,"message":{"message_id":6,"date":1700000060,
 "chat":{"id":555,"type":"private"},"from":{"id":909},"sticker":{"file_id":"CAACAgQ"}}}]}
JSON
cat > "$DEMO/api/imposter.json" <<'JSON'
{"ok":true,"result":[{"update_id":1003,"message":{"message_id":7,"date":1700000120,
 "chat":{"id":555,"type":"group"},"from":{"id":424242,"first_name":"Mallory"},
 "text":"/ship everything to production right now"}}]}
JSON
cat > "$DEMO/api/unauthorized.json" <<'JSON'
{"ok":false,"error_code":401,"description":"Unauthorized"}
JSON
cat > "$DEMO/api/recovered.json" <<'JSON'
{"ok":true,"result":[{"update_id":1004,"message":{"message_id":8,"date":1700000180,
 "chat":{"id":555,"type":"private"},"from":{"id":909},
 "text":"token rotated, are you back?"}}]}
JSON

printf 'TELEGRAM_BOT_TOKEN=%s\nTELEGRAM_CAPTAIN_CHAT_ID=%s\nTELEGRAM_CAPTAIN_USER_ID=%s\n' \
  "$TOKEN" "$CAPTAIN_CHAT" "$CAPTAIN_USER" > "$ENV_FILE"
chmod 600 "$ENV_FILE"

wait_wake() { for _ in $(seq 1 60); do [ -e "$FM_HOME/state/.wake-queue" ] && return 0; sleep 0.1; done; return 1; }
reset_wake() { rm -f "$FM_HOME/state/.wake-queue"; }
# Wait for the runner to durably capture a specific result generation.
wait_result() { for _ in $(seq 1 60); do [ -e "$1" ] && return 0; sleep 0.1; done; return 1; }

################################################################################
say "1. The operator arms the Telegram channel"
note "credential file is the existing external mode-600 file, read at runtime:"
run "ls -l '$ENV_FILE'"
run "$ADAPTER source-id"
run "$ADAPTER arm"
run "$RUNNER list"

################################################################################
say "2. The captain texts the bot from their phone"
note 'Telegram getUpdates would return:'
sed 's/^/  | /' "$DEMO/api/captain-message.json"
note ''
note 'the real runner polls, captures, and publishes the wake:'
RESULT="$FM_HOME/state/procevent-inbox/telegram.1.result"
TG_BODY="$DEMO/api/captain-message.json" run "$RUNNER reconcile"
wait_result "$RESULT" || { echo "NO RESULT CAPTURED"; exit 1; }
wait_wake || { echo "NO WAKE"; exit 1; }
note ''
note 'firstmate is woken - the queue entry the daemon consumes:'
run "cat '$FM_HOME/state/.wake-queue'"
note ''
note 'the durably captured result, and how the adapter classifies it:'
run "cat '$RESULT'"
run "$ADAPTER classify '$RESULT'"
note ''
note 'the captain message itself, durably on disk under state/telegram-inbox'
note 'before the shared offset moved past it:'
run "ls -l '$FM_HOME/state/telegram-inbox'"
run "cat '$FM_HOME/state/telegram-inbox/1001.json'"
run "cat '$FM_HOME/state/.telegram-offset'"
note ''
note 'terminal is never terminal - the channel stays armed forever:'
if "$ADAPTER" terminal "$RESULT"; then note 'terminal -> TERMINAL (wrong)'; else
  note "terminal -> not terminal (exit $?), source still registered:"; fi
run "$RUNNER list"
note ''
note 'the handler acknowledges the delivered result, exactly as the skill'
note 'documents, which clears it from pending:'
run "$RUNNER handled telegram 1"
run "$RUNNER list"

################################################################################
say "3. The bot token never leaks into anything durable"
note 'positive control - the token IS in what curl was handed:'
reset_wake
CURL_CAPTURE="$DEMO/curl-config.txt" TG_BODY="$DEMO/api/sticker.json" "$ADAPTER" poll >/dev/null 2>&1 || true
run "sed 's/${TOKEN}/<<<TOKEN PRESENT>>>/' '$DEMO/curl-config.txt'"
note ''
note 'and it is absent from every file the run produced, and from the results:'
if grep -rl -- "$TOKEN" "$FM_HOME" 2>/dev/null | grep -q .; then
  note "LEAK: $(grep -rl -- "$TOKEN" "$FM_HOME")"
else
  note "grep -r for the token across the whole firstmate home: no matches"
fi

################################################################################
say "4. Noise is consumed without waking the captain's crew"
note 'a sticker (non-text) was polled above; a group message from an imposter'
note 'in the captain-configured chat follows. Neither may wake firstmate.'
reset_wake
TG_BODY="$DEMO/api/imposter.json" run "$ADAPTER poll || echo '  (exit '\$?' - nothing to report)'"
run "ls -l '$FM_HOME/state/telegram-inbox'"
note 'offset advanced past both, so Telegram will not redeliver them:'
run "cat '$FM_HOME/state/.telegram-offset'"
[ -e "$FM_HOME/state/.wake-queue" ] && note 'WAKE PUBLISHED (wrong)' || note 'no wake published'

################################################################################
say "5. A revoked token is a sticky block, announced exactly once"
reset_wake
note 'Telegram answers HTTP 401 Unauthorized:'
BLOCKED="$FM_HOME/state/procevent-inbox/telegram.2.result"
TG_BODY="$DEMO/api/unauthorized.json" TG_HTTP=401 run "$RUNNER reconcile"
wait_result "$BLOCKED" || { echo "NO BLOCKED RESULT CAPTURED"; exit 1; }
wait_wake || { echo "NO WAKE"; exit 1; }
note ''
note 'firstmate is woken with the blocked announcement, and the captured'
note 'result tells the handler the channel is blocked, not merely quiet:'
run "cat '$FM_HOME/state/.wake-queue'"
run "cat '$BLOCKED'"
run "$ADAPTER classify '$BLOCKED'"
note 'the channel is NOT retired - a blocked Telegram channel keeps trying:'
run "$RUNNER list"
run "$RUNNER handled telegram 2"
note ''
note 'the very next 401 poll says nothing - one announcement per episode:'
reset_wake
TG_BODY="$DEMO/api/unauthorized.json" TG_HTTP=401 run "$ADAPTER poll || echo '  (exit '\$?' - silent, already announced)'"
note ''
note 'a malformed HTTP 200 does not end the block either (no valid ok:true result):'
printf '{"ok":true,"result":' > "$DEMO/api/truncated.json"
TG_BODY="$DEMO/api/truncated.json" TG_HTTP=200 run "$ADAPTER poll || echo '  (exit '\$?' - still blocked, nothing announced)'"
note ''
note 'and a Telegram-level rejection (ok:false on HTTP 200) does not either:'
printf '{"ok":false,"result":[]}' > "$DEMO/api/rejected.json"
TG_BODY="$DEMO/api/rejected.json" TG_HTTP=200 run "$ADAPTER poll || echo '  (exit '\$?' - still blocked, nothing announced)'"

say "6. Rotating the token ends the block and delivery resumes"
printf 'TELEGRAM_BOT_TOKEN=%s\nTELEGRAM_CAPTAIN_CHAT_ID=%s\nTELEGRAM_CAPTAIN_USER_ID=%s\n' \
  "ROTATED-$TOKEN" "$CAPTAIN_CHAT" "$CAPTAIN_USER" > "$ENV_FILE"
chmod 600 "$ENV_FILE"
reset_wake
TG_BODY="$DEMO/api/recovered.json" TG_HTTP=200 run "$ADAPTER poll"
run "cat '$FM_HOME/state/telegram-inbox/1004.json'"
note ''
note 'that valid success closed the episode, so a LATER revocation is a new'
note 'episode and is announced again:'
TG_BODY="$DEMO/api/unauthorized.json" TG_HTTP=401 run "$ADAPTER poll || true"

################################################################################
say "7. Credentials removed: silent, inert, zero"
rm -f "$ENV_FILE"
reset_wake
st=0; out=$(TG_BODY="$DEMO/api/recovered.json" "$ADAPTER" poll 2>&1) || st=$?
note "exit status: $st"
note "stdout+stderr: '${out}'"
[ -e "$FM_HOME/state/.wake-queue" ] && note 'WAKE PUBLISHED (wrong)' || note 'no wake published'

################################################################################
say "8. The operator retires the channel"
run "$RUNNER retire telegram"
run "$RUNNER list"

printf '\n\033[1m== walkthrough complete\033[0m\n'
Evidence: Killed-poll inbox regression: before/after a real SIGKILL mid-write

Source: Killed-poll inbox regression: before/after a real SIGKILL mid-write

Regression: a poll killed mid-write must not leave a complete captain payload
in the directory the handler is told to scan.

.agents/skills/process-event-sources/SKILL.md tells the handler to "read every
new file under state/telegram-inbox/" and act on it "exactly as if the captain
had typed it in the terminal". bin/fm-procevent.sh sends kill -TERM to the
child's whole process group on retire/stop. If that lands between the temp
payload write and the hardlink claim, the pre-fix adapter left the temp INSIDE
state/telegram-inbox - a complete captain order for an update whose offset
never advanced, so Telegram redelivers it later and the order runs twice.

Method (killed-poll-inbox-regression.sh): a real poll is started in its own
process group against a 40-message captain backlog and SIGKILLed the instant
its first temp payload appears. Then the handler's documented scan is run.

--- BEFORE the fix (parent commit 3a9b507), run 1 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
  .7003.json.tmp.3276699
  7001.json
  7002.json
unclaimed temp payloads visible to the handler: 1
  contents of .7003.json.tmp.3276699:
    

--- BEFORE the fix (parent commit 3a9b507), run 2 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
  .7001.json.tmp.3276841
unclaimed temp payloads visible to the handler: 1
  contents of .7001.json.tmp.3276841:
    

--- BEFORE the fix (parent commit 3a9b507), run 3 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
  .7002.json.tmp.3276969
  7001.json
unclaimed temp payloads visible to the handler: 1
  contents of .7002.json.tmp.3276969:
    {"update_id": 7002, "date": 2, "chat_id": 555, "from_id": 909, "text": "captain order 2: deploy now"}

--- AFTER the fix (commit b1466ac), run 1 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
  7001.json
  7002.json
  7003.json
unclaimed temp payloads visible to the handler: 0

--- AFTER the fix (commit b1466ac), run 2 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
  7001.json
  7002.json
  7003.json
  7004.json
unclaimed temp payloads visible to the handler: 0

--- AFTER the fix (commit b1466ac), run 3 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
  7001.json
  7002.json
  7003.json
unclaimed temp payloads visible to the handler: 0
Evidence: Reproducible script for the killed-poll regression

Source: Reproducible script for the killed-poll regression

#!/usr/bin/env bash
# Kill a real poll between its temp payload write and the hardlink claim, then
# show what a handler that follows the documented instruction ("read every new
# file under state/telegram-inbox") would actually see.
#
# $1 = adapter to exercise
set -u
ADAPTER=$1
LABEL=$2
DEMO=$(mktemp -d /tmp/tgkill.XXXXXX)
FM_HOME="$DEMO/home"; mkdir -p "$FM_HOME/state" "$DEMO/bin" "$DEMO/api"
ENV_FILE="$DEMO/telegram.env"
printf 'TELEGRAM_BOT_TOKEN=t\nTELEGRAM_CAPTAIN_CHAT_ID=555\nTELEGRAM_CAPTAIN_USER_ID=909\n' > "$ENV_FILE"
chmod 600 "$ENV_FILE"

cat > "$DEMO/bin/curl" <<'SH'
#!/usr/bin/env bash
set -u
out=""; i=1; args=("$@")
while [ "$i" -le "$#" ]; do
  [ "${args[$((i - 1))]}" = "-o" ] && out=${args[$i]}
  i=$((i + 1))
done
cat > /dev/null
[ -n "$out" ] && cp "$TG_BODY" "$out"
printf '200'
SH
chmod +x "$DEMO/bin/curl"
export PATH="$DEMO/bin:$PATH" FM_HOME FM_TELEGRAM_ENV_FILE="$ENV_FILE"

# A realistic backlog: the captain fired off a burst of messages while the
# crew was away, so one poll writes several payloads in a row.
{
  printf '{"ok":true,"result":['
  for i in $(seq 1 40); do
    [ "$i" -gt 1 ] && printf ','
    printf '{"update_id":%d,"message":{"date":%d,"chat":{"id":555},"from":{"id":909},"text":"captain order %d: deploy now"}}' \
      "$((7000 + i))" "$i" "$i"
  done
  printf ']}'
} > "$DEMO/api/burst.json"
export TG_BODY="$DEMO/api/burst.json"

INBOX="$FM_HOME/state/telegram-inbox"
mkdir -p "$INBOX"

setsid "$ADAPTER" poll >/dev/null 2>&1 &
child=$!
# Busy-watch for the first temp payload the poll creates anywhere, then kill
# the whole process group hard - exactly what fm-procevent.sh does on
# retire/stop (kill -TERM -"$pid"), only less survivable.
killed=no
for _ in $(seq 1 200000); do
  if compgen -G "$INBOX/.*tmp*" > /dev/null || compgen -G "$FM_HOME/state/.telegram-delivery-receipts/tmp.*" > /dev/null; then
    kill -9 -"$child" 2>/dev/null && killed=yes
    break
  fi
  kill -0 "$child" 2>/dev/null || break
done
wait "$child" 2>/dev/null

printf '\n--- %s ---\n' "$LABEL"
printf 'poll killed mid-write: %s\n' "$killed"
printf 'offset file: %s\n' "$(cat "$FM_HOME/state/.telegram-offset" 2>/dev/null || echo '(absent - nothing was ever acknowledged to Telegram)')"
printf 'what the handler sees when it scans state/telegram-inbox:\n'
found=$(cd "$INBOX" && find . -mindepth 1 -maxdepth 1 -type f | sed 's|^\./||' | sort)
if [ -z "$found" ]; then
  printf '  (nothing - clean)\n'
else
  printf '%s\n' "$found" | sed 's/^/  /'
fi
unclaimed=$(printf '%s\n' "$found" | grep -c 'tmp' || true)
printf 'unclaimed temp payloads visible to the handler: %s\n' "$unclaimed"
for f in $(printf '%s\n' "$found" | grep 'tmp' || true); do
  printf '  contents of %s:\n    %s\n' "$f" "$(cat "$INBOX/$f")"
done
rm -rf "$DEMO"
Evidence: Captain message delivered end to end, and the same channel blocked and recovered
$ bin/fm-procevent.sh reconcile
reconciled: published=0 started=1 stopped=0 uncertain=0
$ cat state/.wake-queue
1787617832 1 check procevent:telegram:1 check: procevent telegram telegram 1
$ cat state/procevent-inbox/telegram.1.result
message: 1
$ bin/fm-procevent-telegram.sh classify state/procevent-inbox/telegram.1.result
message
$ cat state/telegram-inbox/1001.json
{"update_id": 1001, "date": 1700000000, "chat_id": 555, "from_id": 909, "text": "ship the release branch once CI is green"}
$ cat state/.telegram-offset
1002
terminal -> not terminal (exit 1), source still registered

positive control - the token IS in what curl was handed:
url = "https://api.telegram.org/bot<<<TOKEN PRESENT>>>/getUpdates?offset=1002&timeout=25"
grep -r for the token across the whole firstmate home: no matches

HTTP 401:
$ cat state/procevent-inbox/telegram.2.result
blocked: 401
$ bin/fm-procevent-telegram.sh classify state/procevent-inbox/telegram.2.result
blocked
the very next 401 poll says nothing - one announcement per episode:
(exit 1 - silent, already announced)
a malformed HTTP 200 does not end the block either:
(exit 1 - still blocked, nothing announced)
and a Telegram-level rejection (ok:false on HTTP 200) does not either:
(exit 1 - still blocked, nothing announced)
after rotating the token:
message: 1
{"update_id": 1004, ..., "text": "token rotated, are you back?"}
a LATER revocation is a new episode and is announced again:
blocked: 401

credentials removed -> exit status: 0, stdout+stderr: '', no wake published
Evidence: Killed poll: what the handler's documented inbox scan sees, before vs after
--- BEFORE the fix (parent commit 3a9b507), run 3 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
.7002.json.tmp.3276969
7001.json
unclaimed temp payloads visible to the handler: 1
contents of .7002.json.tmp.3276969:
{"update_id": 7002, "date": 2, "chat_id": 555, "from_id": 909, "text": "captain order 2: deploy now"}

--- AFTER the fix (commit b1466ac), run 3 ---
poll killed mid-write: yes
offset file: (absent - nothing was ever acknowledged to Telegram)
what the handler sees when it scans state/telegram-inbox:
7001.json
7002.json
7003.json
unclaimed temp payloads visible to the handler: 0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-procevent-telegram.sh:495 - Unrecoverable adapter-private state permanently and silently kills the captain's Telegram channel. Reproduced: a receipt file containing not json makes recover_receipts' python json.load (line 495) raise uncaught, so count=$(python3 …) fails, recover_receipts returns 1, and cmd_poll's [ &#34;$rc&#34; -eq 2 ] || exit &#34;$rc&#34; (line 577) exits 1 with empty stdout - the runner's no-result path. Nothing ever removes or quarantines the receipt, so every subsequent poll repeats the identical failure forever: no capture, no wake, no blocked: announcement. The same shape applies to a receipt whose update_id fails valid_update_id (line 500 sys.exit(1); tests/fm-procevent-telegram.test.sh:550-562 plants exactly this state and asserts only the silent nonzero exit, never that the channel recovers), to a $PENDING_FILE that read_pending cannot parse (lines 432-437, reached unconditionally at line 569 - also reproduced over repeated polls), and to clear_blocked failing (line 410), whose || exit 1 at line 774 leaves the offset permanently unadvanced. bin/fm-procevent.sh:367 runs the child as &#34;${ARGV[@]}&#34; 2&gt;/dev/null, so even the 14-line python traceback is discarded. This contradicts the header's own PERMANENT FAILURE rationale ("Retrying either forever in silence lets the captain's primary channel away from the terminal die invisibly") and the self-repair principle deliberately applied to the inbox tree at line 373. Simply deleting a bad receipt would drop an undelivered captain message, so the right resolution is a product call: either quarantine the entry and continue, or surface a durable announcement instead of exiting silently forever.
  • ℹ️ bin/fm-procevent-telegram.sh:705 - The per-update temp payload is written inside state/telegram-inbox/ itself, so a killed poll leaves an unclaimed message file in the exact directory the handler is told to read. bin/fm-procevent.sh:638 sends kill -TERM -&#34;$pid&#34; to the child's process group on retire/stop; if that lands between os.open(tmp) (line 707) and the finally: os.unlink(tmp) (line 733), state/telegram-inbox/.&lt;uid&gt;.json.tmp.&lt;pid&gt; survives with a complete captain payload for an update whose offset never advanced, and nothing ever removes it. .agents/skills/process-event-sources/SKILL.md:89 instructs the handler to "read every new file under state/telegram-inbox/"; a handler using find or ls -A picks the stale temp up and acts on a message Telegram will also redeliver later, running the captain's command twice. Creating the temp under $RECEIPT_DIR instead (same filesystem, so the os.link claim still works) keeps it out of the directory the handler scans, with a matching one-line update to the HANDOFF paragraph.
  • ℹ️ bin/fm-procevent-telegram.sh:256 - telegram_bot_token (256), telegram_captain_chat_id (267), and telegram_captain_user_id (280) are byte-identical apart from the variable name, and each spawns a subshell that sources the credential file. credential_available (303-311) sources it three times and cmd_poll (562-567) three more, so arm reads the file three times and every poll reads it three times. Beyond the duplication, the three values can come from different on-disk revisions if the file is rewritten mid-poll - a rotation that swaps token and chat id together could pair the new token with the old chat id and silently consume the batch as unauthorized. A single telegram_env_value &lt;env-file&gt; &lt;var&gt; helper, or one subshell emitting all three values, collapses the duplication and reads the file once.

🔧 Fix: Move Telegram claim temp out of inbox, unify credential read
1 info still open:

  • ℹ️ bin/fm-procevent-telegram.sh:347 - write_offset (347), write_blocked (378), and write_pending (431) each re-spell the same six-line atomic private-write idiom: mkdir -p &#34;$STATE&#34;, the [ ! -e ] || [ -f ] regular-file guard, the [ ! -L ] symlink guard, tmp=$(umask 077; mktemp ...), chmod 0600, and mv -f. Only the validation and the payload differ. This is a security-relevant idiom rather than incidental duplication - each copy independently carries the symlink refusal and the 0600 mode for state that sits beside the captain's credential-derived material, so a future edit that drops one guard from one copy silently weakens that file alone with nothing to catch it. A single write_private_state &lt;path&gt; helper taking the content on stdin would collapse all three call sites to their own validation plus one call, with no behavior change.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-procevent-telegram.test.sh - 40 behavior checks covering write-before-offset and retry, token absence from outputs/results/inbox, missing and mode-0644 credentials, non-text and unauthorized-sender consumption, strict update-id validation (boolean, zero, out-of-range) in both polling and receipt recovery, sticky 401 across arm/retire, 409 episodes, cleanup-before-wake, permanent terminal, and two end-to-end paths through the real bin/fm-procevent.sh
  • bash tests/fm-procevent.test.sh - the unchanged generic runner the adapter registers with
  • bash tests/fm-documentation-audiences.test.sh - the changed prose surfaces (.agents/skills/process-event-sources/SKILL.md, docs/configuration.md, docs/verification/process-event-sources.md)
  • Manual operator walkthrough telegram-channel-e2e-demo.sh - real adapter + real bin/fm-procevent.sh with a fake curl in place of api.telegram.org: source-id, arm, fm-procevent.sh list, fm-procevent.sh reconcile, wake-queue entry, captured result, classify -> message, state/telegram-inbox/1001.json, .telegram-offset, terminal (non-terminal), fm-procevent.sh handled telegram 1, curl-config token positive control vs grep -r over the whole home, sticker and imposter polls with no wake, HTTP 401 classify -> blocked announced once then silent through a truncated HTTP 200 and an ok:false body, token rotation resuming delivery and reopening a new episode, credential removal -> silent exit 0, fm-procevent.sh retire telegram
  • Manual regression killed-poll-inbox-regression.sh - a live poll started in its own process group against a 40-message captain backlog and SIGKILLed the instant its first temp payload appears, then the handler's documented state/telegram-inbox scan, run three times against parent commit 3a9b507 and three times against target commit b1466ac
  • git diff --stat 038d0f7 HEAD - confirmed bin/fm-procevent.sh, the outbound path, credential files, and the legacy check script are untouched
⚠️ **Document** - 1 info
  • ℹ️ docs/verification/process-event-sources.md:11 - Judgment call, not a gap: I moved the Telegram verification date from 2026-08-24 to 2026-08-25 because the adapter's behavior changed after that entry (sticky-block clearing, strict identifier validation, cleanup-before-wake, temp relocation), and I re-ran tests/fm-procevent-telegram.test.sh today and it passed. That run was on Linux, while this record's earlier entries are scoped to macOS (Darwin 25.5.0). The Telegram line makes no platform claim, so nothing is misstated, but if this record is meant to be macOS-scoped throughout, the maintainer may want to re-run the suite on macOS and say so explicitly on that line.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

bingb0t5 and others added 17 commits August 24, 2026 12:55
Registers the captain's Telegram channel with the generic process-to-event
runner so a captain message wakes firstmate within seconds instead of
waiting up to five minutes for a check sweep. The adapter is deliberately
thin: it owns Telegram's getUpdates long poll, the write-before-offset
invariant that keeps a captain message from being lost, and token
handling; ownership, durable capture, publication, and restart recovery
stay with bin/fm-procevent.sh. The channel is never terminal on its own -
only an explicit retire stops it.
The prior handoff-safety fixes checked whether an update id's inbox file
already existed and then wrote it, which cannot be safe against
state/telegram-watch.check.sh: that home-local script writes in place with
no temp file and no rename, so its output can be observed mid-write, and a
check-then-write gap can still race it.

Replace that with an atomic claim: this adapter always writes its own
complete, fsynced payload to a private temp file first, then hardlinks that
finished file onto the shared <update_id>.json name. A successful hardlink
is an exclusive, race-free claim. A failed one (name already taken) is
resolved by parsing whatever is already there - a complete, well-formed
payload for that update means another claimant (the legacy script or an
earlier invocation of this adapter) already delivered it, so this poll
no-ops without a second captain-visible wake; anything else means a
claimant, most likely the legacy script, is still mid-write, and that
update blocks the whole batch's offset advance exactly like a failed write,
so an unadvanced retry gives the write time to finish. handled/ is checked
first so an archived update is never recreated in the live inbox.

This does not make true simultaneous overlap (both producers inside
getUpdates for the same not-yet-advanced offset) free - the legacy script
has no knowledge of this adapter and can still fire its own independent
wake through the check sweep, which nothing here can suppress. Only
ensuring no legacy invocation is genuinely in flight before arming (not
merely deregistering it) closes that window; the header documents this
plainly rather than claiming a guarantee the design cannot make.

Also keeps the two accumulated fixes this branch already carries: a durable
pending-delivery record so an offset-write failure never strands an already-
written message, checked and reported before any credential validation.

Adds a regression test that reproduces the legacy script's exact non-atomic
write shape mid-write, overlapping a batch that also contains a genuinely
new update, and proves the batch blocks without corruption or duplication
and resolves correctly once the legacy write finishes.
Carry the accepted Telegram adapter and regression coverage onto a fresh validation branch, including permanent API failure signaling and receipt recovery protections.

Co-authored-by: Cursor <cursoragent@cursor.com>
Registers the captain's Telegram channel with the generic process-to-event
runner so a captain message wakes firstmate within seconds instead of
waiting up to five minutes for a check sweep. The adapter is deliberately
thin: it owns Telegram's getUpdates long poll, the write-before-offset
invariant that keeps a captain message from being lost, and token
handling; ownership, durable capture, publication, and restart recovery
stay with bin/fm-procevent.sh. The channel is never terminal on its own -
only an explicit retire stops it.
The prior handoff-safety fixes checked whether an update id's inbox file
already existed and then wrote it, which cannot be safe against
state/telegram-watch.check.sh: that home-local script writes in place with
no temp file and no rename, so its output can be observed mid-write, and a
check-then-write gap can still race it.

Replace that with an atomic claim: this adapter always writes its own
complete, fsynced payload to a private temp file first, then hardlinks that
finished file onto the shared <update_id>.json name. A successful hardlink
is an exclusive, race-free claim. A failed one (name already taken) is
resolved by parsing whatever is already there - a complete, well-formed
payload for that update means another claimant (the legacy script or an
earlier invocation of this adapter) already delivered it, so this poll
no-ops without a second captain-visible wake; anything else means a
claimant, most likely the legacy script, is still mid-write, and that
update blocks the whole batch's offset advance exactly like a failed write,
so an unadvanced retry gives the write time to finish. handled/ is checked
first so an archived update is never recreated in the live inbox.

This does not make true simultaneous overlap (both producers inside
getUpdates for the same not-yet-advanced offset) free - the legacy script
has no knowledge of this adapter and can still fire its own independent
wake through the check sweep, which nothing here can suppress. Only
ensuring no legacy invocation is genuinely in flight before arming (not
merely deregistering it) closes that window; the header documents this
plainly rather than claiming a guarantee the design cannot make.

Also keeps the two accumulated fixes this branch already carries: a durable
pending-delivery record so an offset-write failure never strands an already-
written message, checked and reported before any credential validation.

Adds a regression test that reproduces the legacy script's exact non-atomic
write shape mid-write, overlapping a batch that also contains a genuinely
new update, and proves the batch blocks without corruption or duplication
and resolves correctly once the legacy write finishes.
Carry the accepted Telegram adapter and regression coverage onto a fresh validation branch, including permanent API failure signaling and receipt recovery protections.

Co-authored-by: Cursor <cursoragent@cursor.com>
@greptile-apps

greptile-apps Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Reviews (4): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile

Comment thread bin/fm-procevent-telegram.sh Outdated
Comment thread bin/fm-procevent-telegram.sh Outdated
EOF
write_offset "$target" || return 1
clear_receipts || return 1
printf 'message: %s\n' "$count" || return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Pending marker republishes messages

When removal of the pending-delivery marker fails after message: <count> has been printed, the generic runner captures that nonempty result despite the nonzero exit. The next poll sees the retained marker and prints the same result again, causing a duplicate wake for the captain-message batch.

@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

bingb0t5 and others added 5 commits August 24, 2026 22:06
Preserve the local implementation history while merging the pipeline's rebased base and accepted Telegram safety fixes for the next validation run.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@bingb0t5 bingb0t5 changed the title feat: add Telegram process-event adapter feat(bin): add a Telegram process-event adapter Aug 25, 2026
bingb0t5 added a commit to bingb0t5/firstmate that referenced this pull request Aug 25, 2026
…enguid#2966) (#1)

* feat(bin): add a Telegram process-event adapter

Registers the captain's Telegram channel with the generic process-to-event
runner so a captain message wakes firstmate within seconds instead of
waiting up to five minutes for a check sweep. The adapter is deliberately
thin: it owns Telegram's getUpdates long poll, the write-before-offset
invariant that keeps a captain message from being lost, and token
handling; ownership, durable capture, publication, and restart recovery
stay with bin/fm-procevent.sh. The channel is never terminal on its own -
only an explicit retire stops it.

* no-mistakes(review): Authenticate Telegram captain message ingestion

* no-mistakes(review): Enforce private Telegram credential permissions

* no-mistakes(review): Make Telegram inbox writes crash durable

* no-mistakes(document): Clarify Telegram adapter documentation ownership

* no-mistakes(review): Prevent duplicate Telegram delivery after handoff

* no-mistakes(review): Require legacy Telegram check retirement before arm

* no-mistakes(review): Recover Telegram wakes after offset failures

* no-mistakes(review): Document Telegram pre-capture crash limitations

* no-mistakes(review): Recover pending Telegram wakes without credentials

* no-mistakes(test): Fix Telegram handoff overlap contract

* no-mistakes(document): Polish Telegram channel documentation

* fix(bin): make Telegram inbox delivery atomic against the legacy check

The prior handoff-safety fixes checked whether an update id's inbox file
already existed and then wrote it, which cannot be safe against
state/telegram-watch.check.sh: that home-local script writes in place with
no temp file and no rename, so its output can be observed mid-write, and a
check-then-write gap can still race it.

Replace that with an atomic claim: this adapter always writes its own
complete, fsynced payload to a private temp file first, then hardlinks that
finished file onto the shared <update_id>.json name. A successful hardlink
is an exclusive, race-free claim. A failed one (name already taken) is
resolved by parsing whatever is already there - a complete, well-formed
payload for that update means another claimant (the legacy script or an
earlier invocation of this adapter) already delivered it, so this poll
no-ops without a second captain-visible wake; anything else means a
claimant, most likely the legacy script, is still mid-write, and that
update blocks the whole batch's offset advance exactly like a failed write,
so an unadvanced retry gives the write time to finish. handled/ is checked
first so an archived update is never recreated in the live inbox.

This does not make true simultaneous overlap (both producers inside
getUpdates for the same not-yet-advanced offset) free - the legacy script
has no knowledge of this adapter and can still fire its own independent
wake through the check sweep, which nothing here can suppress. Only
ensuring no legacy invocation is genuinely in flight before arming (not
merely deregistering it) closes that window; the header documents this
plainly rather than claiming a guarantee the design cannot make.

Also keeps the two accumulated fixes this branch already carries: a durable
pending-delivery record so an offset-write failure never strands an already-
written message, checked and reported before any credential validation.

Adds a regression test that reproduces the legacy script's exact non-atomic
write shape mid-write, overlapping a batch that also contains a genuinely
new update, and proves the batch blocks without corruption or duplication
and resolves correctly once the legacy write finishes.

* fix(bin): restore accepted Telegram safety handling

Carry the accepted Telegram adapter and regression coverage onto a fresh validation branch, including permanent API failure signaling and receipt recovery protections.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(bin): add a Telegram process-event adapter

Registers the captain's Telegram channel with the generic process-to-event
runner so a captain message wakes firstmate within seconds instead of
waiting up to five minutes for a check sweep. The adapter is deliberately
thin: it owns Telegram's getUpdates long poll, the write-before-offset
invariant that keeps a captain message from being lost, and token
handling; ownership, durable capture, publication, and restart recovery
stay with bin/fm-procevent.sh. The channel is never terminal on its own -
only an explicit retire stops it.

* no-mistakes(review): Authenticate Telegram captain message ingestion

* no-mistakes(review): Enforce private Telegram credential permissions

* no-mistakes(review): Make Telegram inbox writes crash durable

* no-mistakes(document): Clarify Telegram adapter documentation ownership

* fix(bin): make Telegram inbox delivery atomic against the legacy check

The prior handoff-safety fixes checked whether an update id's inbox file
already existed and then wrote it, which cannot be safe against
state/telegram-watch.check.sh: that home-local script writes in place with
no temp file and no rename, so its output can be observed mid-write, and a
check-then-write gap can still race it.

Replace that with an atomic claim: this adapter always writes its own
complete, fsynced payload to a private temp file first, then hardlinks that
finished file onto the shared <update_id>.json name. A successful hardlink
is an exclusive, race-free claim. A failed one (name already taken) is
resolved by parsing whatever is already there - a complete, well-formed
payload for that update means another claimant (the legacy script or an
earlier invocation of this adapter) already delivered it, so this poll
no-ops without a second captain-visible wake; anything else means a
claimant, most likely the legacy script, is still mid-write, and that
update blocks the whole batch's offset advance exactly like a failed write,
so an unadvanced retry gives the write time to finish. handled/ is checked
first so an archived update is never recreated in the live inbox.

This does not make true simultaneous overlap (both producers inside
getUpdates for the same not-yet-advanced offset) free - the legacy script
has no knowledge of this adapter and can still fire its own independent
wake through the check sweep, which nothing here can suppress. Only
ensuring no legacy invocation is genuinely in flight before arming (not
merely deregistering it) closes that window; the header documents this
plainly rather than claiming a guarantee the design cannot make.

Also keeps the two accumulated fixes this branch already carries: a durable
pending-delivery record so an offset-write failure never strands an already-
written message, checked and reported before any credential validation.

Adds a regression test that reproduces the legacy script's exact non-atomic
write shape mid-write, overlapping a batch that also contains a genuinely
new update, and proves the batch blocks without corruption or duplication
and resolves correctly once the legacy write finishes.

* fix(bin): restore accepted Telegram safety handling

Carry the accepted Telegram adapter and regression coverage onto a fresh validation branch, including permanent API failure signaling and receipt recovery protections.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(review): Fix Telegram blocked lifecycle and credential-gated recovery

* no-mistakes(review): Validate Telegram success before clearing blocked state

* no-mistakes(document): Document Telegram process-event verification

* no-mistakes: apply CI fixes

* no-mistakes(review): Reject invalid Telegram update identifiers

* fix(telegram): prevent duplicate wakes after cleanup failure

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(review): Move Telegram claim temp out of inbox, unify credential read

* no-mistakes(document): document Telegram blocked, identifier, and cleanup-order contracts

* no-mistakes(test): parse ci.yml timeouts with python3 yaml, ruby fallback

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this account has been flagged as attempting malicious activity and can no longer contribute to any of Kun's repos. Closing this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants