Telegram notification onboarding

August 12, 2026 · View on GitHub

This guide documents the bundled Telegram notification setup path from Gajae-Code source. In an interactive GJC session, use /settingsNotifications as the recommended path; gjc notify remains the authoritative headless and automation fallback. It is for the managed reference client, not a separate remote-control product.

What you are setting up

Gajae-Code notifications use the SDK session runtime, the global Broker index, and a managed Telegram provider supervisor:

  • each eligible GJC session registers its exact endpoint generation with the Broker;
  • SDK-core SessionRouter resolves endpoint authority, keeps URL/token credentials private, and presents only opaque current-generation attachments to Telegram;
  • the Telegram supervisor owns getUpdates, rate limits, retries, topics, messages, callbacks, and delivery receipts;
  • replies and inline button taps route through the Router attachment to the exact session/action. Only coordinator/lifecycle sessions are represented by Telegram topics; ordinary sessions use flat delivery.

The setup command stores global notification settings in your GJC agent config and later sessions auto-connect when notifications are enabled.

1. Create a Telegram bot with BotFather

Use Telegram's official BotFather flow to create a bot and copy its HTTP API token:

In Telegram, open @BotFather, run /newbot, choose a display name and a unique username ending in bot, then copy the token BotFather returns. Treat the token like a password: do not paste it into logs, screenshots, issues, or shell history that other people can read.

In an eligible running GJC session, open /settings and select the Notifications tab. It provides the interactive Telegram setup/reconfigure flow and the operational controls in one place:

  • Enable globally with stored credentials or disable globally;
  • turn notifications on or off for the current session only;
  • refresh or probe health, send a test notification, recover dead-owner artifacts, and reconnect the Telegram runtime;
  • remove Telegram credentials without removing configured Discord or Slack adapters.

Telegram token entry is a masked setup field. After entry, the token is never prefilled, rendered, or shown by the tab; status and health use a masked value. The tab also guides the BotFather Threaded Mode check and private-chat pairing.

CLI setup fallback

gjc notify setup retains the same setup workflow for terminal-driven setup and automation:

gjc notify setup

Current implementation path: packages/coding-agent/src/cli/notify-cli.ts.

The wizard does this:

  1. prompts for Telegram BotFather token:;
  2. validates the token with Telegram getMe;
  3. verifies private-chat Threaded Mode capability via getMe.has_topics_enabled and, when it is off in an interactive run, prints @BotFather guidance and lets you retry or continue unverified;
  4. asks you to message the bot from a private Telegram chat;
  5. polls Telegram getUpdates until it sees a private chat message;
  6. writes the paired chat id and enables notifications.

The setup pairing flow is private-chat only. If setup sees a group, supergroup, or channel, it rejects that chat and keeps waiting for a private DM. This is intentional for safe local discovery: group chats must not receive session names, action ids, or pending status by accident.

Telegram private-chat topics: the managed daemon's coordinator/lifecycle delivery uses Telegram forum topics (createForumTopic + message_thread_id). Telegram now supports forum topics in private chats when the bot owner enables Threaded Mode for the bot in @BotFather. GJC cannot enable Threaded Mode through the Bot API; setup only detects the capability (getMe.has_topics_enabled) and guides the manual BotFather toggle. A forum-enabled supergroup is no longer required.

Note: enabling topics in private chats may require an additional Telegram Stars purchase fee, per Telegram's Terms of Service for Bot Developers.

If BotFather's Bot Settings menu does not show Threads Settings or Threaded Mode, do not treat that as a setup blocker. Telegram exposes this capability unevenly across clients/accounts/bot states, and GJC cannot force the menu to appear through the Bot API. The safe fallback is to continue setup with a private DM pairing: choose skip in the interactive prompt (or use --token <botToken> --chat-id <chatId> for non-interactive setup). GJC will save threaded=unverified/threaded=unknown, try topics at runtime when possible, and otherwise deliver flat to the paired private chat with outbound notifications and inline ask buttons only plus the one-time nudge shown below.

Setup verification is capability verification, not a delivery guarantee: even when setup reports threaded=verified, the first runtime createForumTopic for the paired chat can still fail if Telegram refuses it. When orchestration-session topics are unavailable, the daemon does not drop notifications — it routes them to the normal (flat) paired chat and posts a one-time nudge: Flat Telegram private chat supports outbound notifications and inline ask buttons only. Enable Threaded Mode in @BotFather > Bot Settings > Threads Settings for free-text replies and session commands. Because pairing is private-only, flat delivery lands in your own private DM with the bot.

The final setup line reports a threaded= status:

  • threaded=verified: the bot has Threaded Mode capability (has_topics_enabled was true during setup);
  • threaded=unverified: Threaded Mode was off and you skipped, or setup ran non-interactively; setup is saved, topics are attempted when available, and runtime delivery falls back to the paired flat private chat with outbound notifications and inline ask buttons only when Telegram refuses topic creation;
  • threaded=unknown: the Telegram response did not include has_topics_enabled, so capability could not be verified.

After setup succeeds, it prints a masked token and the paired chat id:

Notifications enabled. botToken=1234…(len N) chatId=123456789 threaded=verified

The raw token is never printed by GJC status/setup output after it is stored.

3. Non-interactive setup and CLI operations

For headless provisioning, scripts, and automation, the authoritative commands remain gjc notify setup, gjc notify status, gjc notify health, gjc notify test, and gjc notify recovery. The /settings tab does not replace these CLI subcommands.

For scripts or CI-style local provisioning, pass the bot token and known private chat id explicitly. Non-interactive runs cannot prompt for the BotFather toggle, so if Threaded Mode is off (or the capability is unknown) setup is still saved with a warning and a threaded=unverified/threaded=unknown status:

gjc notify setup --token <botToken> --chat-id <chatId>

Optional redaction can be enabled during setup:

gjc notify setup --token <botToken> --chat-id <chatId> --redact

--redact sets notifications.redact = true. Under redaction, idle summaries and streamed content are suppressed before remote delivery, but ask questions and options remain readable because they must be answerable remotely.

4. Check status without leaking secrets

gjc notify status

The status command reports the global master plus each provider's independent configuration completeness, repair/quarantine state, durable desired-intent source, and effective enablement. Stored tokens are masked with the shared first 4 chars + … + length helper. Destination identifiers such as Telegram chat IDs remain visible and may be sensitive, so redact them before pasting a status report into a public support thread. Runtime readiness and actual delivery outcomes remain separate; use gjc notify health --provider telegram and gjc notify test --provider telegram for those checks.

5. Global configuration, adapters, and precedence

Telegram credentials and all notifications.* values are global-only. GJC reads them from the user/global agent config with schema defaults; notification keys from project config files are ignored, and runtime notification overrides are rejected. A project cannot supply, shadow, or disable an outbound notification identity.

gjc notify setup writes these global Telegram settings through the GJC Settings layer:

  • notifications.enabled = true
  • notifications.telegram.enabled = true (durable desired intent)
  • notifications.telegram.botToken = <token>
  • notifications.telegram.chatId = <paired chat id>
  • notifications.redact = true only when --redact was passed
  • notifications.telegram.streaming.enabled = true by default; set it to false to disable durable live Telegram assistant-output updates globally. GJC_NOTIFICATIONS_STREAM=1 forces process-local streaming, while 0, off, or false forces it off.

Provider completeness, malformed-state quarantine, desired intent, effective enablement, runtime readiness, and delivery outcome are separate status dimensions. Telegram is complete when its bot token and private-chat id are valid; it is effective only when it is complete, not quarantined, desired on, and the global master is on. Provider-local malformed values are quarantined without erasing safe sibling values or secrets. Removing Telegram is adapter-local: it removes only Telegram credentials and sets Telegram desired intent off without changing notifications.enabled or any Discord/Slack state.

Three gates keep SDK hosting, provider setup, and managed delivery separate:

  1. An eligible host receives the dormant notification control surface. GJC_NOTIFY=off, 0, or false is a hard process opt-out; unsupported hosts and helper/subagent sessions are also ineligible.
  2. Every eligible top-level session hosts its local SDK endpoint and registers exact authority with the Broker by default, independently of notification configuration. GJC_SDK_DISABLE=1 opts out for that session. SessionRouter alone reads the endpoint credential and manages replay/reconnect.
  3. A managed Telegram supervisor is ensured only for a complete global Telegram configuration with managed delivery enabled. It reconstructs opaque attachments from Broker state; Discord-only, Slack-only, and environment-only sessions do not start a Telegram supervisor.

Environment/session precedence for managed delivery is implemented in packages/coding-agent/src/sdk/bus/config.ts:

For a GJC-spawned child, notifications.sessionScope=primary suppresses managed notification delivery to avoid duplicate topics; all permits it. GJC_NOTIFICATIONS=1 or GJC_NOTIFICATIONS_TOKEN explicitly opts that child in, but never overrides a hard opt-out or a helper/subagent exclusion.

Managed-delivery precedence is highest first; it does not change independently hosted SDK endpoints:

  1. GJC_NOTIFY=off, 0, or false prevents the notification control surface for that process.
  2. GJC_NOTIFICATIONS=0 suppresses automatic generic current-session admission; explicit /notify on may override that suppression only for the current session.
  3. Local /notify off disables managed delivery only for the current session.
  4. GJC_NOTIFICATIONS=1 or GJC_NOTIFICATIONS_TOKEN enables the legacy explicit managed-delivery path.
  5. A complete global configuration enables managed delivery automatically.
  6. Otherwise managed delivery stays off; the SDK endpoint remains hosted unless GJC_SDK_DISABLE=1 is set.

6. Start or reuse sessions

After setup, start GJC normally:

gjc --tmux

or use any other supported GJC launch mode. Every eligible top-level session writes its SDK endpoint unless GJC_SDK_DISABLE=1; when managed Telegram delivery is configured and enabled, it also ensures the Telegram daemon is running.

The managed daemon is a singleton per bot token/chat pair. Telegram allows only one active getUpdates long-poll owner for a bot token, so GJC keeps a local daemon lock/state file and makes later sessions attach to the fresh owner instead of starting a second poller. This avoids Telegram 409 Conflict failures.

Same-token and foreign-owner safety

Setup and reconfigure never compete with a live same-token daemon. When a live owner already has the stored paired chat, GJC reuses it after non-polling validation. If that owner has no stored chat or the chat changes, provide a validated private chat id; GJC performs zero getUpdates discovery polls. For a foreign or unknown owner, setup does not poll, kill, reload, or take over the owner; the default is to cancel before writing configuration.

For a Telegram-only setup, an explicit Save inactive for later choice may store the credentials with notifications disabled. That choice is unavailable when a complete Discord or Slack adapter is active, because globally disabling notifications would affect that adapter. A post-save identity race similarly stops the current session before reporting that activation is blocked; the foreign daemon remains untouched, and the editor offers an explicit restore or retain-configuration choice.

7. Use the Telegram chat

The managed daemon prefers Telegram forum-topic delivery for coordinator/lifecycle session routing in the paired private chat. When Threaded Mode is available for the bot (verified during setup via getMe.has_topics_enabled), the daemon calls createForumTopic/editForumTopic and sends messages with message_thread_id against the paired notifications.telegram.chatId. If BotFather does not show Threads Settings/Threaded Mode, or if Telegram refuses topic creation even after setup reported threaded=verified, the daemon routes notifications to the normal (flat) paired private chat and posts a one-time nudge to enable Threaded Mode rather than dropping them.

Ask-control capability negotiation

The production Telegram multiplexer is packages/coding-agent/src/sdk/bus/telegram-daemon.ts. It already sends a protocol-v3 ClientHello with ask_controls_v1 and ask_selected_ack_v1. The generic packages/coding-agent/src/sdk/bus/managed-daemon.ts is liveness-only: it advertises client_ping_pong but is intentionally non-capable for controlled asks.

Telegram navigation controls appear only after ask_controls_v1 is negotiated on that session connection. A non-capable or older third-party client receives the non-actionable action_unavailable diagnostic instead of a controlled ask with stripped option buttons, so it cannot be left with unusable controls.

Flat private chat is notification-only plus inline ask buttons. It is not a free-text chat surface: replies typed as normal messages and session commands such as /verbose, /lean, /verbosity, and /redact require Threaded Mode/topic routing.

Flat private-chat fallback preserves outbound notifications and inline-button answers, but it cannot provide a separate Telegram topic per orchestration session. Free- text replies and in-topic config commands depend on topic routing, so enable Threaded Mode in @BotFather > Bot Settings > Threads Settings when you need multi-session reply separation or session commands from Telegram. Do not pair a group, supergroup, or channel as a substitute: setup intentionally accepts only a private DM, and hand-edited non-private chat ids remain fail-closed to avoid leaking session data. If you specifically want group topics, create a forum-enabled Telegram group and use a separate/custom notification integration; the bundled gjc notify setup onboarding path is private-chat only.

The managed daemon can render:

  • session identity headers;
  • context updates;
  • live/finalized assistant output;
  • image attachments;
  • ask prompts with inline buttons;
  • activity/typing indicators;
  • inbound delivery acknowledgements.

Per-tool activity is off by default so important notifications remain visible. This includes bash, read, task, and subagent start/completion bubbles, including both ok and error results. Send /toolactivity on in the paired private chat to opt in globally, or /toolactivity off to suppress these bubbles again. The toggle is durable, works without an active GJC session, and has an equivalent control under /settingsNotificationsPreferences. Turning it off does not affect assistant output, ask prompts, or session notifications.

Reply paths:

  • tap an inline button on an ask notification;
  • reply in the session topic with free text when forum-topic routing is available;
  • send in-topic config commands:
    • /verbose — per-tool-turn assistant text (and opt-in live streaming)

    • /lean — settled assistant answer when the agent reaches idle, plus immediate ask lead-ins (default; no intermediate tool-turn flood)

    • /verbosity <lean|verbose>

    • /redact <on|off>

    • /btw <question> is available only in an authorized, known private-session topic. It uses the current session context in an isolated side turn and never injects or persists either a user or assistant message in the main session history, so it can run while the main session is busy. It accepts no attachments; /btw with an attachment returns Usage: /btw <question>. Foreign bot-command suffixes are silently ignored.

      Each logical session permits at most two concurrent side questions. The host deadline is 120 seconds and cancels the actual provider work. Operational responses are: Usage: /btw <question> for an empty question; Telegram /btw is disabled in local settings. when disabled; Restart this GJC session to enable /btw. when the connected session does not support side turns; Two /btw questions are already running. Wait for one to finish. when busy; This /btw question timed out after 120 seconds. Send it again to retry. on timeout; This /btw question stopped because the GJC session closed or changed. Reopen it and try again. when stopped; and This /btw question failed. Send it again to retry. on failure.

      A transient reconnect to the exact session may deliver a result once. Graceful GJC or daemon shutdown cancels side questions. Crashes or identity changes do not promise delivery, and stale results are fenced. /btw rich replies use Telegram Bot API 10.1 Markdown only. An eligible, complete structured Markdown reply is sent once as {rich_message:{markdown,skip_entity_detection:true}}, correlated to the source message in the same topic; GJC does not send native blocks or media. Eligibility is conservative: valid Unicode; at most 32,768 scalars, 131,072 UTF-8 bytes, 500 blocks, 16 nesting levels, and 20 table columns. Tables and math use Telegram's 10.1 Markdown support. Ineligible content and a definite rich rejection use the existing correlated HTML delivery. Ambiguous rich outcomes never retry or fall back; /rich off keeps HTML-only behavior.

  • send paired-chat lifecycle commands from the Telegram command menu or by typing:
    • /session_create path <dir>
    • /session_create worktree <repo> <branch>
    • /session_create dir <newdir>
    • /session_recent [create|resume]
    • /session_close <sessionId>
    • /session_resume <sessionId|prefix>

