infinicode 2.8.106 → 2.8.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -825,6 +825,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
825
825
  <div class="section"><h3>Live insights</h3><div class="pk-camera-insight" id="pk-camera-ops-insights"></div></div>
826
826
  <div class="section"><h3>Conversation pipeline</h3><div class="pk-camera-pipeline" id="pk-camera-ops-pipeline"></div></div>
827
827
  <div class="section"><h3>Hardware and media</h3><div class="pk-camera-insight" id="pk-camera-ops-hardware"></div></div>
828
+ <div class="section"><h3>Motor operations</h3><div class="rp-field"><label>Saved sequence</label><select id="pk-camera-ops-motor-sequence"></select></div><div class="pk-ctl"><button class="rp-btn" id="pk-camera-ops-motor-discover">Discover relays</button><button class="rp-btn primary" id="pk-camera-ops-motor-run">Run sequence</button><button class="rp-btn danger" id="pk-camera-ops-motor-test">Test registered GPIO</button></div><div class="pk-motor-status" id="pk-camera-ops-motor-status">Loading robot motor profile...</div></div>
828
829
  <div class="section"><h3>Operator note</h3><div class="pk-note">Video is relayed directly from the robot's RoboVision MJPEG service through the authenticated mesh. Voice remains on LiveKit, avoiding camera ownership conflicts.</div></div>
829
830
  </aside>
830
831
  </div>
@@ -1278,6 +1279,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
1278
1279
  else if(Number.isFinite(enrolled)) score+=enrolled;
1279
1280
  return score;
1280
1281
  }
1282
+ function schedulerHeartbeatFresh(device){
1283
+ var heartbeat=Date.parse(device&&device.last_heartbeat||'');
1284
+ return Number.isFinite(heartbeat)&&(Date.now()-heartbeat)<45000;
1285
+ }
1281
1286
  var voiceStacks = []; // scheduler voice stacks (shared with setup forms + templates)
1282
1287
  var FALLBACK_SLOTS = [
1283
1288
  {label:'VOLT', id:'volt', keys:['volt'], nx:0.18, ny:0.26, img:'/static/robopark/volt.png'},
@@ -6829,7 +6834,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
6829
6834
  }
6830
6835
  $('pk-name').textContent=n.label||n.name;
6831
6836
  $('pk-sub').textContent = n.connected ? (n.name+' · '+(n.platform||'?')+' · v'+n.version+(n.site?(' · '+n.site):'')) : 'pad reserved · not joined';
6832
- var st=$('pk-state'); st.textContent=n.connected?'live':'offline'; st.className='pk-pill '+(n.connected?'live':'idle');
6837
+ var initialDevice=schedulerDevice(n),initialHeartbeat=schedulerHeartbeatFresh(initialDevice);
6838
+ var st=$('pk-state'); st.textContent=initialHeartbeat?'live':(n.connected?'mesh only':'offline'); st.className='pk-pill '+(initialHeartbeat?'live':'idle');
6833
6839
  buildDrawer(n);
6834
6840
  pollRoboparkQuiet(); // fill session telemetry right away instead of waiting for the tick
6835
6841
  if(drawerTimer) clearInterval(drawerTimer);
@@ -7635,10 +7641,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
7635
7641
  }
7636
7642
  function motorEditorAddMotor(){captureMotorEditor();var number=motorProfile.registry.length+1;motorProfile.registry.push({id:'motor-'+number,name:'Motor '+number,gpio:Math.min(27,Math.max(2,number+4)),active_high:true,max_duration_ms:3000});renderMotorEditor();}
7637
7643
  function motorEditorAddStep(){captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register a motor before adding a timed node','bad');return;}motorEditorSequence().steps.push({motor_id:motorProfile.registry[0].id,delay_ms:0,duration_ms:300});renderMotorEditor();}
7638
- function motorStatus(message,kind){var host=$('pk-motor-status');if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}}
7644
+ function motorStatus(message,kind){['pk-motor-status','pk-camera-ops-motor-status'].forEach(function(id){var host=$(id);if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}});}
7645
+ function motorErrorMessage(error){var message=error&&error.message?error.message:String(error||'unknown error');return /(?:\s|^)404(?:\s|$)|not found/i.test(message)?'Motor API is not loaded on the running livekit-1 scheduler. Update and restart the scheduler, then retry.':message;}
7639
7646
  function loadMotorProfile(n){
7640
7647
  var robotId=schedId(n);motorStatus('Loading saved motor profile...');
7641
- getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
7648
+ getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+motorErrorMessage(error),'bad');});
7642
7649
  }
