infinicode 2.8.90 → 2.8.92

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.90",
3
+ "version": "2.8.92",
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",
@@ -455,6 +455,9 @@ class PreviewAgent:
455
455
  self._vision_session_id: Optional[str] = None
456
456
  self._vision_trigger_in_flight = False
457
457
  self._speaker_test_in_flight = False
458
+ # The USB adapter is an exclusive ALSA endpoint. Keep operator tests
459
+ # and motion cues from racing each other while LiveKit is paused.
460
+ self._speaker_operation_lock = asyncio.Lock()
458
461
  self._vision_hard_deadline: float = 0.0
459
462
  self._vision_last_activity: float = 0.0
460
463
  self.production_mode = False
@@ -896,18 +899,20 @@ class PreviewAgent:
896
899
  return
897
900
  self._speaker_test_in_flight = True
898
901
  from robot_supervisor import _speaker_roundtrip_test
899
- # LiveKit owns the USB microphone continuously. Release that owner
900
- # for an explicit hardware test, then restore the same session.
901
- restore_state = self._current_state if self._publisher else None
902
- if restore_state:
903
- await self._stop_publisher()
904
- await asyncio.sleep(1.0)
905
- try:
906
- result = await asyncio.to_thread(_speaker_roundtrip_test, request.get("params") or {})
907
- result["media_owner_paused"] = bool(restore_state)
908
- finally:
909
- if restore_state and restore_state.active:
910
- await self._start_publisher(restore_state)
902
+ async with self._speaker_operation_lock:
903
+ # LiveKit owns the USB microphone and speaker continuously.
904
+ # Pause it while holding the same lock as the motion cue so
905
+ # nothing can reopen ALSA before the explicit test starts.
906
+ restore_state = self._current_state if self._publisher else None
907
+ if restore_state:
908
+ await self._stop_publisher()
909
+ await asyncio.sleep(1.0)
910
+ try:
911
+ result = await asyncio.to_thread(_speaker_roundtrip_test, request.get("params") or {})
912
+ result["media_owner_paused"] = bool(restore_state)
913
+ finally:
914
+ if restore_state and restore_state.active:
915
+ await self._start_publisher(restore_state)
911
916
  result_response = await self._session.post(
912
917
  f"{self.scheduler_url.rstrip('/')}/api/devices/{self.device_id}/supervisor-output",
913
918
  json={"kind": "speaker_test", "service": None, "payload": result,
@@ -1066,7 +1071,8 @@ class PreviewAgent:
1066
1071
  # The cue and TTS share one physical ALSA output. Finish and release
1067
1072
  # the cue before LiveKit can deliver the greeting; background playback
1068
1073
  # races aplay and makes greeting delivery intermittently fail busy.
1069
- await asyncio.to_thread(_play_audio_effect, self.audio_playback_device, "motion")
1074
+ async with self._speaker_operation_lock:
1075
+ await asyncio.to_thread(_play_audio_effect, self.audio_playback_device, "motion")
1070
1076
  try:
1071
1077
  r = await self._session.post(
1072
1078
  f"{self.scheduler_url.rstrip('/')}/api/devices/{self.device_id}/request-session",
@@ -1251,17 +1257,14 @@ class LiveKitPublisher:
1251
1257
  # — until it finished, so a short "Hello friend!" greeting could be
1252
1258
  # over and gone before we ever got a chance to subscribe to it.
1253
1259
  if audio_enabled:
1254
- self._tasks.append(asyncio.create_task(self._audio_loop()))
1260
+ self._tasks.append(asyncio.create_task(self._audio_retry_loop()))
1255
1261
  try:
1256
1262
  await asyncio.wait_for(self._mic_streaming.wait(), timeout=5.0)
1257
- await self.agent._report_pipeline(
1258
- "microphone_published", "ok", "Microphone track published with live PCM frames"
1259
- )
1260
1263
  except asyncio.TimeoutError:
1261
1264
  await self.agent._report_pipeline(
1262
1265
  "microphone_published", "blocked", "Microphone track published but PCM capture did not start"
1263
1266
  )
1264
- raise RuntimeError(f"microphone capture did not start for {self.agent.audio_capture_device}")
1267
+ logger.warning("microphone PCM is not ready; keeping the session alive while capture retries")
1265
1268
 
1266
1269
  if self._capture and self.video_source:
1267
1270
  self._tasks.append(asyncio.create_task(self._video_loop()))
@@ -1317,6 +1320,11 @@ class LiveKitPublisher:
1317
1320
  process.wait(timeout=2)
1318
1321
  except subprocess.TimeoutExpired:
1319
1322
  process.terminate()
1323
+ try:
1324
+ process.wait(timeout=1)
1325
+ except subprocess.TimeoutExpired:
1326
+ process.kill()
1327
+ process.wait(timeout=1)
1320
1328
 
1321
1329
  def close(self) -> None:
1322
1330
  return
@@ -1493,16 +1501,24 @@ class LiveKitPublisher:
1493
1501
  else:
1494
1502
  data = b"".join(raw[i:i + 2] * OUT_CHANNELS for i in range(0, len(raw), 2))
1495
1503
  write_queue.put(data)
1496
- except Exception as e:
1497
- logger.warning(f"audio out stream error: {e}")
1498
- finally:
1499
- write_queue.put(None)
1500
- writer_thread.join(timeout=2.0)
1504
+ except Exception as e:
1505
+ logger.warning(f"audio out stream error: {e}")
1506
+ finally:
1507
+ cancelling = bool(asyncio.current_task() and asyncio.current_task().cancelling())
1508
+ if cancelling:
1509
+ # On publisher teardown, close ALSA first. Waiting for the
1510
+ # writer while aplay still owns the device leaves hw:X,Y busy
1511
+ # long enough for the queued dashboard test to fail.
1512
+ stream_failed.set()
1513
+ out.stop_stream()
1514
+ write_queue.put(None)
1515
+ writer_thread.join(timeout=2.0)
1501
1516
  logger.info(
1502
1517
  f"audio out: {sid} received {frame_count} frames, "
1503
1518
  f"writer wrote {written_frames[0]} chunks, queue backlog at close={write_queue.qsize()}"
1504
1519
  )
1505
- out.stop_stream()
1520
+ if not cancelling:
1521
+ out.stop_stream()
1506
1522
  out.close()
1507
1523
  if pa is not None:
1508
1524
  pa.terminate()
@@ -1577,7 +1593,29 @@ class LiveKitPublisher:
1577
1593
  except Exception as e:
1578
1594
  logger.debug(f"preview motion sampling failed: {e}")
1579
1595
 
1580
- async def _audio_loop(self) -> None:
1596
+ async def _audio_retry_loop(self) -> None:
1597
+ """Keep microphone capture alive across transient ALSA ownership errors."""
1598
+ while not self._stop_event.is_set():
1599
+ try:
1600
+ await self._audio_loop()
1601
+ except asyncio.CancelledError:
1602
+ raise
1603
+ except Exception as e:
1604
+ logger.warning(
1605
+ f"audio_loop: capture failed on {self.agent.audio_capture_device}: {e}; retrying"
1606
+ )
1607
+ finally:
1608
+ mic = self._mic_capture
1609
+ self._mic_capture = None
1610
+ if mic is not None:
1611
+ await asyncio.to_thread(mic.stop)
1612
+ if not self._stop_event.is_set():
1613
+ try:
1614
+ await asyncio.wait_for(self._stop_event.wait(), timeout=1.0)
1615
+ except asyncio.TimeoutError:
1616
+ pass
1617
+
1618
+ async def _audio_loop(self) -> None:
1581
1619
  assert self.audio_source is not None
1582
1620
  mic = create_audio_capture(self.agent.audio_capture_device)
1583
1621
  if mic is None:
@@ -1623,7 +1661,12 @@ class LiveKitPublisher:
1623
1661
  data=chunk, sample_rate=48000, num_channels=1, samples_per_channel=samples_per_frame,
1624
1662
  )
1625
1663
  await self.audio_source.capture_frame(frame)
1626
- self._mic_streaming.set()
1664
+ if not self._mic_streaming.is_set():
1665
+ self._mic_streaming.set()
1666
+ await self.agent._report_pipeline(
1667
+ "microphone_published", "ok",
1668
+ "Microphone track published with live PCM frames",
1669
+ )
1627
1670
  p = audioop.max(chunk, 2)
1628
1671
  _diag_peak = max(_diag_peak, p)
1629
1672
  _diag_count += 1
@@ -1833,7 +1876,7 @@ class AlsaAudioCapture(AudioCapture):
1833
1876
  "-f", "S16_LE", "-r", "48000", "-c", "1",
1834
1877
  ],
1835
1878
  stdout=subprocess.PIPE,
1836
- stderr=subprocess.DEVNULL,
1879
+ stderr=subprocess.PIPE,
1837
1880
  )
