Conversation
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>
Confidence Score: 5/5The 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 |
| EOF | ||
| write_offset "$target" || return 1 | ||
| clear_receipts || return 1 | ||
| printf 'message: %s\n' "$count" || return 1 |
There was a problem hiding this comment.
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.
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
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>
…anup-order contracts
…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>
|
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. |
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
bin/fm-procevent-telegram.sh, a thin adapter for the generic process-to-event runner exposingarm,source-id,classify,terminal, andretire(noanswers, so Telegram prose never feeds the keyed-answer intake). Its blockingpollchild runs one boundedgetUpdateslong poll per invocation, reads the mode-0600credential 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 understate/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.terminalnever exits 0, so only explicit operator retirement stops the channel; HTTP 401 and 409 each publish one durableblocked: <code>wake per episode, cleared only by a parsedok: truesuccess, and pending/receipt cleanup completes before the result line is printed.bin/fm_procevent_telegram_validation.py, one shared strictvalid_update_idpredicate 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.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, permanentterminalbehavior, sticky 401 and per-overlap 409 announcements, and invalid identifier rejection..agents/skills/process-event-sources/SKILL.md(includingblockedwake handling and the inbox read/reply/move loop),docs/configuration.md,AGENTS.md'sstate/map, and thedocs/verification/process-event-sources.mdguarantee 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)
Evidence: Reproducible script for the walkthrough above
Source: Reproducible script for the walkthrough above
Evidence: Killed-poll inbox regression: before/after a real SIGKILL mid-write
Source: Killed-poll inbox regression: before/after a real SIGKILL mid-write
Evidence: Reproducible script for the killed-poll regression
Source: Reproducible script for the killed-poll regression
Evidence: Captain message delivered end to end, and the same channel blocked and recovered
Evidence: Killed poll: what the handler's documented inbox scan sees, before vs after
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-procevent-telegram.sh:495- Unrecoverable adapter-private state permanently and silently kills the captain's Telegram channel. Reproduced: a receipt file containingnot jsonmakes recover_receipts' pythonjson.load(line 495) raise uncaught, socount=$(python3 …)fails, recover_receipts returns 1, and cmd_poll's[ "$rc" -eq 2 ] || exit "$rc"(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, noblocked:announcement. The same shape applies to a receipt whose update_id failsvalid_update_id(line 500sys.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_FILEthat read_pending cannot parse (lines 432-437, reached unconditionally at line 569 - also reproduced over repeated polls), and toclear_blockedfailing (line 410), whose|| exit 1at line 774 leaves the offset permanently unadvanced.bin/fm-procevent.sh:367runs the child as"${ARGV[@]}" 2>/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 insidestate/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:638sendskill -TERM -"$pid"to the child's process group on retire/stop; if that lands betweenos.open(tmp)(line 707) and thefinally: os.unlink(tmp)(line 733),state/telegram-inbox/.<uid>.json.tmp.<pid>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 usingfindorls -Apicks 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_DIRinstead (same filesystem, so theos.linkclaim 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, soarmreads 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 singletelegram_env_value <env-file> <var>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), andwrite_pending(431) each re-spell the same six-line atomic private-write idiom:mkdir -p "$STATE", the[ ! -e ] || [ -f ]regular-file guard, the[ ! -L ]symlink guard,tmp=$(umask 077; mktemp ...),chmod 0600, andmv -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 singlewrite_private_state <path>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, permanentterminal, and two end-to-end paths through the realbin/fm-procevent.shbash tests/fm-procevent.test.sh- the unchanged generic runner the adapter registers withbash 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 walkthroughtelegram-channel-e2e-demo.sh- real adapter + realbin/fm-procevent.shwith a fakecurlin 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 vsgrep -rover the whole home, sticker and imposter polls with no wake, HTTP 401classify->blockedannounced once then silent through a truncated HTTP 200 and anok:falsebody, token rotation resuming delivery and reopening a new episode, credential removal -> silent exit 0,fm-procevent.sh retire telegramManual regressionkilled-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 documentedstate/telegram-inboxscan, run three times against parent commit 3a9b507 and three times against target commit b1466acgit diff --stat 038d0f7 HEAD- confirmedbin/fm-procevent.sh, the outbound path, credential files, and the legacy check script are untoucheddocs/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.