Merge branch 'main' into fix/stale-session-recovery
This commit is contained in:
72
src/index.ts
72
src/index.ts
@@ -5,11 +5,14 @@ import { OneCLI } from '@onecli-sh/sdk';
|
||||
|
||||
import {
|
||||
ASSISTANT_NAME,
|
||||
DEFAULT_TRIGGER,
|
||||
getTriggerPattern,
|
||||
GROUPS_DIR,
|
||||
IDLE_TIMEOUT,
|
||||
MAX_MESSAGES_PER_PROMPT,
|
||||
ONECLI_URL,
|
||||
POLL_INTERVAL,
|
||||
TIMEZONE,
|
||||
TRIGGER_PATTERN,
|
||||
} from './config.js';
|
||||
import './channels/index.js';
|
||||
import {
|
||||
@@ -32,6 +35,7 @@ import {
|
||||
getAllSessions,
|
||||
deleteSession,
|
||||
getAllTasks,
|
||||
getLastBotMessageTimestamp,
|
||||
getMessagesSince,
|
||||
getNewMessages,
|
||||
getRouterState,
|
||||
@@ -111,6 +115,27 @@ function loadState(): void {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message cursor for a group, recovering from the last bot reply
|
||||
* if lastAgentTimestamp is missing (new group, corrupted state, restart).
|
||||
*/
|
||||
function getOrRecoverCursor(chatJid: string): string {
|
||||
const existing = lastAgentTimestamp[chatJid];
|
||||
if (existing) return existing;
|
||||
|
||||
const botTs = getLastBotMessageTimestamp(chatJid, ASSISTANT_NAME);
|
||||
if (botTs) {
|
||||
logger.info(
|
||||
{ chatJid, recoveredFrom: botTs },
|
||||
'Recovered message cursor from last bot reply',
|
||||
);
|
||||
lastAgentTimestamp[chatJid] = botTs;
|
||||
saveState();
|
||||
return botTs;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function saveState(): void {
|
||||
setRouterState('last_timestamp', lastTimestamp);
|
||||
setRouterState('last_agent_timestamp', JSON.stringify(lastAgentTimestamp));
|
||||
@@ -134,6 +159,26 @@ function registerGroup(jid: string, group: RegisteredGroup): void {
|
||||
// Create group folder
|
||||
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
||||
|
||||
// Copy CLAUDE.md template into the new group folder so agents have
|
||||
// identity and instructions from the first run. (Fixes #1391)
|
||||
const groupMdFile = path.join(groupDir, 'CLAUDE.md');
|
||||
if (!fs.existsSync(groupMdFile)) {
|
||||
const templateFile = path.join(
|
||||
GROUPS_DIR,
|
||||
group.isMain ? 'main' : 'global',
|
||||
'CLAUDE.md',
|
||||
);
|
||||
if (fs.existsSync(templateFile)) {
|
||||
let content = fs.readFileSync(templateFile, 'utf-8');
|
||||
if (ASSISTANT_NAME !== 'Andy') {
|
||||
content = content.replace(/^# Andy$/m, `# ${ASSISTANT_NAME}`);
|
||||
content = content.replace(/You are Andy/g, `You are ${ASSISTANT_NAME}`);
|
||||
}
|
||||
fs.writeFileSync(groupMdFile, content);
|
||||
logger.info({ folder: group.folder }, 'Created CLAUDE.md from template');
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure a corresponding OneCLI agent exists (best-effort, non-blocking)
|
||||
ensureOneCLIAgent(jid, group);
|
||||
|
||||
@@ -184,21 +229,22 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
||||
|
||||
const isMainGroup = group.isMain === true;
|
||||
|
||||
const sinceTimestamp = lastAgentTimestamp[chatJid] || '';
|
||||
const missedMessages = getMessagesSince(
|
||||
chatJid,
|
||||
sinceTimestamp,
|
||||
getOrRecoverCursor(chatJid),
|
||||
ASSISTANT_NAME,
|
||||
MAX_MESSAGES_PER_PROMPT,
|
||||
);
|
||||
|
||||
if (missedMessages.length === 0) return true;
|
||||
|
||||
// For non-main groups, check if trigger is required and present
|
||||
if (!isMainGroup && group.requiresTrigger !== false) {
|
||||
const triggerPattern = getTriggerPattern(group.trigger);
|
||||
const allowlistCfg = loadSenderAllowlist();
|
||||
const hasTrigger = missedMessages.some(
|
||||
(m) =>
|
||||
TRIGGER_PATTERN.test(m.content.trim()) &&
|
||||
triggerPattern.test(m.content.trim()) &&
|
||||
(m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||
);
|
||||
if (!hasTrigger) return true;
|
||||
@@ -307,6 +353,7 @@ async function runAgent(
|
||||
id: t.id,
|
||||
groupFolder: t.group_folder,
|
||||
prompt: t.prompt,
|
||||
script: t.script || undefined,
|
||||
schedule_type: t.schedule_type,
|
||||
schedule_value: t.schedule_value,
|
||||
status: t.status,
|
||||
@@ -422,7 +469,7 @@ async function startMessageLoop(): Promise<void> {
|
||||
}
|
||||
messageLoopRunning = true;
|
||||
|
||||
logger.info(`NanoClaw running (trigger: @${ASSISTANT_NAME})`);
|
||||
logger.info(`NanoClaw running (default trigger: ${DEFAULT_TRIGGER})`);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@@ -468,10 +515,11 @@ async function startMessageLoop(): Promise<void> {
|
||||
// Non-trigger messages accumulate in DB and get pulled as
|
||||
// context when a trigger eventually arrives.
|
||||
if (needsTrigger) {
|
||||
const triggerPattern = getTriggerPattern(group.trigger);
|
||||
const allowlistCfg = loadSenderAllowlist();
|
||||
const hasTrigger = groupMessages.some(
|
||||
(m) =>
|
||||
TRIGGER_PATTERN.test(m.content.trim()) &&
|
||||
triggerPattern.test(m.content.trim()) &&
|
||||
(m.is_from_me ||
|
||||
isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||
);
|
||||
@@ -482,8 +530,9 @@ async function startMessageLoop(): Promise<void> {
|
||||
// context that accumulated between triggers is included.
|
||||
const allPending = getMessagesSince(
|
||||
chatJid,
|
||||
lastAgentTimestamp[chatJid] || '',
|
||||
getOrRecoverCursor(chatJid),
|
||||
ASSISTANT_NAME,
|
||||
MAX_MESSAGES_PER_PROMPT,
|
||||
);
|
||||
const messagesToSend =
|
||||
allPending.length > 0 ? allPending : groupMessages;
|
||||
@@ -522,8 +571,12 @@ async function startMessageLoop(): Promise<void> {
|
||||
*/
|
||||
function recoverPendingMessages(): void {
|
||||
for (const [chatJid, group] of Object.entries(registeredGroups)) {
|
||||
const sinceTimestamp = lastAgentTimestamp[chatJid] || '';
|
||||
const pending = getMessagesSince(chatJid, sinceTimestamp, ASSISTANT_NAME);
|
||||
const pending = getMessagesSince(
|
||||
chatJid,
|
||||
getOrRecoverCursor(chatJid),
|
||||
ASSISTANT_NAME,
|
||||
MAX_MESSAGES_PER_PROMPT,
|
||||
);
|
||||
if (pending.length > 0) {
|
||||
logger.info(
|
||||
{ group: group.name, pendingCount: pending.length },
|
||||
@@ -707,6 +760,7 @@ async function main(): Promise<void> {
|
||||
id: t.id,
|
||||
groupFolder: t.group_folder,
|
||||
prompt: t.prompt,
|
||||
script: t.script || undefined,
|
||||
schedule_type: t.schedule_type,
|
||||
schedule_value: t.schedule_value,
|
||||
status: t.status,
|
||||
|
||||
Reference in New Issue
Block a user