feat: per-group queue, SQLite state, graceful shutdown

Add per-group container locking with global concurrency limit to prevent
concurrent containers for the same group (#89) and cap total containers.
Fix message batching bug where lastAgentTimestamp advanced to trigger
message instead of latest in batch, causing redundant re-processing.
Move router state, sessions, and registered groups from JSON files to
SQLite with automatic one-time migration. Add SIGTERM/SIGINT handlers
with graceful shutdown (SIGTERM -> grace period -> SIGKILL). Add startup
recovery for messages missed during crash. Remove dead code: utils.ts,
Session type, isScheduledTask flag, ContainerConfig.env, getTaskRunLogs,
GroupQueue.isActive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-06 07:38:07 +02:00
parent db216a459e
commit eac9a6acfd
8 changed files with 591 additions and 115 deletions

View File

@@ -1,9 +1,9 @@
import { ChildProcess } from 'child_process';
import { CronExpressionParser } from 'cron-parser';
import fs from 'fs';
import path from 'path';
import {
DATA_DIR,
GROUPS_DIR,
MAIN_GROUP_FOLDER,
SCHEDULER_POLL_INTERVAL,
@@ -17,6 +17,7 @@ import {
logTaskRun,
updateTaskAfterRun,
} from './db.js';
import { GroupQueue } from './group-queue.js';
import { logger } from './logger.js';
import { RegisteredGroup, ScheduledTask } from './types.js';
@@ -24,6 +25,8 @@ export interface SchedulerDependencies {
sendMessage: (jid: string, text: string) => Promise<void>;
registeredGroups: () => Record<string, RegisteredGroup>;
getSessions: () => Record<string, string>;
queue: GroupQueue;
onProcess: (groupJid: string, proc: ChildProcess) => void;
}
async function runTask(
@@ -86,14 +89,17 @@ async function runTask(
task.context_mode === 'group' ? sessions[task.group_folder] : undefined;
try {
const output = await runContainerAgent(group, {
prompt: task.prompt,
sessionId,
groupFolder: task.group_folder,
chatJid: task.chat_jid,
isMain,
isScheduledTask: true,
});
const output = await runContainerAgent(
group,
{
prompt: task.prompt,
sessionId,
groupFolder: task.group_folder,
chatJid: task.chat_jid,
isMain,
},
(proc) => deps.onProcess(task.chat_jid, proc),
);
if (output.status === 'error') {
error = output.error || 'Unknown error';
@@ -165,7 +171,11 @@ export function startSchedulerLoop(deps: SchedulerDependencies): void {
continue;
}
await runTask(currentTask, deps);
deps.queue.enqueueTask(
currentTask.chat_jid,
currentTask.id,
() => runTask(currentTask, deps),
);
}
} catch (err) {
logger.error({ err }, 'Error in scheduler loop');