Skip to content

v0.6.0 — reliability + remote control#2

Merged
b1rdmania merged 6 commits into
mainfrom
feat/v0.6.0-pr1
Mar 19, 2026
Merged

v0.6.0 — reliability + remote control#2
b1rdmania merged 6 commits into
mainfrom
feat/v0.6.0-pr1

Conversation

@b1rdmania
Copy link
Copy Markdown
Owner

@b1rdmania b1rdmania commented Mar 19, 2026

Summary

  • Fix infinite retry loopscheduleRetry no longer resets retryCount = 0 after MAX_RETRIES. Previously a group that hit max retries would silently loop forever.
  • Orphan PID cleanup on startup — agent PIDs tracked in data/agent-pids.json. On boot, any survivors from the previous run are killed before accepting messages. Fixes the Mar 18 cascade (zombie process from 12:50AM consuming slots).
  • /status command — shows active agents per group, queue depth, and uptime. Available over Telegram.
  • /skills command — lists all installed skills with descriptions, read live from .claude/skills/. Auto-chunks if over Telegram's 4096 char limit.
  • Telegram command menusetMyCommands() called at startup so all commands appear with descriptions when the user types /.
  • GroupQueue.getStatus() — exposes live queue state for external consumers (active count, waiting groups, per-group task/message queues).

Test plan

  • Boot bot — confirm no orphan kills fire on clean start
  • Manually kill bot mid-agent, restart — confirm orphan PID is killed on boot
  • Send /status in Telegram — confirm uptime, active count, group names show correctly
  • Send /skills in Telegram — confirm skill list appears with descriptions
  • Type / in Telegram — confirm command menu shows all 6 commands with descriptions
  • Hit MAX_RETRIES failures — confirm retry loop stops and doesn't restart

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added /status (active agents per group, queue depth, uptime) and /skills (lists available skills with descriptions)
    • Bot commands registered in Telegram's command menu for easier discoverability
    • Queue status snapshot exposed for external consumers
  • Bug Fixes

    • Fixed infinite-retry behavior by enforcing max retries
    • Automatic cleanup of orphaned agent processes on startup
  • Chores

    • Pre-commit hook now stages formatting fixes automatically

- Fix infinite retry loop: scheduleRetry no longer resets retryCount after MAX_RETRIES
- Orphan PID cleanup on startup: track agent PIDs in data/agent-pids.json, kill survivors on boot
- /status command: active agents, queue depth, uptime via Telegram
- /skills command: lists installed skills with descriptions from .claude/skills/
- setMyCommands() at startup: Telegram command menu auto-populated
- GroupQueue.getStatus(): exposes live queue state for external consumers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 19, 2026

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4b951cc-0c4f-4919-86c6-2e5d694d6a48

📥 Commits

Reviewing files that changed from the base of the PR and between 67e38b9 and 77f08fd.

📒 Files selected for processing (6)
  • .husky/pre-commit
  • src/channels/telegram.test.ts
  • src/channels/telegram.ts
  • src/container-runner.test.ts
  • src/container-runner.ts
  • src/index.ts
✅ Files skipped from review due to trivial changes (3)
  • .husky/pre-commit
  • src/container-runner.ts
  • src/container-runner.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/channels/telegram.ts

📝 Walkthrough

Walkthrough

Adds v0.6.0: Telegram /status and /skills commands plus command registration; exposes queue internals via GroupQueue.getStatus(); fixes infinite-retry reset in scheduleRetry; tracks agent PIDs in data/agent-pids.json and kills orphaned agents at startup; uptime/status handler added.

Changes

