Skip to content

Commit d0bea67

Browse files
authored
Merge pull request #58 from atxp-dev/naveen/agent-push-notifications
feat(atxp): switch to /hooks/agent, remove HEARTBEAT.md logic
2 parents b3f8421 + e653533 commit d0bea67

File tree

2 files changed

+20
-114
lines changed

2 files changed

+20
-114
lines changed

packages/atxp/src/commands/notifications.ts

Lines changed: 19 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { execSync } from 'child_process';
66
const NOTIFICATIONS_BASE_URL = 'https://clowdbot-notifications.corp.circuitandchisel.com';
77
const OPENCLAW_CONFIG_PATH = '/data/.openclaw/openclaw.json';
88
const SESSIONS_PATH = '/data/.openclaw/agents/main/sessions/sessions.json';
9-
const WORKSPACE_DIR = '/data/.openclaw/workspace';
10-
const HEARTBEAT_SECTION_HEADER = '# ATXP Notification Relay';
119

1210
// eslint-disable-next-line no-control-regex
1311
const sanitizeSessionValue = (s: string) => s.replace(/[\x00-\x1f"`[\]]/g, '');
@@ -56,128 +54,29 @@ async function discoverConnectedChannels(): Promise<NotificationChannel[]> {
5654
}
5755

5856
/**
59-
* Build the HEARTBEAT.md content with notification relay instructions.
60-
* The default heartbeat prompt says "Read HEARTBEAT.md if it exists. Follow it strictly."
61-
* so writing these instructions directly to the file ensures the LLM relays
62-
* system events to all discovered channels.
63-
*/
64-
function buildHeartbeatMd(channels: NotificationChannel[]): string {
65-
const lines = [
66-
HEARTBEAT_SECTION_HEADER,
67-
'',
68-
'When you see System: event lines about new emails or SMS messages,',
69-
'you MUST immediately relay each one to the user.',
70-
'Do NOT reply HEARTBEAT_OK when System events are present.',
71-
'',
72-
];
73-
74-
if (channels.length > 0) {
75-
lines.push('Relay to ALL of these channels:');
76-
for (const c of channels) {
77-
lines.push(`- Use the message tool with channel=\`${c.channel}\` and target=\`${c.to}\``);
78-
}
79-
lines.push('');
80-
}
81-
82-
return lines.join('\n');
83-
}
84-
85-
/**
86-
* Configure hooks, heartbeat delivery target, and HEARTBEAT.md on the instance.
57+
* Configure hooks token on the instance.
8758
* Only runs when inside a Fly instance (FLY_MACHINE_ID is set).
88-
*
89-
* Discovers all connected messaging channels from the session store, writes
90-
* HEARTBEAT.md with relay instructions for each channel, and sets the primary
91-
* delivery target to the first discovered channel.
59+
* Sets hooks.enabled and hooks.token in openclaw.json, restarts gateway if changed.
9260
*/
9361
async function configureHooksOnInstance(hooksToken: string): Promise<void> {
9462
if (!process.env.FLY_MACHINE_ID) return;
9563

96-
const configPath = OPENCLAW_CONFIG_PATH;
9764
try {
98-
const raw = await fs.readFile(configPath, 'utf-8');
65+
const raw = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf-8');
9966
const config = JSON.parse(raw);
10067

101-
// Discover connected channels from session store
102-
const channels = await discoverConnectedChannels();
103-
104-
let changed = false;
105-
106-
// Configure hooks
10768
if (!config.hooks) config.hooks = {};
108-
if (config.hooks.token !== hooksToken || config.hooks.enabled !== true) {
109-
config.hooks.enabled = true;
110-
config.hooks.token = hooksToken;
111-
changed = true;
112-
}
69+
if (config.hooks.token === hooksToken && config.hooks.enabled === true) return;
11370

114-
// Set primary delivery target to first discovered channel
115-
if (!config.agents) config.agents = {};
116-
if (!config.agents.defaults) config.agents.defaults = {};
117-
if (!config.agents.defaults.heartbeat) config.agents.defaults.heartbeat = {};
118-
const hb = config.agents.defaults.heartbeat;
71+
config.hooks.enabled = true;
72+
config.hooks.token = hooksToken;
73+
await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2));
11974

120-
if (channels.length > 0) {
121-
const primary = channels[0];
122-
if (hb.target !== primary.channel || hb.to !== primary.to) {
123-
hb.target = primary.channel;
124-
hb.to = primary.to;
125-
changed = true;
126-
}
127-
} else if (hb.target !== 'last') {
128-
// No channels discovered — fall back to 'last' and clear stale target
129-
hb.target = 'last';
130-
delete hb.to;
131-
changed = true;
132-
}
133-
134-
if (changed) {
135-
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
136-
console.log(chalk.gray('Hooks and heartbeat configured in openclaw.json'));
137-
}
138-
139-
// Append notification relay instructions to HEARTBEAT.md.
140-
// The default heartbeat prompt reads this file and follows it strictly.
141-
await fs.mkdir(WORKSPACE_DIR, { recursive: true });
142-
const heartbeatPath = `${WORKSPACE_DIR}/HEARTBEAT.md`;
143-
const section = buildHeartbeatMd(channels);
144-
let existing = '';
145-
try { existing = await fs.readFile(heartbeatPath, 'utf-8'); } catch { /* file may not exist */ }
146-
// Replace existing notification section or append if not present.
147-
// Uses split-on-header to avoid regex edge cases with anchors/newlines.
148-
const idx = existing.indexOf(HEARTBEAT_SECTION_HEADER);
149-
if (idx !== -1) {
150-
let before = existing.slice(0, idx);
151-
// Ensure a newline separates preceding content from our section
152-
if (before.length > 0 && !before.endsWith('\n')) before += '\n';
153-
const afterHeader = existing.slice(idx + HEARTBEAT_SECTION_HEADER.length);
154-
// Find next top-level heading. Assumes a preceding newline (standard markdown).
155-
const nextHeading = afterHeader.search(/\n# /);
156-
const after = nextHeading !== -1 ? afterHeader.slice(nextHeading) : '';
157-
await fs.writeFile(heartbeatPath, before + section.trimEnd() + after);
158-
} else {
159-
const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n\n' : existing.length > 0 ? '\n' : '';
160-
await fs.writeFile(heartbeatPath, existing + separator + section);
161-
}
162-
console.log(chalk.gray('HEARTBEAT.md updated with notification relay instructions'));
163-
164-
if (channels.length > 0) {
165-
console.log(chalk.gray(`Notification channels: ${channels.map(c => `${c.channel}:${c.to}`).join(', ')}`));
166-
}
167-
168-
// Restart gateway to pick up new config (watchdog auto-restarts it)
169-
if (changed) {
170-
try {
171-
execSync('pkill -f openclaw-gateway', { stdio: 'ignore' });
172-
console.log(chalk.gray('Gateway restarting to apply config...'));
173-
} catch {
174-
// Gateway may not be running yet — config will be picked up on next start
175-
}
176-
}
75+
try {
76+
execSync('pkill -f openclaw-gateway', { stdio: 'ignore' });
77+
} catch { /* gateway may not be running */ }
17778
} catch (err) {
178-
console.log(chalk.yellow('Warning: Could not configure instance locally.'));
179-
console.log(chalk.gray(`${err instanceof Error ? err.message : err}`));
180-
console.log(chalk.gray('Hooks will be configured on next instance reboot.'));
79+
console.error(chalk.red(`Error configuring instance: ${err instanceof Error ? err.message : err}`));
18180
}
18281
}
18382

@@ -214,11 +113,15 @@ async function enableNotifications(): Promise<void> {
214113

215114
console.log(chalk.gray('Enabling push notifications...'));
216115

116+
// Discover connected channels for delivery targeting
117+
const channels = await discoverConnectedChannels();
118+
217119
// Resolve account ID for event matching
218120
const accountId = await getAccountId();
219121

220-
const body: Record<string, string> = { machine_id: machineId };
122+
const body: Record<string, unknown> = { machine_id: machineId };
221123
if (accountId) body.account_id = accountId;
124+
if (channels.length > 0) body.channels = channels;
222125

223126
let res: Response;
224127
try {
@@ -253,6 +156,9 @@ async function enableNotifications(): Promise<void> {
253156
console.log(' ' + chalk.bold('ID:') + ' ' + (webhook.id || ''));
254157
console.log(' ' + chalk.bold('URL:') + ' ' + (webhook.url || ''));
255158
console.log(' ' + chalk.bold('Events:') + ' ' + (webhook.eventTypes?.join(', ') || ''));
159+
if (channels.length > 0) {
160+
console.log(' ' + chalk.bold('Channels:') + ' ' + channels.map(c => `${c.channel}:${c.to}`).join(', '));
161+
}
256162
if (webhook.secret) {
257163
console.log(' ' + chalk.bold('Secret:') + ' ' + chalk.yellow(webhook.secret));
258164
console.log();

skills/atxp/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ Local contacts database for resolving names to phone numbers and emails. Stored
337337

338338
### Notifications
339339

340-
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.
340+
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.
341341

342342
| Command | Cost | Description |
343343
|---------|------|-------------|

0 commit comments

Comments
 (0)