Skip to content

feat(telegram): a tagged group turn knows the group's recent conversation (abilityai/trinity-enterprise#600) - #2728

Open
trinity-ability wants to merge 1 commit into
devfrom
feature/600-telegram-group-context
Open

trinity-ability wants to merge 1 commit into
devfrom
feature/600-telegram-group-context

Conversation

@trinity-ability

Copy link
Copy Markdown
Contributor

Summary

  • A tagged turn in a Telegram group is answered in the context of what the group has been saying. Un-tagged messages the bot receives are recorded as attributed context (no agent turn, no reply, no typing, no reaction, no rate-limit charge); every group turn — mention, all, observe — is built as sender identity + a bounded, attributed "recent group conversation" block (newest 40 within 24 h) + the tagged message. Trigger rules are unchanged.
  • The group's session is keyed per chat, not per sender ({bot_id}:group:{chat_id}, :topic:{thread} in forum supergroups). That is the re-decision bug: proactive group messages (send_group_message) never persisted to channel session history #1649 pinned a test to force, and it closes bug: proactive group messages (send_group_message) never persisted to channel session history #1649's "agent can't recall its own broadcast" limitation for free. DMs keep their per-user key, so DM history structurally cannot reach a group reply — the reason the old fresh-context rule existed still holds. MEM-001 stays group-excluded.
  • Honest status per group in the Telegram panel: Sees all messages (an un-tagged message actually reached the bot here — evidence beats the getMe flag, so admin bots read right) / Tagged messages only (Privacy Mode on; hint names /setprivacy → Disable, re-add the bot, or make it admin) / Not confirmed yet / Context off. getMe.can_read_all_group_messages is stored at connect, Verify, and when the bot is added to a group.
  • Two additions from the independent plan reviews: a zero-config slice — a tagged reply to someone else's message carries [Replying to X: "…"], which works with Privacy Mode on; and a per-group context_enabled off switch (default on, owner/human-only like allow_proactive) so "how do I turn this off for the legal channel" is never "remove the bot".
  • Trigger rules stayed unchanged only because the transport now checks: once un-tagged messages parse instead of returning None, a bare /reset or /help from any member would have fired un-tagged. The command branch is gated on observe_only; /reset@bot (now recognised as tagged) is the deliberate group-reset gesture. Both independent reviewers found this; the lesson is in docs/memory/learnings.md.
  • The tagged user turn is persisted before execution, so messages posted during a long run sort after the one they answer and a failed run keeps the message in memory.

Changes

Backend

  • services/telegram_group_context.py (new) — bounds + env overrides, format_group_history (NO_REPLY filter, one-line + 500-char clamp, label/delimiter neutralisation, [agent] marker), reply_quote_line, group_context_status, fetch_can_read_all_group_messages.
  • adapters/telegram_adapter.py — group session key; parse_message marks untagged / observe_only, treats /cmd@bot as tagged, sets group speaker labels + topic_id; group_context_enabled / note_untagged_seen hooks; getMe refresh on bot-added (best-effort, after the config write).
  • adapters/base.py — the two hook defaults. adapters/transports/telegram_webhook.py — command branch gated on observe_only.
  • adapters/message_router.py — step 0 _record_observed_message; step 7 group history block + reply quote + persist-before-execute; step 11 skips the already-persisted user row; untagged evidence stamp.
  • db/public_chat.py (since=, prune_session), db/telegram_channels.py (columns, touch_group_untagged_seen, set_can_read_all_group_messages, context_enabled), database.py facade.
  • Schema, dual-track: db/schema.py, db/tables.py, db/migrations.py::telegram_group_context, migrations/versions/0059_telegram_group_context.pytelegram_bindings.can_read_all_group_messages, telegram_group_configs.last_untagged_seen_at, telegram_group_configs.context_enabled DEFAULT 1. Single Alembic head verified.
  • routers/telegram.py — flag stored at connect/Verify; listing + PUT return context_status/context_hint; context_enabled arm is human-only. models.py fields. services/channel_history.py docstring.

Frontend / MCP

Docs / journey

  • Requirements TGRAM-GROUP-CTX (§15.1e-ctx), feature flow (Group Conversation Context section, flow diagram, trigger matrix, schema, UI), user docs (trigger table gains a Context column; status badges; /reset@bot), J12 skeleton in tests/journeys/catalog.yaml (built: no; the generator now renders a fully-qualified private-tracker ref), docs/memory/learnings.md, CSO diff report.

Test Plan

Notes for review

Fixes abilityai/trinity-enterprise#600
Also lifts the recall limitation #1649 recorded (see test_1649_group_message_history.py)

🤖 Generated with Claude Code

…tion (abilityai/trinity-enterprise#600)

In a Telegram group the agent still speaks only per the group's trigger mode,
but it now knows what the group has been saying when it does. Un-tagged
messages the bot receives are recorded as attributed context without an
agent turn (no reply, typing, reaction or rate-limit charge); every group
turn is built as sender identity + a bounded, attributed "recent group
conversation" block + the tagged message.

The group session is keyed per chat, not per sender — the re-decision #1649
pinned a test to force — so proactive broadcasts now land in the session a
reply reads. DMs keep their per-user key: DM history structurally cannot
reach a group reply, which is what the old fresh-context rule protected.

Honest per-group status (sees all / tagged only / not confirmed / off) with
the next action named; getMe's can_read_all_group_messages stored at
connect, Verify and bot-added. Zero-config reply-quote slice works with
Privacy Mode on. Per-group context_enabled off switch (human-only arm).

The transport gates its command branch on observe_only: once un-tagged
messages parse instead of returning None, a bare /reset from any member
would otherwise have fired — trigger rules must not change. /reset@bot is
now recognised as tagged. The tagged user turn is persisted before
execution so stored order matches what the group saw.

Dual-track schema: can_read_all_group_messages on telegram_bindings,
last_untagged_seen_at + context_enabled on telegram_group_configs
(migrations.py telegram_group_context + Alembic 0059). MCP
list_channel_groups passes context_status. J12 journey skeleton.

Fixes abilityai/trinity-enterprise#600
Follow-up filed: #2727 (pre-existing update_id dedup race across workers)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@github-actions

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

@vybe

vybe commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-12: not on this train. Three things, all the PR's own:

  1. Own test red on every seedregression diff names test_ent600_telegram_group_context::test_untagged_bare_command_does_not_fire as a new failure under HEAD on all three seeds (base is 0 failed). Deterministic, not a flake.
  2. Raw-colour ratchet grewbuild fails on src/frontend/src/components/TelegramChannelPanel.vue — raw_gray 64 → 69. A growth is paid down (semantic tokens) or argued in its own re-freeze commit; the train regenerates baselines only when the count shrank.
  3. Conflicting with dev, and 0059_telegram_group_context is parented on 0058_portal_file_dismissals — the same parent as 0059_agent_canvases_pinned (feat(canvas): delete, pin, search and a stated bound for the canvas pile (ent#553) #2619) and 0059_execution_fan_out_task_id (feat(pull): async fan-out join + sync edge adapter — no autonomous trigger is stranded (#2524) #2532). Whichever of the three lands first, the other two must re-parent (the bug: enterprise submodule pin advance lands two Alembic heads — entitled instances silently degrade to OSS-only #2068 two-heads shape: upgrade head applies zero revisions and git reports no conflict).

Rides the next train once the test is green, the ratchet is flat or paid, and dev is merged.

@vybe

vybe commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-13: not on this train. Three failures the PR owns, plus a schema fork:

Also CONFLICTING with dev. Rides the next train once fixed.

@github-actions

Copy link
Copy Markdown

🚧 Alembic head check could not run — this PR conflicts with dev.

git merge-tree reported conflicts, so there is no merged tree to check. GitHub cannot compute refs/pull/N/merge in this state either, which is why a conflicting PR shows no checks at all.

Merge dev into this branch and push. The head check re-runs automatically on the next push to dev touching src/backend/migrations/versions/**.

Advisory — this check does not block merge. · head_sha: 7cc42f2efc6adce6cddb170faedaf1806e89ddff · run

@vybe

vybe commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

merge-train: ejected this run — rides the next train once fixed. Three things, all on the branch: (1) Alembic fork (#2068 class): 0059_telegram_group_context has down_revision = 0058_portal_file_dismissals, but dev already has 0059_agent_canvases_pinned off that same parent (from #2619). Two heads → upgrade head applies zero revisions on PostgreSQL. Re-parent to 0059_agent_canvases_pinned (and renumber to 0060_…); check the SQLite migrations.py version number for the same collision. (2) tests/unit/rawColorRatchet.spec.js fails in the build job — regenerate the baseline against current dev if the count shrank, or pay it down if it grew. (3) journey-smoke, e2e and regression diff are red from 2026-09-11 — re-run after (1)/(2) to see what's real. Also conflicts with dev in learnings.md and docs/testing/JOURNEYS.md (routine, keep both).

@vybe

vybe commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

merge-train: not on this train — two blockers

Rides the next train once both are fixed. Neither is visible on this PR's checks, which is the notable part.

1. Live #2068 Alembic fork — upgrade head would apply zero revisions

src/backend/migrations/versions/0059_telegram_group_context.py declares down_revision = "0058_portal_file_dismissals". So does 0059_agent_canvases_pinned.py, already on dev. Merged, the version line has two heads, and since alembic upgrade head is singular and resolves its target before applying anything, every revision merged since the fork stops arriving — not just this one. Git reports no conflict, because each file is individually valid and the defect exists only in the relationship.

Run against the actual merged tree (origin/dev + this PR), not the branch:

alembic-heads: FAIL — resolves to 2 heads across 64 revision(s); exactly 1 is required.
  • 0059_telegram_group_context  (0059_telegram_group_context.py)
  • 0062_execution_fan_out_task_id  (0062_execution_fan_out_task_id.py)
They fork at: 0058_portal_file_dismissals

pg-migrations ✅ and schema-parity ✅ on this PR do not contradict that — on the branch alone it is a single head. The gate only fails on the merged graph, and schema-parity last ran against a base that no longer exists (#2533).

Fix: re-parent to down_revision = "0062_execution_fan_out_task_id" (dev's current head) and renumber the file/revision to 0063_telegram_group_context.

2. The raw-colour ratchet grew on this PR's own diff

src/frontend/src/components/TelegramChannelPanel.vue — raw_gray 64 → 69

build fails on it, which cascaded to e2e, journey-smoke and regression diff. This is your own diff adding the classes (I checked — the added lines in that file carry them), not a baseline stale against dev, so the ratchet is working as designed: per design-system-contract.md the increase has to be paid down with semantic tokens, or argued and re-frozen in its own commit. Not something a merge train should decide for you.

Everything else looked good — six pytest seeds green, CodeQL clean, prod-image-smoke and verify-non-root green.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants