Skip to content

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

Merged
vybe merged 3 commits into
devfrom
feature/600-telegram-group-context
Sep 27, 2026
Merged

vybe merged 3 commits into
devfrom
feature/600-telegram-group-context

Conversation

@trinity-ability

@trinity-ability trinity-ability commented Sep 11, 2026 •

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". 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.
  • 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), clear_sessions_by_identifier (the off-switch purge), database.py facade.
  • Schema, dual-track: db/schema.py, db/tables.py, db/migrations.py::telegram_group_context, migrations/versions/0079_telegram_group_context.py (re-parented onto 0078_workspace_suggestion_feedback after merging dev) — 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 return context_status/context_hint; context_enabled arm is human-only. models.py fields. Switching context_enabled off calls services/channel_history.py::purge_telegram_group_history; a broadcast to a group with context off is not persisted. 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.

Frontend / MCP

  • TelegramChannelPanel.vue — Group context BaseToggle + BaseBadge status + hint per group, Verify reloads groups. Completion reports also became a BaseToggle; raw-colour ratchet flat at 64. channels.ts list_channel_groups passes context_status, and client.ts declares the new fields on listTelegramGroups() (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

  • 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), J14 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. architecture/integrations.md gains a Telegram group conversation context section; architecture/backend.md catalog updated.

Test Plan

Notes for review

  • test_untagged_bare_command_does_not_fire failed only in full-suite runs: 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.
  • tables.py server_default for context_enabled left out on purpose: tables.py declares none anywhere, and NULL reads as ON.
  • Rejected on purpose: rate-limiting observation (it would punch silent holes in context; observation is one small write). The last_update_id cross-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.
  • Enterprise-tracker feature built OSS-core (the Telegram adapter is OSS; precedent ent#557/ent#535/ent#556). No submodule pointer in this PR.

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 seed — regression 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 grew — build 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

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

⚠️ Alembic head fork if this PR is merged into dev.

dev has advanced since this PR's checks last ran. GitHub recomputes the merge ref when the base moves but does not re-trigger workflows, so a green schema-parity here can describe a base that no longer exists (#2533).

scripts/ci/check_alembic_heads.py against dev + this PR, merged in memory
alembic-heads: FAIL — src/backend/migrations/versions resolves to 2 heads across 80 revision(s); exactly 1 is required.
  • 0059_telegram_group_context  (0059_telegram_group_context.py)
  • 0078_workspace_suggestion_feedback  (0078_workspace_suggestion_feedback.py)

They fork at: 0058_portal_file_dismissals  (0058_portal_file_dismissals.py)

`alembic upgrade head` is singular and resolves its target BEFORE applying anything,
so this graph applies ZERO revisions — every revision since the fork stops arriving,
not only the one that forked. Fix by chaining the newer revision off the real head,
or — if the forked revision may already be applied somewhere — by adding a merge
revision (`alembic merge -m "…" <head-a> <head-b>`), whose tuple `down_revision`
converges the line from any starting state. See Architectural Invariant #3.
alembic-heads: src/backend/enterprise/backend/migrations/versions — version directory absent (submodule not initialised) — skipped.

Evaluated on the version line only — this PR also conflicts with dev in 6 unrelated file(s), which do not change this verdict but must be resolved before merge:

docs/memory/learnings.md
docs/testing/JOURNEYS.md
scripts/ci/generate_journeys_md.py
src/backend/db/migrations.py
tests/journeys/catalog.yaml
tests/unit/test_2338_journey_catalog.py

Fix: rechain this PR's revision off 0078_workspace_suggestion_feedback (the current dev head), or — if the forked revision may already be applied somewhere — add a merge revision (alembic merge -m "…" 0059_telegram_group_context 0078_workspace_suggestion_feedback), whose tuple down_revision converges the line from any starting state. See Architectural Invariant #3.

Push the fix and schema-parity re-checks it against the merge ref immediately; this comment clears 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

@vybe

vybe commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

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. journey-smoke fails at src/tools/channels.ts(106,35):

error TS2339: Property 'context_status' does not exist on type
'{ id: number; binding_id: number; chat_id: string; chat_title: string | null;
   chat_type: string; trigger_mode: string; welcome_enabled: boolean;
   welcome_text: string | null; is_active: boolean; created_at: string;
   updated_at: string; }'

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. tests/unit/rawColorRatchet.spec.js fails in build: "Raw colour usage GREW. Use semantic tokens (design-system-contract.md)" — 1 failed, 2697 passed. Per the design-system contract a grown ratchet has to be paid down with semantic tokens, or the increase argued and re-frozen in its own commit. That's a judgment call, which is why I didn't regenerate the baseline for you: regenerating would silently ratify whatever grew.

