refactor: CI optimization, logging improvements, and codebase formatting (#456)

* fix(db): remove unique constraint on folder to support multi-channel agents

* ci: implement automated skill drift detection and self-healing PRs

* fix: align registration logic with Gavriel's feedback and fix build/test issues from Daniel Mi

* style: conform to prettier standards for CI validation

* test: fix branch naming inconsistency in CI (master vs main)

* fix(ci): robust module resolution by removing file extensions in scripts

* refactor(ci): simplify skill validation by removing redundant combination tests

* style: conform skills-engine to prettier, unify logging in index.ts and cleanup unused imports

* refactor: extract multi-channel DB changes to separate branch

Move channel column, folder suffix logic, and related migrations
to feat/multi-channel-db-v2 for independent review. This PR now
contains only CI/CD optimizations, Prettier formatting, and
logging improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gabi Simons
2026-02-25 23:13:36 +02:00
committed by GitHub
parent bd2e236f73
commit 11c201088b
76 changed files with 2333 additions and 1308 deletions

View File

@@ -15,7 +15,10 @@ import {
writeGroupsSnapshot,
writeTasksSnapshot,
} from './container-runner.js';
import { cleanupOrphans, ensureContainerRuntimeRunning } from './container-runtime.js';
import {
cleanupOrphans,
ensureContainerRuntimeRunning,
} from './container-runtime.js';
import {
getAllChats,
getAllRegisteredGroups,
@@ -71,10 +74,7 @@ function loadState(): void {
function saveState(): void {
setRouterState('last_timestamp', lastTimestamp);
setRouterState(
'last_agent_timestamp',
JSON.stringify(lastAgentTimestamp),
);
setRouterState('last_agent_timestamp', JSON.stringify(lastAgentTimestamp));
}
function registerGroup(jid: string, group: RegisteredGroup): void {
@@ -120,7 +120,9 @@ export function getAvailableGroups(): import('./container-runner.js').AvailableG
}
/** @internal - exported for testing */
export function _setRegisteredGroups(groups: Record<string, RegisteredGroup>): void {
export function _setRegisteredGroups(
groups: Record<string, RegisteredGroup>,
): void {
registeredGroups = groups;
}
@@ -134,14 +136,18 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
const channel = findChannel(channels, chatJid);
if (!channel) {
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
logger.warn({ chatJid }, 'No channel owns JID, skipping messages');
return true;
}
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
const sinceTimestamp = lastAgentTimestamp[chatJid] || '';
const missedMessages = getMessagesSince(chatJid, sinceTimestamp, ASSISTANT_NAME);
const missedMessages = getMessagesSince(
chatJid,
sinceTimestamp,
ASSISTANT_NAME,
);
if (missedMessages.length === 0) return true;
@@ -173,7 +179,10 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
const resetIdleTimer = () => {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
logger.debug({ group: group.name }, 'Idle timeout, closing container stdin');
logger.debug(
{ group: group.name },
'Idle timeout, closing container stdin',
);
queue.closeStdin(chatJid);
}, IDLE_TIMEOUT);
};
@@ -185,7 +194,10 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
const output = await runAgent(group, prompt, chatJid, async (result) => {
// Streaming output callback — called for each agent result
if (result.result) {
const raw = typeof result.result === 'string' ? result.result : JSON.stringify(result.result);
const raw =
typeof result.result === 'string'
? result.result
: JSON.stringify(result.result);
// Strip <internal>...</internal> blocks — agent uses these for internal reasoning
const text = raw.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
logger.info({ group: group.name }, `Agent output: ${raw.slice(0, 200)}`);
@@ -213,13 +225,19 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
// If we already sent output to the user, don't roll back the cursor —
// the user got their response and re-processing would send duplicates.
if (outputSentToUser) {
logger.warn({ group: group.name }, 'Agent error after output was sent, skipping cursor rollback to prevent duplicates');
logger.warn(
{ group: group.name },
'Agent error after output was sent, skipping cursor rollback to prevent duplicates',
);
return true;
}
// Roll back cursor so retries can re-process these messages
lastAgentTimestamp[chatJid] = previousCursor;
saveState();
logger.warn({ group: group.name }, 'Agent error, rolled back message cursor for retry');
logger.warn(
{ group: group.name },
'Agent error, rolled back message cursor for retry',
);
return false;
}
@@ -282,7 +300,8 @@ async function runAgent(
isMain,
assistantName: ASSISTANT_NAME,
},
(proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder),
(proc, containerName) =>
queue.registerProcess(chatJid, proc, containerName, group.folder),
wrappedOnOutput,
);
@@ -318,7 +337,11 @@ async function startMessageLoop(): Promise<void> {
while (true) {
try {
const jids = Object.keys(registeredGroups);
const { messages, newTimestamp } = getNewMessages(jids, lastTimestamp, ASSISTANT_NAME);
const { messages, newTimestamp } = getNewMessages(
jids,
lastTimestamp,
ASSISTANT_NAME,
);
if (messages.length > 0) {
logger.info({ count: messages.length }, 'New messages');
@@ -344,7 +367,7 @@ async function startMessageLoop(): Promise<void> {
const channel = findChannel(channels, chatJid);
if (!channel) {
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
logger.warn({ chatJid }, 'No channel owns JID, skipping messages');
continue;
}
@@ -381,9 +404,11 @@ async function startMessageLoop(): Promise<void> {
messagesToSend[messagesToSend.length - 1].timestamp;
saveState();
// Show typing indicator while the container processes the piped message
channel.setTyping?.(chatJid, true)?.catch((err) =>
logger.warn({ chatJid, err }, 'Failed to set typing indicator'),
);
channel
.setTyping?.(chatJid, true)
?.catch((err) =>
logger.warn({ chatJid, err }, 'Failed to set typing indicator'),
);
} else {
// No active container — enqueue for a new one
queue.enqueueMessageCheck(chatJid);
@@ -439,8 +464,13 @@ async function main(): Promise<void> {
// Channel callbacks (shared by all channels)
const channelOpts = {
onMessage: (_chatJid: string, msg: NewMessage) => storeMessage(msg),
onChatMetadata: (chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean) =>
storeChatMetadata(chatJid, timestamp, name, channel, isGroup),
onChatMetadata: (
chatJid: string,
timestamp: string,
name?: string,
channel?: string,
isGroup?: boolean,
) => storeChatMetadata(chatJid, timestamp, name, channel, isGroup),
registeredGroups: () => registeredGroups,
};
@@ -454,11 +484,12 @@ async function main(): Promise<void> {
registeredGroups: () => registeredGroups,
getSessions: () => sessions,
queue,
onProcess: (groupJid, proc, containerName, groupFolder) => queue.registerProcess(groupJid, proc, containerName, groupFolder),
onProcess: (groupJid, proc, containerName, groupFolder) =>
queue.registerProcess(groupJid, proc, containerName, groupFolder),
sendMessage: async (jid, rawText) => {
const channel = findChannel(channels, jid);
if (!channel) {
console.log(`Warning: no channel owns JID ${jid}, cannot send message`);
logger.warn({ jid }, 'No channel owns JID, cannot send message');
return;
}
const text = formatOutbound(rawText);
@@ -473,9 +504,11 @@ async function main(): Promise<void> {
},
registeredGroups: () => registeredGroups,
registerGroup,
syncGroupMetadata: (force) => whatsapp?.syncGroupMetadata(force) ?? Promise.resolve(),
syncGroupMetadata: (force) =>
whatsapp?.syncGroupMetadata(force) ?? Promise.resolve(),
getAvailableGroups,
writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot(gf, im, ag, rj),
writeGroupsSnapshot: (gf, im, ag, rj) =>
writeGroupsSnapshot(gf, im, ag, rj),
});
queue.setProcessMessagesFn(processGroupMessages);
recoverPendingMessages();
@@ -488,7 +521,8 @@ async function main(): Promise<void> {
// Guard: only run when executed directly, not when imported by tests
const isDirectRun =
process.argv[1] &&
new URL(import.meta.url).pathname === new URL(`file://${process.argv[1]}`).pathname;
new URL(import.meta.url).pathname ===
new URL(`file://${process.argv[1]}`).pathname;
if (isDirectRun) {
main().catch((err) => {