Skip to content

feat(branding): channel-only hop gate, link/@ replace maps, prompt library, branding failure trail - #342

Merged
Opselon merged 9 commits into
masterfrom
feat/branding-suite-upgrade
Nov 6, 2026
Merged

Opselon merged 9 commits into
masterfrom
feat/branding-suite-upgrade

Conversation

@Opselon

@Opselon Opselon commented Nov 6, 2026

Copy link
Copy Markdown
Owner

What this PR does

Upgrades the branding/replace/AI suite end to end — one shared decision per behavior, implemented across the Python engine, the Go worker and all three clients (Telegram bot, dashboard, mobile).

1. Branding hop (A → C → B) with the channel-only gate

  • New branding_require_channel gate: a message written by a normal user fails the gate and is delivered directly with its own "Forwarded from <user>" header; channel content (a native channel post in A, or a forward whose origin is a channel) takes the branded hop. Composes with branding_require_forward.
  • MessagePayload.chat_type rides the job payload so the Go worker re-decides the gate at claim time from the same inputs Python used.
  • Cross-engine parity: 5 new fixtures in worker/internal/parity/reference_branding.json (regenerated from the live Python decision); the Go harness now carries chat_type and the origin type.
  • Surfaces: bot smart menu "📡 Channel-only hop" toggle (rsmrc:), dashboard RuleModal checkbox + hint, mobile settings sheet (registry row, localized), REST settings group branding.

2. Replace engine — "replace any link found" / "replace any @id"

  • link_replace_enabled + link_rewrite_map: * rewrites every link to one target, a domain key rewrites one domain, a URL-prefix key rewrites the deep links it starts with (entity-aware, both engines).
  • New mention_rewrite_map: @old → @new (case-insensitive) or * → @main for every @id — applied in the final replacement pass after the AI stage, so a rewrite cannot resurrect a replaced handle. Go port avoids RE2's missing look-around with index-based boundary checks that make the same decisions.
  • Surfaces: bot smart menu → 🔁 Link & @replace maps (rmap: panel, one-pair-per-line editors, - clears), dashboard RuleModal section (checkbox + two textareas), mobile registry rows, REST settings.

3. AI rewrite composition + prompt library

  • Pipeline order (Python and Go): cleaner → link engine → content policy → AI rewrite → final replacements (rules + mention map) → header/footer. The branding hop posts the post-transform text into C, and B receives it without re-processing — so with AI rewrite enabled, a branded task delivers the rewritten message through C to B, once, per branding channel.
  • prompt_templates.enabled column (guarded ALTERs on SQLite/Postgres, backup export/restore carry it). A disabled template stays in the library (preview/test keep working) but deliveries fall back to the task's AI-config prompt ([PIPELINE:AI_REWRITE:PROMPT_OFF]; the Go store resolves it as not found).
  • New bot prompt manager (AI → 🧠 Prompts): list, create, rename, edit system prompt / user template / description, test against a real provider, delete with confirmation, enable/disable; system templates view/test only; the per-rule prompt picker badges disabled templates. Mobile gains the same library (CRUD + test) over /api/prompts.

4. Failure trail into the Rust log store

BRANDING_HOP_FAILED / BRANDING_HOP_EMPTY (Python audit events), BRANDING_STAGE1_FAILED / BRANDING_STAGE2_FAILED (worker logger attrs forwarded by the fanout), and DELIVERY_BLOCKED is reported as BRANDING_STAGE2_FAILED when the job died inside the hop — one Log Center filter per failure class.

5. i18n — 10 locales, 1:1 parity everywhere

Bot catalog +79 keys ×10 (incl. registry labels/help for the four new settings and a localized sentence for the new reason code), catalog re-sorted into the batch writer's canonical order; web locales +12 ×10 + regenerated settingLabels.generated.ts; mobile catalogs +32 ×10. audit_cross_language, i18n_audit, the telegram literal guard and both UI-copy gates pass.

Tests

  • New: core/tests/test_replace_maps.py (24), core/tests/test_prompt_enabled.py, worker/internal/domain/cleaning/mentions_test.go, links_star_test.go (table-driven), 5 new branding parity fixtures.
  • Updated: branding group pin, Go registry parity expectations, prompt_test.go-compatible store.
  • Full worker + api Go suites pass; the Python suite shows zero new failures vs master (the only locally-unrunnable tests are the async ones — no pytest-asyncio in this environment; logic verified directly with asyncio.run).

Docs

docs/branding-and-replace.md (the flows, the composition, the failure trail, the worker configurability), feature matrix rows X13–X16, coverage doc, README changelog.

…y, failure trail