1838
1881
  if self.process.stdout is None:
1839
1882
  raise RuntimeError("arecord did not provide a PCM stream")
@@ -1846,7 +1889,10 @@ class AlsaAudioCapture(AudioCapture):
1846
1889
  while len(self.buffer) < bytes_needed:
1847
1890
  chunk = self.process.stdout.read(bytes_needed - len(self.buffer))
1848
1891
  if not chunk:
1849
- raise OSError(f"ALSA capture stopped on {self.alsa_device}")
1892
+ detail = ""
1893
+ if self.process.stderr is not None:
1894
+ detail = self.process.stderr.read().decode("utf-8", errors="replace").strip()
1895
+ raise OSError(f"ALSA capture stopped on {self.alsa_device}: {detail or 'no PCM data'}")
1850
1896
  self.buffer.extend(chunk)
1851
1897
  data = bytes(self.buffer[:bytes_needed])
1852
1898
  del self.buffer[:bytes_needed]
@@ -1864,6 +1910,7 @@ class AlsaAudioCapture(AudioCapture):
1864
1910
  self.process.wait(timeout=2)
1865
1911
  except Exception:
1866
1912
  self.process.kill()
1913
+ self.process.wait(timeout=1)
1867
1914
 
1868
1915
 
1869
1916
  class PyAudioCapture(AudioCapture):