aboutsummaryrefslogtreecommitdiff
path: root/working/triage-telegram-down-launch/triage-intake.telegram.org.proposed
diff options
context:
space:
mode:
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.proposed290
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).