Branding hop (A -> C -> B)
- new require_channel gate (branding_require_channel): messages written by
  normal users fail the gate and are delivered directly with their own
  "Forwarded from <user>" header; channel-originated content (a native
  channel post in A, or a forward whose origin is a channel) takes the
  branded hop. Composes with branding_require_forward.
- Python core (branding.should_hop + entity bridge + registry) and the Go
  worker port stay decision-identical: 5 new cross-engine parity fixtures
  (reference_branding.json regenerated; the Go harness carries chat_type
  and the origin type).
- the job payload now carries chat_type so the worker re-decides the gate
  at claim time from the same inputs Python used.
- smart menu gains the "Channel-only hop" toggle (rsmrc:) beside the
  forward-gate and force-forward toggles; dashboard RuleModal gains the
  checkbox + hint; the registry exposes the key to the settings surfaces.

Replace engine ("replace any link found" / "replace any @id")
- link_replace_enabled upgrades a task with no stricter link policy to
  REWRITE_LINKS; link_rewrite_map gains the "*" catch-all (every link to
  one target) and URL-prefix keys (one deep link) alongside domain keys.
- new mention_rewrite_map: @old -> @new pairs (case-insensitive) with the
  same "*" catch-all, applied in the final replacement pass after the AI
  stage so a rewrite cannot resurrect a replaced handle. Go port
  (cleaning/mentions.go) is index-based where RE2 lacks look-around, with
  the same decisions.
- dedicated bot panel (smart menu -> "Link & @replace maps", rmap:) with
  one-pair-per-line editors; dashboard RuleModal section (checkbox + two
  textareas); registry rows render on the mobile settings sheet.

Prompt library: enable/disable + the missing bot manager
- prompt_templates gains an enabled column (guarded ALTER on SQLite,
  ADD COLUMN IF NOT EXISTS on Postgres, schema.sql updated, backup
  export/restore carry it); PromptService.set_enabled switches a template
  without touching its versions.
- delivery semantics: a disabled template stays in the library (preview
  and the tester keep working) but the pipeline falls back to the task's
  AI config own prompt ([PIPELINE:AI_REWRITE:PROMPT_OFF]); the Go prompt
  store resolves a disabled template as not found.
- the bot gets the global prompt manager it never had (AI -> Prompts):
  list, create, rename, edit system prompt / user template / description,
  test against a real provider, delete with confirmation, enable/disable;
  system templates are view/test only. The per-rule prompt picker badges
  disabled templates and links to the manager. The mobile AI screen gains
  the same library (CRUD + test) over the shared /api/prompts contract.

Failure trail
- branding failures now carry their own event codes into the Rust log
  store: BRANDING_HOP_FAILED / BRANDING_HOP_EMPTY (Python audit events),
  BRANDING_STAGE1_FAILED / BRANDING_STAGE2_FAILED (worker logger attrs
  the fanout forwards), and DELIVERY_BLOCKED is reported as
  BRANDING_STAGE2_FAILED when the job died inside the hop.

i18n (10 locales, 1:1 parity on every surface)
- bot catalog +79 keys x10 (prompt manager, replace maps, smart-menu
  gate, registry labels/help for the four new settings, localized reason
  sentence); catalog re-sorted into the batch writer's canonical order.
- web locales +12 keys x10; settingLabels.generated.ts regenerated;
  mobile catalogs +32 keys x10 (prompt section + setting labels).

Tests
- core: test_replace_maps.py (24) and test_prompt_enabled.py pin the
  mention/link semantics, the channel gate, registry exposure and the
  prompt enable contract; branding group pin updated.
- worker: mentions_test.go, links_star_test.go (table-driven), parity
  fixtures extended; full worker + api suites pass unchanged.

Docs: docs/branding-and-replace.md (flows + composition + failure trail),
feature matrix rows X13-X16, coverage doc, README changelog.
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-06T19:46:41.328296Z
We couldn’t complete this review. Comment @codex review to try again.
46de34b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Opselon and others added 8 commits November 6, 2026 23:44
…very defects

The documented pyrogram surface (docs.pyrogram.org, verified against the
v2.0.106 tag source) is now committed as docs/pyrogram-methods-2.0.106.json
— 163 high-level methods with exact signatures, 104 types, 14 enums,
handlers, the generated error classes and the raw TL schema counts.

scripts/audit_pyrogram_surface.py holds the repository to it: every
client-method call site's kwargs and every pyrogram.errors/enums/types
import must resolve 1:1. Today: 125 call sites, 130 symbol imports — PASS.

Two real defects fell out of the audit; both failed real deliveries and
are regression-pinned in telegram_runtime/tests/test_documented_surface.py:

