infinicode 2.8.104 → 2.8.106

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.
@@ -26,7 +26,7 @@ export const DASHBOARD_HTML = `<!doctype html>
26
26
  <head>
27
27
  <meta charset="utf-8">
28
28
  <meta name="viewport" content="width=device-width, initial-scale=1">
29
- <meta name="robopark-ui-build" content="provisioning-token-20260717-1">
29
+ <meta name="robopark-ui-build" content="provisioning-command-20260717-2">
30
30
  <title>RoboPark Control</title>
31
31
  <style>
32
32
  /* ── premium tech glass theme ── dark is the committed default; light is a frosted toggle ── */
@@ -1264,8 +1264,20 @@ button.act:disabled{opacity:0.5;cursor:default;}
1264
1264
  var lastFedStatusAt = 0; // updated by poll() on every successful /fed/status fetch
1265
1265
  var lastStatus = null; // most recent /fed/status snapshot (shared with Park)
1266
1266
  var roboparkByName = {}; // robot_id -> latest telemetry row (shared with Park drawer)
1267
- var schedulerDevicesById = {}; // enrolled device rows + camera/audio inventory
1268
- var schedulerDevicesByName = {}; // same rows, keyed by device name (id != name, e.g. dev_8984a80e vs jaguar2)
1267
+ var schedulerDevicesById = {}; // enrolled device rows + camera/audio inventory
1268
+ var schedulerDevicesByName = {}; // same rows, keyed by device name (id != name, e.g. dev_8984a80e vs jaguar2)
1269
+ function schedulerDeviceScore(device){
1270
+ if(!device) return -1;
1271
+ var status=String(device.status||'').trim().toLowerCase();
1272
+ var heartbeat=Date.parse(device.last_heartbeat||'');
1273
+ var enrolled=Date.parse(device.enrolled_at||device.created_at||'');
1274
+ var fresh=Number.isFinite(heartbeat)&&(Date.now()-heartbeat)<45000;
1275
+ var score=fresh?1000000000000000:0;
1276
+ if(status==='online'||status==='running'||status==='detecting'||status==='connecting') score+=1000000000000;
1277
+ if(Number.isFinite(heartbeat)) score+=heartbeat;
1278
+ else if(Number.isFinite(enrolled)) score+=enrolled;
1279
+ return score;
1280
+ }
1269
1281
  var voiceStacks = []; // scheduler voice stacks (shared with setup forms + templates)
1270
1282
  var FALLBACK_SLOTS = [
1271
1283
  {label:'VOLT', id:'volt', keys:['volt'], nx:0.18, ny:0.26, img:'/static/robopark/volt.png'},
@@ -1732,17 +1744,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
1732
1744
  // A stale re-enrollment can leave more than one row with the same
1733
1745
  // display name. Keep the live/fresh device, never whichever SQLite
1734
1746
  // happened to return last, because all operator routes are id-keyed.
1735
- function deviceScore(device){
1736
- var status=norm(device&&device.status), at=Date.parse(device&&device.last_heartbeat||''), score=0;
1737
- if(status==='online'||status==='running'||status==='detecting'||status==='connecting') score+=10000;
1738
- if(Number.isFinite(at)) score+=Math.max(0, Math.floor(at/1000));
1739
- return score;
1740
- }
1741
1747
  (Array.isArray(devices)?devices:[]).forEach(function(device){
1742
1748
  deviceById[device.id] = device;
1743
1749
  if(device.name){
1744
1750
  var key=norm(device.name), current=deviceByName[key];
1745
- if(!current || deviceScore(device)>deviceScore(current)) deviceByName[key] = device;
1751
+ if(!current || schedulerDeviceScore(device)>schedulerDeviceScore(current)) deviceByName[key] = device;
1746
1752
  }
1747
1753
  });
1748
1754
  schedulerDevicesById = deviceById;
@@ -2275,8 +2281,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
2275
2281
  argv.push('--token', meshToken);
2276
2282
  if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
2277
2283
  argv.push(network==='tailscale'?'--tailscale':'--lan','--start','--auto-start','--yes');
2278
- return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
2279
- }
2284
+ return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
2285
+ }
2286
+ function buildPadRobotCommand(name,characterId,enrollmentToken){
2287
+ var env=rpCommandEnv(),hubHost=env.lan||'192.168.0.9';
2288
+ var hubUrl=/^https?:\/\//i.test(hubHost)?hubHost.replace(/\\\/+$/,''):'http://'+hubHost.replace(/\\\/+$/,'')+':47913';
2289
+ var meshToken=env.token||dashboardMeshToken()||'<mesh-token>',site=env.site||'Tel Aviv';
2290
+ var argv;
2291
+ if(enrollmentToken){
2292
+ argv=['sudo','robopark','setup','--robot','--name',name,'--site',site,'--hub-url',hubUrl,'--token',meshToken,'--scheduler-port','8080','--port','47913','--lan','--enrollment-token',enrollmentToken,'--start','--auto-start','--yes'];
2293
+ }else{
2294
+ argv=['robopark','add-robot',name];
2295
+ if(characterId)argv.push('--character',characterId);
2296
+ argv.push('--site',site,'--hub-url',hubUrl,'--token',meshToken,'--scheduler-url',hubUrl+'/robopark','--lan','--start','--auto-start','--yes');
2297
+ }
2298
+ return argv.map(function(a){return /[^a-zA-Z0-9_./:=,-]/.test(a)?'"'+String(a).replace(/"/g,'\\"')+'"':String(a);}).join(' ');
2299
+ }
2280
2300
  function refreshRobotCliCommand(){
2281
2301
  var name=$('rb-name').value.trim(); var cid=$('rb-char').value||'';
2282
2302
  var row=$('rb-clicmd-row'), inp=$('rb-clicmd');
@@ -6881,11 +6901,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
6881
6901
  livekit_url:$('pk-setup-lk').value.trim()||null,
6882
6902
  video_device:'auto', audio_device:'default'
6883
6903
  };
6884
- rpAction('POST','/robopark/api/devices',body).then(function(j){
6885
- var token=(j&&j.enrollment_token)||'';
6886
- $('pk-setup-token').value=token;
6887
- $('pk-setup-result').style.display='block';
6888
- showOut('robot added: '+esc((j&&j.name)||name)+'\\ntoken: '+esc(token));
6904
+ rpAction('POST','/robopark/api/devices',body).then(function(j){
6905
+ var token=(j&&j.enrollment_token)||'';
6906
+ $('pk-setup-token').value=token;
6907
+ $('pk-setup-result').style.display='block';
6908
+ var command=$('pk-setup-clicmd'),label=$('pk-setup-clicmd-label');
6909
+ if(command)command.value=buildPadRobotCommand(name,body.character_id||n.id,token);
6910
+ if(label)label.textContent='run this ON THE ROBOT (uses the token minted above)';
6911
+ showOut('robot added: '+esc((j&&j.name)||name)+'\\ntoken: '+esc(token));
6889
6912
  pollRoboparkQuiet();
6890
6913
  }).catch(function(e){ showOut('pad setup ('+esc(n.label)+'): '+e.message); });
6891
6914
  }
@@ -7017,14 +7040,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
7017
7040
  function schedulerDevice(n){
7018
7041
  // Deliberately does not call schedRow: telemetry can contain a legacy
7019
7042
  // robot name, while this lookup must choose the current enrolled device.
7043
+ var candidates=[];
7020
7044
  var ids=[n.schedulerDeviceId,n.schedulerRobotId,n.id,n.nodeId];
7021
- for(var i=0;i<ids.length;i++) if(ids[i]&&schedulerDevicesById[ids[i]]) return schedulerDevicesById[ids[i]];
7045
+ for(var i=0;i<ids.length;i++) if(ids[i]&&schedulerDevicesById[ids[i]]) candidates.push(schedulerDevicesById[ids[i]]);
7022
7046
  var names=[n.name,n.displayName,n.label];
7023
7047
  for(var j=0;j<names.length;j++){
7024
7048
  var key=String(names[j]||'').toLowerCase();
7025
- if(key&&schedulerDevicesByName[key]) return schedulerDevicesByName[key];
7049
+ if(key&&schedulerDevicesByName[key]) candidates.push(schedulerDevicesByName[key]);
7026
7050
  }
7027
- return null;
7051
+ candidates=candidates.filter(function(device,index,list){return device&&list.indexOf(device)===index;});
7052
+ candidates.sort(function(a,b){return schedulerDeviceScore(b)-schedulerDeviceScore(a);});
7053
+ return candidates[0]||null;
7028
7054
  }
7029
7055
  function deviceList(inv, key, fallback){
7030
7056
  var list=inv&&Array.isArray(inv[key])?inv[key]:[];
@@ -7423,7 +7449,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7423
7449
  + '<div class="rp-field"><label>Robot name</label><input id="pk-setup-name" value="'+esc(defaultName)+'"></div>'
7424
7450
  + '<div class="rp-field"><label>Character preset</label><select id="pk-setup-char"><option value="">— auto —</option>'+presetOptions+'</select></div>'
7425
7451
  + '<div class="rp-quickrow" style="padding:0;"><button class="rp-btn primary" id="pk-setup-btn" data-preset="'+esc(n.id)+'" style="width:fit-content;">Mint enrollment token &amp; add robot</button></div>'
7426
- + '<div class="rp-field"><label>or run this ON THE ROBOT (mints its own token)</label><input id="pk-setup-clicmd" readonly></div>'
7452
+ + '<div class="rp-field"><label id="pk-setup-clicmd-label">or run this ON THE ROBOT (mints its own token)</label><textarea id="pk-setup-clicmd" readonly rows="4" style="resize:vertical;font-family:var(--mono);font-size:.62rem;"></textarea></div>'
7427
7453
  + '<div class="rp-quickrow" style="padding:0;"><button class="rp-btn" id="pk-setup-clicmd-copy" style="width:fit-content;">📋 Copy command</button></div>'
7428
7454
  + '<details class="rp-advanced"><summary>Advanced — pre-set network/voice config</summary>'
7429
7455
  + '<div class="rp-advanced-grid">'
@@ -7437,10 +7463,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
7437
7463
  + '<div class="pk-sect" id="pk-setup-result" style="display:none;"><h4>Enrollment token (copy once)</h4><input id="pk-setup-token" readonly style="margin-bottom:0.4rem;"><button class="rp-btn" id="pk-setup-copy">Copy</button></div>');
7438
7464
  var setupBtn=$('pk-setup-btn'); if(setupBtn) setupBtn.addEventListener('click', function(){ submitPadSetup(n); });
7439
7465
  var copyBtn=$('pk-setup-copy'); if(copyBtn) copyBtn.addEventListener('click', function(){ var t=$('pk-setup-token'); if(t){ t.select(); try{document.execCommand('copy');}catch(e){} showOut('token copied'); } });
7440
- function refreshPadCliCommand(){
7441
- var nm=$('pk-setup-name').value.trim()||defaultName; var cid=$('pk-setup-char').value||n.id;
7442
- var el=$('pk-setup-clicmd'); if(el) el.value=buildAddRobotCommand(nm, cid);
7443
- }
7466
+ function refreshPadCliCommand(){
7467
+ var nm=$('pk-setup-name').value.trim()||defaultName; var cid=$('pk-setup-char').value||n.id;
7468
+ var el=$('pk-setup-clicmd'),label=$('pk-setup-clicmd-label');if(el)el.value=buildPadRobotCommand(nm,cid,'');if(label)label.textContent='or run this ON THE ROBOT (mints its own token)';
7469
+ }
7444
7470
  var padNameEl=$('pk-setup-name'); if(padNameEl) padNameEl.addEventListener('input', refreshPadCliCommand);
7445
7471
  var padCharEl=$('pk-setup-char'); if(padCharEl) padCharEl.addEventListener('change', refreshPadCliCommand);
7446
7472
  var padCliCopy=$('pk-setup-clicmd-copy'); if(padCliCopy) padCliCopy.addEventListener('click', function(){ var i=$('pk-setup-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied — run it on the robot itself'); });
@@ -1,6 +1,6 @@
1
1
  # RoboPark Production Memory
2
2
 
3
- Last verified: 2026-07-16
3
+ Last verified: 2026-07-17
4
4
 
5
5
  This records production facts future operators and coding agents must
6
6
  preserve. Never add enrollment tokens, mesh tokens, API keys, or device tokens.
@@ -16,6 +16,30 @@ preserve. Never add enrollment tokens, mesh tokens, API keys, or device tokens.
16
16
  - Robots publish camera and microphone tracks to LiveKit. Park camera preview
17
17
  uses the RoboVision relay rather than LiveKit video.
18
18
 
19
+ ## Production Robot Network Boundary
20
+
21
+ - All onsite production robots, including RoboPanda and BMW/VIXEN, are LAN
22
+ satellites of `livekit-1`. They are not expected to be directly reachable
23
+ from the development laptop, even when the laptop can reach `livekit-1`
24
+ through Tailscale.
25
+ - RoboPanda is reachable only through the `livekit-1` LAN mesh. A missing Panda
26
+ node in the development laptop's direct peer list does not by itself prove
27
+ Panda is offline; inspect it from `livekit-1` and through the scheduler.
28
+ - Route robot commands, spawned agents, supervisor shell requests, camera
29
+ relays, media tests, and production diagnostics through `livekit-1`. Do not
30
+ substitute direct SSH or a development-laptop LAN route.
31
+ - The control path is: operator -> Tailscale/mesh -> `livekit-1:47913` ->
32
+ scheduler proxy `/robopark` -> robot heartbeat/supervisor on the onsite LAN.
33
+ The camera path is: browser -> authenticated `livekit-1` RoboVision mesh
34
+ relay -> robot-local RoboVision service.
35
+ - `livekit-1` currently advertises LAN IPv4 `192.168.0.9`; verify it on the hub
36
+ before generating commands because DHCP may change it. Never use the
37
+ development laptop's LAN address as a robot hub address.
38
+ - When a robot reports a stale heartbeat, diagnose from `livekit-1` first:
39
+ scheduler enrollment row, hub-side mesh membership, robot heartbeat, then
40
+ robot-local runtime logs. Camera troubleshooting starts only after heartbeat
41
+ and mesh connectivity are healthy.
42
+
19
43
  ## BMW Launch Device Lock
20
44
 
21
45
  - Camera: `/dev/video0` (`H65 USB CAMERA`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.104",
3
+ "version": "2.8.106",
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",
@@ -2671,7 +2671,15 @@ async def list_devices():
2671
2671
  """List all enrolled Pi devices."""
2672
2672
  async with aiosqlite.connect(DB_PATH) as db:
2673
2673
  db.row_factory = aiosqlite.Row
2674
- async with db.execute("SELECT * FROM devices ORDER BY name") as cursor:
2674
+ async with db.execute(
2675
+ """SELECT * FROM devices
2676
+ ORDER BY lower(name),
2677
+ CASE lower(status)
2678
+ WHEN 'online' THEN 0 WHEN 'running' THEN 1
2679
+ WHEN 'detecting' THEN 2 WHEN 'connecting' THEN 3
2680
+ ELSE 4 END,
2681
+ last_heartbeat DESC, enrolled_at DESC, created_at DESC"""
2682
+ ) as cursor:
2675
2683
  return [_device_row_to_model(r) for r in await cursor.fetchall()]
2676
2684
 
2677
2685
  @app.get("/api/devices/by-room/{room_name}")
@@ -3984,7 +3992,14 @@ async def _resolve_device_by_id_or_name(robot_id: str):
3984
3992
  async with aiosqlite.connect(DB_PATH) as db:
3985
3993
  db.row_factory = aiosqlite.Row
3986
3994
  async with db.execute(
3987
- "SELECT * FROM devices WHERE id=? OR lower(name)=lower(?) ORDER BY CASE WHEN id=? THEN 0 ELSE 1 END LIMIT 1",
3995
+ """SELECT * FROM devices WHERE id=? OR lower(name)=lower(?)
3996
+ ORDER BY CASE WHEN id=? THEN 0 ELSE 1 END,
3997
+ CASE lower(status)
3998
+ WHEN 'online' THEN 0 WHEN 'running' THEN 1
3999
+ WHEN 'detecting' THEN 2 WHEN 'connecting' THEN 3
4000
+ ELSE 4 END,
4001
+ last_heartbeat DESC, enrolled_at DESC, created_at DESC
4002
+ LIMIT 1""",
3988
4003
  (robot_id, robot_id, robot_id),
3989
4004
  ) as cursor:
3990
4005
  device = await cursor.fetchone()
@@ -426,8 +426,8 @@ async def _bootstrap_mesh_device(scheduler_url: str, robot_id: str) -> tuple[str
426
426
  return device_id, device_token
427
427
 
428
428
 
429
- async def _enroll(scheduler_url: str, enrollment_token: str, robot_id: str) -> str:
430
- """Enroll this Pi with the scheduler and return the device token."""
429
+ async def _enroll(scheduler_url: str, enrollment_token: str, robot_id: str) -> tuple[str, str]:
430
+ """Enroll this Pi and return the exact scheduler identity and token."""
431
431
  import socket
432
432
  payload = {
433
433
  "enrollment_token": enrollment_token,
@@ -443,16 +443,17 @@ async def _enroll(scheduler_url: str, enrollment_token: str, robot_id: str) -> s
443
443
  )
444
444
  r.raise_for_status()
445
445
  data = r.json()
446
- device_token = data.get("device_token")
447
- if not device_token:
448
- raise RuntimeError("enrollment did not return a device_token")
446
+ device_id = str(data.get("device_id") or "").strip()
447
+ device_token = str(data.get("device_token") or "").strip()
448
+ if not device_id or not device_token:
449
+ raise RuntimeError("enrollment did not return device credentials")
449
450
  _save_token(device_token)
450
451
  cfg = _load_config()
451
- cfg["device_id"] = data.get("device_id")
452
+ cfg["device_id"] = device_id
452
453
  cfg["scheduler_url"] = data.get("scheduler_url", scheduler_url)
453
454
  _save_config(cfg)
454
- logger.info(f"enrolled as device {data.get('device_id')}")
455
- return device_token
455
+ logger.info("enrolled as device %s", device_id)
456
+ return device_id, device_token
456
457
 
457
458
 
458
459
  def _get_lan_ip() -> Optional[str]:
@@ -629,11 +630,16 @@ class PreviewAgent:
629
630
 
630
631
  async def run(self) -> None:
631
632
  enrollment_token = os.getenv("ENROLLMENT_TOKEN") or self.enrollment_token
632
- if _mesh_proxy_headers():
633
- await self._ensure_mesh_identity()
634
-
633
+ # A UI-minted token identifies a specific pre-created device row.
634
+ # Consume it before generic mesh recovery so heartbeats cannot bind to
635
+ # a stale same-name identity.
635
636
  if not self.device_token and enrollment_token:
636
- self.device_token = await _enroll(self.scheduler_url, enrollment_token, self.robot_id)
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
641
+ elif _mesh_proxy_headers():
642
+ await self._ensure_mesh_identity()
637
643
 
638
644
  if not self.device_token:
639
645
  logger.error("no DEVICE_TOKEN and no ENROLLMENT_TOKEN; cannot poll scheduler")
@@ -485,6 +485,7 @@ def _run_motor_sequence(params: dict) -> dict:
485
485
  steps = params.get("steps") or []
486
486
  started = time.monotonic()
487
487
  completed = []
488
+ gpio_log = []
488
489
  try:
489
490
  with httpx.Client(timeout=8.0) as client:
490
491
  existing = client.get(f"{base}/list-motors").json().get("motors", [])
@@ -504,8 +505,22 @@ def _run_motor_sequence(params: dict) -> dict:
504
505
  })
505
506
  response.raise_for_status()
506
507
  deadline = time.monotonic() + len(registry) * ((duration_ms + pause_ms) / 1000.0) + 3.0
508
+ last_test_action = None
507
509
  while time.monotonic() < deadline:
508
510
  status = client.get(f"{base}/status").json()
511
+ action = str(status.get("last_action") or "")
512
+ if action.startswith("test:gpio:") and action != last_test_action:
513
+ try:
514
+ observed_gpio = int(action.rsplit(":", 1)[-1])
515
+ except ValueError:
516
+ observed_gpio = -1
517
+ if observed_gpio >= 2:
518
+ gpio_log.append({
519
+ "gpio": observed_gpio,
520
+ "status": "active_observed",
521
+ "elapsed_ms": int((time.monotonic() - started) * 1000),
522
+ })
523
+ last_test_action = action
509
524
  if status.get("status") == "idle":
510
525
  if status.get("error"):
511
526
  raise RuntimeError(f"registered GPIO test failed: {status['error']}")
@@ -538,15 +553,17 @@ def _run_motor_sequence(params: dict) -> dict:
538
553
  time.sleep(0.05)
539
554
  else:
540
555
  raise TimeoutError(f"motor {motor_id} did not return to idle")
541
- completed.append({"motor_id": motor_id, "duration_ms": duration_ms})
556
+ completed.append({"motor_id": motor_id, "gpio": int(motor["gpio"]), "duration_ms": duration_ms})
542
557
  except Exception as exc:
543
558
  try:
544
559
  httpx.post(f"{base}/stop-motors", json={}, timeout=3.0)
545
560
  except Exception:
546
561
  pass
547
562
  return {"ok": False, "error": f"{type(exc).__name__}: {exc}", "completed_steps": completed,
563
+ "gpio_log": gpio_log,
548
564
  "duration_ms": int((time.monotonic() - started) * 1000), "session_id": params.get("session_id")}
549
565
  return {"ok": True, "pass": True, "sequence_id": params.get("sequence_id"), "completed_steps": completed,
566
+ "gpio_log": gpio_log,
550
567
  "duration_ms": int((time.monotonic() - started) * 1000), "motor_server_url": base,
551
568
  "session_id": params.get("session_id")}
552
569