infinicode 2.8.84 → 2.8.85

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.84",
3
+ "version": "2.8.85",
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",
@@ -839,10 +839,70 @@ async def broadcast(event_type: str, data: dict):
839
839
  ws_clients.remove(client)
840
840
 
841
841
  # =============================================================================
842
- # ROBOT ENDPOINTS
843
- # =============================================================================
844
-
845
- @app.get("/api/robots", response_model=List[Robot])
842
+ # ROBOT ENDPOINTS
843
+ # =============================================================================
844
+
845
+ CONNECTING_SESSION_LEASE_SECONDS = max(
846
+ 15, int(os.getenv("ROBOPARK_CONNECTING_SESSION_LEASE_SECONDS", "45"))
847
+ )
848
+ ACTIVE_SESSION_LEASE_SECONDS = max(
849
+ CONNECTING_SESSION_LEASE_SECONDS,
850
+ int(os.getenv("ROBOPARK_ACTIVE_SESSION_LEASE_SECONDS", "180")),
851
+ )
852
+
853
+
854
+ async def _reap_robot_session_if_stale(db, robot_id: str, current_session_id: Optional[str],
855
+ robot_status: Optional[str] = None) -> Optional[str]:
856
+ """Release an invalid or expired session pointer without operator action."""
857
+ if not current_session_id:
858
+ return None
859
+
860
+ async with db.execute(
861
+ "SELECT id, started_at, last_activity_at, ended_at FROM sessions WHERE id = ?",
862
+ (current_session_id,),
863
+ ) as cursor:
864
+ session = await cursor.fetchone()
865
+
866
+ reason = None
867
+ if not session:
868
+ reason = "missing_session"
869
+ elif session["ended_at"]:
870
+ reason = "ended_session"
871
+ else:
872
+ timestamp = session["last_activity_at"] or session["started_at"]
873
+ try:
874
+ activity_age = (datetime.utcnow() - datetime.fromisoformat(timestamp)).total_seconds()
875
+ except (TypeError, ValueError):
876
+ activity_age = ACTIVE_SESSION_LEASE_SECONDS + 1
877
+ lease = (
878
+ CONNECTING_SESSION_LEASE_SECONDS
879
+ if robot_status == "connecting"
880
+ else ACTIVE_SESSION_LEASE_SECONDS
881
+ )
882
+ if activity_age >= lease:
883
+ reason = "stale_connecting" if robot_status == "connecting" else "stale_activity"
884
+
885
+ if not reason:
886
+ return None
887
+
888
+ now = datetime.utcnow().isoformat()
889
+ if session and not session["ended_at"]:
890
+ await db.execute(
891
+ "UPDATE sessions SET ended_at = ?, end_reason = ? WHERE id = ? AND ended_at IS NULL",
892
+ (now, reason, current_session_id),
893
+ )
894
+ await db.execute(
895
+ "UPDATE robots SET status = 'idle', current_session_id = NULL, connected_server_id = NULL "
896
+ "WHERE id = ? AND current_session_id = ?",
897
+ (robot_id, current_session_id),
898
+ )
899
+ logger.warning(
900
+ "Automatically released %s session %s for robot %s",
901
+ reason, current_session_id, robot_id,
902
+ )
903
+ return reason
904
+
905
+ @app.get("/api/robots", response_model=List[Robot])
846
906
  async def list_robots():
847
907
  """List all robots"""
848
908
  async with aiosqlite.connect(DB_PATH) as db:
@@ -909,11 +969,23 @@ async def request_session(robot_id: str,
909
969
  (robot_id, device_row["name"]),
910
970
  )
911
971
  await db.commit()
912
- robot = {"id": robot_id, "name": device_row["name"], "status": "idle"}
913
- else:
914
- raise HTTPException(404, "Robot not found")
915
- if robot["status"] != "idle":
916
- raise HTTPException(400, f"Robot is {robot['status']}, not idle")
972
+ robot = {
973
+ "id": robot_id,
974
+ "name": device_row["name"],
975
+ "status": "idle",
976
+ "current_session_id": None,
977
+ }
978
+ else:
979
+ raise HTTPException(404, "Robot not found")
980
+ stale_reason = await _reap_robot_session_if_stale(
981
+ db, robot_id, robot["current_session_id"], robot["status"]
982
+ )
983
+ if stale_reason:
984
+ await db.commit()
985
+ async with db.execute("SELECT * FROM robots WHERE id = ?", (robot_id,)) as cursor:
986
+ robot = await cursor.fetchone()
987
+ if robot["current_session_id"] or robot["status"] != "idle":
988
+ raise HTTPException(409, f"Robot is {robot['status']}, not idle")
917
989
 