1. get_album_message_ids called get_messages(chat_id, limit=40) — the
   pinned get_messages has no `limit` parameter (a Bot-API concept), so
   the resolver raised on every album and worker album deliveries degraded
   to single copies. It now uses the documented one-to-one method
   get_media_group(chat_id, message_id).

2. message_thread_id does not exist on the pinned forward_messages /
   copy_message / copy_media_group / send_message / send_* signatures (a
   Bot-API parameter official pyrogram's MTProto layer never grew), so a
   task pointed at a forum topic failed EVERY delivery with TypeError —
   on both the live dispatcher/queue path and the Go Worker's runtime
   adapter. New core/infrastructure/telegram/pyrogram_compat.py probes the
   installed client's signature once and threads the kwarg only when the
   method accepts it: on the pin a topic task delivers to the chat's
   General topic; on a fork that grew the parameter, topic targeting works
   unchanged. The adapter's five delivery methods and the durable queue /
   dispatcher thread_kwargs construction are wired through it.

docs/pyrogram-api-map.md documents the full one-to-one map, including the
Go Worker's gateway RPC -> runtime handler -> pyrogram call resolution
table and the failure taxonomy each hop reports.
…ip review 0-100

Full ownership-and-resolution review of the Go worker (claim lane, send
fence, tenant scoping, outcome lifecycle, reconciler, gRPC client, store
security). Verification: go vet, full go test ./... -count=1, -race on
internal/queue, internal/engine/..., internal/ops and test/ownership —
all green; findings and evidence in docs/worker-ownership-review.md.

Two crash-class defects found and fixed:

1. No recover() anywhere in the worker: one panicking payload crashed the
   process, the restart re-claimed the SAME job and panicked again — a
   crash loop stalling the whole queue. Engine.Process now contains the
   panic (CRITICAL log + stack + JOB_PANIC_CONTAINED event code), hands
   the job to the retry budget (bounded, dead-letters at exhaustion) and
   deliberately leaves the fence untouched: a post-send panic keeps the
   fence's terminal SENT record, an unrecorded one ages out to UNKNOWN
   like every other ambiguity. The ops collector gains a dedicated
   "panicked" outcome counter (never folded into Telegram "failed"),
   exposed through /healthz. Regression-pinned by
   panic_containment_test.go: the poisoned job retries with one spent
   attempt and a healthy job right after it still delivers SENT.

2. backoffFor multiplied 2^attempts in one step: 2^63 seconds already
   overflows an int64 nanosecond duration, so a high-attempt job wrapped
   to a near-zero backoff and retried in a tight loop instead of waiting
   out the 5-minute cap (the table test caught backoffFor(99) == 1s).
   Replaced with an overflow-free doubling loop that returns the cap as
   soon as it is reached; TestBackoffForIsBoundedExponential pins the
   table including the overflow classes.

docs/worker-ownership-review.md records the model as reviewed (two-phase
claim lane, pre-read fence with never-stolen CLAIMED/SENDING rows,
evidence-gated reconciler, tenant-scoped filter/AI-config/prompt reads,
parameterized SQL, AES-256-GCM provider keys, redaction choke points)
and the deliberate non-changes with their reasons.
…es in the dispatch harness

Two CI failures, two root causes:

1. python-tests: the bot dispatch harness swaps filters.regex for its fake
   in every REGISTERING_MODULES entry — the new replace_maps_bot_ui and
   prompt_bot_ui mixins were missing from that list, so their real
   pyrogram filters (which expose .p, not .matches) read as "matches
   everything" and rmap: swallowed every press. Both modules registered.

2. flutter: the l10n catalogs were rewritten by sync_mobile_catalog.py,
   whose serializer turned the files' literal \n escapes into real
   newlines inside single-quoted strings — invalid Dart, one syntax error
   per catalog. All ten files restored from master and the 32 new keys
   re-inserted in place (quote-escaped, no rewriter run): the diff is now
   exactly 32 added lines per file.
- mobile: import core/models.dart in prompt_templates_section.dart.
  ErrorKind is declared there; the missing import made flutter analyze
  fail with undefined_identifier at line 64.
- bot: spell the prompt-manager test verb as CB_PRM_TEST. The literal
  verb == "pt" is a callback verb (prompt test), not a language tag,
  but it matched the language-tag audit's Portuguese pattern and failed
  test_no_language_tag_is_compared_in_code. Same wire format, one
  spelling, no false positive.
@Opselon
Opselon merged commit fd7406a into master Nov 6, 2026
16 checks passed
@Opselon Opselon mentioned this pull request Nov 6, 2026
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.

1 participant