infinicode 2.8.45 → 2.8.46

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.45",
3
+ "version": "2.8.46",
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",
@@ -4230,13 +4230,24 @@ async def dashboard():
4230
4230
  const buildDeviceInventory = () => {
4231
4231
  const map = {};
4232
4232
  for (const robot of robots.value) {
4233
- const device = devices.value.find(d => d.name === robot.name || d.id === robot.id);
4233
+ const robotName = String(robot.name || '').toLowerCase();
4234
+ const device = devices.value.find(d => String(d.name || '').toLowerCase() === robotName || d.id === robot.id);
4234
4235
  const savedVideo = device?.video_device || 'auto';
4235
4236
  const savedAudio = device?.audio_device || 'default';
4236
4237
  const hardware = device?.device_inventory || {};
4237
- const videoOptions = (hardware.video || []).map(x => typeof x === 'string' ? x : x.id).filter(Boolean);
4238
- const audioOptions = (hardware.audio_input || []).map(x => typeof x === 'string' ? x : x.id).filter(Boolean);
4239
- const audioOutputOptions = (hardware.audio_output || []).map(x => typeof x === 'string' ? x : x.id).filter(Boolean);
4238
+ const videoOptions = (hardware.video || []).filter(Boolean);
4239
+ const audioOptions = (hardware.audio_input || []).filter(Boolean);
4240
+ const audioOutputOptions = (hardware.audio_output || []).filter(Boolean);
4241
+ const optionId = x => typeof x === 'string' ? x : x.id;
4242
+ const optionName = x => typeof x === 'string' ? x : (x.name || x.id);
4243
+ const uniqueOptions = (items, defaults) => {
4244
+ const seen = new Set();
4245
+ return defaults.concat(items).filter(x => {
4246
+ const id = optionId(x);
4247
+ if (!id || seen.has(id)) return false;
4248
+ seen.add(id); return true;
4249
+ });
4250
+ };
4240
4251
  map[robot.id] = {
4241
4252
  deviceId: device?.id || null,
4242
4253
  robotName: robot.name,
@@ -4244,9 +4255,10 @@ async def dashboard():
4244
4255
  selectedAudio: savedAudio,
4245
4256
  selectedAudioOutput: device?.audio_output_device || 'default',
4246
4257
  greetingText: (device?.greeting_phrases || []).join('\\n'),
4247
- videoOptions: [...new Set(['auto', 'none', savedVideo, ...videoOptions])],
4248
- audioOptions: [...new Set(['default', 'none', savedAudio, ...audioOptions])],
4249
- audioOutputOptions: [...new Set(['default', 'none', device?.audio_output_device || 'default', ...audioOutputOptions])],
4258
+ videoOptions: uniqueOptions(videoOptions, ['auto', 'none', savedVideo]),
4259
+ audioOptions: uniqueOptions(audioOptions, ['default', 'none', savedAudio]),
4260
+ audioOutputOptions: uniqueOptions(audioOutputOptions, ['default', 'none', device?.audio_output_device || 'default']),
4261
+ optionId, optionName,
4250
4262
  };
4251
4263
  }
4252
4264
  deviceInventory.value = map;
@@ -4366,7 +4378,7 @@ async def dashboard():
4366
4378
  const inv = deviceInventory.value[robotId];
4367
4379
  if (!inv || !inv.deviceId) return;
4368
4380
  try {
4369
- await fetch(`/api/devices/${inv.deviceId}`, {
4381
+ const response = await fetch(`/api/devices/${inv.deviceId}`, {
4370
4382
  method: 'PATCH',
4371
4383
  headers: {'Content-Type': 'application/json'},
4372
4384
  body: JSON.stringify({
@@ -4416,6 +4428,31 @@ async def dashboard():
4416
4428
  } catch (e) { console.warn('preview stop failed', e); }
4417
4429
  }
4418
4430
  };
4431
+
4432
+ const testSpeaker = async (robot) => {
4433
+ const inv = deviceInventory.value[robot.id];
4434
+ const deviceId = inv?.deviceId || robot.id;
4435
+ try {
4436
+ const response = await fetch(`/api/robots/${deviceId}/shell/speaker-test`, {
4437
+ method: 'POST', headers: {'Content-Type': 'application/json'},
4438
+ body: JSON.stringify({
4439
+ output: inv?.selectedAudioOutput || 'default',
4440
+ input: inv?.selectedAudio || 'default',
4441
+ duration: 0.6,
4442
+ }),
4443
+ });
4444
+ const queued = await response.json();
4445
+ if (!response.ok) throw new Error(queued.detail || `HTTP ${response.status}`);
4446
+ const result = await fetch(`/api/robots/${deviceId}/shell/wait/${queued.request_id}?timeout=12`).then(r => r.json());
4447
+ if (!result.completed) throw new Error('Robot did not answer. Check its heartbeat and supervisor.');
4448
+ const detail = result.result || {};
4449
+ alert(detail.ok
4450
+ ? `Audio test passed\\nOutput: ${detail.output_device || 'default'}\\nInput: ${detail.input_device || 'default'}\\nPeak: ${detail.peak_db ?? 'n/a'} dB`
4451
+ : `Audio test failed: ${detail.error || 'no microphone signal'}`);
4452
+ } catch (e) {
4453
+ alert('Audio test failed: ' + e.message);
4454
+ }
4455
+ };
4419
4456
 
4420
4457
  const simulateTrigger = async (robot) => {
4421
4458
  if (!settings.value.production_mode || simulation.value.busy) return;
@@ -4766,14 +4803,14 @@ async def dashboard():
4766
4803
  return {
4767
4804
  robots, servers, sessions, stats, connected, serverMetrics,
4768
4805
  devices, settings, livekit, showAddDevice, showDevices, showCommands, commandNetwork, commandHubLan, commandHubTailscale, commandRobotId, selectedCommandRobot, commandRows, copyCommand, newDevice, rotatedToken, enrollmentNetwork, enrollmentCommand, simulation,
4769
- showLiveKitConfig, livekitForm, preview, deviceInventory,
4806
+ showLiveKitConfig, livekitForm, preview, deviceInventory,
4770
4807
  voiceStacks, characterPresets, showVoiceStackForm, showCharacterForm,
4771
4808
  editingVoiceStack, editingCharacter, voiceStackForm, characterForm,
4772
4809
  loadModel, unloadModel, statusColor, formatDuration, formatTime,
4773
4810
  toggleProductionMode, rotateEnrollmentToken, provisionAgentToken,
4774
4811
  submitNewDevice, deleteDevice, rotateDeviceToken, dismissRotatedToken,
4775
4812
  joinConversation, saveLiveKitConfig, openLiveKitConfig,
4776
- startPreview, stopPreview, simulateTrigger, stopConversation, recoverRobot, toggleDeviceProduction, setDeviceProduction, saveDeviceGreetings, updateDeviceMedia,
4813
+ startPreview, stopPreview, simulateTrigger, stopConversation, recoverRobot, toggleDeviceProduction, setDeviceProduction, saveDeviceGreetings, updateDeviceMedia, testSpeaker,
4777
4814
  openVoiceStackForm, saveVoiceStack, deleteVoiceStack,
4778
4815
  openCharacterForm, saveCharacter, deleteCharacter,
4779
4816
  assignRobotCharacter, stackName, characterName,
@@ -5118,7 +5155,7 @@ async def dashboard():
5118
5155
  <select v-model="deviceInventory[robot.id].selectedVideo"
5119
5156
  @change="updateDeviceMedia(robot.id)"
5120
5157
  class="w-full bg-gray-800 rounded px-2 py-1 text-xs">
5121
- <option v-for="opt in deviceInventory[robot.id].videoOptions" :key="opt" :value="opt">{{ opt }}</option>
5158
+ <option v-for="opt in deviceInventory[robot.id].videoOptions" :key="deviceInventory[robot.id].optionId(opt)" :value="deviceInventory[robot.id].optionId(opt)">{{ deviceInventory[robot.id].optionName(opt) }}</option>
5122
5159
  </select>
5123
5160
  </div>
5124
5161
  <div>
@@ -5126,7 +5163,7 @@ async def dashboard():
5126
5163
  <select v-model="deviceInventory[robot.id].selectedAudio"
5127
5164
  @change="updateDeviceMedia(robot.id)"
5128
5165
  class="w-full bg-gray-800 rounded px-2 py-1 text-xs">
5129
- <option v-for="opt in deviceInventory[robot.id].audioOptions" :key="opt" :value="opt">{{ opt }}</option>
5166
+ <option v-for="opt in deviceInventory[robot.id].audioOptions" :key="deviceInventory[robot.id].optionId(opt)" :value="deviceInventory[robot.id].optionId(opt)">{{ deviceInventory[robot.id].optionName(opt) }}</option>
5130
5167
  </select>
5131
5168
  </div>
5132
5169
  <div>
@@ -5134,7 +5171,7 @@ async def dashboard():
5134
5171
  <select v-model="deviceInventory[robot.id].selectedAudioOutput"
5135
5172
  @change="updateDeviceMedia(robot.id)"
5136
5173
  class="w-full bg-gray-800 rounded px-2 py-1 text-xs">
5137
- <option v-for="opt in deviceInventory[robot.id].audioOutputOptions" :key="opt" :value="opt">{{ opt }}</option>
5174
+ <option v-for="opt in deviceInventory[robot.id].audioOutputOptions" :key="deviceInventory[robot.id].optionId(opt)" :value="deviceInventory[robot.id].optionId(opt)">{{ deviceInventory[robot.id].optionName(opt) }}</option>
5138
5175
  </select>
5139
5176
  </div>
5140
5177
  </div>
@@ -5143,12 +5180,16 @@ async def dashboard():
5143
5180
  <textarea v-model="deviceInventory[robot.id].greetingText" rows="2" class="w-full bg-gray-800 rounded px-2 py-1 text-xs" placeholder="Hello! Want to go for a ride?"></textarea>
5144
5181
  <button @click="saveDeviceGreetings(robot.id)" class="mt-1 px-2 py-1 rounded bg-gray-600 hover:bg-gray-500 text-xs">Save Greetings</button>
5145
5182
  </div>
5146
- <button @click="startPreview(robot)"
5183
+ <button @click="startPreview(robot)"
5147
5184
  :disabled="!settings.production_mode || !livekit.url || !livekit.has_secret || preview.active"
5148
5185
  :class="(settings.production_mode && livekit.url && livekit.has_secret && !preview.active) ? 'bg-cyan-600 hover:bg-cyan-700' : 'bg-gray-700 cursor-not-allowed'"
5149
5186
  class="w-full px-2 py-1 rounded text-xs">
5150
- {{ preview.active && preview.robot?.id === robot.id ? 'Preview Active' : 'Start Preview' }}
5151
- </button>
5187
+ {{ preview.active && preview.robot?.id === robot.id ? 'Preview Active' : 'Start Preview' }}
5188
+ </button>
5189
+ <button @click="testSpeaker(robot)"
5190
+ class="w-full px-2 py-1 rounded text-xs bg-emerald-700 hover:bg-emerald-600">
5191
+ Test Speaker + Mic
5192
+ </button>
5152
5193
  <div class="grid grid-cols-2 gap-2">
5153
5194
  <button @click="simulateTrigger(robot)"
5154
5195
  :disabled="!settings.production_mode || simulation.busy || robot.status === 'connecting' || robot.status === 'running'"
@@ -66,7 +66,9 @@ import httpx
66
66
 
67
67
  logger = logging.getLogger("robopark.preview_agent")
68
68
 
69
- _DEVICE_INVENTORY_CACHE: Optional[dict] = None
69
+ _DEVICE_INVENTORY_CACHE: Optional[dict] = None
70
+ _DEVICE_INVENTORY_CACHE_AT = 0.0
71
+ DEVICE_INVENTORY_CACHE_SECONDS = 30.0
70
72
 
71
73
 
72
74
  def _normalize_livekit_url(url: Optional[str]) -> Optional[str]:
@@ -131,11 +133,13 @@ def _play_audio_effect(selected_output: str | None, effect: str) -> None:
131
133
  pa.terminate()
132
134
 
133
135
 
134
- def _get_device_inventory() -> dict:
135
- """Return discoverable camera and audio devices for dashboard selection."""
136
- global _DEVICE_INVENTORY_CACHE
137
- if _DEVICE_INVENTORY_CACHE is not None:
138
- return _DEVICE_INVENTORY_CACHE
136
+ def _get_device_inventory() -> dict:
137
+ """Return discoverable camera and audio devices for dashboard selection."""
138
+ global _DEVICE_INVENTORY_CACHE, _DEVICE_INVENTORY_CACHE_AT
139
+ now = time.monotonic()
140
+ if (_DEVICE_INVENTORY_CACHE is not None
141
+ and now - _DEVICE_INVENTORY_CACHE_AT < DEVICE_INVENTORY_CACHE_SECONDS):
142
+ return _DEVICE_INVENTORY_CACHE
139
143
 
140
144
  inventory = {"video": [], "audio_input": [], "audio_output": [], "platform": sys.platform}
141
145
  inventory["video"].append({"id": "auto", "name": "Auto detect"})
@@ -182,7 +186,8 @@ def _get_device_inventory() -> dict:
182
186
  except Exception as e:
183
187
  logger.debug(f"audio inventory unavailable: {e}")
184
188
 
185
- _DEVICE_INVENTORY_CACHE = inventory
189
+ _DEVICE_INVENTORY_CACHE = inventory
190
+ _DEVICE_INVENTORY_CACHE_AT = now
186
191
  logger.info(
187
192
  f"device inventory: {len(inventory['video']) - 2} cameras, "
188
193
  f"{len(inventory['audio_input']) - 1} inputs, "