3. A conflict in src/backend/db/migrations.py. docs/memory/learnings.md and docs/testing/JOURNEYS.md also conflict, and both of those are routine — but a migrations conflict is a re-apply over someone else's schema change and needs your eyes, especially since this PR is +schema and the dual-track rule (Invariant #9) means the SQLite entry and the Alembic revision have to stay consistent with whatever dev moved underneath.

Also worth knowing: the failing runs are from 2026-09-11, so they predate nine days of dev. Merge dev in first — the picture may change once the three conflicts are resolved, and e2e and regression diff were also red but produced no retrievable log, so they're worth a fresh run rather than a diagnosis from those runs.

Once dev is merged, the type is declared and the ratchet is settled, this comes back onto a train. The feature itself wasn't the problem — I didn't get far enough to assess it, and that assessment is worth redoing against green CI rather than inheriting today's read.

@vybe

vybe commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

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. 0059_telegram_group_context declares down_revision = "0058_portal_file_dismissals", which is byte-identical to dev's own 0059_agent_canvases_pinned. Two revisions sharing a parent are two heads, and alembic upgrade head is singular — it resolves its target before applying anything, so on the merged tree it applies zero revisions. Not just this one: every revision merged since the fork stops arriving, currently 0059 through 0064. Git reports no conflict, because each file is individually valid and the defect exists only in the relationship.

The single-head guard passes on this PR alone, because against dev in isolation it is a single head — the fork only exists after the merge. Re-parent onto the current head. Note #2920 is ahead of you in the queue and takes 0065_agent_skills_delivery_status, so re-parent onto whatever is head when you push, and renumber off the 0059 prefix — it now collides with a shipped revision's number even though the ids differ.

2. The MCP surface does not compile (build and journey-smoke, both red):

src/tools/channels.ts(106,35): error TS2339: Property 'context_status' does not exist on type
  '{ id: number; binding_id: number; chat_id: string; ... }'

channels.ts:106 reads g.context_status, and the backend does add it (models.py:3859, populated at routers/telegram.py:367) — but the MCP client's listTelegramGroups() return type was never widened, so the field the whole ent#600 status feature exists to surface cannot reach a consumer through the third surface. Invariant #13: backend router, agent server, MCP tool must move together.

3. The raw-colour ratchet grew (Build (typecheck) + unit):

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

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: src/backend/db/migrations.py conflicts with dev (the SQLite half of the same two-track change), and every check on this branch is from 2026-09-11 — ten days of dev has moved underneath it, so expect more once it re-runs.

I did not repair this in-train. The ratchet growth needs your argument, and the TS fix needs a decision about where context_status belongs in the client types — neither is a mechanical edit.

@vybe

vybe commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

merge-train — not on the 2026-09-21 train; needs a rebase and a re-parent first

Nothing 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 dev, last commit 2026-09-11.

2. Four checks red since that commit: journey-smoke, build, e2e, regression diff.

3. Conflicts in src/backend/db/migrations.py, plus docs/memory/learnings.md and docs/testing/JOURNEYS.md. The first is not one of the routine classes the train resolves — the SQLite migration tail moved a long way in 103 commits.

