Entry Point and Initialization Flow
本頁內容尚未翻譯。
src/index.ts is a thin orchestrator. All initialization logic lives in src/init/ modules.
src/index.ts: orchestrator (calls init modules in order)src/init/backup.ts: non-production automatic data backup gatesrc/init/healthServer.ts: health HTTP serversrc/init/secrets.ts: secrets loading + key manager initsrc/init/discord.ts: Discord client construction + error handlerssrc/init/database.ts: DB init, cooldown cleanup, pg_cron setupsrc/init/loaders.ts: tool registry, localizer, caches, event handlersrc/init/bridges.ts: Matrix bridge (optional)src/init/timers.ts: health tracker, scheduled work, memory monitor, cache metrics, quota cleanupsrc/types/config.ts:AppConfiginterface +resolveEnvironment()
Startup Sequence
Section titled “Startup Sequence”- Load
.env(dotenv); resolveAppEnvironment. - In non-production: run the automatic data backup gate before secrets, Discord, or database initialization. It creates a full
backupData.ts-compatible bundle when the latest data backup was made by another bot version or is older thanTOMORI_AUTO_BACKUP_INTERVAL_HOURS(default 24). Only automatic bundles count towardTOMORI_AUTO_BACKUP_MAXretention (default 5); manualbun run backupbundles are never pruned by this gate. - In production: bind health HTTP server on
$PORT(default 8080); returns 503 until Discord ready. - Load secrets via
getAppSecrets(); populateprocess.envfor downstream consumers; initializekeyManager. - Construct Discord client with intents + sweepers; register process/client error handlers.
- Initialize database:
- run narrow pre-schema legacy rename bridges for known table renames that would otherwise conflict with fresh
schema.sql- The persona rename bridge (
runPreSchemaPersonaRenameBridge) also self-heals rollback artifacts: running pre-rename code against an already-migrated database re-materializes legacy tables (tomoris,tomori_presets,tomori_configs) as empty shells via their oldCREATE TABLE IF NOT EXISTSschema. When an empty legacy table coexists with its populated renamed counterpart, the bridge drops the empty shell (logged vialog.warn) and continues. A non-empty legacy table coexisting with the renamed table is treated as an ambiguous data fork and still throws, requiring human inspection before boot. This runs on every startup (unlike run-once migrations), so the fix survives repeated rollback/merge cycles.
- The persona rename bridge (
- run
src/db/schema.sql - run
src/db/schema_rag.sqlonly when pgvector is detected- Detection (
detectRagAvailability) is probed once per initialization attempt rather than once above the retry loop, and only a probe that completed is cached. A probe that could not reach the server proves nothing about pgvector, so caching that as “absent” would gate every RAG command until the next restart:isRagAvailable()reads a process-lifetime flag with no invalidation path. Production hit exactly that, answering/teach documentwith “Document RAG Disabled” against a database carrying pgvector 0.8.2. - While no probe has completed,
isRagAvailable()returnsfalse, which is the honest answer for an unknown state.isRagAvailable()stays synchronous and side-effect free because the context builder consults it on every chat turn; recovery is owned byragAvailabilityMonitor(registered insrc/init/timers.ts), which re-probes everyRAG_AVAILABILITY_REPROBE_INTERVAL_MS(default 5 minutes) and stops on the first success. A startup probe that completed starts no timer, so a database genuinely without pgvector is never polled. - The monitor’s re-probe additionally requires
document_chunksto exist, because the same failure that skipped detection also skippedschema_rag.sql, and flipping the flag on the extension alone would expose commands to missing tables.getRagAvailabilityState()exposes thenull(undetermined) vsfalse(probed, absent) distinction thatisRagAvailable()collapses.
- Detection (
- run
src/db/schema_stpreset.sql - run typed seed catalogs in order: models, personas, system prompts, NovelAI presets
- run pending numbered migrations from
src/db/migrations/(fresh databases instead record every historical migration as applied viamarkAllMigrationsApplied, becauseschema.sqlalready embodies the final shape)- Because
schema.sqlis applied before the migration runner, a legacy untracked production database (still carryingtomori_configs, never seen by the runner) reaches the historical “expand + backfill” migrations with the split tables already in their final post-migration shape. The data-mover backfills that copy out of the god table (002,003,004,007) therefore detect whether the destination still has the pre-021image-tag columns (nai_style_tags/nai_tags/nai_char_tags) or the post-021ones (image_default_positive_tags/physical_appearance_tags) and write the matching column set. Without this guard the backfill fails withcolumn "nai_style_tags" of relation "server_novelai_imagegen_configs" does not exist(42703). Migration021reconciles the rename afterwards, so no tag data is lost.
- Because
- run narrow pre-schema legacy rename bridges for known table renames that would otherwise conflict with fresh
- Cleanup expired cooldown rows at startup (
cleanupExpiredCooldowns). - Attempt optional
pg_cronregistration for hourly cooldown cleanup job. - Initialize tool registry (
initializeTools). - Initialize localization (
initializeLocalizer). - Initialize model caches:
- LLM cache (
initializeLLMCache) - OpenRouter text catalog (
initializeOpenRouterCapabilityCache) - OpenRouter modality catalogs, in parallel (
initializeOpenRouterVideoModelCache,initializeOpenRouterImageModelCache,initializeOpenRouterEmbeddingModelCache) - A failed catalog fetch here is non-fatal and non-permanent: lookups refresh on a miss and the background refresher retries on the TTL, so the bot recovers without a restart.
- LLM cache (
- Preload preset avatar cache from DB presets.
- Initialize Matrix bridge (optional; non-fatal on failure).
- Attach all event listeners (
eventHandler(client)). - Register post-ready startup hooks (deferred until
clientReady):- health tracker init
- scheduled work coordinator init (reminders + random triggers)
- memory monitor init
- cache metrics logger init
- OpenRouter catalog refresher init
- Initialize upload quota cleanup scheduler.
await client.login(DISCORD_TOKEN). Any failure exits the process, and the container restart policy retries with a fresh process. In-process retrying is not available:Client#loginawaitsclient.destroy()on failure, which setsws.destroyedpermanently (initialized false in the WebSocket manager constructor and only ever set true indestroy()), dropsclient.token, and never restarts the cache sweepers. A secondlogin()therefore leavesisReady()false for the life of the process, which the health endpoint reports as 503 and the runtime reads as a dead container. Rebuilding the client instead is not an option because the Matrix bridge closes over the instance it was handed.isTransientGatewayError()insrc/init/discord.tsonly chooses the message, because a transient gateway failure and a misconfiguration need different operator responses even though both exit. The failure is carried by the exit code and the log line rather than by/health: a failed login ends the process, so nothing recorded for it could be read by the probe that is meant to report it.
Error Criticality
Section titled “Error Criticality”- Fatal (process exits):
- due automatic startup backup failure in non-production
- database init failure
- tool registry init failure
- Discord login failure of any kind, including a transient gateway failure (the runtime restarts the process, which is the only path to a client that can report ready)
- Non-fatal (warn and continue):
- cache warmup failures
- pg_cron setup failures
- matrix init failure
- cooldown cleanup failure
- scheduled work/memory monitor/quota cleanup init failures
Discord Client Configuration Notes
Section titled “Discord Client Configuration Notes”GuildPresencesis a privileged intent resolved byresolvePresenceIntentEnabled()insrc/init/discord.ts. Before the client is built, it probesGET /applications/@meand includes the intent only when Discord reports it as enabled (ApplicationFlags.GatewayPresenceorGatewayPresenceLimited). This is self-resolving: the intent turns on automatically on the next restart once Discord approves it (no code or env change). If the probe fails (e.g. network error), it falls back to the legacy default: enabled outside production, disabled in production.- Consumers detect the intent at runtime via
client.options.intents.has(GatewayIntentBits.GuildPresences)(see the participants context builder) and omit presence/status lines when it is absent, so toggling it needs no other code changes. - Sweeper configuration is enabled for message/user cache pressure control.
- Gateway session lifecycle is logged at the rate-limit level for
shardReady,shardResume,shardDisconnect,shardReconnecting, andinvalidated, so a resumed session (which replays missed dispatches) is distinguishable from a fresh identify.shardErrorreports at error level once per shard per episode and at warn level for the repeats, because discord.js retries the handshake itself and one outage otherwise writes an identicalerror_logsrow per attempt. The per-shard episode is cleared only onshardReadyorshardResume: clearing it on a disconnect would re-arm error level for the next attempt of the same outage, which is exactly the repetition being absorbed.
clientReady Event Work
Section titled “clientReady Event Work”eventHandler executes all handlers in src/events/clientReady/ (sorted), including:
- command registration
- MCP server registration
- command registry initialization
- status/presence setup
Additional client.once("clientReady") hooks in index.ts initialize health tracking, scheduled work, and memory monitoring.
Production Health Endpoint
Section titled “Production Health Endpoint”GET /health returns:
200when healthy503when unhealthy
Health is computed from:
- Discord ready state
- websocket ping threshold
- recent Discord event activity