7643
7650
  function saveMotorProfile(n){
7644
7651
  captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
@@ -7665,7 +7672,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7665
7672
  }
7666
7673
  function testAllMotorRelays(n){
7667
7674
  captureMotorEditor();var ready=motorProfile.registry.length?Promise.resolve():discoverMotorRelays(n,true);
7668
- ready.then(function(){if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return null;var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');return saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');});}).catch(function(error){motorStatus('GPIO test failed: '+error.message,'bad');});
7675
+ ready.then(function(){if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return null;var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');return saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');});}).catch(function(error){motorStatus('GPIO test failed: '+motorErrorMessage(error),'bad');});
7669
7676
  }
7670
7677
 
7671
7678
  var pipelineHistoryPage=1;
@@ -7834,7 +7841,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
7834
7841
  set('pk-tv-lat', row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'—');
7835
7842
  var dEl=$('pk-tv-drop'); if(dEl){ var d=row.drop_rate!=null?Math.round(row.drop_rate*100):null; dEl.textContent=d!=null?d+'%':'—'; dEl.className='v '+(d>=20?'bad':d>=5?'warn':'ok'); }
7836
7843
  var active=row.active_session||row.in_session||row.session_active||row.current_session_id||row.status==='connecting'||row.status==='running';
7837
- var st=$('pk-state'); if(st){ st.textContent=active?'in session':(n.connected?'live':'idle'); st.className='pk-pill '+(active?'live':(n.connected?'live':'idle')); }
7844
+ var heartbeatLive=schedulerHeartbeatFresh(schedulerDevice(n));
7845
+ var st=$('pk-state'); if(st){ st.textContent=active?'in session':(heartbeatLive?'live':(n.connected?'mesh only':'offline')); st.className='pk-pill '+(active||heartbeatLive?'live':'idle'); }
7838
7846
  }
7839
7847
  }
7840
7848
  function set(id,v){ var e=$(id); if(e) e.textContent=v; }
@@ -7856,20 +7864,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
7856
7864
  var stages=[[currentStageLabel,true,'ACTIVE'],['Robot online',!!n.connected,'ready'],['Camera streaming',cameraLive,'ready'],['Motion detector armed',!!device.production_mode,'armed'],['Scheduler session',active,'active']],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s,index){return '<div class="'+(index===0?'fleet-stage-focus '+esc(currentStageStatus):'')+'"><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?s[2]:'waiting')+'</b></div>';}).join('');
7857
7865
  var inv=device.device_inventory||{},mh=inv.media_health||{},hardware=$('pk-camera-ops-hardware');if(hardware)hardware.innerHTML='<span>CPU</span><b>'+(hw.cpu&&hw.cpu.percent!=null?Math.round(hw.cpu.percent)+'%':'--')+'</b><span>Memory</span><b>'+(hw.memory&&hw.memory.percent!=null?Math.round(hw.memory.percent)+'%':'--')+'</b><span>Temperature</span><b>'+(hw.temperatureC!=null?Math.round(hw.temperatureC)+'C':'--')+'</b><span>Camera inputs</span><b>'+((inv.video||[]).length||0)+'</b><span>Microphones</span><b>'+((inv.audio_input||[]).length||0)+'</b><span>Speaker outputs</span><b>'+((inv.audio_output||[]).length||0)+'</b><span>Camera permission</span><b class="'+(mh.camera_access===false?'bad':'ok')+'">'+(mh.camera_access===false?'DENIED':mh.camera_access===true?'granted':'--')+'</b><span>Audio permission</span><b class="'+(mh.audio_access===false?'bad':'ok')+'">'+(mh.audio_access===false?'DENIED':mh.audio_access===true?'granted':'--')+'</b><span>Camera worker</span><b class="'+(mh.camera_stalled?'bad':'ok')+'">'+(mh.camera_stalled?'STALLED':mh.camera_worker?'streaming':'--')+'</b><span>Frame age</span><b>'+(mh.camera_frame_age_seconds!=null?Number(mh.camera_frame_age_seconds).toFixed(1)+'s':'--')+'</b><span>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
7858
7866
  var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
7867
+ var motorSelect=$('pk-camera-ops-motor-sequence');if(motorSelect){var selected=motorSelect.value||motorSequenceId;motorSelect.innerHTML=(motorProfile.sequences||[]).map(function(sequence){return '<option value="'+esc(sequence.id)+'">'+esc(sequence.name||sequence.id)+'</option>';}).join('')||'<option value="">No saved sequences</option>';if(selected)motorSelect.value=selected;}
7859
7868
  }
7860
7869
  function reloadCameraOperations(){
7861
7870
  if(!cameraOpsNode)return;var img=$('pk-camera-ops-img'),empty=$('pk-camera-ops-empty');if(!img)return;img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='opening RoboVision camera...';}
7862
7871
  img.onload=function(){img.dataset.live='true';if(cameraOpsRetryTimer){clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=null;}if(empty)empty.style.display='none';refreshCameraOperations();};img.onerror=function(){var failedNode=cameraOpsNode;img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='camera relay unavailable / retrying';}refreshCameraOperations();if(cameraOpsRetryTimer)clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=setTimeout(function(){if(cameraOpsNode&&cameraOpsNode===failedNode)reloadCameraOperations();},3000);};img.src=cameraOpsFeedUrl(cameraOpsNode);refreshCameraOperations();
7863
7872
  }
7864
7873
  function openCameraOperations(n){
7865
- cameraOpsNode=n;var host=$('pk-camera-ops');if(!host)return;host.classList.add('open');document.body.classList.add('camera-ops-open');var name=n.displayName||n.name||'robot',avatar=rpAvatarFor(name),av=$('pk-camera-ops-avatar');$('pk-camera-ops-name').textContent=name+' / live operations';$('pk-camera-ops-sub').textContent='RoboVision mesh relay / '+(n.connected?'connected':'offline');if(av){if(avatar){av.src=avatar+QS;av.style.display='block';}else{av.removeAttribute('src');av.style.display='none';}}reloadCameraOperations();if(cameraOpsTimer)clearInterval(cameraOpsTimer);cameraOpsTimer=setInterval(refreshCameraOperations,1000);
7874
+ cameraOpsNode=n;var host=$('pk-camera-ops');if(!host)return;host.classList.add('open');document.body.classList.add('camera-ops-open');var name=n.displayName||n.name||'robot',avatar=rpAvatarFor(name),av=$('pk-camera-ops-avatar');$('pk-camera-ops-name').textContent=name+' / live operations';$('pk-camera-ops-sub').textContent='RoboVision mesh relay / '+(n.connected?'connected':'offline');if(av){if(avatar){av.src=avatar+QS;av.style.display='block';}else{av.removeAttribute('src');av.style.display='none';}}loadMotorProfile(n);reloadCameraOperations();if(cameraOpsTimer)clearInterval(cameraOpsTimer);cameraOpsTimer=setInterval(refreshCameraOperations,1000);
7866
7875
  }
7867
7876
  function closeCameraOperations(){
7868
7877
  var host=$('pk-camera-ops'),img=$('pk-camera-ops-img');if(host)host.classList.remove('open');document.body.classList.remove('camera-ops-open');if(cameraOpsRetryTimer){clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=null;}if(img){img.src='';img.dataset.live='false';}if(cameraOpsTimer){clearInterval(cameraOpsTimer);cameraOpsTimer=null;}cameraOpsNode=null;if(document.fullscreenElement&&document.exitFullscreen)document.exitFullscreen().catch(function(){});
7869
7878
  }
7870
7879
  (function bindCameraOperations(){
7871
- var close=$('pk-camera-ops-close'),refresh=$('pk-camera-ops-refresh'),nativeBtn=$('pk-camera-ops-native'),trigger=$('pk-camera-ops-trigger'),prod=$('pk-camera-ops-production'),end=$('pk-camera-ops-end');
7880
+ var close=$('pk-camera-ops-close'),refresh=$('pk-camera-ops-refresh'),nativeBtn=$('pk-camera-ops-native'),trigger=$('pk-camera-ops-trigger'),prod=$('pk-camera-ops-production'),end=$('pk-camera-ops-end'),motorDiscover=$('pk-camera-ops-motor-discover'),motorRun=$('pk-camera-ops-motor-run'),motorTest=$('pk-camera-ops-motor-test'),motorSelect=$('pk-camera-ops-motor-sequence');
7872
7881
  if(close)close.addEventListener('click',closeCameraOperations);if(refresh)refresh.addEventListener('click',reloadCameraOperations);if(nativeBtn)nativeBtn.addEventListener('click',function(){var host=$('pk-camera-ops');if(host&&host.requestFullscreen)host.requestFullscreen().catch(function(e){showOut('fullscreen: '+e.message);});});if(trigger)trigger.addEventListener('click',function(){if(cameraOpsNode)triggerSession(schedId(cameraOpsNode));});if(prod)prod.addEventListener('click',function(){if(cameraOpsNode){var d=schedulerDevice(cameraOpsNode)||{};setProductionMode(cameraOpsNode,!d.production_mode);setTimeout(refreshCameraOperations,400);}});if(end)end.addEventListener('click',function(){if(cameraOpsNode)endSession(schedId(cameraOpsNode));});document.addEventListener('keydown',function(e){if(e.key==='Escape'&&cameraOpsNode)closeCameraOperations();});
7882
+ if(motorDiscover)motorDiscover.addEventListener('click',function(){if(cameraOpsNode)discoverMotorRelays(cameraOpsNode,false).catch(function(error){motorStatus('Discovery failed: '+error.message,'bad');});});if(motorRun)motorRun.addEventListener('click',function(){if(cameraOpsNode)runMotorSequence(cameraOpsNode);});if(motorTest)motorTest.addEventListener('click',function(){if(cameraOpsNode)testAllMotorRelays(cameraOpsNode);});if(motorSelect)motorSelect.addEventListener('change',function(){motorSequenceId=motorSelect.value;renderMotorEditor();refreshCameraOperations();});
7873
7883
  })();
