-
Notifications
You must be signed in to change notification settings - Fork 7
v0.6.0 — reliability + remote control #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
57ea2b7
67e38b9
2ec5780
1d55a21
d1ae7a2
77f08fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| npm run format:fix | ||
| git add -u |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,7 +39,12 @@ import { | |
| import { GroupQueue } from './group-queue.js'; | ||
| import { resolveGroupFolderPath } from './group-folder.js'; | ||
| import { startIpcWatcher } from './ipc.js'; | ||
| import { findChannel, formatMessages, formatOutbound } from './router.js'; | ||
| import { | ||
| findChannel, | ||
| formatMessages, | ||
| formatOutbound, | ||
| escapeXml, | ||
| } from './router.js'; | ||
| import { startSchedulerLoop } from './task-scheduler.js'; | ||
| import { Channel, NewMessage, RegisteredGroup } from './types.js'; | ||
| import { logger } from './logger.js'; | ||
|
|
@@ -54,6 +59,7 @@ let sessions: Record<string, string> = {}; | |
| let registeredGroups: Record<string, RegisteredGroup> = {}; | ||
| let lastAgentTimestamp: Record<string, string> = {}; | ||
| let messageLoopRunning = false; | ||
| const startTime = Date.now(); | ||
|
|
||
| let whatsapp: WhatsAppChannel; | ||
| const channels: Channel[] = []; | ||
|
|
@@ -307,8 +313,13 @@ 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); | ||
| if (proc.pid) { | ||
| trackAgentPid(proc.pid); | ||
| proc.once('exit', () => untrackAgentPid(proc.pid!)); | ||
| } | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| wrappedOnOutput, | ||
| ); | ||
|
|
||
|
|
@@ -481,8 +492,67 @@ function releasePidLock(): void { | |
| } | ||
| } | ||
|
|
||
| const agentPidsFile = path.join(DATA_DIR, 'agent-pids.json'); | ||
|
|
||
| function readAgentPids(): number[] { | ||
| try { | ||
| const raw: unknown = JSON.parse(fs.readFileSync(agentPidsFile, 'utf-8')); | ||
| if (!Array.isArray(raw)) return []; | ||
| // Accept only positive integers — 0/negative have process-group semantics on POSIX | ||
| return raw.filter( | ||
| (v): v is number => typeof v === 'number' && Number.isInteger(v) && v > 0, | ||
| ); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| function writeAgentPids(pids: number[]): void { | ||
| try { | ||
| fs.mkdirSync(DATA_DIR, { recursive: true }); | ||
| fs.writeFileSync(agentPidsFile, JSON.stringify(pids)); | ||
| } catch { | ||
| /* ignore */ | ||
| } | ||
| } | ||
|
|
||
| function trackAgentPid(pid: number): void { | ||
| const pids = readAgentPids(); | ||
| if (!pids.includes(pid)) { | ||
| pids.push(pid); | ||
| writeAgentPids(pids); | ||
| } | ||
| } | ||
|
|
||
| function untrackAgentPid(pid: number): void { | ||
| const pids = readAgentPids().filter((p) => p !== pid); | ||
| writeAgentPids(pids); | ||
|
Comment on lines
+497
to
+529
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate and identify persisted agent PIDs before cleanup.
Based on learnings: "Run agents directly on the host machine as Node.js child processes without containerization". Also applies to: 522-537 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| function cleanupOrphanedAgents(): void { | ||
| const pids = readAgentPids(); | ||
| if (pids.length === 0) return; | ||
|
|
||
| let killed = 0; | ||
| for (const pid of pids) { | ||
| try { | ||
| process.kill(pid, 0); // throws if dead | ||
| process.kill(pid, 'SIGKILL'); | ||
| killed++; | ||
| logger.warn({ pid }, 'Killed orphaned agent process from previous run'); | ||
| } catch { | ||
| /* already dead */ | ||
| } | ||
| } | ||
| writeAgentPids([]); | ||
| if (killed > 0) { | ||
| logger.info({ killed }, 'Orphan agent cleanup complete'); | ||
| } | ||
| } | ||
|
|
||
| async function main(): Promise<void> { | ||
| acquirePidLock(); | ||
| cleanupOrphanedAgents(); | ||
|
|
||
| const errorsLog = path.join(process.cwd(), 'logs', 'errors.log'); | ||
| try { | ||
|
|
@@ -531,6 +601,36 @@ async function main(): Promise<void> { | |
| queue.clearQueue(chatJid); | ||
| return queue.killAgent(chatJid); | ||
| }, | ||
| onGetStatus: () => { | ||
| const status = queue.getStatus(); | ||
| const uptimeMs = Date.now() - startTime; | ||
| const uptimeMin = Math.floor(uptimeMs / 60000); | ||
| const uptimeHr = Math.floor(uptimeMin / 60); | ||
| const uptime = | ||
| uptimeHr > 0 ? `${uptimeHr}h ${uptimeMin % 60}m` : `${uptimeMin}m`; | ||
|
|
||
| const lines = [ | ||
| `<b>GhostClaw status</b>`, | ||
| `Uptime: ${uptime}`, | ||
| `Active agents: ${status.active}`, | ||
| `Waiting groups: ${status.waiting}`, | ||
| ]; | ||
|
|
||
| if (status.groups.length > 0) { | ||
| lines.push(''); | ||
| for (const g of status.groups) { | ||
| const group = registeredGroups[g.jid]; | ||
| const name = escapeXml(group?.name || g.jid); | ||
| const parts: string[] = []; | ||
| if (g.active) parts.push('running'); | ||
| if (g.queuedTasks > 0) parts.push(`${g.queuedTasks} task(s) queued`); | ||
| if (g.queuedMessages) parts.push('messages queued'); | ||
| lines.push(`• ${name}: ${parts.join(', ')}`); | ||
|
Comment on lines
+622
to
+628
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's examine the file and get context around lines 614-620
wc -l src/index.tsRepository: b1rdmania/ghostclaw Length of output: 79 🏁 Script executed: # Read the relevant section with surrounding context
sed -n '600,650p' src/index.tsRepository: b1rdmania/ghostclaw Length of output: 1615 🏁 Script executed: # Search for where the 'lines' variable is used and how the status message is constructed
rg -n "lines\." src/index.ts | head -20Repository: b1rdmania/ghostclaw Length of output: 185 🏁 Script executed: # Look for parse_mode usage to understand if HTML mode is being used
rg -n "parse_mode" src/index.tsRepository: b1rdmania/ghostclaw Length of output: 45 🏁 Script executed: # Search for any HTML escaping utilities in the codebase
rg -n "escapeHtml|htmlEscape|escapeXml" src/Repository: b1rdmania/ghostclaw Length of output: 1323 🏁 Script executed: # Let's get more context to identify the function containing this code
sed -n '575,625p' src/index.tsRepository: b1rdmania/ghostclaw Length of output: 1642 🏁 Script executed: # Search for where this function result is used and what parse_mode is set
rg -n "sendMessage\|parse_mode" src/index.ts | head -20Repository: b1rdmania/ghostclaw Length of output: 45 🏁 Script executed: # Look for TelegramChannel class definition to understand parse_mode handling
rg -n "class TelegramChannel\|parse_mode.*HTML" src/Repository: b1rdmania/ghostclaw Length of output: 45 🏁 Script executed: # Search for onGetStatus usage/calls
rg -n "onGetStatus" src/Repository: b1rdmania/ghostclaw Length of output: 262 🏁 Script executed: # Find TelegramChannel class and implementation
rg -n "class TelegramChannel" src/Repository: b1rdmania/ghostclaw Length of output: 140 🏁 Script executed: # Search for sendMessage implementation
rg -n "sendMessage\(" src/ -A 2Repository: b1rdmania/ghostclaw Length of output: 8602 🏁 Script executed: # Let's see the telegram.ts sendMessage implementation more clearly
sed -n '392,430p' src/channels/telegram.tsRepository: b1rdmania/ghostclaw Length of output: 1072 🏁 Script executed: # And check where onGetStatus is called in telegram.ts
sed -n '100,130p' src/channels/telegram.tsRepository: b1rdmania/ghostclaw Length of output: 1102 🏁 Script executed: # Verify escapeHtml function in router.ts
sed -n '93,102p' src/router.tsRepository: b1rdmania/ghostclaw Length of output: 364 🏁 Script executed: # Check the markdownToTelegramHtml function to understand how it handles escaping
rg -n "markdownToTelegramHtml" src/ -A 10Repository: b1rdmania/ghostclaw Length of output: 7576 🏁 Script executed: # Let's also verify the exact path where onGetStatus is called in telegram.ts
sed -n '105,115p' src/channels/telegram.tsRepository: b1rdmania/ghostclaw Length of output: 420 🏁 Script executed: # Let's verify there's no escaping happening at the ctx.reply level
sed -n '1,50p' src/channels/telegram.ts | grep -i "import\|from"Repository: b1rdmania/ghostclaw Length of output: 553 🏁 Script executed: # Check if ctx.reply is from grammyjs and if it does any escaping
rg -n "ctx.reply" src/channels/telegram.ts | head -5Repository: b1rdmania/ghostclaw Length of output: 250 Escape dynamic group labels in the HTML status message body. The 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| return lines.join('\n'); | ||
| }, | ||
| }; | ||
|
|
||
| if (TELEGRAM_BOT_TOKEN) { | ||
|
|
@@ -564,8 +664,13 @@ 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); | ||
| if (proc.pid) { | ||
| trackAgentPid(proc.pid); | ||
| proc.once('exit', () => untrackAgentPid(proc.pid!)); | ||
| } | ||
| }, | ||
| sendMessage: async (jid, rawText) => { | ||
| const channel = findChannel(channels, jid); | ||
| if (!channel) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.