@@ -6,8 +6,6 @@ import { execSync } from 'child_process';
66const NOTIFICATIONS_BASE_URL = 'https://clowdbot-notifications.corp.circuitandchisel.com' ;
77const OPENCLAW_CONFIG_PATH = '/data/.openclaw/openclaw.json' ;
88const 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
1311const 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 */
9361async 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 ( ) ;
0 commit comments