The removed legacy /answer <session-tag> <answer> flow is not the primary UX; Telegram topic routing identifies the target session when the configured chat supports it.

/btw operational rollback

notifications.telegram.btw.enabled defaults to true and is the local kill switch. Disabling it consumes /btw without forwarding it to the session. To roll back, restart the Telegram daemon, and probe health:

gjc config set notifications.telegram.btw.enabled false
gjc daemon restart telegram --json
gjc notify health --probe

8. Local /notify inside a session

Inside a running GJC session, /notify controls the current session only; it does not edit global config or credentials:

  • /notify status reports current session notification status without secrets;
  • /notify off disables the current session endpoint and removes its discovery record without changing global setup;
  • /notify on explicitly re-enables the current generic session when a complete effective provider or another explicit environment path is available.

GJC_NOTIFICATIONS=0 suppresses automatic generic current-session admission only. An explicit /notify on may override that one automatic-admission suppression for the current session; it does not alter durable provider intent or enable a direct provider API. GJC_NOTIFY=off, 0, or false remains the hard process-level opt-out and exposes no notification control surface to override.

Troubleshooting

Telegram getMe failed

The BotFather token is invalid or was revoked. Re-copy the token from BotFather or regenerate it in the official BotFather UI.

Setup times out waiting for a private chat

Send any message directly to the bot from your Telegram user account. Do not add it to a group for pairing; groups/supergroups/channels are intentionally rejected by the current setup flow.

Setup succeeds but no Telegram session messages arrive

Check the threaded= status from the last gjc notify setup run. If it is threaded=unverified or threaded=unknown, first try the current Telegram client's @BotFather flow for this bot. If BotFather's Bot Settings menu lacks Threads Settings/Threaded Mode, continue with the saved private-chat pairing; this is supported. GJC cannot enable Threaded Mode through the Bot API, and no paid/Stars option is required just to receive flat private-chat notifications. When createForumTopic is refused for the paired chat, the daemon falls back to flat delivery in the paired private chat and posts a one-time nudge that points to @BotFather > Bot Settings > Threads Settings. Flat fallback is limited to outbound notifications and inline ask buttons; free-text replies and session commands require Threaded Mode/topic routing.

Managed adapter lacks ask controls

A custom client must not attach to a GJC session transport. Upgrade or configure the bundled managed Telegram adapter; SessionRouter performs its internal capability negotiation and rejects unsupported controlled asks without exposing the endpoint or credentials. Third-party controllers use Coordinator MCP or the SDK session CLI instead.

Telegram 409 conflict

Only one getUpdates poller can own a bot token. GJC never takes over a fresh foreign or unknown owner. If you own the other process, stop or reconfigure it, then use gjc notify health, gjc notify recovery, or gjc notify reconnect; recovery removes only dead-owner artifacts and never touches a live owner.

A session does not send notifications

Check, in order:

  1. gjc notify status and confirm Telegram is complete, not quarantined, desired on, and effective
  2. the session has not run /notify off; when GJC_NOTIFICATIONS=0 suppresses automatic admission, run /notify on explicitly
  3. the Broker reports the session as live with a current endpoint generation
  4. the Telegram supervisor is ready and SessionRouter has reconstructed the attachment
  5. the provider owner state is fresh under the GJC agent notifications directory

Endpoint discovery records contain per-session credentials. They are SDK-core implementation details and must not be copied into provider state or public issues.