4. The Alembic revision forks against dev itself. 0059_telegram_group_context declares down_revision = "0058_portal_file_dismissals" — but dev has carried 0059_agent_canvases_pinned off that same parent since long before today, and is now at 0064. So this is already a two-head graph against the branch it targets, and upgrade head would apply zero revisions rather than fail loudly. It needs re-parenting onto the head that exists when it lands (0064, or whatever #2920 leaves behind), not just a rebase.

Rebase onto dev, re-parent the revision, and get the four checks green; it can ride the next train after that.

@vybe

vybe commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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 7cc42f2ef; the branch has not moved since 2026-09-11:

  1. bug: enterprise submodule pin advance lands two Alembic heads — entitled instances silently degrade to OSS-only #2068 Alembic fork — 0059_telegram_group_context still declares down_revision = "0058_portal_file_dismissals"; dev is now at 0067_agent_role_readiness (and 0068 lands on this train). Re-parent onto the current head.
  2. Conflicts in src/ with dev (CONFLICTING / DIRTY) plus 100+ commits behind — needs a rebase, not a merge-train resolution.
  3. Four checks red on the PR's own head: journey-smoke (TS2339 context_status in src/tools/channels.ts), build, e2e, regression diff.

Given six ejections with no movement, consider closing this and re-opening off a fresh dev when the feature is picked up again; a re-parent alone will not clear item 2.

@vybe

vybe commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

merge-train (2026-09-23): not included in this run — rides a later train once reworked:

  • Alembic fork (bug: enterprise submodule pin advance lands two Alembic heads — entitled instances silently degrade to OSS-only #2068 class): 0059_telegram_group_context has down_revision = "0058_portal_file_dismissals", but dev is now at 0071_seat_decisions. Merging as-is creates two heads and upgrade head applies zero revisions. Re-parent onto the current head (renumber to 0072, down_revision = "0071_seat_decisions") and renumber the SQLite entry in db/migrations.py to match.
  • Conflicts with dev outside the routine classes: src/backend/db/migrations.py, scripts/ci/generate_journeys_md.py, tests/journeys/catalog.yaml, tests/unit/test_2338_journey_catalog.py, docs/testing/JOURNEYS.md (plus learnings.md).
  • Stale failing checks (2026-09-11): journey-smoke, build, e2e, regression diff — re-run after the rebase.

@AndriiPasternak31

Copy link
Copy Markdown
Contributor

/validate-pr — ❌ REQUEST CHANGES

State: CONFLICTING, 135 commits behind dev; head unchanged since 2026-09-11, so all CI results are stale. It conflicts in db/migrations.py, learnings.md, JOURNEYS.md, scripts/ci/generate_journeys_md.py, tests/journeys/catalog.yaml and tests/unit/test_2338_journey_catalog.py. dev has not overtaken the feature. Given the repeated train ejections, re-opening off a fresh dev (as already suggested) looks cheaper than a rebase.

Blocking

  1. Alembic fork. migrations/versions/0059_telegram_group_context.py:23-24 has parent 0058_portal_file_dismissals, the same parent as dev's 0059_agent_canvases_pinned. Re-parent onto the current head (0072_agent_capability_grants at the time of writing) and redo the SQLite entry.

  2. The MCP server doesn't compile. src/mcp-server/src/tools/channels.ts:106 reads context_status, which the return type in client.ts:2418-2430 doesn't declare (TS2339). The mcp-server image build fails, so journey-smoke and e2e never exercised anything.

  3. The raw-colour ratchet grew. TelegramChannelPanel.vue goes from 64 to 69 raw gray classes, and the new help text (~L221) uses light-theme text-gray-400, which design-system-contract.md:18 forbids.

  4. The PR's own test fails in the full suite. test_untagged_bare_command_does_not_fire is red on all 3 CI head seeds (regression diff). It passes in isolation, so it looks like a test-isolation leak; the root cause was not confirmed.

  5. The off switch doesn't match the requirement "OFF ⇒ nothing recorded". With context off:

    • tagged messages and every agent reply are still written to the shared group session
    • in all/observe modes every message is still stored
    • nothing is purged on toggle-off, so re-enabling replays whatever is still inside the 24 h window

    Either fix the behaviour or fix the requirement text.

Warnings

  • Retention. The 40-message/24 h cap limits only what is read into the prompt. Stored rows in public_chat_messages never expire by time. Pruning to 500 rows runs only on the untagged-record path, every 50th insert, so groups in all/observe mode are never pruned.
  • Prompt injection is handled in part. The history block is delimited and sanitised, but:
    • the forged-delimiter strip has no test (mutating it stays green)
    • reply_quote_line sits outside the block and isn't escaped
  • The toggle's reject_agent_principal isn't pinned by any test (removing it stays green). A group with no config row fails open (reads as ON).
  • Once the "sees all messages" badge is set, it never clears, even if Privacy Mode is re-enabled.
  • The TELEGRAM_GROUP_CONTEXT_* env knobs aren't wired into compose or .env.example, so they do nothing on deploy.
  • Docs: architecture/backend.md:226 and integrations.md weren't updated.
  • tables.py is missing server_default for context_enabled (harmless, since NULL reads as ON).

Clean:

  • no cross-group or DM leakage (the session key is per chat, plus the thread id only for real forum topics)
  • the badge is backed by real getMe and evidence
  • owner-only auth on the toggle
  • the adapter hooks have safe defaults
  • no new broadcasts

Run:

  • the PR's tests plus neighbours: 157 passed
  • a 12-file neighbour set × 3 seeds: 348 passed, 2 failed; both failures are in test_186 and also fail on the merge-base
  • 6 mutations
  • the heads check, the raw-colour scanner, and merge-tree

Not run: the full unit suite locally, tsc locally, and a live Telegram bot.

vybe and others added 2 commits September 27, 2026 12:22
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>
@vybe

vybe commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Review items addressed — 865e471d6 (merge dev) + 651d3c1ea (fixes). Ready for the next train once CI is green on the new head.

Blockers from the train ejections and /validate-pr:

  1. Alembic fork — revision is now 0079_telegram_group_context, down_revision = "0078_workspace_suggestion_feedback" (dev's head). check_alembic_heads.py: 80 revisions, 1 head. SQLite entry appended after dev's tail; check_alembic_parity.py PASS.
  2. Conflicts — all six resolved (migrations.py, learnings.md, the journey catalog/generator/test, JOURNEYS.md regenerated). The journey is J14 now; dev took J12/J13.
  3. TS2339 — client.ts listTelegramGroups() declares context_enabled / context_status / context_hint / last_untagged_seen_at. tsc --noEmit clean (it fails with TS2339 before the change).
  4. Raw-colour ratchet — TelegramChannelPanel.vue raw_gray back to 64 (was 69). Paid down, not re-frozen: Group context and Completion reports are both BaseToggle; the light-theme text-gray-400 help text is gone.
  5. test_untagged_bare_command_does_not_fire — a test-order leak, not the code: test_slack_multi_connection.py / test_slack_watchdog.py stub adapters.transports.base at collection with a ChannelTransport that has no on_event. Reproduced by running those files first, fixed by asserting at on_event; still fails if the observe_only gate is removed.
  6. Off switch — made the behaviour match the requirement. Off ⇒ no rows for tagged turns, replies, observed messages or broadcasts, and switching off purges the group's sessions (chat + forum topics), so re-enabling never replays the window.

Warnings: pruning now runs on every group write path; reply_quote_line is escaped; the stale Sees all messages badge clears when getMe reports Privacy Mode on; env knobs are wired into .env.example + the three compose files; the context_enabled gate and the delimiter strip are pinned by tests; integrations.md / backend.md are updated. tables.py server_default is left out (no server_default anywhere in that file; NULL reads as ON).

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 (test_2915_operator_queue_sync_honesty, test_ent477_definitions_endpoint), and plain dev fails the same 9 at that seed, so they predate this PR.

🤖 Generated with Claude Code

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-check group_context_enabled at step 11.
  • telegram_webhook.py:104-118: a bare /reset in all/observe mode clears the whole group's shared context. The requirement documents this only for /reset@bot.
  • TelegramChannelPanel.vue:212: text-gray-400 on 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-81 still swaps adapters.transports.base in sys.modules with no restore, which is the leak that made the bare-command test flaky.

@vybe
vybe merged commit ebc2bfb into dev Sep 27, 2026
30 checks passed
vybe pushed a commit that referenced this pull request Sep 27, 2026
…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>
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.

3 participants