From 4bea6a20c95898681d80422282ecef7797484549 Mon Sep 17 00:00:00 2001 From: R-M-Naveen Date: Fri, 6 Mar 2026 17:02:17 -0600 Subject: [PATCH 1/2] feat(atxp): switch to /hooks/agent, remove HEARTBEAT.md logic Replace /hooks/wake + HEARTBEAT.md approach with direct /hooks/agent channel targeting. Discovered channels are now sent to the notifications service which stores them and uses them for delivery. - Remove HEARTBEAT.md read/write/section-replace logic (~100 lines) - Simplify configureHooksOnInstance to only set hooks.token - Send discovered channels in the enable API request Co-Authored-By: Claude Opus 4.6 --- packages/atxp/src/commands/notifications.ts | 132 +++----------------- 1 file changed, 19 insertions(+), 113 deletions(-) diff --git a/packages/atxp/src/commands/notifications.ts b/packages/atxp/src/commands/notifications.ts index 77ccf51..4042ba7 100644 --- a/packages/atxp/src/commands/notifications.ts +++ b/packages/atxp/src/commands/notifications.ts @@ -6,8 +6,6 @@ import { execSync } from 'child_process'; const NOTIFICATIONS_BASE_URL = 'https://clowdbot-notifications.corp.circuitandchisel.com'; const OPENCLAW_CONFIG_PATH = '/data/.openclaw/openclaw.json'; const SESSIONS_PATH = '/data/.openclaw/agents/main/sessions/sessions.json'; -const WORKSPACE_DIR = '/data/.openclaw/workspace'; -const HEARTBEAT_SECTION_HEADER = '# ATXP Notification Relay'; // eslint-disable-next-line no-control-regex const sanitizeSessionValue = (s: string) => s.replace(/[\x00-\x1f"`[\]]/g, ''); @@ -56,128 +54,29 @@ async function discoverConnectedChannels(): Promise { } /** - * Build the HEARTBEAT.md content with notification relay instructions. - * The default heartbeat prompt says "Read HEARTBEAT.md if it exists. Follow it strictly." - * so writing these instructions directly to the file ensures the LLM relays - * system events to all discovered channels. - */ -function buildHeartbeatMd(channels: NotificationChannel[]): string { - const lines = [ - HEARTBEAT_SECTION_HEADER, - '', - 'When you see System: event lines about new emails or SMS messages,', - 'you MUST immediately relay each one to the user.', - 'Do NOT reply HEARTBEAT_OK when System events are present.', - '', - ]; - - if (channels.length > 0) { - lines.push('Relay to ALL of these channels:'); - for (const c of channels) { - lines.push(`- Use the message tool with channel=\`${c.channel}\` and target=\`${c.to}\``); - } - lines.push(''); - } - - return lines.join('\n'); -} - -/** - * Configure hooks, heartbeat delivery target, and HEARTBEAT.md on the instance. + * Configure hooks token on the instance. * Only runs when inside a Fly instance (FLY_MACHINE_ID is set). - * - * Discovers all connected messaging channels from the session store, writes - * HEARTBEAT.md with relay instructions for each channel, and sets the primary - * delivery target to the first discovered channel. + * Sets hooks.enabled and hooks.token in openclaw.json, restarts gateway if changed. */ async function configureHooksOnInstance(hooksToken: string): Promise { if (!process.env.FLY_MACHINE_ID) return; - const configPath = OPENCLAW_CONFIG_PATH; try { - const raw = await fs.readFile(configPath, 'utf-8'); + const raw = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf-8'); const config = JSON.parse(raw); - // Discover connected channels from session store - const channels = await discoverConnectedChannels(); - - let changed = false; - - // Configure hooks if (!config.hooks) config.hooks = {}; - if (config.hooks.token !== hooksToken || config.hooks.enabled !== true) { - config.hooks.enabled = true; - config.hooks.token = hooksToken; - changed = true; - } + if (config.hooks.token === hooksToken && config.hooks.enabled === true) return; - // Set primary delivery target to first discovered channel - if (!config.agents) config.agents = {}; - if (!config.agents.defaults) config.agents.defaults = {}; - if (!config.agents.defaults.heartbeat) config.agents.defaults.heartbeat = {}; - const hb = config.agents.defaults.heartbeat; + config.hooks.enabled = true; + config.hooks.token = hooksToken; + await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2)); - if (channels.length > 0) { - const primary = channels[0]; - if (hb.target !== primary.channel || hb.to !== primary.to) { - hb.target = primary.channel; - hb.to = primary.to; - changed = true; - } - } else if (hb.target !== 'last') { - // No channels discovered — fall back to 'last' and clear stale target - hb.target = 'last'; - delete hb.to; - changed = true; - } - - if (changed) { - await fs.writeFile(configPath, JSON.stringify(config, null, 2)); - console.log(chalk.gray('Hooks and heartbeat configured in openclaw.json')); - } - - // Append notification relay instructions to HEARTBEAT.md. - // The default heartbeat prompt reads this file and follows it strictly. - await fs.mkdir(WORKSPACE_DIR, { recursive: true }); - const heartbeatPath = `${WORKSPACE_DIR}/HEARTBEAT.md`; - const section = buildHeartbeatMd(channels); - let existing = ''; - try { existing = await fs.readFile(heartbeatPath, 'utf-8'); } catch { /* file may not exist */ } - // Replace existing notification section or append if not present. - // Uses split-on-header to avoid regex edge cases with anchors/newlines. - const idx = existing.indexOf(HEARTBEAT_SECTION_HEADER); - if (idx !== -1) { - let before = existing.slice(0, idx); - // Ensure a newline separates preceding content from our section - if (before.length > 0 && !before.endsWith('\n')) before += '\n'; - const afterHeader = existing.slice(idx + HEARTBEAT_SECTION_HEADER.length); - // Find next top-level heading. Assumes a preceding newline (standard markdown). - const nextHeading = afterHeader.search(/\n# /); - const after = nextHeading !== -1 ? afterHeader.slice(nextHeading) : ''; - await fs.writeFile(heartbeatPath, before + section.trimEnd() + after); - } else { - const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n\n' : existing.length > 0 ? '\n' : ''; - await fs.writeFile(heartbeatPath, existing + separator + section); - } - console.log(chalk.gray('HEARTBEAT.md updated with notification relay instructions')); - - if (channels.length > 0) { - console.log(chalk.gray(`Notification channels: ${channels.map(c => `${c.channel}:${c.to}`).join(', ')}`)); - } - - // Restart gateway to pick up new config (watchdog auto-restarts it) - if (changed) { - try { - execSync('pkill -f openclaw-gateway', { stdio: 'ignore' }); - console.log(chalk.gray('Gateway restarting to apply config...')); - } catch { - // Gateway may not be running yet — config will be picked up on next start - } - } + try { + execSync('pkill -f openclaw-gateway', { stdio: 'ignore' }); + } catch { /* gateway may not be running */ } } catch (err) { - console.log(chalk.yellow('Warning: Could not configure instance locally.')); - console.log(chalk.gray(`${err instanceof Error ? err.message : err}`)); - console.log(chalk.gray('Hooks will be configured on next instance reboot.')); + console.error(chalk.red(`Error configuring instance: ${err instanceof Error ? err.message : err}`)); } } @@ -214,11 +113,15 @@ async function enableNotifications(): Promise { console.log(chalk.gray('Enabling push notifications...')); + // Discover connected channels for delivery targeting + const channels = await discoverConnectedChannels(); + // Resolve account ID for event matching const accountId = await getAccountId(); - const body: Record = { machine_id: machineId }; + const body: Record = { machine_id: machineId }; if (accountId) body.account_id = accountId; + if (channels.length > 0) body.channels = channels; let res: Response; try { @@ -253,6 +156,9 @@ async function enableNotifications(): Promise { console.log(' ' + chalk.bold('ID:') + ' ' + (webhook.id || '')); console.log(' ' + chalk.bold('URL:') + ' ' + (webhook.url || '')); console.log(' ' + chalk.bold('Events:') + ' ' + (webhook.eventTypes?.join(', ') || '')); + if (channels.length > 0) { + console.log(' ' + chalk.bold('Channels:') + ' ' + channels.map(c => `${c.channel}:${c.to}`).join(', ')); + } if (webhook.secret) { console.log(' ' + chalk.bold('Secret:') + ' ' + chalk.yellow(webhook.secret)); console.log(); From e6535336177940da6f0ba421126997869d21ad84 Mon Sep 17 00:00:00 2001 From: R-M-Naveen Date: Fri, 6 Mar 2026 17:06:37 -0600 Subject: [PATCH 2/2] docs(atxp): update SKILL.md to reference /hooks/agent Co-Authored-By: Claude Opus 4.6 --- skills/atxp/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/atxp/SKILL.md b/skills/atxp/SKILL.md index e966330..5ac305d 100644 --- a/skills/atxp/SKILL.md +++ b/skills/atxp/SKILL.md @@ -337,7 +337,7 @@ Local contacts database for resolving names to phone numbers and emails. Stored ### Notifications -Enable push notifications so your agent receives a POST to its `/hooks/wake` endpoint when events happen (e.g., inbound email or SMS), instead of polling. +Enable push notifications so your agent receives a POST to its `/hooks/agent` endpoint when events happen (e.g., inbound email or SMS), instead of polling. | Command | Cost | Description | |---------|------|-------------|