feat(telegram): a tagged group turn knows the group's recent conversation (abilityai/trinity-enterprise#600) - #2728
Conversation
…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>
|
Resolve by running |
|
Resolve by merging |
|
merge-train 2026-09-12: not on this train. Three things, all the PR's own:
Rides the next train once the test is green, the ratchet is flat or paid, and |
|
merge-train 2026-09-13: not on this train. Three failures the PR owns, plus a schema fork:
Also CONFLICTING with |
|
|
|
merge-train: ejected this run — rides the next train once fixed. Three things, all on the branch: (1) Alembic fork (#2068 class): |
merge-train: not on this train — two blockersRides 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 —
|
|
merge-train: ejected from today's train — rides the next one once fixed. Nothing was pushed to this branch. Three independent blockers, each needing a decision rather than a mechanical repair: 1. A TypeScript error the PR owns. The MCP tool reads a field the binding type doesn't declare — the Invariant #13 third-surface sync, where the backend router gained a field and the TypeScript proxy's type didn't. 2. The raw-colour ratchet grew. 3. A conflict in Also worth knowing: the failing runs are from 2026-09-11, so they predate nine days of Once |
|
merge-train: ejected from the 2026-09-21 train — rides the next one once fixed. Nothing was pushed to this branch. Three separate blockers, and the first one is the serious one because CI cannot see it. 1. This is the #2068 Alembic fork. The single-head guard passes on this PR alone, because against 2. The MCP surface does not compile (
3. The raw-colour ratchet grew ( Per the contract that is pay-down-or-argue, not a regenerate — either use semantic tokens for the five new ones, or re-freeze in its own commit with the reason. Also worth knowing before the re-push: I did not repair this in-train. The ratchet growth needs your argument, and the TS fix needs a decision about where |
merge-train — not on the 2026-09-21 train; needs a rebase and a re-parent firstNothing was pushed to this branch. Validated as lane B+schema. This one is not a judgement call — it has drifted too far to ride: 1. 103 commits behind 2. Four checks red since that commit: 3. Conflicts in 4. The Alembic revision forks against Rebase onto |
|
merge-train: ejected from the 2026-09-22 train — rides the next one once fixed. Nothing was pushed to this branch. Same three blockers as the five previous ejections (2026-09-14, -15, -20, -21 ×2), all still present on head
Given six ejections with no movement, consider closing this and re-opening off a fresh |
|
merge-train (2026-09-23): not included in this run — rides a later train once reworked:
|
/validate-pr — ❌ REQUEST CHANGESState: CONFLICTING, 135 commits behind Blocking
Warnings
Clean:
Run:
Not run: the full unit suite locally, |
Re-parents the Alembic revision onto dev's head (0059 -> 0079, down_revision 0078_workspace_suggestion_feedback) and renumbers the journey J12 -> J14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… MCP type, ratchet, test isolation (Abilityai/trinity-enterprise#600) Blockers from the merge-train ejections and /validate-pr: - MCP: widen listTelegramGroups()'s return type with the ent#600 fields (context_enabled/context_status/context_hint/last_untagged_seen_at); channels.ts read context_status off a type that did not declare it (TS2339), so the mcp-server image did not build. - Raw-colour ratchet: TelegramChannelPanel.vue back to raw_gray 64 (was 69). The Group context switch is a BaseToggle beside the status badge, with the backend hint as its only help text (the separate paragraph used light-theme text-gray-400, which the contract forbids); Completion reports becomes a BaseToggle too, which pays the remainder down. - test_untagged_bare_command_does_not_fire: failed only in a full-suite run because test_slack_multi_connection.py / test_slack_watchdog.py replace adapters.transports.base in sys.modules at collection with a stub ChannelTransport that has no on_event. The test now asserts the hand-off at on_event; still fails when the observe_only gate is removed. - "OFF => nothing recorded" now holds: with context off a group turn writes no user row (step 7) and no reply (step 11), broadcasts are not persisted, and the OFF write purges the chat's session and its forum-topic sessions (channel_history.purge_telegram_group_history), so re-enabling never replays the window. Warnings: - Pruning runs on every group write path (tagged turns and all/observe turns, not only observed messages), on a PRUNE_EVERY boundary crossing. - reply_quote_line neutralises delimiters, brackets and double quotes. - Storing can_read_all_group_messages=False clears last_untagged_seen_at, so the "Sees all messages" badge cannot outlive Privacy Mode being re-enabled. - TELEGRAM_GROUP_CONTEXT_* wired into .env.example and all three compose files. - Tests pin the human-only context_enabled gate, the delimiter strip (substring count), and every behaviour above; each was mutation-checked. - Docs: integrations.md gains the group-context section; backend.md catalog, requirements, feature flow and user docs updated. tables.py server_default is left as is: tables.py declares no server_default anywhere, and NULL already reads as ON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review items addressed — Blockers from the train ejections and
Warnings: pruning now runs on every group write path; Local evidence: PR test file 63 passed (each new behaviour mutation-checked); neighbours 402 passed; frontend 3680; mcp-server 550. Full unit suite at seed 12345 has 9 failures ( 🤖 Generated with Claude Code |
vybe
left a comment
There was a problem hiding this comment.
Validated at lane C + schema on 651d3c1: READY. All five earlier blockers are fixed: the revision is re-parented onto 0078 with a single head; the MCP type declares context_status; the raw-colour ratchet is paid down, not re-frozen; the bare-command test is working again, not skipped; and the off switch is traced end to end (observed, tagged, all/observe, broadcast, purge). Every check on this head is green, and a local full unit run passes (18,548 / 0 failed, one random seed).
Follow-ups, not blocking:
message_router.py:963: step 11 never re-reads the toggle. A turn that starts with context on and is switched off mid-run writes the reply to the purged session: an orphan row on SQLite, a foreign-key error on PostgreSQL that loses the reply. Fix: re-checkgroup_context_enabledat step 11.telegram_webhook.py:104-118: a bare/resetin all/observe mode clears the whole group's shared context. The requirement documents this only for/reset@bot.TelegramChannelPanel.vue:212:text-gray-400on light theme.- Observed messages only pass the per-group toggle and
any_verified, so a member the access policy would refuse still lands in the prompt (fenced as reference). Recorded as a design decision. test_slack_multi_connection.py:79-81still swapsadapters.transports.baseinsys.moduleswith no restore, which is the leak that made the bare-command test flaky.
…am_group_context (ent#641) dev's version line now runs 0074 -> 0075_auto_sync_enabled_backfill -> ... -> 0078_workspace_suggestion_feedback, and #2728 adds 0079_telegram_group_context on top. This PR's revision chained off 0074, a second head (the #2068 fork: `upgrade head` would apply zero revisions). Renamed 0075_seat_ask_class_state.py -> 0080_seat_ask_class_state.py, revision 0080_seat_ask_class_state, down_revision 0079_telegram_group_context; docstring, the SQLite migration's pointer, the ent641 test that reads the revision by path, and the architecture/requirements/flow docs updated. The SQLite entry is name-keyed and unchanged. Depends on #2728 landing first: on this branch alone the heads check reports a missing parent until 0079 is on dev. Verified with 0079 copied into a scratch copy of the versions directory: 81 revisions, 1 head (0080_seat_ask_class_state). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
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.{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.getMeflag, 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_messagesis stored at connect, Verify, and when the bot is added to a group.[Replying to X: "…"], which works with Privacy Mode on; and a per-groupcontext_enabledoff switch (default on, owner/human-only likeallow_proactive) so "how do I turn this off for the legal channel" is never "remove the bot". Off means nothing about the group is recorded — no observed messages, no tagged turns or replies, no broadcasts — and switching it off deletes what was already stored (the chat's session and its forum-topic sessions), so switching back on starts empty.None, a bare/resetor/helpfrom any member would have fired un-tagged. The command branch is gated onobserve_only;/reset@bot(now recognised as tagged) is the deliberate group-reset gesture. Both independent reviewers found this; the lesson is indocs/memory/learnings.md.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_messagemarksuntagged/observe_only, treats/cmd@botas tagged, sets group speaker labels +topic_id;group_context_enabled/note_untagged_seenhooks; 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 onobserve_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),clear_sessions_by_identifier(the off-switch purge),database.pyfacade.db/schema.py,db/tables.py,db/migrations.py::telegram_group_context,migrations/versions/0079_telegram_group_context.py(re-parented onto0078_workspace_suggestion_feedbackafter mergingdev) —telegram_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 returncontext_status/context_hint;context_enabledarm is human-only.models.pyfields. Switchingcontext_enabledoff callsservices/channel_history.py::purge_telegram_group_history; a broadcast to a group with context off is not persisted. Storingcan_read_all_group_messages=Falseclearslast_untagged_seen_at, so the Sees all messages badge cannot outlive Privacy Mode being re-enabled.Frontend / MCP
TelegramChannelPanel.vue— Group contextBaseToggle+BaseBadgestatus + hint per group, Verify reloads groups. Completion reports also became aBaseToggle; raw-colour ratchet flat at 64.channels.tslist_channel_groupspassescontext_status, andclient.tsdeclares the new fields onlistTelegramGroups()(Invariant feat: SMARTS trading pipeline with Telegram notifications and Miro visualization #13)..env.example+ all three compose files —TELEGRAM_GROUP_CONTEXT_MAX_MESSAGES/_MAX_AGE_HOURS(empty = defaults 40 / 24 h).Docs / journey
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), J14 skeleton intests/journeys/catalog.yaml(built: no; the generator now renders a fully-qualified private-tracker ref),docs/memory/learnings.md, CSO diff report.architecture/integrations.mdgains a Telegram group conversation context section;architecture/backend.mdcatalog updated.Test Plan
tests/unit/test_ent600_telegram_group_context.py— 63 tests (49 original + 14 from review: off ⇒ no rows for tagged andall-mode turns, purge on switch-off incl. forum topics and not a prefix-sharing chat id, broadcast skipped when off, human-onlycontext_enabledgate, turn-path prune on a boundary crossing, reply-quote neutralisation, delimiter strip counted as a substring, stale-evidence clear on a real DB; each mutation-checked). Original 49: parser marks, session keys, renderer (bounds, NO_REPLY, forgery), status matrix, reply quote, router observe path (records / no execute / no rate-limit / locked group / toggle off / bare command / prune cadence / db failure swallowed), group turn context + persist ordering + DM unchanged, transport command gate, adapter hooks, getMe best-effort,since/prune on a tmp SQLite engine. Written red-first: 44/49 failed on behaviour before the implementation.scripts/ci/check_alembic_heads.py— 80 revisions, 1 head (0079_telegram_group_context);check_alembic_parity.pyPASS.test_2915_operator_queue_sync_honesty,test_ent477_definitions_endpoint), identical on plaindevat the same seed — pre-existing order leaks, not this PR. Frontend vitest 3680 passed; mcp-servernpm test550 passed;tsc --noEmitclean.@bot summariseanswers from it; panel shows Sees all messages. Same bot with Privacy Mode on → today's behaviour, panel shows Tagged messages only with the BotFather hint.Notes for review
test_untagged_bare_command_does_not_firefailed only in full-suite runs:test_slack_multi_connection.py/test_slack_watchdog.pyreplaceadapters.transports.baseinsys.modulesat collection with a stubChannelTransportthat has noon_event. The test now asserts the hand-off aton_event.tables.pyserver_defaultforcontext_enabledleft out on purpose:tables.pydeclares none anywhere, and NULL reads as ON.last_update_idcross-worker dedup race the engineering review raised is pre-existing and needs a different mechanism — filed separately as bug(telegram): update_id dedup is read-then-write per worker, so an out-of-order webhook delivery drops a legitimate message #2727.Fixes abilityai/trinity-enterprise#600
Also lifts the recall limitation #1649 recorded (see
test_1649_group_message_history.py)🤖 Generated with Claude Code