Cohort / File(s) Summary
Version & Changelog
CHANGELOG.md, package.json
Bump to v0.6.0 and add release notes describing reliability fixes, retry behavior change, PID cleanup, Telegram commands, and queue status exposure.
Telegram Commands & Integration
src/channels/telegram.ts
Added onGetStatus?: () => string to TelegramChannelOpts; implemented /status and /skills handlers; registers bot commands via bot.api.setMyCommands() (non-fatal on failure); /skills reads .claude/skills/*/SKILL.md, extracts descriptions, HTML-escapes and chunks replies.
Queue Behavior
src/group-queue.ts
Added public getStatus() returning overall active/waiting counts and per-group entries; changed scheduleRetry() to stop resetting state.retryCount after exceeding MAX_RETRIES so retries terminate.
Agent PID Tracking & Startup Cleanup
src/index.ts
Added startTime; PID persistence helpers using data/agent-pids.json (readAgentPids, writeAgentPids, trackAgentPid, untrackAgentPid); cleanupOrphanedAgents() invoked at startup to kill persisted PIDs; container-runner hooks now track/untrack PIDs; composed channelOpts.onGetStatus returns uptime and queue status.
Container Runner & Tests
src/container-runner.ts, src/container-runner.test.ts, src/channels/telegram.test.ts
Imported execSync at module scope; tests updated to mock execSync and bot.api.setMyCommands.
Pre-commit Hook
.husky/pre-commit
Pre-commit now runs formatter and stages updated files via git add -u.

Sequence Diagram(s)

sequenceDiagram
  participant Startup
  participant PIDStore as AgentPIDStore
  participant OS
  participant Logger

  Startup->>PIDStore: readAgentPids()
  PIDStore-->>Startup: list of PIDs
  loop for each pid
    Startup->>OS: process.kill(pid, 0) (liveness check)
    alt alive
      Startup->>OS: process.kill(pid, 'SIGKILL')
      OS-->>Logger: killed pid
    else not alive
      OS-->>Logger: pid not running
    end
  end
  Startup->>PIDStore: writeAgentPids([]) (clear persisted list)
  PIDStore-->>Logger: persisted list cleared
Loading
sequenceDiagram
  participant User
  participant TelegramBot as Bot
  participant Queue as GroupQueue
  participant Index as Server

  User->>TelegramBot: /status
  TelegramBot->>Index: invoke onGetStatus()
  Index->>Queue: getStatus()
  Queue-->>Index: status object
  Index-->>TelegramBot: formatted status (HTML)
  TelegramBot-->>User: reply with status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble logs at morning light,

stray PIDs find their final flight.
Retries hush, queues hum anew,
skills and status hop into view.
A tidy meadow — code and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'v0.6.0 — reliability + remote control' accurately summarizes the two main themes of the changeset: reliability improvements (retry loop fix, orphan PID cleanup) and remote control features (/status, /skills, command menu, queue exposure).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v0.6.0-pr1
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/channels/telegram.ts`:
- Around line 133-136: The /skills handler is inserting unescaped values into an
HTML message: escape both the directory name variable dir and the extracted
description desc using the exported escapeHtml() function (from src/router.ts
via src/index.ts) before interpolating them into lines; replace the raw dir and
desc usage in the block that builds lines (where content is read with
fs.readFileSync and desc is derived from descMatch) with escapeHtml(dir) and
escapeHtml(desc.slice(0,80)) (or empty string when desc is empty) so the message
sent with parse_mode: 'HTML' cannot be broken or injected with raw HTML
characters.
- Around line 114-115: The /skills command handler (this.bot.command('skills',
...)) must be made async and every call to ctx.reply() inside the skills handler
(including the chunking loop that sends parts of the skills message) must be
awaited; change the handler to async (e.g., this.bot.command('skills', async
(ctx) => { ... })) and prepend await to each ctx.reply(...) so messages are sent
sequentially and send failures surface properly—mirror the pattern used in the
/update handler.

In `@src/index.ts`:
- Around line 492-519: readAgentPids currently trusts JSON.parse and may return
unsafe values; change it to parse into unknown, ensure the result is an array of
positive integers (reject 0/negatives/non-integers), dedupe and return only
valid PIDs; update writeAgentPids to sanitize the pids array before persisting
(use the same validation). In cleanupOrphanedAgents (and related logic around
lines referenced) verify each PID still belongs to a GhostClaw agent before
sending SIGKILL by checking the process command/argv (e.g., /proc/<pid>/cmdline
on POSIX or platform-appropriate process listing), skip invalid or non-matching
PIDs, and handle all errors without throwing; ensure
trackAgentPid/untrackAgentPid use the validated read/write helpers so only safe
PIDs are stored and acted upon.
- Around line 311-317: The scheduler's onProcess callback passed into
runContainerAgent/task-scheduler.ts is missing the PID tracking wrapper present
in the interactive agent callback; update the scheduler-side callback (the
function that currently calls queue.registerProcess(chatJid, proc,
containerName, group.folder)) to mirror the interactive version by calling
trackAgentPid(proc.pid) when proc.pid is present and registering a
proc.once('exit', () => untrackAgentPid(proc.pid!)) handler; ensure this change
references the same queue.registerProcess(...) call and integrates with
runContainerAgent's onProcess hook so scheduled agents are tracked and untracked
the same way.
- Around line 614-620: The group label interpolation uses unescaped values
(registeredGroups, group?.name and g.jid) when building the status lines and
sending HTML to Telegram; wrap both group?.name and g.jid with the existing
escapeHtml(...) helper from router.js before composing name and before calling
lines.push (i.e., compute name = escapeHtml(group?.name || g.jid) or escape both
individually) so any '<', '>' or '&' are encoded prior to lines.push and
subsequent send with parse_mode: 'HTML'.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3499f306-e92b-40a1-8807-c226b3075824

📥 Commits

Reviewing files that changed from the base of the PR and between 0492f03 and 57ea2b7.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • package.json
  • src/channels/telegram.ts
  • src/group-queue.ts
  • src/index.ts

Comment thread src/channels/telegram.ts Outdated
Comment thread src/channels/telegram.ts Outdated
Comment thread src/index.ts
Comment thread src/index.ts
Comment on lines +492 to +519
function readAgentPids(): number[] {
try {
return JSON.parse(fs.readFileSync(agentPidsFile, 'utf-8'));
} 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Validate and identify persisted agent PIDs before cleanup.

readAgentPids() trusts raw JSON.parse() output, and cleanupOrphanedAgents() later iterates and SIGKILLs whatever numbers come back. A valid-but-wrong payload can crash startup, 0/negative values have process-group semantics on POSIX, and a reused PID can target an unrelated host process. Parse into unknown, accept only positive integers, and verify the record still belongs to a GhostClaw agent before killing it.

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
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 492 - 519, readAgentPids currently trusts
JSON.parse and may return unsafe values; change it to parse into unknown, ensure
the result is an array of positive integers (reject 0/negatives/non-integers),
dedupe and return only valid PIDs; update writeAgentPids to sanitize the pids
array before persisting (use the same validation). In cleanupOrphanedAgents (and
related logic around lines referenced) verify each PID still belongs to a
GhostClaw agent before sending SIGKILL by checking the process command/argv
(e.g., /proc/<pid>/cmdline on POSIX or platform-appropriate process listing),
skip invalid or non-matching PIDs, and handle all errors without throwing;
ensure trackAgentPid/untrackAgentPid use the validated read/write helpers so
only safe PIDs are stored and acted upon.

Comment thread src/index.ts
Comment on lines +614 to +620
const group = registeredGroups[g.jid];
const name = 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(', ')}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file and get context around lines 614-620
wc -l src/index.ts

Repository: b1rdmania/ghostclaw

Length of output: 79


🏁 Script executed:

# Read the relevant section with surrounding context
sed -n '600,650p' src/index.ts

Repository: 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 -20

Repository: 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.ts

Repository: 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.ts

Repository: 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 -20

Repository: 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 2

Repository: 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.ts

Repository: b1rdmania/ghostclaw

Length of output: 1072


🏁 Script executed:

# And check where onGetStatus is called in telegram.ts
sed -n '100,130p' src/channels/telegram.ts

Repository: b1rdmania/ghostclaw

Length of output: 1102


🏁 Script executed:

# Verify escapeHtml function in router.ts
sed -n '93,102p' src/router.ts

Repository: b1rdmania/ghostclaw

Length of output: 364


🏁 Script executed:

# Check the markdownToTelegramHtml function to understand how it handles escaping
rg -n "markdownToTelegramHtml" src/ -A 10

Repository: 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.ts

Repository: 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 -5

Repository: b1rdmania/ghostclaw

Length of output: 250


Escape dynamic group labels in the HTML status message body.

The group.name and g.jid values interpolated at lines 614–620 are sent directly into a Telegram message with parse_mode: 'HTML' (line 111 of telegram.ts). A group title or JID containing <, >, or & will be parsed as HTML, allowing an attacker to inject formatting or break the message layout. Use the existing escapeHtml() function (available from router.js) to escape these values before interpolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 614 - 620, The group label interpolation uses
unescaped values (registeredGroups, group?.name and g.jid) when building the
status lines and sending HTML to Telegram; wrap both group?.name and g.jid with
the existing escapeHtml(...) helper from router.js before composing name and
before calling lines.push (i.e., compute name = escapeHtml(group?.name || g.jid)
or escape both individually) so any '<', '>' or '&' are encoded prior to
lines.push and subsequent send with parse_mode: 'HTML'.

b1rdmania and others added 3 commits March 19, 2026 10:20
- Make /skills handler async; await all ctx.reply() calls to preserve chunk order
- Escape skill dir name and description with escapeXml() before HTML interpolation
- Escape group name/JID in /status output with escapeXml()
- Mirror PID tracking on scheduler onProcess hook (scheduled tasks were not tracked)
- Validate agent-pids.json before killing: accept only positive integers, ignore malformed entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pre-commit hook was reformatting but not re-staging, causing committed
versions to diverge from what prettier --check expects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…age formatted files

- telegram.test.ts: add setMyCommands vi.fn() to the mock bot api so connect() doesn't throw
- .husky/pre-commit: add 'git add -u' after format:fix so prettier-reformatted files are
  included in the commit rather than left as unstaged changes (was causing CI format checks to fail)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
src/index.ts (3)

611-618: ⚠️ Potential issue | 🟠 Major

Escape group labels before formatting the HTML status body.

group?.name / g.jid are inserted directly into a Telegram HTML reply. A group title containing <, > or & can break the message or inject markup.

Suggested fix
-import { findChannel, formatMessages, formatOutbound } from './router.js';
+import { escapeXml, findChannel, formatMessages, formatOutbound } from './router.js';
@@
-          const name = group?.name || g.jid;
+          const name = escapeXml(group?.name || g.jid);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 611 - 618, The code inserts group?.name or g.jid
directly into Telegram HTML-formatted replies (in the loop over status.groups
and the lines.push call), which can break/allow markup; locate the loop using
status.groups, registeredGroups and the lines.push(`• ${name}: ...`) and
sanitize the label by escaping HTML entities (&, <, >, " and ') before
interpolation (create or use an escapeHtml helper and call it on group?.name and
g.jid), then use the escaped value in the lines.push string.

311-317: ⚠️ Potential issue | 🟠 Major

Mirror this PID wrapper in the scheduler path too.

Only interactive agents are tracked here. The later startSchedulerLoop(... onProcess ...) callback still just forwards queue.registerProcess(...), so scheduled agents never reach agent-pids.json and won't be reaped on restart.

Suggested fix
-    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!));
+      }
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 311 - 317, The scheduler path for starting
processes (the callback you pass into startSchedulerLoop / the onProcess
handler) currently only calls queue.registerProcess(...) and therefore never
invokes trackAgentPid/untrackAgentPid, so scheduled agents aren't recorded in
agent-pids.json; update the scheduler's onProcess callback to mirror the
interactive-agent wrapper: after calling queue.registerProcess(chatJid?, proc,
containerName, group.folder) (use the same argument order and identifiers used
in this diff), if proc.pid is present call trackAgentPid(proc.pid) and attach
proc.once('exit', () => untrackAgentPid(proc.pid!)); ensure the same
registerProcess invocation remains but add the pid tracking/untracking logic so
scheduled agents are tracked and reaped on restart.

492-541: ⚠️ Potential issue | 🔴 Critical

Validate persisted PIDs before issuing SIGKILL.

readAgentPids() trusts arbitrary JSON as number[], so a valid-but-wrong payload can either crash startup ({} / "123") or signal the wrong target (0, negative values, or a reused PID). Sanitize to positive integers on read/write, verify the live process still belongs to a GhostClaw agent before killing it, and only drop entries you actually proved dead. Based on learnings: "Run agents directly on the host machine as Node.js child processes without containerization".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 492 - 541, Sanitize and validate persisted PIDs
and only kill confirmed agent processes: update readAgentPids to parse JSON
defensively and return a deduplicated array of positive integers (filter out
non-numbers, <=0, NaN, strings, objects), update
writeAgentPids/trackAgentPid/untrackAgentPid to persist only the sanitized,
deduped PIDs; in cleanupOrphanedAgents, skip invalid or current process PID, and
before sending SIGKILL verify the PID belongs to a GhostClaw agent by checking a
platform-specific marker (e.g. read /proc/<pid>/cmdline on Linux for a known
AGENT_EXECUTABLE_NAME constant or fall back to verifying parent PID matches this
process when /proc is unavailable), only remove entries that you proved dead and
write back any remaining live PIDs.
src/channels/telegram.ts (2)

116-159: ⚠️ Potential issue | 🟡 Minor

Make /skills async and await each ctx.reply().

The chunking path fires multiple replies concurrently right now, so long skill lists can arrive out of order and send failures are silently dropped.

Suggested fix
-    this.bot.command('skills', (ctx) => {
+    this.bot.command('skills', async (ctx) => {
       const chatJid = `tg:${ctx.chat.id}`;
       const group = this.opts.registeredGroups()[chatJid];
       if (!group) {
-        ctx.reply('Not a registered chat.');
+        await ctx.reply('Not a registered chat.');
         return;
       }
       const skillsDir = path.join(process.cwd(), '.claude', 'skills');
       if (!fs.existsSync(skillsDir)) {
-        ctx.reply('No skills directory found.');
+        await ctx.reply('No skills directory found.');
         return;
       }
@@
       const MAX = 4096;
       if (text.length <= MAX) {
-        ctx.reply(text, { parse_mode: 'HTML' });
+        await ctx.reply(text, { parse_mode: 'HTML' });
       } else {
         let chunk = '';
         for (const line of lines) {
           if (chunk.length + line.length + 1 > MAX) {
-            ctx.reply(chunk, { parse_mode: 'HTML' });
+            await ctx.reply(chunk, { parse_mode: 'HTML' });
             chunk = line;
           } else {
             chunk = chunk ? `${chunk}\n${line}` : line;
           }
         }
-        if (chunk) ctx.reply(chunk, { parse_mode: 'HTML' });
+        if (chunk) await ctx.reply(chunk, { parse_mode: 'HTML' });
       }
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/channels/telegram.ts` around lines 116 - 159, Change the skills command
handler to be async and await all ctx.reply() calls so replies are sent
sequentially and failures surface: update the this.bot.command('skills', (ctx)
=> { ... }) handler to async (ctx) => { ... } and replace each ctx.reply(...)
(the early exit replies, the single whole-text reply, and every reply inside the
chunking loop) with await ctx.reply(...); ensure you await the reply before
returning from the handler and await each chunk reply inside the for loop so
messages preserve order and errors propagate.

128-140: ⚠️ Potential issue | 🟠 Major

Escape skill metadata before building the HTML response.

dir and desc are interpolated into a parse_mode: 'HTML' message verbatim. A skill name or description containing <, > or & will break formatting or inject markup into the reply.

Suggested fix
-import { markdownToTelegramHtml } from '../router.js';
+import { escapeXml, markdownToTelegramHtml } from '../router.js';
@@
-        const desc = descMatch ? descMatch[1].trim() : '';
+        const skillName = escapeXml(dir);
+        const desc = descMatch
+          ? escapeXml(descMatch[1].trim().slice(0, 80))
+          : '';
         lines.push(
-          `• <code>/${dir}</code>${desc ? ` — ${desc.slice(0, 80)}` : ''}`,
+          `• <code>/${skillName}</code>${desc ? ` — ${desc}` : ''}`,
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/channels/telegram.ts` around lines 128 - 140, The HTML response is built
using unescaped values (dir and desc) which can contain <, >, & and break or
inject markup; update the construction inside the loop (where lines, dirs, dir,
desc, skillsDir and skillMd are handled) to HTML-escape both the skill name and
the description before interpolation into the `<code>…</code>` and surrounding
text (escape &, <, >, " and '), then truncate the escaped description to 80
characters if needed and use the escaped strings in the lines.push call so
parse_mode: 'HTML' is safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/channels/telegram.ts`:
- Around line 360-373: The call to bot.api.setMyCommands in connect() can throw
synchronously because the test Bot mock lacks an api.setMyCommands
implementation; either add a mock implementation (e.g., setMyCommands:
vi.fn().mockResolvedValue(undefined) on the test's api mock in telegram.test.ts)
or make connect() defensive by wrapping the await
this.bot.api.setMyCommands(...) call (or the whole setMyCommands chain) in
try/catch and logging the error non-fatally (same pattern as the existing
.catch) so a synchronous TypeError won't break connect() before bot.start()
runs.

---

Duplicate comments:
In `@src/channels/telegram.ts`:
- Around line 116-159: Change the skills command handler to be async and await
all ctx.reply() calls so replies are sent sequentially and failures surface:
update the this.bot.command('skills', (ctx) => { ... }) handler to async (ctx)
=> { ... } and replace each ctx.reply(...) (the early exit replies, the single
whole-text reply, and every reply inside the chunking loop) with await
ctx.reply(...); ensure you await the reply before returning from the handler and
await each chunk reply inside the for loop so messages preserve order and errors
propagate.
- Around line 128-140: The HTML response is built using unescaped values (dir
and desc) which can contain <, >, & and break or inject markup; update the
construction inside the loop (where lines, dirs, dir, desc, skillsDir and
skillMd are handled) to HTML-escape both the skill name and the description
before interpolation into the `<code>…</code>` and surrounding text (escape &,
<, >, " and '), then truncate the escaped description to 80 characters if needed
and use the escaped strings in the lines.push call so parse_mode: 'HTML' is
safe.

In `@src/index.ts`:
- Around line 611-618: The code inserts group?.name or g.jid directly into
Telegram HTML-formatted replies (in the loop over status.groups and the
lines.push call), which can break/allow markup; locate the loop using
status.groups, registeredGroups and the lines.push(`• ${name}: ...`) and
sanitize the label by escaping HTML entities (&, <, >, " and ') before
interpolation (create or use an escapeHtml helper and call it on group?.name and
g.jid), then use the escaped value in the lines.push string.
- Around line 311-317: The scheduler path for starting processes (the callback
you pass into startSchedulerLoop / the onProcess handler) currently only calls
queue.registerProcess(...) and therefore never invokes
trackAgentPid/untrackAgentPid, so scheduled agents aren't recorded in
agent-pids.json; update the scheduler's onProcess callback to mirror the
interactive-agent wrapper: after calling queue.registerProcess(chatJid?, proc,
containerName, group.folder) (use the same argument order and identifiers used
in this diff), if proc.pid is present call trackAgentPid(proc.pid) and attach
proc.once('exit', () => untrackAgentPid(proc.pid!)); ensure the same
registerProcess invocation remains but add the pid tracking/untracking logic so
scheduled agents are tracked and reaped on restart.
- Around line 492-541: Sanitize and validate persisted PIDs and only kill
confirmed agent processes: update readAgentPids to parse JSON defensively and
return a deduplicated array of positive integers (filter out non-numbers, <=0,
NaN, strings, objects), update writeAgentPids/trackAgentPid/untrackAgentPid to
persist only the sanitized, deduped PIDs; in cleanupOrphanedAgents, skip invalid
or current process PID, and before sending SIGKILL verify the PID belongs to a
GhostClaw agent by checking a platform-specific marker (e.g. read
/proc/<pid>/cmdline on Linux for a known AGENT_EXECUTABLE_NAME constant or fall
back to verifying parent PID matches this process when /proc is unavailable),
only remove entries that you proved dead and write back any remaining live PIDs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4799702-8979-4c2d-a9a7-f876f2d26292

📥 Commits

Reviewing files that changed from the base of the PR and between 57ea2b7 and 67e38b9.

📒 Files selected for processing (3)
  • src/channels/telegram.ts
  • src/group-queue.ts
  • src/index.ts

Comment thread src/channels/telegram.ts
- container-runner.ts: replace dynamic require('child_process') with static
  import so vi.mock() intercepts execSync in tests
- container-runner.test.ts: add execSync mock to child_process stub
- telegram.test.ts: add setMyCommands to mock bot api (already in previous commit)

All 32 test files, 448 tests now pass locally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@b1rdmania b1rdmania merged commit 59c8382 into main Mar 19, 2026
1 of 2 checks passed
@b1rdmania b1rdmania deleted the feat/v0.6.0-pr1 branch March 23, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant