infinicode 2.8.59 → 2.8.60

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.
@@ -73,7 +73,7 @@ Wants=network-online.target
73
73
  [Service]
74
74
  Type=simple
75
75
  ExecStart=${service.command} ${service.args.map(shellQuote).join(' ')}
76
- WorkingDirectory=${service.workingDir ?? '/opt/robopark'}
76
+ WorkingDirectory=${service.workingDir ?? homedir()}
77
77
  Restart=always
78
78
  RestartSec=5
79
79
  ${envLines}
@@ -83,7 +83,15 @@ WantedBy=multi-user.target
83
83
  `;
84
84
  try {
85
85
  writeFileSync(unitPath, unit, 'utf8');
86
- return { ok: true, message: `wrote systemd unit ${unitPath}. Run: systemctl enable --now ${unitName}` };
86
+ const reload = spawnSync('systemctl', ['daemon-reload'], { encoding: 'utf8' });
87
+ if (reload.status !== 0) {
88
+ return { ok: false, message: `wrote ${unitPath}, but systemctl daemon-reload failed: ${reload.stderr || reload.stdout}` };
89
+ }
90
+ const enabled = spawnSync('systemctl', ['enable', '--now', unitName], { encoding: 'utf8' });
91
+ if (enabled.status !== 0) {
92
+ return { ok: false, message: `wrote ${unitPath}, but systemctl enable --now failed: ${enabled.stderr || enabled.stdout}` };
93
+ }
94
+ return { ok: true, message: `enabled and started systemd unit ${unitName}` };
87
95
  }
88
96
  catch (e) {
89
97
  return { ok: false, message: `could not write ${unitPath}: ${e instanceof Error ? e.message : String(e)}` };
@@ -101,8 +101,6 @@ export async function roboparkRobotRuntime(opts) {
101
101
  '--scheduler-url', opts.schedulerUrl, '--robot-id', opts.name,
102
102
  '--video-device', opts.videoDevice ?? 'auto', '--audio-device', opts.audioDevice ?? 'default',
103
103
  '--save-config'];
104
- if (opts.vision !== false)
105
- previewArgs.push('--robovision-url', 'http://127.0.0.1:5000');
106
104
  if (opts.enrollmentToken)
107
105
  previewArgs.push('--enrollment-token', opts.enrollmentToken);
108
106
  const preview = {
@@ -113,6 +111,13 @@ export async function roboparkRobotRuntime(opts) {
113
111
  // Keep this in the child environment, never in the persisted robot config.
114
112
  env: {
115
113
  ROBOPARK_MESH_TOKEN: opts.token,
114
+ // Keep internal service wiring out of the Commander argument boundary.
115
+ // This also lets preview start while RoboVision is still warming up.
116
+ ROBOVISION_URL: opts.vision !== false ? 'http://127.0.0.1:5000' : '',
117
+ ROBOVISION_MEDIA_URL: opts.vision !== false
118
+ ? 'http://127.0.0.1:5000/api/media/inventory'
119
+ : '',
120
+ ROBOVISION_CAMERA: opts.vision !== false ? '1' : '0',
116
121
  // Inventory changes should reach the Control Center promptly.
117
122
  HEARTBEAT_INTERVAL: '5',
118
123
  },
@@ -189,7 +189,10 @@ async function setupRobot(config, opts, ctx, internal) {
189
189
  console.log(' ' + chalk.cyan(`robopark ${runtimeArgs.join(' ')}`));
190
190
  console.log(chalk.dim(` network: ${network} (scheduler: ${schedulerUrl})`));
191
191
  console.log();
192
- if (opts.start) {
192
+ // On Linux, registerAutoStart enables and starts the systemd service. Do not
193
+ // also launch a detached copy that can leave mesh healthy while preview is
194
+ // duplicated, blocked, or running under a different HOME.
195
+ if (opts.start && !(opts.autoStart && process.platform === 'linux')) {
193
196
  const robotArgv = [process.execPath, process.argv[1], ...runtimeArgs];
194
197
  console.log(chalk.dim(' starting unified robot runtime…'));
195
198
  runDetached(robotArgv[0], robotArgv.slice(1));
@@ -135,6 +135,7 @@ program
135
135
  .option('--enrollment-token <token>', 'one-time enrollment token')
136
136
  .option('--video-device <dev>', 'camera: auto, /dev/video0, 0, picamera', 'auto')
137
137
  .option('--audio-device <dev>', 'microphone: default, none, or name/index', 'default')
138
+ .option('--robovision-url <url>', 'RoboVision base URL used for camera streaming and hardware inventory')
138
139
  .option('--width <px>', 'video width', '640')
139
140
  .option('--height <px>', 'video height', '480')
140
141
  .option('--fps <n>', 'video fps', '15')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.59",
3
+ "version": "2.8.60",
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",
@@ -337,7 +337,9 @@ def _get_lan_ip() -> Optional[str]:
337
337
  return None
338
338
 
339
339
 
340
- async def _send_heartbeat(scheduler_url: str, device_id: str, token: str) -> Optional[bool]:
340
+ async def _send_heartbeat(
341
+ scheduler_url: str, device_id: str, token: str
342
+ ) -> tuple[Optional[bool], bool]:
341
343
  try:
342
344
  inventory = _get_device_inventory()
343
345
  headers = {"Authorization": f"Bearer {token}", **_mesh_proxy_headers()}
@@ -348,6 +350,13 @@ async def _send_heartbeat(scheduler_url: str, device_id: str, token: str) -> Opt
348
350
  headers=headers,
349
351
  timeout=10.0,
350
352
  )
353
+ if response.status_code in (401, 404):
354
+ logger.warning(
355
+ "heartbeat credentials rejected for %s (%s)",
356
+ device_id,
357
+ response.status_code,
358
+ )
359
+ return None, False
351
360
  response.raise_for_status()
352
361
  logger.info(
353
362
  "heartbeat accepted: inventory source=%s camera=%d mic=%d speaker=%d",
@@ -356,10 +365,10 @@ async def _send_heartbeat(scheduler_url: str, device_id: str, token: str) -> Opt
356
365
  len(inventory.get("audio_input", [])),
357
366
  len(inventory.get("audio_output", [])),
358
367
  )
359
- return bool(response.json().get("production_mode", False))
368
+ return bool(response.json().get("production_mode", False)), True
360
369
  except Exception as e:
361
370
  logger.warning(f"heartbeat failed: {e}")
362
- return None
371
+ return None, True
363
372
 
364
373
 
365
374
  @dataclass
@@ -420,18 +429,34 @@ class PreviewAgent:
420
429
  self._last_device_config_poll = 0.0
421
430
  self._motion_reference = None
422
431
  self._last_motion_sample = 0.0
423
- self._motion_capture: Optional[VideoCapture] = None
424
- self._motion_capture_lock = asyncio.Lock()
425
-
432
+ self._motion_capture: Optional[VideoCapture] = None
433
+ self._motion_capture_lock = asyncio.Lock()
434
+ self._mesh_bootstrap_ready = False
435
+ self._next_mesh_bootstrap = 0.0
436
+
437
+ async def _ensure_mesh_identity(self) -> bool:
438
+ if not _mesh_proxy_headers():
439
+ return bool(self.device_id and self.device_token)
440
+ now = time.monotonic()
441
+ if self._mesh_bootstrap_ready:
442
+ return True
443
+ if now < self._next_mesh_bootstrap:
444
+ return False
445
+ self._next_mesh_bootstrap = now + 5.0
446
+ try:
447
+ self.device_id, self.device_token = await _bootstrap_mesh_device(
448
+ self.scheduler_url, self.robot_id
449
+ )
450
+ self._mesh_bootstrap_ready = True
451
+ return True
452
+ except Exception as exc:
453
+ logger.warning("mesh device bootstrap failed: %s", exc)
454
+ return False
455
+
426
456
  async def run(self) -> None:
427
457
  enrollment_token = os.getenv("ENROLLMENT_TOKEN") or self.enrollment_token
428
458
  if _mesh_proxy_headers():
429
- try:
430
- self.device_id, self.device_token = await _bootstrap_mesh_device(
431
- self.scheduler_url, self.robot_id
432
- )
433
- except Exception as exc:
434
- logger.warning("mesh device bootstrap failed: %s", exc)
459
+ await self._ensure_mesh_identity()
435
460
 
436
461
  if not self.device_token and enrollment_token:
437
462
  self.device_token = await _enroll(self.scheduler_url, enrollment_token, self.robot_id)
@@ -747,13 +772,18 @@ class PreviewAgent:
747
772
  await asyncio.to_thread(_play_audio_effect, self.audio_output_device, "disconnect")
748
773
  self._current_state = PreviewState()
749
774
 
750
- async def _heartbeat_loop(self) -> None:
751
- while not self._shutdown.is_set():
752
- if self.device_id and self.device_token:
753
- production_mode = await _send_heartbeat(
754
- self.scheduler_url, self.device_id, self.device_token
755
- )
756
- if production_mode is not None:
775
+ async def _heartbeat_loop(self) -> None:
776
+ while not self._shutdown.is_set():
777
+ if _mesh_proxy_headers() and not self._mesh_bootstrap_ready:
778
+ await self._ensure_mesh_identity()
779
+ if self.device_id and self.device_token:
780
+ production_mode, credentials_ok = await _send_heartbeat(
781
+ self.scheduler_url, self.device_id, self.device_token
782
+ )
783
+ if not credentials_ok:
784
+ self._mesh_bootstrap_ready = False
785
+ self._next_mesh_bootstrap = 0.0
786
+ if production_mode is not None:
757
787
  self.production_mode = production_mode
758
788
  if not production_mode and self._vision_session_id:
759
789
  await self._end_vision_session()
@@ -1674,7 +1704,9 @@ def main() -> None:
1674
1704
  "video_device": args.video_device,
1675
1705
  "audio_device": args.audio_device,
1676
1706
  "robovision_url": args.robovision_url or os.getenv("ROBOVISION_URL", "http://127.0.0.1:5000"),
1677
- "use_robovision_camera": bool(args.robovision_url),
1707
+ "use_robovision_camera": bool(args.robovision_url) or os.getenv(
1708
+ "ROBOVISION_CAMERA", ""
1709
+ ).lower() in ("1", "true", "yes", "on"),
1678
1710
  "video_width": args.width,
1679
1711
  "video_height": args.height,
1680
1712
  "video_fps": args.fps,