diff options
Diffstat (limited to 'working/triage-telegram-down-launch/triage-intake.telegram.org.proposed')
| -rw-r--r-- | working/triage-telegram-down-launch/triage-intake.telegram.org.proposed | 290 |
1 files changed, 0 insertions, 290 deletions
diff --git a/working/triage-telegram-down-launch/triage-intake.telegram.org.proposed b/working/triage-telegram-down-launch/triage-intake.telegram.org.proposed deleted file mode 100644 index 42d46fe..0000000 --- a/working/triage-telegram-down-launch/triage-intake.telegram.org.proposed +++ /dev/null @@ -1,290 +0,0 @@ -#+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: - -⚠ *DOWN / not-loaded is the TRIGGER to launch, never a reason to skip or fail.* -This is the exact mistake two projects (work + home, 2026-07-24) made: they -probed telega, saw =(telega-server-live-p)= nil or telega not =featurep=, and -reported =SCAN FAILED: telegram — not loaded= or a silent SKIP — a *blind* -sweep — instead of running Step 1 to start it. A down or unloaded telega is the -normal entry state; =(telega t)= both LOADS the package and STARTS the docker -server (work confirmed: down → =(telega t)= → Ready, 18 chats). So the plugin -MUST run Step 1's launch whenever telega is down/unloaded, wait for Ready, then -scan. =SCAN FAILED= is reserved for a launch that was actually ATTEMPTED and did -not reach Ready (image missing, server crash on start, daemon unreachable) — -never for the pre-launch down state itself. The =:ENABLED:= guard above tests -whether telega is INSTALLED (=fboundp=), not whether the server is up; a down -server never disables the source. - -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 *after the launch was attempted* (docker image -missing, server crash on start, daemon unreachable, Ready never reached), the -sweep reports it as SCAN FAILED at the top of the summary per the engine's -failure rule — never as a silent skip. This does NOT cover the ordinary -pre-launch down state: a down server means "run Step 1," not "SCAN FAILED." -Craig gets real traffic here, so a blind sweep that skipped the launch is worse -than a clean failure — it hides real unread messages behind a false all-clear. - -** 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). |