7874
7884
 
7875
7885
  // ── cam: target a LiveKit track; fall back to a styled preview ──
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { spawn } from 'node:child_process';
11
11
  import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
- import { homedir } from 'node:os';
12
+ import { homedir, networkInterfaces } from 'node:os';
13
13
  import { dirname, join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import chalk from 'chalk';
@@ -18,6 +18,122 @@ const VISION_URL = 'http://127.0.0.1:5000/api/media/inventory';
18
18
  const VISION_STATUS_URL = 'http://127.0.0.1:5000/api/camera/status';
19
19
  const RESTART_DELAY_MS = 5_000;
20
20
  const MEDIA_WATCHDOG_INTERVAL_MS = 10_000;
21
+ const SCHEDULER_HEARTBEAT_INTERVAL_MS = 5_000;
22
+ function currentLanIp() {
23
+ for (const addresses of Object.values(networkInterfaces())) {
24
+ for (const address of addresses ?? []) {
25
+ if (address.family === 'IPv4' && !address.internal)
26
+ return address.address;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+ function readSchedulerIdentity() {
32
+ const configDir = join(homedir(), '.robopark');
33
+ try {
34
+ const config = JSON.parse(readFileSync(join(configDir, 'preview_agent.json'), 'utf8'));
35
+ const deviceId = String(config.device_id ?? '').trim();
36
+ const fileToken = existsSync(join(configDir, 'device_token'))
37
+ ? readFileSync(join(configDir, 'device_token'), 'utf8').trim()
38
+ : '';
39
+ const deviceToken = fileToken || String(config.device_token ?? '').trim();
40
+ if (deviceId && deviceToken)
41
+ return { deviceId, deviceToken };
42
+ }
43
+ catch {
44
+ // First boot has no identity yet.
45
+ }
46
+ return undefined;
47
+ }
48
+ function persistSchedulerIdentity(identity, schedulerUrl) {
49
+ const configDir = join(homedir(), '.robopark');
50
+ const configPath = join(configDir, 'preview_agent.json');
51
+ mkdirSync(configDir, { recursive: true });
52
+ let config = {};
53
+ try {
54
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
55
+ }
56
+ catch { /* first boot */ }
57
+ config.device_id = identity.deviceId;
58
+ config.device_token = identity.deviceToken;
59
+ config.scheduler_url = schedulerUrl;
60
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
61
+ writeFileSync(join(configDir, 'device_token'), identity.deviceToken, { encoding: 'utf8', mode: 0o600 });
62
+ }
63
+ function startSchedulerHeartbeat(opts, livekitUrl) {
64
+ let identity = readSchedulerIdentity();
65
+ let stopped = false;
66
+ let inFlight = false;
67
+ const bootstrap = async () => {
68
+ const response = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/devices/bootstrap`, {
69
+ method: 'POST',
70
+ headers: {
71
+ 'content-type': 'application/json',
72
+ 'x-robopark-mesh-token': opts.token,
73
+ },
74
+ body: JSON.stringify({ name: opts.name, lan_ip: currentLanIp(), livekit_url: livekitUrl }),
75
+ signal: AbortSignal.timeout(10_000),
76
+ });
77
+ if (!response.ok)
78
+ throw new Error(`bootstrap HTTP ${response.status}`);
79
+ const data = await response.json();
80
+ const next = {
81
+ deviceId: String(data.device_id ?? '').trim(),
82
+ deviceToken: String(data.device_token ?? '').trim(),
83
+ };
84
+ if (!next.deviceId || !next.deviceToken)
85
+ throw new Error('bootstrap returned no device credentials');
86
+ persistSchedulerIdentity(next, opts.schedulerUrl);
87
+ return next;
88
+ };
89
+ const beat = async () => {
90
+ if (stopped || inFlight)
91
+ return;
92
+ inFlight = true;
93
+ try {
94
+ identity = readSchedulerIdentity() ?? identity;
95
+ if (!identity)
96
+ identity = await bootstrap();
97
+ let response = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/devices/${encodeURIComponent(identity.deviceId)}/heartbeat`, {
98
+ method: 'POST',
99
+ headers: {
100
+ authorization: `Bearer ${identity.deviceToken}`,
101
+ 'content-type': 'application/json',
102
+ 'x-robopark-mesh-token': opts.token,
103
+ },
104
+ body: JSON.stringify({ status: 'online', ip: currentLanIp(), livekit_url: livekitUrl }),
105
+ signal: AbortSignal.timeout(10_000),
106
+ });
107
+ if (response.status === 401 || response.status === 404) {
108
+ identity = await bootstrap();
109
+ response = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/devices/${encodeURIComponent(identity.deviceId)}/heartbeat`, {
110
+ method: 'POST',
111
+ headers: {
112
+ authorization: `Bearer ${identity.deviceToken}`,
113
+ 'content-type': 'application/json',
114
+ 'x-robopark-mesh-token': opts.token,
115
+ },
116
+ body: JSON.stringify({ status: 'online', ip: currentLanIp(), livekit_url: livekitUrl }),
117
+ signal: AbortSignal.timeout(10_000),
118
+ });
119
+ }
120
+ if (!response.ok)
121
+ throw new Error(`heartbeat HTTP ${response.status}`);
122
+ }
123
+ catch (error) {
124
+ console.error(chalk.yellow(` scheduler heartbeat watchdog: ${error instanceof Error ? error.message : String(error)}`));
125
+ }
126
+ finally {
127
+ inFlight = false;
128
+ }
129
+ };
130
+ void beat();
131
+ const timer = setInterval(() => { void beat(); }, SCHEDULER_HEARTBEAT_INTERVAL_MS);
132
+ return () => {
133
+ stopped = true;
134
+ clearInterval(timer);
135
+ };
136
+ }
21
137
  function resetStaleEnrollment() {
22
138
  const configDir = join(homedir(), '.robopark');
23
139
  const tokenPath = join(configDir, 'device_token');
@@ -288,6 +404,7 @@ export async function roboparkRobotRuntime(opts) {
288
404
  }
289
405
  // Identity and inventory reporting start immediately. RoboVision may still
290
406
  // be installing or recovering without making the robot disappear.
407
+ const stopSchedulerHeartbeat = startSchedulerHeartbeat(opts, livekitUrl);
291
408
  start(children[0]);
292
409
  start(preview);
293
410
  if (opts.vision !== false) {
@@ -303,6 +420,7 @@ export async function roboparkRobotRuntime(opts) {
303
420
  const mediaWatchdog = setInterval(() => { void monitorVision(); }, MEDIA_WATCHDOG_INTERVAL_MS);
304
421
  const shutdown = () => {
305
422
  stopping = true;
423
+ stopSchedulerHeartbeat();
306
424
  clearInterval(mediaWatchdog);
307
425
  for (const entry of [...children, preview])
308
426
  signalChild(entry, 'SIGTERM');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.106",
3
+ "version": "2.8.108",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",
@@ -634,10 +634,20 @@ class PreviewAgent:
634
634
  # Consume it before generic mesh recovery so heartbeats cannot bind to
635
635
  # a stale same-name identity.
636
636
  if not self.device_token and enrollment_token:
637
- self.device_id, self.device_token = await _enroll(
638
- self.scheduler_url, enrollment_token, self.robot_id
639
- )
640
- self._mesh_bootstrap_ready = True
637
+ try:
638
+ self.device_id, self.device_token = await _enroll(
639
+ self.scheduler_url, enrollment_token, self.robot_id
640
+ )
641
+ self._mesh_bootstrap_ready = True
642
+ except Exception as exc:
643
+ if not _mesh_proxy_headers():
644
+ raise
645
+ # Enrollment tokens are intentionally one-time. A service
646
+ # reinstall can retain the original systemd argument after
647
+ # its credential file was removed; recover through the hub's
648
+ # authenticated mesh path rather than crash-loop forever.
649
+ logger.warning("device enrollment failed; recovering through mesh bootstrap: %s", exc)
650
+ await self._ensure_mesh_identity()
641
651
  elif _mesh_proxy_headers():
642
652
  await self._ensure_mesh_identity()
643
653