OpenPlotva is a Telegram bot + web service runtime in Rust (19-crate workspace, edition 2024), migrated from a Go original (go-plotva): agentic AI dialog with tool-calling turns, media generation (image/music/vision/youtube), bitemporal memory, content-safety shield, Telegram Stars payments, a token-driven admin Web UI, and an operator GraphQL runtime API. Single process backed by Postgres 17 + pgvector and Dragonfly (Redis).
Stack: Rust 1.99.0 · tokio · axum · async-graphql · sqlx 0.9 (Postgres-only) · carapax (Telegram) · pgvector · redis · rustls · handlebars (prompts).
Structure: openplotva-app is the composition root (binary+lib; lib.rs ≈15.6k lines owns RuntimeWorkers, run(), start_runtime_workers); domain crates (core, dialog, agent, memory, history, shield, taskman, updates) hold pure logic and must not depend on web/Telegram/SQLx/vendor SDKs; integration crates (telegram, llm, storage, server, web, media) own vendor boundaries; config/observability/prompts are cross-cutting leaves. Two axum servers run concurrently: the public web server and a TLS-terminated runtime API (Bearer-token GraphQL). All outbound sends flow through a persisted, debounced, deduped dispatcher queue.
For detailed architecture, module-by-module guide, data-flow diagrams, schema overview, gotchas, and a navigation guide, see docs/CODEBASE_MAP.md.
- Answer the user in their language; write code, comments, docs, and commits in English unless asked otherwise.
- Verify repository facts before claiming them. Do not describe crate ownership, runtime flow, commands, or deployment state from memory.
- Favor broad product progress over auxiliary scaffolding. Avoid placeholders, provenance notes, and speculative abstractions.
- Dirty user changes are normal in this migrated repository; account for them and continue unless they directly conflict with the task. Never reset, clean, push, deploy, restart services, or rewrite history unless explicitly asked in the current request.
- Never commit secrets: live
.envvalues, Telegram tokens, provider keys, database dumps, Redis snapshots, private file IDs, or smoke inputs. - For throwaway Rust builds and smokes, use task-specific
CARGO_TARGET_DIRand temporary/log directories when practical. At completion, remove those task-owned artifacts plus disposable containers, images, volumes, and task-scoped container-builder caches; never clean shared Cargo or Docker caches indiscriminately. - Report exact verification commands and results. If checks are skipped, say so and name the risk.
- Prefer self-explanatory code: clear names, typed data, small cohesive functions, and explicit boundaries over comments.
- Add comments only for non-obvious intent, invariants, protocol constraints, concurrency/lifetime reasoning, or safety notes.
- Remove obsolete comments instead of updating them mechanically. Do not leave TODOs, commented-out alternatives, or release-irrelevant history in code.
- Use
cargo fmt --allafter Rust edits. Match nearby conventions before introducing new patterns. - Use typed errors in library/domain crates; use
anyhowat application boundaries. - Avoid
unwrap()in runtime code. Useexpect()only for local, obvious invariants. - In async code, do not hold locks across
.await; make cancellation, queue draining, and worker lifetime explicit.
openplotva-appis the composition root for config wiring, runtime startup, handlers, workers, and HTTP route assembly.- Domain crates must not depend on web, Telegram, SQLx, or vendor SDKs unless the crate owns that integration boundary.
- Declare traits and adapters close to their consumers. Extend existing crates before creating new top-level crates.
- Keep vendor request/response types inside the owning integration crate or app boundary.
openplotva-telegramowns Bot API builders, transport helpers, update sources, callback helpers, HTML handling, deduplication, rate limits, and outbound persistence.openplotva-storageowns SQLx Postgres, Redis stores, embedded migrations, and persistence adapters.openplotva-webowns admin/settings assets, WebApp signatures, login/auth helpers, and cookie/signature primitives.- Provider-specific LLM, media, memory, shield, history, payment, and queue code should stay in the crate/module that already owns that boundary.
- Prompt templates stay as
.promptfiles underprompts/; preserve partial names, role markers, cache-sensitive system text, and template data shape.
- Treat these as public contracts: Telegram payloads and callback data, commands, HTTP routes, GraphQL/runtime API shape, DB schema meaning, Redis keys/order/TTLs, prompts, shipped web assets, provider/payment requests, and operator-visible diagnostics.
- Preserve payload shape, lifecycle behavior, ordering, TTLs, error visibility, and security checks unless the user explicitly approves a behavior change.
- Prefer the smallest Rust-native implementation that preserves the contract. Do not copy convoluted internal control flow when the external artifact is the same.
- Sanitize Telegram-visible HTML at the boundary before sending, splitting, deduping, or persisting.
- Treat Settings WebApp state, Telegram login data, payment callbacks, provider credentials, and admin/runtime tokens as security boundaries.
- The admin UI (
web/admin/) is a token-driven component library. Route every interactive element through thepl-*custom elements (web/admin/components.js) and token-backed classes; never write raw<button>/<input>/<select>/<textarea>/<table>,onclick=/onsubmit=,el.onclick =, inlinestyle=, hex/rgb()colors, or nativealert()/confirm()inweb/admin/. - Feedback is non-blocking via
PL.toast; dialogs usePL.alert/PL.confirm. Every async load shows loading (skeleton/pl-tablestate), empty, and error states. - Colors/spacing/type/motion/elevation/z-index live only in
web/admin/tokens.css.admin.css/components.cssreference tokens; no literals. - Editing or adding any
web/asset requires updating the matchingsha256constant incrates/openplotva-web/src/lib.rsand runningcargo test -p openplotva-web(the guard + hash tests enforce all of the above). - Run the
openplotva-design-system-reviewskill before merging admin UI changes. The Settings WebApp (web/settings/, Framework7 + Telegram theme) is a separate boundary, out of scope for this library.
- Formatting:
cargo fmt --all. - Focused Rust tests:
cargo test -p <crate> <filter>. - Local/runtime smokes when relevant:
tools/local-smoke.sh,tools/service-smoke.sh,tools/provider-smoke.sh,tools/update-queue-smoke.sh,tools/live-update-injection-smoke.sh,tools/container-smoke.sh.
The default path for delivering every feature or fix into the repository:
- Branch + ready PR. Ship each change on its own
feat/...orfix/...branch and open a ready-for-review PR intomain. Do not create draft PRs by default: draft PRs skip the full PR-Agent review path in this repository. Use a draft PR only when the user explicitly asks for one or when you intentionally need to prevent automated review before the branch is ready. Never commit straight tomain, and never self-attribute the work to Claude or any AI in branch names, commit messages, or PR text. - Green locally first. Before opening the PR, run
cargo fmt --all,cargo clippy --workspace --all-targets -- -D warnings, and the relevantcargo test. For anyweb/admin/change, update the matchingsha256incrates/openplotva-web/src/lib.rs, runcargo test -p openplotva-web, and run theopenplotva-design-system-reviewskill. - Open the PR, then start the PR delivery loop. After the feature branch is pushed and the PR exists, monitor it on a ~60s cadence until it is mergeable. Every iteration must poll both CI/check status and review artifacts: issue comments, review summaries, review decisions, and inline review comments/threads.
- Treat review artifacts as work, not notifications. When a new or edited comment, review, or inline thread appears, read the full latest body before acting. If it is valid, make the smallest necessary code/doc/test change, rerun the relevant local checks, commit, push, and restart the loop. If it is wrong, harmful, already addressed, or low-value churn, reply with the concrete reason. Inline review threads must be replied to and resolved; non-thread comments get a direct explanatory reply when needed.
- Do not let metadata edits interrupt review. Editing a PR body/title, replying to Danger, or other metadata/comment maintenance must never cancel an in-progress PR-Agent/Qodo review. The PR automation workflow must not run on
pull_request.edited; if a new review is needed, trigger it explicitly withworkflow_dispatchinstead of relying on an edit event. If PR-Agent was cancelled before publishing its review/suggestions, rerun that workflow and wait for it before merge. - Bots edit ONE comment in place — re-read bodies, never gate on counts. PR-Agent keeps a single "Persistent review / PR Reviewer Guide" comment (plus a "PR Code Suggestions" comment) and Qodo keeps a review comment; they EDIT it on every push, so the comment count and the unresolved-review-thread count do NOT change when new findings appear. On every poll, re-fetch and read the FULL body of each bot review comment (
gh api repos/<owner>/<repo>/issues/comments/<id>, or match by author/title) — do not detect changes by comment count or thread count. Treat every finding in the LATEST body as open until it is fixed in a commit or rebutted in a reply; the bot's "updated to latest commit<sha>" line tells you which HEAD its findings apply to. PR-Agent also posts its code suggestions as inline review-comment threads (gh api repos/<owner>/<repo>/pulls/<N>/comments) — a separate artifact from the persistent guide comment; fetch and read those thread bodies every poll too, not only the guide. - Handle CI inside the same loop. If a check fails, inspect the failing job/logs rather than waiting for reviewers. Fix the root cause, rerun the narrow local reproduction plus the relevant broader check, commit, push, and continue polling. Do not merge while any required check is pending, failing, cancelled for a real reason, or stale against an older commit. A lone
CANCELLED/failing PR-Agent code suggestions check is non-blocking only when the suggestions were still published as inline threads and handled; if cancellation happened before artifacts were published, rerun the workflow and wait. - Merge only when fully green AND every finding is handled. Immediately before merging, re-read the FINAL body of every bot review comment (PR-Agent persistent review + PR code suggestions, Qodo review, Danger) at the exact commit you are merging, and confirm each finding is fixed-in-a-commit or rebutted-in-a-reply — not merely that the comment/thread counts are unchanged. Every inline review thread must additionally be replied to and resolved (GraphQL
resolveReviewThread), so the unresolved-thread count is genuinely 0 at the merge commit. Do not tell the user a PR is "ready to merge" until you have actually read each inline suggestion body and resolved its thread — stating the criteria is not the same as verifying them. Thengh pr merge <N> --merge. - Deploy only when the user asks. Deploy is the
deploy-production.ymlworkflow run againstmain(authorized by the repo owner). After it reports success, spend a couple of minutes verifying: the running image matches the merged commit, the service is healthy, and recent logs are error-free; spot-check the changed behavior against real data where you can. Report what you verified and any remaining risk. If a check is skipped, say so and name the risk. - Migrations ship as numbered up/down pairs with compatibility notes and a representative SQLx/storage check. Data backfills are one-way — say so in the down migration. Build indexes on hot or large tables with
CREATE INDEX CONCURRENTLY(outside a transaction) so the migration does not block writes during deploy.