Cartographer: fix OOM when backing up servers with many attachments - #283
Open
RWolfyo wants to merge 5683 commits into
Open
Cartographer: fix OOM when backing up servers with many attachments#283RWolfyo wants to merge 5683 commits into
RWolfyo wants to merge 5683 commits into
Conversation
… improving user guidance and validation
…support) (vertyco#269) * Update quickpull command to stop reimplementing Downloader (+ 3.5.25 support) * Fix formatting * Whoops!
- Update image URL pattern to allow query parameters
…elper function for finding channels by name
Add [p]levelset prestige emoji <level> <emoji> to update the emoji of an existing prestige level in place, instead of having to remove and re-create the level.
…tion Lets the VrtUtils RPC bridge insert + schedule a task in-process (build model, store, save, ensure_jobs) with duplicate guarding, so scheduled commands can be added without the interactive menu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enables out-of-process skill editing (read-before-write) via the RPC bridge; rpc_list_skills intentionally omits body, so edits had no way to read current state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ENDPOINT uses named placeholder {guild_id} but three call sites passed
guild.id positionally, raising KeyError on member update/remove and
cleanup revert.
- Fix save_conf wiping live conversations in non-persistent mode - Fix startup cleanup deleting guild config/embeddings during gateway degradation - Fix phantom reminders re-firing after reload (deletion never persisted) - Fix stale embedding migration re-dropping the live ChromaDB collection - Fix listener crash on replies to deleted messages - Fix unguarded auto-answer embedding call dropping messages on API errors - Fix tool-result trim note char count and None-author log crash - Realign MixinMeta signatures with implementations - Token counting: single cached o200k_base encoding, no per-model tables, model param removed from all counting helpers - Add gpt-5.6-sol/terra/luna (1.05M ctx, vision, tools, reasoning)
…o#281) * Assistant - fix GPT-5.6 family reasoning_effort + tools error The gpt-5.6-sol/terra/luna models reject function tools on Chat Completions unless reasoning_effort is explicitly 'none'. Omitting the param (as done for gpt-5.4/5.5) is not enough because 5.6's default effort is non-none, so the API errored with: Function tools with reasoning_effort are not supported for gpt-5.6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'. Split 5.6 into its own branch and force reasoning_effort='none' whenever tools are present; 5.4/5.5 keep the omit behavior. * Assistant - route gpt-5.4/5.5/5.6 reasoning+tools through Responses API These models can't combine a configurable reasoning_effort with function tools on /v1/chat/completions. Add a thin, stateless adapter that routes exactly those calls to /v1/responses and translates the request/response back into Chat Completions shapes, so conversation storage, tool-call parsing, and usage/cache accounting are untouched. - responses.py: to_responses_input / to_responses_tools / responses_to_chat_completion translators (handles text, vision parts, tool calls, tool results, reasoning summaries, usage mapping). - calls.py: needs_responses_api() predicate + request_responses_raw(); request_chat_completion_raw delegates when reasoning+tools are both needed on the 5.4/5.5/5.6 family. Everything else stays on chat completions unchanged. - Conversation store stays chat-format (source of truth); translation is per-call with store=False, so mid-convo model switching still works. Non-tool turns, other models, and reasoning_effort=none are unaffected. * Assistant - simplify tool_call dict access in responses adapter messages is list[dict] by contract, so drop the misleading isinstance guard that only covered one of three tc.get() call sites. * Assistant - remove em-dashes from responses adapter docs Plain-language cleanup of the module docstring and changelog entry. * Assistant - remove em-dashes from touched files Replace em-dashes with commas/colons/periods across calls.py error strings, constants.py prompt text, and CHANGELOG entries.
Adds an opt-in heads-up DM (notify_users, default on) sent once as a user nears (warn_days, default 3) or enters inactivity decay, reset when they become active again. New admin cmds: [p]bankdecay notify / setwarndays. Closes the recurring 'nobody told me' complaint on decay tickets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…decays One-time notify_migrated flag marks all already-tracked users as warned on first load, so enabling the heads-up DM doesn't blast the ~hundreds already inactive/decaying. Active users reset on next activity; only decays that begin after this ships will DM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BankDecay - use server currency in decay DM, surface DM failures BankDecay is a public cog but the inactivity decay warning DM hardcoded Vertycos "VertCoin" title. Use the guilds own bank currency name so it reads correctly on any server. When the warning DM fails (user has DMs closed/blocked), log a warning and post a notice to the configured log channel so staff can see the user never got warned. @
The RPC handlers were inline in vrtutils.py and limited to rpc_master and rpc_quickpull. Split them into rpc.py as an RPCMethods mixin and add the moderation suite ops automation needs: warn, unwarn, get_warnings, get_cases, timeout, untimeout, modnote. The stock Warnings/Mod command callbacks need a Context built from a real Message, which an RPC caller has no way to produce. The work underneath (config write, modlog case, DM) needs no ctx, so these reimplement it directly: warnings key off a generated snowflake so [p]unwarn still works on them, and cases file through modlog.create_case, so [p]listcases sees them like any human action. Native Discord timeouts have no stock casetype; the cog registers timeout and untimeout on load (register_casetypes is idempotent). Every mutating method takes an explicit moderator_id rather than inferring one, so a case always attributes to whoever the caller names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Admins can set a wait after a ban before a first appeal (bancooldown, read from the target guild audit log) and a wait after a denial before re-appealing (reappealcooldown, when appeal_limit > 1). Both default disabled, take human durations, bypass admins, and show in [p]appeal view. Adds decided_at to submissions to track decision time. Version 0.3.0.
Fires member + payload (guild, channel, amount_spent, amount_awarded, valid_purchases, currency_name, purchases) after credit deposit so other cogs can react to claims. Restores the 0.3.0 change that was lost from the working tree before it was committed.
…ides, point decay, history (0.1.0)
Manage PalWorld dedicated servers from Discord over the official REST API. RCON is unsupported on purpose: Pocketpair deprecated it and the REST API covers everything here. - 30 second poll loop with join/leave and online/offline log embeds - Live status panel (Components V2) with per-server counts and a 12 hour player graph rebuilt from stored sessions, no snapshot table - Per Discord server graph timezone, IANA names, label tracks DST - Player database: identity, name history, IP history, sessions, playtime - Lookups: palstats, paltop, palstatus, palplayers, paltools findplayer - Staff controls: announce, kick, ban, unban, save, confirmed shutdown - paltools settings reads the /settings endpoint, headline values in an embed with the full payload attached as JSON - Interactive server manager for adding, editing, testing and removing - Backup and restore to move a setup between bots or databases, remapping every row id so a dump can land anywhere - findplayer only reveals IPs with logips on, a mod or higher asking, and a channel @everyone cannot read
The join/leave embeds are replaced with one arktools-style line per event, batched into a single message per poll tick: 🟢 `Name, Account, userId` joined **Server** (Lvl 21, 78ms) 🔴 `Name, Account, userId` left **Server** (2m) Player IPs are dropped from the log entirely rather than gated behind logips, which now covers findplayer alone. Addresses are still recorded. Since names now reach Discord as message content rather than embed text, mentions are suppressed on send and backticks are stripped from the identity, so a crafted name cannot ping the guild or break out of the code span.
… memory Every message attachment was downloaded, base64-encoded and kept on the model until the whole guild had been serialized, so peak memory scaled with the total size of a server's media - roughly 4x it, before the JSON document was built on top. The v2.2.0 guard only rejects individual files over the guild's upload limit; it puts no ceiling on the total. A server with a busy media channel produced a 1.9GB backup document and a ~16GB RSS peak, ended by the host's OOM killer. Restoring had the same problem in reverse, since the whole file was read into a string before being parsed. Backups are now zip archives: backup.json plus an attachments/ folder. Attachments stream into the archive as they download and are read back out one at a time, so memory stays flat regardless of how much media a server has. Nothing is skipped - a large backup just takes longer. The archives are also smaller, since base64 inflated every file by a third and the JSON is now compressed. Backups written before this change still load and restore: load_backup picks the format by extension and FileBackup keeps the base64 field. Measured on a server with 52MB of attachments: 288MB -> 444MB RSS across a full backup and restore, where the previous code climbed until killed.
channel.history(limit=...) yields newest-first, and restore_channel_messages replays the stored list in order, so a restored channel was rebuilt backwards. Store messages oldest-first instead. The same most-recent N messages are still the ones backed up; only the order they are written in changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
FileBackup.serializedownloads each attachment, base64-encodes it, and holds it on the model until the entire guild has been serialized. Peak memory therefore scales with a server's total media — about 4x it, since the raw bytes, the base64 bytes and the decodedstrare all live at once, beforemodel_dump_jsonbuilds the document on top.The v2.2.0 check is per-file:
That rejects individual oversized files but places no ceiling on the total, so any number of small attachments passes straight through.
On my bot this produced a 1.9 GB backup document and a ~16 GB RSS peak against a 16 GB
MemoryMax, and the OOM killer ended the process roughly every 10 minutes —last_backupnever persisted, so every restart retried the same backup and died again. Restoring such a backup would have failed the same way, sincerestorelatestdoesmodel_validate_json(path.read_text())on the whole file.The change
Backups are written as
.zip—backup.jsonplus anattachments/folder. Attachments stream into the archive as they download; on restore they are read back one at a time. Memory stays flat regardless of media volume, and nothing is dropped: a large backup simply takes longer.A backup is still a single file on disk, so the rotation, wipe, listing and size-reporting paths are untouched.
Backward compatible.
load_backuppicks the format by extension andFileBackupkeepsfilebytesfor pre-2.3.0 backups. Existing backups load and restore unchanged; only new ones are zips.The guild upload limit check is unchanged — same
filesize_limitfallback, same comparison.Second commit
Unrelated bug found while testing:
channel.history(limit=...)returns newest-first andrestore_channel_messagesreplays the list in order, so restored channels came back backwards. Messages are now stored oldest-first; the same most-recent N are still selected. Kept as a separate commit so it can be dropped independently.Testing
Run against a live bot (Red 3.5, Python 3.11):
.jsonbackup restored successfully via the base64 fallback.Version
2.2.0 → 2.3.0 — minor rather than patch, since the on-disk backup format changes.