918
990
  # Find least-loaded online server
919
991
  async with db.execute("""
@@ -1046,7 +1118,7 @@ async def simulate_trigger(robot_id: str):
1046
1118
  async with aiosqlite.connect(DB_PATH) as db:
1047
1119
  db.row_factory = aiosqlite.Row
1048
1120
  async with db.execute(
1049
- "SELECT id, current_session_id FROM robots WHERE id = ?", (robot_id,)
1121
+ "SELECT id, current_session_id, status FROM robots WHERE id = ?", (robot_id,)
1050
1122
  ) as c:
1051
1123
  robot = await c.fetchone()
1052
1124
  if not robot:
@@ -1059,11 +1131,14 @@ async def simulate_trigger(robot_id: str):
1059
1131
  (device["id"], device["name"], device["character_id"]),
1060
1132
  )
1061
1133
  await db.commit()
1062
- robot = {"id": device["id"], "current_session_id": None}
1134
+ robot = {"id": device["id"], "current_session_id": None, "status": "idle"}
1063
1135
  if not await _effective_device_production_mode(db, robot_id):
1064
1136
  return {"queued": False, "reason": "device_production_mode_off"}
1065
- if robot["current_session_id"]:
1066
- return {"queued": False, "reason": "session_active"}
1137
+ stale_reason = await _reap_robot_session_if_stale(
1138
+ db, robot_id, robot["current_session_id"], robot["status"]
1139
+ )
1140
+ if robot["current_session_id"] and not stale_reason:
1141
+ return {"queued": False, "reason": "session_active"}
1067
1142
  await db.execute(
1068
1143
  "INSERT INTO trigger_commands (robot_id, source, requested_at) VALUES (?, ?, ?) "
1069
1144
  "ON CONFLICT(robot_id) DO UPDATE SET source = excluded.source, requested_at = excluded.requested_at",
@@ -1072,7 +1147,12 @@ async def simulate_trigger(robot_id: str):
1072
1147
  await db.commit()
1073
1148
  await broadcast("robot_triggered", {"robot_id": robot_id, "source": "dashboard"})
1074
1149
  await _log_history("robot", "robot", robot_id, "simulate_trigger", "operator")
1075
- return {"queued": True, "source": "dashboard"}
1150
+ return {
1151
+ "queued": True,
1152
+ "source": "dashboard",
1153
+ "recovered_stale_session": bool(stale_reason),
1154
+ "recovery_reason": stale_reason,
1155
+ }
1076
1156
 
1077
1157
  @app.post("/api/robots/{robot_id}/simulate-stop")
1078
1158
  async def simulate_stop(robot_id: str):
@@ -1487,14 +1567,31 @@ async def session_joined(session_id: str,
1487
1567
  session = await c.fetchone()
1488
1568
  if not session:
1489
1569
  raise HTTPException(404, "Session not found")
1490
- if session["joined_at"]:
1491
- return {
1492
- "status": "already_joined",
1493
- "joined_at": session["joined_at"],
1494
- "latency_ms": _session_latency_ms(session["started_at"], session["joined_at"]),
1495
- }
1496
- await db.execute("UPDATE sessions SET joined_at = ? WHERE id = ?", (now, session_id))
1497
- await db.commit()
1570
+ if session["joined_at"]:
1571
+ if not session["ended_at"]:
1572
+ await db.execute(
1573
+ "UPDATE sessions SET last_activity_at = ? WHERE id = ?",
1574
+ (now, session_id),
1575
+ )
1576
+ await db.execute(
1577
+ "UPDATE robots SET status = 'running' WHERE id = ? AND current_session_id = ?",
1578
+ (session["robot_id"], session_id),
1579
+ )
1580
+ await db.commit()
1581
+ return {
1582
+ "status": "already_joined",
1583
+ "joined_at": session["joined_at"],
1584
+ "latency_ms": _session_latency_ms(session["started_at"], session["joined_at"]),
1585
+ }
1586
+ await db.execute(
1587
+ "UPDATE sessions SET joined_at = ?, last_activity_at = ? WHERE id = ?",
1588
+ (now, now, session_id),
1589
+ )
1590
+ await db.execute(
1591
+ "UPDATE robots SET status = 'running' WHERE id = ? AND current_session_id = ?",
1592
+ (session["robot_id"], session_id),
1593
+ )
1594
+ await db.commit()
1498
1595
  latency_ms = _session_latency_ms(session["started_at"], now)
1499
1596
  await broadcast("session_joined", {"session_id": session_id, "latency_ms": latency_ms})
1500
1597
  await _log_history("session", "session", session_id, "joined", "robot", f"latency_ms={latency_ms}")
@@ -1955,19 +2052,38 @@ async def health_checker():
1955
2052
  await db.execute("UPDATE devices SET status = 'offline' WHERE id = ?", (did,))
1956
2053
  await broadcast("device_status_changed", {"device_id": did, "status": "offline"})
1957
2054
 
1958
- # Reap sessions leaked by a crashed worker or broken LiveKit
1959
- # disconnect. Normal sessions are closed by the worker, but
1960
- # this prevents stale rooms consuming server capacity forever.
1961
- session_cutoff = (datetime.utcnow() - timedelta(seconds=120)).isoformat()
1962
- async with db.execute(
1963
- "SELECT id, robot_id FROM sessions "
1964
- "WHERE ended_at IS NULL AND COALESCE(last_activity_at, started_at) < ?",
1965
- (session_cutoff,),
1966
- ) as c:
2055
+ # Release stale robot session pointers with the same lease rules
2056
+ # used by the trigger path. Failed joins cannot require manual
2057
+ # operator recovery or block an unattended robot indefinitely.
2058
+ async with db.execute(
2059
+ "SELECT id, status, current_session_id FROM robots WHERE current_session_id IS NOT NULL"
2060
+ ) as c:
2061
+ session_owners = await c.fetchall()
2062
+ for owner in session_owners:
2063
+ reason = await _reap_robot_session_if_stale(
2064
+ db, owner["id"], owner["current_session_id"], owner["status"]
2065
+ )
2066
+ if reason:
2067
+ await broadcast("session_expired", {
2068
+ "session_id": owner["current_session_id"],
2069
+ "robot_id": owner["id"],
2070
+ "reason": reason,
2071
+ })
2072
+
2073
+ # Reap old orphan rows that are no longer owned by a robot.
2074
+ session_cutoff = (
2075
+ datetime.utcnow() - timedelta(seconds=ACTIVE_SESSION_LEASE_SECONDS)
2076
+ ).isoformat()
2077
+ async with db.execute(
2078
+ "SELECT id, robot_id FROM sessions "
2079
+ "WHERE ended_at IS NULL AND COALESCE(last_activity_at, started_at) < ? "
2080
+ "AND NOT EXISTS (SELECT 1 FROM robots r WHERE r.current_session_id = sessions.id)",
2081
+ (session_cutoff,),
2082
+ ) as c:
1967
2083
  stale_sessions = await c.fetchall()
1968
2084
  for session in stale_sessions:
1969
2085
  await db.execute(
1970
- "UPDATE sessions SET ended_at = ?, end_reason = 'silence' "
2086
+ "UPDATE sessions SET ended_at = ?, end_reason = 'stale_orphan' "
1971
2087
  "WHERE id = ? AND ended_at IS NULL",
1972
2088
  (datetime.utcnow().isoformat(), session["id"]),
1973
2089
  )
@@ -1979,7 +2095,7 @@ async def health_checker():
1979
2095
  await broadcast("session_expired", {
1980
2096
  "session_id": session["id"],
1981
2097
  "robot_id": session["robot_id"],
1982
- "reason": "silence",
2098
+ "reason": "stale_orphan",
1983
2099
  })
1984
2100
 
1985
2101
  await db.commit()
@@ -4748,10 +4864,10 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
4748
4864
  readiness_code, readiness_message = "server_unavailable", f"LiveKit server {robot['connected_server_id']} is {server_status or 'missing'}"
4749
4865
  elif not robot["current_session_id"]:
4750
4866
  readiness_code, readiness_message = "ready", "ready for motion trigger"
4751
- elif robot["status"] == "connecting" and session_age is not None and session_age > 30:
4752
- readiness_code, readiness_message = "stuck_session", f"joining LiveKit for {session_age}s; recover robot"
4753
- elif activity_age is not None and activity_age > 180:
4754
- readiness_code, readiness_message = "stale_session", f"no session activity for {activity_age}s; recover robot"
4867
+ elif robot["status"] == "connecting" and session_age is not None and session_age > CONNECTING_SESSION_LEASE_SECONDS:
4868
+ readiness_code, readiness_message = "stuck_session", f"joining LiveKit for {session_age}s; automatic recovery pending"
4869
+ elif activity_age is not None and activity_age > ACTIVE_SESSION_LEASE_SECONDS:
4870
+ readiness_code, readiness_message = "stale_session", f"no session activity for {activity_age}s; automatic recovery pending"
4755
4871
  else:
4756
4872
  readiness_code, readiness_message = "in_session", "conversation session is active"
4757
4873