diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-24 17:42:54 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-24 17:42:54 -0500 |
| commit | b19d420b11af77e991b4bf14ef5a312c70cbfa3e (patch) | |
| tree | cbda5af7f2afd1d1523a65b002f4a1aaa48c5ad6 /working/triage-telegram-down-launch/triage-intake.telegram.org.superseded-1723 | |
| parent | f0c1bc40708615d5b423922c08c1f27e6cf96259 (diff) | |
| download | rulesets-b19d420b11af77e991b4bf14ef5a312c70cbfa3e.tar.gz rulesets-b19d420b11af77e991b4bf14ef5a312c70cbfa3e.zip | |
chore(inbox): park lint-org and telegram fixes from home and .emacs.d
Diffstat (limited to 'working/triage-telegram-down-launch/triage-intake.telegram.org.superseded-1723')
| -rw-r--r-- | working/triage-telegram-down-launch/triage-intake.telegram.org.superseded-1723 | 273 |
1 files changed, 273 insertions, 0 deletions
diff --git a/working/triage-telegram-down-launch/triage-intake.telegram.org.superseded-1723 b/working/triage-telegram-down-launch/triage-intake.telegram.org.superseded-1723 new file mode 100644 index 0000000..497f74b --- /dev/null +++ b/working/triage-telegram-down-launch/triage-intake.telegram.org.superseded-1723 @@ -0,0 +1,273 @@ +#+TITLE: Triage Intake — Telegram Source +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-09 + +# Source plugin for the triage-intake engine. See triage-intake.org for the +# contract and the Phase A-D orchestration. This file declares ONE source. +# +# General (personal) source: Telegram via the Emacs telega.el package (tdlib +# backend). It lives in .ai/workflows/ and is template-synced, sitting with the +# other general personal sources (personal-gmail, cmail, personal-calendar, +# signal, github-prs) — not the project plugins. Telegram is personal +# messaging, not project-specific. +# +# Unlike signal-cli (a standalone CLI), Telegram has no headless CLI here. The +# client is telega.el running inside Craig's long-lived `emacs --daemon`, so the +# plugin drives it over `emacsclient -e`. tdlib keeps a persisted session in +# ~/.telega (td.binlog), so a started telega reconnects without re-auth. + +* Source: telegram +:PROPERTIES: +:ORDER: 24 +:ENABLED: command -v emacsclient && emacsclient -e "(or (featurep 'telega) (fboundp 'telega))" | grep -q t +:ANCHOR: none +:SUBAGENT_OVER: 40 +:END: + +** Quick reference — full lifecycle + +Telega does not autostart with the Emacs daemon. "Down" is its normal state +unless Craig has Telegram open in Emacs. The scan therefore runs the full +lifecycle every time, never skips because the server is down: + +1. Record prior state: TELEGA_WAS_RUNNING via (telega-server-live-p). +2. Launch (only if not running): + emacsclient -e "(progn (setq telega-use-docker t) (telega t) 'started)" + The setq is mandatory defense: tdlib segfaults outside docker mode + (2026-06-09), and Craig's daemon currently has telega-use-docker nil. + Wait ~2s for Ready, then (telega--loadChats 'main) until telega--chats + is populated. +3. Check messages: the maphash unread scan in ** Scan Step 2 (filters the + messageContactRegistered join-notice noise). +4. Send (needs the server live; /voice personal first — Telegram + occasionally carries WORK communication to Kostya and Vrezh, so treat + sends with the same care as Slack): + emacsclient -e "(telega-chat-send-msg (telega-chat-get <CHAT-ID>) \"<body>\")" +5. Shutdown (ONLY if step 1 recorded not-running): + emacsclient -e "(progn (telega-server-kill) (ignore-errors (telega-kill t)) 'stopped)" + Verify: telega-server-live-p → nil, no zevlg/telega-server container in + docker ps. If Craig had it running, leave it untouched. + +If any lifecycle step fails (docker image missing, server crash, daemon +unreachable), the sweep reports it as SCAN FAILED at the top of the summary +per the engine's failure rule — never as a silent skip. Craig gets real +traffic here. + +** Scan + +Telegram direct messages and groups via telega.el in the running Emacs daemon. +=ANCHOR: none= because telega reports live unread *state* (each chat's +=:unread_count=), not a since-window — the engine substitutes no cutoff. Phase B +uses each message's timestamp only to order and label recency. + +The scan reads the =telega--chats= hash table (chat-id → chat plist), which +telega populates as chats sync. *This is robust to the tdlib server crashing +mid-session* (see the SEGFAULT gotcha below): the hash retains the last-synced +unread counts and =:last_message= even after the server dies, so a scan reading +the hash still returns the most recent known state. + +*** Leave-no-trace lifecycle (start only if needed, shut down only if we started it) + +telega is a long-lived client inside Craig's daemon. If he already has it +running, the scan must leave it running. If it's *not* running, the scan starts +it for the read and shuts it down cleanly afterward, restoring the daemon to its +prior state. The discipline: *record the prior liveness, branch on it at the +end.* + +*** Step 0 — record prior state + +#+begin_src bash +# t if telega's tdlib server was ALREADY live before this scan, nil otherwise. +# Hold this value; Step 3 reads it to decide whether to shut telega down. +TELEGA_WAS_RUNNING=$(emacsclient -e "(and (fboundp 'telega-server-live-p) (telega-server-live-p) t)" 2>/dev/null) +#+end_src + +*** Step 1 — start (docker mode) if not already running, wait for Ready + +#+begin_src bash +# `(telega t)` starts without popping the root buffer. Docker mode (the stable +# path — see the SEGFAULT gotcha) reconnects the persisted ~/.telega session in +# ~2s. Then load the main chat list so telega--chats populates. +# +# The `(setq telega-use-docker t)` is mandatory and must come BEFORE `(telega t)`: +# tdlib segfaults in native mode (SEGFAULT gotcha below), and the daemon's default +# is nil unless something (e.g. an Emacs-config :custom) has already forced it. It +# was missing here while the Quick Reference and the gotcha both require it — +# a session that started telega without it on a native-mode daemon would crash the +# server, surfacing as a triage SCAN FAILED. Match the Quick Reference exactly. +emacsclient -e "(progn + (setq telega-use-docker t) + (unless (and (fboundp 'telega-server-live-p) (telega-server-live-p)) (telega t)) + 'started)" +# Poll until Ready with chats synced, or a crash/timeout. Background this with an +# until-loop so the wait doesn't block; exit on Ready-with-chats OR an abnormal +# server exit. Then force a chat-list load if the hash is thin: +emacsclient -e "(progn (ignore-errors (telega--loadChats 'main)) (ignore-errors (telega--loadChats 'main)) 'loaded)" +#+end_src + +On a persisted session telega reaches status "Ready" within ~2s; the chat list +loads over a few more. If =(hash-table-count telega--chats)= is 0 or thin, +re-issue =telega--loadChats= and poll until it stabilizes. + +*** Step 2 — read unread, classified by last-message type + +The single most important filter: =messageContactRegistered=. Telegram counts a +"<name> joined Telegram" service notice as one unread message, so every contact +from Craig's old address book who ever joined shows as a 1-unread "DM" *that +person never actually sent*. On the 2026-06-09 first scan this was 30 of ~50 +unread chats. Drop them entirely (tally only). + +#+begin_src bash +emacsclient -e "(let (real svc other) + (when (boundp 'telega--chats) + (maphash (lambda (id chat) + (let* ((uc (or (plist-get chat :unread_count) 0)) + (lm (plist-get chat :last_message)) + (ctype (when lm (plist-get (plist-get lm :content) :@type))) + (title (or (ignore-errors (substring-no-properties (telega-chat-title chat))) \"?\"))) + (when (> uc 0) + (cond + ((equal ctype \"messageContactRegistered\") (push title svc)) + ((member ctype '(\"messageText\" \"messagePhoto\" \"messageVideo\" \"messageDocument\" \"messageVoiceNote\" \"messageSticker\" \"messageAnimation\")) + (push (list title uc ctype) real)) + (t (push (list title uc (or ctype \"nil\")) other)))))) + telega--chats)) + (list (cons 'real (nreverse real)) + (cons 'joined-telegram-count (length svc)) + (cons 'other (nreverse other))))" +#+end_src + +For a chat that survives as Action-worthy, pull the last message's text to +classify and summarize: + +#+begin_src bash +# <CHAT-ID> from the maphash key (the scan can also return ids alongside titles) +emacsclient -e "(let ((c (gethash <CHAT-ID> telega--chats))) + (substring-no-properties + (or (telega--tl-get c :last_message :content :text :text) \"\")))" +#+end_src + +*** Step 3 — restore prior state (shut down only if we started it) + +#+begin_src bash +# If telega was NOT running before this scan, shut it down cleanly to leave the +# daemon as we found it. If Craig already had it running, leave it alone. +if [ "$TELEGA_WAS_RUNNING" != "t" ]; then + emacsclient -e "(progn (ignore-errors (telega-server-kill)) (ignore-errors (telega-kill t)) 'killed)" +fi +#+end_src + +⚠ *In docker mode, =telega-kill= alone is not enough.* =telega-kill= buries the +telega buffers but leaves the dockerized tdlib server running (=telega-server-live-p= +stays non-nil). =telega-server-kill= is what actually stops the server. Call +*both* — server-kill then kill — for a clean teardown. Verified clean afterward: +=telega-server-live-p= → nil, root buffer gone, no =zevlg/telega-server= container +left in =docker ps=. Skipping this whole branch when =TELEGA_WAS_RUNNING= is t is +the point of Step 0: never tear down a session Craig is actively using. + +⚠ *SEGFAULT GOTCHA — crashes are spontaneous; treat server death as routine.* +The dockerized =telega-server= (=zevlg/telega-server:latest=, image built +2026-06-04, tdlib 1.8.64) SIGSEGVs (exit 139) *on its own*, minutes-to-hours +into a session — 11 host coredumps between 2026-06-09 and 2026-06-11, several at +times when no triage verb was running. The 2026-06-11 investigation reproduced +the crash-free verbs and the spontaneous deaths side by side: coredump +backtraces show a corrupted stack (memory corruption in the musl build), and +no newer image exists upstream. Earlier theories — "native mode is the trigger", +"toggle-read is the trigger" — were timing coincidences; the verbs are sound. + +Operationally: docker mode stays mandatory (=telega-use-docker= = t; the setq +before =(telega t)= is still the right defense), and *every action batch checks +the server first* — =(process-live-p (telega-server--proc))= — restarting via +=(telega t)= when dead and re-checking Ready before firing verbs. A mid-sweep +death is recoverable, not an abort: restart, confirm Ready, resume. Durable-fix +candidates if the crashing gets worse: pin a pre-2026-06 image digest, build +=telega-server= natively against tdlib, or report upstream to zevlg with the +coredumps (=coredumpctl list /usr/bin/telega-server=). + +Defense in depth: even if the server does die, the scan still works because it +reads the cached =telega--chats= hash, not a live query. A dead server is +*scan-only* — you can still report unread state, but cannot read new bodies, mark +read, or reply until it restarts. Treat that as "scan-only, no actions this run" +and say so. + +** Classify + +Bias: Craig's personal Telegram is *spam-dominated* with a thin layer of real +signal. The opposite of Signal (high signal/low volume) — here the volume is +high and almost all noise. Filter aggressively; surface only the few real +threads. Kostya and Vrezh occasionally reach Craig here, so a real DM from a +work contact is Action, full stop. + +- *Noise-trash (tally only, never itemized):* + - =messageContactRegistered= "joined Telegram" notices — always noise, no + matter whose name is on them. The real contacts Craig knows live here; a + join notice is not a message from them. + - Romance/crypto spam DMs — the signature is an emoji-laden handle or a + two-word "RealName + FantasyWord" suffix (=Gayle ⚾🤎RoyalVineyard=, + =Cherie🌷🏰 InfiniteRhapsody=, =Jane 🍒🔥=, =Luna Skye=). One unread, + unsolicited, no prior thread. + - =Deleted Account-NNNN= threads, blank-title chats, bot channels + (=Z-Library Official=), Telegram's own =✔️Telegram= service notices. +- *Noise-keep (never reported):* unread in dev-community groups Craig follows — + =GNU Emacs=, =zed=, =Kitty=, and similar. Skipped in sweep reports entirely — + not even a name + count line — unless Craig specifically asks about them + (Craig's ruling, 2026-06-11, via the work project's handoff). Leave them + unread; they're reading material, not signal. +- *Action:* a real text/voice/media message from a *known personal contact* in + an existing one-to-one thread — an explicit ask, a question, a reply owed. On + a spam-heavy account these are rare; when one appears, surface it prominently + with the sender + gist, because it's the needle in the haystack. + +The 2026-06-09 calibration run: 30 join-notices + ~10 spam/deleted/bot + 3 dev +groups + 0 real personal DMs. Expect most sweeps to look like this — a clean +"nothing real" is the common, correct result. + +** Render + +#+begin_example +**Telegram — N unread chats (M real after filtering).** <one-line summary> +- Action: <real DMs from known contacts, sender + gist, reply owed called out> +- Noise: K joined-Telegram notices, J spam/bot/deleted (tally only) +#+end_example + +Dev-community group traffic never appears here — no FYI line, no name + count — +unless Craig asks for it in that sweep (2026-06-11 ruling). Real DMs from known +contacts still surface as Action. + +Omit the block entirely when there's nothing but group traffic, join-notices, +and spam — under the engine's deltas-only rule that's a no-change source. Render +the block only when there's an Action item or a Noise tally worth a state-change +suggestion (e.g. a trash batch). + +** Actions + +Actions need the tdlib server *live* (see the SEGFAULT gotcha — a dead server is +scan-only). All run through telega in the daemon: + +- reply :: =emacsclient -e "(telega-chat-send-msg (telega-chat-get <CHAT-ID>) \"<body>\")"= — public-facing (goes out under Craig's name), so run =/voice personal= before sending. Prefer a body file for multi-line. +- mark-read :: verified 2026-06-11 (the previously documented =telega-chat--mark-read= never existed in telega). The idempotent per-chat verb: + + #+begin_example + emacsclient -e "(let ((chat (telega-chat-get <CHAT-ID>))) + (telega--viewMessages chat (list (plist-get chat :last_message)) + :source '(:@type \"messageSourceChatList\") :force t) + (telega--readAllChatMentions chat) + (telega--readAllChatReactions chat))" + #+end_example + + =telega-chat-toggle-read= also works but *toggles*: on a chat with zero unread it marks the chat UNREAD, so scripting must guard on =(> (plist-get chat :unread_count) 0)=. Never mark the whole account read blindly; a real DM is handled deliberately, not swept. +- delete-join-notice :: standing policy (Craig, 2026-06-11): a chat whose *newest* message is a =messageContactRegistered= "joined Telegram" notice is a chat Craig never responded to and doesn't want to keep — *delete it* rather than mark it read. The bulk sweep (returns the count deleted): + + #+begin_example + emacsclient -e "(let ((n 0)) + (maphash (lambda (_id chat) + (when (equal (plist-get (plist-get (plist-get chat :last_message) :content) :@type) + \"messageContactRegistered\") + (telega--deleteChatHistory chat t nil) + (setq n (1+ n)))) + telega--chats) + n)" + #+end_example + + =telega--deleteChatHistory chat t nil= removes the chat from the list on Craig's side only (no revoke). First run 2026-06-11 deleted 41 such chats and cut the unread-chat count from 48 to 16. +- open :: =emacsclient -e "(telega-chat-with (telega-chat-get <CHAT-ID>))"= — pop the chat buffer for Craig to read/handle by hand (useful when a real DM needs a considered reply). |
